##// END OF EJS Templates
release: Merge default into stable for release preparation
marcink -
r3497:d3230053 merge stable
parent child Browse files
Show More
@@ -0,0 +1,42 b''
1 .. _restore-deleted-repositories:
2
3 Restoring Deleted Repositories
4 ==============================
5
6 By default when repository or whole repository group is deleted an archived copy
7 of filesystem repositories are kept. You can see them as special entries in the
8 repository storage such as::
9
10 drwxrwxr-x 3 rcdev rcdev 4096 Dec 4 2017 rm__20171204_105727_400795__ce-import
11 drwxrwxr-x 6 rcdev rcdev 4096 Nov 21 2017 rm__20180221_152430_675047__svn-repo
12 drwxr-xr-x 7 rcdev rcdev 4096 Mar 28 2018 rm__20180328_143124_617576__test-git
13 drwxr-xr-x 7 rcdev rcdev 4096 Mar 28 2018 rm__20180328_144954_317729__test-git-install-hooks
14
15
16 Data from those repositories can be restored by simply removing the
17 `rm_YYYYDDMM_HHMMSS_DDDDDD__` prefix and additionally only in case of Mercurial
18 repositories remove the `.hg` store prefix.::
19
20 rm__.hg => .hg
21
22
23 For Git or SVN repositories this operation is not required.
24
25 After removing the prefix repository can be brought by opening
26 :menuselection:`Admin --> Settings --> Remap and Rescan` and running `Rescan Filesystem`
27
28 This will create a new DB entry restoring the data previously removed.
29 To restore OLD database entries this should be done by restoring from a Database backup.
30
31 RhodeCode also keeps the repository group structure, this is marked by entries that
32 in addition have GROUP in the prefix, eg::
33
34 drwxr-xr-x 2 rcdev rcdev 4096 Jan 18 16:13 rm__20181130_120650_977082_GROUP_Test1
35 drwxr-xr-x 2 rcdev rcdev 4096 Jan 18 16:13 rm__20181130_120659_922952_GROUP_Docs
36
37
38
39 .. note::
40
41 RhodeCode Tools have a special cleanup tool for the archived repositories. Please
42 see :ref:`clean-up-cmds`
@@ -0,0 +1,37 b''
1 .. _store-methods-ref:
2
3 store methods
4 =============
5
6 file_store_add (EE only)
7 ------------------------
8
9 .. py:function:: file_store_add(apiuser, filename, content)
10
11 Upload API for the file_store
12
13 Example usage from CLI::
14 rhodecode-api --instance-name=enterprise-1 upload_file "{"content": "$(cat image.jpg | base64)", "filename":"image.jpg"}"
15
16 This command takes the following options:
17
18 :param apiuser: This is filled automatically from the |authtoken|.
19 :type apiuser: AuthUser
20 :param filename: name of the file uploaded
21 :type filename: str
22 :param content: base64 encoded content of the uploaded file
23 :type content: str
24
25 Example output:
26
27 .. code-block:: bash
28
29 id : <id_given_in_input>
30 result: {
31 "access_path": "/_file_store/download/84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg",
32 "access_path_fqn": "http://server.domain.com/_file_store/download/84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg",
33 "store_fid": "84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg"
34 }
35 error : null
36
37
@@ -0,0 +1,88 b''
1 .. _auth-saml-bulk-enroll-users-ref:
2
3
4 Bulk enroll multiple existing users
5 -----------------------------------
6
7
8 RhodeCode Supports standard SAML 2.0 SSO for the web-application part.
9 Below is an example how to enroll list of all or some users to use SAML authentication.
10 This method simply enables SAML authentication for many users at once.
11
12
13 From the server RhodeCode Enterprise is running run ishell on the instance which we
14 want to apply the SAML migration::
15
16 rccontrol ishell enterprise-1
17
18 Follow these steps to enable SAML authentication for multiple users.
19
20
21 1) Create a user_id => attribute mapping
22
23
24 `saml2user` is a mapping of external ID from SAML provider such as OneLogin, DuoSecurity, Google.
25 This mapping consists of local rhodecode user_id mapped to set of required attributes needed to bind SAML
26 account to internal rhodecode user.
27 For example, 123 is local rhodecode user_id, and '48253211' is OneLogin ID.
28 For other providers you'd have to figure out what would be the user-id, sometimes it's the email, i.e for Google
29 The most important this id needs to be unique for each user.
30
31 .. code-block:: python
32
33 In [1]: saml2user = {
34 ...: # OneLogin, uses externalID available to read from in the UI
35 ...: 123: {'id: '48253211'},
36 ...: # for Google/DuoSecurity email is also an option for unique ID
37 ...: 124: {'id: 'email@domain.com'},
38 ...: }
39
40
41 2) Import the plugin you want to run migration for.
42
43 From available options pick only one and run the `import` statement
44
45 .. code-block:: python
46
47 # for Duo Security
48 In [2]: from rc_auth_plugins.auth_duo_security import RhodeCodeAuthPlugin
49 # for OneLogin
50 In [2]: from rc_auth_plugins.auth_onelogin import RhodeCodeAuthPlugin
51 # generic SAML plugin
52 In [2]: from rc_auth_plugins.auth_saml import RhodeCodeAuthPlugin
53
54 3) Run the migration based on saml2user mapping.
55
56 Enter in the ishell prompt
57
58 .. code-block:: python
59
60 In [3]: for user in User.get_all():
61 ...: existing_identity = ExternalIdentity().query().filter(ExternalIdentity.local_user_id == user.user_id).scalar()
62 ...: attrs = saml2user.get(user.user_id)
63 ...: provider = RhodeCodeAuthPlugin.uid
64 ...: if existing_identity:
65 ...: print('Identity for user `{}` already exists, skipping'.format(user.username))
66 ...: continue
67 ...: if attrs:
68 ...: external_id = attrs['id']
69 ...: new_external_identity = ExternalIdentity()
70 ...: new_external_identity.external_id = external_id
71 ...: new_external_identity.external_username = '{}-saml-{}'.format(user.username, user.user_id)
72 ...: new_external_identity.provider_name = provider
73 ...: new_external_identity.local_user_id = user_id
74 ...: new_external_identity.access_token = ''
75 ...: new_external_identity.token_secret = ''
76 ...: new_external_identity.alt_token = ''
77 ...: Session().add(ex_identity)
78 ...: Session().commit()
79 ...: print('Set user `{}` external identity bound to ExternalID:{}'.format(user.username, external_id))
80
81 .. note::
82
83 saml2user can be really big and hard to maintain in ishell. It's also possible
84 to load it as a JSON file prepared before and stored on disk. To do so run::
85
86 import json
87 saml2user = json.loads(open('/path/to/saml2user.json','rb').read())
88
@@ -0,0 +1,148 b''
1 |RCE| 4.16.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2019-02-15
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14 - Full-text search: added support for ElasticSearch 6.X (ES6)
15 - Full-text search: Expose a quick way to search within repository groups using ES6.
16 - Full-text search: Add quick links to broaden/narrow search scope to repositories or
17 repository groups from global search.
18 - Full-text search: ES6 backend adds new highlighter, and search markers for better UX when searching.
19 - Full-text search: ES6 backend has enabled advanced `query string syntax`
20 adding more search and filtering capabilities.
21 - Full-text search: ES6 engine will now show added information where available such as line numbers file size.
22 - Files: added option to use highlight marker to show keywords inside file source. This
23 is used now for ES6 backend extended highlighting capabilities
24 - Artifacts (beta): EE edition exposes new feature called storage_api this allows storing
25 binary files outside of Version Control System, but in the scope of a repository or group.
26 This will soon become an Artifacts functionality available in EE edition.
27 - Authentication: introduced `User restriction` and `Scope restriction` for RhodeCode authentication plugins.
28 Admins can limit usage of RhodeCode plugins to super-admins user types, and usage in Web, or VCS protocol only.
29 This is mostly to help to migrate users to SAML, keeping the super-admins to manage instances via local-logins,
30 and secondly to force usage of AuthenticationTokens instead of re-using same credentials for
31 WEB and VCS authentication.
32 - API: added basic upload API for the storage_api. It's possible to store files using internal
33 API. This is a start for attachments upload in RhodeCode.
34 - API: added store_exception_api for remote exception storage. This is used by a new
35 indexer that will report any problems back into the RhodeCode instance in case of indexing problems.
36 - API: added function to fetch comments for a repository.
37 - Quick search: improve the styling of search input and results.
38 - Pull requests: allowed to select all forks and parent forks of target repository in creation UI.
39 This is a common workflow supported by GitHub etc.
40
41
42 General
43 ^^^^^^^
44
45 - Users/Repositories/Repository groups: expose IDs of those objects in advanced views.
46 Useful for API calls or usage in ishell.
47 - UI: moved repo group select next to the name as it's very relevant to each other.
48 - Pull requests: increase the stability of concurrent pull requests created.
49 - Pull requests: introduced operation state for pull requests to prevent from
50 locks during merge/update operations in concurrent busy environments.
51 - Pull requests: ensure that merge response provide more details about failed operations.
52 - UI / Files: expose downloads options onto files view similar as in summary page.
53 - Repositories: show hooks version and update link in the advanced section of repository page.
54 - Events: trigger 'review_status_change' in all cases when reviewers are changed
55 influencing review status.
56 - Files: display submodules in a sorted way, equal to how Directories are sorted.
57 - API: fetching all pull-requests now sorts the results and exposed a flag to show/hide
58 the merge result state for faster result fetching.
59 - API: merge_pull_request expose detailed merge message in the merge operation
60 next to numeric merge response code.
61 - API: added possibility to specify owner to create_pull_request API.
62 - SSH: Added ability to disable server-side SSH key generation to enforce users
63 generated SSH keys only outside of the server.
64 - Integrations: allow PUT method for WebHook integration.
65 - Dependencies: bumped git to 2.19.2 release.
66 - Dependencies: dropped pygments-markdown-lexer as it's natively supported by pygments now.
67 - Dependencies: bumped pyramid to 1.10.1
68 - Dependencies: bumped pastedeploy to 2.0.1
69 - Dependencies: bumped pastescript to 3.0.0
70 - Dependencies: bumped pathlib2 to 2.3.3
71 - Dependencies: bumped webob to 1.8.4
72 - Dependencies: bumped iso8601 to 0.1.12
73 - Dependencies: bumped more-itertools to 5.0.0
74 - Dependencies: bumped psutil to 5.4.8
75 - Dependencies: bumped pyasn1 to 0.4.5
76 - Dependencies: bumped pygments to 2.3.1
77 - Dependencies: bumped pyramid-debugtoolbar to 4.5.0
78 - Dependencies: bumped subprocess32 to 3.5.3
79 - Dependencies: bumped supervisor to 3.3.5
80 - Dependencies: bumped dogpile.cache to 0.7.1
81 - Dependencies: bumped simplejson to 3.16.0
82 - Dependencies: bumped gevent to 1.4.0
83 - Dependencies: bumped configparser to 3.5.1
84
85
86 Security
87 ^^^^^^^^
88
89 - Fork page: don't expose fork origin link if we don't have permission to access this repository.
90 Additionally don't pre-select such repository in pull request ref selector.
91 - Security: fix possible XSS in the issue tracker URL.
92 - Security: sanitize plaintext renderer with bleach, preventing XSS in rendered html.
93 - Audit logs: added audit logs for API permission calls.
94
95
96 Performance
97 ^^^^^^^^^^^
98
99 - Summary page: don't load repo size when showing expanded information about repository.
100 Size calculation needs to be triggered manually.
101 - Git: use rev-list for fetching last commit data in case of single commit history.
102 In some cases, it is much faster than previously used git log command.
103
104
105 Fixes
106 ^^^^^
107
108 - Installer: fixed 32bit package builds broken in previous releases.
109 - Git: use iterative fetch to prevent errors about too many arguments on
110 synchronizing very large repositories.
111 - Git: pass in the SSL dir that is exposed from wire for remote GIT commands.
112 - LDAP+Groups: improve logging, and fix the case when extracting group name from LDAP
113 returned nothing. We should warn about that, but not FAIL on login.
114 - Default reviewers: fixed submodule support in picking reviewers from annotation for files.
115 - Hooks: handle non-ascii characters in hooks new pull-requests open template.
116 - Diffs: fixed missing limited diff container display on over-size limit diffs.
117 - Diffs: fixed 500 error in case of some very uncommon diffs containing only Unicode characters.
118 - Repositories: handle VCS backend unavailable correctly in advanced settings for the repository.
119 - Remap & rescan: prevent empty/damaged repositories to break the remap operation.
120 - Visual: fixed show revision/commit length settings.
121 - Mercurial submodules: only show submodule in the path that it belongs too.
122 Before even submodules from root node were shown in subdirectories.
123 - UI/Files: fixed icons in file tree search.
124 - WebHook integration: quote URL variables to prevent URL errors with special chars
125 like # in the title.
126 - API: pull-requests, fixed invocation of merge as another user.
127 - VCS: limit fd leaks on subprocessio calls.
128 - VCS: expose SSL certificate path over the wire to the vcsserver, this solves some
129 remote SSL import problems reported.
130
131
132 Upgrade notes
133 ^^^^^^^^^^^^^
134
135 This release brings the new Full-text search capabilities using ElasticSearch 6.
136 If you use Elastic Search backend a backward compatibility mode is enabled and
137 ElasticSearch backend defaults to previously used ElasticSearch 2.
138
139 To use new features a full index rebuild is required, in addition ```--es-version=6``` flag
140 needs to be used with indexer and ```search.es_version = 6``` should be set in rhodecode.ini
141
142 Additionally new mapping format is available for the indexer that has additional capabilities
143 for include/exclude rules. Old format should work as well, but we encourage to
144 generate a new mapping.ini file using rhodecode-index command, and migrate your repositories
145 to the new format.
146
147 Please refer to the :ref:`indexing-ref` documentation for more details.
148
@@ -0,0 +1,107 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import pytest
23
24 from rhodecode.model.db import User, ChangesetComment
25 from rhodecode.model.meta import Session
26 from rhodecode.model.comment import CommentsModel
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_call_ok)
29
30
31 @pytest.fixture()
32 def make_repo_comments_factory(request):
33
34 def maker(repo):
35 user = User.get_first_super_admin()
36 commit = repo.scm_instance()[0]
37
38 commit_id = commit.raw_id
39 file_0 = commit.affected_files[0]
40 comments = []
41
42 # general
43 CommentsModel().create(
44 text='General Comment', repo=repo, user=user, commit_id=commit_id,
45 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
46
47 # inline
48 CommentsModel().create(
49 text='Inline Comment', repo=repo, user=user, commit_id=commit_id,
50 f_path=file_0, line_no='n1',
51 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
52
53 # todo
54 CommentsModel().create(
55 text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id,
56 f_path=file_0, line_no='n1',
57 comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False)
58
59 @request.addfinalizer
60 def cleanup():
61 for comment in comments:
62 Session().delete(comment)
63 return maker
64
65
66 @pytest.mark.usefixtures("testuser_api", "app")
67 class TestGetRepo(object):
68
69 @pytest.mark.parametrize('filters, expected_count', [
70 ({}, 3),
71 ({'comment_type': ChangesetComment.COMMENT_TYPE_NOTE}, 2),
72 ({'comment_type': ChangesetComment.COMMENT_TYPE_TODO}, 1),
73 ({'commit_id': 'FILLED DYNAMIC'}, 3),
74 ])
75 def test_api_get_repo_comments(self, backend, user_util,
76 make_repo_comments_factory, filters, expected_count):
77 commits = [{'message': 'A'}, {'message': 'B'}]
78 repo = backend.create_repo(commits=commits)
79 make_repo_comments_factory(repo)
80
81 api_call_params = {'repoid': repo.repo_name,}
82 api_call_params.update(filters)
83
84 if 'commit_id' in api_call_params:
85 commit = repo.scm_instance()[0]
86 commit_id = commit.raw_id
87 api_call_params['commit_id'] = commit_id
88
89 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
90 response = api_call(self.app, params)
91 result = assert_call_ok(id_, given=response.body)
92
93 assert len(result) == expected_count
94
95 def test_api_get_repo_comments_wrong_comment_typ(self, backend_hg):
96
97 repo = backend_hg.create_repo()
98 make_repo_comments_factory(repo)
99
100 api_call_params = {'repoid': repo.repo_name,}
101 api_call_params.update({'comment_type': 'bogus'})
102
103 expected = 'comment_type must be one of `{}` got {}'.format(
104 ChangesetComment.COMMENT_TYPES, 'bogus')
105 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
106 response = api_call(self.app, params)
107 assert_error(id_, expected, given=response.body)
@@ -0,0 +1,59 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import pytest
23
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok, assert_error
25
26
27 @pytest.mark.usefixtures("testuser_api", "app")
28 class TestStoreException(object):
29
30 def test_store_exception_invalid_json(self):
31 id_, params = build_data(self.apikey, 'store_exception',
32 exc_data_json='XXX,{')
33 response = api_call(self.app, params)
34
35 expected = 'Failed to parse JSON data from exc_data_json field. ' \
36 'Please make sure it contains a valid JSON.'
37 assert_error(id_, expected, given=response.body)
38
39 def test_store_exception_missing_json_params_json(self):
40 id_, params = build_data(self.apikey, 'store_exception',
41 exc_data_json='{"foo":"bar"}')
42 response = api_call(self.app, params)
43
44 expected = "Missing exc_traceback, or exc_type_name in " \
45 "exc_data_json field. Missing: 'exc_traceback'"
46 assert_error(id_, expected, given=response.body)
47
48 def test_store_exception(self):
49 id_, params = build_data(
50 self.apikey, 'store_exception',
51 exc_data_json='{"exc_traceback": "invalid", "exc_type_name":"ValueError"}')
52 response = api_call(self.app, params)
53 exc_id = response.json['result']['exc_id']
54
55 expected = {
56 'exc_id': exc_id,
57 'exc_url': 'http://example.com/_admin/settings/exceptions/{}'.format(exc_id)
58 }
59 assert_ok(id_, expected, given=response.body)
@@ -0,0 +1,49 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.config.middleware import _bool_setting, _string_setting
23
24
25 def _sanitize_settings_and_apply_defaults(settings):
26 """
27 Set defaults, convert to python types and validate settings.
28 """
29 _bool_setting(settings, config_keys.enabled, 'true')
30
31 _string_setting(settings, config_keys.backend, 'local')
32
33 default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store')
34 _string_setting(settings, config_keys.store_path, default_store)
35
36
37 def includeme(config):
38 settings = config.registry.settings
39 _sanitize_settings_and_apply_defaults(settings)
40
41 config.add_route(
42 name='upload_file',
43 pattern='/_file_store/upload')
44 config.add_route(
45 name='download_file',
46 pattern='/_file_store/download/{fid}')
47
48 # Scan module for configuration decorators.
49 config.scan('.views', ignore='.tests')
@@ -0,0 +1,27 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 # Definition of setting keys used to configure this module. Defined here to
23 # avoid repetition of keys throughout the module.
24
25 enabled = 'file_store.enabled'
26 backend = 'file_store.backend'
27 store_path = 'file_store.storage_path'
@@ -0,0 +1,31 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 class FileNotAllowedException(Exception):
23 """
24 Thrown if file does not have an allowed extension.
25 """
26
27
28 class FileOverSizeException(Exception):
29 """
30 Thrown if file is over the set limit.
31 """
@@ -0,0 +1,66 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 ANY = []
23 TEXT_EXT = ['txt', 'md', 'rst', 'log']
24 DOCUMENTS_EXT = ['pdf', 'rtf', 'odf', 'ods', 'gnumeric', 'abw', 'doc', 'docx', 'xls', 'xlsx']
25 IMAGES_EXT = ['jpg', 'jpe', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'tiff']
26 AUDIO_EXT = ['wav', 'mp3', 'aac', 'ogg', 'oga', 'flac']
27 VIDEO_EXT = ['mpeg', '3gp', 'avi', 'divx', 'dvr', 'flv', 'mp4', 'wmv']
28 DATA_EXT = ['csv', 'ini', 'json', 'plist', 'xml', 'yaml', 'yml']
29 SCRIPTS_EXT = ['js', 'php', 'pl', 'py', 'rb', 'sh', 'go', 'c', 'h']
30 ARCHIVES_EXT = ['gz', 'bz2', 'zip', 'tar', 'tgz', 'txz', '7z']
31 EXECUTABLES_EXT = ['so', 'exe', 'dll']
32
33
34 DEFAULT = DOCUMENTS_EXT + TEXT_EXT + IMAGES_EXT + DATA_EXT
35
36 GROUPS = dict((
37 ('any', ANY),
38 ('text', TEXT_EXT),
39 ('documents', DOCUMENTS_EXT),
40 ('images', IMAGES_EXT),
41 ('audio', AUDIO_EXT),
42 ('video', VIDEO_EXT),
43 ('data', DATA_EXT),
44 ('scripts', SCRIPTS_EXT),
45 ('archives', ARCHIVES_EXT),
46 ('executables', EXECUTABLES_EXT),
47 ('default', DEFAULT),
48 ))
49
50
51 def resolve_extensions(extensions, groups=None):
52 """
53 Calculate allowed extensions based on a list of extensions provided, and optional
54 groups of extensions from the available lists.
55
56 :param extensions: a list of extensions e.g ['py', 'txt']
57 :param groups: additionally groups to extend the extensions.
58 """
59 groups = groups or []
60 valid_exts = set([x.lower() for x in extensions])
61
62 for group in groups:
63 if group in GROUPS:
64 valid_exts.update(GROUPS[group])
65
66 return valid_exts
@@ -0,0 +1,211 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import time
23 import shutil
24 import hashlib
25
26 from rhodecode.lib.ext_json import json
27 from rhodecode.apps.file_store import utils
28 from rhodecode.apps.file_store.extensions import resolve_extensions
29 from rhodecode.apps.file_store.exceptions import FileNotAllowedException
30
31 METADATA_VER = 'v1'
32
33
34 class LocalFileStorage(object):
35
36 @classmethod
37 def resolve_name(cls, name, directory):
38 """
39 Resolves a unique name and the correct path. If a filename
40 for that path already exists then a numeric prefix with values > 0 will be
41 added, for example test.jpg -> test-1.jpg etc. initially file would have 0 prefix.
42
43 :param name: base name of file
44 :param directory: absolute directory path
45 """
46
47 basename, ext = os.path.splitext(name)
48 counter = 0
49 while True:
50 name = '%s-%d%s' % (basename, counter, ext)
51
52 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
53 sub_store = cls._sub_store_from_filename(basename)
54 sub_store_path = os.path.join(directory, sub_store)
55 if not os.path.exists(sub_store_path):
56 os.makedirs(sub_store_path)
57
58 path = os.path.join(sub_store_path, name)
59 if not os.path.exists(path):
60 return name, path
61 counter += 1
62
63 @classmethod
64 def _sub_store_from_filename(cls, filename):
65 return filename[:2]
66
67 @classmethod
68 def calculate_path_hash(cls, file_path):
69 """
70 Efficient calculation of file_path sha256 sum
71
72 :param file_path:
73 :return: sha256sum
74 """
75 digest = hashlib.sha256()
76 with open(file_path, 'rb') as f:
77 for chunk in iter(lambda: f.read(1024 * 100), b""):
78 digest.update(chunk)
79
80 return digest.hexdigest()
81
82 def __init__(self, base_path, extension_groups=None):
83
84 """
85 Local file storage
86
87 :param base_path: the absolute base path where uploads are stored
88 :param extension_groups: extensions string
89 """
90
91 extension_groups = extension_groups or ['any']
92 self.base_path = base_path
93 self.extensions = resolve_extensions([], groups=extension_groups)
94
95 def store_path(self, filename):
96 """
97 Returns absolute file path of the filename, joined to the
98 base_path.
99
100 :param filename: base name of file
101 """
102 sub_store = self._sub_store_from_filename(filename)
103 return os.path.join(self.base_path, sub_store, filename)
104
105 def delete(self, filename):
106 """
107 Deletes the filename. Filename is resolved with the
108 absolute path based on base_path. If file does not exist,
109 returns **False**, otherwise **True**
110
111 :param filename: base name of file
112 """
113 if self.exists(filename):
114 os.remove(self.store_path(filename))
115 return True
116 return False
117
118 def exists(self, filename):
119 """
120 Checks if file exists. Resolves filename's absolute
121 path based on base_path.
122
123 :param filename: base name of file
124 """
125 return os.path.exists(self.store_path(filename))
126
127 def filename_allowed(self, filename, extensions=None):
128 """Checks if a filename has an allowed extension
129
130 :param filename: base name of file
131 :param extensions: iterable of extensions (or self.extensions)
132 """
133 _, ext = os.path.splitext(filename)
134 return self.extension_allowed(ext, extensions)
135
136 def extension_allowed(self, ext, extensions=None):
137 """
138 Checks if an extension is permitted. Both e.g. ".jpg" and
139 "jpg" can be passed in. Extension lookup is case-insensitive.
140
141 :param ext: extension to check
142 :param extensions: iterable of extensions to validate against (or self.extensions)
143 """
144
145 extensions = extensions or self.extensions
146 if not extensions:
147 return True
148 if ext.startswith('.'):
149 ext = ext[1:]
150 return ext.lower() in extensions
151
152 def save_file(self, file_obj, filename, directory=None, extensions=None,
153 extra_metadata=None, **kwargs):
154 """
155 Saves a file object to the uploads location.
156 Returns the resolved filename, i.e. the directory +
157 the (randomized/incremented) base name.
158
159 :param file_obj: **cgi.FieldStorage** object (or similar)
160 :param filename: original filename
161 :param directory: relative path of sub-directory
162 :param extensions: iterable of allowed extensions, if not default
163 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
164 """
165
166 extensions = extensions or self.extensions
167
168 if not self.filename_allowed(filename, extensions):
169 raise FileNotAllowedException()
170
171 if directory:
172 dest_directory = os.path.join(self.base_path, directory)
173 else:
174 dest_directory = self.base_path
175
176 if not os.path.exists(dest_directory):
177 os.makedirs(dest_directory)
178
179 filename = utils.uid_filename(filename)
180
181 # resolve also produces special sub-dir for file optimized store
182 filename, path = self.resolve_name(filename, dest_directory)
183 stored_file_dir = os.path.dirname(path)
184
185 file_obj.seek(0)
186
187 with open(path, "wb") as dest:
188 shutil.copyfileobj(file_obj, dest)
189
190 metadata = {}
191 if extra_metadata:
192 metadata = extra_metadata
193
194 size = os.stat(path).st_size
195 file_hash = self.calculate_path_hash(path)
196
197 metadata.update(
198 {"filename": filename,
199 "size": size,
200 "time": time.time(),
201 "sha256": file_hash,
202 "meta_ver": METADATA_VER})
203
204 filename_meta = filename + '.meta'
205 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
206 dest_meta.write(json.dumps(metadata))
207
208 if directory:
209 filename = os.path.join(directory, filename)
210
211 return filename, metadata
@@ -0,0 +1,20 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
@@ -0,0 +1,111 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
21 import pytest
22
23 from rhodecode.lib.ext_json import json
24 from rhodecode.tests import TestController
25 from rhodecode.apps.file_store import utils, config_keys
26
27
28 def route_path(name, params=None, **kwargs):
29 import urllib
30
31 base_url = {
32 'upload_file': '/_file_store/upload',
33 'download_file': '/_file_store/download/{fid}',
34
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 return base_url
40
41
42 class TestFileStoreViews(TestController):
43
44 @pytest.mark.parametrize("fid, content, exists", [
45 ('abcde-0.jpg', "xxxxx", True),
46 ('abcde-0.exe', "1234567", True),
47 ('abcde-0.jpg', "xxxxx", False),
48 ])
49 def test_get_files_from_store(self, fid, content, exists, tmpdir):
50 self.log_user()
51 store_path = self.app._pyramid_settings[config_keys.store_path]
52
53 if exists:
54 status = 200
55 store = utils.get_file_storage({config_keys.store_path: store_path})
56 filesystem_file = os.path.join(str(tmpdir), fid)
57 with open(filesystem_file, 'wb') as f:
58 f.write(content)
59
60 with open(filesystem_file, 'rb') as f:
61 fid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
62
63 else:
64 status = 404
65
66 response = self.app.get(route_path('download_file', fid=fid), status=status)
67
68 if exists:
69 assert response.text == content
70 file_store_path = os.path.dirname(store.resolve_name(fid, store_path)[1])
71 metadata_file = os.path.join(file_store_path, fid + '.meta')
72 assert os.path.exists(metadata_file)
73 with open(metadata_file, 'rb') as f:
74 json_data = json.loads(f.read())
75
76 assert json_data
77 assert 'size' in json_data
78
79 def test_upload_files_without_content_to_store(self):
80 self.log_user()
81 response = self.app.post(
82 route_path('upload_file'),
83 params={'csrf_token': self.csrf_token},
84 status=200)
85
86 assert response.json == {
87 u'error': u'store_file data field is missing',
88 u'access_path': None,
89 u'store_fid': None}
90
91 def test_upload_files_bogus_content_to_store(self):
92 self.log_user()
93 response = self.app.post(
94 route_path('upload_file'),
95 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
96 status=200)
97
98 assert response.json == {
99 u'error': u'filename cannot be read from the data field',
100 u'access_path': None,
101 u'store_fid': None}
102
103 def test_upload_content_to_store(self):
104 self.log_user()
105 response = self.app.post(
106 route_path('upload_file'),
107 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
108 params={'csrf_token': self.csrf_token},
109 status=200)
110
111 assert response.json['store_fid']
@@ -0,0 +1,47 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import os
23 import uuid
24
25
26 def get_file_storage(settings):
27 from rhodecode.apps.file_store.local_store import LocalFileStorage
28 from rhodecode.apps.file_store import config_keys
29 store_path = settings.get(config_keys.store_path)
30 return LocalFileStorage(base_path=store_path)
31
32
33 def uid_filename(filename, randomized=True):
34 """
35 Generates a randomized or stable (uuid) filename,
36 preserving the original extension.
37
38 :param filename: the original filename
39 :param randomized: define if filename should be stable (sha1 based) or randomized
40 """
41 _, ext = os.path.splitext(filename)
42 if randomized:
43 uid = uuid.uuid4()
44 else:
45 hash_key = '{}.{}'.format(filename, 'store')
46 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
47 return str(uid) + ext.lower()
@@ -0,0 +1,115 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import logging
21
22 from pyramid.view import view_config
23 from pyramid.response import FileResponse
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps.file_store import utils
28 from rhodecode.apps.file_store.exceptions import (
29 FileNotAllowedException, FileOverSizeException)
30
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib.auth import (CSRFRequired, NotAnonymous)
34 from rhodecode.model.db import Session, FileStore
35
36 log = logging.getLogger(__name__)
37
38
39 class FileStoreView(BaseAppView):
40 upload_key = 'store_file'
41
42 def load_default_context(self):
43 c = self._get_local_tmpl_context()
44 self.storage = utils.get_file_storage(self.request.registry.settings)
45 return c
46
47 @NotAnonymous()
48 @CSRFRequired()
49 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
50 def upload_file(self):
51 self.load_default_context()
52 file_obj = self.request.POST.get(self.upload_key)
53
54 if file_obj is None:
55 return {'store_fid': None,
56 'access_path': None,
57 'error': '{} data field is missing'.format(self.upload_key)}
58
59 if not hasattr(file_obj, 'filename'):
60 return {'store_fid': None,
61 'access_path': None,
62 'error': 'filename cannot be read from the data field'}
63
64 filename = file_obj.filename
65
66 metadata = {
67 'user_uploaded': {'username': self._rhodecode_user.username,
68 'user_id': self._rhodecode_user.user_id,
69 'ip': self._rhodecode_user.ip_addr}}
70 try:
71 store_fid, metadata = self.storage.save_file(
72 file_obj.file, filename, extra_metadata=metadata)
73 except FileNotAllowedException:
74 return {'store_fid': None,
75 'access_path': None,
76 'error': 'File {} is not allowed.'.format(filename)}
77
78 except FileOverSizeException:
79 return {'store_fid': None,
80 'access_path': None,
81 'error': 'File {} is exceeding allowed limit.'.format(filename)}
82
83 try:
84 entry = FileStore.create(
85 file_uid=store_fid, filename=metadata["filename"],
86 file_hash=metadata["sha256"], file_size=metadata["size"],
87 file_description='upload attachment',
88 check_acl=False, user_id=self._rhodecode_user.user_id
89 )
90 Session().add(entry)
91 Session().commit()
92 log.debug('Stored upload in DB as %s', entry)
93 except Exception:
94 log.exception('Failed to store file %s', filename)
95 return {'store_fid': None,
96 'access_path': None,
97 'error': 'File {} failed to store in DB.'.format(filename)}
98
99 return {'store_fid': store_fid,
100 'access_path': h.route_path('download_file', fid=store_fid)}
101
102 @view_config(route_name='download_file')
103 def download_file(self):
104 self.load_default_context()
105 file_uid = self.request.matchdict['fid']
106 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
107
108 if not self.storage.exists(file_uid):
109 log.debug('File with FID:%s not found in the store', file_uid)
110 raise HTTPNotFound()
111
112 FileStore.bump_access_counter(file_uid)
113
114 file_path = self.storage.store_path(file_uid)
115 return FileResponse(file_path)
@@ -0,0 +1,110 b''
1 # Example to validate pushed files names and size using some sort of rules
2
3
4
5 @has_kwargs({
6 'server_url': 'url of instance that triggered this hook',
7 'config': 'path to .ini config used',
8 'scm': 'type of version control "git", "hg", "svn"',
9 'username': 'username of actor who triggered this event',
10 'ip': 'ip address of actor who triggered this hook',
11 'action': '',
12 'repository': 'repository name',
13 'repo_store_path': 'full path to where repositories are stored',
14 'commit_ids': 'pre transaction metadata for commit ids',
15 'hook_type': '',
16 'user_agent': 'Client user agent, e.g git or mercurial CLI version',
17 })
18 def _pre_push_hook(*args, **kwargs):
19 """
20 Post push hook
21 To stop version control from storing the transaction and send a message to user
22 use non-zero HookResponse with a message, e.g return HookResponse(1, 'Not allowed')
23
24 This message will be shown back to client during PUSH operation
25
26 Commit ids might look like that::
27
28 [{u'hg_env|git_env': ...,
29 u'multiple_heads': [],
30 u'name': u'default',
31 u'new_rev': u'd0befe0692e722e01d5677f27a104631cf798b69',
32 u'old_rev': u'd0befe0692e722e01d5677f27a104631cf798b69',
33 u'ref': u'',
34 u'total_commits': 2,
35 u'type': u'branch'}]
36 """
37 import fnmatch
38 from .helpers import extra_fields, extract_pre_files
39 from .utils import str2bool, aslist
40 from rhodecode.lib.helpers import format_byte_size_binary
41
42 # returns list of dicts with key-val fetched from extra fields
43 repo_extra_fields = extra_fields.run(**kwargs)
44
45 # optionally use 'extra fields' to control the logic per repo
46 # e.g store a list of patterns to be forbidden e.g `*.exe, *.dump`
47 forbid_files = repo_extra_fields.get('forbid_files_glob', {}).get('field_value')
48 forbid_files = aslist(forbid_files)
49
50 # optionally get bytes limit for a single file, e.g 1024 for 1KB
51 forbid_size_over = repo_extra_fields.get('forbid_size_over', {}).get('field_value')
52 forbid_size_over = int(forbid_size_over or 0)
53
54 def validate_file_name_and_size(file_data, forbidden_files=None, size_limit=None):
55 """
56 This function validates commited files against some sort of rules.
57 It should return a valid boolean, and a reason for failure
58
59 file_data =[
60 'raw_diff', 'old_revision', 'stats', 'original_filename', 'is_limited_diff',
61 'chunks', 'new_revision', 'operation', 'exceeds_limit', 'filename'
62 ]
63 file_data['ops'] = {
64 # is file binary
65 'binary': False,
66
67 # lines
68 'added': 32,
69 'deleted': 0
70
71 'ops': {3: 'modified file'},
72 'new_mode': '100644',
73 'old_mode': None
74 }
75 """
76 file_name = file_data['filename']
77 operation = file_data['operation'] # can be A(dded), M(odified), D(eleted)
78
79 # check files names
80 if forbidden_files:
81 reason = 'File {} is forbidden to be pushed'.format(file_name)
82 for forbidden_pattern in forbid_files:
83 # here we can also filter for operation, e.g if check for only ADDED files
84 # if operation == 'A':
85 if fnmatch.fnmatch(file_name, forbidden_pattern):
86 return False, reason
87
88 # validate A(dded) files and size
89 if size_limit and operation == 'A':
90 size = len(file_data['raw_diff'])
91
92 reason = 'File {} size of {} bytes exceeds limit {}'.format(
93 file_name, format_byte_size_binary(size),
94 format_byte_size_binary(size_limit))
95 if size > size_limit:
96 return False, reason
97
98 return True, ''
99
100 if forbid_files or forbid_size_over:
101 # returns list of dicts with key-val fetched from extra fields
102 file_list = extract_pre_files.run(**kwargs)
103
104 for file_data in file_list:
105 file_valid, reason = validate_file_name_and_size(
106 file_data, forbid_files, forbid_size_over)
107 if not file_valid:
108 return HookResponse(1, reason)
109
110 return HookResponse(0, '')
@@ -0,0 +1,96 b''
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2016-2019 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
20 """
21 us in hooks::
22
23 from .helpers import extract_pre_files
24 # returns list of dicts with key-val fetched from extra fields
25 file_list = extract_pre_files.run(**kwargs)
26
27 """
28 import re
29 import collections
30 import json
31
32 from rhodecode.lib import diffs
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
34 from rhodecode.lib.vcs.backends.git.diff import GitDiff
35
36
37 def get_hg_files(repo, refs):
38 files = []
39 return files
40
41
42 def get_git_files(repo, refs):
43 files = []
44
45 for data in refs:
46 # we should now extract commit data
47 old_rev = data['old_rev']
48 new_rev = data['new_rev']
49
50 if '00000000' in old_rev:
51 # new branch, we don't need to extract nothing
52 return files
53
54 git_env = dict(data['git_env'])
55
56 cmd = [
57 'diff', old_rev, new_rev
58 ]
59
60 stdout, stderr = repo.run_git_command(cmd, extra_env=git_env)
61 vcs_diff = GitDiff(stdout)
62
63 diff_processor = diffs.DiffProcessor(vcs_diff, format='newdiff')
64 # this is list of dicts with diff information
65 # _parsed[0].keys()
66 # ['raw_diff', 'old_revision', 'stats', 'original_filename',
67 # 'is_limited_diff', 'chunks', 'new_revision', 'operation',
68 # 'exceeds_limit', 'filename']
69 files = _parsed = diff_processor.prepare()
70
71 return files
72
73
74 def run(*args, **kwargs):
75 from rhodecode.model.db import Repository
76
77 vcs_type = kwargs['scm']
78 # use temp name then the main one propagated
79 repo_name = kwargs.pop('REPOSITORY', None) or kwargs['repository']
80
81 repo = Repository.get_by_repo_name(repo_name)
82 vcs_repo = repo.scm_instance(cache=False)
83
84 files = []
85
86 if vcs_type == 'git':
87 for rev_data in kwargs['commit_ids']:
88 new_environ = dict((k, v) for k, v in rev_data['git_env'])
89 files = get_git_files(vcs_repo, kwargs['commit_ids'])
90
91 if vcs_type == 'hg':
92 for rev_data in kwargs['commit_ids']:
93 new_environ = dict((k, v) for k, v in rev_data['hg_env'])
94 files = get_hg_files(vcs_repo, kwargs['commit_ids'])
95
96 return files
This diff has been collapsed as it changes many lines, (4758 lines changed) Show them Hide them
@@ -0,0 +1,4758 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Database Models for RhodeCode Enterprise
23 """
24
25 import re
26 import os
27 import time
28 import hashlib
29 import logging
30 import datetime
31 import warnings
32 import ipaddress
33 import functools
34 import traceback
35 import collections
36
37 from sqlalchemy import (
38 or_, and_, not_, func, TypeDecorator, event,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType)
42 from sqlalchemy.sql.expression import true, false
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.orm import (
45 relationship, joinedload, class_mapper, validates, aliased)
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from pyramid import compat
52 from pyramid.threadlocal import get_current_request
53
54 from rhodecode.translation import _
55 from rhodecode.lib.vcs import get_vcs_instance
56 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
57 from rhodecode.lib.utils2 import (
58 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
60 glob2re, StrictAttributeDict, cleaned_uri)
61 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
62 JsonRaw
63 from rhodecode.lib.ext_json import json
64 from rhodecode.lib.caching_query import FromCache
65 from rhodecode.lib.encrypt import AESCipher
66
67 from rhodecode.model.meta import Base, Session
68
69 URL_SEP = '/'
70 log = logging.getLogger(__name__)
71
72 # =============================================================================
73 # BASE CLASSES
74 # =============================================================================
75
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 # beaker.session.secret if first is not set.
78 # and initialized at environment.py
79 ENCRYPTION_KEY = None
80
81 # used to sort permissions by types, '#' used here is not allowed to be in
82 # usernames, and it's very early in sorted string.printable table.
83 PERMISSION_TYPE_SORT = {
84 'admin': '####',
85 'write': '###',
86 'read': '##',
87 'none': '#',
88 }
89
90
91 def display_user_sort(obj):
92 """
93 Sort function used to sort permissions in .permissions() function of
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 of all other resources
96 """
97
98 if obj.username == User.DEFAULT_USER:
99 return '#####'
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 return prefix + obj.username
102
103
104 def display_user_group_sort(obj):
105 """
106 Sort function used to sort permissions in .permissions() function of
107 Repository, RepoGroup, UserGroup. Also it put the default user in front
108 of all other resources
109 """
110
111 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
112 return prefix + obj.users_group_name
113
114
115 def _hash_key(k):
116 return sha1_safe(k)
117
118
119 def in_filter_generator(qry, items, limit=500):
120 """
121 Splits IN() into multiple with OR
122 e.g.::
123 cnt = Repository.query().filter(
124 or_(
125 *in_filter_generator(Repository.repo_id, range(100000))
126 )).count()
127 """
128 if not items:
129 # empty list will cause empty query which might cause security issues
130 # this can lead to hidden unpleasant results
131 items = [-1]
132
133 parts = []
134 for chunk in xrange(0, len(items), limit):
135 parts.append(
136 qry.in_(items[chunk: chunk + limit])
137 )
138
139 return parts
140
141
142 base_table_args = {
143 'extend_existing': True,
144 'mysql_engine': 'InnoDB',
145 'mysql_charset': 'utf8',
146 'sqlite_autoincrement': True
147 }
148
149
150 class EncryptedTextValue(TypeDecorator):
151 """
152 Special column for encrypted long text data, use like::
153
154 value = Column("encrypted_value", EncryptedValue(), nullable=False)
155
156 This column is intelligent so if value is in unencrypted form it return
157 unencrypted form, but on save it always encrypts
158 """
159 impl = Text
160
161 def process_bind_param(self, value, dialect):
162 if not value:
163 return value
164 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
165 # protect against double encrypting if someone manually starts
166 # doing
167 raise ValueError('value needs to be in unencrypted format, ie. '
168 'not starting with enc$aes')
169 return 'enc$aes_hmac$%s' % AESCipher(
170 ENCRYPTION_KEY, hmac=True).encrypt(value)
171
172 def process_result_value(self, value, dialect):
173 import rhodecode
174
175 if not value:
176 return value
177
178 parts = value.split('$', 3)
179 if not len(parts) == 3:
180 # probably not encrypted values
181 return value
182 else:
183 if parts[0] != 'enc':
184 # parts ok but without our header ?
185 return value
186 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
187 'rhodecode.encrypted_values.strict') or True)
188 # at that stage we know it's our encryption
189 if parts[1] == 'aes':
190 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
191 elif parts[1] == 'aes_hmac':
192 decrypted_data = AESCipher(
193 ENCRYPTION_KEY, hmac=True,
194 strict_verification=enc_strict_mode).decrypt(parts[2])
195 else:
196 raise ValueError(
197 'Encryption type part is wrong, must be `aes` '
198 'or `aes_hmac`, got `%s` instead' % (parts[1]))
199 return decrypted_data
200
201
202 class BaseModel(object):
203 """
204 Base Model for all classes
205 """
206
207 @classmethod
208 def _get_keys(cls):
209 """return column names for this model """
210 return class_mapper(cls).c.keys()
211
212 def get_dict(self):
213 """
214 return dict with keys and values corresponding
215 to this model data """
216
217 d = {}
218 for k in self._get_keys():
219 d[k] = getattr(self, k)
220
221 # also use __json__() if present to get additional fields
222 _json_attr = getattr(self, '__json__', None)
223 if _json_attr:
224 # update with attributes from __json__
225 if callable(_json_attr):
226 _json_attr = _json_attr()
227 for k, val in _json_attr.iteritems():
228 d[k] = val
229 return d
230
231 def get_appstruct(self):
232 """return list with keys and values tuples corresponding
233 to this model data """
234
235 lst = []
236 for k in self._get_keys():
237 lst.append((k, getattr(self, k),))
238 return lst
239
240 def populate_obj(self, populate_dict):
241 """populate model with data from given populate_dict"""
242
243 for k in self._get_keys():
244 if k in populate_dict:
245 setattr(self, k, populate_dict[k])
246
247 @classmethod
248 def query(cls):
249 return Session().query(cls)
250
251 @classmethod
252 def get(cls, id_):
253 if id_:
254 return cls.query().get(id_)
255
256 @classmethod
257 def get_or_404(cls, id_):
258 from pyramid.httpexceptions import HTTPNotFound
259
260 try:
261 id_ = int(id_)
262 except (TypeError, ValueError):
263 raise HTTPNotFound()
264
265 res = cls.query().get(id_)
266 if not res:
267 raise HTTPNotFound()
268 return res
269
270 @classmethod
271 def getAll(cls):
272 # deprecated and left for backward compatibility
273 return cls.get_all()
274
275 @classmethod
276 def get_all(cls):
277 return cls.query().all()
278
279 @classmethod
280 def delete(cls, id_):
281 obj = cls.query().get(id_)
282 Session().delete(obj)
283
284 @classmethod
285 def identity_cache(cls, session, attr_name, value):
286 exist_in_session = []
287 for (item_cls, pkey), instance in session.identity_map.items():
288 if cls == item_cls and getattr(instance, attr_name) == value:
289 exist_in_session.append(instance)
290 if exist_in_session:
291 if len(exist_in_session) == 1:
292 return exist_in_session[0]
293 log.exception(
294 'multiple objects with attr %s and '
295 'value %s found with same name: %r',
296 attr_name, value, exist_in_session)
297
298 def __repr__(self):
299 if hasattr(self, '__unicode__'):
300 # python repr needs to return str
301 try:
302 return safe_str(self.__unicode__())
303 except UnicodeDecodeError:
304 pass
305 return '<DB:%s>' % (self.__class__.__name__)
306
307
308 class RhodeCodeSetting(Base, BaseModel):
309 __tablename__ = 'rhodecode_settings'
310 __table_args__ = (
311 UniqueConstraint('app_settings_name'),
312 base_table_args
313 )
314
315 SETTINGS_TYPES = {
316 'str': safe_str,
317 'int': safe_int,
318 'unicode': safe_unicode,
319 'bool': str2bool,
320 'list': functools.partial(aslist, sep=',')
321 }
322 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
323 GLOBAL_CONF_KEY = 'app_settings'
324
325 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
326 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
327 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
328 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
329
330 def __init__(self, key='', val='', type='unicode'):
331 self.app_settings_name = key
332 self.app_settings_type = type
333 self.app_settings_value = val
334
335 @validates('_app_settings_value')
336 def validate_settings_value(self, key, val):
337 assert type(val) == unicode
338 return val
339
340 @hybrid_property
341 def app_settings_value(self):
342 v = self._app_settings_value
343 _type = self.app_settings_type
344 if _type:
345 _type = self.app_settings_type.split('.')[0]
346 # decode the encrypted value
347 if 'encrypted' in self.app_settings_type:
348 cipher = EncryptedTextValue()
349 v = safe_unicode(cipher.process_result_value(v, None))
350
351 converter = self.SETTINGS_TYPES.get(_type) or \
352 self.SETTINGS_TYPES['unicode']
353 return converter(v)
354
355 @app_settings_value.setter
356 def app_settings_value(self, val):
357 """
358 Setter that will always make sure we use unicode in app_settings_value
359
360 :param val:
361 """
362 val = safe_unicode(val)
363 # encode the encrypted value
364 if 'encrypted' in self.app_settings_type:
365 cipher = EncryptedTextValue()
366 val = safe_unicode(cipher.process_bind_param(val, None))
367 self._app_settings_value = val
368
369 @hybrid_property
370 def app_settings_type(self):
371 return self._app_settings_type
372
373 @app_settings_type.setter
374 def app_settings_type(self, val):
375 if val.split('.')[0] not in self.SETTINGS_TYPES:
376 raise Exception('type must be one of %s got %s'
377 % (self.SETTINGS_TYPES.keys(), val))
378 self._app_settings_type = val
379
380 @classmethod
381 def get_by_prefix(cls, prefix):
382 return RhodeCodeSetting.query()\
383 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
384 .all()
385
386 def __unicode__(self):
387 return u"<%s('%s:%s[%s]')>" % (
388 self.__class__.__name__,
389 self.app_settings_name, self.app_settings_value,
390 self.app_settings_type
391 )
392
393
394 class RhodeCodeUi(Base, BaseModel):
395 __tablename__ = 'rhodecode_ui'
396 __table_args__ = (
397 UniqueConstraint('ui_key'),
398 base_table_args
399 )
400
401 HOOK_REPO_SIZE = 'changegroup.repo_size'
402 # HG
403 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
404 HOOK_PULL = 'outgoing.pull_logger'
405 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
406 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
407 HOOK_PUSH = 'changegroup.push_logger'
408 HOOK_PUSH_KEY = 'pushkey.key_push'
409
410 # TODO: johbo: Unify way how hooks are configured for git and hg,
411 # git part is currently hardcoded.
412
413 # SVN PATTERNS
414 SVN_BRANCH_ID = 'vcs_svn_branch'
415 SVN_TAG_ID = 'vcs_svn_tag'
416
417 ui_id = Column(
418 "ui_id", Integer(), nullable=False, unique=True, default=None,
419 primary_key=True)
420 ui_section = Column(
421 "ui_section", String(255), nullable=True, unique=None, default=None)
422 ui_key = Column(
423 "ui_key", String(255), nullable=True, unique=None, default=None)
424 ui_value = Column(
425 "ui_value", String(255), nullable=True, unique=None, default=None)
426 ui_active = Column(
427 "ui_active", Boolean(), nullable=True, unique=None, default=True)
428
429 def __repr__(self):
430 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
431 self.ui_key, self.ui_value)
432
433
434 class RepoRhodeCodeSetting(Base, BaseModel):
435 __tablename__ = 'repo_rhodecode_settings'
436 __table_args__ = (
437 UniqueConstraint(
438 'app_settings_name', 'repository_id',
439 name='uq_repo_rhodecode_setting_name_repo_id'),
440 base_table_args
441 )
442
443 repository_id = Column(
444 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
445 nullable=False)
446 app_settings_id = Column(
447 "app_settings_id", Integer(), nullable=False, unique=True,
448 default=None, primary_key=True)
449 app_settings_name = Column(
450 "app_settings_name", String(255), nullable=True, unique=None,
451 default=None)
452 _app_settings_value = Column(
453 "app_settings_value", String(4096), nullable=True, unique=None,
454 default=None)
455 _app_settings_type = Column(
456 "app_settings_type", String(255), nullable=True, unique=None,
457 default=None)
458
459 repository = relationship('Repository')
460
461 def __init__(self, repository_id, key='', val='', type='unicode'):
462 self.repository_id = repository_id
463 self.app_settings_name = key
464 self.app_settings_type = type
465 self.app_settings_value = val
466
467 @validates('_app_settings_value')
468 def validate_settings_value(self, key, val):
469 assert type(val) == unicode
470 return val
471
472 @hybrid_property
473 def app_settings_value(self):
474 v = self._app_settings_value
475 type_ = self.app_settings_type
476 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
477 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
478 return converter(v)
479
480 @app_settings_value.setter
481 def app_settings_value(self, val):
482 """
483 Setter that will always make sure we use unicode in app_settings_value
484
485 :param val:
486 """
487 self._app_settings_value = safe_unicode(val)
488
489 @hybrid_property
490 def app_settings_type(self):
491 return self._app_settings_type
492
493 @app_settings_type.setter
494 def app_settings_type(self, val):
495 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
496 if val not in SETTINGS_TYPES:
497 raise Exception('type must be one of %s got %s'
498 % (SETTINGS_TYPES.keys(), val))
499 self._app_settings_type = val
500
501 def __unicode__(self):
502 return u"<%s('%s:%s:%s[%s]')>" % (
503 self.__class__.__name__, self.repository.repo_name,
504 self.app_settings_name, self.app_settings_value,
505 self.app_settings_type
506 )
507
508
509 class RepoRhodeCodeUi(Base, BaseModel):
510 __tablename__ = 'repo_rhodecode_ui'
511 __table_args__ = (
512 UniqueConstraint(
513 'repository_id', 'ui_section', 'ui_key',
514 name='uq_repo_rhodecode_ui_repository_id_section_key'),
515 base_table_args
516 )
517
518 repository_id = Column(
519 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
520 nullable=False)
521 ui_id = Column(
522 "ui_id", Integer(), nullable=False, unique=True, default=None,
523 primary_key=True)
524 ui_section = Column(
525 "ui_section", String(255), nullable=True, unique=None, default=None)
526 ui_key = Column(
527 "ui_key", String(255), nullable=True, unique=None, default=None)
528 ui_value = Column(
529 "ui_value", String(255), nullable=True, unique=None, default=None)
530 ui_active = Column(
531 "ui_active", Boolean(), nullable=True, unique=None, default=True)
532
533 repository = relationship('Repository')
534
535 def __repr__(self):
536 return '<%s[%s:%s]%s=>%s]>' % (
537 self.__class__.__name__, self.repository.repo_name,
538 self.ui_section, self.ui_key, self.ui_value)
539
540
541 class User(Base, BaseModel):
542 __tablename__ = 'users'
543 __table_args__ = (
544 UniqueConstraint('username'), UniqueConstraint('email'),
545 Index('u_username_idx', 'username'),
546 Index('u_email_idx', 'email'),
547 base_table_args
548 )
549
550 DEFAULT_USER = 'default'
551 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
552 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
553
554 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
555 username = Column("username", String(255), nullable=True, unique=None, default=None)
556 password = Column("password", String(255), nullable=True, unique=None, default=None)
557 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
558 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
559 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
560 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
561 _email = Column("email", String(255), nullable=True, unique=None, default=None)
562 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
563 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
564
565 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
566 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
567 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
568 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
569 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
570 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
571
572 user_log = relationship('UserLog')
573 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
574
575 repositories = relationship('Repository')
576 repository_groups = relationship('RepoGroup')
577 user_groups = relationship('UserGroup')
578
579 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
580 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
581
582 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
583 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
584 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
585
586 group_member = relationship('UserGroupMember', cascade='all')
587
588 notifications = relationship('UserNotification', cascade='all')
589 # notifications assigned to this user
590 user_created_notifications = relationship('Notification', cascade='all')
591 # comments created by this user
592 user_comments = relationship('ChangesetComment', cascade='all')
593 # user profile extra info
594 user_emails = relationship('UserEmailMap', cascade='all')
595 user_ip_map = relationship('UserIpMap', cascade='all')
596 user_auth_tokens = relationship('UserApiKeys', cascade='all')
597 user_ssh_keys = relationship('UserSshKeys', cascade='all')
598
599 # gists
600 user_gists = relationship('Gist', cascade='all')
601 # user pull requests
602 user_pull_requests = relationship('PullRequest', cascade='all')
603 # external identities
604 extenal_identities = relationship(
605 'ExternalIdentity',
606 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
607 cascade='all')
608 # review rules
609 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
610
611 def __unicode__(self):
612 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
613 self.user_id, self.username)
614
615 @hybrid_property
616 def email(self):
617 return self._email
618
619 @email.setter
620 def email(self, val):
621 self._email = val.lower() if val else None
622
623 @hybrid_property
624 def first_name(self):
625 from rhodecode.lib import helpers as h
626 if self.name:
627 return h.escape(self.name)
628 return self.name
629
630 @hybrid_property
631 def last_name(self):
632 from rhodecode.lib import helpers as h
633 if self.lastname:
634 return h.escape(self.lastname)
635 return self.lastname
636
637 @hybrid_property
638 def api_key(self):
639 """
640 Fetch if exist an auth-token with role ALL connected to this user
641 """
642 user_auth_token = UserApiKeys.query()\
643 .filter(UserApiKeys.user_id == self.user_id)\
644 .filter(or_(UserApiKeys.expires == -1,
645 UserApiKeys.expires >= time.time()))\
646 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
647 if user_auth_token:
648 user_auth_token = user_auth_token.api_key
649
650 return user_auth_token
651
652 @api_key.setter
653 def api_key(self, val):
654 # don't allow to set API key this is deprecated for now
655 self._api_key = None
656
657 @property
658 def reviewer_pull_requests(self):
659 return PullRequestReviewers.query() \
660 .options(joinedload(PullRequestReviewers.pull_request)) \
661 .filter(PullRequestReviewers.user_id == self.user_id) \
662 .all()
663
664 @property
665 def firstname(self):
666 # alias for future
667 return self.name
668
669 @property
670 def emails(self):
671 other = UserEmailMap.query()\
672 .filter(UserEmailMap.user == self) \
673 .order_by(UserEmailMap.email_id.asc()) \
674 .all()
675 return [self.email] + [x.email for x in other]
676
677 @property
678 def auth_tokens(self):
679 auth_tokens = self.get_auth_tokens()
680 return [x.api_key for x in auth_tokens]
681
682 def get_auth_tokens(self):
683 return UserApiKeys.query()\
684 .filter(UserApiKeys.user == self)\
685 .order_by(UserApiKeys.user_api_key_id.asc())\
686 .all()
687
688 @LazyProperty
689 def feed_token(self):
690 return self.get_feed_token()
691
692 def get_feed_token(self, cache=True):
693 feed_tokens = UserApiKeys.query()\
694 .filter(UserApiKeys.user == self)\
695 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
696 if cache:
697 feed_tokens = feed_tokens.options(
698 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
699
700 feed_tokens = feed_tokens.all()
701 if feed_tokens:
702 return feed_tokens[0].api_key
703 return 'NO_FEED_TOKEN_AVAILABLE'
704
705 @classmethod
706 def get(cls, user_id, cache=False):
707 if not user_id:
708 return
709
710 user = cls.query()
711 if cache:
712 user = user.options(
713 FromCache("sql_cache_short", "get_users_%s" % user_id))
714 return user.get(user_id)
715
716 @classmethod
717 def extra_valid_auth_tokens(cls, user, role=None):
718 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
719 .filter(or_(UserApiKeys.expires == -1,
720 UserApiKeys.expires >= time.time()))
721 if role:
722 tokens = tokens.filter(or_(UserApiKeys.role == role,
723 UserApiKeys.role == UserApiKeys.ROLE_ALL))
724 return tokens.all()
725
726 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
727 from rhodecode.lib import auth
728
729 log.debug('Trying to authenticate user: %s via auth-token, '
730 'and roles: %s', self, roles)
731
732 if not auth_token:
733 return False
734
735 crypto_backend = auth.crypto_backend()
736
737 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
738 tokens_q = UserApiKeys.query()\
739 .filter(UserApiKeys.user_id == self.user_id)\
740 .filter(or_(UserApiKeys.expires == -1,
741 UserApiKeys.expires >= time.time()))
742
743 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
744
745 plain_tokens = []
746 hash_tokens = []
747
748 user_tokens = tokens_q.all()
749 log.debug('Found %s user tokens to check for authentication', len(user_tokens))
750 for token in user_tokens:
751 log.debug('AUTH_TOKEN: checking if user token with id `%s` matches',
752 token.user_api_key_id)
753 # verify scope first, since it's way faster than hash calculation of
754 # encrypted tokens
755 if token.repo_id:
756 # token has a scope, we need to verify it
757 if scope_repo_id != token.repo_id:
758 log.debug(
759 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
760 'and calling scope is:%s, skipping further checks',
761 token.repo, scope_repo_id)
762 # token has a scope, and it doesn't match, skip token
763 continue
764
765 if token.api_key.startswith(crypto_backend.ENC_PREF):
766 hash_tokens.append(token.api_key)
767 else:
768 plain_tokens.append(token.api_key)
769
770 is_plain_match = auth_token in plain_tokens
771 if is_plain_match:
772 return True
773
774 for hashed in hash_tokens:
775 # NOTE(marcink): this is expensive to calculate, but most secure
776 match = crypto_backend.hash_check(auth_token, hashed)
777 if match:
778 return True
779
780 return False
781
782 @property
783 def ip_addresses(self):
784 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
785 return [x.ip_addr for x in ret]
786
787 @property
788 def username_and_name(self):
789 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
790
791 @property
792 def username_or_name_or_email(self):
793 full_name = self.full_name if self.full_name is not ' ' else None
794 return self.username or full_name or self.email
795
796 @property
797 def full_name(self):
798 return '%s %s' % (self.first_name, self.last_name)
799
800 @property
801 def full_name_or_username(self):
802 return ('%s %s' % (self.first_name, self.last_name)
803 if (self.first_name and self.last_name) else self.username)
804
805 @property
806 def full_contact(self):
807 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
808
809 @property
810 def short_contact(self):
811 return '%s %s' % (self.first_name, self.last_name)
812
813 @property
814 def is_admin(self):
815 return self.admin
816
817 def AuthUser(self, **kwargs):
818 """
819 Returns instance of AuthUser for this user
820 """
821 from rhodecode.lib.auth import AuthUser
822 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
823
824 @hybrid_property
825 def user_data(self):
826 if not self._user_data:
827 return {}
828
829 try:
830 return json.loads(self._user_data)
831 except TypeError:
832 return {}
833
834 @user_data.setter
835 def user_data(self, val):
836 if not isinstance(val, dict):
837 raise Exception('user_data must be dict, got %s' % type(val))
838 try:
839 self._user_data = json.dumps(val)
840 except Exception:
841 log.error(traceback.format_exc())
842
843 @classmethod
844 def get_by_username(cls, username, case_insensitive=False,
845 cache=False, identity_cache=False):
846 session = Session()
847
848 if case_insensitive:
849 q = cls.query().filter(
850 func.lower(cls.username) == func.lower(username))
851 else:
852 q = cls.query().filter(cls.username == username)
853
854 if cache:
855 if identity_cache:
856 val = cls.identity_cache(session, 'username', username)
857 if val:
858 return val
859 else:
860 cache_key = "get_user_by_name_%s" % _hash_key(username)
861 q = q.options(
862 FromCache("sql_cache_short", cache_key))
863
864 return q.scalar()
865
866 @classmethod
867 def get_by_auth_token(cls, auth_token, cache=False):
868 q = UserApiKeys.query()\
869 .filter(UserApiKeys.api_key == auth_token)\
870 .filter(or_(UserApiKeys.expires == -1,
871 UserApiKeys.expires >= time.time()))
872 if cache:
873 q = q.options(
874 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
875
876 match = q.first()
877 if match:
878 return match.user
879
880 @classmethod
881 def get_by_email(cls, email, case_insensitive=False, cache=False):
882
883 if case_insensitive:
884 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
885
886 else:
887 q = cls.query().filter(cls.email == email)
888
889 email_key = _hash_key(email)
890 if cache:
891 q = q.options(
892 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
893
894 ret = q.scalar()
895 if ret is None:
896 q = UserEmailMap.query()
897 # try fetching in alternate email map
898 if case_insensitive:
899 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
900 else:
901 q = q.filter(UserEmailMap.email == email)
902 q = q.options(joinedload(UserEmailMap.user))
903 if cache:
904 q = q.options(
905 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
906 ret = getattr(q.scalar(), 'user', None)
907
908 return ret
909
910 @classmethod
911 def get_from_cs_author(cls, author):
912 """
913 Tries to get User objects out of commit author string
914
915 :param author:
916 """
917 from rhodecode.lib.helpers import email, author_name
918 # Valid email in the attribute passed, see if they're in the system
919 _email = email(author)
920 if _email:
921 user = cls.get_by_email(_email, case_insensitive=True)
922 if user:
923 return user
924 # Maybe we can match by username?
925 _author = author_name(author)
926 user = cls.get_by_username(_author, case_insensitive=True)
927 if user:
928 return user
929
930 def update_userdata(self, **kwargs):
931 usr = self
932 old = usr.user_data
933 old.update(**kwargs)
934 usr.user_data = old
935 Session().add(usr)
936 log.debug('updated userdata with ', kwargs)
937
938 def update_lastlogin(self):
939 """Update user lastlogin"""
940 self.last_login = datetime.datetime.now()
941 Session().add(self)
942 log.debug('updated user %s lastlogin', self.username)
943
944 def update_password(self, new_password):
945 from rhodecode.lib.auth import get_crypt_password
946
947 self.password = get_crypt_password(new_password)
948 Session().add(self)
949
950 @classmethod
951 def get_first_super_admin(cls):
952 user = User.query()\
953 .filter(User.admin == true()) \
954 .order_by(User.user_id.asc()) \
955 .first()
956
957 if user is None:
958 raise Exception('FATAL: Missing administrative account!')
959 return user
960
961 @classmethod
962 def get_all_super_admins(cls):
963 """
964 Returns all admin accounts sorted by username
965 """
966 return User.query().filter(User.admin == true())\
967 .order_by(User.username.asc()).all()
968
969 @classmethod
970 def get_default_user(cls, cache=False, refresh=False):
971 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
972 if user is None:
973 raise Exception('FATAL: Missing default account!')
974 if refresh:
975 # The default user might be based on outdated state which
976 # has been loaded from the cache.
977 # A call to refresh() ensures that the
978 # latest state from the database is used.
979 Session().refresh(user)
980 return user
981
982 def _get_default_perms(self, user, suffix=''):
983 from rhodecode.model.permission import PermissionModel
984 return PermissionModel().get_default_perms(user.user_perms, suffix)
985
986 def get_default_perms(self, suffix=''):
987 return self._get_default_perms(self, suffix)
988
989 def get_api_data(self, include_secrets=False, details='full'):
990 """
991 Common function for generating user related data for API
992
993 :param include_secrets: By default secrets in the API data will be replaced
994 by a placeholder value to prevent exposing this data by accident. In case
995 this data shall be exposed, set this flag to ``True``.
996
997 :param details: details can be 'basic|full' basic gives only a subset of
998 the available user information that includes user_id, name and emails.
999 """
1000 user = self
1001 user_data = self.user_data
1002 data = {
1003 'user_id': user.user_id,
1004 'username': user.username,
1005 'firstname': user.name,
1006 'lastname': user.lastname,
1007 'email': user.email,
1008 'emails': user.emails,
1009 }
1010 if details == 'basic':
1011 return data
1012
1013 auth_token_length = 40
1014 auth_token_replacement = '*' * auth_token_length
1015
1016 extras = {
1017 'auth_tokens': [auth_token_replacement],
1018 'active': user.active,
1019 'admin': user.admin,
1020 'extern_type': user.extern_type,
1021 'extern_name': user.extern_name,
1022 'last_login': user.last_login,
1023 'last_activity': user.last_activity,
1024 'ip_addresses': user.ip_addresses,
1025 'language': user_data.get('language')
1026 }
1027 data.update(extras)
1028
1029 if include_secrets:
1030 data['auth_tokens'] = user.auth_tokens
1031 return data
1032
1033 def __json__(self):
1034 data = {
1035 'full_name': self.full_name,
1036 'full_name_or_username': self.full_name_or_username,
1037 'short_contact': self.short_contact,
1038 'full_contact': self.full_contact,
1039 }
1040 data.update(self.get_api_data())
1041 return data
1042
1043
1044 class UserApiKeys(Base, BaseModel):
1045 __tablename__ = 'user_api_keys'
1046 __table_args__ = (
1047 Index('uak_api_key_idx', 'api_key', unique=True),
1048 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1049 base_table_args
1050 )
1051 __mapper_args__ = {}
1052
1053 # ApiKey role
1054 ROLE_ALL = 'token_role_all'
1055 ROLE_HTTP = 'token_role_http'
1056 ROLE_VCS = 'token_role_vcs'
1057 ROLE_API = 'token_role_api'
1058 ROLE_FEED = 'token_role_feed'
1059 ROLE_PASSWORD_RESET = 'token_password_reset'
1060
1061 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1062
1063 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1064 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1065 api_key = Column("api_key", String(255), nullable=False, unique=True)
1066 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1067 expires = Column('expires', Float(53), nullable=False)
1068 role = Column('role', String(255), nullable=True)
1069 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1070
1071 # scope columns
1072 repo_id = Column(
1073 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1074 nullable=True, unique=None, default=None)
1075 repo = relationship('Repository', lazy='joined')
1076
1077 repo_group_id = Column(
1078 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1079 nullable=True, unique=None, default=None)
1080 repo_group = relationship('RepoGroup', lazy='joined')
1081
1082 user = relationship('User', lazy='joined')
1083
1084 def __unicode__(self):
1085 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1086
1087 def __json__(self):
1088 data = {
1089 'auth_token': self.api_key,
1090 'role': self.role,
1091 'scope': self.scope_humanized,
1092 'expired': self.expired
1093 }
1094 return data
1095
1096 def get_api_data(self, include_secrets=False):
1097 data = self.__json__()
1098 if include_secrets:
1099 return data
1100 else:
1101 data['auth_token'] = self.token_obfuscated
1102 return data
1103
1104 @hybrid_property
1105 def description_safe(self):
1106 from rhodecode.lib import helpers as h
1107 return h.escape(self.description)
1108
1109 @property
1110 def expired(self):
1111 if self.expires == -1:
1112 return False
1113 return time.time() > self.expires
1114
1115 @classmethod
1116 def _get_role_name(cls, role):
1117 return {
1118 cls.ROLE_ALL: _('all'),
1119 cls.ROLE_HTTP: _('http/web interface'),
1120 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1121 cls.ROLE_API: _('api calls'),
1122 cls.ROLE_FEED: _('feed access'),
1123 }.get(role, role)
1124
1125 @property
1126 def role_humanized(self):
1127 return self._get_role_name(self.role)
1128
1129 def _get_scope(self):
1130 if self.repo:
1131 return repr(self.repo)
1132 if self.repo_group:
1133 return repr(self.repo_group) + ' (recursive)'
1134 return 'global'
1135
1136 @property
1137 def scope_humanized(self):
1138 return self._get_scope()
1139
1140 @property
1141 def token_obfuscated(self):
1142 if self.api_key:
1143 return self.api_key[:4] + "****"
1144
1145
1146 class UserEmailMap(Base, BaseModel):
1147 __tablename__ = 'user_email_map'
1148 __table_args__ = (
1149 Index('uem_email_idx', 'email'),
1150 UniqueConstraint('email'),
1151 base_table_args
1152 )
1153 __mapper_args__ = {}
1154
1155 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1156 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1157 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1158 user = relationship('User', lazy='joined')
1159
1160 @validates('_email')
1161 def validate_email(self, key, email):
1162 # check if this email is not main one
1163 main_email = Session().query(User).filter(User.email == email).scalar()
1164 if main_email is not None:
1165 raise AttributeError('email %s is present is user table' % email)
1166 return email
1167
1168 @hybrid_property
1169 def email(self):
1170 return self._email
1171
1172 @email.setter
1173 def email(self, val):
1174 self._email = val.lower() if val else None
1175
1176
1177 class UserIpMap(Base, BaseModel):
1178 __tablename__ = 'user_ip_map'
1179 __table_args__ = (
1180 UniqueConstraint('user_id', 'ip_addr'),
1181 base_table_args
1182 )
1183 __mapper_args__ = {}
1184
1185 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1186 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1187 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1188 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1189 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1190 user = relationship('User', lazy='joined')
1191
1192 @hybrid_property
1193 def description_safe(self):
1194 from rhodecode.lib import helpers as h
1195 return h.escape(self.description)
1196
1197 @classmethod
1198 def _get_ip_range(cls, ip_addr):
1199 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1200 return [str(net.network_address), str(net.broadcast_address)]
1201
1202 def __json__(self):
1203 return {
1204 'ip_addr': self.ip_addr,
1205 'ip_range': self._get_ip_range(self.ip_addr),
1206 }
1207
1208 def __unicode__(self):
1209 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1210 self.user_id, self.ip_addr)
1211
1212
1213 class UserSshKeys(Base, BaseModel):
1214 __tablename__ = 'user_ssh_keys'
1215 __table_args__ = (
1216 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1217
1218 UniqueConstraint('ssh_key_fingerprint'),
1219
1220 base_table_args
1221 )
1222 __mapper_args__ = {}
1223
1224 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1225 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1226 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1227
1228 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1229
1230 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1231 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1232 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1233
1234 user = relationship('User', lazy='joined')
1235
1236 def __json__(self):
1237 data = {
1238 'ssh_fingerprint': self.ssh_key_fingerprint,
1239 'description': self.description,
1240 'created_on': self.created_on
1241 }
1242 return data
1243
1244 def get_api_data(self):
1245 data = self.__json__()
1246 return data
1247
1248
1249 class UserLog(Base, BaseModel):
1250 __tablename__ = 'user_logs'
1251 __table_args__ = (
1252 base_table_args,
1253 )
1254
1255 VERSION_1 = 'v1'
1256 VERSION_2 = 'v2'
1257 VERSIONS = [VERSION_1, VERSION_2]
1258
1259 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1260 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1261 username = Column("username", String(255), nullable=True, unique=None, default=None)
1262 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1263 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1264 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1265 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1266 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1267
1268 version = Column("version", String(255), nullable=True, default=VERSION_1)
1269 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1270 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1271
1272 def __unicode__(self):
1273 return u"<%s('id:%s:%s')>" % (
1274 self.__class__.__name__, self.repository_name, self.action)
1275
1276 def __json__(self):
1277 return {
1278 'user_id': self.user_id,
1279 'username': self.username,
1280 'repository_id': self.repository_id,
1281 'repository_name': self.repository_name,
1282 'user_ip': self.user_ip,
1283 'action_date': self.action_date,
1284 'action': self.action,
1285 }
1286
1287 @hybrid_property
1288 def entry_id(self):
1289 return self.user_log_id
1290
1291 @property
1292 def action_as_day(self):
1293 return datetime.date(*self.action_date.timetuple()[:3])
1294
1295 user = relationship('User')
1296 repository = relationship('Repository', cascade='')
1297
1298
1299 class UserGroup(Base, BaseModel):
1300 __tablename__ = 'users_groups'
1301 __table_args__ = (
1302 base_table_args,
1303 )
1304
1305 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1306 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1307 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1308 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1309 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1310 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1311 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1312 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1313
1314 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1315 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1316 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1317 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1318 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1319 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1320
1321 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1322 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1323
1324 @classmethod
1325 def _load_group_data(cls, column):
1326 if not column:
1327 return {}
1328
1329 try:
1330 return json.loads(column) or {}
1331 except TypeError:
1332 return {}
1333
1334 @hybrid_property
1335 def description_safe(self):
1336 from rhodecode.lib import helpers as h
1337 return h.escape(self.user_group_description)
1338
1339 @hybrid_property
1340 def group_data(self):
1341 return self._load_group_data(self._group_data)
1342
1343 @group_data.expression
1344 def group_data(self, **kwargs):
1345 return self._group_data
1346
1347 @group_data.setter
1348 def group_data(self, val):
1349 try:
1350 self._group_data = json.dumps(val)
1351 except Exception:
1352 log.error(traceback.format_exc())
1353
1354 @classmethod
1355 def _load_sync(cls, group_data):
1356 if group_data:
1357 return group_data.get('extern_type')
1358
1359 @property
1360 def sync(self):
1361 return self._load_sync(self.group_data)
1362
1363 def __unicode__(self):
1364 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1365 self.users_group_id,
1366 self.users_group_name)
1367
1368 @classmethod
1369 def get_by_group_name(cls, group_name, cache=False,
1370 case_insensitive=False):
1371 if case_insensitive:
1372 q = cls.query().filter(func.lower(cls.users_group_name) ==
1373 func.lower(group_name))
1374
1375 else:
1376 q = cls.query().filter(cls.users_group_name == group_name)
1377 if cache:
1378 q = q.options(
1379 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1380 return q.scalar()
1381
1382 @classmethod
1383 def get(cls, user_group_id, cache=False):
1384 if not user_group_id:
1385 return
1386
1387 user_group = cls.query()
1388 if cache:
1389 user_group = user_group.options(
1390 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1391 return user_group.get(user_group_id)
1392
1393 def permissions(self, with_admins=True, with_owner=True):
1394 """
1395 Permissions for user groups
1396 """
1397 _admin_perm = 'usergroup.admin'
1398
1399 owner_row = []
1400 if with_owner:
1401 usr = AttributeDict(self.user.get_dict())
1402 usr.owner_row = True
1403 usr.permission = _admin_perm
1404 owner_row.append(usr)
1405
1406 super_admin_ids = []
1407 super_admin_rows = []
1408 if with_admins:
1409 for usr in User.get_all_super_admins():
1410 super_admin_ids.append(usr.user_id)
1411 # if this admin is also owner, don't double the record
1412 if usr.user_id == owner_row[0].user_id:
1413 owner_row[0].admin_row = True
1414 else:
1415 usr = AttributeDict(usr.get_dict())
1416 usr.admin_row = True
1417 usr.permission = _admin_perm
1418 super_admin_rows.append(usr)
1419
1420 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1421 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1422 joinedload(UserUserGroupToPerm.user),
1423 joinedload(UserUserGroupToPerm.permission),)
1424
1425 # get owners and admins and permissions. We do a trick of re-writing
1426 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1427 # has a global reference and changing one object propagates to all
1428 # others. This means if admin is also an owner admin_row that change
1429 # would propagate to both objects
1430 perm_rows = []
1431 for _usr in q.all():
1432 usr = AttributeDict(_usr.user.get_dict())
1433 # if this user is also owner/admin, mark as duplicate record
1434 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1435 usr.duplicate_perm = True
1436 usr.permission = _usr.permission.permission_name
1437 perm_rows.append(usr)
1438
1439 # filter the perm rows by 'default' first and then sort them by
1440 # admin,write,read,none permissions sorted again alphabetically in
1441 # each group
1442 perm_rows = sorted(perm_rows, key=display_user_sort)
1443
1444 return super_admin_rows + owner_row + perm_rows
1445
1446 def permission_user_groups(self):
1447 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1448 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1449 joinedload(UserGroupUserGroupToPerm.target_user_group),
1450 joinedload(UserGroupUserGroupToPerm.permission),)
1451
1452 perm_rows = []
1453 for _user_group in q.all():
1454 usr = AttributeDict(_user_group.user_group.get_dict())
1455 usr.permission = _user_group.permission.permission_name
1456 perm_rows.append(usr)
1457
1458 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1459 return perm_rows
1460
1461 def _get_default_perms(self, user_group, suffix=''):
1462 from rhodecode.model.permission import PermissionModel
1463 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1464
1465 def get_default_perms(self, suffix=''):
1466 return self._get_default_perms(self, suffix)
1467
1468 def get_api_data(self, with_group_members=True, include_secrets=False):
1469 """
1470 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1471 basically forwarded.
1472
1473 """
1474 user_group = self
1475 data = {
1476 'users_group_id': user_group.users_group_id,
1477 'group_name': user_group.users_group_name,
1478 'group_description': user_group.user_group_description,
1479 'active': user_group.users_group_active,
1480 'owner': user_group.user.username,
1481 'sync': user_group.sync,
1482 'owner_email': user_group.user.email,
1483 }
1484
1485 if with_group_members:
1486 users = []
1487 for user in user_group.members:
1488 user = user.user
1489 users.append(user.get_api_data(include_secrets=include_secrets))
1490 data['users'] = users
1491
1492 return data
1493
1494
1495 class UserGroupMember(Base, BaseModel):
1496 __tablename__ = 'users_groups_members'
1497 __table_args__ = (
1498 base_table_args,
1499 )
1500
1501 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1502 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1503 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1504
1505 user = relationship('User', lazy='joined')
1506 users_group = relationship('UserGroup')
1507
1508 def __init__(self, gr_id='', u_id=''):
1509 self.users_group_id = gr_id
1510 self.user_id = u_id
1511
1512
1513 class RepositoryField(Base, BaseModel):
1514 __tablename__ = 'repositories_fields'
1515 __table_args__ = (
1516 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1517 base_table_args,
1518 )
1519
1520 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1521
1522 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1524 field_key = Column("field_key", String(250))
1525 field_label = Column("field_label", String(1024), nullable=False)
1526 field_value = Column("field_value", String(10000), nullable=False)
1527 field_desc = Column("field_desc", String(1024), nullable=False)
1528 field_type = Column("field_type", String(255), nullable=False, unique=None)
1529 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1530
1531 repository = relationship('Repository')
1532
1533 @property
1534 def field_key_prefixed(self):
1535 return 'ex_%s' % self.field_key
1536
1537 @classmethod
1538 def un_prefix_key(cls, key):
1539 if key.startswith(cls.PREFIX):
1540 return key[len(cls.PREFIX):]
1541 return key
1542
1543 @classmethod
1544 def get_by_key_name(cls, key, repo):
1545 row = cls.query()\
1546 .filter(cls.repository == repo)\
1547 .filter(cls.field_key == key).scalar()
1548 return row
1549
1550
1551 class Repository(Base, BaseModel):
1552 __tablename__ = 'repositories'
1553 __table_args__ = (
1554 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1555 base_table_args,
1556 )
1557 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1558 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1559 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1560
1561 STATE_CREATED = 'repo_state_created'
1562 STATE_PENDING = 'repo_state_pending'
1563 STATE_ERROR = 'repo_state_error'
1564
1565 LOCK_AUTOMATIC = 'lock_auto'
1566 LOCK_API = 'lock_api'
1567 LOCK_WEB = 'lock_web'
1568 LOCK_PULL = 'lock_pull'
1569
1570 NAME_SEP = URL_SEP
1571
1572 repo_id = Column(
1573 "repo_id", Integer(), nullable=False, unique=True, default=None,
1574 primary_key=True)
1575 _repo_name = Column(
1576 "repo_name", Text(), nullable=False, default=None)
1577 _repo_name_hash = Column(
1578 "repo_name_hash", String(255), nullable=False, unique=True)
1579 repo_state = Column("repo_state", String(255), nullable=True)
1580
1581 clone_uri = Column(
1582 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1583 default=None)
1584 push_uri = Column(
1585 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1586 default=None)
1587 repo_type = Column(
1588 "repo_type", String(255), nullable=False, unique=False, default=None)
1589 user_id = Column(
1590 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1591 unique=False, default=None)
1592 private = Column(
1593 "private", Boolean(), nullable=True, unique=None, default=None)
1594 archived = Column(
1595 "archived", Boolean(), nullable=True, unique=None, default=None)
1596 enable_statistics = Column(
1597 "statistics", Boolean(), nullable=True, unique=None, default=True)
1598 enable_downloads = Column(
1599 "downloads", Boolean(), nullable=True, unique=None, default=True)
1600 description = Column(
1601 "description", String(10000), nullable=True, unique=None, default=None)
1602 created_on = Column(
1603 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1604 default=datetime.datetime.now)
1605 updated_on = Column(
1606 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1607 default=datetime.datetime.now)
1608 _landing_revision = Column(
1609 "landing_revision", String(255), nullable=False, unique=False,
1610 default=None)
1611 enable_locking = Column(
1612 "enable_locking", Boolean(), nullable=False, unique=None,
1613 default=False)
1614 _locked = Column(
1615 "locked", String(255), nullable=True, unique=False, default=None)
1616 _changeset_cache = Column(
1617 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1618
1619 fork_id = Column(
1620 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1621 nullable=True, unique=False, default=None)
1622 group_id = Column(
1623 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1624 unique=False, default=None)
1625
1626 user = relationship('User', lazy='joined')
1627 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1628 group = relationship('RepoGroup', lazy='joined')
1629 repo_to_perm = relationship(
1630 'UserRepoToPerm', cascade='all',
1631 order_by='UserRepoToPerm.repo_to_perm_id')
1632 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1633 stats = relationship('Statistics', cascade='all', uselist=False)
1634
1635 followers = relationship(
1636 'UserFollowing',
1637 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1638 cascade='all')
1639 extra_fields = relationship(
1640 'RepositoryField', cascade="all, delete, delete-orphan")
1641 logs = relationship('UserLog')
1642 comments = relationship(
1643 'ChangesetComment', cascade="all, delete, delete-orphan")
1644 pull_requests_source = relationship(
1645 'PullRequest',
1646 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1647 cascade="all, delete, delete-orphan")
1648 pull_requests_target = relationship(
1649 'PullRequest',
1650 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1651 cascade="all, delete, delete-orphan")
1652 ui = relationship('RepoRhodeCodeUi', cascade="all")
1653 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1654 integrations = relationship('Integration',
1655 cascade="all, delete, delete-orphan")
1656
1657 scoped_tokens = relationship('UserApiKeys', cascade="all")
1658
1659 def __unicode__(self):
1660 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1661 safe_unicode(self.repo_name))
1662
1663 @hybrid_property
1664 def description_safe(self):
1665 from rhodecode.lib import helpers as h
1666 return h.escape(self.description)
1667
1668 @hybrid_property
1669 def landing_rev(self):
1670 # always should return [rev_type, rev]
1671 if self._landing_revision:
1672 _rev_info = self._landing_revision.split(':')
1673 if len(_rev_info) < 2:
1674 _rev_info.insert(0, 'rev')
1675 return [_rev_info[0], _rev_info[1]]
1676 return [None, None]
1677
1678 @landing_rev.setter
1679 def landing_rev(self, val):
1680 if ':' not in val:
1681 raise ValueError('value must be delimited with `:` and consist '
1682 'of <rev_type>:<rev>, got %s instead' % val)
1683 self._landing_revision = val
1684
1685 @hybrid_property
1686 def locked(self):
1687 if self._locked:
1688 user_id, timelocked, reason = self._locked.split(':')
1689 lock_values = int(user_id), timelocked, reason
1690 else:
1691 lock_values = [None, None, None]
1692 return lock_values
1693
1694 @locked.setter
1695 def locked(self, val):
1696 if val and isinstance(val, (list, tuple)):
1697 self._locked = ':'.join(map(str, val))
1698 else:
1699 self._locked = None
1700
1701 @hybrid_property
1702 def changeset_cache(self):
1703 from rhodecode.lib.vcs.backends.base import EmptyCommit
1704 dummy = EmptyCommit().__json__()
1705 if not self._changeset_cache:
1706 return dummy
1707 try:
1708 return json.loads(self._changeset_cache)
1709 except TypeError:
1710 return dummy
1711 except Exception:
1712 log.error(traceback.format_exc())
1713 return dummy
1714
1715 @changeset_cache.setter
1716 def changeset_cache(self, val):
1717 try:
1718 self._changeset_cache = json.dumps(val)
1719 except Exception:
1720 log.error(traceback.format_exc())
1721
1722 @hybrid_property
1723 def repo_name(self):
1724 return self._repo_name
1725
1726 @repo_name.setter
1727 def repo_name(self, value):
1728 self._repo_name = value
1729 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1730
1731 @classmethod
1732 def normalize_repo_name(cls, repo_name):
1733 """
1734 Normalizes os specific repo_name to the format internally stored inside
1735 database using URL_SEP
1736
1737 :param cls:
1738 :param repo_name:
1739 """
1740 return cls.NAME_SEP.join(repo_name.split(os.sep))
1741
1742 @classmethod
1743 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1744 session = Session()
1745 q = session.query(cls).filter(cls.repo_name == repo_name)
1746
1747 if cache:
1748 if identity_cache:
1749 val = cls.identity_cache(session, 'repo_name', repo_name)
1750 if val:
1751 return val
1752 else:
1753 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1754 q = q.options(
1755 FromCache("sql_cache_short", cache_key))
1756
1757 return q.scalar()
1758
1759 @classmethod
1760 def get_by_id_or_repo_name(cls, repoid):
1761 if isinstance(repoid, (int, long)):
1762 try:
1763 repo = cls.get(repoid)
1764 except ValueError:
1765 repo = None
1766 else:
1767 repo = cls.get_by_repo_name(repoid)
1768 return repo
1769
1770 @classmethod
1771 def get_by_full_path(cls, repo_full_path):
1772 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1773 repo_name = cls.normalize_repo_name(repo_name)
1774 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1775
1776 @classmethod
1777 def get_repo_forks(cls, repo_id):
1778 return cls.query().filter(Repository.fork_id == repo_id)
1779
1780 @classmethod
1781 def base_path(cls):
1782 """
1783 Returns base path when all repos are stored
1784
1785 :param cls:
1786 """
1787 q = Session().query(RhodeCodeUi)\
1788 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1789 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1790 return q.one().ui_value
1791
1792 @classmethod
1793 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1794 case_insensitive=True, archived=False):
1795 q = Repository.query()
1796
1797 if not archived:
1798 q = q.filter(Repository.archived.isnot(true()))
1799
1800 if not isinstance(user_id, Optional):
1801 q = q.filter(Repository.user_id == user_id)
1802
1803 if not isinstance(group_id, Optional):
1804 q = q.filter(Repository.group_id == group_id)
1805
1806 if case_insensitive:
1807 q = q.order_by(func.lower(Repository.repo_name))
1808 else:
1809 q = q.order_by(Repository.repo_name)
1810
1811 return q.all()
1812
1813 @property
1814 def forks(self):
1815 """
1816 Return forks of this repo
1817 """
1818 return Repository.get_repo_forks(self.repo_id)
1819
1820 @property
1821 def parent(self):
1822 """
1823 Returns fork parent
1824 """
1825 return self.fork
1826
1827 @property
1828 def just_name(self):
1829 return self.repo_name.split(self.NAME_SEP)[-1]
1830
1831 @property
1832 def groups_with_parents(self):
1833 groups = []
1834 if self.group is None:
1835 return groups
1836
1837 cur_gr = self.group
1838 groups.insert(0, cur_gr)
1839 while 1:
1840 gr = getattr(cur_gr, 'parent_group', None)
1841 cur_gr = cur_gr.parent_group
1842 if gr is None:
1843 break
1844 groups.insert(0, gr)
1845
1846 return groups
1847
1848 @property
1849 def groups_and_repo(self):
1850 return self.groups_with_parents, self
1851
1852 @LazyProperty
1853 def repo_path(self):
1854 """
1855 Returns base full path for that repository means where it actually
1856 exists on a filesystem
1857 """
1858 q = Session().query(RhodeCodeUi).filter(
1859 RhodeCodeUi.ui_key == self.NAME_SEP)
1860 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1861 return q.one().ui_value
1862
1863 @property
1864 def repo_full_path(self):
1865 p = [self.repo_path]
1866 # we need to split the name by / since this is how we store the
1867 # names in the database, but that eventually needs to be converted
1868 # into a valid system path
1869 p += self.repo_name.split(self.NAME_SEP)
1870 return os.path.join(*map(safe_unicode, p))
1871
1872 @property
1873 def cache_keys(self):
1874 """
1875 Returns associated cache keys for that repo
1876 """
1877 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1878 repo_id=self.repo_id)
1879 return CacheKey.query()\
1880 .filter(CacheKey.cache_args == invalidation_namespace)\
1881 .order_by(CacheKey.cache_key)\
1882 .all()
1883
1884 @property
1885 def cached_diffs_relative_dir(self):
1886 """
1887 Return a relative to the repository store path of cached diffs
1888 used for safe display for users, who shouldn't know the absolute store
1889 path
1890 """
1891 return os.path.join(
1892 os.path.dirname(self.repo_name),
1893 self.cached_diffs_dir.split(os.path.sep)[-1])
1894
1895 @property
1896 def cached_diffs_dir(self):
1897 path = self.repo_full_path
1898 return os.path.join(
1899 os.path.dirname(path),
1900 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1901
1902 def cached_diffs(self):
1903 diff_cache_dir = self.cached_diffs_dir
1904 if os.path.isdir(diff_cache_dir):
1905 return os.listdir(diff_cache_dir)
1906 return []
1907
1908 def shadow_repos(self):
1909 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
1910 return [
1911 x for x in os.listdir(os.path.dirname(self.repo_full_path))
1912 if x.startswith(shadow_repos_pattern)]
1913
1914 def get_new_name(self, repo_name):
1915 """
1916 returns new full repository name based on assigned group and new new
1917
1918 :param group_name:
1919 """
1920 path_prefix = self.group.full_path_splitted if self.group else []
1921 return self.NAME_SEP.join(path_prefix + [repo_name])
1922
1923 @property
1924 def _config(self):
1925 """
1926 Returns db based config object.
1927 """
1928 from rhodecode.lib.utils import make_db_config
1929 return make_db_config(clear_session=False, repo=self)
1930
1931 def permissions(self, with_admins=True, with_owner=True):
1932 """
1933 Permissions for repositories
1934 """
1935 _admin_perm = 'repository.admin'
1936
1937 owner_row = []
1938 if with_owner:
1939 usr = AttributeDict(self.user.get_dict())
1940 usr.owner_row = True
1941 usr.permission = _admin_perm
1942 usr.permission_id = None
1943 owner_row.append(usr)
1944
1945 super_admin_ids = []
1946 super_admin_rows = []
1947 if with_admins:
1948 for usr in User.get_all_super_admins():
1949 super_admin_ids.append(usr.user_id)
1950 # if this admin is also owner, don't double the record
1951 if usr.user_id == owner_row[0].user_id:
1952 owner_row[0].admin_row = True
1953 else:
1954 usr = AttributeDict(usr.get_dict())
1955 usr.admin_row = True
1956 usr.permission = _admin_perm
1957 usr.permission_id = None
1958 super_admin_rows.append(usr)
1959
1960 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1961 q = q.options(joinedload(UserRepoToPerm.repository),
1962 joinedload(UserRepoToPerm.user),
1963 joinedload(UserRepoToPerm.permission),)
1964
1965 # get owners and admins and permissions. We do a trick of re-writing
1966 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1967 # has a global reference and changing one object propagates to all
1968 # others. This means if admin is also an owner admin_row that change
1969 # would propagate to both objects
1970 perm_rows = []
1971 for _usr in q.all():
1972 usr = AttributeDict(_usr.user.get_dict())
1973 # if this user is also owner/admin, mark as duplicate record
1974 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1975 usr.duplicate_perm = True
1976 # also check if this permission is maybe used by branch_permissions
1977 if _usr.branch_perm_entry:
1978 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
1979
1980 usr.permission = _usr.permission.permission_name
1981 usr.permission_id = _usr.repo_to_perm_id
1982 perm_rows.append(usr)
1983
1984 # filter the perm rows by 'default' first and then sort them by
1985 # admin,write,read,none permissions sorted again alphabetically in
1986 # each group
1987 perm_rows = sorted(perm_rows, key=display_user_sort)
1988
1989 return super_admin_rows + owner_row + perm_rows
1990
1991 def permission_user_groups(self):
1992 q = UserGroupRepoToPerm.query().filter(
1993 UserGroupRepoToPerm.repository == self)
1994 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1995 joinedload(UserGroupRepoToPerm.users_group),
1996 joinedload(UserGroupRepoToPerm.permission),)
1997
1998 perm_rows = []
1999 for _user_group in q.all():
2000 usr = AttributeDict(_user_group.users_group.get_dict())
2001 usr.permission = _user_group.permission.permission_name
2002 perm_rows.append(usr)
2003
2004 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2005 return perm_rows
2006
2007 def get_api_data(self, include_secrets=False):
2008 """
2009 Common function for generating repo api data
2010
2011 :param include_secrets: See :meth:`User.get_api_data`.
2012
2013 """
2014 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2015 # move this methods on models level.
2016 from rhodecode.model.settings import SettingsModel
2017 from rhodecode.model.repo import RepoModel
2018
2019 repo = self
2020 _user_id, _time, _reason = self.locked
2021
2022 data = {
2023 'repo_id': repo.repo_id,
2024 'repo_name': repo.repo_name,
2025 'repo_type': repo.repo_type,
2026 'clone_uri': repo.clone_uri or '',
2027 'push_uri': repo.push_uri or '',
2028 'url': RepoModel().get_url(self),
2029 'private': repo.private,
2030 'created_on': repo.created_on,
2031 'description': repo.description_safe,
2032 'landing_rev': repo.landing_rev,
2033 'owner': repo.user.username,
2034 'fork_of': repo.fork.repo_name if repo.fork else None,
2035 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2036 'enable_statistics': repo.enable_statistics,
2037 'enable_locking': repo.enable_locking,
2038 'enable_downloads': repo.enable_downloads,
2039 'last_changeset': repo.changeset_cache,
2040 'locked_by': User.get(_user_id).get_api_data(
2041 include_secrets=include_secrets) if _user_id else None,
2042 'locked_date': time_to_datetime(_time) if _time else None,
2043 'lock_reason': _reason if _reason else None,
2044 }
2045
2046 # TODO: mikhail: should be per-repo settings here
2047 rc_config = SettingsModel().get_all_settings()
2048 repository_fields = str2bool(
2049 rc_config.get('rhodecode_repository_fields'))
2050 if repository_fields:
2051 for f in self.extra_fields:
2052 data[f.field_key_prefixed] = f.field_value
2053
2054 return data
2055
2056 @classmethod
2057 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2058 if not lock_time:
2059 lock_time = time.time()
2060 if not lock_reason:
2061 lock_reason = cls.LOCK_AUTOMATIC
2062 repo.locked = [user_id, lock_time, lock_reason]
2063 Session().add(repo)
2064 Session().commit()
2065
2066 @classmethod
2067 def unlock(cls, repo):
2068 repo.locked = None
2069 Session().add(repo)
2070 Session().commit()
2071
2072 @classmethod
2073 def getlock(cls, repo):
2074 return repo.locked
2075
2076 def is_user_lock(self, user_id):
2077 if self.lock[0]:
2078 lock_user_id = safe_int(self.lock[0])
2079 user_id = safe_int(user_id)
2080 # both are ints, and they are equal
2081 return all([lock_user_id, user_id]) and lock_user_id == user_id
2082
2083 return False
2084
2085 def get_locking_state(self, action, user_id, only_when_enabled=True):
2086 """
2087 Checks locking on this repository, if locking is enabled and lock is
2088 present returns a tuple of make_lock, locked, locked_by.
2089 make_lock can have 3 states None (do nothing) True, make lock
2090 False release lock, This value is later propagated to hooks, which
2091 do the locking. Think about this as signals passed to hooks what to do.
2092
2093 """
2094 # TODO: johbo: This is part of the business logic and should be moved
2095 # into the RepositoryModel.
2096
2097 if action not in ('push', 'pull'):
2098 raise ValueError("Invalid action value: %s" % repr(action))
2099
2100 # defines if locked error should be thrown to user
2101 currently_locked = False
2102 # defines if new lock should be made, tri-state
2103 make_lock = None
2104 repo = self
2105 user = User.get(user_id)
2106
2107 lock_info = repo.locked
2108
2109 if repo and (repo.enable_locking or not only_when_enabled):
2110 if action == 'push':
2111 # check if it's already locked !, if it is compare users
2112 locked_by_user_id = lock_info[0]
2113 if user.user_id == locked_by_user_id:
2114 log.debug(
2115 'Got `push` action from user %s, now unlocking', user)
2116 # unlock if we have push from user who locked
2117 make_lock = False
2118 else:
2119 # we're not the same user who locked, ban with
2120 # code defined in settings (default is 423 HTTP Locked) !
2121 log.debug('Repo %s is currently locked by %s', repo, user)
2122 currently_locked = True
2123 elif action == 'pull':
2124 # [0] user [1] date
2125 if lock_info[0] and lock_info[1]:
2126 log.debug('Repo %s is currently locked by %s', repo, user)
2127 currently_locked = True
2128 else:
2129 log.debug('Setting lock on repo %s by %s', repo, user)
2130 make_lock = True
2131
2132 else:
2133 log.debug('Repository %s do not have locking enabled', repo)
2134
2135 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2136 make_lock, currently_locked, lock_info)
2137
2138 from rhodecode.lib.auth import HasRepoPermissionAny
2139 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2140 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2141 # if we don't have at least write permission we cannot make a lock
2142 log.debug('lock state reset back to FALSE due to lack '
2143 'of at least read permission')
2144 make_lock = False
2145
2146 return make_lock, currently_locked, lock_info
2147
2148 @property
2149 def last_db_change(self):
2150 return self.updated_on
2151
2152 @property
2153 def clone_uri_hidden(self):
2154 clone_uri = self.clone_uri
2155 if clone_uri:
2156 import urlobject
2157 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2158 if url_obj.password:
2159 clone_uri = url_obj.with_password('*****')
2160 return clone_uri
2161
2162 @property
2163 def push_uri_hidden(self):
2164 push_uri = self.push_uri
2165 if push_uri:
2166 import urlobject
2167 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2168 if url_obj.password:
2169 push_uri = url_obj.with_password('*****')
2170 return push_uri
2171
2172 def clone_url(self, **override):
2173 from rhodecode.model.settings import SettingsModel
2174
2175 uri_tmpl = None
2176 if 'with_id' in override:
2177 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2178 del override['with_id']
2179
2180 if 'uri_tmpl' in override:
2181 uri_tmpl = override['uri_tmpl']
2182 del override['uri_tmpl']
2183
2184 ssh = False
2185 if 'ssh' in override:
2186 ssh = True
2187 del override['ssh']
2188
2189 # we didn't override our tmpl from **overrides
2190 if not uri_tmpl:
2191 rc_config = SettingsModel().get_all_settings(cache=True)
2192 if ssh:
2193 uri_tmpl = rc_config.get(
2194 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2195 else:
2196 uri_tmpl = rc_config.get(
2197 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2198
2199 request = get_current_request()
2200 return get_clone_url(request=request,
2201 uri_tmpl=uri_tmpl,
2202 repo_name=self.repo_name,
2203 repo_id=self.repo_id, **override)
2204
2205 def set_state(self, state):
2206 self.repo_state = state
2207 Session().add(self)
2208 #==========================================================================
2209 # SCM PROPERTIES
2210 #==========================================================================
2211
2212 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2213 return get_commit_safe(
2214 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2215
2216 def get_changeset(self, rev=None, pre_load=None):
2217 warnings.warn("Use get_commit", DeprecationWarning)
2218 commit_id = None
2219 commit_idx = None
2220 if isinstance(rev, compat.string_types):
2221 commit_id = rev
2222 else:
2223 commit_idx = rev
2224 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2225 pre_load=pre_load)
2226
2227 def get_landing_commit(self):
2228 """
2229 Returns landing commit, or if that doesn't exist returns the tip
2230 """
2231 _rev_type, _rev = self.landing_rev
2232 commit = self.get_commit(_rev)
2233 if isinstance(commit, EmptyCommit):
2234 return self.get_commit()
2235 return commit
2236
2237 def update_commit_cache(self, cs_cache=None, config=None):
2238 """
2239 Update cache of last changeset for repository, keys should be::
2240
2241 short_id
2242 raw_id
2243 revision
2244 parents
2245 message
2246 date
2247 author
2248
2249 :param cs_cache:
2250 """
2251 from rhodecode.lib.vcs.backends.base import BaseChangeset
2252 if cs_cache is None:
2253 # use no-cache version here
2254 scm_repo = self.scm_instance(cache=False, config=config)
2255
2256 empty = scm_repo.is_empty()
2257 if not empty:
2258 cs_cache = scm_repo.get_commit(
2259 pre_load=["author", "date", "message", "parents"])
2260 else:
2261 cs_cache = EmptyCommit()
2262
2263 if isinstance(cs_cache, BaseChangeset):
2264 cs_cache = cs_cache.__json__()
2265
2266 def is_outdated(new_cs_cache):
2267 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2268 new_cs_cache['revision'] != self.changeset_cache['revision']):
2269 return True
2270 return False
2271
2272 # check if we have maybe already latest cached revision
2273 if is_outdated(cs_cache) or not self.changeset_cache:
2274 _default = datetime.datetime.utcnow()
2275 last_change = cs_cache.get('date') or _default
2276 if self.updated_on and self.updated_on > last_change:
2277 # we check if last update is newer than the new value
2278 # if yes, we use the current timestamp instead. Imagine you get
2279 # old commit pushed 1y ago, we'd set last update 1y to ago.
2280 last_change = _default
2281 log.debug('updated repo %s with new cs cache %s',
2282 self.repo_name, cs_cache)
2283 self.updated_on = last_change
2284 self.changeset_cache = cs_cache
2285 Session().add(self)
2286 Session().commit()
2287 else:
2288 log.debug('Skipping update_commit_cache for repo:`%s` '
2289 'commit already with latest changes', self.repo_name)
2290
2291 @property
2292 def tip(self):
2293 return self.get_commit('tip')
2294
2295 @property
2296 def author(self):
2297 return self.tip.author
2298
2299 @property
2300 def last_change(self):
2301 return self.scm_instance().last_change
2302
2303 def get_comments(self, revisions=None):
2304 """
2305 Returns comments for this repository grouped by revisions
2306
2307 :param revisions: filter query by revisions only
2308 """
2309 cmts = ChangesetComment.query()\
2310 .filter(ChangesetComment.repo == self)
2311 if revisions:
2312 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2313 grouped = collections.defaultdict(list)
2314 for cmt in cmts.all():
2315 grouped[cmt.revision].append(cmt)
2316 return grouped
2317
2318 def statuses(self, revisions=None):
2319 """
2320 Returns statuses for this repository
2321
2322 :param revisions: list of revisions to get statuses for
2323 """
2324 statuses = ChangesetStatus.query()\
2325 .filter(ChangesetStatus.repo == self)\
2326 .filter(ChangesetStatus.version == 0)
2327
2328 if revisions:
2329 # Try doing the filtering in chunks to avoid hitting limits
2330 size = 500
2331 status_results = []
2332 for chunk in xrange(0, len(revisions), size):
2333 status_results += statuses.filter(
2334 ChangesetStatus.revision.in_(
2335 revisions[chunk: chunk+size])
2336 ).all()
2337 else:
2338 status_results = statuses.all()
2339
2340 grouped = {}
2341
2342 # maybe we have open new pullrequest without a status?
2343 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2344 status_lbl = ChangesetStatus.get_status_lbl(stat)
2345 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2346 for rev in pr.revisions:
2347 pr_id = pr.pull_request_id
2348 pr_repo = pr.target_repo.repo_name
2349 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2350
2351 for stat in status_results:
2352 pr_id = pr_repo = None
2353 if stat.pull_request:
2354 pr_id = stat.pull_request.pull_request_id
2355 pr_repo = stat.pull_request.target_repo.repo_name
2356 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2357 pr_id, pr_repo]
2358 return grouped
2359
2360 # ==========================================================================
2361 # SCM CACHE INSTANCE
2362 # ==========================================================================
2363
2364 def scm_instance(self, **kwargs):
2365 import rhodecode
2366
2367 # Passing a config will not hit the cache currently only used
2368 # for repo2dbmapper
2369 config = kwargs.pop('config', None)
2370 cache = kwargs.pop('cache', None)
2371 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2372 # if cache is NOT defined use default global, else we have a full
2373 # control over cache behaviour
2374 if cache is None and full_cache and not config:
2375 return self._get_instance_cached()
2376 return self._get_instance(cache=bool(cache), config=config)
2377
2378 def _get_instance_cached(self):
2379 from rhodecode.lib import rc_cache
2380
2381 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2382 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2383 repo_id=self.repo_id)
2384 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2385
2386 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2387 def get_instance_cached(repo_id, context_id):
2388 return self._get_instance()
2389
2390 # we must use thread scoped cache here,
2391 # because each thread of gevent needs it's own not shared connection and cache
2392 # we also alter `args` so the cache key is individual for every green thread.
2393 inv_context_manager = rc_cache.InvalidationContext(
2394 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2395 thread_scoped=True)
2396 with inv_context_manager as invalidation_context:
2397 args = (self.repo_id, inv_context_manager.cache_key)
2398 # re-compute and store cache if we get invalidate signal
2399 if invalidation_context.should_invalidate():
2400 instance = get_instance_cached.refresh(*args)
2401 else:
2402 instance = get_instance_cached(*args)
2403
2404 log.debug(
2405 'Repo instance fetched in %.3fs', inv_context_manager.compute_time)
2406 return instance
2407
2408 def _get_instance(self, cache=True, config=None):
2409 config = config or self._config
2410 custom_wire = {
2411 'cache': cache # controls the vcs.remote cache
2412 }
2413 repo = get_vcs_instance(
2414 repo_path=safe_str(self.repo_full_path),
2415 config=config,
2416 with_wire=custom_wire,
2417 create=False,
2418 _vcs_alias=self.repo_type)
2419
2420 return repo
2421
2422 def __json__(self):
2423 return {'landing_rev': self.landing_rev}
2424
2425 def get_dict(self):
2426
2427 # Since we transformed `repo_name` to a hybrid property, we need to
2428 # keep compatibility with the code which uses `repo_name` field.
2429
2430 result = super(Repository, self).get_dict()
2431 result['repo_name'] = result.pop('_repo_name', None)
2432 return result
2433
2434
2435 class RepoGroup(Base, BaseModel):
2436 __tablename__ = 'groups'
2437 __table_args__ = (
2438 UniqueConstraint('group_name', 'group_parent_id'),
2439 base_table_args,
2440 )
2441 __mapper_args__ = {'order_by': 'group_name'}
2442
2443 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2444
2445 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2446 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2447 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2448 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2449 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2450 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2451 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2452 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2453 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2454
2455 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2456 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2457 parent_group = relationship('RepoGroup', remote_side=group_id)
2458 user = relationship('User')
2459 integrations = relationship('Integration',
2460 cascade="all, delete, delete-orphan")
2461
2462 def __init__(self, group_name='', parent_group=None):
2463 self.group_name = group_name
2464 self.parent_group = parent_group
2465
2466 def __unicode__(self):
2467 return u"<%s('id:%s:%s')>" % (
2468 self.__class__.__name__, self.group_id, self.group_name)
2469
2470 @hybrid_property
2471 def description_safe(self):
2472 from rhodecode.lib import helpers as h
2473 return h.escape(self.group_description)
2474
2475 @classmethod
2476 def _generate_choice(cls, repo_group):
2477 from webhelpers.html import literal as _literal
2478 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2479 return repo_group.group_id, _name(repo_group.full_path_splitted)
2480
2481 @classmethod
2482 def groups_choices(cls, groups=None, show_empty_group=True):
2483 if not groups:
2484 groups = cls.query().all()
2485
2486 repo_groups = []
2487 if show_empty_group:
2488 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2489
2490 repo_groups.extend([cls._generate_choice(x) for x in groups])
2491
2492 repo_groups = sorted(
2493 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2494 return repo_groups
2495
2496 @classmethod
2497 def url_sep(cls):
2498 return URL_SEP
2499
2500 @classmethod
2501 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2502 if case_insensitive:
2503 gr = cls.query().filter(func.lower(cls.group_name)
2504 == func.lower(group_name))
2505 else:
2506 gr = cls.query().filter(cls.group_name == group_name)
2507 if cache:
2508 name_key = _hash_key(group_name)
2509 gr = gr.options(
2510 FromCache("sql_cache_short", "get_group_%s" % name_key))
2511 return gr.scalar()
2512
2513 @classmethod
2514 def get_user_personal_repo_group(cls, user_id):
2515 user = User.get(user_id)
2516 if user.username == User.DEFAULT_USER:
2517 return None
2518
2519 return cls.query()\
2520 .filter(cls.personal == true()) \
2521 .filter(cls.user == user) \
2522 .order_by(cls.group_id.asc()) \
2523 .first()
2524
2525 @classmethod
2526 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2527 case_insensitive=True):
2528 q = RepoGroup.query()
2529
2530 if not isinstance(user_id, Optional):
2531 q = q.filter(RepoGroup.user_id == user_id)
2532
2533 if not isinstance(group_id, Optional):
2534 q = q.filter(RepoGroup.group_parent_id == group_id)
2535
2536 if case_insensitive:
2537 q = q.order_by(func.lower(RepoGroup.group_name))
2538 else:
2539 q = q.order_by(RepoGroup.group_name)
2540 return q.all()
2541
2542 @property
2543 def parents(self):
2544 parents_recursion_limit = 10
2545 groups = []
2546 if self.parent_group is None:
2547 return groups
2548 cur_gr = self.parent_group
2549 groups.insert(0, cur_gr)
2550 cnt = 0
2551 while 1:
2552 cnt += 1
2553 gr = getattr(cur_gr, 'parent_group', None)
2554 cur_gr = cur_gr.parent_group
2555 if gr is None:
2556 break
2557 if cnt == parents_recursion_limit:
2558 # this will prevent accidental infinit loops
2559 log.error('more than %s parents found for group %s, stopping '
2560 'recursive parent fetching', parents_recursion_limit, self)
2561 break
2562
2563 groups.insert(0, gr)
2564 return groups
2565
2566 @property
2567 def last_db_change(self):
2568 return self.updated_on
2569
2570 @property
2571 def children(self):
2572 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2573
2574 @property
2575 def name(self):
2576 return self.group_name.split(RepoGroup.url_sep())[-1]
2577
2578 @property
2579 def full_path(self):
2580 return self.group_name
2581
2582 @property
2583 def full_path_splitted(self):
2584 return self.group_name.split(RepoGroup.url_sep())
2585
2586 @property
2587 def repositories(self):
2588 return Repository.query()\
2589 .filter(Repository.group == self)\
2590 .order_by(Repository.repo_name)
2591
2592 @property
2593 def repositories_recursive_count(self):
2594 cnt = self.repositories.count()
2595
2596 def children_count(group):
2597 cnt = 0
2598 for child in group.children:
2599 cnt += child.repositories.count()
2600 cnt += children_count(child)
2601 return cnt
2602
2603 return cnt + children_count(self)
2604
2605 def _recursive_objects(self, include_repos=True):
2606 all_ = []
2607
2608 def _get_members(root_gr):
2609 if include_repos:
2610 for r in root_gr.repositories:
2611 all_.append(r)
2612 childs = root_gr.children.all()
2613 if childs:
2614 for gr in childs:
2615 all_.append(gr)
2616 _get_members(gr)
2617
2618 _get_members(self)
2619 return [self] + all_
2620
2621 def recursive_groups_and_repos(self):
2622 """
2623 Recursive return all groups, with repositories in those groups
2624 """
2625 return self._recursive_objects()
2626
2627 def recursive_groups(self):
2628 """
2629 Returns all children groups for this group including children of children
2630 """
2631 return self._recursive_objects(include_repos=False)
2632
2633 def get_new_name(self, group_name):
2634 """
2635 returns new full group name based on parent and new name
2636
2637 :param group_name:
2638 """
2639 path_prefix = (self.parent_group.full_path_splitted if
2640 self.parent_group else [])
2641 return RepoGroup.url_sep().join(path_prefix + [group_name])
2642
2643 def permissions(self, with_admins=True, with_owner=True):
2644 """
2645 Permissions for repository groups
2646 """
2647 _admin_perm = 'group.admin'
2648
2649 owner_row = []
2650 if with_owner:
2651 usr = AttributeDict(self.user.get_dict())
2652 usr.owner_row = True
2653 usr.permission = _admin_perm
2654 owner_row.append(usr)
2655
2656 super_admin_ids = []
2657 super_admin_rows = []
2658 if with_admins:
2659 for usr in User.get_all_super_admins():
2660 super_admin_ids.append(usr.user_id)
2661 # if this admin is also owner, don't double the record
2662 if usr.user_id == owner_row[0].user_id:
2663 owner_row[0].admin_row = True
2664 else:
2665 usr = AttributeDict(usr.get_dict())
2666 usr.admin_row = True
2667 usr.permission = _admin_perm
2668 super_admin_rows.append(usr)
2669
2670 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2671 q = q.options(joinedload(UserRepoGroupToPerm.group),
2672 joinedload(UserRepoGroupToPerm.user),
2673 joinedload(UserRepoGroupToPerm.permission),)
2674
2675 # get owners and admins and permissions. We do a trick of re-writing
2676 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2677 # has a global reference and changing one object propagates to all
2678 # others. This means if admin is also an owner admin_row that change
2679 # would propagate to both objects
2680 perm_rows = []
2681 for _usr in q.all():
2682 usr = AttributeDict(_usr.user.get_dict())
2683 # if this user is also owner/admin, mark as duplicate record
2684 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2685 usr.duplicate_perm = True
2686 usr.permission = _usr.permission.permission_name
2687 perm_rows.append(usr)
2688
2689 # filter the perm rows by 'default' first and then sort them by
2690 # admin,write,read,none permissions sorted again alphabetically in
2691 # each group
2692 perm_rows = sorted(perm_rows, key=display_user_sort)
2693
2694 return super_admin_rows + owner_row + perm_rows
2695
2696 def permission_user_groups(self):
2697 q = UserGroupRepoGroupToPerm.query().filter(
2698 UserGroupRepoGroupToPerm.group == self)
2699 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2700 joinedload(UserGroupRepoGroupToPerm.users_group),
2701 joinedload(UserGroupRepoGroupToPerm.permission),)
2702
2703 perm_rows = []
2704 for _user_group in q.all():
2705 usr = AttributeDict(_user_group.users_group.get_dict())
2706 usr.permission = _user_group.permission.permission_name
2707 perm_rows.append(usr)
2708
2709 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2710 return perm_rows
2711
2712 def get_api_data(self):
2713 """
2714 Common function for generating api data
2715
2716 """
2717 group = self
2718 data = {
2719 'group_id': group.group_id,
2720 'group_name': group.group_name,
2721 'group_description': group.description_safe,
2722 'parent_group': group.parent_group.group_name if group.parent_group else None,
2723 'repositories': [x.repo_name for x in group.repositories],
2724 'owner': group.user.username,
2725 }
2726 return data
2727
2728
2729 class Permission(Base, BaseModel):
2730 __tablename__ = 'permissions'
2731 __table_args__ = (
2732 Index('p_perm_name_idx', 'permission_name'),
2733 base_table_args,
2734 )
2735
2736 PERMS = [
2737 ('hg.admin', _('RhodeCode Super Administrator')),
2738
2739 ('repository.none', _('Repository no access')),
2740 ('repository.read', _('Repository read access')),
2741 ('repository.write', _('Repository write access')),
2742 ('repository.admin', _('Repository admin access')),
2743
2744 ('group.none', _('Repository group no access')),
2745 ('group.read', _('Repository group read access')),
2746 ('group.write', _('Repository group write access')),
2747 ('group.admin', _('Repository group admin access')),
2748
2749 ('usergroup.none', _('User group no access')),
2750 ('usergroup.read', _('User group read access')),
2751 ('usergroup.write', _('User group write access')),
2752 ('usergroup.admin', _('User group admin access')),
2753
2754 ('branch.none', _('Branch no permissions')),
2755 ('branch.merge', _('Branch access by web merge')),
2756 ('branch.push', _('Branch access by push')),
2757 ('branch.push_force', _('Branch access by push with force')),
2758
2759 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2760 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2761
2762 ('hg.usergroup.create.false', _('User Group creation disabled')),
2763 ('hg.usergroup.create.true', _('User Group creation enabled')),
2764
2765 ('hg.create.none', _('Repository creation disabled')),
2766 ('hg.create.repository', _('Repository creation enabled')),
2767 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2768 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2769
2770 ('hg.fork.none', _('Repository forking disabled')),
2771 ('hg.fork.repository', _('Repository forking enabled')),
2772
2773 ('hg.register.none', _('Registration disabled')),
2774 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2775 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2776
2777 ('hg.password_reset.enabled', _('Password reset enabled')),
2778 ('hg.password_reset.hidden', _('Password reset hidden')),
2779 ('hg.password_reset.disabled', _('Password reset disabled')),
2780
2781 ('hg.extern_activate.manual', _('Manual activation of external account')),
2782 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2783
2784 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2785 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2786 ]
2787
2788 # definition of system default permissions for DEFAULT user, created on
2789 # system setup
2790 DEFAULT_USER_PERMISSIONS = [
2791 # object perms
2792 'repository.read',
2793 'group.read',
2794 'usergroup.read',
2795 # branch, for backward compat we need same value as before so forced pushed
2796 'branch.push_force',
2797 # global
2798 'hg.create.repository',
2799 'hg.repogroup.create.false',
2800 'hg.usergroup.create.false',
2801 'hg.create.write_on_repogroup.true',
2802 'hg.fork.repository',
2803 'hg.register.manual_activate',
2804 'hg.password_reset.enabled',
2805 'hg.extern_activate.auto',
2806 'hg.inherit_default_perms.true',
2807 ]
2808
2809 # defines which permissions are more important higher the more important
2810 # Weight defines which permissions are more important.
2811 # The higher number the more important.
2812 PERM_WEIGHTS = {
2813 'repository.none': 0,
2814 'repository.read': 1,
2815 'repository.write': 3,
2816 'repository.admin': 4,
2817
2818 'group.none': 0,
2819 'group.read': 1,
2820 'group.write': 3,
2821 'group.admin': 4,
2822
2823 'usergroup.none': 0,
2824 'usergroup.read': 1,
2825 'usergroup.write': 3,
2826 'usergroup.admin': 4,
2827
2828 'branch.none': 0,
2829 'branch.merge': 1,
2830 'branch.push': 3,
2831 'branch.push_force': 4,
2832
2833 'hg.repogroup.create.false': 0,
2834 'hg.repogroup.create.true': 1,
2835
2836 'hg.usergroup.create.false': 0,
2837 'hg.usergroup.create.true': 1,
2838
2839 'hg.fork.none': 0,
2840 'hg.fork.repository': 1,
2841 'hg.create.none': 0,
2842 'hg.create.repository': 1
2843 }
2844
2845 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2846 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2847 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2848
2849 def __unicode__(self):
2850 return u"<%s('%s:%s')>" % (
2851 self.__class__.__name__, self.permission_id, self.permission_name
2852 )
2853
2854 @classmethod
2855 def get_by_key(cls, key):
2856 return cls.query().filter(cls.permission_name == key).scalar()
2857
2858 @classmethod
2859 def get_default_repo_perms(cls, user_id, repo_id=None):
2860 q = Session().query(UserRepoToPerm, Repository, Permission)\
2861 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2862 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2863 .filter(UserRepoToPerm.user_id == user_id)
2864 if repo_id:
2865 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2866 return q.all()
2867
2868 @classmethod
2869 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
2870 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
2871 .join(
2872 Permission,
2873 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
2874 .join(
2875 UserRepoToPerm,
2876 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
2877 .filter(UserRepoToPerm.user_id == user_id)
2878
2879 if repo_id:
2880 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
2881 return q.order_by(UserToRepoBranchPermission.rule_order).all()
2882
2883 @classmethod
2884 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2885 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2886 .join(
2887 Permission,
2888 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2889 .join(
2890 Repository,
2891 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2892 .join(
2893 UserGroup,
2894 UserGroupRepoToPerm.users_group_id ==
2895 UserGroup.users_group_id)\
2896 .join(
2897 UserGroupMember,
2898 UserGroupRepoToPerm.users_group_id ==
2899 UserGroupMember.users_group_id)\
2900 .filter(
2901 UserGroupMember.user_id == user_id,
2902 UserGroup.users_group_active == true())
2903 if repo_id:
2904 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2905 return q.all()
2906
2907 @classmethod
2908 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
2909 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
2910 .join(
2911 Permission,
2912 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
2913 .join(
2914 UserGroupRepoToPerm,
2915 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
2916 .join(
2917 UserGroup,
2918 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
2919 .join(
2920 UserGroupMember,
2921 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
2922 .filter(
2923 UserGroupMember.user_id == user_id,
2924 UserGroup.users_group_active == true())
2925
2926 if repo_id:
2927 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
2928 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
2929
2930 @classmethod
2931 def get_default_group_perms(cls, user_id, repo_group_id=None):
2932 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2933 .join(
2934 Permission,
2935 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
2936 .join(
2937 RepoGroup,
2938 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
2939 .filter(UserRepoGroupToPerm.user_id == user_id)
2940 if repo_group_id:
2941 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2942 return q.all()
2943
2944 @classmethod
2945 def get_default_group_perms_from_user_group(
2946 cls, user_id, repo_group_id=None):
2947 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2948 .join(
2949 Permission,
2950 UserGroupRepoGroupToPerm.permission_id ==
2951 Permission.permission_id)\
2952 .join(
2953 RepoGroup,
2954 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2955 .join(
2956 UserGroup,
2957 UserGroupRepoGroupToPerm.users_group_id ==
2958 UserGroup.users_group_id)\
2959 .join(
2960 UserGroupMember,
2961 UserGroupRepoGroupToPerm.users_group_id ==
2962 UserGroupMember.users_group_id)\
2963 .filter(
2964 UserGroupMember.user_id == user_id,
2965 UserGroup.users_group_active == true())
2966 if repo_group_id:
2967 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2968 return q.all()
2969
2970 @classmethod
2971 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2972 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2973 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2974 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2975 .filter(UserUserGroupToPerm.user_id == user_id)
2976 if user_group_id:
2977 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2978 return q.all()
2979
2980 @classmethod
2981 def get_default_user_group_perms_from_user_group(
2982 cls, user_id, user_group_id=None):
2983 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2984 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2985 .join(
2986 Permission,
2987 UserGroupUserGroupToPerm.permission_id ==
2988 Permission.permission_id)\
2989 .join(
2990 TargetUserGroup,
2991 UserGroupUserGroupToPerm.target_user_group_id ==
2992 TargetUserGroup.users_group_id)\
2993 .join(
2994 UserGroup,
2995 UserGroupUserGroupToPerm.user_group_id ==
2996 UserGroup.users_group_id)\
2997 .join(
2998 UserGroupMember,
2999 UserGroupUserGroupToPerm.user_group_id ==
3000 UserGroupMember.users_group_id)\
3001 .filter(
3002 UserGroupMember.user_id == user_id,
3003 UserGroup.users_group_active == true())
3004 if user_group_id:
3005 q = q.filter(
3006 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3007
3008 return q.all()
3009
3010
3011 class UserRepoToPerm(Base, BaseModel):
3012 __tablename__ = 'repo_to_perm'
3013 __table_args__ = (
3014 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3015 base_table_args
3016 )
3017
3018 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3019 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3020 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3021 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3022
3023 user = relationship('User')
3024 repository = relationship('Repository')
3025 permission = relationship('Permission')
3026
3027 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete, delete-orphan", lazy='joined')
3028
3029 @classmethod
3030 def create(cls, user, repository, permission):
3031 n = cls()
3032 n.user = user
3033 n.repository = repository
3034 n.permission = permission
3035 Session().add(n)
3036 return n
3037
3038 def __unicode__(self):
3039 return u'<%s => %s >' % (self.user, self.repository)
3040
3041
3042 class UserUserGroupToPerm(Base, BaseModel):
3043 __tablename__ = 'user_user_group_to_perm'
3044 __table_args__ = (
3045 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3046 base_table_args
3047 )
3048
3049 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3051 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3052 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3053
3054 user = relationship('User')
3055 user_group = relationship('UserGroup')
3056 permission = relationship('Permission')
3057
3058 @classmethod
3059 def create(cls, user, user_group, permission):
3060 n = cls()
3061 n.user = user
3062 n.user_group = user_group
3063 n.permission = permission
3064 Session().add(n)
3065 return n
3066
3067 def __unicode__(self):
3068 return u'<%s => %s >' % (self.user, self.user_group)
3069
3070
3071 class UserToPerm(Base, BaseModel):
3072 __tablename__ = 'user_to_perm'
3073 __table_args__ = (
3074 UniqueConstraint('user_id', 'permission_id'),
3075 base_table_args
3076 )
3077
3078 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3079 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3080 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3081
3082 user = relationship('User')
3083 permission = relationship('Permission', lazy='joined')
3084
3085 def __unicode__(self):
3086 return u'<%s => %s >' % (self.user, self.permission)
3087
3088
3089 class UserGroupRepoToPerm(Base, BaseModel):
3090 __tablename__ = 'users_group_repo_to_perm'
3091 __table_args__ = (
3092 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3093 base_table_args
3094 )
3095
3096 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3097 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3098 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3099 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3100
3101 users_group = relationship('UserGroup')
3102 permission = relationship('Permission')
3103 repository = relationship('Repository')
3104 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3105
3106 @classmethod
3107 def create(cls, users_group, repository, permission):
3108 n = cls()
3109 n.users_group = users_group
3110 n.repository = repository
3111 n.permission = permission
3112 Session().add(n)
3113 return n
3114
3115 def __unicode__(self):
3116 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3117
3118
3119 class UserGroupUserGroupToPerm(Base, BaseModel):
3120 __tablename__ = 'user_group_user_group_to_perm'
3121 __table_args__ = (
3122 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3123 CheckConstraint('target_user_group_id != user_group_id'),
3124 base_table_args
3125 )
3126
3127 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3128 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3129 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3130 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3131
3132 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3133 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3134 permission = relationship('Permission')
3135
3136 @classmethod
3137 def create(cls, target_user_group, user_group, permission):
3138 n = cls()
3139 n.target_user_group = target_user_group
3140 n.user_group = user_group
3141 n.permission = permission
3142 Session().add(n)
3143 return n
3144
3145 def __unicode__(self):
3146 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3147
3148
3149 class UserGroupToPerm(Base, BaseModel):
3150 __tablename__ = 'users_group_to_perm'
3151 __table_args__ = (
3152 UniqueConstraint('users_group_id', 'permission_id',),
3153 base_table_args
3154 )
3155
3156 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3157 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3158 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3159
3160 users_group = relationship('UserGroup')
3161 permission = relationship('Permission')
3162
3163
3164 class UserRepoGroupToPerm(Base, BaseModel):
3165 __tablename__ = 'user_repo_group_to_perm'
3166 __table_args__ = (
3167 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3168 base_table_args
3169 )
3170
3171 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3172 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3173 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3174 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3175
3176 user = relationship('User')
3177 group = relationship('RepoGroup')
3178 permission = relationship('Permission')
3179
3180 @classmethod
3181 def create(cls, user, repository_group, permission):
3182 n = cls()
3183 n.user = user
3184 n.group = repository_group
3185 n.permission = permission
3186 Session().add(n)
3187 return n
3188
3189
3190 class UserGroupRepoGroupToPerm(Base, BaseModel):
3191 __tablename__ = 'users_group_repo_group_to_perm'
3192 __table_args__ = (
3193 UniqueConstraint('users_group_id', 'group_id'),
3194 base_table_args
3195 )
3196
3197 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3198 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3199 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3200 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3201
3202 users_group = relationship('UserGroup')
3203 permission = relationship('Permission')
3204 group = relationship('RepoGroup')
3205
3206 @classmethod
3207 def create(cls, user_group, repository_group, permission):
3208 n = cls()
3209 n.users_group = user_group
3210 n.group = repository_group
3211 n.permission = permission
3212 Session().add(n)
3213 return n
3214
3215 def __unicode__(self):
3216 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3217
3218
3219 class Statistics(Base, BaseModel):
3220 __tablename__ = 'statistics'
3221 __table_args__ = (
3222 base_table_args
3223 )
3224
3225 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3226 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3227 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3228 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3229 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3230 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3231
3232 repository = relationship('Repository', single_parent=True)
3233
3234
3235 class UserFollowing(Base, BaseModel):
3236 __tablename__ = 'user_followings'
3237 __table_args__ = (
3238 UniqueConstraint('user_id', 'follows_repository_id'),
3239 UniqueConstraint('user_id', 'follows_user_id'),
3240 base_table_args
3241 )
3242
3243 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3244 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3245 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3246 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3247 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3248
3249 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3250
3251 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3252 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3253
3254 @classmethod
3255 def get_repo_followers(cls, repo_id):
3256 return cls.query().filter(cls.follows_repo_id == repo_id)
3257
3258
3259 class CacheKey(Base, BaseModel):
3260 __tablename__ = 'cache_invalidation'
3261 __table_args__ = (
3262 UniqueConstraint('cache_key'),
3263 Index('key_idx', 'cache_key'),
3264 base_table_args,
3265 )
3266
3267 CACHE_TYPE_FEED = 'FEED'
3268 CACHE_TYPE_README = 'README'
3269 # namespaces used to register process/thread aware caches
3270 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3271 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3272
3273 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3274 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3275 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3276 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3277
3278 def __init__(self, cache_key, cache_args=''):
3279 self.cache_key = cache_key
3280 self.cache_args = cache_args
3281 self.cache_active = False
3282
3283 def __unicode__(self):
3284 return u"<%s('%s:%s[%s]')>" % (
3285 self.__class__.__name__,
3286 self.cache_id, self.cache_key, self.cache_active)
3287
3288 def _cache_key_partition(self):
3289 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3290 return prefix, repo_name, suffix
3291
3292 def get_prefix(self):
3293 """
3294 Try to extract prefix from existing cache key. The key could consist
3295 of prefix, repo_name, suffix
3296 """
3297 # this returns prefix, repo_name, suffix
3298 return self._cache_key_partition()[0]
3299
3300 def get_suffix(self):
3301 """
3302 get suffix that might have been used in _get_cache_key to
3303 generate self.cache_key. Only used for informational purposes
3304 in repo_edit.mako.
3305 """
3306 # prefix, repo_name, suffix
3307 return self._cache_key_partition()[2]
3308
3309 @classmethod
3310 def delete_all_cache(cls):
3311 """
3312 Delete all cache keys from database.
3313 Should only be run when all instances are down and all entries
3314 thus stale.
3315 """
3316 cls.query().delete()
3317 Session().commit()
3318
3319 @classmethod
3320 def set_invalidate(cls, cache_uid, delete=False):
3321 """
3322 Mark all caches of a repo as invalid in the database.
3323 """
3324
3325 try:
3326 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3327 if delete:
3328 qry.delete()
3329 log.debug('cache objects deleted for cache args %s',
3330 safe_str(cache_uid))
3331 else:
3332 qry.update({"cache_active": False})
3333 log.debug('cache objects marked as invalid for cache args %s',
3334 safe_str(cache_uid))
3335
3336 Session().commit()
3337 except Exception:
3338 log.exception(
3339 'Cache key invalidation failed for cache args %s',
3340 safe_str(cache_uid))
3341 Session().rollback()
3342
3343 @classmethod
3344 def get_active_cache(cls, cache_key):
3345 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3346 if inv_obj:
3347 return inv_obj
3348 return None
3349
3350
3351 class ChangesetComment(Base, BaseModel):
3352 __tablename__ = 'changeset_comments'
3353 __table_args__ = (
3354 Index('cc_revision_idx', 'revision'),
3355 base_table_args,
3356 )
3357
3358 COMMENT_OUTDATED = u'comment_outdated'
3359 COMMENT_TYPE_NOTE = u'note'
3360 COMMENT_TYPE_TODO = u'todo'
3361 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3362
3363 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3364 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3365 revision = Column('revision', String(40), nullable=True)
3366 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3367 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3368 line_no = Column('line_no', Unicode(10), nullable=True)
3369 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3370 f_path = Column('f_path', Unicode(1000), nullable=True)
3371 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3372 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3373 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3374 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3375 renderer = Column('renderer', Unicode(64), nullable=True)
3376 display_state = Column('display_state', Unicode(128), nullable=True)
3377
3378 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3379 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3380
3381 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3382 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3383
3384 author = relationship('User', lazy='joined')
3385 repo = relationship('Repository')
3386 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3387 pull_request = relationship('PullRequest', lazy='joined')
3388 pull_request_version = relationship('PullRequestVersion')
3389
3390 @classmethod
3391 def get_users(cls, revision=None, pull_request_id=None):
3392 """
3393 Returns user associated with this ChangesetComment. ie those
3394 who actually commented
3395
3396 :param cls:
3397 :param revision:
3398 """
3399 q = Session().query(User)\
3400 .join(ChangesetComment.author)
3401 if revision:
3402 q = q.filter(cls.revision == revision)
3403 elif pull_request_id:
3404 q = q.filter(cls.pull_request_id == pull_request_id)
3405 return q.all()
3406
3407 @classmethod
3408 def get_index_from_version(cls, pr_version, versions):
3409 num_versions = [x.pull_request_version_id for x in versions]
3410 try:
3411 return num_versions.index(pr_version) +1
3412 except (IndexError, ValueError):
3413 return
3414
3415 @property
3416 def outdated(self):
3417 return self.display_state == self.COMMENT_OUTDATED
3418
3419 def outdated_at_version(self, version):
3420 """
3421 Checks if comment is outdated for given pull request version
3422 """
3423 return self.outdated and self.pull_request_version_id != version
3424
3425 def older_than_version(self, version):
3426 """
3427 Checks if comment is made from previous version than given
3428 """
3429 if version is None:
3430 return self.pull_request_version_id is not None
3431
3432 return self.pull_request_version_id < version
3433
3434 @property
3435 def resolved(self):
3436 return self.resolved_by[0] if self.resolved_by else None
3437
3438 @property
3439 def is_todo(self):
3440 return self.comment_type == self.COMMENT_TYPE_TODO
3441
3442 @property
3443 def is_inline(self):
3444 return self.line_no and self.f_path
3445
3446 def get_index_version(self, versions):
3447 return self.get_index_from_version(
3448 self.pull_request_version_id, versions)
3449
3450 def __repr__(self):
3451 if self.comment_id:
3452 return '<DB:Comment #%s>' % self.comment_id
3453 else:
3454 return '<DB:Comment at %#x>' % id(self)
3455
3456 def get_api_data(self):
3457 comment = self
3458 data = {
3459 'comment_id': comment.comment_id,
3460 'comment_type': comment.comment_type,
3461 'comment_text': comment.text,
3462 'comment_status': comment.status_change,
3463 'comment_f_path': comment.f_path,
3464 'comment_lineno': comment.line_no,
3465 'comment_author': comment.author,
3466 'comment_created_on': comment.created_on
3467 }
3468 return data
3469
3470 def __json__(self):
3471 data = dict()
3472 data.update(self.get_api_data())
3473 return data
3474
3475
3476 class ChangesetStatus(Base, BaseModel):
3477 __tablename__ = 'changeset_statuses'
3478 __table_args__ = (
3479 Index('cs_revision_idx', 'revision'),
3480 Index('cs_version_idx', 'version'),
3481 UniqueConstraint('repo_id', 'revision', 'version'),
3482 base_table_args
3483 )
3484
3485 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3486 STATUS_APPROVED = 'approved'
3487 STATUS_REJECTED = 'rejected'
3488 STATUS_UNDER_REVIEW = 'under_review'
3489
3490 STATUSES = [
3491 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3492 (STATUS_APPROVED, _("Approved")),
3493 (STATUS_REJECTED, _("Rejected")),
3494 (STATUS_UNDER_REVIEW, _("Under Review")),
3495 ]
3496
3497 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3498 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3499 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3500 revision = Column('revision', String(40), nullable=False)
3501 status = Column('status', String(128), nullable=False, default=DEFAULT)
3502 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3503 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3504 version = Column('version', Integer(), nullable=False, default=0)
3505 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3506
3507 author = relationship('User', lazy='joined')
3508 repo = relationship('Repository')
3509 comment = relationship('ChangesetComment', lazy='joined')
3510 pull_request = relationship('PullRequest', lazy='joined')
3511
3512 def __unicode__(self):
3513 return u"<%s('%s[v%s]:%s')>" % (
3514 self.__class__.__name__,
3515 self.status, self.version, self.author
3516 )
3517
3518 @classmethod
3519 def get_status_lbl(cls, value):
3520 return dict(cls.STATUSES).get(value)
3521
3522 @property
3523 def status_lbl(self):
3524 return ChangesetStatus.get_status_lbl(self.status)
3525
3526 def get_api_data(self):
3527 status = self
3528 data = {
3529 'status_id': status.changeset_status_id,
3530 'status': status.status,
3531 }
3532 return data
3533
3534 def __json__(self):
3535 data = dict()
3536 data.update(self.get_api_data())
3537 return data
3538
3539
3540 class _PullRequestBase(BaseModel):
3541 """
3542 Common attributes of pull request and version entries.
3543 """
3544
3545 # .status values
3546 STATUS_NEW = u'new'
3547 STATUS_OPEN = u'open'
3548 STATUS_CLOSED = u'closed'
3549
3550 # available states
3551 STATE_CREATING = u'creating'
3552 STATE_UPDATING = u'updating'
3553 STATE_MERGING = u'merging'
3554 STATE_CREATED = u'created'
3555
3556 title = Column('title', Unicode(255), nullable=True)
3557 description = Column(
3558 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3559 nullable=True)
3560 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3561
3562 # new/open/closed status of pull request (not approve/reject/etc)
3563 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3564 created_on = Column(
3565 'created_on', DateTime(timezone=False), nullable=False,
3566 default=datetime.datetime.now)
3567 updated_on = Column(
3568 'updated_on', DateTime(timezone=False), nullable=False,
3569 default=datetime.datetime.now)
3570
3571 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3572
3573 @declared_attr
3574 def user_id(cls):
3575 return Column(
3576 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3577 unique=None)
3578
3579 # 500 revisions max
3580 _revisions = Column(
3581 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3582
3583 @declared_attr
3584 def source_repo_id(cls):
3585 # TODO: dan: rename column to source_repo_id
3586 return Column(
3587 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3588 nullable=False)
3589
3590 _source_ref = Column('org_ref', Unicode(255), nullable=False)
3591
3592 @hybrid_property
3593 def source_ref(self):
3594 return self._source_ref
3595
3596 @source_ref.setter
3597 def source_ref(self, val):
3598 parts = (val or '').split(':')
3599 if len(parts) != 3:
3600 raise ValueError(
3601 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3602 self._source_ref = safe_unicode(val)
3603
3604 _target_ref = Column('other_ref', Unicode(255), nullable=False)
3605
3606 @hybrid_property
3607 def target_ref(self):
3608 return self._target_ref
3609
3610 @target_ref.setter
3611 def target_ref(self, val):
3612 parts = (val or '').split(':')
3613 if len(parts) != 3:
3614 raise ValueError(
3615 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3616 self._target_ref = safe_unicode(val)
3617
3618 @declared_attr
3619 def target_repo_id(cls):
3620 # TODO: dan: rename column to target_repo_id
3621 return Column(
3622 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3623 nullable=False)
3624
3625 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3626
3627 # TODO: dan: rename column to last_merge_source_rev
3628 _last_merge_source_rev = Column(
3629 'last_merge_org_rev', String(40), nullable=True)
3630 # TODO: dan: rename column to last_merge_target_rev
3631 _last_merge_target_rev = Column(
3632 'last_merge_other_rev', String(40), nullable=True)
3633 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3634 merge_rev = Column('merge_rev', String(40), nullable=True)
3635
3636 reviewer_data = Column(
3637 'reviewer_data_json', MutationObj.as_mutable(
3638 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3639
3640 @property
3641 def reviewer_data_json(self):
3642 return json.dumps(self.reviewer_data)
3643
3644 @hybrid_property
3645 def description_safe(self):
3646 from rhodecode.lib import helpers as h
3647 return h.escape(self.description)
3648
3649 @hybrid_property
3650 def revisions(self):
3651 return self._revisions.split(':') if self._revisions else []
3652
3653 @revisions.setter
3654 def revisions(self, val):
3655 self._revisions = ':'.join(val)
3656
3657 @hybrid_property
3658 def last_merge_status(self):
3659 return safe_int(self._last_merge_status)
3660
3661 @last_merge_status.setter
3662 def last_merge_status(self, val):
3663 self._last_merge_status = val
3664
3665 @declared_attr
3666 def author(cls):
3667 return relationship('User', lazy='joined')
3668
3669 @declared_attr
3670 def source_repo(cls):
3671 return relationship(
3672 'Repository',
3673 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3674
3675 @property
3676 def source_ref_parts(self):
3677 return self.unicode_to_reference(self.source_ref)
3678
3679 @declared_attr
3680 def target_repo(cls):
3681 return relationship(
3682 'Repository',
3683 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3684
3685 @property
3686 def target_ref_parts(self):
3687 return self.unicode_to_reference(self.target_ref)
3688
3689 @property
3690 def shadow_merge_ref(self):
3691 return self.unicode_to_reference(self._shadow_merge_ref)
3692
3693 @shadow_merge_ref.setter
3694 def shadow_merge_ref(self, ref):
3695 self._shadow_merge_ref = self.reference_to_unicode(ref)
3696
3697 @staticmethod
3698 def unicode_to_reference(raw):
3699 """
3700 Convert a unicode (or string) to a reference object.
3701 If unicode evaluates to False it returns None.
3702 """
3703 if raw:
3704 refs = raw.split(':')
3705 return Reference(*refs)
3706 else:
3707 return None
3708
3709 @staticmethod
3710 def reference_to_unicode(ref):
3711 """
3712 Convert a reference object to unicode.
3713 If reference is None it returns None.
3714 """
3715 if ref:
3716 return u':'.join(ref)
3717 else:
3718 return None
3719
3720 def get_api_data(self, with_merge_state=True):
3721 from rhodecode.model.pull_request import PullRequestModel
3722
3723 pull_request = self
3724 if with_merge_state:
3725 merge_status = PullRequestModel().merge_status(pull_request)
3726 merge_state = {
3727 'status': merge_status[0],
3728 'message': safe_unicode(merge_status[1]),
3729 }
3730 else:
3731 merge_state = {'status': 'not_available',
3732 'message': 'not_available'}
3733
3734 merge_data = {
3735 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3736 'reference': (
3737 pull_request.shadow_merge_ref._asdict()
3738 if pull_request.shadow_merge_ref else None),
3739 }
3740
3741 data = {
3742 'pull_request_id': pull_request.pull_request_id,
3743 'url': PullRequestModel().get_url(pull_request),
3744 'title': pull_request.title,
3745 'description': pull_request.description,
3746 'status': pull_request.status,
3747 'created_on': pull_request.created_on,
3748 'updated_on': pull_request.updated_on,
3749 'commit_ids': pull_request.revisions,
3750 'review_status': pull_request.calculated_review_status(),
3751 'mergeable': merge_state,
3752 'source': {
3753 'clone_url': pull_request.source_repo.clone_url(),
3754 'repository': pull_request.source_repo.repo_name,
3755 'reference': {
3756 'name': pull_request.source_ref_parts.name,
3757 'type': pull_request.source_ref_parts.type,
3758 'commit_id': pull_request.source_ref_parts.commit_id,
3759 },
3760 },
3761 'target': {
3762 'clone_url': pull_request.target_repo.clone_url(),
3763 'repository': pull_request.target_repo.repo_name,
3764 'reference': {
3765 'name': pull_request.target_ref_parts.name,
3766 'type': pull_request.target_ref_parts.type,
3767 'commit_id': pull_request.target_ref_parts.commit_id,
3768 },
3769 },
3770 'merge': merge_data,
3771 'author': pull_request.author.get_api_data(include_secrets=False,
3772 details='basic'),
3773 'reviewers': [
3774 {
3775 'user': reviewer.get_api_data(include_secrets=False,
3776 details='basic'),
3777 'reasons': reasons,
3778 'review_status': st[0][1].status if st else 'not_reviewed',
3779 }
3780 for obj, reviewer, reasons, mandatory, st in
3781 pull_request.reviewers_statuses()
3782 ]
3783 }
3784
3785 return data
3786
3787
3788 class PullRequest(Base, _PullRequestBase):
3789 __tablename__ = 'pull_requests'
3790 __table_args__ = (
3791 base_table_args,
3792 )
3793
3794 pull_request_id = Column(
3795 'pull_request_id', Integer(), nullable=False, primary_key=True)
3796
3797 def __repr__(self):
3798 if self.pull_request_id:
3799 return '<DB:PullRequest #%s>' % self.pull_request_id
3800 else:
3801 return '<DB:PullRequest at %#x>' % id(self)
3802
3803 reviewers = relationship('PullRequestReviewers',
3804 cascade="all, delete, delete-orphan")
3805 statuses = relationship('ChangesetStatus',
3806 cascade="all, delete, delete-orphan")
3807 comments = relationship('ChangesetComment',
3808 cascade="all, delete, delete-orphan")
3809 versions = relationship('PullRequestVersion',
3810 cascade="all, delete, delete-orphan",
3811 lazy='dynamic')
3812
3813 @classmethod
3814 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3815 internal_methods=None):
3816
3817 class PullRequestDisplay(object):
3818 """
3819 Special object wrapper for showing PullRequest data via Versions
3820 It mimics PR object as close as possible. This is read only object
3821 just for display
3822 """
3823
3824 def __init__(self, attrs, internal=None):
3825 self.attrs = attrs
3826 # internal have priority over the given ones via attrs
3827 self.internal = internal or ['versions']
3828
3829 def __getattr__(self, item):
3830 if item in self.internal:
3831 return getattr(self, item)
3832 try:
3833 return self.attrs[item]
3834 except KeyError:
3835 raise AttributeError(
3836 '%s object has no attribute %s' % (self, item))
3837
3838 def __repr__(self):
3839 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3840
3841 def versions(self):
3842 return pull_request_obj.versions.order_by(
3843 PullRequestVersion.pull_request_version_id).all()
3844
3845 def is_closed(self):
3846 return pull_request_obj.is_closed()
3847
3848 @property
3849 def pull_request_version_id(self):
3850 return getattr(pull_request_obj, 'pull_request_version_id', None)
3851
3852 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3853
3854 attrs.author = StrictAttributeDict(
3855 pull_request_obj.author.get_api_data())
3856 if pull_request_obj.target_repo:
3857 attrs.target_repo = StrictAttributeDict(
3858 pull_request_obj.target_repo.get_api_data())
3859 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3860
3861 if pull_request_obj.source_repo:
3862 attrs.source_repo = StrictAttributeDict(
3863 pull_request_obj.source_repo.get_api_data())
3864 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3865
3866 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3867 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3868 attrs.revisions = pull_request_obj.revisions
3869
3870 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3871 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3872 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3873
3874 return PullRequestDisplay(attrs, internal=internal_methods)
3875
3876 def is_closed(self):
3877 return self.status == self.STATUS_CLOSED
3878
3879 def __json__(self):
3880 return {
3881 'revisions': self.revisions,
3882 }
3883
3884 def calculated_review_status(self):
3885 from rhodecode.model.changeset_status import ChangesetStatusModel
3886 return ChangesetStatusModel().calculated_review_status(self)
3887
3888 def reviewers_statuses(self):
3889 from rhodecode.model.changeset_status import ChangesetStatusModel
3890 return ChangesetStatusModel().reviewers_statuses(self)
3891
3892 @property
3893 def workspace_id(self):
3894 from rhodecode.model.pull_request import PullRequestModel
3895 return PullRequestModel()._workspace_id(self)
3896
3897 def get_shadow_repo(self):
3898 workspace_id = self.workspace_id
3899 vcs_obj = self.target_repo.scm_instance()
3900 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3901 self.target_repo.repo_id, workspace_id)
3902 if os.path.isdir(shadow_repository_path):
3903 return vcs_obj._get_shadow_instance(shadow_repository_path)
3904
3905
3906 class PullRequestVersion(Base, _PullRequestBase):
3907 __tablename__ = 'pull_request_versions'
3908 __table_args__ = (
3909 base_table_args,
3910 )
3911
3912 pull_request_version_id = Column(
3913 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3914 pull_request_id = Column(
3915 'pull_request_id', Integer(),
3916 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3917 pull_request = relationship('PullRequest')
3918
3919 def __repr__(self):
3920 if self.pull_request_version_id:
3921 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3922 else:
3923 return '<DB:PullRequestVersion at %#x>' % id(self)
3924
3925 @property
3926 def reviewers(self):
3927 return self.pull_request.reviewers
3928
3929 @property
3930 def versions(self):
3931 return self.pull_request.versions
3932
3933 def is_closed(self):
3934 # calculate from original
3935 return self.pull_request.status == self.STATUS_CLOSED
3936
3937 def calculated_review_status(self):
3938 return self.pull_request.calculated_review_status()
3939
3940 def reviewers_statuses(self):
3941 return self.pull_request.reviewers_statuses()
3942
3943
3944 class PullRequestReviewers(Base, BaseModel):
3945 __tablename__ = 'pull_request_reviewers'
3946 __table_args__ = (
3947 base_table_args,
3948 )
3949
3950 @hybrid_property
3951 def reasons(self):
3952 if not self._reasons:
3953 return []
3954 return self._reasons
3955
3956 @reasons.setter
3957 def reasons(self, val):
3958 val = val or []
3959 if any(not isinstance(x, compat.string_types) for x in val):
3960 raise Exception('invalid reasons type, must be list of strings')
3961 self._reasons = val
3962
3963 pull_requests_reviewers_id = Column(
3964 'pull_requests_reviewers_id', Integer(), nullable=False,
3965 primary_key=True)
3966 pull_request_id = Column(
3967 "pull_request_id", Integer(),
3968 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3969 user_id = Column(
3970 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3971 _reasons = Column(
3972 'reason', MutationList.as_mutable(
3973 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3974
3975 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3976 user = relationship('User')
3977 pull_request = relationship('PullRequest')
3978
3979 rule_data = Column(
3980 'rule_data_json',
3981 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
3982
3983 def rule_user_group_data(self):
3984 """
3985 Returns the voting user group rule data for this reviewer
3986 """
3987
3988 if self.rule_data and 'vote_rule' in self.rule_data:
3989 user_group_data = {}
3990 if 'rule_user_group_entry_id' in self.rule_data:
3991 # means a group with voting rules !
3992 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
3993 user_group_data['name'] = self.rule_data['rule_name']
3994 user_group_data['vote_rule'] = self.rule_data['vote_rule']
3995
3996 return user_group_data
3997
3998 def __unicode__(self):
3999 return u"<%s('id:%s')>" % (self.__class__.__name__,
4000 self.pull_requests_reviewers_id)
4001
4002
4003 class Notification(Base, BaseModel):
4004 __tablename__ = 'notifications'
4005 __table_args__ = (
4006 Index('notification_type_idx', 'type'),
4007 base_table_args,
4008 )
4009
4010 TYPE_CHANGESET_COMMENT = u'cs_comment'
4011 TYPE_MESSAGE = u'message'
4012 TYPE_MENTION = u'mention'
4013 TYPE_REGISTRATION = u'registration'
4014 TYPE_PULL_REQUEST = u'pull_request'
4015 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4016
4017 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4018 subject = Column('subject', Unicode(512), nullable=True)
4019 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4020 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4021 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4022 type_ = Column('type', Unicode(255))
4023
4024 created_by_user = relationship('User')
4025 notifications_to_users = relationship('UserNotification', lazy='joined',
4026 cascade="all, delete, delete-orphan")
4027
4028 @property
4029 def recipients(self):
4030 return [x.user for x in UserNotification.query()\
4031 .filter(UserNotification.notification == self)\
4032 .order_by(UserNotification.user_id.asc()).all()]
4033
4034 @classmethod
4035 def create(cls, created_by, subject, body, recipients, type_=None):
4036 if type_ is None:
4037 type_ = Notification.TYPE_MESSAGE
4038
4039 notification = cls()
4040 notification.created_by_user = created_by
4041 notification.subject = subject
4042 notification.body = body
4043 notification.type_ = type_
4044 notification.created_on = datetime.datetime.now()
4045
4046 # For each recipient link the created notification to his account
4047 for u in recipients:
4048 assoc = UserNotification()
4049 assoc.user_id = u.user_id
4050 assoc.notification = notification
4051
4052 # if created_by is inside recipients mark his notification
4053 # as read
4054 if u.user_id == created_by.user_id:
4055 assoc.read = True
4056 Session().add(assoc)
4057
4058 Session().add(notification)
4059
4060 return notification
4061
4062
4063 class UserNotification(Base, BaseModel):
4064 __tablename__ = 'user_to_notification'
4065 __table_args__ = (
4066 UniqueConstraint('user_id', 'notification_id'),
4067 base_table_args
4068 )
4069
4070 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4071 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4072 read = Column('read', Boolean, default=False)
4073 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4074
4075 user = relationship('User', lazy="joined")
4076 notification = relationship('Notification', lazy="joined",
4077 order_by=lambda: Notification.created_on.desc(),)
4078
4079 def mark_as_read(self):
4080 self.read = True
4081 Session().add(self)
4082
4083
4084 class Gist(Base, BaseModel):
4085 __tablename__ = 'gists'
4086 __table_args__ = (
4087 Index('g_gist_access_id_idx', 'gist_access_id'),
4088 Index('g_created_on_idx', 'created_on'),
4089 base_table_args
4090 )
4091
4092 GIST_PUBLIC = u'public'
4093 GIST_PRIVATE = u'private'
4094 DEFAULT_FILENAME = u'gistfile1.txt'
4095
4096 ACL_LEVEL_PUBLIC = u'acl_public'
4097 ACL_LEVEL_PRIVATE = u'acl_private'
4098
4099 gist_id = Column('gist_id', Integer(), primary_key=True)
4100 gist_access_id = Column('gist_access_id', Unicode(250))
4101 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4102 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4103 gist_expires = Column('gist_expires', Float(53), nullable=False)
4104 gist_type = Column('gist_type', Unicode(128), nullable=False)
4105 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4106 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4107 acl_level = Column('acl_level', Unicode(128), nullable=True)
4108
4109 owner = relationship('User')
4110
4111 def __repr__(self):
4112 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4113
4114 @hybrid_property
4115 def description_safe(self):
4116 from rhodecode.lib import helpers as h
4117 return h.escape(self.gist_description)
4118
4119 @classmethod
4120 def get_or_404(cls, id_):
4121 from pyramid.httpexceptions import HTTPNotFound
4122
4123 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4124 if not res:
4125 raise HTTPNotFound()
4126 return res
4127
4128 @classmethod
4129 def get_by_access_id(cls, gist_access_id):
4130 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4131
4132 def gist_url(self):
4133 from rhodecode.model.gist import GistModel
4134 return GistModel().get_url(self)
4135
4136 @classmethod
4137 def base_path(cls):
4138 """
4139 Returns base path when all gists are stored
4140
4141 :param cls:
4142 """
4143 from rhodecode.model.gist import GIST_STORE_LOC
4144 q = Session().query(RhodeCodeUi)\
4145 .filter(RhodeCodeUi.ui_key == URL_SEP)
4146 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4147 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4148
4149 def get_api_data(self):
4150 """
4151 Common function for generating gist related data for API
4152 """
4153 gist = self
4154 data = {
4155 'gist_id': gist.gist_id,
4156 'type': gist.gist_type,
4157 'access_id': gist.gist_access_id,
4158 'description': gist.gist_description,
4159 'url': gist.gist_url(),
4160 'expires': gist.gist_expires,
4161 'created_on': gist.created_on,
4162 'modified_at': gist.modified_at,
4163 'content': None,
4164 'acl_level': gist.acl_level,
4165 }
4166 return data
4167
4168 def __json__(self):
4169 data = dict(
4170 )
4171 data.update(self.get_api_data())
4172 return data
4173 # SCM functions
4174
4175 def scm_instance(self, **kwargs):
4176 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4177 return get_vcs_instance(
4178 repo_path=safe_str(full_repo_path), create=False)
4179
4180
4181 class ExternalIdentity(Base, BaseModel):
4182 __tablename__ = 'external_identities'
4183 __table_args__ = (
4184 Index('local_user_id_idx', 'local_user_id'),
4185 Index('external_id_idx', 'external_id'),
4186 base_table_args
4187 )
4188
4189 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4190 external_username = Column('external_username', Unicode(1024), default=u'')
4191 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4192 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4193 access_token = Column('access_token', String(1024), default=u'')
4194 alt_token = Column('alt_token', String(1024), default=u'')
4195 token_secret = Column('token_secret', String(1024), default=u'')
4196
4197 @classmethod
4198 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4199 """
4200 Returns ExternalIdentity instance based on search params
4201
4202 :param external_id:
4203 :param provider_name:
4204 :return: ExternalIdentity
4205 """
4206 query = cls.query()
4207 query = query.filter(cls.external_id == external_id)
4208 query = query.filter(cls.provider_name == provider_name)
4209 if local_user_id:
4210 query = query.filter(cls.local_user_id == local_user_id)
4211 return query.first()
4212
4213 @classmethod
4214 def user_by_external_id_and_provider(cls, external_id, provider_name):
4215 """
4216 Returns User instance based on search params
4217
4218 :param external_id:
4219 :param provider_name:
4220 :return: User
4221 """
4222 query = User.query()
4223 query = query.filter(cls.external_id == external_id)
4224 query = query.filter(cls.provider_name == provider_name)
4225 query = query.filter(User.user_id == cls.local_user_id)
4226 return query.first()
4227
4228 @classmethod
4229 def by_local_user_id(cls, local_user_id):
4230 """
4231 Returns all tokens for user
4232
4233 :param local_user_id:
4234 :return: ExternalIdentity
4235 """
4236 query = cls.query()
4237 query = query.filter(cls.local_user_id == local_user_id)
4238 return query
4239
4240 @classmethod
4241 def load_provider_plugin(cls, plugin_id):
4242 from rhodecode.authentication.base import loadplugin
4243 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4244 auth_plugin = loadplugin(_plugin_id)
4245 return auth_plugin
4246
4247
4248 class Integration(Base, BaseModel):
4249 __tablename__ = 'integrations'
4250 __table_args__ = (
4251 base_table_args
4252 )
4253
4254 integration_id = Column('integration_id', Integer(), primary_key=True)
4255 integration_type = Column('integration_type', String(255))
4256 enabled = Column('enabled', Boolean(), nullable=False)
4257 name = Column('name', String(255), nullable=False)
4258 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4259 default=False)
4260
4261 settings = Column(
4262 'settings_json', MutationObj.as_mutable(
4263 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4264 repo_id = Column(
4265 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4266 nullable=True, unique=None, default=None)
4267 repo = relationship('Repository', lazy='joined')
4268
4269 repo_group_id = Column(
4270 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4271 nullable=True, unique=None, default=None)
4272 repo_group = relationship('RepoGroup', lazy='joined')
4273
4274 @property
4275 def scope(self):
4276 if self.repo:
4277 return repr(self.repo)
4278 if self.repo_group:
4279 if self.child_repos_only:
4280 return repr(self.repo_group) + ' (child repos only)'
4281 else:
4282 return repr(self.repo_group) + ' (recursive)'
4283 if self.child_repos_only:
4284 return 'root_repos'
4285 return 'global'
4286
4287 def __repr__(self):
4288 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4289
4290
4291 class RepoReviewRuleUser(Base, BaseModel):
4292 __tablename__ = 'repo_review_rules_users'
4293 __table_args__ = (
4294 base_table_args
4295 )
4296
4297 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4298 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4299 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4300 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4301 user = relationship('User')
4302
4303 def rule_data(self):
4304 return {
4305 'mandatory': self.mandatory
4306 }
4307
4308
4309 class RepoReviewRuleUserGroup(Base, BaseModel):
4310 __tablename__ = 'repo_review_rules_users_groups'
4311 __table_args__ = (
4312 base_table_args
4313 )
4314
4315 VOTE_RULE_ALL = -1
4316
4317 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4318 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4319 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4320 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4321 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4322 users_group = relationship('UserGroup')
4323
4324 def rule_data(self):
4325 return {
4326 'mandatory': self.mandatory,
4327 'vote_rule': self.vote_rule
4328 }
4329
4330 @property
4331 def vote_rule_label(self):
4332 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4333 return 'all must vote'
4334 else:
4335 return 'min. vote {}'.format(self.vote_rule)
4336
4337
4338 class RepoReviewRule(Base, BaseModel):
4339 __tablename__ = 'repo_review_rules'
4340 __table_args__ = (
4341 base_table_args
4342 )
4343
4344 repo_review_rule_id = Column(
4345 'repo_review_rule_id', Integer(), primary_key=True)
4346 repo_id = Column(
4347 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4348 repo = relationship('Repository', backref='review_rules')
4349
4350 review_rule_name = Column('review_rule_name', String(255))
4351 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4352 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4353 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4354
4355 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4356 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4357 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4358 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4359
4360 rule_users = relationship('RepoReviewRuleUser')
4361 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4362
4363 def _validate_pattern(self, value):
4364 re.compile('^' + glob2re(value) + '$')
4365
4366 @hybrid_property
4367 def source_branch_pattern(self):
4368 return self._branch_pattern or '*'
4369
4370 @source_branch_pattern.setter
4371 def source_branch_pattern(self, value):
4372 self._validate_pattern(value)
4373 self._branch_pattern = value or '*'
4374
4375 @hybrid_property
4376 def target_branch_pattern(self):
4377 return self._target_branch_pattern or '*'
4378
4379 @target_branch_pattern.setter
4380 def target_branch_pattern(self, value):
4381 self._validate_pattern(value)
4382 self._target_branch_pattern = value or '*'
4383
4384 @hybrid_property
4385 def file_pattern(self):
4386 return self._file_pattern or '*'
4387
4388 @file_pattern.setter
4389 def file_pattern(self, value):
4390 self._validate_pattern(value)
4391 self._file_pattern = value or '*'
4392
4393 def matches(self, source_branch, target_branch, files_changed):
4394 """
4395 Check if this review rule matches a branch/files in a pull request
4396
4397 :param source_branch: source branch name for the commit
4398 :param target_branch: target branch name for the commit
4399 :param files_changed: list of file paths changed in the pull request
4400 """
4401
4402 source_branch = source_branch or ''
4403 target_branch = target_branch or ''
4404 files_changed = files_changed or []
4405
4406 branch_matches = True
4407 if source_branch or target_branch:
4408 if self.source_branch_pattern == '*':
4409 source_branch_match = True
4410 else:
4411 if self.source_branch_pattern.startswith('re:'):
4412 source_pattern = self.source_branch_pattern[3:]
4413 else:
4414 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4415 source_branch_regex = re.compile(source_pattern)
4416 source_branch_match = bool(source_branch_regex.search(source_branch))
4417 if self.target_branch_pattern == '*':
4418 target_branch_match = True
4419 else:
4420 if self.target_branch_pattern.startswith('re:'):
4421 target_pattern = self.target_branch_pattern[3:]
4422 else:
4423 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4424 target_branch_regex = re.compile(target_pattern)
4425 target_branch_match = bool(target_branch_regex.search(target_branch))
4426
4427 branch_matches = source_branch_match and target_branch_match
4428
4429 files_matches = True
4430 if self.file_pattern != '*':
4431 files_matches = False
4432 if self.file_pattern.startswith('re:'):
4433 file_pattern = self.file_pattern[3:]
4434 else:
4435 file_pattern = glob2re(self.file_pattern)
4436 file_regex = re.compile(file_pattern)
4437 for filename in files_changed:
4438 if file_regex.search(filename):
4439 files_matches = True
4440 break
4441
4442 return branch_matches and files_matches
4443
4444 @property
4445 def review_users(self):
4446 """ Returns the users which this rule applies to """
4447
4448 users = collections.OrderedDict()
4449
4450 for rule_user in self.rule_users:
4451 if rule_user.user.active:
4452 if rule_user.user not in users:
4453 users[rule_user.user.username] = {
4454 'user': rule_user.user,
4455 'source': 'user',
4456 'source_data': {},
4457 'data': rule_user.rule_data()
4458 }
4459
4460 for rule_user_group in self.rule_user_groups:
4461 source_data = {
4462 'user_group_id': rule_user_group.users_group.users_group_id,
4463 'name': rule_user_group.users_group.users_group_name,
4464 'members': len(rule_user_group.users_group.members)
4465 }
4466 for member in rule_user_group.users_group.members:
4467 if member.user.active:
4468 key = member.user.username
4469 if key in users:
4470 # skip this member as we have him already
4471 # this prevents from override the "first" matched
4472 # users with duplicates in multiple groups
4473 continue
4474
4475 users[key] = {
4476 'user': member.user,
4477 'source': 'user_group',
4478 'source_data': source_data,
4479 'data': rule_user_group.rule_data()
4480 }
4481
4482 return users
4483
4484 def user_group_vote_rule(self, user_id):
4485
4486 rules = []
4487 if not self.rule_user_groups:
4488 return rules
4489
4490 for user_group in self.rule_user_groups:
4491 user_group_members = [x.user_id for x in user_group.users_group.members]
4492 if user_id in user_group_members:
4493 rules.append(user_group)
4494 return rules
4495
4496 def __repr__(self):
4497 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4498 self.repo_review_rule_id, self.repo)
4499
4500
4501 class ScheduleEntry(Base, BaseModel):
4502 __tablename__ = 'schedule_entries'
4503 __table_args__ = (
4504 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4505 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4506 base_table_args,
4507 )
4508
4509 schedule_types = ['crontab', 'timedelta', 'integer']
4510 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4511
4512 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4513 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4514 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4515
4516 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4517 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4518
4519 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4520 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4521
4522 # task
4523 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4524 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4525 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4526 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4527
4528 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4529 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4530
4531 @hybrid_property
4532 def schedule_type(self):
4533 return self._schedule_type
4534
4535 @schedule_type.setter
4536 def schedule_type(self, val):
4537 if val not in self.schedule_types:
4538 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4539 val, self.schedule_type))
4540
4541 self._schedule_type = val
4542
4543 @classmethod
4544 def get_uid(cls, obj):
4545 args = obj.task_args
4546 kwargs = obj.task_kwargs
4547 if isinstance(args, JsonRaw):
4548 try:
4549 args = json.loads(args)
4550 except ValueError:
4551 args = tuple()
4552
4553 if isinstance(kwargs, JsonRaw):
4554 try:
4555 kwargs = json.loads(kwargs)
4556 except ValueError:
4557 kwargs = dict()
4558
4559 dot_notation = obj.task_dot_notation
4560 val = '.'.join(map(safe_str, [
4561 sorted(dot_notation), args, sorted(kwargs.items())]))
4562 return hashlib.sha1(val).hexdigest()
4563
4564 @classmethod
4565 def get_by_schedule_name(cls, schedule_name):
4566 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4567
4568 @classmethod
4569 def get_by_schedule_id(cls, schedule_id):
4570 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4571
4572 @property
4573 def task(self):
4574 return self.task_dot_notation
4575
4576 @property
4577 def schedule(self):
4578 from rhodecode.lib.celerylib.utils import raw_2_schedule
4579 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4580 return schedule
4581
4582 @property
4583 def args(self):
4584 try:
4585 return list(self.task_args or [])
4586 except ValueError:
4587 return list()
4588
4589 @property
4590 def kwargs(self):
4591 try:
4592 return dict(self.task_kwargs or {})
4593 except ValueError:
4594 return dict()
4595
4596 def _as_raw(self, val):
4597 if hasattr(val, 'de_coerce'):
4598 val = val.de_coerce()
4599 if val:
4600 val = json.dumps(val)
4601
4602 return val
4603
4604 @property
4605 def schedule_definition_raw(self):
4606 return self._as_raw(self.schedule_definition)
4607
4608 @property
4609 def args_raw(self):
4610 return self._as_raw(self.task_args)
4611
4612 @property
4613 def kwargs_raw(self):
4614 return self._as_raw(self.task_kwargs)
4615
4616 def __repr__(self):
4617 return '<DB:ScheduleEntry({}:{})>'.format(
4618 self.schedule_entry_id, self.schedule_name)
4619
4620
4621 @event.listens_for(ScheduleEntry, 'before_update')
4622 def update_task_uid(mapper, connection, target):
4623 target.task_uid = ScheduleEntry.get_uid(target)
4624
4625
4626 @event.listens_for(ScheduleEntry, 'before_insert')
4627 def set_task_uid(mapper, connection, target):
4628 target.task_uid = ScheduleEntry.get_uid(target)
4629
4630
4631 class _BaseBranchPerms(BaseModel):
4632 @classmethod
4633 def compute_hash(cls, value):
4634 return sha1_safe(value)
4635
4636 @hybrid_property
4637 def branch_pattern(self):
4638 return self._branch_pattern or '*'
4639
4640 @hybrid_property
4641 def branch_hash(self):
4642 return self._branch_hash
4643
4644 def _validate_glob(self, value):
4645 re.compile('^' + glob2re(value) + '$')
4646
4647 @branch_pattern.setter
4648 def branch_pattern(self, value):
4649 self._validate_glob(value)
4650 self._branch_pattern = value or '*'
4651 # set the Hash when setting the branch pattern
4652 self._branch_hash = self.compute_hash(self._branch_pattern)
4653
4654 def matches(self, branch):
4655 """
4656 Check if this the branch matches entry
4657
4658 :param branch: branch name for the commit
4659 """
4660
4661 branch = branch or ''
4662
4663 branch_matches = True
4664 if branch:
4665 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4666 branch_matches = bool(branch_regex.search(branch))
4667
4668 return branch_matches
4669
4670
4671 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
4672 __tablename__ = 'user_to_repo_branch_permissions'
4673 __table_args__ = (
4674 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4675 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4676 )
4677
4678 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
4679
4680 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
4681 repo = relationship('Repository', backref='user_branch_perms')
4682
4683 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
4684 permission = relationship('Permission')
4685
4686 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
4687 user_repo_to_perm = relationship('UserRepoToPerm')
4688
4689 rule_order = Column('rule_order', Integer(), nullable=False)
4690 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
4691 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
4692
4693 def __unicode__(self):
4694 return u'<UserBranchPermission(%s => %r)>' % (
4695 self.user_repo_to_perm, self.branch_pattern)
4696
4697
4698 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
4699 __tablename__ = 'user_group_to_repo_branch_permissions'
4700 __table_args__ = (
4701 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4702 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4703 )
4704
4705 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
4706
4707 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
4708 repo = relationship('Repository', backref='user_group_branch_perms')
4709
4710 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
4711 permission = relationship('Permission')
4712
4713 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
4714 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
4715
4716 rule_order = Column('rule_order', Integer(), nullable=False)
4717 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
4718 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
4719
4720 def __unicode__(self):
4721 return u'<UserBranchPermission(%s => %r)>' % (
4722 self.user_group_repo_to_perm, self.branch_pattern)
4723
4724
4725 class DbMigrateVersion(Base, BaseModel):
4726 __tablename__ = 'db_migrate_version'
4727 __table_args__ = (
4728 base_table_args,
4729 )
4730
4731 repository_id = Column('repository_id', String(250), primary_key=True)
4732 repository_path = Column('repository_path', Text)
4733 version = Column('version', Integer)
4734
4735 @classmethod
4736 def set_version(cls, version):
4737 """
4738 Helper for forcing a different version, usually for debugging purposes via ishell.
4739 """
4740 ver = DbMigrateVersion.query().first()
4741 ver.version = version
4742 Session().commit()
4743
4744
4745 class DbSession(Base, BaseModel):
4746 __tablename__ = 'db_session'
4747 __table_args__ = (
4748 base_table_args,
4749 )
4750
4751 def __repr__(self):
4752 return '<DB:DbSession({})>'.format(self.id)
4753
4754 id = Column('id', Integer())
4755 namespace = Column('namespace', String(255), primary_key=True)
4756 accessed = Column('accessed', DateTime, nullable=False)
4757 created = Column('created', DateTime, nullable=False)
4758 data = Column('data', PickleType, nullable=False)
This diff has been collapsed as it changes many lines, (4857 lines changed) Show them Hide them
@@ -0,0 +1,4857 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Database Models for RhodeCode Enterprise
23 """
24
25 import re
26 import os
27 import time
28 import hashlib
29 import logging
30 import datetime
31 import warnings
32 import ipaddress
33 import functools
34 import traceback
35 import collections
36
37 from sqlalchemy import (
38 or_, and_, not_, func, TypeDecorator, event,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType)
42 from sqlalchemy.sql.expression import true, false
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.orm import (
45 relationship, joinedload, class_mapper, validates, aliased)
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from pyramid import compat
52 from pyramid.threadlocal import get_current_request
53
54 from rhodecode.translation import _
55 from rhodecode.lib.vcs import get_vcs_instance
56 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
57 from rhodecode.lib.utils2 import (
58 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
60 glob2re, StrictAttributeDict, cleaned_uri)
61 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
62 JsonRaw
63 from rhodecode.lib.ext_json import json
64 from rhodecode.lib.caching_query import FromCache
65 from rhodecode.lib.encrypt import AESCipher
66
67 from rhodecode.model.meta import Base, Session
68
69 URL_SEP = '/'
70 log = logging.getLogger(__name__)
71
72 # =============================================================================
73 # BASE CLASSES
74 # =============================================================================
75
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 # beaker.session.secret if first is not set.
78 # and initialized at environment.py
79 ENCRYPTION_KEY = None
80
81 # used to sort permissions by types, '#' used here is not allowed to be in
82 # usernames, and it's very early in sorted string.printable table.
83 PERMISSION_TYPE_SORT = {
84 'admin': '####',
85 'write': '###',
86 'read': '##',
87 'none': '#',
88 }
89
90
91 def display_user_sort(obj):
92 """
93 Sort function used to sort permissions in .permissions() function of
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 of all other resources
96 """
97
98 if obj.username == User.DEFAULT_USER:
99 return '#####'
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 return prefix + obj.username
102
103
104 def display_user_group_sort(obj):
105 """
106 Sort function used to sort permissions in .permissions() function of
107 Repository, RepoGroup, UserGroup. Also it put the default user in front
108 of all other resources
109 """
110
111 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
112 return prefix + obj.users_group_name
113
114
115 def _hash_key(k):
116 return sha1_safe(k)
117
118
119 def in_filter_generator(qry, items, limit=500):
120 """
121 Splits IN() into multiple with OR
122 e.g.::
123 cnt = Repository.query().filter(
124 or_(
125 *in_filter_generator(Repository.repo_id, range(100000))
126 )).count()
127 """
128 if not items:
129 # empty list will cause empty query which might cause security issues
130 # this can lead to hidden unpleasant results
131 items = [-1]
132
133 parts = []
134 for chunk in xrange(0, len(items), limit):
135 parts.append(
136 qry.in_(items[chunk: chunk + limit])
137 )
138
139 return parts
140
141
142 base_table_args = {
143 'extend_existing': True,
144 'mysql_engine': 'InnoDB',
145 'mysql_charset': 'utf8',
146 'sqlite_autoincrement': True
147 }
148
149
150 class EncryptedTextValue(TypeDecorator):
151 """
152 Special column for encrypted long text data, use like::
153
154 value = Column("encrypted_value", EncryptedValue(), nullable=False)
155
156 This column is intelligent so if value is in unencrypted form it return
157 unencrypted form, but on save it always encrypts
158 """
159 impl = Text
160
161 def process_bind_param(self, value, dialect):
162 if not value:
163 return value
164 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
165 # protect against double encrypting if someone manually starts
166 # doing
167 raise ValueError('value needs to be in unencrypted format, ie. '
168 'not starting with enc$aes')
169 return 'enc$aes_hmac$%s' % AESCipher(
170 ENCRYPTION_KEY, hmac=True).encrypt(value)
171
172 def process_result_value(self, value, dialect):
173 import rhodecode
174
175 if not value:
176 return value
177
178 parts = value.split('$', 3)
179 if not len(parts) == 3:
180 # probably not encrypted values
181 return value
182 else:
183 if parts[0] != 'enc':
184 # parts ok but without our header ?
185 return value
186 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
187 'rhodecode.encrypted_values.strict') or True)
188 # at that stage we know it's our encryption
189 if parts[1] == 'aes':
190 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
191 elif parts[1] == 'aes_hmac':
192 decrypted_data = AESCipher(
193 ENCRYPTION_KEY, hmac=True,
194 strict_verification=enc_strict_mode).decrypt(parts[2])
195 else:
196 raise ValueError(
197 'Encryption type part is wrong, must be `aes` '
198 'or `aes_hmac`, got `%s` instead' % (parts[1]))
199 return decrypted_data
200
201
202 class BaseModel(object):
203 """
204 Base Model for all classes
205 """
206
207 @classmethod
208 def _get_keys(cls):
209 """return column names for this model """
210 return class_mapper(cls).c.keys()
211
212 def get_dict(self):
213 """
214 return dict with keys and values corresponding
215 to this model data """
216
217 d = {}
218 for k in self._get_keys():
219 d[k] = getattr(self, k)
220
221 # also use __json__() if present to get additional fields
222 _json_attr = getattr(self, '__json__', None)
223 if _json_attr:
224 # update with attributes from __json__
225 if callable(_json_attr):
226 _json_attr = _json_attr()
227 for k, val in _json_attr.iteritems():
228 d[k] = val
229 return d
230
231 def get_appstruct(self):
232 """return list with keys and values tuples corresponding
233 to this model data """
234
235 lst = []
236 for k in self._get_keys():
237 lst.append((k, getattr(self, k),))
238 return lst
239
240 def populate_obj(self, populate_dict):
241 """populate model with data from given populate_dict"""
242
243 for k in self._get_keys():
244 if k in populate_dict:
245 setattr(self, k, populate_dict[k])
246
247 @classmethod
248 def query(cls):
249 return Session().query(cls)
250
251 @classmethod
252 def get(cls, id_):
253 if id_:
254 return cls.query().get(id_)
255
256 @classmethod
257 def get_or_404(cls, id_):
258 from pyramid.httpexceptions import HTTPNotFound
259
260 try:
261 id_ = int(id_)
262 except (TypeError, ValueError):
263 raise HTTPNotFound()
264
265 res = cls.query().get(id_)
266 if not res:
267 raise HTTPNotFound()
268 return res
269
270 @classmethod
271 def getAll(cls):
272 # deprecated and left for backward compatibility
273 return cls.get_all()
274
275 @classmethod
276 def get_all(cls):
277 return cls.query().all()
278
279 @classmethod
280 def delete(cls, id_):
281 obj = cls.query().get(id_)
282 Session().delete(obj)
283
284 @classmethod
285 def identity_cache(cls, session, attr_name, value):
286 exist_in_session = []
287 for (item_cls, pkey), instance in session.identity_map.items():
288 if cls == item_cls and getattr(instance, attr_name) == value:
289 exist_in_session.append(instance)
290 if exist_in_session:
291 if len(exist_in_session) == 1:
292 return exist_in_session[0]
293 log.exception(
294 'multiple objects with attr %s and '
295 'value %s found with same name: %r',
296 attr_name, value, exist_in_session)
297
298 def __repr__(self):
299 if hasattr(self, '__unicode__'):
300 # python repr needs to return str
301 try:
302 return safe_str(self.__unicode__())
303 except UnicodeDecodeError:
304 pass
305 return '<DB:%s>' % (self.__class__.__name__)
306
307
308 class RhodeCodeSetting(Base, BaseModel):
309 __tablename__ = 'rhodecode_settings'
310 __table_args__ = (
311 UniqueConstraint('app_settings_name'),
312 base_table_args
313 )
314
315 SETTINGS_TYPES = {
316 'str': safe_str,
317 'int': safe_int,
318 'unicode': safe_unicode,
319 'bool': str2bool,
320 'list': functools.partial(aslist, sep=',')
321 }
322 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
323 GLOBAL_CONF_KEY = 'app_settings'
324
325 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
326 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
327 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
328 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
329
330 def __init__(self, key='', val='', type='unicode'):
331 self.app_settings_name = key
332 self.app_settings_type = type
333 self.app_settings_value = val
334
335 @validates('_app_settings_value')
336 def validate_settings_value(self, key, val):
337 assert type(val) == unicode
338 return val
339
340 @hybrid_property
341 def app_settings_value(self):
342 v = self._app_settings_value
343 _type = self.app_settings_type
344 if _type:
345 _type = self.app_settings_type.split('.')[0]
346 # decode the encrypted value
347 if 'encrypted' in self.app_settings_type:
348 cipher = EncryptedTextValue()
349 v = safe_unicode(cipher.process_result_value(v, None))
350
351 converter = self.SETTINGS_TYPES.get(_type) or \
352 self.SETTINGS_TYPES['unicode']
353 return converter(v)
354
355 @app_settings_value.setter
356 def app_settings_value(self, val):
357 """
358 Setter that will always make sure we use unicode in app_settings_value
359
360 :param val:
361 """
362 val = safe_unicode(val)
363 # encode the encrypted value
364 if 'encrypted' in self.app_settings_type:
365 cipher = EncryptedTextValue()
366 val = safe_unicode(cipher.process_bind_param(val, None))
367 self._app_settings_value = val
368
369 @hybrid_property
370 def app_settings_type(self):
371 return self._app_settings_type
372
373 @app_settings_type.setter
374 def app_settings_type(self, val):
375 if val.split('.')[0] not in self.SETTINGS_TYPES:
376 raise Exception('type must be one of %s got %s'
377 % (self.SETTINGS_TYPES.keys(), val))
378 self._app_settings_type = val
379
380 @classmethod
381 def get_by_prefix(cls, prefix):
382 return RhodeCodeSetting.query()\
383 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
384 .all()
385
386 def __unicode__(self):
387 return u"<%s('%s:%s[%s]')>" % (
388 self.__class__.__name__,
389 self.app_settings_name, self.app_settings_value,
390 self.app_settings_type
391 )
392
393
394 class RhodeCodeUi(Base, BaseModel):
395 __tablename__ = 'rhodecode_ui'
396 __table_args__ = (
397 UniqueConstraint('ui_key'),
398 base_table_args
399 )
400
401 HOOK_REPO_SIZE = 'changegroup.repo_size'
402 # HG
403 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
404 HOOK_PULL = 'outgoing.pull_logger'
405 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
406 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
407 HOOK_PUSH = 'changegroup.push_logger'
408 HOOK_PUSH_KEY = 'pushkey.key_push'
409
410 # TODO: johbo: Unify way how hooks are configured for git and hg,
411 # git part is currently hardcoded.
412
413 # SVN PATTERNS
414 SVN_BRANCH_ID = 'vcs_svn_branch'
415 SVN_TAG_ID = 'vcs_svn_tag'
416
417 ui_id = Column(
418 "ui_id", Integer(), nullable=False, unique=True, default=None,
419 primary_key=True)
420 ui_section = Column(
421 "ui_section", String(255), nullable=True, unique=None, default=None)
422 ui_key = Column(
423 "ui_key", String(255), nullable=True, unique=None, default=None)
424 ui_value = Column(
425 "ui_value", String(255), nullable=True, unique=None, default=None)
426 ui_active = Column(
427 "ui_active", Boolean(), nullable=True, unique=None, default=True)
428
429 def __repr__(self):
430 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
431 self.ui_key, self.ui_value)
432
433
434 class RepoRhodeCodeSetting(Base, BaseModel):
435 __tablename__ = 'repo_rhodecode_settings'
436 __table_args__ = (
437 UniqueConstraint(
438 'app_settings_name', 'repository_id',
439 name='uq_repo_rhodecode_setting_name_repo_id'),
440 base_table_args
441 )
442
443 repository_id = Column(
444 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
445 nullable=False)
446 app_settings_id = Column(
447 "app_settings_id", Integer(), nullable=False, unique=True,
448 default=None, primary_key=True)
449 app_settings_name = Column(
450 "app_settings_name", String(255), nullable=True, unique=None,
451 default=None)
452 _app_settings_value = Column(
453 "app_settings_value", String(4096), nullable=True, unique=None,
454 default=None)
455 _app_settings_type = Column(
456 "app_settings_type", String(255), nullable=True, unique=None,
457 default=None)
458
459 repository = relationship('Repository')
460
461 def __init__(self, repository_id, key='', val='', type='unicode'):
462 self.repository_id = repository_id
463 self.app_settings_name = key
464 self.app_settings_type = type
465 self.app_settings_value = val
466
467 @validates('_app_settings_value')
468 def validate_settings_value(self, key, val):
469 assert type(val) == unicode
470 return val
471
472 @hybrid_property
473 def app_settings_value(self):
474 v = self._app_settings_value
475 type_ = self.app_settings_type
476 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
477 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
478 return converter(v)
479
480 @app_settings_value.setter
481 def app_settings_value(self, val):
482 """
483 Setter that will always make sure we use unicode in app_settings_value
484
485 :param val:
486 """
487 self._app_settings_value = safe_unicode(val)
488
489 @hybrid_property
490 def app_settings_type(self):
491 return self._app_settings_type
492
493 @app_settings_type.setter
494 def app_settings_type(self, val):
495 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
496 if val not in SETTINGS_TYPES:
497 raise Exception('type must be one of %s got %s'
498 % (SETTINGS_TYPES.keys(), val))
499 self._app_settings_type = val
500
501 def __unicode__(self):
502 return u"<%s('%s:%s:%s[%s]')>" % (
503 self.__class__.__name__, self.repository.repo_name,
504 self.app_settings_name, self.app_settings_value,
505 self.app_settings_type
506 )
507
508
509 class RepoRhodeCodeUi(Base, BaseModel):
510 __tablename__ = 'repo_rhodecode_ui'
511 __table_args__ = (
512 UniqueConstraint(
513 'repository_id', 'ui_section', 'ui_key',
514 name='uq_repo_rhodecode_ui_repository_id_section_key'),
515 base_table_args
516 )
517
518 repository_id = Column(
519 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
520 nullable=False)
521 ui_id = Column(
522 "ui_id", Integer(), nullable=False, unique=True, default=None,
523 primary_key=True)
524 ui_section = Column(
525 "ui_section", String(255), nullable=True, unique=None, default=None)
526 ui_key = Column(
527 "ui_key", String(255), nullable=True, unique=None, default=None)
528 ui_value = Column(
529 "ui_value", String(255), nullable=True, unique=None, default=None)
530 ui_active = Column(
531 "ui_active", Boolean(), nullable=True, unique=None, default=True)
532
533 repository = relationship('Repository')
534
535 def __repr__(self):
536 return '<%s[%s:%s]%s=>%s]>' % (
537 self.__class__.__name__, self.repository.repo_name,
538 self.ui_section, self.ui_key, self.ui_value)
539
540
541 class User(Base, BaseModel):
542 __tablename__ = 'users'
543 __table_args__ = (
544 UniqueConstraint('username'), UniqueConstraint('email'),
545 Index('u_username_idx', 'username'),
546 Index('u_email_idx', 'email'),
547 base_table_args
548 )
549
550 DEFAULT_USER = 'default'
551 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
552 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
553
554 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
555 username = Column("username", String(255), nullable=True, unique=None, default=None)
556 password = Column("password", String(255), nullable=True, unique=None, default=None)
557 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
558 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
559 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
560 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
561 _email = Column("email", String(255), nullable=True, unique=None, default=None)
562 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
563 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
564
565 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
566 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
567 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
568 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
569 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
570 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
571
572 user_log = relationship('UserLog')
573 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
574
575 repositories = relationship('Repository')
576 repository_groups = relationship('RepoGroup')
577 user_groups = relationship('UserGroup')
578
579 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
580 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
581
582 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
583 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
584 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
585
586 group_member = relationship('UserGroupMember', cascade='all')
587
588 notifications = relationship('UserNotification', cascade='all')
589 # notifications assigned to this user
590 user_created_notifications = relationship('Notification', cascade='all')
591 # comments created by this user
592 user_comments = relationship('ChangesetComment', cascade='all')
593 # user profile extra info
594 user_emails = relationship('UserEmailMap', cascade='all')
595 user_ip_map = relationship('UserIpMap', cascade='all')
596 user_auth_tokens = relationship('UserApiKeys', cascade='all')
597 user_ssh_keys = relationship('UserSshKeys', cascade='all')
598
599 # gists
600 user_gists = relationship('Gist', cascade='all')
601 # user pull requests
602 user_pull_requests = relationship('PullRequest', cascade='all')
603 # external identities
604 extenal_identities = relationship(
605 'ExternalIdentity',
606 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
607 cascade='all')
608 # review rules
609 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
610
611 def __unicode__(self):
612 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
613 self.user_id, self.username)
614
615 @hybrid_property
616 def email(self):
617 return self._email
618
619 @email.setter
620 def email(self, val):
621 self._email = val.lower() if val else None
622
623 @hybrid_property
624 def first_name(self):
625 from rhodecode.lib import helpers as h
626 if self.name:
627 return h.escape(self.name)
628 return self.name
629
630 @hybrid_property
631 def last_name(self):
632 from rhodecode.lib import helpers as h
633 if self.lastname:
634 return h.escape(self.lastname)
635 return self.lastname
636
637 @hybrid_property
638 def api_key(self):
639 """
640 Fetch if exist an auth-token with role ALL connected to this user
641 """
642 user_auth_token = UserApiKeys.query()\
643 .filter(UserApiKeys.user_id == self.user_id)\
644 .filter(or_(UserApiKeys.expires == -1,
645 UserApiKeys.expires >= time.time()))\
646 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
647 if user_auth_token:
648 user_auth_token = user_auth_token.api_key
649
650 return user_auth_token
651
652 @api_key.setter
653 def api_key(self, val):
654 # don't allow to set API key this is deprecated for now
655 self._api_key = None
656
657 @property
658 def reviewer_pull_requests(self):
659 return PullRequestReviewers.query() \
660 .options(joinedload(PullRequestReviewers.pull_request)) \
661 .filter(PullRequestReviewers.user_id == self.user_id) \
662 .all()
663
664 @property
665 def firstname(self):
666 # alias for future
667 return self.name
668
669 @property
670 def emails(self):
671 other = UserEmailMap.query()\
672 .filter(UserEmailMap.user == self) \
673 .order_by(UserEmailMap.email_id.asc()) \
674 .all()
675 return [self.email] + [x.email for x in other]
676
677 @property
678 def auth_tokens(self):
679 auth_tokens = self.get_auth_tokens()
680 return [x.api_key for x in auth_tokens]
681
682 def get_auth_tokens(self):
683 return UserApiKeys.query()\
684 .filter(UserApiKeys.user == self)\
685 .order_by(UserApiKeys.user_api_key_id.asc())\
686 .all()
687
688 @LazyProperty
689 def feed_token(self):
690 return self.get_feed_token()
691
692 def get_feed_token(self, cache=True):
693 feed_tokens = UserApiKeys.query()\
694 .filter(UserApiKeys.user == self)\
695 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
696 if cache:
697 feed_tokens = feed_tokens.options(
698 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
699
700 feed_tokens = feed_tokens.all()
701 if feed_tokens:
702 return feed_tokens[0].api_key
703 return 'NO_FEED_TOKEN_AVAILABLE'
704
705 @classmethod
706 def get(cls, user_id, cache=False):
707 if not user_id:
708 return
709
710 user = cls.query()
711 if cache:
712 user = user.options(
713 FromCache("sql_cache_short", "get_users_%s" % user_id))
714 return user.get(user_id)
715
716 @classmethod
717 def extra_valid_auth_tokens(cls, user, role=None):
718 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
719 .filter(or_(UserApiKeys.expires == -1,
720 UserApiKeys.expires >= time.time()))
721 if role:
722 tokens = tokens.filter(or_(UserApiKeys.role == role,
723 UserApiKeys.role == UserApiKeys.ROLE_ALL))
724 return tokens.all()
725
726 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
727 from rhodecode.lib import auth
728
729 log.debug('Trying to authenticate user: %s via auth-token, '
730 'and roles: %s', self, roles)
731
732 if not auth_token:
733 return False
734
735 crypto_backend = auth.crypto_backend()
736
737 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
738 tokens_q = UserApiKeys.query()\
739 .filter(UserApiKeys.user_id == self.user_id)\
740 .filter(or_(UserApiKeys.expires == -1,
741 UserApiKeys.expires >= time.time()))
742
743 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
744
745 plain_tokens = []
746 hash_tokens = []
747
748 user_tokens = tokens_q.all()
749 log.debug('Found %s user tokens to check for authentication', len(user_tokens))
750 for token in user_tokens:
751 log.debug('AUTH_TOKEN: checking if user token with id `%s` matches',
752 token.user_api_key_id)
753 # verify scope first, since it's way faster than hash calculation of
754 # encrypted tokens
755 if token.repo_id:
756 # token has a scope, we need to verify it
757 if scope_repo_id != token.repo_id:
758 log.debug(
759 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
760 'and calling scope is:%s, skipping further checks',
761 token.repo, scope_repo_id)
762 # token has a scope, and it doesn't match, skip token
763 continue
764
765 if token.api_key.startswith(crypto_backend.ENC_PREF):
766 hash_tokens.append(token.api_key)
767 else:
768 plain_tokens.append(token.api_key)
769
770 is_plain_match = auth_token in plain_tokens
771 if is_plain_match:
772 return True
773
774 for hashed in hash_tokens:
775 # NOTE(marcink): this is expensive to calculate, but most secure
776 match = crypto_backend.hash_check(auth_token, hashed)
777 if match:
778 return True
779
780 return False
781
782 @property
783 def ip_addresses(self):
784 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
785 return [x.ip_addr for x in ret]
786
787 @property
788 def username_and_name(self):
789 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
790
791 @property
792 def username_or_name_or_email(self):
793 full_name = self.full_name if self.full_name is not ' ' else None
794 return self.username or full_name or self.email
795
796 @property
797 def full_name(self):
798 return '%s %s' % (self.first_name, self.last_name)
799
800 @property
801 def full_name_or_username(self):
802 return ('%s %s' % (self.first_name, self.last_name)
803 if (self.first_name and self.last_name) else self.username)
804
805 @property
806 def full_contact(self):
807 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
808
809 @property
810 def short_contact(self):
811 return '%s %s' % (self.first_name, self.last_name)
812
813 @property
814 def is_admin(self):
815 return self.admin
816
817 def AuthUser(self, **kwargs):
818 """
819 Returns instance of AuthUser for this user
820 """
821 from rhodecode.lib.auth import AuthUser
822 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
823
824 @hybrid_property
825 def user_data(self):
826 if not self._user_data:
827 return {}
828
829 try:
830 return json.loads(self._user_data)
831 except TypeError:
832 return {}
833
834 @user_data.setter
835 def user_data(self, val):
836 if not isinstance(val, dict):
837 raise Exception('user_data must be dict, got %s' % type(val))
838 try:
839 self._user_data = json.dumps(val)
840 except Exception:
841 log.error(traceback.format_exc())
842
843 @classmethod
844 def get_by_username(cls, username, case_insensitive=False,
845 cache=False, identity_cache=False):
846 session = Session()
847
848 if case_insensitive:
849 q = cls.query().filter(
850 func.lower(cls.username) == func.lower(username))
851 else:
852 q = cls.query().filter(cls.username == username)
853
854 if cache:
855 if identity_cache:
856 val = cls.identity_cache(session, 'username', username)
857 if val:
858 return val
859 else:
860 cache_key = "get_user_by_name_%s" % _hash_key(username)
861 q = q.options(
862 FromCache("sql_cache_short", cache_key))
863
864 return q.scalar()
865
866 @classmethod
867 def get_by_auth_token(cls, auth_token, cache=False):
868 q = UserApiKeys.query()\
869 .filter(UserApiKeys.api_key == auth_token)\
870 .filter(or_(UserApiKeys.expires == -1,
871 UserApiKeys.expires >= time.time()))
872 if cache:
873 q = q.options(
874 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
875
876 match = q.first()
877 if match:
878 return match.user
879
880 @classmethod
881 def get_by_email(cls, email, case_insensitive=False, cache=False):
882
883 if case_insensitive:
884 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
885
886 else:
887 q = cls.query().filter(cls.email == email)
888
889 email_key = _hash_key(email)
890 if cache:
891 q = q.options(
892 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
893
894 ret = q.scalar()
895 if ret is None:
896 q = UserEmailMap.query()
897 # try fetching in alternate email map
898 if case_insensitive:
899 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
900 else:
901 q = q.filter(UserEmailMap.email == email)
902 q = q.options(joinedload(UserEmailMap.user))
903 if cache:
904 q = q.options(
905 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
906 ret = getattr(q.scalar(), 'user', None)
907
908 return ret
909
910 @classmethod
911 def get_from_cs_author(cls, author):
912 """
913 Tries to get User objects out of commit author string
914
915 :param author:
916 """
917 from rhodecode.lib.helpers import email, author_name
918 # Valid email in the attribute passed, see if they're in the system
919 _email = email(author)
920 if _email:
921 user = cls.get_by_email(_email, case_insensitive=True)
922 if user:
923 return user
924 # Maybe we can match by username?
925 _author = author_name(author)
926 user = cls.get_by_username(_author, case_insensitive=True)
927 if user:
928 return user
929
930 def update_userdata(self, **kwargs):
931 usr = self
932 old = usr.user_data
933 old.update(**kwargs)
934 usr.user_data = old
935 Session().add(usr)
936 log.debug('updated userdata with ', kwargs)
937
938 def update_lastlogin(self):
939 """Update user lastlogin"""
940 self.last_login = datetime.datetime.now()
941 Session().add(self)
942 log.debug('updated user %s lastlogin', self.username)
943
944 def update_password(self, new_password):
945 from rhodecode.lib.auth import get_crypt_password
946
947 self.password = get_crypt_password(new_password)
948 Session().add(self)
949
950 @classmethod
951 def get_first_super_admin(cls):
952 user = User.query()\
953 .filter(User.admin == true()) \
954 .order_by(User.user_id.asc()) \
955 .first()
956
957 if user is None:
958 raise Exception('FATAL: Missing administrative account!')
959 return user
960
961 @classmethod
962 def get_all_super_admins(cls, only_active=False):
963 """
964 Returns all admin accounts sorted by username
965 """
966 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
967 if only_active:
968 qry = qry.filter(User.active == true())
969 return qry.all()
970
971 @classmethod
972 def get_default_user(cls, cache=False, refresh=False):
973 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
974 if user is None:
975 raise Exception('FATAL: Missing default account!')
976 if refresh:
977 # The default user might be based on outdated state which
978 # has been loaded from the cache.
979 # A call to refresh() ensures that the
980 # latest state from the database is used.
981 Session().refresh(user)
982 return user
983
984 def _get_default_perms(self, user, suffix=''):
985 from rhodecode.model.permission import PermissionModel
986 return PermissionModel().get_default_perms(user.user_perms, suffix)
987
988 def get_default_perms(self, suffix=''):
989 return self._get_default_perms(self, suffix)
990
991 def get_api_data(self, include_secrets=False, details='full'):
992 """
993 Common function for generating user related data for API
994
995 :param include_secrets: By default secrets in the API data will be replaced
996 by a placeholder value to prevent exposing this data by accident. In case
997 this data shall be exposed, set this flag to ``True``.
998
999 :param details: details can be 'basic|full' basic gives only a subset of
1000 the available user information that includes user_id, name and emails.
1001 """
1002 user = self
1003 user_data = self.user_data
1004 data = {
1005 'user_id': user.user_id,
1006 'username': user.username,
1007 'firstname': user.name,
1008 'lastname': user.lastname,
1009 'email': user.email,
1010 'emails': user.emails,
1011 }
1012 if details == 'basic':
1013 return data
1014
1015 auth_token_length = 40
1016 auth_token_replacement = '*' * auth_token_length
1017
1018 extras = {
1019 'auth_tokens': [auth_token_replacement],
1020 'active': user.active,
1021 'admin': user.admin,
1022 'extern_type': user.extern_type,
1023 'extern_name': user.extern_name,
1024 'last_login': user.last_login,
1025 'last_activity': user.last_activity,
1026 'ip_addresses': user.ip_addresses,
1027 'language': user_data.get('language')
1028 }
1029 data.update(extras)
1030
1031 if include_secrets:
1032 data['auth_tokens'] = user.auth_tokens
1033 return data
1034
1035 def __json__(self):
1036 data = {
1037 'full_name': self.full_name,
1038 'full_name_or_username': self.full_name_or_username,
1039 'short_contact': self.short_contact,
1040 'full_contact': self.full_contact,
1041 }
1042 data.update(self.get_api_data())
1043 return data
1044
1045
1046 class UserApiKeys(Base, BaseModel):
1047 __tablename__ = 'user_api_keys'
1048 __table_args__ = (
1049 Index('uak_api_key_idx', 'api_key', unique=True),
1050 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1051 base_table_args
1052 )
1053 __mapper_args__ = {}
1054
1055 # ApiKey role
1056 ROLE_ALL = 'token_role_all'
1057 ROLE_HTTP = 'token_role_http'
1058 ROLE_VCS = 'token_role_vcs'
1059 ROLE_API = 'token_role_api'
1060 ROLE_FEED = 'token_role_feed'
1061 ROLE_PASSWORD_RESET = 'token_password_reset'
1062
1063 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1064
1065 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1066 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1067 api_key = Column("api_key", String(255), nullable=False, unique=True)
1068 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1069 expires = Column('expires', Float(53), nullable=False)
1070 role = Column('role', String(255), nullable=True)
1071 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1072
1073 # scope columns
1074 repo_id = Column(
1075 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1076 nullable=True, unique=None, default=None)
1077 repo = relationship('Repository', lazy='joined')
1078
1079 repo_group_id = Column(
1080 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1081 nullable=True, unique=None, default=None)
1082 repo_group = relationship('RepoGroup', lazy='joined')
1083
1084 user = relationship('User', lazy='joined')
1085
1086 def __unicode__(self):
1087 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1088
1089 def __json__(self):
1090 data = {
1091 'auth_token': self.api_key,
1092 'role': self.role,
1093 'scope': self.scope_humanized,
1094 'expired': self.expired
1095 }
1096 return data
1097
1098 def get_api_data(self, include_secrets=False):
1099 data = self.__json__()
1100 if include_secrets:
1101 return data
1102 else:
1103 data['auth_token'] = self.token_obfuscated
1104 return data
1105
1106 @hybrid_property
1107 def description_safe(self):
1108 from rhodecode.lib import helpers as h
1109 return h.escape(self.description)
1110
1111 @property
1112 def expired(self):
1113 if self.expires == -1:
1114 return False
1115 return time.time() > self.expires
1116
1117 @classmethod
1118 def _get_role_name(cls, role):
1119 return {
1120 cls.ROLE_ALL: _('all'),
1121 cls.ROLE_HTTP: _('http/web interface'),
1122 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1123 cls.ROLE_API: _('api calls'),
1124 cls.ROLE_FEED: _('feed access'),
1125 }.get(role, role)
1126
1127 @property
1128 def role_humanized(self):
1129 return self._get_role_name(self.role)
1130
1131 def _get_scope(self):
1132 if self.repo:
1133 return 'Repository: {}'.format(self.repo.repo_name)
1134 if self.repo_group:
1135 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1136 return 'Global'
1137
1138 @property
1139 def scope_humanized(self):
1140 return self._get_scope()
1141
1142 @property
1143 def token_obfuscated(self):
1144 if self.api_key:
1145 return self.api_key[:4] + "****"
1146
1147
1148 class UserEmailMap(Base, BaseModel):
1149 __tablename__ = 'user_email_map'
1150 __table_args__ = (
1151 Index('uem_email_idx', 'email'),
1152 UniqueConstraint('email'),
1153 base_table_args
1154 )
1155 __mapper_args__ = {}
1156
1157 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1158 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1159 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1160 user = relationship('User', lazy='joined')
1161
1162 @validates('_email')
1163 def validate_email(self, key, email):
1164 # check if this email is not main one
1165 main_email = Session().query(User).filter(User.email == email).scalar()
1166 if main_email is not None:
1167 raise AttributeError('email %s is present is user table' % email)
1168 return email
1169
1170 @hybrid_property
1171 def email(self):
1172 return self._email
1173
1174 @email.setter
1175 def email(self, val):
1176 self._email = val.lower() if val else None
1177
1178
1179 class UserIpMap(Base, BaseModel):
1180 __tablename__ = 'user_ip_map'
1181 __table_args__ = (
1182 UniqueConstraint('user_id', 'ip_addr'),
1183 base_table_args
1184 )
1185 __mapper_args__ = {}
1186
1187 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1189 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1190 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1191 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1192 user = relationship('User', lazy='joined')
1193
1194 @hybrid_property
1195 def description_safe(self):
1196 from rhodecode.lib import helpers as h
1197 return h.escape(self.description)
1198
1199 @classmethod
1200 def _get_ip_range(cls, ip_addr):
1201 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1202 return [str(net.network_address), str(net.broadcast_address)]
1203
1204 def __json__(self):
1205 return {
1206 'ip_addr': self.ip_addr,
1207 'ip_range': self._get_ip_range(self.ip_addr),
1208 }
1209
1210 def __unicode__(self):
1211 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1212 self.user_id, self.ip_addr)
1213
1214
1215 class UserSshKeys(Base, BaseModel):
1216 __tablename__ = 'user_ssh_keys'
1217 __table_args__ = (
1218 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1219
1220 UniqueConstraint('ssh_key_fingerprint'),
1221
1222 base_table_args
1223 )
1224 __mapper_args__ = {}
1225
1226 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1227 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1228 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1229
1230 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1231
1232 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1233 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1234 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1235
1236 user = relationship('User', lazy='joined')
1237
1238 def __json__(self):
1239 data = {
1240 'ssh_fingerprint': self.ssh_key_fingerprint,
1241 'description': self.description,
1242 'created_on': self.created_on
1243 }
1244 return data
1245
1246 def get_api_data(self):
1247 data = self.__json__()
1248 return data
1249
1250
1251 class UserLog(Base, BaseModel):
1252 __tablename__ = 'user_logs'
1253 __table_args__ = (
1254 base_table_args,
1255 )
1256
1257 VERSION_1 = 'v1'
1258 VERSION_2 = 'v2'
1259 VERSIONS = [VERSION_1, VERSION_2]
1260
1261 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1262 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1263 username = Column("username", String(255), nullable=True, unique=None, default=None)
1264 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1265 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1266 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1267 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1268 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1269
1270 version = Column("version", String(255), nullable=True, default=VERSION_1)
1271 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1272 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1273
1274 def __unicode__(self):
1275 return u"<%s('id:%s:%s')>" % (
1276 self.__class__.__name__, self.repository_name, self.action)
1277
1278 def __json__(self):
1279 return {
1280 'user_id': self.user_id,
1281 'username': self.username,
1282 'repository_id': self.repository_id,
1283 'repository_name': self.repository_name,
1284 'user_ip': self.user_ip,
1285 'action_date': self.action_date,
1286 'action': self.action,
1287 }
1288
1289 @hybrid_property
1290 def entry_id(self):
1291 return self.user_log_id
1292
1293 @property
1294 def action_as_day(self):
1295 return datetime.date(*self.action_date.timetuple()[:3])
1296
1297 user = relationship('User')
1298 repository = relationship('Repository', cascade='')
1299
1300
1301 class UserGroup(Base, BaseModel):
1302 __tablename__ = 'users_groups'
1303 __table_args__ = (
1304 base_table_args,
1305 )
1306
1307 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1308 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1309 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1310 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1311 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1312 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1313 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1314 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1315
1316 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1317 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1318 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1319 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1320 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1321 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1322
1323 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1324 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1325
1326 @classmethod
1327 def _load_group_data(cls, column):
1328 if not column:
1329 return {}
1330
1331 try:
1332 return json.loads(column) or {}
1333 except TypeError:
1334 return {}
1335
1336 @hybrid_property
1337 def description_safe(self):
1338 from rhodecode.lib import helpers as h
1339 return h.escape(self.user_group_description)
1340
1341 @hybrid_property
1342 def group_data(self):
1343 return self._load_group_data(self._group_data)
1344
1345 @group_data.expression
1346 def group_data(self, **kwargs):
1347 return self._group_data
1348
1349 @group_data.setter
1350 def group_data(self, val):
1351 try:
1352 self._group_data = json.dumps(val)
1353 except Exception:
1354 log.error(traceback.format_exc())
1355
1356 @classmethod
1357 def _load_sync(cls, group_data):
1358 if group_data:
1359 return group_data.get('extern_type')
1360
1361 @property
1362 def sync(self):
1363 return self._load_sync(self.group_data)
1364
1365 def __unicode__(self):
1366 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1367 self.users_group_id,
1368 self.users_group_name)
1369
1370 @classmethod
1371 def get_by_group_name(cls, group_name, cache=False,
1372 case_insensitive=False):
1373 if case_insensitive:
1374 q = cls.query().filter(func.lower(cls.users_group_name) ==
1375 func.lower(group_name))
1376
1377 else:
1378 q = cls.query().filter(cls.users_group_name == group_name)
1379 if cache:
1380 q = q.options(
1381 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1382 return q.scalar()
1383
1384 @classmethod
1385 def get(cls, user_group_id, cache=False):
1386 if not user_group_id:
1387 return
1388
1389 user_group = cls.query()
1390 if cache:
1391 user_group = user_group.options(
1392 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1393 return user_group.get(user_group_id)
1394
1395 def permissions(self, with_admins=True, with_owner=True,
1396 expand_from_user_groups=False):
1397 """
1398 Permissions for user groups
1399 """
1400 _admin_perm = 'usergroup.admin'
1401
1402 owner_row = []
1403 if with_owner:
1404 usr = AttributeDict(self.user.get_dict())
1405 usr.owner_row = True
1406 usr.permission = _admin_perm
1407 owner_row.append(usr)
1408
1409 super_admin_ids = []
1410 super_admin_rows = []
1411 if with_admins:
1412 for usr in User.get_all_super_admins():
1413 super_admin_ids.append(usr.user_id)
1414 # if this admin is also owner, don't double the record
1415 if usr.user_id == owner_row[0].user_id:
1416 owner_row[0].admin_row = True
1417 else:
1418 usr = AttributeDict(usr.get_dict())
1419 usr.admin_row = True
1420 usr.permission = _admin_perm
1421 super_admin_rows.append(usr)
1422
1423 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1424 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1425 joinedload(UserUserGroupToPerm.user),
1426 joinedload(UserUserGroupToPerm.permission),)
1427
1428 # get owners and admins and permissions. We do a trick of re-writing
1429 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1430 # has a global reference and changing one object propagates to all
1431 # others. This means if admin is also an owner admin_row that change
1432 # would propagate to both objects
1433 perm_rows = []
1434 for _usr in q.all():
1435 usr = AttributeDict(_usr.user.get_dict())
1436 # if this user is also owner/admin, mark as duplicate record
1437 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1438 usr.duplicate_perm = True
1439 usr.permission = _usr.permission.permission_name
1440 perm_rows.append(usr)
1441
1442 # filter the perm rows by 'default' first and then sort them by
1443 # admin,write,read,none permissions sorted again alphabetically in
1444 # each group
1445 perm_rows = sorted(perm_rows, key=display_user_sort)
1446
1447 user_groups_rows = []
1448 if expand_from_user_groups:
1449 for ug in self.permission_user_groups(with_members=True):
1450 for user_data in ug.members:
1451 user_groups_rows.append(user_data)
1452
1453 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1454
1455 def permission_user_groups(self, with_members=False):
1456 q = UserGroupUserGroupToPerm.query()\
1457 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1458 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1459 joinedload(UserGroupUserGroupToPerm.target_user_group),
1460 joinedload(UserGroupUserGroupToPerm.permission),)
1461
1462 perm_rows = []
1463 for _user_group in q.all():
1464 entry = AttributeDict(_user_group.user_group.get_dict())
1465 entry.permission = _user_group.permission.permission_name
1466 if with_members:
1467 entry.members = [x.user.get_dict()
1468 for x in _user_group.users_group.members]
1469 perm_rows.append(entry)
1470
1471 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1472 return perm_rows
1473
1474 def _get_default_perms(self, user_group, suffix=''):
1475 from rhodecode.model.permission import PermissionModel
1476 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1477
1478 def get_default_perms(self, suffix=''):
1479 return self._get_default_perms(self, suffix)
1480
1481 def get_api_data(self, with_group_members=True, include_secrets=False):
1482 """
1483 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1484 basically forwarded.
1485
1486 """
1487 user_group = self
1488 data = {
1489 'users_group_id': user_group.users_group_id,
1490 'group_name': user_group.users_group_name,
1491 'group_description': user_group.user_group_description,
1492 'active': user_group.users_group_active,
1493 'owner': user_group.user.username,
1494 'sync': user_group.sync,
1495 'owner_email': user_group.user.email,
1496 }
1497
1498 if with_group_members:
1499 users = []
1500 for user in user_group.members:
1501 user = user.user
1502 users.append(user.get_api_data(include_secrets=include_secrets))
1503 data['users'] = users
1504
1505 return data
1506
1507
1508 class UserGroupMember(Base, BaseModel):
1509 __tablename__ = 'users_groups_members'
1510 __table_args__ = (
1511 base_table_args,
1512 )
1513
1514 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1515 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1516 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1517
1518 user = relationship('User', lazy='joined')
1519 users_group = relationship('UserGroup')
1520
1521 def __init__(self, gr_id='', u_id=''):
1522 self.users_group_id = gr_id
1523 self.user_id = u_id
1524
1525
1526 class RepositoryField(Base, BaseModel):
1527 __tablename__ = 'repositories_fields'
1528 __table_args__ = (
1529 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1530 base_table_args,
1531 )
1532
1533 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1534
1535 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1536 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1537 field_key = Column("field_key", String(250))
1538 field_label = Column("field_label", String(1024), nullable=False)
1539 field_value = Column("field_value", String(10000), nullable=False)
1540 field_desc = Column("field_desc", String(1024), nullable=False)
1541 field_type = Column("field_type", String(255), nullable=False, unique=None)
1542 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1543
1544 repository = relationship('Repository')
1545
1546 @property
1547 def field_key_prefixed(self):
1548 return 'ex_%s' % self.field_key
1549
1550 @classmethod
1551 def un_prefix_key(cls, key):
1552 if key.startswith(cls.PREFIX):
1553 return key[len(cls.PREFIX):]
1554 return key
1555
1556 @classmethod
1557 def get_by_key_name(cls, key, repo):
1558 row = cls.query()\
1559 .filter(cls.repository == repo)\
1560 .filter(cls.field_key == key).scalar()
1561 return row
1562
1563
1564 class Repository(Base, BaseModel):
1565 __tablename__ = 'repositories'
1566 __table_args__ = (
1567 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1568 base_table_args,
1569 )
1570 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1571 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1572 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1573
1574 STATE_CREATED = 'repo_state_created'
1575 STATE_PENDING = 'repo_state_pending'
1576 STATE_ERROR = 'repo_state_error'
1577
1578 LOCK_AUTOMATIC = 'lock_auto'
1579 LOCK_API = 'lock_api'
1580 LOCK_WEB = 'lock_web'
1581 LOCK_PULL = 'lock_pull'
1582
1583 NAME_SEP = URL_SEP
1584
1585 repo_id = Column(
1586 "repo_id", Integer(), nullable=False, unique=True, default=None,
1587 primary_key=True)
1588 _repo_name = Column(
1589 "repo_name", Text(), nullable=False, default=None)
1590 _repo_name_hash = Column(
1591 "repo_name_hash", String(255), nullable=False, unique=True)
1592 repo_state = Column("repo_state", String(255), nullable=True)
1593
1594 clone_uri = Column(
1595 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1596 default=None)
1597 push_uri = Column(
1598 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1599 default=None)
1600 repo_type = Column(
1601 "repo_type", String(255), nullable=False, unique=False, default=None)
1602 user_id = Column(
1603 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1604 unique=False, default=None)
1605 private = Column(
1606 "private", Boolean(), nullable=True, unique=None, default=None)
1607 archived = Column(
1608 "archived", Boolean(), nullable=True, unique=None, default=None)
1609 enable_statistics = Column(
1610 "statistics", Boolean(), nullable=True, unique=None, default=True)
1611 enable_downloads = Column(
1612 "downloads", Boolean(), nullable=True, unique=None, default=True)
1613 description = Column(
1614 "description", String(10000), nullable=True, unique=None, default=None)
1615 created_on = Column(
1616 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1617 default=datetime.datetime.now)
1618 updated_on = Column(
1619 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1620 default=datetime.datetime.now)
1621 _landing_revision = Column(
1622 "landing_revision", String(255), nullable=False, unique=False,
1623 default=None)
1624 enable_locking = Column(
1625 "enable_locking", Boolean(), nullable=False, unique=None,
1626 default=False)
1627 _locked = Column(
1628 "locked", String(255), nullable=True, unique=False, default=None)
1629 _changeset_cache = Column(
1630 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1631
1632 fork_id = Column(
1633 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1634 nullable=True, unique=False, default=None)
1635 group_id = Column(
1636 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1637 unique=False, default=None)
1638
1639 user = relationship('User', lazy='joined')
1640 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1641 group = relationship('RepoGroup', lazy='joined')
1642 repo_to_perm = relationship(
1643 'UserRepoToPerm', cascade='all',
1644 order_by='UserRepoToPerm.repo_to_perm_id')
1645 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1646 stats = relationship('Statistics', cascade='all', uselist=False)
1647
1648 followers = relationship(
1649 'UserFollowing',
1650 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1651 cascade='all')
1652 extra_fields = relationship(
1653 'RepositoryField', cascade="all, delete, delete-orphan")
1654 logs = relationship('UserLog')
1655 comments = relationship(
1656 'ChangesetComment', cascade="all, delete, delete-orphan")
1657 pull_requests_source = relationship(
1658 'PullRequest',
1659 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1660 cascade="all, delete, delete-orphan")
1661 pull_requests_target = relationship(
1662 'PullRequest',
1663 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1664 cascade="all, delete, delete-orphan")
1665 ui = relationship('RepoRhodeCodeUi', cascade="all")
1666 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1667 integrations = relationship('Integration',
1668 cascade="all, delete, delete-orphan")
1669
1670 scoped_tokens = relationship('UserApiKeys', cascade="all")
1671
1672 def __unicode__(self):
1673 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1674 safe_unicode(self.repo_name))
1675
1676 @hybrid_property
1677 def description_safe(self):
1678 from rhodecode.lib import helpers as h
1679 return h.escape(self.description)
1680
1681 @hybrid_property
1682 def landing_rev(self):
1683 # always should return [rev_type, rev]
1684 if self._landing_revision:
1685 _rev_info = self._landing_revision.split(':')
1686 if len(_rev_info) < 2:
1687 _rev_info.insert(0, 'rev')
1688 return [_rev_info[0], _rev_info[1]]
1689 return [None, None]
1690
1691 @landing_rev.setter
1692 def landing_rev(self, val):
1693 if ':' not in val:
1694 raise ValueError('value must be delimited with `:` and consist '
1695 'of <rev_type>:<rev>, got %s instead' % val)
1696 self._landing_revision = val
1697
1698 @hybrid_property
1699 def locked(self):
1700 if self._locked:
1701 user_id, timelocked, reason = self._locked.split(':')
1702 lock_values = int(user_id), timelocked, reason
1703 else:
1704 lock_values = [None, None, None]
1705 return lock_values
1706
1707 @locked.setter
1708 def locked(self, val):
1709 if val and isinstance(val, (list, tuple)):
1710 self._locked = ':'.join(map(str, val))
1711 else:
1712 self._locked = None
1713
1714 @hybrid_property
1715 def changeset_cache(self):
1716 from rhodecode.lib.vcs.backends.base import EmptyCommit
1717 dummy = EmptyCommit().__json__()
1718 if not self._changeset_cache:
1719 return dummy
1720 try:
1721 return json.loads(self._changeset_cache)
1722 except TypeError:
1723 return dummy
1724 except Exception:
1725 log.error(traceback.format_exc())
1726 return dummy
1727
1728 @changeset_cache.setter
1729 def changeset_cache(self, val):
1730 try:
1731 self._changeset_cache = json.dumps(val)
1732 except Exception:
1733 log.error(traceback.format_exc())
1734
1735 @hybrid_property
1736 def repo_name(self):
1737 return self._repo_name
1738
1739 @repo_name.setter
1740 def repo_name(self, value):
1741 self._repo_name = value
1742 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1743
1744 @classmethod
1745 def normalize_repo_name(cls, repo_name):
1746 """
1747 Normalizes os specific repo_name to the format internally stored inside
1748 database using URL_SEP
1749
1750 :param cls:
1751 :param repo_name:
1752 """
1753 return cls.NAME_SEP.join(repo_name.split(os.sep))
1754
1755 @classmethod
1756 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1757 session = Session()
1758 q = session.query(cls).filter(cls.repo_name == repo_name)
1759
1760 if cache:
1761 if identity_cache:
1762 val = cls.identity_cache(session, 'repo_name', repo_name)
1763 if val:
1764 return val
1765 else:
1766 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1767 q = q.options(
1768 FromCache("sql_cache_short", cache_key))
1769
1770 return q.scalar()
1771
1772 @classmethod
1773 def get_by_id_or_repo_name(cls, repoid):
1774 if isinstance(repoid, (int, long)):
1775 try:
1776 repo = cls.get(repoid)
1777 except ValueError:
1778 repo = None
1779 else:
1780 repo = cls.get_by_repo_name(repoid)
1781 return repo
1782
1783 @classmethod
1784 def get_by_full_path(cls, repo_full_path):
1785 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1786 repo_name = cls.normalize_repo_name(repo_name)
1787 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1788
1789 @classmethod
1790 def get_repo_forks(cls, repo_id):
1791 return cls.query().filter(Repository.fork_id == repo_id)
1792
1793 @classmethod
1794 def base_path(cls):
1795 """
1796 Returns base path when all repos are stored
1797
1798 :param cls:
1799 """
1800 q = Session().query(RhodeCodeUi)\
1801 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1802 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1803 return q.one().ui_value
1804
1805 @classmethod
1806 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1807 case_insensitive=True, archived=False):
1808 q = Repository.query()
1809
1810 if not archived:
1811 q = q.filter(Repository.archived.isnot(true()))
1812
1813 if not isinstance(user_id, Optional):
1814 q = q.filter(Repository.user_id == user_id)
1815
1816 if not isinstance(group_id, Optional):
1817 q = q.filter(Repository.group_id == group_id)
1818
1819 if case_insensitive:
1820 q = q.order_by(func.lower(Repository.repo_name))
1821 else:
1822 q = q.order_by(Repository.repo_name)
1823
1824 return q.all()
1825
1826 @property
1827 def forks(self):
1828 """
1829 Return forks of this repo
1830 """
1831 return Repository.get_repo_forks(self.repo_id)
1832
1833 @property
1834 def parent(self):
1835 """
1836 Returns fork parent
1837 """
1838 return self.fork
1839
1840 @property
1841 def just_name(self):
1842 return self.repo_name.split(self.NAME_SEP)[-1]
1843
1844 @property
1845 def groups_with_parents(self):
1846 groups = []
1847 if self.group is None:
1848 return groups
1849
1850 cur_gr = self.group
1851 groups.insert(0, cur_gr)
1852 while 1:
1853 gr = getattr(cur_gr, 'parent_group', None)
1854 cur_gr = cur_gr.parent_group
1855 if gr is None:
1856 break
1857 groups.insert(0, gr)
1858
1859 return groups
1860
1861 @property
1862 def groups_and_repo(self):
1863 return self.groups_with_parents, self
1864
1865 @LazyProperty
1866 def repo_path(self):
1867 """
1868 Returns base full path for that repository means where it actually
1869 exists on a filesystem
1870 """
1871 q = Session().query(RhodeCodeUi).filter(
1872 RhodeCodeUi.ui_key == self.NAME_SEP)
1873 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1874 return q.one().ui_value
1875
1876 @property
1877 def repo_full_path(self):
1878 p = [self.repo_path]
1879 # we need to split the name by / since this is how we store the
1880 # names in the database, but that eventually needs to be converted
1881 # into a valid system path
1882 p += self.repo_name.split(self.NAME_SEP)
1883 return os.path.join(*map(safe_unicode, p))
1884
1885 @property
1886 def cache_keys(self):
1887 """
1888 Returns associated cache keys for that repo
1889 """
1890 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1891 repo_id=self.repo_id)
1892 return CacheKey.query()\
1893 .filter(CacheKey.cache_args == invalidation_namespace)\
1894 .order_by(CacheKey.cache_key)\
1895 .all()
1896
1897 @property
1898 def cached_diffs_relative_dir(self):
1899 """
1900 Return a relative to the repository store path of cached diffs
1901 used for safe display for users, who shouldn't know the absolute store
1902 path
1903 """
1904 return os.path.join(
1905 os.path.dirname(self.repo_name),
1906 self.cached_diffs_dir.split(os.path.sep)[-1])
1907
1908 @property
1909 def cached_diffs_dir(self):
1910 path = self.repo_full_path
1911 return os.path.join(
1912 os.path.dirname(path),
1913 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1914
1915 def cached_diffs(self):
1916 diff_cache_dir = self.cached_diffs_dir
1917 if os.path.isdir(diff_cache_dir):
1918 return os.listdir(diff_cache_dir)
1919 return []
1920
1921 def shadow_repos(self):
1922 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
1923 return [
1924 x for x in os.listdir(os.path.dirname(self.repo_full_path))
1925 if x.startswith(shadow_repos_pattern)]
1926
1927 def get_new_name(self, repo_name):
1928 """
1929 returns new full repository name based on assigned group and new new
1930
1931 :param group_name:
1932 """
1933 path_prefix = self.group.full_path_splitted if self.group else []
1934 return self.NAME_SEP.join(path_prefix + [repo_name])
1935
1936 @property
1937 def _config(self):
1938 """
1939 Returns db based config object.
1940 """
1941 from rhodecode.lib.utils import make_db_config
1942 return make_db_config(clear_session=False, repo=self)
1943
1944 def permissions(self, with_admins=True, with_owner=True,
1945 expand_from_user_groups=False):
1946 """
1947 Permissions for repositories
1948 """
1949 _admin_perm = 'repository.admin'
1950
1951 owner_row = []
1952 if with_owner:
1953 usr = AttributeDict(self.user.get_dict())
1954 usr.owner_row = True
1955 usr.permission = _admin_perm
1956 usr.permission_id = None
1957 owner_row.append(usr)
1958
1959 super_admin_ids = []
1960 super_admin_rows = []
1961 if with_admins:
1962 for usr in User.get_all_super_admins():
1963 super_admin_ids.append(usr.user_id)
1964 # if this admin is also owner, don't double the record
1965 if usr.user_id == owner_row[0].user_id:
1966 owner_row[0].admin_row = True
1967 else:
1968 usr = AttributeDict(usr.get_dict())
1969 usr.admin_row = True
1970 usr.permission = _admin_perm
1971 usr.permission_id = None
1972 super_admin_rows.append(usr)
1973
1974 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1975 q = q.options(joinedload(UserRepoToPerm.repository),
1976 joinedload(UserRepoToPerm.user),
1977 joinedload(UserRepoToPerm.permission),)
1978
1979 # get owners and admins and permissions. We do a trick of re-writing
1980 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1981 # has a global reference and changing one object propagates to all
1982 # others. This means if admin is also an owner admin_row that change
1983 # would propagate to both objects
1984 perm_rows = []
1985 for _usr in q.all():
1986 usr = AttributeDict(_usr.user.get_dict())
1987 # if this user is also owner/admin, mark as duplicate record
1988 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1989 usr.duplicate_perm = True
1990 # also check if this permission is maybe used by branch_permissions
1991 if _usr.branch_perm_entry:
1992 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
1993
1994 usr.permission = _usr.permission.permission_name
1995 usr.permission_id = _usr.repo_to_perm_id
1996 perm_rows.append(usr)
1997
1998 # filter the perm rows by 'default' first and then sort them by
1999 # admin,write,read,none permissions sorted again alphabetically in
2000 # each group
2001 perm_rows = sorted(perm_rows, key=display_user_sort)
2002
2003 user_groups_rows = []
2004 if expand_from_user_groups:
2005 for ug in self.permission_user_groups(with_members=True):
2006 for user_data in ug.members:
2007 user_groups_rows.append(user_data)
2008
2009 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2010
2011 def permission_user_groups(self, with_members=True):
2012 q = UserGroupRepoToPerm.query()\
2013 .filter(UserGroupRepoToPerm.repository == self)
2014 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2015 joinedload(UserGroupRepoToPerm.users_group),
2016 joinedload(UserGroupRepoToPerm.permission),)
2017
2018 perm_rows = []
2019 for _user_group in q.all():
2020 entry = AttributeDict(_user_group.users_group.get_dict())
2021 entry.permission = _user_group.permission.permission_name
2022 if with_members:
2023 entry.members = [x.user.get_dict()
2024 for x in _user_group.users_group.members]
2025 perm_rows.append(entry)
2026
2027 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2028 return perm_rows
2029
2030 def get_api_data(self, include_secrets=False):
2031 """
2032 Common function for generating repo api data
2033
2034 :param include_secrets: See :meth:`User.get_api_data`.
2035
2036 """
2037 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2038 # move this methods on models level.
2039 from rhodecode.model.settings import SettingsModel
2040 from rhodecode.model.repo import RepoModel
2041
2042 repo = self
2043 _user_id, _time, _reason = self.locked
2044
2045 data = {
2046 'repo_id': repo.repo_id,
2047 'repo_name': repo.repo_name,
2048 'repo_type': repo.repo_type,
2049 'clone_uri': repo.clone_uri or '',
2050 'push_uri': repo.push_uri or '',
2051 'url': RepoModel().get_url(self),
2052 'private': repo.private,
2053 'created_on': repo.created_on,
2054 'description': repo.description_safe,
2055 'landing_rev': repo.landing_rev,
2056 'owner': repo.user.username,
2057 'fork_of': repo.fork.repo_name if repo.fork else None,
2058 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2059 'enable_statistics': repo.enable_statistics,
2060 'enable_locking': repo.enable_locking,
2061 'enable_downloads': repo.enable_downloads,
2062 'last_changeset': repo.changeset_cache,
2063 'locked_by': User.get(_user_id).get_api_data(
2064 include_secrets=include_secrets) if _user_id else None,
2065 'locked_date': time_to_datetime(_time) if _time else None,
2066 'lock_reason': _reason if _reason else None,
2067 }
2068
2069 # TODO: mikhail: should be per-repo settings here
2070 rc_config = SettingsModel().get_all_settings()
2071 repository_fields = str2bool(
2072 rc_config.get('rhodecode_repository_fields'))
2073 if repository_fields:
2074 for f in self.extra_fields:
2075 data[f.field_key_prefixed] = f.field_value
2076
2077 return data
2078
2079 @classmethod
2080 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2081 if not lock_time:
2082 lock_time = time.time()
2083 if not lock_reason:
2084 lock_reason = cls.LOCK_AUTOMATIC
2085 repo.locked = [user_id, lock_time, lock_reason]
2086 Session().add(repo)
2087 Session().commit()
2088
2089 @classmethod
2090 def unlock(cls, repo):
2091 repo.locked = None
2092 Session().add(repo)
2093 Session().commit()
2094
2095 @classmethod
2096 def getlock(cls, repo):
2097 return repo.locked
2098
2099 def is_user_lock(self, user_id):
2100 if self.lock[0]:
2101 lock_user_id = safe_int(self.lock[0])
2102 user_id = safe_int(user_id)
2103 # both are ints, and they are equal
2104 return all([lock_user_id, user_id]) and lock_user_id == user_id
2105
2106 return False
2107
2108 def get_locking_state(self, action, user_id, only_when_enabled=True):
2109 """
2110 Checks locking on this repository, if locking is enabled and lock is
2111 present returns a tuple of make_lock, locked, locked_by.
2112 make_lock can have 3 states None (do nothing) True, make lock
2113 False release lock, This value is later propagated to hooks, which
2114 do the locking. Think about this as signals passed to hooks what to do.
2115
2116 """
2117 # TODO: johbo: This is part of the business logic and should be moved
2118 # into the RepositoryModel.
2119
2120 if action not in ('push', 'pull'):
2121 raise ValueError("Invalid action value: %s" % repr(action))
2122
2123 # defines if locked error should be thrown to user
2124 currently_locked = False
2125 # defines if new lock should be made, tri-state
2126 make_lock = None
2127 repo = self
2128 user = User.get(user_id)
2129
2130 lock_info = repo.locked
2131
2132 if repo and (repo.enable_locking or not only_when_enabled):
2133 if action == 'push':
2134 # check if it's already locked !, if it is compare users
2135 locked_by_user_id = lock_info[0]
2136 if user.user_id == locked_by_user_id:
2137 log.debug(
2138 'Got `push` action from user %s, now unlocking', user)
2139 # unlock if we have push from user who locked
2140 make_lock = False
2141 else:
2142 # we're not the same user who locked, ban with
2143 # code defined in settings (default is 423 HTTP Locked) !
2144 log.debug('Repo %s is currently locked by %s', repo, user)
2145 currently_locked = True
2146 elif action == 'pull':
2147 # [0] user [1] date
2148 if lock_info[0] and lock_info[1]:
2149 log.debug('Repo %s is currently locked by %s', repo, user)
2150 currently_locked = True
2151 else:
2152 log.debug('Setting lock on repo %s by %s', repo, user)
2153 make_lock = True
2154
2155 else:
2156 log.debug('Repository %s do not have locking enabled', repo)
2157
2158 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2159 make_lock, currently_locked, lock_info)
2160
2161 from rhodecode.lib.auth import HasRepoPermissionAny
2162 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2163 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2164 # if we don't have at least write permission we cannot make a lock
2165 log.debug('lock state reset back to FALSE due to lack '
2166 'of at least read permission')
2167 make_lock = False
2168
2169 return make_lock, currently_locked, lock_info
2170
2171 @property
2172 def last_db_change(self):
2173 return self.updated_on
2174
2175 @property
2176 def clone_uri_hidden(self):
2177 clone_uri = self.clone_uri
2178 if clone_uri:
2179 import urlobject
2180 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2181 if url_obj.password:
2182 clone_uri = url_obj.with_password('*****')
2183 return clone_uri
2184
2185 @property
2186 def push_uri_hidden(self):
2187 push_uri = self.push_uri
2188 if push_uri:
2189 import urlobject
2190 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2191 if url_obj.password:
2192 push_uri = url_obj.with_password('*****')
2193 return push_uri
2194
2195 def clone_url(self, **override):
2196 from rhodecode.model.settings import SettingsModel
2197
2198 uri_tmpl = None
2199 if 'with_id' in override:
2200 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2201 del override['with_id']
2202
2203 if 'uri_tmpl' in override:
2204 uri_tmpl = override['uri_tmpl']
2205 del override['uri_tmpl']
2206
2207 ssh = False
2208 if 'ssh' in override:
2209 ssh = True
2210 del override['ssh']
2211
2212 # we didn't override our tmpl from **overrides
2213 if not uri_tmpl:
2214 rc_config = SettingsModel().get_all_settings(cache=True)
2215 if ssh:
2216 uri_tmpl = rc_config.get(
2217 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2218 else:
2219 uri_tmpl = rc_config.get(
2220 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2221
2222 request = get_current_request()
2223 return get_clone_url(request=request,
2224 uri_tmpl=uri_tmpl,
2225 repo_name=self.repo_name,
2226 repo_id=self.repo_id, **override)
2227
2228 def set_state(self, state):
2229 self.repo_state = state
2230 Session().add(self)
2231 #==========================================================================
2232 # SCM PROPERTIES
2233 #==========================================================================
2234
2235 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2236 return get_commit_safe(
2237 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2238
2239 def get_changeset(self, rev=None, pre_load=None):
2240 warnings.warn("Use get_commit", DeprecationWarning)
2241 commit_id = None
2242 commit_idx = None
2243 if isinstance(rev, compat.string_types):
2244 commit_id = rev
2245 else:
2246 commit_idx = rev
2247 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2248 pre_load=pre_load)
2249
2250 def get_landing_commit(self):
2251 """
2252 Returns landing commit, or if that doesn't exist returns the tip
2253 """
2254 _rev_type, _rev = self.landing_rev
2255 commit = self.get_commit(_rev)
2256 if isinstance(commit, EmptyCommit):
2257 return self.get_commit()
2258 return commit
2259
2260 def update_commit_cache(self, cs_cache=None, config=None):
2261 """
2262 Update cache of last changeset for repository, keys should be::
2263
2264 short_id
2265 raw_id
2266 revision
2267 parents
2268 message
2269 date
2270 author
2271
2272 :param cs_cache:
2273 """
2274 from rhodecode.lib.vcs.backends.base import BaseChangeset
2275 if cs_cache is None:
2276 # use no-cache version here
2277 scm_repo = self.scm_instance(cache=False, config=config)
2278
2279 empty = not scm_repo or scm_repo.is_empty()
2280 if not empty:
2281 cs_cache = scm_repo.get_commit(
2282 pre_load=["author", "date", "message", "parents"])
2283 else:
2284 cs_cache = EmptyCommit()
2285
2286 if isinstance(cs_cache, BaseChangeset):
2287 cs_cache = cs_cache.__json__()
2288
2289 def is_outdated(new_cs_cache):
2290 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2291 new_cs_cache['revision'] != self.changeset_cache['revision']):
2292 return True
2293 return False
2294
2295 # check if we have maybe already latest cached revision
2296 if is_outdated(cs_cache) or not self.changeset_cache:
2297 _default = datetime.datetime.utcnow()
2298 last_change = cs_cache.get('date') or _default
2299 if self.updated_on and self.updated_on > last_change:
2300 # we check if last update is newer than the new value
2301 # if yes, we use the current timestamp instead. Imagine you get
2302 # old commit pushed 1y ago, we'd set last update 1y to ago.
2303 last_change = _default
2304 log.debug('updated repo %s with new cs cache %s',
2305 self.repo_name, cs_cache)
2306 self.updated_on = last_change
2307 self.changeset_cache = cs_cache
2308 Session().add(self)
2309 Session().commit()
2310 else:
2311 log.debug('Skipping update_commit_cache for repo:`%s` '
2312 'commit already with latest changes', self.repo_name)
2313
2314 @property
2315 def tip(self):
2316 return self.get_commit('tip')
2317
2318 @property
2319 def author(self):
2320 return self.tip.author
2321
2322 @property
2323 def last_change(self):
2324 return self.scm_instance().last_change
2325
2326 def get_comments(self, revisions=None):
2327 """
2328 Returns comments for this repository grouped by revisions
2329
2330 :param revisions: filter query by revisions only
2331 """
2332 cmts = ChangesetComment.query()\
2333 .filter(ChangesetComment.repo == self)
2334 if revisions:
2335 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2336 grouped = collections.defaultdict(list)
2337 for cmt in cmts.all():
2338 grouped[cmt.revision].append(cmt)
2339 return grouped
2340
2341 def statuses(self, revisions=None):
2342 """
2343 Returns statuses for this repository
2344
2345 :param revisions: list of revisions to get statuses for
2346 """
2347 statuses = ChangesetStatus.query()\
2348 .filter(ChangesetStatus.repo == self)\
2349 .filter(ChangesetStatus.version == 0)
2350
2351 if revisions:
2352 # Try doing the filtering in chunks to avoid hitting limits
2353 size = 500
2354 status_results = []
2355 for chunk in xrange(0, len(revisions), size):
2356 status_results += statuses.filter(
2357 ChangesetStatus.revision.in_(
2358 revisions[chunk: chunk+size])
2359 ).all()
2360 else:
2361 status_results = statuses.all()
2362
2363 grouped = {}
2364
2365 # maybe we have open new pullrequest without a status?
2366 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2367 status_lbl = ChangesetStatus.get_status_lbl(stat)
2368 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2369 for rev in pr.revisions:
2370 pr_id = pr.pull_request_id
2371 pr_repo = pr.target_repo.repo_name
2372 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2373
2374 for stat in status_results:
2375 pr_id = pr_repo = None
2376 if stat.pull_request:
2377 pr_id = stat.pull_request.pull_request_id
2378 pr_repo = stat.pull_request.target_repo.repo_name
2379 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2380 pr_id, pr_repo]
2381 return grouped
2382
2383 # ==========================================================================
2384 # SCM CACHE INSTANCE
2385 # ==========================================================================
2386
2387 def scm_instance(self, **kwargs):
2388 import rhodecode
2389
2390 # Passing a config will not hit the cache currently only used
2391 # for repo2dbmapper
2392 config = kwargs.pop('config', None)
2393 cache = kwargs.pop('cache', None)
2394 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2395 # if cache is NOT defined use default global, else we have a full
2396 # control over cache behaviour
2397 if cache is None and full_cache and not config:
2398 return self._get_instance_cached()
2399 return self._get_instance(cache=bool(cache), config=config)
2400
2401 def _get_instance_cached(self):
2402 from rhodecode.lib import rc_cache
2403
2404 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2405 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2406 repo_id=self.repo_id)
2407 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2408
2409 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2410 def get_instance_cached(repo_id, context_id):
2411 return self._get_instance()
2412
2413 # we must use thread scoped cache here,
2414 # because each thread of gevent needs it's own not shared connection and cache
2415 # we also alter `args` so the cache key is individual for every green thread.
2416 inv_context_manager = rc_cache.InvalidationContext(
2417 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2418 thread_scoped=True)
2419 with inv_context_manager as invalidation_context:
2420 args = (self.repo_id, inv_context_manager.cache_key)
2421 # re-compute and store cache if we get invalidate signal
2422 if invalidation_context.should_invalidate():
2423 instance = get_instance_cached.refresh(*args)
2424 else:
2425 instance = get_instance_cached(*args)
2426
2427 log.debug(
2428 'Repo instance fetched in %.3fs', inv_context_manager.compute_time)
2429 return instance
2430
2431 def _get_instance(self, cache=True, config=None):
2432 config = config or self._config
2433 custom_wire = {
2434 'cache': cache # controls the vcs.remote cache
2435 }
2436 repo = get_vcs_instance(
2437 repo_path=safe_str(self.repo_full_path),
2438 config=config,
2439 with_wire=custom_wire,
2440 create=False,
2441 _vcs_alias=self.repo_type)
2442
2443 return repo
2444
2445 def __json__(self):
2446 return {'landing_rev': self.landing_rev}
2447
2448 def get_dict(self):
2449
2450 # Since we transformed `repo_name` to a hybrid property, we need to
2451 # keep compatibility with the code which uses `repo_name` field.
2452
2453 result = super(Repository, self).get_dict()
2454 result['repo_name'] = result.pop('_repo_name', None)
2455 return result
2456
2457
2458 class RepoGroup(Base, BaseModel):
2459 __tablename__ = 'groups'
2460 __table_args__ = (
2461 UniqueConstraint('group_name', 'group_parent_id'),
2462 base_table_args,
2463 )
2464 __mapper_args__ = {'order_by': 'group_name'}
2465
2466 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2467
2468 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2469 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2470 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2471 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2472 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2473 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2474 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2475 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2476 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2477
2478 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2479 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2480 parent_group = relationship('RepoGroup', remote_side=group_id)
2481 user = relationship('User')
2482 integrations = relationship('Integration',
2483 cascade="all, delete, delete-orphan")
2484
2485 def __init__(self, group_name='', parent_group=None):
2486 self.group_name = group_name
2487 self.parent_group = parent_group
2488
2489 def __unicode__(self):
2490 return u"<%s('id:%s:%s')>" % (
2491 self.__class__.__name__, self.group_id, self.group_name)
2492
2493 @hybrid_property
2494 def description_safe(self):
2495 from rhodecode.lib import helpers as h
2496 return h.escape(self.group_description)
2497
2498 @classmethod
2499 def _generate_choice(cls, repo_group):
2500 from webhelpers.html import literal as _literal
2501 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2502 return repo_group.group_id, _name(repo_group.full_path_splitted)
2503
2504 @classmethod
2505 def groups_choices(cls, groups=None, show_empty_group=True):
2506 if not groups:
2507 groups = cls.query().all()
2508
2509 repo_groups = []
2510 if show_empty_group:
2511 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2512
2513 repo_groups.extend([cls._generate_choice(x) for x in groups])
2514
2515 repo_groups = sorted(
2516 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2517 return repo_groups
2518
2519 @classmethod
2520 def url_sep(cls):
2521 return URL_SEP
2522
2523 @classmethod
2524 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2525 if case_insensitive:
2526 gr = cls.query().filter(func.lower(cls.group_name)
2527 == func.lower(group_name))
2528 else:
2529 gr = cls.query().filter(cls.group_name == group_name)
2530 if cache:
2531 name_key = _hash_key(group_name)
2532 gr = gr.options(
2533 FromCache("sql_cache_short", "get_group_%s" % name_key))
2534 return gr.scalar()
2535
2536 @classmethod
2537 def get_user_personal_repo_group(cls, user_id):
2538 user = User.get(user_id)
2539 if user.username == User.DEFAULT_USER:
2540 return None
2541
2542 return cls.query()\
2543 .filter(cls.personal == true()) \
2544 .filter(cls.user == user) \
2545 .order_by(cls.group_id.asc()) \
2546 .first()
2547
2548 @classmethod
2549 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2550 case_insensitive=True):
2551 q = RepoGroup.query()
2552
2553 if not isinstance(user_id, Optional):
2554 q = q.filter(RepoGroup.user_id == user_id)
2555
2556 if not isinstance(group_id, Optional):
2557 q = q.filter(RepoGroup.group_parent_id == group_id)
2558
2559 if case_insensitive:
2560 q = q.order_by(func.lower(RepoGroup.group_name))
2561 else:
2562 q = q.order_by(RepoGroup.group_name)
2563 return q.all()
2564
2565 @property
2566 def parents(self):
2567 parents_recursion_limit = 10
2568 groups = []
2569 if self.parent_group is None:
2570 return groups
2571 cur_gr = self.parent_group
2572 groups.insert(0, cur_gr)
2573 cnt = 0
2574 while 1:
2575 cnt += 1
2576 gr = getattr(cur_gr, 'parent_group', None)
2577 cur_gr = cur_gr.parent_group
2578 if gr is None:
2579 break
2580 if cnt == parents_recursion_limit:
2581 # this will prevent accidental infinit loops
2582 log.error('more than %s parents found for group %s, stopping '
2583 'recursive parent fetching', parents_recursion_limit, self)
2584 break
2585
2586 groups.insert(0, gr)
2587 return groups
2588
2589 @property
2590 def last_db_change(self):
2591 return self.updated_on
2592
2593 @property
2594 def children(self):
2595 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2596
2597 @property
2598 def name(self):
2599 return self.group_name.split(RepoGroup.url_sep())[-1]
2600
2601 @property
2602 def full_path(self):
2603 return self.group_name
2604
2605 @property
2606 def full_path_splitted(self):
2607 return self.group_name.split(RepoGroup.url_sep())
2608
2609 @property
2610 def repositories(self):
2611 return Repository.query()\
2612 .filter(Repository.group == self)\
2613 .order_by(Repository.repo_name)
2614
2615 @property
2616 def repositories_recursive_count(self):
2617 cnt = self.repositories.count()
2618
2619 def children_count(group):
2620 cnt = 0
2621 for child in group.children:
2622 cnt += child.repositories.count()
2623 cnt += children_count(child)
2624 return cnt
2625
2626 return cnt + children_count(self)
2627
2628 def _recursive_objects(self, include_repos=True):
2629 all_ = []
2630
2631 def _get_members(root_gr):
2632 if include_repos:
2633 for r in root_gr.repositories:
2634 all_.append(r)
2635 childs = root_gr.children.all()
2636 if childs:
2637 for gr in childs:
2638 all_.append(gr)
2639 _get_members(gr)
2640
2641 _get_members(self)
2642 return [self] + all_
2643
2644 def recursive_groups_and_repos(self):
2645 """
2646 Recursive return all groups, with repositories in those groups
2647 """
2648 return self._recursive_objects()
2649
2650 def recursive_groups(self):
2651 """
2652 Returns all children groups for this group including children of children
2653 """
2654 return self._recursive_objects(include_repos=False)
2655
2656 def get_new_name(self, group_name):
2657 """
2658 returns new full group name based on parent and new name
2659
2660 :param group_name:
2661 """
2662 path_prefix = (self.parent_group.full_path_splitted if
2663 self.parent_group else [])
2664 return RepoGroup.url_sep().join(path_prefix + [group_name])
2665
2666 def permissions(self, with_admins=True, with_owner=True,
2667 expand_from_user_groups=False):
2668 """
2669 Permissions for repository groups
2670 """
2671 _admin_perm = 'group.admin'
2672
2673 owner_row = []
2674 if with_owner:
2675 usr = AttributeDict(self.user.get_dict())
2676 usr.owner_row = True
2677 usr.permission = _admin_perm
2678 owner_row.append(usr)
2679
2680 super_admin_ids = []
2681 super_admin_rows = []
2682 if with_admins:
2683 for usr in User.get_all_super_admins():
2684 super_admin_ids.append(usr.user_id)
2685 # if this admin is also owner, don't double the record
2686 if usr.user_id == owner_row[0].user_id:
2687 owner_row[0].admin_row = True
2688 else:
2689 usr = AttributeDict(usr.get_dict())
2690 usr.admin_row = True
2691 usr.permission = _admin_perm
2692 super_admin_rows.append(usr)
2693
2694 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2695 q = q.options(joinedload(UserRepoGroupToPerm.group),
2696 joinedload(UserRepoGroupToPerm.user),
2697 joinedload(UserRepoGroupToPerm.permission),)
2698
2699 # get owners and admins and permissions. We do a trick of re-writing
2700 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2701 # has a global reference and changing one object propagates to all
2702 # others. This means if admin is also an owner admin_row that change
2703 # would propagate to both objects
2704 perm_rows = []
2705 for _usr in q.all():
2706 usr = AttributeDict(_usr.user.get_dict())
2707 # if this user is also owner/admin, mark as duplicate record
2708 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2709 usr.duplicate_perm = True
2710 usr.permission = _usr.permission.permission_name
2711 perm_rows.append(usr)
2712
2713 # filter the perm rows by 'default' first and then sort them by
2714 # admin,write,read,none permissions sorted again alphabetically in
2715 # each group
2716 perm_rows = sorted(perm_rows, key=display_user_sort)
2717
2718 user_groups_rows = []
2719 if expand_from_user_groups:
2720 for ug in self.permission_user_groups(with_members=True):
2721 for user_data in ug.members:
2722 user_groups_rows.append(user_data)
2723
2724 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2725
2726 def permission_user_groups(self, with_members=False):
2727 q = UserGroupRepoGroupToPerm.query()\
2728 .filter(UserGroupRepoGroupToPerm.group == self)
2729 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2730 joinedload(UserGroupRepoGroupToPerm.users_group),
2731 joinedload(UserGroupRepoGroupToPerm.permission),)
2732
2733 perm_rows = []
2734 for _user_group in q.all():
2735 entry = AttributeDict(_user_group.users_group.get_dict())
2736 entry.permission = _user_group.permission.permission_name
2737 if with_members:
2738 entry.members = [x.user.get_dict()
2739 for x in _user_group.users_group.members]
2740 perm_rows.append(entry)
2741
2742 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2743 return perm_rows
2744
2745 def get_api_data(self):
2746 """
2747 Common function for generating api data
2748
2749 """
2750 group = self
2751 data = {
2752 'group_id': group.group_id,
2753 'group_name': group.group_name,
2754 'group_description': group.description_safe,
2755 'parent_group': group.parent_group.group_name if group.parent_group else None,
2756 'repositories': [x.repo_name for x in group.repositories],
2757 'owner': group.user.username,
2758 }
2759 return data
2760
2761
2762 class Permission(Base, BaseModel):
2763 __tablename__ = 'permissions'
2764 __table_args__ = (
2765 Index('p_perm_name_idx', 'permission_name'),
2766 base_table_args,
2767 )
2768
2769 PERMS = [
2770 ('hg.admin', _('RhodeCode Super Administrator')),
2771
2772 ('repository.none', _('Repository no access')),
2773 ('repository.read', _('Repository read access')),
2774 ('repository.write', _('Repository write access')),
2775 ('repository.admin', _('Repository admin access')),
2776
2777 ('group.none', _('Repository group no access')),
2778 ('group.read', _('Repository group read access')),
2779 ('group.write', _('Repository group write access')),
2780 ('group.admin', _('Repository group admin access')),
2781
2782 ('usergroup.none', _('User group no access')),
2783 ('usergroup.read', _('User group read access')),
2784 ('usergroup.write', _('User group write access')),
2785 ('usergroup.admin', _('User group admin access')),
2786
2787 ('branch.none', _('Branch no permissions')),
2788 ('branch.merge', _('Branch access by web merge')),
2789 ('branch.push', _('Branch access by push')),
2790 ('branch.push_force', _('Branch access by push with force')),
2791
2792 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2793 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2794
2795 ('hg.usergroup.create.false', _('User Group creation disabled')),
2796 ('hg.usergroup.create.true', _('User Group creation enabled')),
2797
2798 ('hg.create.none', _('Repository creation disabled')),
2799 ('hg.create.repository', _('Repository creation enabled')),
2800 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2801 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2802
2803 ('hg.fork.none', _('Repository forking disabled')),
2804 ('hg.fork.repository', _('Repository forking enabled')),
2805
2806 ('hg.register.none', _('Registration disabled')),
2807 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2808 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2809
2810 ('hg.password_reset.enabled', _('Password reset enabled')),
2811 ('hg.password_reset.hidden', _('Password reset hidden')),
2812 ('hg.password_reset.disabled', _('Password reset disabled')),
2813
2814 ('hg.extern_activate.manual', _('Manual activation of external account')),
2815 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2816
2817 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2818 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2819 ]
2820
2821 # definition of system default permissions for DEFAULT user, created on
2822 # system setup
2823 DEFAULT_USER_PERMISSIONS = [
2824 # object perms
2825 'repository.read',
2826 'group.read',
2827 'usergroup.read',
2828 # branch, for backward compat we need same value as before so forced pushed
2829 'branch.push_force',
2830 # global
2831 'hg.create.repository',
2832 'hg.repogroup.create.false',
2833 'hg.usergroup.create.false',
2834 'hg.create.write_on_repogroup.true',
2835 'hg.fork.repository',
2836 'hg.register.manual_activate',
2837 'hg.password_reset.enabled',
2838 'hg.extern_activate.auto',
2839 'hg.inherit_default_perms.true',
2840 ]
2841
2842 # defines which permissions are more important higher the more important
2843 # Weight defines which permissions are more important.
2844 # The higher number the more important.
2845 PERM_WEIGHTS = {
2846 'repository.none': 0,
2847 'repository.read': 1,
2848 'repository.write': 3,
2849 'repository.admin': 4,
2850
2851 'group.none': 0,
2852 'group.read': 1,
2853 'group.write': 3,
2854 'group.admin': 4,
2855
2856 'usergroup.none': 0,
2857 'usergroup.read': 1,
2858 'usergroup.write': 3,
2859 'usergroup.admin': 4,
2860
2861 'branch.none': 0,
2862 'branch.merge': 1,
2863 'branch.push': 3,
2864 'branch.push_force': 4,
2865
2866 'hg.repogroup.create.false': 0,
2867 'hg.repogroup.create.true': 1,
2868
2869 'hg.usergroup.create.false': 0,
2870 'hg.usergroup.create.true': 1,
2871
2872 'hg.fork.none': 0,
2873 'hg.fork.repository': 1,
2874 'hg.create.none': 0,
2875 'hg.create.repository': 1
2876 }
2877
2878 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2879 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2880 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2881
2882 def __unicode__(self):
2883 return u"<%s('%s:%s')>" % (
2884 self.__class__.__name__, self.permission_id, self.permission_name
2885 )
2886
2887 @classmethod
2888 def get_by_key(cls, key):
2889 return cls.query().filter(cls.permission_name == key).scalar()
2890
2891 @classmethod
2892 def get_default_repo_perms(cls, user_id, repo_id=None):
2893 q = Session().query(UserRepoToPerm, Repository, Permission)\
2894 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2895 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2896 .filter(UserRepoToPerm.user_id == user_id)
2897 if repo_id:
2898 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2899 return q.all()
2900
2901 @classmethod
2902 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
2903 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
2904 .join(
2905 Permission,
2906 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
2907 .join(
2908 UserRepoToPerm,
2909 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
2910 .filter(UserRepoToPerm.user_id == user_id)
2911
2912 if repo_id:
2913 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
2914 return q.order_by(UserToRepoBranchPermission.rule_order).all()
2915
2916 @classmethod
2917 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2918 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2919 .join(
2920 Permission,
2921 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2922 .join(
2923 Repository,
2924 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2925 .join(
2926 UserGroup,
2927 UserGroupRepoToPerm.users_group_id ==
2928 UserGroup.users_group_id)\
2929 .join(
2930 UserGroupMember,
2931 UserGroupRepoToPerm.users_group_id ==
2932 UserGroupMember.users_group_id)\
2933 .filter(
2934 UserGroupMember.user_id == user_id,
2935 UserGroup.users_group_active == true())
2936 if repo_id:
2937 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2938 return q.all()
2939
2940 @classmethod
2941 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
2942 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
2943 .join(
2944 Permission,
2945 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
2946 .join(
2947 UserGroupRepoToPerm,
2948 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
2949 .join(
2950 UserGroup,
2951 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
2952 .join(
2953 UserGroupMember,
2954 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
2955 .filter(
2956 UserGroupMember.user_id == user_id,
2957 UserGroup.users_group_active == true())
2958
2959 if repo_id:
2960 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
2961 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
2962
2963 @classmethod
2964 def get_default_group_perms(cls, user_id, repo_group_id=None):
2965 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2966 .join(
2967 Permission,
2968 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
2969 .join(
2970 RepoGroup,
2971 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
2972 .filter(UserRepoGroupToPerm.user_id == user_id)
2973 if repo_group_id:
2974 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2975 return q.all()
2976
2977 @classmethod
2978 def get_default_group_perms_from_user_group(
2979 cls, user_id, repo_group_id=None):
2980 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2981 .join(
2982 Permission,
2983 UserGroupRepoGroupToPerm.permission_id ==
2984 Permission.permission_id)\
2985 .join(
2986 RepoGroup,
2987 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2988 .join(
2989 UserGroup,
2990 UserGroupRepoGroupToPerm.users_group_id ==
2991 UserGroup.users_group_id)\
2992 .join(
2993 UserGroupMember,
2994 UserGroupRepoGroupToPerm.users_group_id ==
2995 UserGroupMember.users_group_id)\
2996 .filter(
2997 UserGroupMember.user_id == user_id,
2998 UserGroup.users_group_active == true())
2999 if repo_group_id:
3000 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3001 return q.all()
3002
3003 @classmethod
3004 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3005 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3006 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3007 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3008 .filter(UserUserGroupToPerm.user_id == user_id)
3009 if user_group_id:
3010 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3011 return q.all()
3012
3013 @classmethod
3014 def get_default_user_group_perms_from_user_group(
3015 cls, user_id, user_group_id=None):
3016 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3017 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3018 .join(
3019 Permission,
3020 UserGroupUserGroupToPerm.permission_id ==
3021 Permission.permission_id)\
3022 .join(
3023 TargetUserGroup,
3024 UserGroupUserGroupToPerm.target_user_group_id ==
3025 TargetUserGroup.users_group_id)\
3026 .join(
3027 UserGroup,
3028 UserGroupUserGroupToPerm.user_group_id ==
3029 UserGroup.users_group_id)\
3030 .join(
3031 UserGroupMember,
3032 UserGroupUserGroupToPerm.user_group_id ==
3033 UserGroupMember.users_group_id)\
3034 .filter(
3035 UserGroupMember.user_id == user_id,
3036 UserGroup.users_group_active == true())
3037 if user_group_id:
3038 q = q.filter(
3039 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3040
3041 return q.all()
3042
3043
3044 class UserRepoToPerm(Base, BaseModel):
3045 __tablename__ = 'repo_to_perm'
3046 __table_args__ = (
3047 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3048 base_table_args
3049 )
3050
3051 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3052 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3053 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3054 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3055
3056 user = relationship('User')
3057 repository = relationship('Repository')
3058 permission = relationship('Permission')
3059
3060 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete, delete-orphan", lazy='joined')
3061
3062 @classmethod
3063 def create(cls, user, repository, permission):
3064 n = cls()
3065 n.user = user
3066 n.repository = repository
3067 n.permission = permission
3068 Session().add(n)
3069 return n
3070
3071 def __unicode__(self):
3072 return u'<%s => %s >' % (self.user, self.repository)
3073
3074
3075 class UserUserGroupToPerm(Base, BaseModel):
3076 __tablename__ = 'user_user_group_to_perm'
3077 __table_args__ = (
3078 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3079 base_table_args
3080 )
3081
3082 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3083 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3084 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3085 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3086
3087 user = relationship('User')
3088 user_group = relationship('UserGroup')
3089 permission = relationship('Permission')
3090
3091 @classmethod
3092 def create(cls, user, user_group, permission):
3093 n = cls()
3094 n.user = user
3095 n.user_group = user_group
3096 n.permission = permission
3097 Session().add(n)
3098 return n
3099
3100 def __unicode__(self):
3101 return u'<%s => %s >' % (self.user, self.user_group)
3102
3103
3104 class UserToPerm(Base, BaseModel):
3105 __tablename__ = 'user_to_perm'
3106 __table_args__ = (
3107 UniqueConstraint('user_id', 'permission_id'),
3108 base_table_args
3109 )
3110
3111 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3112 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3113 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3114
3115 user = relationship('User')
3116 permission = relationship('Permission', lazy='joined')
3117
3118 def __unicode__(self):
3119 return u'<%s => %s >' % (self.user, self.permission)
3120
3121
3122 class UserGroupRepoToPerm(Base, BaseModel):
3123 __tablename__ = 'users_group_repo_to_perm'
3124 __table_args__ = (
3125 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3126 base_table_args
3127 )
3128
3129 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3130 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3131 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3132 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3133
3134 users_group = relationship('UserGroup')
3135 permission = relationship('Permission')
3136 repository = relationship('Repository')
3137 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3138
3139 @classmethod
3140 def create(cls, users_group, repository, permission):
3141 n = cls()
3142 n.users_group = users_group
3143 n.repository = repository
3144 n.permission = permission
3145 Session().add(n)
3146 return n
3147
3148 def __unicode__(self):
3149 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3150
3151
3152 class UserGroupUserGroupToPerm(Base, BaseModel):
3153 __tablename__ = 'user_group_user_group_to_perm'
3154 __table_args__ = (
3155 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3156 CheckConstraint('target_user_group_id != user_group_id'),
3157 base_table_args
3158 )
3159
3160 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3161 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3162 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3163 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3164
3165 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3166 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3167 permission = relationship('Permission')
3168
3169 @classmethod
3170 def create(cls, target_user_group, user_group, permission):
3171 n = cls()
3172 n.target_user_group = target_user_group
3173 n.user_group = user_group
3174 n.permission = permission
3175 Session().add(n)
3176 return n
3177
3178 def __unicode__(self):
3179 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3180
3181
3182 class UserGroupToPerm(Base, BaseModel):
3183 __tablename__ = 'users_group_to_perm'
3184 __table_args__ = (
3185 UniqueConstraint('users_group_id', 'permission_id',),
3186 base_table_args
3187 )
3188
3189 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3191 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3192
3193 users_group = relationship('UserGroup')
3194 permission = relationship('Permission')
3195
3196
3197 class UserRepoGroupToPerm(Base, BaseModel):
3198 __tablename__ = 'user_repo_group_to_perm'
3199 __table_args__ = (
3200 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3201 base_table_args
3202 )
3203
3204 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3205 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3206 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3207 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3208
3209 user = relationship('User')
3210 group = relationship('RepoGroup')
3211 permission = relationship('Permission')
3212
3213 @classmethod
3214 def create(cls, user, repository_group, permission):
3215 n = cls()
3216 n.user = user
3217 n.group = repository_group
3218 n.permission = permission
3219 Session().add(n)
3220 return n
3221
3222
3223 class UserGroupRepoGroupToPerm(Base, BaseModel):
3224 __tablename__ = 'users_group_repo_group_to_perm'
3225 __table_args__ = (
3226 UniqueConstraint('users_group_id', 'group_id'),
3227 base_table_args
3228 )
3229
3230 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3231 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3232 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3233 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3234
3235 users_group = relationship('UserGroup')
3236 permission = relationship('Permission')
3237 group = relationship('RepoGroup')
3238
3239 @classmethod
3240 def create(cls, user_group, repository_group, permission):
3241 n = cls()
3242 n.users_group = user_group
3243 n.group = repository_group
3244 n.permission = permission
3245 Session().add(n)
3246 return n
3247
3248 def __unicode__(self):
3249 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3250
3251
3252 class Statistics(Base, BaseModel):
3253 __tablename__ = 'statistics'
3254 __table_args__ = (
3255 base_table_args
3256 )
3257
3258 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3259 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3260 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3261 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3262 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3263 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3264
3265 repository = relationship('Repository', single_parent=True)
3266
3267
3268 class UserFollowing(Base, BaseModel):
3269 __tablename__ = 'user_followings'
3270 __table_args__ = (
3271 UniqueConstraint('user_id', 'follows_repository_id'),
3272 UniqueConstraint('user_id', 'follows_user_id'),
3273 base_table_args
3274 )
3275
3276 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3277 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3278 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3279 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3280 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3281
3282 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3283
3284 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3285 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3286
3287 @classmethod
3288 def get_repo_followers(cls, repo_id):
3289 return cls.query().filter(cls.follows_repo_id == repo_id)
3290
3291
3292 class CacheKey(Base, BaseModel):
3293 __tablename__ = 'cache_invalidation'
3294 __table_args__ = (
3295 UniqueConstraint('cache_key'),
3296 Index('key_idx', 'cache_key'),
3297 base_table_args,
3298 )
3299
3300 CACHE_TYPE_FEED = 'FEED'
3301 CACHE_TYPE_README = 'README'
3302 # namespaces used to register process/thread aware caches
3303 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3304 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3305
3306 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3307 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3308 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3309 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3310
3311 def __init__(self, cache_key, cache_args=''):
3312 self.cache_key = cache_key
3313 self.cache_args = cache_args
3314 self.cache_active = False
3315
3316 def __unicode__(self):
3317 return u"<%s('%s:%s[%s]')>" % (
3318 self.__class__.__name__,
3319 self.cache_id, self.cache_key, self.cache_active)
3320
3321 def _cache_key_partition(self):
3322 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3323 return prefix, repo_name, suffix
3324
3325 def get_prefix(self):
3326 """
3327 Try to extract prefix from existing cache key. The key could consist
3328 of prefix, repo_name, suffix
3329 """
3330 # this returns prefix, repo_name, suffix
3331 return self._cache_key_partition()[0]
3332
3333 def get_suffix(self):
3334 """
3335 get suffix that might have been used in _get_cache_key to
3336 generate self.cache_key. Only used for informational purposes
3337 in repo_edit.mako.
3338 """
3339 # prefix, repo_name, suffix
3340 return self._cache_key_partition()[2]
3341
3342 @classmethod
3343 def delete_all_cache(cls):
3344 """
3345 Delete all cache keys from database.
3346 Should only be run when all instances are down and all entries
3347 thus stale.
3348 """
3349 cls.query().delete()
3350 Session().commit()
3351
3352 @classmethod
3353 def set_invalidate(cls, cache_uid, delete=False):
3354 """
3355 Mark all caches of a repo as invalid in the database.
3356 """
3357
3358 try:
3359 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3360 if delete:
3361 qry.delete()
3362 log.debug('cache objects deleted for cache args %s',
3363 safe_str(cache_uid))
3364 else:
3365 qry.update({"cache_active": False})
3366 log.debug('cache objects marked as invalid for cache args %s',
3367 safe_str(cache_uid))
3368
3369 Session().commit()
3370 except Exception:
3371 log.exception(
3372 'Cache key invalidation failed for cache args %s',
3373 safe_str(cache_uid))
3374 Session().rollback()
3375
3376 @classmethod
3377 def get_active_cache(cls, cache_key):
3378 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3379 if inv_obj:
3380 return inv_obj
3381 return None
3382
3383
3384 class ChangesetComment(Base, BaseModel):
3385 __tablename__ = 'changeset_comments'
3386 __table_args__ = (
3387 Index('cc_revision_idx', 'revision'),
3388 base_table_args,
3389 )
3390
3391 COMMENT_OUTDATED = u'comment_outdated'
3392 COMMENT_TYPE_NOTE = u'note'
3393 COMMENT_TYPE_TODO = u'todo'
3394 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3395
3396 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3397 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3398 revision = Column('revision', String(40), nullable=True)
3399 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3400 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3401 line_no = Column('line_no', Unicode(10), nullable=True)
3402 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3403 f_path = Column('f_path', Unicode(1000), nullable=True)
3404 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3405 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3406 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3407 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3408 renderer = Column('renderer', Unicode(64), nullable=True)
3409 display_state = Column('display_state', Unicode(128), nullable=True)
3410
3411 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3412 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3413
3414 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3415 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3416
3417 author = relationship('User', lazy='joined')
3418 repo = relationship('Repository')
3419 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3420 pull_request = relationship('PullRequest', lazy='joined')
3421 pull_request_version = relationship('PullRequestVersion')
3422
3423 @classmethod
3424 def get_users(cls, revision=None, pull_request_id=None):
3425 """
3426 Returns user associated with this ChangesetComment. ie those
3427 who actually commented
3428
3429 :param cls:
3430 :param revision:
3431 """
3432 q = Session().query(User)\
3433 .join(ChangesetComment.author)
3434 if revision:
3435 q = q.filter(cls.revision == revision)
3436 elif pull_request_id:
3437 q = q.filter(cls.pull_request_id == pull_request_id)
3438 return q.all()
3439
3440 @classmethod
3441 def get_index_from_version(cls, pr_version, versions):
3442 num_versions = [x.pull_request_version_id for x in versions]
3443 try:
3444 return num_versions.index(pr_version) +1
3445 except (IndexError, ValueError):
3446 return
3447
3448 @property
3449 def outdated(self):
3450 return self.display_state == self.COMMENT_OUTDATED
3451
3452 def outdated_at_version(self, version):
3453 """
3454 Checks if comment is outdated for given pull request version
3455 """
3456 return self.outdated and self.pull_request_version_id != version
3457
3458 def older_than_version(self, version):
3459 """
3460 Checks if comment is made from previous version than given
3461 """
3462 if version is None:
3463 return self.pull_request_version_id is not None
3464
3465 return self.pull_request_version_id < version
3466
3467 @property
3468 def resolved(self):
3469 return self.resolved_by[0] if self.resolved_by else None
3470
3471 @property
3472 def is_todo(self):
3473 return self.comment_type == self.COMMENT_TYPE_TODO
3474
3475 @property
3476 def is_inline(self):
3477 return self.line_no and self.f_path
3478
3479 def get_index_version(self, versions):
3480 return self.get_index_from_version(
3481 self.pull_request_version_id, versions)
3482
3483 def __repr__(self):
3484 if self.comment_id:
3485 return '<DB:Comment #%s>' % self.comment_id
3486 else:
3487 return '<DB:Comment at %#x>' % id(self)
3488
3489 def get_api_data(self):
3490 comment = self
3491 data = {
3492 'comment_id': comment.comment_id,
3493 'comment_type': comment.comment_type,
3494 'comment_text': comment.text,
3495 'comment_status': comment.status_change,
3496 'comment_f_path': comment.f_path,
3497 'comment_lineno': comment.line_no,
3498 'comment_author': comment.author,
3499 'comment_created_on': comment.created_on
3500 }
3501 return data
3502
3503 def __json__(self):
3504 data = dict()
3505 data.update(self.get_api_data())
3506 return data
3507
3508
3509 class ChangesetStatus(Base, BaseModel):
3510 __tablename__ = 'changeset_statuses'
3511 __table_args__ = (
3512 Index('cs_revision_idx', 'revision'),
3513 Index('cs_version_idx', 'version'),
3514 UniqueConstraint('repo_id', 'revision', 'version'),
3515 base_table_args
3516 )
3517
3518 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3519 STATUS_APPROVED = 'approved'
3520 STATUS_REJECTED = 'rejected'
3521 STATUS_UNDER_REVIEW = 'under_review'
3522
3523 STATUSES = [
3524 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3525 (STATUS_APPROVED, _("Approved")),
3526 (STATUS_REJECTED, _("Rejected")),
3527 (STATUS_UNDER_REVIEW, _("Under Review")),
3528 ]
3529
3530 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3531 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3532 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3533 revision = Column('revision', String(40), nullable=False)
3534 status = Column('status', String(128), nullable=False, default=DEFAULT)
3535 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3536 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3537 version = Column('version', Integer(), nullable=False, default=0)
3538 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3539
3540 author = relationship('User', lazy='joined')
3541 repo = relationship('Repository')
3542 comment = relationship('ChangesetComment', lazy='joined')
3543 pull_request = relationship('PullRequest', lazy='joined')
3544
3545 def __unicode__(self):
3546 return u"<%s('%s[v%s]:%s')>" % (
3547 self.__class__.__name__,
3548 self.status, self.version, self.author
3549 )
3550
3551 @classmethod
3552 def get_status_lbl(cls, value):
3553 return dict(cls.STATUSES).get(value)
3554
3555 @property
3556 def status_lbl(self):
3557 return ChangesetStatus.get_status_lbl(self.status)
3558
3559 def get_api_data(self):
3560 status = self
3561 data = {
3562 'status_id': status.changeset_status_id,
3563 'status': status.status,
3564 }
3565 return data
3566
3567 def __json__(self):
3568 data = dict()
3569 data.update(self.get_api_data())
3570 return data
3571
3572
3573 class _SetState(object):
3574 """
3575 Context processor allowing changing state for sensitive operation such as
3576 pull request update or merge
3577 """
3578
3579 def __init__(self, pull_request, pr_state, back_state=None):
3580 self._pr = pull_request
3581 self._org_state = back_state or pull_request.pull_request_state
3582 self._pr_state = pr_state
3583
3584 def __enter__(self):
3585 log.debug('StateLock: entering set state context, setting state to: `%s`',
3586 self._pr_state)
3587 self._pr.pull_request_state = self._pr_state
3588 Session().add(self._pr)
3589 Session().commit()
3590
3591 def __exit__(self, exc_type, exc_val, exc_tb):
3592 log.debug('StateLock: exiting set state context, setting state to: `%s`',
3593 self._org_state)
3594 self._pr.pull_request_state = self._org_state
3595 Session().add(self._pr)
3596 Session().commit()
3597
3598
3599 class _PullRequestBase(BaseModel):
3600 """
3601 Common attributes of pull request and version entries.
3602 """
3603
3604 # .status values
3605 STATUS_NEW = u'new'
3606 STATUS_OPEN = u'open'
3607 STATUS_CLOSED = u'closed'
3608
3609 # available states
3610 STATE_CREATING = u'creating'
3611 STATE_UPDATING = u'updating'
3612 STATE_MERGING = u'merging'
3613 STATE_CREATED = u'created'
3614
3615 title = Column('title', Unicode(255), nullable=True)
3616 description = Column(
3617 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3618 nullable=True)
3619 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3620
3621 # new/open/closed status of pull request (not approve/reject/etc)
3622 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3623 created_on = Column(
3624 'created_on', DateTime(timezone=False), nullable=False,
3625 default=datetime.datetime.now)
3626 updated_on = Column(
3627 'updated_on', DateTime(timezone=False), nullable=False,
3628 default=datetime.datetime.now)
3629
3630 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3631
3632 @declared_attr
3633 def user_id(cls):
3634 return Column(
3635 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3636 unique=None)
3637
3638 # 500 revisions max
3639 _revisions = Column(
3640 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3641
3642 @declared_attr
3643 def source_repo_id(cls):
3644 # TODO: dan: rename column to source_repo_id
3645 return Column(
3646 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3647 nullable=False)
3648
3649 _source_ref = Column('org_ref', Unicode(255), nullable=False)
3650
3651 @hybrid_property
3652 def source_ref(self):
3653 return self._source_ref
3654
3655 @source_ref.setter
3656 def source_ref(self, val):
3657 parts = (val or '').split(':')
3658 if len(parts) != 3:
3659 raise ValueError(
3660 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3661 self._source_ref = safe_unicode(val)
3662
3663 _target_ref = Column('other_ref', Unicode(255), nullable=False)
3664
3665 @hybrid_property
3666 def target_ref(self):
3667 return self._target_ref
3668
3669 @target_ref.setter
3670 def target_ref(self, val):
3671 parts = (val or '').split(':')
3672 if len(parts) != 3:
3673 raise ValueError(
3674 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3675 self._target_ref = safe_unicode(val)
3676
3677 @declared_attr
3678 def target_repo_id(cls):
3679 # TODO: dan: rename column to target_repo_id
3680 return Column(
3681 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3682 nullable=False)
3683
3684 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3685
3686 # TODO: dan: rename column to last_merge_source_rev
3687 _last_merge_source_rev = Column(
3688 'last_merge_org_rev', String(40), nullable=True)
3689 # TODO: dan: rename column to last_merge_target_rev
3690 _last_merge_target_rev = Column(
3691 'last_merge_other_rev', String(40), nullable=True)
3692 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3693 merge_rev = Column('merge_rev', String(40), nullable=True)
3694
3695 reviewer_data = Column(
3696 'reviewer_data_json', MutationObj.as_mutable(
3697 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3698
3699 @property
3700 def reviewer_data_json(self):
3701 return json.dumps(self.reviewer_data)
3702
3703 @hybrid_property
3704 def description_safe(self):
3705 from rhodecode.lib import helpers as h
3706 return h.escape(self.description)
3707
3708 @hybrid_property
3709 def revisions(self):
3710 return self._revisions.split(':') if self._revisions else []
3711
3712 @revisions.setter
3713 def revisions(self, val):
3714 self._revisions = ':'.join(val)
3715
3716 @hybrid_property
3717 def last_merge_status(self):
3718 return safe_int(self._last_merge_status)
3719
3720 @last_merge_status.setter
3721 def last_merge_status(self, val):
3722 self._last_merge_status = val
3723
3724 @declared_attr
3725 def author(cls):
3726 return relationship('User', lazy='joined')
3727
3728 @declared_attr
3729 def source_repo(cls):
3730 return relationship(
3731 'Repository',
3732 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3733
3734 @property
3735 def source_ref_parts(self):
3736 return self.unicode_to_reference(self.source_ref)
3737
3738 @declared_attr
3739 def target_repo(cls):
3740 return relationship(
3741 'Repository',
3742 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3743
3744 @property
3745 def target_ref_parts(self):
3746 return self.unicode_to_reference(self.target_ref)
3747
3748 @property
3749 def shadow_merge_ref(self):
3750 return self.unicode_to_reference(self._shadow_merge_ref)
3751
3752 @shadow_merge_ref.setter
3753 def shadow_merge_ref(self, ref):
3754 self._shadow_merge_ref = self.reference_to_unicode(ref)
3755
3756 @staticmethod
3757 def unicode_to_reference(raw):
3758 """
3759 Convert a unicode (or string) to a reference object.
3760 If unicode evaluates to False it returns None.
3761 """
3762 if raw:
3763 refs = raw.split(':')
3764 return Reference(*refs)
3765 else:
3766 return None
3767
3768 @staticmethod
3769 def reference_to_unicode(ref):
3770 """
3771 Convert a reference object to unicode.
3772 If reference is None it returns None.
3773 """
3774 if ref:
3775 return u':'.join(ref)
3776 else:
3777 return None
3778
3779 def get_api_data(self, with_merge_state=True):
3780 from rhodecode.model.pull_request import PullRequestModel
3781
3782 pull_request = self
3783 if with_merge_state:
3784 merge_status = PullRequestModel().merge_status(pull_request)
3785 merge_state = {
3786 'status': merge_status[0],
3787 'message': safe_unicode(merge_status[1]),
3788 }
3789 else:
3790 merge_state = {'status': 'not_available',
3791 'message': 'not_available'}
3792
3793 merge_data = {
3794 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3795 'reference': (
3796 pull_request.shadow_merge_ref._asdict()
3797 if pull_request.shadow_merge_ref else None),
3798 }
3799
3800 data = {
3801 'pull_request_id': pull_request.pull_request_id,
3802 'url': PullRequestModel().get_url(pull_request),
3803 'title': pull_request.title,
3804 'description': pull_request.description,
3805 'status': pull_request.status,
3806 'state': pull_request.pull_request_state,
3807 'created_on': pull_request.created_on,
3808 'updated_on': pull_request.updated_on,
3809 'commit_ids': pull_request.revisions,
3810 'review_status': pull_request.calculated_review_status(),
3811 'mergeable': merge_state,
3812 'source': {
3813 'clone_url': pull_request.source_repo.clone_url(),
3814 'repository': pull_request.source_repo.repo_name,
3815 'reference': {
3816 'name': pull_request.source_ref_parts.name,
3817 'type': pull_request.source_ref_parts.type,
3818 'commit_id': pull_request.source_ref_parts.commit_id,
3819 },
3820 },
3821 'target': {
3822 'clone_url': pull_request.target_repo.clone_url(),
3823 'repository': pull_request.target_repo.repo_name,
3824 'reference': {
3825 'name': pull_request.target_ref_parts.name,
3826 'type': pull_request.target_ref_parts.type,
3827 'commit_id': pull_request.target_ref_parts.commit_id,
3828 },
3829 },
3830 'merge': merge_data,
3831 'author': pull_request.author.get_api_data(include_secrets=False,
3832 details='basic'),
3833 'reviewers': [
3834 {
3835 'user': reviewer.get_api_data(include_secrets=False,
3836 details='basic'),
3837 'reasons': reasons,
3838 'review_status': st[0][1].status if st else 'not_reviewed',
3839 }
3840 for obj, reviewer, reasons, mandatory, st in
3841 pull_request.reviewers_statuses()
3842 ]
3843 }
3844
3845 return data
3846
3847 def set_state(self, pull_request_state, final_state=None):
3848 """
3849 # goes from initial state to updating to initial state.
3850 # initial state can be changed by specifying back_state=
3851 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
3852 pull_request.merge()
3853
3854 :param pull_request_state:
3855 :param final_state:
3856
3857 """
3858
3859 return _SetState(self, pull_request_state, back_state=final_state)
3860
3861
3862 class PullRequest(Base, _PullRequestBase):
3863 __tablename__ = 'pull_requests'
3864 __table_args__ = (
3865 base_table_args,
3866 )
3867
3868 pull_request_id = Column(
3869 'pull_request_id', Integer(), nullable=False, primary_key=True)
3870
3871 def __repr__(self):
3872 if self.pull_request_id:
3873 return '<DB:PullRequest #%s>' % self.pull_request_id
3874 else:
3875 return '<DB:PullRequest at %#x>' % id(self)
3876
3877 reviewers = relationship('PullRequestReviewers',
3878 cascade="all, delete, delete-orphan")
3879 statuses = relationship('ChangesetStatus',
3880 cascade="all, delete, delete-orphan")
3881 comments = relationship('ChangesetComment',
3882 cascade="all, delete, delete-orphan")
3883 versions = relationship('PullRequestVersion',
3884 cascade="all, delete, delete-orphan",
3885 lazy='dynamic')
3886
3887 @classmethod
3888 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3889 internal_methods=None):
3890
3891 class PullRequestDisplay(object):
3892 """
3893 Special object wrapper for showing PullRequest data via Versions
3894 It mimics PR object as close as possible. This is read only object
3895 just for display
3896 """
3897
3898 def __init__(self, attrs, internal=None):
3899 self.attrs = attrs
3900 # internal have priority over the given ones via attrs
3901 self.internal = internal or ['versions']
3902
3903 def __getattr__(self, item):
3904 if item in self.internal:
3905 return getattr(self, item)
3906 try:
3907 return self.attrs[item]
3908 except KeyError:
3909 raise AttributeError(
3910 '%s object has no attribute %s' % (self, item))
3911
3912 def __repr__(self):
3913 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3914
3915 def versions(self):
3916 return pull_request_obj.versions.order_by(
3917 PullRequestVersion.pull_request_version_id).all()
3918
3919 def is_closed(self):
3920 return pull_request_obj.is_closed()
3921
3922 @property
3923 def pull_request_version_id(self):
3924 return getattr(pull_request_obj, 'pull_request_version_id', None)
3925
3926 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3927
3928 attrs.author = StrictAttributeDict(
3929 pull_request_obj.author.get_api_data())
3930 if pull_request_obj.target_repo:
3931 attrs.target_repo = StrictAttributeDict(
3932 pull_request_obj.target_repo.get_api_data())
3933 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3934
3935 if pull_request_obj.source_repo:
3936 attrs.source_repo = StrictAttributeDict(
3937 pull_request_obj.source_repo.get_api_data())
3938 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3939
3940 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3941 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3942 attrs.revisions = pull_request_obj.revisions
3943
3944 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3945 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3946 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3947
3948 return PullRequestDisplay(attrs, internal=internal_methods)
3949
3950 def is_closed(self):
3951 return self.status == self.STATUS_CLOSED
3952
3953 def __json__(self):
3954 return {
3955 'revisions': self.revisions,
3956 }
3957
3958 def calculated_review_status(self):
3959 from rhodecode.model.changeset_status import ChangesetStatusModel
3960 return ChangesetStatusModel().calculated_review_status(self)
3961
3962 def reviewers_statuses(self):
3963 from rhodecode.model.changeset_status import ChangesetStatusModel
3964 return ChangesetStatusModel().reviewers_statuses(self)
3965
3966 @property
3967 def workspace_id(self):
3968 from rhodecode.model.pull_request import PullRequestModel
3969 return PullRequestModel()._workspace_id(self)
3970
3971 def get_shadow_repo(self):
3972 workspace_id = self.workspace_id
3973 vcs_obj = self.target_repo.scm_instance()
3974 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3975 self.target_repo.repo_id, workspace_id)
3976 if os.path.isdir(shadow_repository_path):
3977 return vcs_obj._get_shadow_instance(shadow_repository_path)
3978
3979
3980 class PullRequestVersion(Base, _PullRequestBase):
3981 __tablename__ = 'pull_request_versions'
3982 __table_args__ = (
3983 base_table_args,
3984 )
3985
3986 pull_request_version_id = Column(
3987 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3988 pull_request_id = Column(
3989 'pull_request_id', Integer(),
3990 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3991 pull_request = relationship('PullRequest')
3992
3993 def __repr__(self):
3994 if self.pull_request_version_id:
3995 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3996 else:
3997 return '<DB:PullRequestVersion at %#x>' % id(self)
3998
3999 @property
4000 def reviewers(self):
4001 return self.pull_request.reviewers
4002
4003 @property
4004 def versions(self):
4005 return self.pull_request.versions
4006
4007 def is_closed(self):
4008 # calculate from original
4009 return self.pull_request.status == self.STATUS_CLOSED
4010
4011 def calculated_review_status(self):
4012 return self.pull_request.calculated_review_status()
4013
4014 def reviewers_statuses(self):
4015 return self.pull_request.reviewers_statuses()
4016
4017
4018 class PullRequestReviewers(Base, BaseModel):
4019 __tablename__ = 'pull_request_reviewers'
4020 __table_args__ = (
4021 base_table_args,
4022 )
4023
4024 @hybrid_property
4025 def reasons(self):
4026 if not self._reasons:
4027 return []
4028 return self._reasons
4029
4030 @reasons.setter
4031 def reasons(self, val):
4032 val = val or []
4033 if any(not isinstance(x, compat.string_types) for x in val):
4034 raise Exception('invalid reasons type, must be list of strings')
4035 self._reasons = val
4036
4037 pull_requests_reviewers_id = Column(
4038 'pull_requests_reviewers_id', Integer(), nullable=False,
4039 primary_key=True)
4040 pull_request_id = Column(
4041 "pull_request_id", Integer(),
4042 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4043 user_id = Column(
4044 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4045 _reasons = Column(
4046 'reason', MutationList.as_mutable(
4047 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4048
4049 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4050 user = relationship('User')
4051 pull_request = relationship('PullRequest')
4052
4053 rule_data = Column(
4054 'rule_data_json',
4055 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4056
4057 def rule_user_group_data(self):
4058 """
4059 Returns the voting user group rule data for this reviewer
4060 """
4061
4062 if self.rule_data and 'vote_rule' in self.rule_data:
4063 user_group_data = {}
4064 if 'rule_user_group_entry_id' in self.rule_data:
4065 # means a group with voting rules !
4066 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4067 user_group_data['name'] = self.rule_data['rule_name']
4068 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4069
4070 return user_group_data
4071
4072 def __unicode__(self):
4073 return u"<%s('id:%s')>" % (self.__class__.__name__,
4074 self.pull_requests_reviewers_id)
4075
4076
4077 class Notification(Base, BaseModel):
4078 __tablename__ = 'notifications'
4079 __table_args__ = (
4080 Index('notification_type_idx', 'type'),
4081 base_table_args,
4082 )
4083
4084 TYPE_CHANGESET_COMMENT = u'cs_comment'
4085 TYPE_MESSAGE = u'message'
4086 TYPE_MENTION = u'mention'
4087 TYPE_REGISTRATION = u'registration'
4088 TYPE_PULL_REQUEST = u'pull_request'
4089 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4090
4091 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4092 subject = Column('subject', Unicode(512), nullable=True)
4093 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4094 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4095 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4096 type_ = Column('type', Unicode(255))
4097
4098 created_by_user = relationship('User')
4099 notifications_to_users = relationship('UserNotification', lazy='joined',
4100 cascade="all, delete, delete-orphan")
4101
4102 @property
4103 def recipients(self):
4104 return [x.user for x in UserNotification.query()\
4105 .filter(UserNotification.notification == self)\
4106 .order_by(UserNotification.user_id.asc()).all()]
4107
4108 @classmethod
4109 def create(cls, created_by, subject, body, recipients, type_=None):
4110 if type_ is None:
4111 type_ = Notification.TYPE_MESSAGE
4112
4113 notification = cls()
4114 notification.created_by_user = created_by
4115 notification.subject = subject
4116 notification.body = body
4117 notification.type_ = type_
4118 notification.created_on = datetime.datetime.now()
4119
4120 # For each recipient link the created notification to his account
4121 for u in recipients:
4122 assoc = UserNotification()
4123 assoc.user_id = u.user_id
4124 assoc.notification = notification
4125
4126 # if created_by is inside recipients mark his notification
4127 # as read
4128 if u.user_id == created_by.user_id:
4129 assoc.read = True
4130 Session().add(assoc)
4131
4132 Session().add(notification)
4133
4134 return notification
4135
4136
4137 class UserNotification(Base, BaseModel):
4138 __tablename__ = 'user_to_notification'
4139 __table_args__ = (
4140 UniqueConstraint('user_id', 'notification_id'),
4141 base_table_args
4142 )
4143
4144 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4145 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4146 read = Column('read', Boolean, default=False)
4147 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4148
4149 user = relationship('User', lazy="joined")
4150 notification = relationship('Notification', lazy="joined",
4151 order_by=lambda: Notification.created_on.desc(),)
4152
4153 def mark_as_read(self):
4154 self.read = True
4155 Session().add(self)
4156
4157
4158 class Gist(Base, BaseModel):
4159 __tablename__ = 'gists'
4160 __table_args__ = (
4161 Index('g_gist_access_id_idx', 'gist_access_id'),
4162 Index('g_created_on_idx', 'created_on'),
4163 base_table_args
4164 )
4165
4166 GIST_PUBLIC = u'public'
4167 GIST_PRIVATE = u'private'
4168 DEFAULT_FILENAME = u'gistfile1.txt'
4169
4170 ACL_LEVEL_PUBLIC = u'acl_public'
4171 ACL_LEVEL_PRIVATE = u'acl_private'
4172
4173 gist_id = Column('gist_id', Integer(), primary_key=True)
4174 gist_access_id = Column('gist_access_id', Unicode(250))
4175 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4176 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4177 gist_expires = Column('gist_expires', Float(53), nullable=False)
4178 gist_type = Column('gist_type', Unicode(128), nullable=False)
4179 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4180 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4181 acl_level = Column('acl_level', Unicode(128), nullable=True)
4182
4183 owner = relationship('User')
4184
4185 def __repr__(self):
4186 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4187
4188 @hybrid_property
4189 def description_safe(self):
4190 from rhodecode.lib import helpers as h
4191 return h.escape(self.gist_description)
4192
4193 @classmethod
4194 def get_or_404(cls, id_):
4195 from pyramid.httpexceptions import HTTPNotFound
4196
4197 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4198 if not res:
4199 raise HTTPNotFound()
4200 return res
4201
4202 @classmethod
4203 def get_by_access_id(cls, gist_access_id):
4204 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4205
4206 def gist_url(self):
4207 from rhodecode.model.gist import GistModel
4208 return GistModel().get_url(self)
4209
4210 @classmethod
4211 def base_path(cls):
4212 """
4213 Returns base path when all gists are stored
4214
4215 :param cls:
4216 """
4217 from rhodecode.model.gist import GIST_STORE_LOC
4218 q = Session().query(RhodeCodeUi)\
4219 .filter(RhodeCodeUi.ui_key == URL_SEP)
4220 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4221 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4222
4223 def get_api_data(self):
4224 """
4225 Common function for generating gist related data for API
4226 """
4227 gist = self
4228 data = {
4229 'gist_id': gist.gist_id,
4230 'type': gist.gist_type,
4231 'access_id': gist.gist_access_id,
4232 'description': gist.gist_description,
4233 'url': gist.gist_url(),
4234 'expires': gist.gist_expires,
4235 'created_on': gist.created_on,
4236 'modified_at': gist.modified_at,
4237 'content': None,
4238 'acl_level': gist.acl_level,
4239 }
4240 return data
4241
4242 def __json__(self):
4243 data = dict(
4244 )
4245 data.update(self.get_api_data())
4246 return data
4247 # SCM functions
4248
4249 def scm_instance(self, **kwargs):
4250 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4251 return get_vcs_instance(
4252 repo_path=safe_str(full_repo_path), create=False)
4253
4254
4255 class ExternalIdentity(Base, BaseModel):
4256 __tablename__ = 'external_identities'
4257 __table_args__ = (
4258 Index('local_user_id_idx', 'local_user_id'),
4259 Index('external_id_idx', 'external_id'),
4260 base_table_args
4261 )
4262
4263 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4264 external_username = Column('external_username', Unicode(1024), default=u'')
4265 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4266 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4267 access_token = Column('access_token', String(1024), default=u'')
4268 alt_token = Column('alt_token', String(1024), default=u'')
4269 token_secret = Column('token_secret', String(1024), default=u'')
4270
4271 @classmethod
4272 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4273 """
4274 Returns ExternalIdentity instance based on search params
4275
4276 :param external_id:
4277 :param provider_name:
4278 :return: ExternalIdentity
4279 """
4280 query = cls.query()
4281 query = query.filter(cls.external_id == external_id)
4282 query = query.filter(cls.provider_name == provider_name)
4283 if local_user_id:
4284 query = query.filter(cls.local_user_id == local_user_id)
4285 return query.first()
4286
4287 @classmethod
4288 def user_by_external_id_and_provider(cls, external_id, provider_name):
4289 """
4290 Returns User instance based on search params
4291
4292 :param external_id:
4293 :param provider_name:
4294 :return: User
4295 """
4296 query = User.query()
4297 query = query.filter(cls.external_id == external_id)
4298 query = query.filter(cls.provider_name == provider_name)
4299 query = query.filter(User.user_id == cls.local_user_id)
4300 return query.first()
4301
4302 @classmethod
4303 def by_local_user_id(cls, local_user_id):
4304 """
4305 Returns all tokens for user
4306
4307 :param local_user_id:
4308 :return: ExternalIdentity
4309 """
4310 query = cls.query()
4311 query = query.filter(cls.local_user_id == local_user_id)
4312 return query
4313
4314 @classmethod
4315 def load_provider_plugin(cls, plugin_id):
4316 from rhodecode.authentication.base import loadplugin
4317 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4318 auth_plugin = loadplugin(_plugin_id)
4319 return auth_plugin
4320
4321
4322 class Integration(Base, BaseModel):
4323 __tablename__ = 'integrations'
4324 __table_args__ = (
4325 base_table_args
4326 )
4327
4328 integration_id = Column('integration_id', Integer(), primary_key=True)
4329 integration_type = Column('integration_type', String(255))
4330 enabled = Column('enabled', Boolean(), nullable=False)
4331 name = Column('name', String(255), nullable=False)
4332 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4333 default=False)
4334
4335 settings = Column(
4336 'settings_json', MutationObj.as_mutable(
4337 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4338 repo_id = Column(
4339 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4340 nullable=True, unique=None, default=None)
4341 repo = relationship('Repository', lazy='joined')
4342
4343 repo_group_id = Column(
4344 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4345 nullable=True, unique=None, default=None)
4346 repo_group = relationship('RepoGroup', lazy='joined')
4347
4348 @property
4349 def scope(self):
4350 if self.repo:
4351 return repr(self.repo)
4352 if self.repo_group:
4353 if self.child_repos_only:
4354 return repr(self.repo_group) + ' (child repos only)'
4355 else:
4356 return repr(self.repo_group) + ' (recursive)'
4357 if self.child_repos_only:
4358 return 'root_repos'
4359 return 'global'
4360
4361 def __repr__(self):
4362 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4363
4364
4365 class RepoReviewRuleUser(Base, BaseModel):
4366 __tablename__ = 'repo_review_rules_users'
4367 __table_args__ = (
4368 base_table_args
4369 )
4370
4371 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4372 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4373 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4374 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4375 user = relationship('User')
4376
4377 def rule_data(self):
4378 return {
4379 'mandatory': self.mandatory
4380 }
4381
4382
4383 class RepoReviewRuleUserGroup(Base, BaseModel):
4384 __tablename__ = 'repo_review_rules_users_groups'
4385 __table_args__ = (
4386 base_table_args
4387 )
4388
4389 VOTE_RULE_ALL = -1
4390
4391 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4392 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4393 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4394 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4395 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4396 users_group = relationship('UserGroup')
4397
4398 def rule_data(self):
4399 return {
4400 'mandatory': self.mandatory,
4401 'vote_rule': self.vote_rule
4402 }
4403
4404 @property
4405 def vote_rule_label(self):
4406 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4407 return 'all must vote'
4408 else:
4409 return 'min. vote {}'.format(self.vote_rule)
4410
4411
4412 class RepoReviewRule(Base, BaseModel):
4413 __tablename__ = 'repo_review_rules'
4414 __table_args__ = (
4415 base_table_args
4416 )
4417
4418 repo_review_rule_id = Column(
4419 'repo_review_rule_id', Integer(), primary_key=True)
4420 repo_id = Column(
4421 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4422 repo = relationship('Repository', backref='review_rules')
4423
4424 review_rule_name = Column('review_rule_name', String(255))
4425 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4426 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4427 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4428
4429 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4430 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4431 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4432 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4433
4434 rule_users = relationship('RepoReviewRuleUser')
4435 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4436
4437 def _validate_pattern(self, value):
4438 re.compile('^' + glob2re(value) + '$')
4439
4440 @hybrid_property
4441 def source_branch_pattern(self):
4442 return self._branch_pattern or '*'
4443
4444 @source_branch_pattern.setter
4445 def source_branch_pattern(self, value):
4446 self._validate_pattern(value)
4447 self._branch_pattern = value or '*'
4448
4449 @hybrid_property
4450 def target_branch_pattern(self):
4451 return self._target_branch_pattern or '*'
4452
4453 @target_branch_pattern.setter
4454 def target_branch_pattern(self, value):
4455 self._validate_pattern(value)
4456 self._target_branch_pattern = value or '*'
4457
4458 @hybrid_property
4459 def file_pattern(self):
4460 return self._file_pattern or '*'
4461
4462 @file_pattern.setter
4463 def file_pattern(self, value):
4464 self._validate_pattern(value)
4465 self._file_pattern = value or '*'
4466
4467 def matches(self, source_branch, target_branch, files_changed):
4468 """
4469 Check if this review rule matches a branch/files in a pull request
4470
4471 :param source_branch: source branch name for the commit
4472 :param target_branch: target branch name for the commit
4473 :param files_changed: list of file paths changed in the pull request
4474 """
4475
4476 source_branch = source_branch or ''
4477 target_branch = target_branch or ''
4478 files_changed = files_changed or []
4479
4480 branch_matches = True
4481 if source_branch or target_branch:
4482 if self.source_branch_pattern == '*':
4483 source_branch_match = True
4484 else:
4485 if self.source_branch_pattern.startswith('re:'):
4486 source_pattern = self.source_branch_pattern[3:]
4487 else:
4488 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4489 source_branch_regex = re.compile(source_pattern)
4490 source_branch_match = bool(source_branch_regex.search(source_branch))
4491 if self.target_branch_pattern == '*':
4492 target_branch_match = True
4493 else:
4494 if self.target_branch_pattern.startswith('re:'):
4495 target_pattern = self.target_branch_pattern[3:]
4496 else:
4497 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4498 target_branch_regex = re.compile(target_pattern)
4499 target_branch_match = bool(target_branch_regex.search(target_branch))
4500
4501 branch_matches = source_branch_match and target_branch_match
4502
4503 files_matches = True
4504 if self.file_pattern != '*':
4505 files_matches = False
4506 if self.file_pattern.startswith('re:'):
4507 file_pattern = self.file_pattern[3:]
4508 else:
4509 file_pattern = glob2re(self.file_pattern)
4510 file_regex = re.compile(file_pattern)
4511 for filename in files_changed:
4512 if file_regex.search(filename):
4513 files_matches = True
4514 break
4515
4516 return branch_matches and files_matches
4517
4518 @property
4519 def review_users(self):
4520 """ Returns the users which this rule applies to """
4521
4522 users = collections.OrderedDict()
4523
4524 for rule_user in self.rule_users:
4525 if rule_user.user.active:
4526 if rule_user.user not in users:
4527 users[rule_user.user.username] = {
4528 'user': rule_user.user,
4529 'source': 'user',
4530 'source_data': {},
4531 'data': rule_user.rule_data()
4532 }
4533
4534 for rule_user_group in self.rule_user_groups:
4535 source_data = {
4536 'user_group_id': rule_user_group.users_group.users_group_id,
4537 'name': rule_user_group.users_group.users_group_name,
4538 'members': len(rule_user_group.users_group.members)
4539 }
4540 for member in rule_user_group.users_group.members:
4541 if member.user.active:
4542 key = member.user.username
4543 if key in users:
4544 # skip this member as we have him already
4545 # this prevents from override the "first" matched
4546 # users with duplicates in multiple groups
4547 continue
4548
4549 users[key] = {
4550 'user': member.user,
4551 'source': 'user_group',
4552 'source_data': source_data,
4553 'data': rule_user_group.rule_data()
4554 }
4555
4556 return users
4557
4558 def user_group_vote_rule(self, user_id):
4559
4560 rules = []
4561 if not self.rule_user_groups:
4562 return rules
4563
4564 for user_group in self.rule_user_groups:
4565 user_group_members = [x.user_id for x in user_group.users_group.members]
4566 if user_id in user_group_members:
4567 rules.append(user_group)
4568 return rules
4569
4570 def __repr__(self):
4571 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4572 self.repo_review_rule_id, self.repo)
4573
4574
4575 class ScheduleEntry(Base, BaseModel):
4576 __tablename__ = 'schedule_entries'
4577 __table_args__ = (
4578 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4579 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4580 base_table_args,
4581 )
4582
4583 schedule_types = ['crontab', 'timedelta', 'integer']
4584 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4585
4586 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4587 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4588 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4589
4590 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4591 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4592
4593 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4594 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4595
4596 # task
4597 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4598 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4599 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4600 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4601
4602 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4603 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4604
4605 @hybrid_property
4606 def schedule_type(self):
4607 return self._schedule_type
4608
4609 @schedule_type.setter
4610 def schedule_type(self, val):
4611 if val not in self.schedule_types:
4612 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4613 val, self.schedule_type))
4614
4615 self._schedule_type = val
4616
4617 @classmethod
4618 def get_uid(cls, obj):
4619 args = obj.task_args
4620 kwargs = obj.task_kwargs
4621 if isinstance(args, JsonRaw):
4622 try:
4623 args = json.loads(args)
4624 except ValueError:
4625 args = tuple()
4626
4627 if isinstance(kwargs, JsonRaw):
4628 try:
4629 kwargs = json.loads(kwargs)
4630 except ValueError:
4631 kwargs = dict()
4632
4633 dot_notation = obj.task_dot_notation
4634 val = '.'.join(map(safe_str, [
4635 sorted(dot_notation), args, sorted(kwargs.items())]))
4636 return hashlib.sha1(val).hexdigest()
4637
4638 @classmethod
4639 def get_by_schedule_name(cls, schedule_name):
4640 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4641
4642 @classmethod
4643 def get_by_schedule_id(cls, schedule_id):
4644 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4645
4646 @property
4647 def task(self):
4648 return self.task_dot_notation
4649
4650 @property
4651 def schedule(self):
4652 from rhodecode.lib.celerylib.utils import raw_2_schedule
4653 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4654 return schedule
4655
4656 @property
4657 def args(self):
4658 try:
4659 return list(self.task_args or [])
4660 except ValueError:
4661 return list()
4662
4663 @property
4664 def kwargs(self):
4665 try:
4666 return dict(self.task_kwargs or {})
4667 except ValueError:
4668 return dict()
4669
4670 def _as_raw(self, val):
4671 if hasattr(val, 'de_coerce'):
4672 val = val.de_coerce()
4673 if val:
4674 val = json.dumps(val)
4675
4676 return val
4677
4678 @property
4679 def schedule_definition_raw(self):
4680 return self._as_raw(self.schedule_definition)
4681
4682 @property
4683 def args_raw(self):
4684 return self._as_raw(self.task_args)
4685
4686 @property
4687 def kwargs_raw(self):
4688 return self._as_raw(self.task_kwargs)
4689
4690 def __repr__(self):
4691 return '<DB:ScheduleEntry({}:{})>'.format(
4692 self.schedule_entry_id, self.schedule_name)
4693
4694
4695 @event.listens_for(ScheduleEntry, 'before_update')
4696 def update_task_uid(mapper, connection, target):
4697 target.task_uid = ScheduleEntry.get_uid(target)
4698
4699
4700 @event.listens_for(ScheduleEntry, 'before_insert')
4701 def set_task_uid(mapper, connection, target):
4702 target.task_uid = ScheduleEntry.get_uid(target)
4703
4704
4705 class _BaseBranchPerms(BaseModel):
4706 @classmethod
4707 def compute_hash(cls, value):
4708 return sha1_safe(value)
4709
4710 @hybrid_property
4711 def branch_pattern(self):
4712 return self._branch_pattern or '*'
4713
4714 @hybrid_property
4715 def branch_hash(self):
4716 return self._branch_hash
4717
4718 def _validate_glob(self, value):
4719 re.compile('^' + glob2re(value) + '$')
4720
4721 @branch_pattern.setter
4722 def branch_pattern(self, value):
4723 self._validate_glob(value)
4724 self._branch_pattern = value or '*'
4725 # set the Hash when setting the branch pattern
4726 self._branch_hash = self.compute_hash(self._branch_pattern)
4727
4728 def matches(self, branch):
4729 """
4730 Check if this the branch matches entry
4731
4732 :param branch: branch name for the commit
4733 """
4734
4735 branch = branch or ''
4736
4737 branch_matches = True
4738 if branch:
4739 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4740 branch_matches = bool(branch_regex.search(branch))
4741
4742 return branch_matches
4743
4744
4745 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
4746 __tablename__ = 'user_to_repo_branch_permissions'
4747 __table_args__ = (
4748 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4749 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4750 )
4751
4752 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
4753
4754 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
4755 repo = relationship('Repository', backref='user_branch_perms')
4756
4757 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
4758 permission = relationship('Permission')
4759
4760 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
4761 user_repo_to_perm = relationship('UserRepoToPerm')
4762
4763 rule_order = Column('rule_order', Integer(), nullable=False)
4764 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
4765 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
4766
4767 def __unicode__(self):
4768 return u'<UserBranchPermission(%s => %r)>' % (
4769 self.user_repo_to_perm, self.branch_pattern)
4770
4771
4772 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
4773 __tablename__ = 'user_group_to_repo_branch_permissions'
4774 __table_args__ = (
4775 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4776 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4777 )
4778
4779 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
4780
4781 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
4782 repo = relationship('Repository', backref='user_group_branch_perms')
4783
4784 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
4785 permission = relationship('Permission')
4786
4787 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
4788 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
4789
4790 rule_order = Column('rule_order', Integer(), nullable=False)
4791 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
4792 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
4793
4794 def __unicode__(self):
4795 return u'<UserBranchPermission(%s => %r)>' % (
4796 self.user_group_repo_to_perm, self.branch_pattern)
4797
4798
4799 class UserBookmark(Base, BaseModel):
4800 __tablename__ = 'user_bookmarks'
4801 __table_args__ = (
4802 UniqueConstraint('user_id', 'bookmark_repo_id'),
4803 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
4804 UniqueConstraint('user_id', 'bookmark_position'),
4805 base_table_args
4806 )
4807
4808 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
4809 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
4810 position = Column("bookmark_position", Integer(), nullable=False)
4811 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
4812 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
4813 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4814
4815 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
4816 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
4817
4818 user = relationship("User")
4819
4820 repository = relationship("Repository")
4821 repository_group = relationship("RepoGroup")
4822
4823
4824 class DbMigrateVersion(Base, BaseModel):
4825 __tablename__ = 'db_migrate_version'
4826 __table_args__ = (
4827 base_table_args,
4828 )
4829
4830 repository_id = Column('repository_id', String(250), primary_key=True)
4831 repository_path = Column('repository_path', Text)
4832 version = Column('version', Integer)
4833
4834 @classmethod
4835 def set_version(cls, version):
4836 """
4837 Helper for forcing a different version, usually for debugging purposes via ishell.
4838 """
4839 ver = DbMigrateVersion.query().first()
4840 ver.version = version
4841 Session().commit()
4842
4843
4844 class DbSession(Base, BaseModel):
4845 __tablename__ = 'db_session'
4846 __table_args__ = (
4847 base_table_args,
4848 )
4849
4850 def __repr__(self):
4851 return '<DB:DbSession({})>'.format(self.id)
4852
4853 id = Column('id', Integer())
4854 namespace = Column('namespace', String(255), primary_key=True)
4855 accessed = Column('accessed', DateTime, nullable=False)
4856 created = Column('created', DateTime, nullable=False)
4857 data = Column('data', PickleType, nullable=False)
This diff has been collapsed as it changes many lines, (4932 lines changed) Show them Hide them
@@ -0,0 +1,4932 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Database Models for RhodeCode Enterprise
23 """
24
25 import re
26 import os
27 import time
28 import hashlib
29 import logging
30 import datetime
31 import warnings
32 import ipaddress
33 import functools
34 import traceback
35 import collections
36
37 from sqlalchemy import (
38 or_, and_, not_, func, TypeDecorator, event,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType)
42 from sqlalchemy.sql.expression import true, false
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.orm import (
45 relationship, joinedload, class_mapper, validates, aliased)
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from pyramid import compat
52 from pyramid.threadlocal import get_current_request
53
54 from rhodecode.translation import _
55 from rhodecode.lib.vcs import get_vcs_instance
56 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
57 from rhodecode.lib.utils2 import (
58 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
60 glob2re, StrictAttributeDict, cleaned_uri)
61 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
62 JsonRaw
63 from rhodecode.lib.ext_json import json
64 from rhodecode.lib.caching_query import FromCache
65 from rhodecode.lib.encrypt import AESCipher
66
67 from rhodecode.model.meta import Base, Session
68
69 URL_SEP = '/'
70 log = logging.getLogger(__name__)
71
72 # =============================================================================
73 # BASE CLASSES
74 # =============================================================================
75
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 # beaker.session.secret if first is not set.
78 # and initialized at environment.py
79 ENCRYPTION_KEY = None
80
81 # used to sort permissions by types, '#' used here is not allowed to be in
82 # usernames, and it's very early in sorted string.printable table.
83 PERMISSION_TYPE_SORT = {
84 'admin': '####',
85 'write': '###',
86 'read': '##',
87 'none': '#',
88 }
89
90
91 def display_user_sort(obj):
92 """
93 Sort function used to sort permissions in .permissions() function of
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 of all other resources
96 """
97
98 if obj.username == User.DEFAULT_USER:
99 return '#####'
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 return prefix + obj.username
102
103
104 def display_user_group_sort(obj):
105 """
106 Sort function used to sort permissions in .permissions() function of
107 Repository, RepoGroup, UserGroup. Also it put the default user in front
108 of all other resources
109 """
110
111 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
112 return prefix + obj.users_group_name
113
114
115 def _hash_key(k):
116 return sha1_safe(k)
117
118
119 def in_filter_generator(qry, items, limit=500):
120 """
121 Splits IN() into multiple with OR
122 e.g.::
123 cnt = Repository.query().filter(
124 or_(
125 *in_filter_generator(Repository.repo_id, range(100000))
126 )).count()
127 """
128 if not items:
129 # empty list will cause empty query which might cause security issues
130 # this can lead to hidden unpleasant results
131 items = [-1]
132
133 parts = []
134 for chunk in xrange(0, len(items), limit):
135 parts.append(
136 qry.in_(items[chunk: chunk + limit])
137 )
138
139 return parts
140
141
142 base_table_args = {
143 'extend_existing': True,
144 'mysql_engine': 'InnoDB',
145 'mysql_charset': 'utf8',
146 'sqlite_autoincrement': True
147 }
148
149
150 class EncryptedTextValue(TypeDecorator):
151 """
152 Special column for encrypted long text data, use like::
153
154 value = Column("encrypted_value", EncryptedValue(), nullable=False)
155
156 This column is intelligent so if value is in unencrypted form it return
157 unencrypted form, but on save it always encrypts
158 """
159 impl = Text
160
161 def process_bind_param(self, value, dialect):
162 if not value:
163 return value
164 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
165 # protect against double encrypting if someone manually starts
166 # doing
167 raise ValueError('value needs to be in unencrypted format, ie. '
168 'not starting with enc$aes')
169 return 'enc$aes_hmac$%s' % AESCipher(
170 ENCRYPTION_KEY, hmac=True).encrypt(value)
171
172 def process_result_value(self, value, dialect):
173 import rhodecode
174
175 if not value:
176 return value
177
178 parts = value.split('$', 3)
179 if not len(parts) == 3:
180 # probably not encrypted values
181 return value
182 else:
183 if parts[0] != 'enc':
184 # parts ok but without our header ?
185 return value
186 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
187 'rhodecode.encrypted_values.strict') or True)
188 # at that stage we know it's our encryption
189 if parts[1] == 'aes':
190 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
191 elif parts[1] == 'aes_hmac':
192 decrypted_data = AESCipher(
193 ENCRYPTION_KEY, hmac=True,
194 strict_verification=enc_strict_mode).decrypt(parts[2])
195 else:
196 raise ValueError(
197 'Encryption type part is wrong, must be `aes` '
198 'or `aes_hmac`, got `%s` instead' % (parts[1]))
199 return decrypted_data
200
201
202 class BaseModel(object):
203 """
204 Base Model for all classes
205 """
206
207 @classmethod
208 def _get_keys(cls):
209 """return column names for this model """
210 return class_mapper(cls).c.keys()
211
212 def get_dict(self):
213 """
214 return dict with keys and values corresponding
215 to this model data """
216
217 d = {}
218 for k in self._get_keys():
219 d[k] = getattr(self, k)
220
221 # also use __json__() if present to get additional fields
222 _json_attr = getattr(self, '__json__', None)
223 if _json_attr:
224 # update with attributes from __json__
225 if callable(_json_attr):
226 _json_attr = _json_attr()
227 for k, val in _json_attr.iteritems():
228 d[k] = val
229 return d
230
231 def get_appstruct(self):
232 """return list with keys and values tuples corresponding
233 to this model data """
234
235 lst = []
236 for k in self._get_keys():
237 lst.append((k, getattr(self, k),))
238 return lst
239
240 def populate_obj(self, populate_dict):
241 """populate model with data from given populate_dict"""
242
243 for k in self._get_keys():
244 if k in populate_dict:
245 setattr(self, k, populate_dict[k])
246
247 @classmethod
248 def query(cls):
249 return Session().query(cls)
250
251 @classmethod
252 def get(cls, id_):
253 if id_:
254 return cls.query().get(id_)
255
256 @classmethod
257 def get_or_404(cls, id_):
258 from pyramid.httpexceptions import HTTPNotFound
259
260 try:
261 id_ = int(id_)
262 except (TypeError, ValueError):
263 raise HTTPNotFound()
264
265 res = cls.query().get(id_)
266 if not res:
267 raise HTTPNotFound()
268 return res
269
270 @classmethod
271 def getAll(cls):
272 # deprecated and left for backward compatibility
273 return cls.get_all()
274
275 @classmethod
276 def get_all(cls):
277 return cls.query().all()
278
279 @classmethod
280 def delete(cls, id_):
281 obj = cls.query().get(id_)
282 Session().delete(obj)
283
284 @classmethod
285 def identity_cache(cls, session, attr_name, value):
286 exist_in_session = []
287 for (item_cls, pkey), instance in session.identity_map.items():
288 if cls == item_cls and getattr(instance, attr_name) == value:
289 exist_in_session.append(instance)
290 if exist_in_session:
291 if len(exist_in_session) == 1:
292 return exist_in_session[0]
293 log.exception(
294 'multiple objects with attr %s and '
295 'value %s found with same name: %r',
296 attr_name, value, exist_in_session)
297
298 def __repr__(self):
299 if hasattr(self, '__unicode__'):
300 # python repr needs to return str
301 try:
302 return safe_str(self.__unicode__())
303 except UnicodeDecodeError:
304 pass
305 return '<DB:%s>' % (self.__class__.__name__)
306
307
308 class RhodeCodeSetting(Base, BaseModel):
309 __tablename__ = 'rhodecode_settings'
310 __table_args__ = (
311 UniqueConstraint('app_settings_name'),
312 base_table_args
313 )
314
315 SETTINGS_TYPES = {
316 'str': safe_str,
317 'int': safe_int,
318 'unicode': safe_unicode,
319 'bool': str2bool,
320 'list': functools.partial(aslist, sep=',')
321 }
322 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
323 GLOBAL_CONF_KEY = 'app_settings'
324
325 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
326 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
327 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
328 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
329
330 def __init__(self, key='', val='', type='unicode'):
331 self.app_settings_name = key
332 self.app_settings_type = type
333 self.app_settings_value = val
334
335 @validates('_app_settings_value')
336 def validate_settings_value(self, key, val):
337 assert type(val) == unicode
338 return val
339
340 @hybrid_property
341 def app_settings_value(self):
342 v = self._app_settings_value
343 _type = self.app_settings_type
344 if _type:
345 _type = self.app_settings_type.split('.')[0]
346 # decode the encrypted value
347 if 'encrypted' in self.app_settings_type:
348 cipher = EncryptedTextValue()
349 v = safe_unicode(cipher.process_result_value(v, None))
350
351 converter = self.SETTINGS_TYPES.get(_type) or \
352 self.SETTINGS_TYPES['unicode']
353 return converter(v)
354
355 @app_settings_value.setter
356 def app_settings_value(self, val):
357 """
358 Setter that will always make sure we use unicode in app_settings_value
359
360 :param val:
361 """
362 val = safe_unicode(val)
363 # encode the encrypted value
364 if 'encrypted' in self.app_settings_type:
365 cipher = EncryptedTextValue()
366 val = safe_unicode(cipher.process_bind_param(val, None))
367 self._app_settings_value = val
368
369 @hybrid_property
370 def app_settings_type(self):
371 return self._app_settings_type
372
373 @app_settings_type.setter
374 def app_settings_type(self, val):
375 if val.split('.')[0] not in self.SETTINGS_TYPES:
376 raise Exception('type must be one of %s got %s'
377 % (self.SETTINGS_TYPES.keys(), val))
378 self._app_settings_type = val
379
380 @classmethod
381 def get_by_prefix(cls, prefix):
382 return RhodeCodeSetting.query()\
383 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
384 .all()
385
386 def __unicode__(self):
387 return u"<%s('%s:%s[%s]')>" % (
388 self.__class__.__name__,
389 self.app_settings_name, self.app_settings_value,
390 self.app_settings_type
391 )
392
393
394 class RhodeCodeUi(Base, BaseModel):
395 __tablename__ = 'rhodecode_ui'
396 __table_args__ = (
397 UniqueConstraint('ui_key'),
398 base_table_args
399 )
400
401 HOOK_REPO_SIZE = 'changegroup.repo_size'
402 # HG
403 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
404 HOOK_PULL = 'outgoing.pull_logger'
405 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
406 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
407 HOOK_PUSH = 'changegroup.push_logger'
408 HOOK_PUSH_KEY = 'pushkey.key_push'
409
410 # TODO: johbo: Unify way how hooks are configured for git and hg,
411 # git part is currently hardcoded.
412
413 # SVN PATTERNS
414 SVN_BRANCH_ID = 'vcs_svn_branch'
415 SVN_TAG_ID = 'vcs_svn_tag'
416
417 ui_id = Column(
418 "ui_id", Integer(), nullable=False, unique=True, default=None,
419 primary_key=True)
420 ui_section = Column(
421 "ui_section", String(255), nullable=True, unique=None, default=None)
422 ui_key = Column(
423 "ui_key", String(255), nullable=True, unique=None, default=None)
424 ui_value = Column(
425 "ui_value", String(255), nullable=True, unique=None, default=None)
426 ui_active = Column(
427 "ui_active", Boolean(), nullable=True, unique=None, default=True)
428
429 def __repr__(self):
430 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
431 self.ui_key, self.ui_value)
432
433
434 class RepoRhodeCodeSetting(Base, BaseModel):
435 __tablename__ = 'repo_rhodecode_settings'
436 __table_args__ = (
437 UniqueConstraint(
438 'app_settings_name', 'repository_id',
439 name='uq_repo_rhodecode_setting_name_repo_id'),
440 base_table_args
441 )
442
443 repository_id = Column(
444 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
445 nullable=False)
446 app_settings_id = Column(
447 "app_settings_id", Integer(), nullable=False, unique=True,
448 default=None, primary_key=True)
449 app_settings_name = Column(
450 "app_settings_name", String(255), nullable=True, unique=None,
451 default=None)
452 _app_settings_value = Column(
453 "app_settings_value", String(4096), nullable=True, unique=None,
454 default=None)
455 _app_settings_type = Column(
456 "app_settings_type", String(255), nullable=True, unique=None,
457 default=None)
458
459 repository = relationship('Repository')
460
461 def __init__(self, repository_id, key='', val='', type='unicode'):
462 self.repository_id = repository_id
463 self.app_settings_name = key
464 self.app_settings_type = type
465 self.app_settings_value = val
466
467 @validates('_app_settings_value')
468 def validate_settings_value(self, key, val):
469 assert type(val) == unicode
470 return val
471
472 @hybrid_property
473 def app_settings_value(self):
474 v = self._app_settings_value
475 type_ = self.app_settings_type
476 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
477 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
478 return converter(v)
479
480 @app_settings_value.setter
481 def app_settings_value(self, val):
482 """
483 Setter that will always make sure we use unicode in app_settings_value
484
485 :param val:
486 """
487 self._app_settings_value = safe_unicode(val)
488
489 @hybrid_property
490 def app_settings_type(self):
491 return self._app_settings_type
492
493 @app_settings_type.setter
494 def app_settings_type(self, val):
495 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
496 if val not in SETTINGS_TYPES:
497 raise Exception('type must be one of %s got %s'
498 % (SETTINGS_TYPES.keys(), val))
499 self._app_settings_type = val
500
501 def __unicode__(self):
502 return u"<%s('%s:%s:%s[%s]')>" % (
503 self.__class__.__name__, self.repository.repo_name,
504 self.app_settings_name, self.app_settings_value,
505 self.app_settings_type
506 )
507
508
509 class RepoRhodeCodeUi(Base, BaseModel):
510 __tablename__ = 'repo_rhodecode_ui'
511 __table_args__ = (
512 UniqueConstraint(
513 'repository_id', 'ui_section', 'ui_key',
514 name='uq_repo_rhodecode_ui_repository_id_section_key'),
515 base_table_args
516 )
517
518 repository_id = Column(
519 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
520 nullable=False)
521 ui_id = Column(
522 "ui_id", Integer(), nullable=False, unique=True, default=None,
523 primary_key=True)
524 ui_section = Column(
525 "ui_section", String(255), nullable=True, unique=None, default=None)
526 ui_key = Column(
527 "ui_key", String(255), nullable=True, unique=None, default=None)
528 ui_value = Column(
529 "ui_value", String(255), nullable=True, unique=None, default=None)
530 ui_active = Column(
531 "ui_active", Boolean(), nullable=True, unique=None, default=True)
532
533 repository = relationship('Repository')
534
535 def __repr__(self):
536 return '<%s[%s:%s]%s=>%s]>' % (
537 self.__class__.__name__, self.repository.repo_name,
538 self.ui_section, self.ui_key, self.ui_value)
539
540
541 class User(Base, BaseModel):
542 __tablename__ = 'users'
543 __table_args__ = (
544 UniqueConstraint('username'), UniqueConstraint('email'),
545 Index('u_username_idx', 'username'),
546 Index('u_email_idx', 'email'),
547 base_table_args
548 )
549
550 DEFAULT_USER = 'default'
551 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
552 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
553
554 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
555 username = Column("username", String(255), nullable=True, unique=None, default=None)
556 password = Column("password", String(255), nullable=True, unique=None, default=None)
557 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
558 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
559 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
560 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
561 _email = Column("email", String(255), nullable=True, unique=None, default=None)
562 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
563 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
564
565 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
566 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
567 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
568 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
569 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
570 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
571
572 user_log = relationship('UserLog')
573 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
574
575 repositories = relationship('Repository')
576 repository_groups = relationship('RepoGroup')
577 user_groups = relationship('UserGroup')
578
579 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
580 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
581
582 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
583 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
584 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
585
586 group_member = relationship('UserGroupMember', cascade='all')
587
588 notifications = relationship('UserNotification', cascade='all')
589 # notifications assigned to this user
590 user_created_notifications = relationship('Notification', cascade='all')
591 # comments created by this user
592 user_comments = relationship('ChangesetComment', cascade='all')
593 # user profile extra info
594 user_emails = relationship('UserEmailMap', cascade='all')
595 user_ip_map = relationship('UserIpMap', cascade='all')
596 user_auth_tokens = relationship('UserApiKeys', cascade='all')
597 user_ssh_keys = relationship('UserSshKeys', cascade='all')
598
599 # gists
600 user_gists = relationship('Gist', cascade='all')
601 # user pull requests
602 user_pull_requests = relationship('PullRequest', cascade='all')
603 # external identities
604 extenal_identities = relationship(
605 'ExternalIdentity',
606 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
607 cascade='all')
608 # review rules
609 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
610
611 def __unicode__(self):
612 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
613 self.user_id, self.username)
614
615 @hybrid_property
616 def email(self):
617 return self._email
618
619 @email.setter
620 def email(self, val):
621 self._email = val.lower() if val else None
622
623 @hybrid_property
624 def first_name(self):
625 from rhodecode.lib import helpers as h
626 if self.name:
627 return h.escape(self.name)
628 return self.name
629
630 @hybrid_property
631 def last_name(self):
632 from rhodecode.lib import helpers as h
633 if self.lastname:
634 return h.escape(self.lastname)
635 return self.lastname
636
637 @hybrid_property
638 def api_key(self):
639 """
640 Fetch if exist an auth-token with role ALL connected to this user
641 """
642 user_auth_token = UserApiKeys.query()\
643 .filter(UserApiKeys.user_id == self.user_id)\
644 .filter(or_(UserApiKeys.expires == -1,
645 UserApiKeys.expires >= time.time()))\
646 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
647 if user_auth_token:
648 user_auth_token = user_auth_token.api_key
649
650 return user_auth_token
651
652 @api_key.setter
653 def api_key(self, val):
654 # don't allow to set API key this is deprecated for now
655 self._api_key = None
656
657 @property
658 def reviewer_pull_requests(self):
659 return PullRequestReviewers.query() \
660 .options(joinedload(PullRequestReviewers.pull_request)) \
661 .filter(PullRequestReviewers.user_id == self.user_id) \
662 .all()
663
664 @property
665 def firstname(self):
666 # alias for future
667 return self.name
668
669 @property
670 def emails(self):
671 other = UserEmailMap.query()\
672 .filter(UserEmailMap.user == self) \
673 .order_by(UserEmailMap.email_id.asc()) \
674 .all()
675 return [self.email] + [x.email for x in other]
676
677 @property
678 def auth_tokens(self):
679 auth_tokens = self.get_auth_tokens()
680 return [x.api_key for x in auth_tokens]
681
682 def get_auth_tokens(self):
683 return UserApiKeys.query()\
684 .filter(UserApiKeys.user == self)\
685 .order_by(UserApiKeys.user_api_key_id.asc())\
686 .all()
687
688 @LazyProperty
689 def feed_token(self):
690 return self.get_feed_token()
691
692 def get_feed_token(self, cache=True):
693 feed_tokens = UserApiKeys.query()\
694 .filter(UserApiKeys.user == self)\
695 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
696 if cache:
697 feed_tokens = feed_tokens.options(
698 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
699
700 feed_tokens = feed_tokens.all()
701 if feed_tokens:
702 return feed_tokens[0].api_key
703 return 'NO_FEED_TOKEN_AVAILABLE'
704
705 @classmethod
706 def get(cls, user_id, cache=False):
707 if not user_id:
708 return
709
710 user = cls.query()
711 if cache:
712 user = user.options(
713 FromCache("sql_cache_short", "get_users_%s" % user_id))
714 return user.get(user_id)
715
716 @classmethod
717 def extra_valid_auth_tokens(cls, user, role=None):
718 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
719 .filter(or_(UserApiKeys.expires == -1,
720 UserApiKeys.expires >= time.time()))
721 if role:
722 tokens = tokens.filter(or_(UserApiKeys.role == role,
723 UserApiKeys.role == UserApiKeys.ROLE_ALL))
724 return tokens.all()
725
726 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
727 from rhodecode.lib import auth
728
729 log.debug('Trying to authenticate user: %s via auth-token, '
730 'and roles: %s', self, roles)
731
732 if not auth_token:
733 return False
734
735 crypto_backend = auth.crypto_backend()
736
737 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
738 tokens_q = UserApiKeys.query()\
739 .filter(UserApiKeys.user_id == self.user_id)\
740 .filter(or_(UserApiKeys.expires == -1,
741 UserApiKeys.expires >= time.time()))
742
743 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
744
745 plain_tokens = []
746 hash_tokens = []
747
748 user_tokens = tokens_q.all()
749 log.debug('Found %s user tokens to check for authentication', len(user_tokens))
750 for token in user_tokens:
751 log.debug('AUTH_TOKEN: checking if user token with id `%s` matches',
752 token.user_api_key_id)
753 # verify scope first, since it's way faster than hash calculation of
754 # encrypted tokens
755 if token.repo_id:
756 # token has a scope, we need to verify it
757 if scope_repo_id != token.repo_id:
758 log.debug(
759 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
760 'and calling scope is:%s, skipping further checks',
761 token.repo, scope_repo_id)
762 # token has a scope, and it doesn't match, skip token
763 continue
764
765 if token.api_key.startswith(crypto_backend.ENC_PREF):
766 hash_tokens.append(token.api_key)
767 else:
768 plain_tokens.append(token.api_key)
769
770 is_plain_match = auth_token in plain_tokens
771 if is_plain_match:
772 return True
773
774 for hashed in hash_tokens:
775 # NOTE(marcink): this is expensive to calculate, but most secure
776 match = crypto_backend.hash_check(auth_token, hashed)
777 if match:
778 return True
779
780 return False
781
782 @property
783 def ip_addresses(self):
784 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
785 return [x.ip_addr for x in ret]
786
787 @property
788 def username_and_name(self):
789 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
790
791 @property
792 def username_or_name_or_email(self):
793 full_name = self.full_name if self.full_name is not ' ' else None
794 return self.username or full_name or self.email
795
796 @property
797 def full_name(self):
798 return '%s %s' % (self.first_name, self.last_name)
799
800 @property
801 def full_name_or_username(self):
802 return ('%s %s' % (self.first_name, self.last_name)
803 if (self.first_name and self.last_name) else self.username)
804
805 @property
806 def full_contact(self):
807 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
808
809 @property
810 def short_contact(self):
811 return '%s %s' % (self.first_name, self.last_name)
812
813 @property
814 def is_admin(self):
815 return self.admin
816
817 def AuthUser(self, **kwargs):
818 """
819 Returns instance of AuthUser for this user
820 """
821 from rhodecode.lib.auth import AuthUser
822 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
823
824 @hybrid_property
825 def user_data(self):
826 if not self._user_data:
827 return {}
828
829 try:
830 return json.loads(self._user_data)
831 except TypeError:
832 return {}
833
834 @user_data.setter
835 def user_data(self, val):
836 if not isinstance(val, dict):
837 raise Exception('user_data must be dict, got %s' % type(val))
838 try:
839 self._user_data = json.dumps(val)
840 except Exception:
841 log.error(traceback.format_exc())
842
843 @classmethod
844 def get_by_username(cls, username, case_insensitive=False,
845 cache=False, identity_cache=False):
846 session = Session()
847
848 if case_insensitive:
849 q = cls.query().filter(
850 func.lower(cls.username) == func.lower(username))
851 else:
852 q = cls.query().filter(cls.username == username)
853
854 if cache:
855 if identity_cache:
856 val = cls.identity_cache(session, 'username', username)
857 if val:
858 return val
859 else:
860 cache_key = "get_user_by_name_%s" % _hash_key(username)
861 q = q.options(
862 FromCache("sql_cache_short", cache_key))
863
864 return q.scalar()
865
866 @classmethod
867 def get_by_auth_token(cls, auth_token, cache=False):
868 q = UserApiKeys.query()\
869 .filter(UserApiKeys.api_key == auth_token)\
870 .filter(or_(UserApiKeys.expires == -1,
871 UserApiKeys.expires >= time.time()))
872 if cache:
873 q = q.options(
874 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
875
876 match = q.first()
877 if match:
878 return match.user
879
880 @classmethod
881 def get_by_email(cls, email, case_insensitive=False, cache=False):
882
883 if case_insensitive:
884 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
885
886 else:
887 q = cls.query().filter(cls.email == email)
888
889 email_key = _hash_key(email)
890 if cache:
891 q = q.options(
892 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
893
894 ret = q.scalar()
895 if ret is None:
896 q = UserEmailMap.query()
897 # try fetching in alternate email map
898 if case_insensitive:
899 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
900 else:
901 q = q.filter(UserEmailMap.email == email)
902 q = q.options(joinedload(UserEmailMap.user))
903 if cache:
904 q = q.options(
905 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
906 ret = getattr(q.scalar(), 'user', None)
907
908 return ret
909
910 @classmethod
911 def get_from_cs_author(cls, author):
912 """
913 Tries to get User objects out of commit author string
914
915 :param author:
916 """
917 from rhodecode.lib.helpers import email, author_name
918 # Valid email in the attribute passed, see if they're in the system
919 _email = email(author)
920 if _email:
921 user = cls.get_by_email(_email, case_insensitive=True)
922 if user:
923 return user
924 # Maybe we can match by username?
925 _author = author_name(author)
926 user = cls.get_by_username(_author, case_insensitive=True)
927 if user:
928 return user
929
930 def update_userdata(self, **kwargs):
931 usr = self
932 old = usr.user_data
933 old.update(**kwargs)
934 usr.user_data = old
935 Session().add(usr)
936 log.debug('updated userdata with ', kwargs)
937
938 def update_lastlogin(self):
939 """Update user lastlogin"""
940 self.last_login = datetime.datetime.now()
941 Session().add(self)
942 log.debug('updated user %s lastlogin', self.username)
943
944 def update_password(self, new_password):
945 from rhodecode.lib.auth import get_crypt_password
946
947 self.password = get_crypt_password(new_password)
948 Session().add(self)
949
950 @classmethod
951 def get_first_super_admin(cls):
952 user = User.query()\
953 .filter(User.admin == true()) \
954 .order_by(User.user_id.asc()) \
955 .first()
956
957 if user is None:
958 raise Exception('FATAL: Missing administrative account!')
959 return user
960
961 @classmethod
962 def get_all_super_admins(cls, only_active=False):
963 """
964 Returns all admin accounts sorted by username
965 """
966 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
967 if only_active:
968 qry = qry.filter(User.active == true())
969 return qry.all()
970
971 @classmethod
972 def get_default_user(cls, cache=False, refresh=False):
973 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
974 if user is None:
975 raise Exception('FATAL: Missing default account!')
976 if refresh:
977 # The default user might be based on outdated state which
978 # has been loaded from the cache.
979 # A call to refresh() ensures that the
980 # latest state from the database is used.
981 Session().refresh(user)
982 return user
983
984 def _get_default_perms(self, user, suffix=''):
985 from rhodecode.model.permission import PermissionModel
986 return PermissionModel().get_default_perms(user.user_perms, suffix)
987
988 def get_default_perms(self, suffix=''):
989 return self._get_default_perms(self, suffix)
990
991 def get_api_data(self, include_secrets=False, details='full'):
992 """
993 Common function for generating user related data for API
994
995 :param include_secrets: By default secrets in the API data will be replaced
996 by a placeholder value to prevent exposing this data by accident. In case
997 this data shall be exposed, set this flag to ``True``.
998
999 :param details: details can be 'basic|full' basic gives only a subset of
1000 the available user information that includes user_id, name and emails.
1001 """
1002 user = self
1003 user_data = self.user_data
1004 data = {
1005 'user_id': user.user_id,
1006 'username': user.username,
1007 'firstname': user.name,
1008 'lastname': user.lastname,
1009 'email': user.email,
1010 'emails': user.emails,
1011 }
1012 if details == 'basic':
1013 return data
1014
1015 auth_token_length = 40
1016 auth_token_replacement = '*' * auth_token_length
1017
1018 extras = {
1019 'auth_tokens': [auth_token_replacement],
1020 'active': user.active,
1021 'admin': user.admin,
1022 'extern_type': user.extern_type,
1023 'extern_name': user.extern_name,
1024 'last_login': user.last_login,
1025 'last_activity': user.last_activity,
1026 'ip_addresses': user.ip_addresses,
1027 'language': user_data.get('language')
1028 }
1029 data.update(extras)
1030
1031 if include_secrets:
1032 data['auth_tokens'] = user.auth_tokens
1033 return data
1034
1035 def __json__(self):
1036 data = {
1037 'full_name': self.full_name,
1038 'full_name_or_username': self.full_name_or_username,
1039 'short_contact': self.short_contact,
1040 'full_contact': self.full_contact,
1041 }
1042 data.update(self.get_api_data())
1043 return data
1044
1045
1046 class UserApiKeys(Base, BaseModel):
1047 __tablename__ = 'user_api_keys'
1048 __table_args__ = (
1049 Index('uak_api_key_idx', 'api_key', unique=True),
1050 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1051 base_table_args
1052 )
1053 __mapper_args__ = {}
1054
1055 # ApiKey role
1056 ROLE_ALL = 'token_role_all'
1057 ROLE_HTTP = 'token_role_http'
1058 ROLE_VCS = 'token_role_vcs'
1059 ROLE_API = 'token_role_api'
1060 ROLE_FEED = 'token_role_feed'
1061 ROLE_PASSWORD_RESET = 'token_password_reset'
1062
1063 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1064
1065 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1066 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1067 api_key = Column("api_key", String(255), nullable=False, unique=True)
1068 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1069 expires = Column('expires', Float(53), nullable=False)
1070 role = Column('role', String(255), nullable=True)
1071 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1072
1073 # scope columns
1074 repo_id = Column(
1075 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1076 nullable=True, unique=None, default=None)
1077 repo = relationship('Repository', lazy='joined')
1078
1079 repo_group_id = Column(
1080 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1081 nullable=True, unique=None, default=None)
1082 repo_group = relationship('RepoGroup', lazy='joined')
1083
1084 user = relationship('User', lazy='joined')
1085
1086 def __unicode__(self):
1087 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1088
1089 def __json__(self):
1090 data = {
1091 'auth_token': self.api_key,
1092 'role': self.role,
1093 'scope': self.scope_humanized,
1094 'expired': self.expired
1095 }
1096 return data
1097
1098 def get_api_data(self, include_secrets=False):
1099 data = self.__json__()
1100 if include_secrets:
1101 return data
1102 else:
1103 data['auth_token'] = self.token_obfuscated
1104 return data
1105
1106 @hybrid_property
1107 def description_safe(self):
1108 from rhodecode.lib import helpers as h
1109 return h.escape(self.description)
1110
1111 @property
1112 def expired(self):
1113 if self.expires == -1:
1114 return False
1115 return time.time() > self.expires
1116
1117 @classmethod
1118 def _get_role_name(cls, role):
1119 return {
1120 cls.ROLE_ALL: _('all'),
1121 cls.ROLE_HTTP: _('http/web interface'),
1122 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1123 cls.ROLE_API: _('api calls'),
1124 cls.ROLE_FEED: _('feed access'),
1125 }.get(role, role)
1126
1127 @property
1128 def role_humanized(self):
1129 return self._get_role_name(self.role)
1130
1131 def _get_scope(self):
1132 if self.repo:
1133 return 'Repository: {}'.format(self.repo.repo_name)
1134 if self.repo_group:
1135 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1136 return 'Global'
1137
1138 @property
1139 def scope_humanized(self):
1140 return self._get_scope()
1141
1142 @property
1143 def token_obfuscated(self):
1144 if self.api_key:
1145 return self.api_key[:4] + "****"
1146
1147
1148 class UserEmailMap(Base, BaseModel):
1149 __tablename__ = 'user_email_map'
1150 __table_args__ = (
1151 Index('uem_email_idx', 'email'),
1152 UniqueConstraint('email'),
1153 base_table_args
1154 )
1155 __mapper_args__ = {}
1156
1157 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1158 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1159 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1160 user = relationship('User', lazy='joined')
1161
1162 @validates('_email')
1163 def validate_email(self, key, email):
1164 # check if this email is not main one
1165 main_email = Session().query(User).filter(User.email == email).scalar()
1166 if main_email is not None:
1167 raise AttributeError('email %s is present is user table' % email)
1168 return email
1169
1170 @hybrid_property
1171 def email(self):
1172 return self._email
1173
1174 @email.setter
1175 def email(self, val):
1176 self._email = val.lower() if val else None
1177
1178
1179 class UserIpMap(Base, BaseModel):
1180 __tablename__ = 'user_ip_map'
1181 __table_args__ = (
1182 UniqueConstraint('user_id', 'ip_addr'),
1183 base_table_args
1184 )
1185 __mapper_args__ = {}
1186
1187 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1189 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1190 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1191 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1192 user = relationship('User', lazy='joined')
1193
1194 @hybrid_property
1195 def description_safe(self):
1196 from rhodecode.lib import helpers as h
1197 return h.escape(self.description)
1198
1199 @classmethod
1200 def _get_ip_range(cls, ip_addr):
1201 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1202 return [str(net.network_address), str(net.broadcast_address)]
1203
1204 def __json__(self):
1205 return {
1206 'ip_addr': self.ip_addr,
1207 'ip_range': self._get_ip_range(self.ip_addr),
1208 }
1209
1210 def __unicode__(self):
1211 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1212 self.user_id, self.ip_addr)
1213
1214
1215 class UserSshKeys(Base, BaseModel):
1216 __tablename__ = 'user_ssh_keys'
1217 __table_args__ = (
1218 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1219
1220 UniqueConstraint('ssh_key_fingerprint'),
1221
1222 base_table_args
1223 )
1224 __mapper_args__ = {}
1225
1226 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1227 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1228 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1229
1230 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1231
1232 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1233 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1234 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1235
1236 user = relationship('User', lazy='joined')
1237
1238 def __json__(self):
1239 data = {
1240 'ssh_fingerprint': self.ssh_key_fingerprint,
1241 'description': self.description,
1242 'created_on': self.created_on
1243 }
1244 return data
1245
1246 def get_api_data(self):
1247 data = self.__json__()
1248 return data
1249
1250
1251 class UserLog(Base, BaseModel):
1252 __tablename__ = 'user_logs'
1253 __table_args__ = (
1254 base_table_args,
1255 )
1256
1257 VERSION_1 = 'v1'
1258 VERSION_2 = 'v2'
1259 VERSIONS = [VERSION_1, VERSION_2]
1260
1261 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1262 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1263 username = Column("username", String(255), nullable=True, unique=None, default=None)
1264 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1265 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1266 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1267 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1268 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1269
1270 version = Column("version", String(255), nullable=True, default=VERSION_1)
1271 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1272 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1273
1274 def __unicode__(self):
1275 return u"<%s('id:%s:%s')>" % (
1276 self.__class__.__name__, self.repository_name, self.action)
1277
1278 def __json__(self):
1279 return {
1280 'user_id': self.user_id,
1281 'username': self.username,
1282 'repository_id': self.repository_id,
1283 'repository_name': self.repository_name,
1284 'user_ip': self.user_ip,
1285 'action_date': self.action_date,
1286 'action': self.action,
1287 }
1288
1289 @hybrid_property
1290 def entry_id(self):
1291 return self.user_log_id
1292
1293 @property
1294 def action_as_day(self):
1295 return datetime.date(*self.action_date.timetuple()[:3])
1296
1297 user = relationship('User')
1298 repository = relationship('Repository', cascade='')
1299
1300
1301 class UserGroup(Base, BaseModel):
1302 __tablename__ = 'users_groups'
1303 __table_args__ = (
1304 base_table_args,
1305 )
1306
1307 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1308 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1309 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1310 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1311 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1312 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1313 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1314 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1315
1316 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1317 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1318 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1319 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1320 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1321 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1322
1323 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1324 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1325
1326 @classmethod
1327 def _load_group_data(cls, column):
1328 if not column:
1329 return {}
1330
1331 try:
1332 return json.loads(column) or {}
1333 except TypeError:
1334 return {}
1335
1336 @hybrid_property
1337 def description_safe(self):
1338 from rhodecode.lib import helpers as h
1339 return h.escape(self.user_group_description)
1340
1341 @hybrid_property
1342 def group_data(self):
1343 return self._load_group_data(self._group_data)
1344
1345 @group_data.expression
1346 def group_data(self, **kwargs):
1347 return self._group_data
1348
1349 @group_data.setter
1350 def group_data(self, val):
1351 try:
1352 self._group_data = json.dumps(val)
1353 except Exception:
1354 log.error(traceback.format_exc())
1355
1356 @classmethod
1357 def _load_sync(cls, group_data):
1358 if group_data:
1359 return group_data.get('extern_type')
1360
1361 @property
1362 def sync(self):
1363 return self._load_sync(self.group_data)
1364
1365 def __unicode__(self):
1366 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1367 self.users_group_id,
1368 self.users_group_name)
1369
1370 @classmethod
1371 def get_by_group_name(cls, group_name, cache=False,
1372 case_insensitive=False):
1373 if case_insensitive:
1374 q = cls.query().filter(func.lower(cls.users_group_name) ==
1375 func.lower(group_name))
1376
1377 else:
1378 q = cls.query().filter(cls.users_group_name == group_name)
1379 if cache:
1380 q = q.options(
1381 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1382 return q.scalar()
1383
1384 @classmethod
1385 def get(cls, user_group_id, cache=False):
1386 if not user_group_id:
1387 return
1388
1389 user_group = cls.query()
1390 if cache:
1391 user_group = user_group.options(
1392 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1393 return user_group.get(user_group_id)
1394
1395 def permissions(self, with_admins=True, with_owner=True,
1396 expand_from_user_groups=False):
1397 """
1398 Permissions for user groups
1399 """
1400 _admin_perm = 'usergroup.admin'
1401
1402 owner_row = []
1403 if with_owner:
1404 usr = AttributeDict(self.user.get_dict())
1405 usr.owner_row = True
1406 usr.permission = _admin_perm
1407 owner_row.append(usr)
1408
1409 super_admin_ids = []
1410 super_admin_rows = []
1411 if with_admins:
1412 for usr in User.get_all_super_admins():
1413 super_admin_ids.append(usr.user_id)
1414 # if this admin is also owner, don't double the record
1415 if usr.user_id == owner_row[0].user_id:
1416 owner_row[0].admin_row = True
1417 else:
1418 usr = AttributeDict(usr.get_dict())
1419 usr.admin_row = True
1420 usr.permission = _admin_perm
1421 super_admin_rows.append(usr)
1422
1423 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1424 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1425 joinedload(UserUserGroupToPerm.user),
1426 joinedload(UserUserGroupToPerm.permission),)
1427
1428 # get owners and admins and permissions. We do a trick of re-writing
1429 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1430 # has a global reference and changing one object propagates to all
1431 # others. This means if admin is also an owner admin_row that change
1432 # would propagate to both objects
1433 perm_rows = []
1434 for _usr in q.all():
1435 usr = AttributeDict(_usr.user.get_dict())
1436 # if this user is also owner/admin, mark as duplicate record
1437 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1438 usr.duplicate_perm = True
1439 usr.permission = _usr.permission.permission_name
1440 perm_rows.append(usr)
1441
1442 # filter the perm rows by 'default' first and then sort them by
1443 # admin,write,read,none permissions sorted again alphabetically in
1444 # each group
1445 perm_rows = sorted(perm_rows, key=display_user_sort)
1446
1447 user_groups_rows = []
1448 if expand_from_user_groups:
1449 for ug in self.permission_user_groups(with_members=True):
1450 for user_data in ug.members:
1451 user_groups_rows.append(user_data)
1452
1453 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1454
1455 def permission_user_groups(self, with_members=False):
1456 q = UserGroupUserGroupToPerm.query()\
1457 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1458 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1459 joinedload(UserGroupUserGroupToPerm.target_user_group),
1460 joinedload(UserGroupUserGroupToPerm.permission),)
1461
1462 perm_rows = []
1463 for _user_group in q.all():
1464 entry = AttributeDict(_user_group.user_group.get_dict())
1465 entry.permission = _user_group.permission.permission_name
1466 if with_members:
1467 entry.members = [x.user.get_dict()
1468 for x in _user_group.users_group.members]
1469 perm_rows.append(entry)
1470
1471 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1472 return perm_rows
1473
1474 def _get_default_perms(self, user_group, suffix=''):
1475 from rhodecode.model.permission import PermissionModel
1476 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1477
1478 def get_default_perms(self, suffix=''):
1479 return self._get_default_perms(self, suffix)
1480
1481 def get_api_data(self, with_group_members=True, include_secrets=False):
1482 """
1483 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1484 basically forwarded.
1485
1486 """
1487 user_group = self
1488 data = {
1489 'users_group_id': user_group.users_group_id,
1490 'group_name': user_group.users_group_name,
1491 'group_description': user_group.user_group_description,
1492 'active': user_group.users_group_active,
1493 'owner': user_group.user.username,
1494 'sync': user_group.sync,
1495 'owner_email': user_group.user.email,
1496 }
1497
1498 if with_group_members:
1499 users = []
1500 for user in user_group.members:
1501 user = user.user
1502 users.append(user.get_api_data(include_secrets=include_secrets))
1503 data['users'] = users
1504
1505 return data
1506
1507
1508 class UserGroupMember(Base, BaseModel):
1509 __tablename__ = 'users_groups_members'
1510 __table_args__ = (
1511 base_table_args,
1512 )
1513
1514 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1515 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1516 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1517
1518 user = relationship('User', lazy='joined')
1519 users_group = relationship('UserGroup')
1520
1521 def __init__(self, gr_id='', u_id=''):
1522 self.users_group_id = gr_id
1523 self.user_id = u_id
1524
1525
1526 class RepositoryField(Base, BaseModel):
1527 __tablename__ = 'repositories_fields'
1528 __table_args__ = (
1529 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1530 base_table_args,
1531 )
1532
1533 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1534
1535 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1536 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1537 field_key = Column("field_key", String(250))
1538 field_label = Column("field_label", String(1024), nullable=False)
1539 field_value = Column("field_value", String(10000), nullable=False)
1540 field_desc = Column("field_desc", String(1024), nullable=False)
1541 field_type = Column("field_type", String(255), nullable=False, unique=None)
1542 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1543
1544 repository = relationship('Repository')
1545
1546 @property
1547 def field_key_prefixed(self):
1548 return 'ex_%s' % self.field_key
1549
1550 @classmethod
1551 def un_prefix_key(cls, key):
1552 if key.startswith(cls.PREFIX):
1553 return key[len(cls.PREFIX):]
1554 return key
1555
1556 @classmethod
1557 def get_by_key_name(cls, key, repo):
1558 row = cls.query()\
1559 .filter(cls.repository == repo)\
1560 .filter(cls.field_key == key).scalar()
1561 return row
1562
1563
1564 class Repository(Base, BaseModel):
1565 __tablename__ = 'repositories'
1566 __table_args__ = (
1567 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1568 base_table_args,
1569 )
1570 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1571 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1572 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1573
1574 STATE_CREATED = 'repo_state_created'
1575 STATE_PENDING = 'repo_state_pending'
1576 STATE_ERROR = 'repo_state_error'
1577
1578 LOCK_AUTOMATIC = 'lock_auto'
1579 LOCK_API = 'lock_api'
1580 LOCK_WEB = 'lock_web'
1581 LOCK_PULL = 'lock_pull'
1582
1583 NAME_SEP = URL_SEP
1584
1585 repo_id = Column(
1586 "repo_id", Integer(), nullable=False, unique=True, default=None,
1587 primary_key=True)
1588 _repo_name = Column(
1589 "repo_name", Text(), nullable=False, default=None)
1590 _repo_name_hash = Column(
1591 "repo_name_hash", String(255), nullable=False, unique=True)
1592 repo_state = Column("repo_state", String(255), nullable=True)
1593
1594 clone_uri = Column(
1595 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1596 default=None)
1597 push_uri = Column(
1598 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1599 default=None)
1600 repo_type = Column(
1601 "repo_type", String(255), nullable=False, unique=False, default=None)
1602 user_id = Column(
1603 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1604 unique=False, default=None)
1605 private = Column(
1606 "private", Boolean(), nullable=True, unique=None, default=None)
1607 archived = Column(
1608 "archived", Boolean(), nullable=True, unique=None, default=None)
1609 enable_statistics = Column(
1610 "statistics", Boolean(), nullable=True, unique=None, default=True)
1611 enable_downloads = Column(
1612 "downloads", Boolean(), nullable=True, unique=None, default=True)
1613 description = Column(
1614 "description", String(10000), nullable=True, unique=None, default=None)
1615 created_on = Column(
1616 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1617 default=datetime.datetime.now)
1618 updated_on = Column(
1619 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1620 default=datetime.datetime.now)
1621 _landing_revision = Column(
1622 "landing_revision", String(255), nullable=False, unique=False,
1623 default=None)
1624 enable_locking = Column(
1625 "enable_locking", Boolean(), nullable=False, unique=None,
1626 default=False)
1627 _locked = Column(
1628 "locked", String(255), nullable=True, unique=False, default=None)
1629 _changeset_cache = Column(
1630 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1631
1632 fork_id = Column(
1633 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1634 nullable=True, unique=False, default=None)
1635 group_id = Column(
1636 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1637 unique=False, default=None)
1638
1639 user = relationship('User', lazy='joined')
1640 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1641 group = relationship('RepoGroup', lazy='joined')
1642 repo_to_perm = relationship(
1643 'UserRepoToPerm', cascade='all',
1644 order_by='UserRepoToPerm.repo_to_perm_id')
1645 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1646 stats = relationship('Statistics', cascade='all', uselist=False)
1647
1648 followers = relationship(
1649 'UserFollowing',
1650 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1651 cascade='all')
1652 extra_fields = relationship(
1653 'RepositoryField', cascade="all, delete, delete-orphan")
1654 logs = relationship('UserLog')
1655 comments = relationship(
1656 'ChangesetComment', cascade="all, delete, delete-orphan")
1657 pull_requests_source = relationship(
1658 'PullRequest',
1659 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1660 cascade="all, delete, delete-orphan")
1661 pull_requests_target = relationship(
1662 'PullRequest',
1663 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1664 cascade="all, delete, delete-orphan")
1665 ui = relationship('RepoRhodeCodeUi', cascade="all")
1666 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1667 integrations = relationship('Integration',
1668 cascade="all, delete, delete-orphan")
1669
1670 scoped_tokens = relationship('UserApiKeys', cascade="all")
1671
1672 def __unicode__(self):
1673 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1674 safe_unicode(self.repo_name))
1675
1676 @hybrid_property
1677 def description_safe(self):
1678 from rhodecode.lib import helpers as h
1679 return h.escape(self.description)
1680
1681 @hybrid_property
1682 def landing_rev(self):
1683 # always should return [rev_type, rev]
1684 if self._landing_revision:
1685 _rev_info = self._landing_revision.split(':')
1686 if len(_rev_info) < 2:
1687 _rev_info.insert(0, 'rev')
1688 return [_rev_info[0], _rev_info[1]]
1689 return [None, None]
1690
1691 @landing_rev.setter
1692 def landing_rev(self, val):
1693 if ':' not in val:
1694 raise ValueError('value must be delimited with `:` and consist '
1695 'of <rev_type>:<rev>, got %s instead' % val)
1696 self._landing_revision = val
1697
1698 @hybrid_property
1699 def locked(self):
1700 if self._locked:
1701 user_id, timelocked, reason = self._locked.split(':')
1702 lock_values = int(user_id), timelocked, reason
1703 else:
1704 lock_values = [None, None, None]
1705 return lock_values
1706
1707 @locked.setter
1708 def locked(self, val):
1709 if val and isinstance(val, (list, tuple)):
1710 self._locked = ':'.join(map(str, val))
1711 else:
1712 self._locked = None
1713
1714 @hybrid_property
1715 def changeset_cache(self):
1716 from rhodecode.lib.vcs.backends.base import EmptyCommit
1717 dummy = EmptyCommit().__json__()
1718 if not self._changeset_cache:
1719 return dummy
1720 try:
1721 return json.loads(self._changeset_cache)
1722 except TypeError:
1723 return dummy
1724 except Exception:
1725 log.error(traceback.format_exc())
1726 return dummy
1727
1728 @changeset_cache.setter
1729 def changeset_cache(self, val):
1730 try:
1731 self._changeset_cache = json.dumps(val)
1732 except Exception:
1733 log.error(traceback.format_exc())
1734
1735 @hybrid_property
1736 def repo_name(self):
1737 return self._repo_name
1738
1739 @repo_name.setter
1740 def repo_name(self, value):
1741 self._repo_name = value
1742 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1743
1744 @classmethod
1745 def normalize_repo_name(cls, repo_name):
1746 """
1747 Normalizes os specific repo_name to the format internally stored inside
1748 database using URL_SEP
1749
1750 :param cls:
1751 :param repo_name:
1752 """
1753 return cls.NAME_SEP.join(repo_name.split(os.sep))
1754
1755 @classmethod
1756 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1757 session = Session()
1758 q = session.query(cls).filter(cls.repo_name == repo_name)
1759
1760 if cache:
1761 if identity_cache:
1762 val = cls.identity_cache(session, 'repo_name', repo_name)
1763 if val:
1764 return val
1765 else:
1766 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1767 q = q.options(
1768 FromCache("sql_cache_short", cache_key))
1769
1770 return q.scalar()
1771
1772 @classmethod
1773 def get_by_id_or_repo_name(cls, repoid):
1774 if isinstance(repoid, (int, long)):
1775 try:
1776 repo = cls.get(repoid)
1777 except ValueError:
1778 repo = None
1779 else:
1780 repo = cls.get_by_repo_name(repoid)
1781 return repo
1782
1783 @classmethod
1784 def get_by_full_path(cls, repo_full_path):
1785 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1786 repo_name = cls.normalize_repo_name(repo_name)
1787 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1788
1789 @classmethod
1790 def get_repo_forks(cls, repo_id):
1791 return cls.query().filter(Repository.fork_id == repo_id)
1792
1793 @classmethod
1794 def base_path(cls):
1795 """
1796 Returns base path when all repos are stored
1797
1798 :param cls:
1799 """
1800 q = Session().query(RhodeCodeUi)\
1801 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1802 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1803 return q.one().ui_value
1804
1805 @classmethod
1806 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1807 case_insensitive=True, archived=False):
1808 q = Repository.query()
1809
1810 if not archived:
1811 q = q.filter(Repository.archived.isnot(true()))
1812
1813 if not isinstance(user_id, Optional):
1814 q = q.filter(Repository.user_id == user_id)
1815
1816 if not isinstance(group_id, Optional):
1817 q = q.filter(Repository.group_id == group_id)
1818
1819 if case_insensitive:
1820 q = q.order_by(func.lower(Repository.repo_name))
1821 else:
1822 q = q.order_by(Repository.repo_name)
1823
1824 return q.all()
1825
1826 @property
1827 def forks(self):
1828 """
1829 Return forks of this repo
1830 """
1831 return Repository.get_repo_forks(self.repo_id)
1832
1833 @property
1834 def parent(self):
1835 """
1836 Returns fork parent
1837 """
1838 return self.fork
1839
1840 @property
1841 def just_name(self):
1842 return self.repo_name.split(self.NAME_SEP)[-1]
1843
1844 @property
1845 def groups_with_parents(self):
1846 groups = []
1847 if self.group is None:
1848 return groups
1849
1850 cur_gr = self.group
1851 groups.insert(0, cur_gr)
1852 while 1:
1853 gr = getattr(cur_gr, 'parent_group', None)
1854 cur_gr = cur_gr.parent_group
1855 if gr is None:
1856 break
1857 groups.insert(0, gr)
1858
1859 return groups
1860
1861 @property
1862 def groups_and_repo(self):
1863 return self.groups_with_parents, self
1864
1865 @LazyProperty
1866 def repo_path(self):
1867 """
1868 Returns base full path for that repository means where it actually
1869 exists on a filesystem
1870 """
1871 q = Session().query(RhodeCodeUi).filter(
1872 RhodeCodeUi.ui_key == self.NAME_SEP)
1873 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1874 return q.one().ui_value
1875
1876 @property
1877 def repo_full_path(self):
1878 p = [self.repo_path]
1879 # we need to split the name by / since this is how we store the
1880 # names in the database, but that eventually needs to be converted
1881 # into a valid system path
1882 p += self.repo_name.split(self.NAME_SEP)
1883 return os.path.join(*map(safe_unicode, p))
1884
1885 @property
1886 def cache_keys(self):
1887 """
1888 Returns associated cache keys for that repo
1889 """
1890 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1891 repo_id=self.repo_id)
1892 return CacheKey.query()\
1893 .filter(CacheKey.cache_args == invalidation_namespace)\
1894 .order_by(CacheKey.cache_key)\
1895 .all()
1896
1897 @property
1898 def cached_diffs_relative_dir(self):
1899 """
1900 Return a relative to the repository store path of cached diffs
1901 used for safe display for users, who shouldn't know the absolute store
1902 path
1903 """
1904 return os.path.join(
1905 os.path.dirname(self.repo_name),
1906 self.cached_diffs_dir.split(os.path.sep)[-1])
1907
1908 @property
1909 def cached_diffs_dir(self):
1910 path = self.repo_full_path
1911 return os.path.join(
1912 os.path.dirname(path),
1913 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1914
1915 def cached_diffs(self):
1916 diff_cache_dir = self.cached_diffs_dir
1917 if os.path.isdir(diff_cache_dir):
1918 return os.listdir(diff_cache_dir)
1919 return []
1920
1921 def shadow_repos(self):
1922 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
1923 return [
1924 x for x in os.listdir(os.path.dirname(self.repo_full_path))
1925 if x.startswith(shadow_repos_pattern)]
1926
1927 def get_new_name(self, repo_name):
1928 """
1929 returns new full repository name based on assigned group and new new
1930
1931 :param group_name:
1932 """
1933 path_prefix = self.group.full_path_splitted if self.group else []
1934 return self.NAME_SEP.join(path_prefix + [repo_name])
1935
1936 @property
1937 def _config(self):
1938 """
1939 Returns db based config object.
1940 """
1941 from rhodecode.lib.utils import make_db_config
1942 return make_db_config(clear_session=False, repo=self)
1943
1944 def permissions(self, with_admins=True, with_owner=True,
1945 expand_from_user_groups=False):
1946 """
1947 Permissions for repositories
1948 """
1949 _admin_perm = 'repository.admin'
1950
1951 owner_row = []
1952 if with_owner:
1953 usr = AttributeDict(self.user.get_dict())
1954 usr.owner_row = True
1955 usr.permission = _admin_perm
1956 usr.permission_id = None
1957 owner_row.append(usr)
1958
1959 super_admin_ids = []
1960 super_admin_rows = []
1961 if with_admins:
1962 for usr in User.get_all_super_admins():
1963 super_admin_ids.append(usr.user_id)
1964 # if this admin is also owner, don't double the record
1965 if usr.user_id == owner_row[0].user_id:
1966 owner_row[0].admin_row = True
1967 else:
1968 usr = AttributeDict(usr.get_dict())
1969 usr.admin_row = True
1970 usr.permission = _admin_perm
1971 usr.permission_id = None
1972 super_admin_rows.append(usr)
1973
1974 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1975 q = q.options(joinedload(UserRepoToPerm.repository),
1976 joinedload(UserRepoToPerm.user),
1977 joinedload(UserRepoToPerm.permission),)
1978
1979 # get owners and admins and permissions. We do a trick of re-writing
1980 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1981 # has a global reference and changing one object propagates to all
1982 # others. This means if admin is also an owner admin_row that change
1983 # would propagate to both objects
1984 perm_rows = []
1985 for _usr in q.all():
1986 usr = AttributeDict(_usr.user.get_dict())
1987 # if this user is also owner/admin, mark as duplicate record
1988 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1989 usr.duplicate_perm = True
1990 # also check if this permission is maybe used by branch_permissions
1991 if _usr.branch_perm_entry:
1992 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
1993
1994 usr.permission = _usr.permission.permission_name
1995 usr.permission_id = _usr.repo_to_perm_id
1996 perm_rows.append(usr)
1997
1998 # filter the perm rows by 'default' first and then sort them by
1999 # admin,write,read,none permissions sorted again alphabetically in
2000 # each group
2001 perm_rows = sorted(perm_rows, key=display_user_sort)
2002
2003 user_groups_rows = []
2004 if expand_from_user_groups:
2005 for ug in self.permission_user_groups(with_members=True):
2006 for user_data in ug.members:
2007 user_groups_rows.append(user_data)
2008
2009 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2010
2011 def permission_user_groups(self, with_members=True):
2012 q = UserGroupRepoToPerm.query()\
2013 .filter(UserGroupRepoToPerm.repository == self)
2014 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2015 joinedload(UserGroupRepoToPerm.users_group),
2016 joinedload(UserGroupRepoToPerm.permission),)
2017
2018 perm_rows = []
2019 for _user_group in q.all():
2020 entry = AttributeDict(_user_group.users_group.get_dict())
2021 entry.permission = _user_group.permission.permission_name
2022 if with_members:
2023 entry.members = [x.user.get_dict()
2024 for x in _user_group.users_group.members]
2025 perm_rows.append(entry)
2026
2027 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2028 return perm_rows
2029
2030 def get_api_data(self, include_secrets=False):
2031 """
2032 Common function for generating repo api data
2033
2034 :param include_secrets: See :meth:`User.get_api_data`.
2035
2036 """
2037 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2038 # move this methods on models level.
2039 from rhodecode.model.settings import SettingsModel
2040 from rhodecode.model.repo import RepoModel
2041
2042 repo = self
2043 _user_id, _time, _reason = self.locked
2044
2045 data = {
2046 'repo_id': repo.repo_id,
2047 'repo_name': repo.repo_name,
2048 'repo_type': repo.repo_type,
2049 'clone_uri': repo.clone_uri or '',
2050 'push_uri': repo.push_uri or '',
2051 'url': RepoModel().get_url(self),
2052 'private': repo.private,
2053 'created_on': repo.created_on,
2054 'description': repo.description_safe,
2055 'landing_rev': repo.landing_rev,
2056 'owner': repo.user.username,
2057 'fork_of': repo.fork.repo_name if repo.fork else None,
2058 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2059 'enable_statistics': repo.enable_statistics,
2060 'enable_locking': repo.enable_locking,
2061 'enable_downloads': repo.enable_downloads,
2062 'last_changeset': repo.changeset_cache,
2063 'locked_by': User.get(_user_id).get_api_data(
2064 include_secrets=include_secrets) if _user_id else None,
2065 'locked_date': time_to_datetime(_time) if _time else None,
2066 'lock_reason': _reason if _reason else None,
2067 }
2068
2069 # TODO: mikhail: should be per-repo settings here
2070 rc_config = SettingsModel().get_all_settings()
2071 repository_fields = str2bool(
2072 rc_config.get('rhodecode_repository_fields'))
2073 if repository_fields:
2074 for f in self.extra_fields:
2075 data[f.field_key_prefixed] = f.field_value
2076
2077 return data
2078
2079 @classmethod
2080 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2081 if not lock_time:
2082 lock_time = time.time()
2083 if not lock_reason:
2084 lock_reason = cls.LOCK_AUTOMATIC
2085 repo.locked = [user_id, lock_time, lock_reason]
2086 Session().add(repo)
2087 Session().commit()
2088
2089 @classmethod
2090 def unlock(cls, repo):
2091 repo.locked = None
2092 Session().add(repo)
2093 Session().commit()
2094
2095 @classmethod
2096 def getlock(cls, repo):
2097 return repo.locked
2098
2099 def is_user_lock(self, user_id):
2100 if self.lock[0]:
2101 lock_user_id = safe_int(self.lock[0])
2102 user_id = safe_int(user_id)
2103 # both are ints, and they are equal
2104 return all([lock_user_id, user_id]) and lock_user_id == user_id
2105
2106 return False
2107
2108 def get_locking_state(self, action, user_id, only_when_enabled=True):
2109 """
2110 Checks locking on this repository, if locking is enabled and lock is
2111 present returns a tuple of make_lock, locked, locked_by.
2112 make_lock can have 3 states None (do nothing) True, make lock
2113 False release lock, This value is later propagated to hooks, which
2114 do the locking. Think about this as signals passed to hooks what to do.
2115
2116 """
2117 # TODO: johbo: This is part of the business logic and should be moved
2118 # into the RepositoryModel.
2119
2120 if action not in ('push', 'pull'):
2121 raise ValueError("Invalid action value: %s" % repr(action))
2122
2123 # defines if locked error should be thrown to user
2124 currently_locked = False
2125 # defines if new lock should be made, tri-state
2126 make_lock = None
2127 repo = self
2128 user = User.get(user_id)
2129
2130 lock_info = repo.locked
2131
2132 if repo and (repo.enable_locking or not only_when_enabled):
2133 if action == 'push':
2134 # check if it's already locked !, if it is compare users
2135 locked_by_user_id = lock_info[0]
2136 if user.user_id == locked_by_user_id:
2137 log.debug(
2138 'Got `push` action from user %s, now unlocking', user)
2139 # unlock if we have push from user who locked
2140 make_lock = False
2141 else:
2142 # we're not the same user who locked, ban with
2143 # code defined in settings (default is 423 HTTP Locked) !
2144 log.debug('Repo %s is currently locked by %s', repo, user)
2145 currently_locked = True
2146 elif action == 'pull':
2147 # [0] user [1] date
2148 if lock_info[0] and lock_info[1]:
2149 log.debug('Repo %s is currently locked by %s', repo, user)
2150 currently_locked = True
2151 else:
2152 log.debug('Setting lock on repo %s by %s', repo, user)
2153 make_lock = True
2154
2155 else:
2156 log.debug('Repository %s do not have locking enabled', repo)
2157
2158 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2159 make_lock, currently_locked, lock_info)
2160
2161 from rhodecode.lib.auth import HasRepoPermissionAny
2162 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2163 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2164 # if we don't have at least write permission we cannot make a lock
2165 log.debug('lock state reset back to FALSE due to lack '
2166 'of at least read permission')
2167 make_lock = False
2168
2169 return make_lock, currently_locked, lock_info
2170
2171 @property
2172 def last_db_change(self):
2173 return self.updated_on
2174
2175 @property
2176 def clone_uri_hidden(self):
2177 clone_uri = self.clone_uri
2178 if clone_uri:
2179 import urlobject
2180 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2181 if url_obj.password:
2182 clone_uri = url_obj.with_password('*****')
2183 return clone_uri
2184
2185 @property
2186 def push_uri_hidden(self):
2187 push_uri = self.push_uri
2188 if push_uri:
2189 import urlobject
2190 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2191 if url_obj.password:
2192 push_uri = url_obj.with_password('*****')
2193 return push_uri
2194
2195 def clone_url(self, **override):
2196 from rhodecode.model.settings import SettingsModel
2197
2198 uri_tmpl = None
2199 if 'with_id' in override:
2200 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2201 del override['with_id']
2202
2203 if 'uri_tmpl' in override:
2204 uri_tmpl = override['uri_tmpl']
2205 del override['uri_tmpl']
2206
2207 ssh = False
2208 if 'ssh' in override:
2209 ssh = True
2210 del override['ssh']
2211
2212 # we didn't override our tmpl from **overrides
2213 if not uri_tmpl:
2214 rc_config = SettingsModel().get_all_settings(cache=True)
2215 if ssh:
2216 uri_tmpl = rc_config.get(
2217 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2218 else:
2219 uri_tmpl = rc_config.get(
2220 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2221
2222 request = get_current_request()
2223 return get_clone_url(request=request,
2224 uri_tmpl=uri_tmpl,
2225 repo_name=self.repo_name,
2226 repo_id=self.repo_id, **override)
2227
2228 def set_state(self, state):
2229 self.repo_state = state
2230 Session().add(self)
2231 #==========================================================================
2232 # SCM PROPERTIES
2233 #==========================================================================
2234
2235 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2236 return get_commit_safe(
2237 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2238
2239 def get_changeset(self, rev=None, pre_load=None):
2240 warnings.warn("Use get_commit", DeprecationWarning)
2241 commit_id = None
2242 commit_idx = None
2243 if isinstance(rev, compat.string_types):
2244 commit_id = rev
2245 else:
2246 commit_idx = rev
2247 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2248 pre_load=pre_load)
2249
2250 def get_landing_commit(self):
2251 """
2252 Returns landing commit, or if that doesn't exist returns the tip
2253 """
2254 _rev_type, _rev = self.landing_rev
2255 commit = self.get_commit(_rev)
2256 if isinstance(commit, EmptyCommit):
2257 return self.get_commit()
2258 return commit
2259
2260 def update_commit_cache(self, cs_cache=None, config=None):
2261 """
2262 Update cache of last changeset for repository, keys should be::
2263
2264 short_id
2265 raw_id
2266 revision
2267 parents
2268 message
2269 date
2270 author
2271
2272 :param cs_cache:
2273 """
2274 from rhodecode.lib.vcs.backends.base import BaseChangeset
2275 if cs_cache is None:
2276 # use no-cache version here
2277 scm_repo = self.scm_instance(cache=False, config=config)
2278
2279 empty = not scm_repo or scm_repo.is_empty()
2280 if not empty:
2281 cs_cache = scm_repo.get_commit(
2282 pre_load=["author", "date", "message", "parents"])
2283 else:
2284 cs_cache = EmptyCommit()
2285
2286 if isinstance(cs_cache, BaseChangeset):
2287 cs_cache = cs_cache.__json__()
2288
2289 def is_outdated(new_cs_cache):
2290 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2291 new_cs_cache['revision'] != self.changeset_cache['revision']):
2292 return True
2293 return False
2294
2295 # check if we have maybe already latest cached revision
2296 if is_outdated(cs_cache) or not self.changeset_cache:
2297 _default = datetime.datetime.utcnow()
2298 last_change = cs_cache.get('date') or _default
2299 if self.updated_on and self.updated_on > last_change:
2300 # we check if last update is newer than the new value
2301 # if yes, we use the current timestamp instead. Imagine you get
2302 # old commit pushed 1y ago, we'd set last update 1y to ago.
2303 last_change = _default
2304 log.debug('updated repo %s with new cs cache %s',
2305 self.repo_name, cs_cache)
2306 self.updated_on = last_change
2307 self.changeset_cache = cs_cache
2308 Session().add(self)
2309 Session().commit()
2310 else:
2311 log.debug('Skipping update_commit_cache for repo:`%s` '
2312 'commit already with latest changes', self.repo_name)
2313
2314 @property
2315 def tip(self):
2316 return self.get_commit('tip')
2317
2318 @property
2319 def author(self):
2320 return self.tip.author
2321
2322 @property
2323 def last_change(self):
2324 return self.scm_instance().last_change
2325
2326 def get_comments(self, revisions=None):
2327 """
2328 Returns comments for this repository grouped by revisions
2329
2330 :param revisions: filter query by revisions only
2331 """
2332 cmts = ChangesetComment.query()\
2333 .filter(ChangesetComment.repo == self)
2334 if revisions:
2335 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2336 grouped = collections.defaultdict(list)
2337 for cmt in cmts.all():
2338 grouped[cmt.revision].append(cmt)
2339 return grouped
2340
2341 def statuses(self, revisions=None):
2342 """
2343 Returns statuses for this repository
2344
2345 :param revisions: list of revisions to get statuses for
2346 """
2347 statuses = ChangesetStatus.query()\
2348 .filter(ChangesetStatus.repo == self)\
2349 .filter(ChangesetStatus.version == 0)
2350
2351 if revisions:
2352 # Try doing the filtering in chunks to avoid hitting limits
2353 size = 500
2354 status_results = []
2355 for chunk in xrange(0, len(revisions), size):
2356 status_results += statuses.filter(
2357 ChangesetStatus.revision.in_(
2358 revisions[chunk: chunk+size])
2359 ).all()
2360 else:
2361 status_results = statuses.all()
2362
2363 grouped = {}
2364
2365 # maybe we have open new pullrequest without a status?
2366 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2367 status_lbl = ChangesetStatus.get_status_lbl(stat)
2368 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2369 for rev in pr.revisions:
2370 pr_id = pr.pull_request_id
2371 pr_repo = pr.target_repo.repo_name
2372 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2373
2374 for stat in status_results:
2375 pr_id = pr_repo = None
2376 if stat.pull_request:
2377 pr_id = stat.pull_request.pull_request_id
2378 pr_repo = stat.pull_request.target_repo.repo_name
2379 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2380 pr_id, pr_repo]
2381 return grouped
2382
2383 # ==========================================================================
2384 # SCM CACHE INSTANCE
2385 # ==========================================================================
2386
2387 def scm_instance(self, **kwargs):
2388 import rhodecode
2389
2390 # Passing a config will not hit the cache currently only used
2391 # for repo2dbmapper
2392 config = kwargs.pop('config', None)
2393 cache = kwargs.pop('cache', None)
2394 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2395 # if cache is NOT defined use default global, else we have a full
2396 # control over cache behaviour
2397 if cache is None and full_cache and not config:
2398 return self._get_instance_cached()
2399 return self._get_instance(cache=bool(cache), config=config)
2400
2401 def _get_instance_cached(self):
2402 from rhodecode.lib import rc_cache
2403
2404 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2405 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2406 repo_id=self.repo_id)
2407 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2408
2409 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2410 def get_instance_cached(repo_id, context_id):
2411 return self._get_instance()
2412
2413 # we must use thread scoped cache here,
2414 # because each thread of gevent needs it's own not shared connection and cache
2415 # we also alter `args` so the cache key is individual for every green thread.
2416 inv_context_manager = rc_cache.InvalidationContext(
2417 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2418 thread_scoped=True)
2419 with inv_context_manager as invalidation_context:
2420 args = (self.repo_id, inv_context_manager.cache_key)
2421 # re-compute and store cache if we get invalidate signal
2422 if invalidation_context.should_invalidate():
2423 instance = get_instance_cached.refresh(*args)
2424 else:
2425 instance = get_instance_cached(*args)
2426
2427 log.debug(
2428 'Repo instance fetched in %.3fs', inv_context_manager.compute_time)
2429 return instance
2430
2431 def _get_instance(self, cache=True, config=None):
2432 config = config or self._config
2433 custom_wire = {
2434 'cache': cache # controls the vcs.remote cache
2435 }
2436 repo = get_vcs_instance(
2437 repo_path=safe_str(self.repo_full_path),
2438 config=config,
2439 with_wire=custom_wire,
2440 create=False,
2441 _vcs_alias=self.repo_type)
2442
2443 return repo
2444
2445 def __json__(self):
2446 return {'landing_rev': self.landing_rev}
2447
2448 def get_dict(self):
2449
2450 # Since we transformed `repo_name` to a hybrid property, we need to
2451 # keep compatibility with the code which uses `repo_name` field.
2452
2453 result = super(Repository, self).get_dict()
2454 result['repo_name'] = result.pop('_repo_name', None)
2455 return result
2456
2457
2458 class RepoGroup(Base, BaseModel):
2459 __tablename__ = 'groups'
2460 __table_args__ = (
2461 UniqueConstraint('group_name', 'group_parent_id'),
2462 base_table_args,
2463 )
2464 __mapper_args__ = {'order_by': 'group_name'}
2465
2466 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2467
2468 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2469 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2470 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2471 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2472 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2473 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2474 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2475 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2476 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2477
2478 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2479 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2480 parent_group = relationship('RepoGroup', remote_side=group_id)
2481 user = relationship('User')
2482 integrations = relationship('Integration',
2483 cascade="all, delete, delete-orphan")
2484
2485 def __init__(self, group_name='', parent_group=None):
2486 self.group_name = group_name
2487 self.parent_group = parent_group
2488
2489 def __unicode__(self):
2490 return u"<%s('id:%s:%s')>" % (
2491 self.__class__.__name__, self.group_id, self.group_name)
2492
2493 @hybrid_property
2494 def description_safe(self):
2495 from rhodecode.lib import helpers as h
2496 return h.escape(self.group_description)
2497
2498 @classmethod
2499 def _generate_choice(cls, repo_group):
2500 from webhelpers.html import literal as _literal
2501 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2502 return repo_group.group_id, _name(repo_group.full_path_splitted)
2503
2504 @classmethod
2505 def groups_choices(cls, groups=None, show_empty_group=True):
2506 if not groups:
2507 groups = cls.query().all()
2508
2509 repo_groups = []
2510 if show_empty_group:
2511 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2512
2513 repo_groups.extend([cls._generate_choice(x) for x in groups])
2514
2515 repo_groups = sorted(
2516 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2517 return repo_groups
2518
2519 @classmethod
2520 def url_sep(cls):
2521 return URL_SEP
2522
2523 @classmethod
2524 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2525 if case_insensitive:
2526 gr = cls.query().filter(func.lower(cls.group_name)
2527 == func.lower(group_name))
2528 else:
2529 gr = cls.query().filter(cls.group_name == group_name)
2530 if cache:
2531 name_key = _hash_key(group_name)
2532 gr = gr.options(
2533 FromCache("sql_cache_short", "get_group_%s" % name_key))
2534 return gr.scalar()
2535
2536 @classmethod
2537 def get_user_personal_repo_group(cls, user_id):
2538 user = User.get(user_id)
2539 if user.username == User.DEFAULT_USER:
2540 return None
2541
2542 return cls.query()\
2543 .filter(cls.personal == true()) \
2544 .filter(cls.user == user) \
2545 .order_by(cls.group_id.asc()) \
2546 .first()
2547
2548 @classmethod
2549 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2550 case_insensitive=True):
2551 q = RepoGroup.query()
2552
2553 if not isinstance(user_id, Optional):
2554 q = q.filter(RepoGroup.user_id == user_id)
2555
2556 if not isinstance(group_id, Optional):
2557 q = q.filter(RepoGroup.group_parent_id == group_id)
2558
2559 if case_insensitive:
2560 q = q.order_by(func.lower(RepoGroup.group_name))
2561 else:
2562 q = q.order_by(RepoGroup.group_name)
2563 return q.all()
2564
2565 @property
2566 def parents(self):
2567 parents_recursion_limit = 10
2568 groups = []
2569 if self.parent_group is None:
2570 return groups
2571 cur_gr = self.parent_group
2572 groups.insert(0, cur_gr)
2573 cnt = 0
2574 while 1:
2575 cnt += 1
2576 gr = getattr(cur_gr, 'parent_group', None)
2577 cur_gr = cur_gr.parent_group
2578 if gr is None:
2579 break
2580 if cnt == parents_recursion_limit:
2581 # this will prevent accidental infinit loops
2582 log.error('more than %s parents found for group %s, stopping '
2583 'recursive parent fetching', parents_recursion_limit, self)
2584 break
2585
2586 groups.insert(0, gr)
2587 return groups
2588
2589 @property
2590 def last_db_change(self):
2591 return self.updated_on
2592
2593 @property
2594 def children(self):
2595 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2596
2597 @property
2598 def name(self):
2599 return self.group_name.split(RepoGroup.url_sep())[-1]
2600
2601 @property
2602 def full_path(self):
2603 return self.group_name
2604
2605 @property
2606 def full_path_splitted(self):
2607 return self.group_name.split(RepoGroup.url_sep())
2608
2609 @property
2610 def repositories(self):
2611 return Repository.query()\
2612 .filter(Repository.group == self)\
2613 .order_by(Repository.repo_name)
2614
2615 @property
2616 def repositories_recursive_count(self):
2617 cnt = self.repositories.count()
2618
2619 def children_count(group):
2620 cnt = 0
2621 for child in group.children:
2622 cnt += child.repositories.count()
2623 cnt += children_count(child)
2624 return cnt
2625
2626 return cnt + children_count(self)
2627
2628 def _recursive_objects(self, include_repos=True):
2629 all_ = []
2630
2631 def _get_members(root_gr):
2632 if include_repos:
2633 for r in root_gr.repositories:
2634 all_.append(r)
2635 childs = root_gr.children.all()
2636 if childs:
2637 for gr in childs:
2638 all_.append(gr)
2639 _get_members(gr)
2640
2641 _get_members(self)
2642 return [self] + all_
2643
2644 def recursive_groups_and_repos(self):
2645 """
2646 Recursive return all groups, with repositories in those groups
2647 """
2648 return self._recursive_objects()
2649
2650 def recursive_groups(self):
2651 """
2652 Returns all children groups for this group including children of children
2653 """
2654 return self._recursive_objects(include_repos=False)
2655
2656 def get_new_name(self, group_name):
2657 """
2658 returns new full group name based on parent and new name
2659
2660 :param group_name:
2661 """
2662 path_prefix = (self.parent_group.full_path_splitted if
2663 self.parent_group else [])
2664 return RepoGroup.url_sep().join(path_prefix + [group_name])
2665
2666 def permissions(self, with_admins=True, with_owner=True,
2667 expand_from_user_groups=False):
2668 """
2669 Permissions for repository groups
2670 """
2671 _admin_perm = 'group.admin'
2672
2673 owner_row = []
2674 if with_owner:
2675 usr = AttributeDict(self.user.get_dict())
2676 usr.owner_row = True
2677 usr.permission = _admin_perm
2678 owner_row.append(usr)
2679
2680 super_admin_ids = []
2681 super_admin_rows = []
2682 if with_admins:
2683 for usr in User.get_all_super_admins():
2684 super_admin_ids.append(usr.user_id)
2685 # if this admin is also owner, don't double the record
2686 if usr.user_id == owner_row[0].user_id:
2687 owner_row[0].admin_row = True
2688 else:
2689 usr = AttributeDict(usr.get_dict())
2690 usr.admin_row = True
2691 usr.permission = _admin_perm
2692 super_admin_rows.append(usr)
2693
2694 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2695 q = q.options(joinedload(UserRepoGroupToPerm.group),
2696 joinedload(UserRepoGroupToPerm.user),
2697 joinedload(UserRepoGroupToPerm.permission),)
2698
2699 # get owners and admins and permissions. We do a trick of re-writing
2700 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2701 # has a global reference and changing one object propagates to all
2702 # others. This means if admin is also an owner admin_row that change
2703 # would propagate to both objects
2704 perm_rows = []
2705 for _usr in q.all():
2706 usr = AttributeDict(_usr.user.get_dict())
2707 # if this user is also owner/admin, mark as duplicate record
2708 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2709 usr.duplicate_perm = True
2710 usr.permission = _usr.permission.permission_name
2711 perm_rows.append(usr)
2712
2713 # filter the perm rows by 'default' first and then sort them by
2714 # admin,write,read,none permissions sorted again alphabetically in
2715 # each group
2716 perm_rows = sorted(perm_rows, key=display_user_sort)
2717
2718 user_groups_rows = []
2719 if expand_from_user_groups:
2720 for ug in self.permission_user_groups(with_members=True):
2721 for user_data in ug.members:
2722 user_groups_rows.append(user_data)
2723
2724 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2725
2726 def permission_user_groups(self, with_members=False):
2727 q = UserGroupRepoGroupToPerm.query()\
2728 .filter(UserGroupRepoGroupToPerm.group == self)
2729 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2730 joinedload(UserGroupRepoGroupToPerm.users_group),
2731 joinedload(UserGroupRepoGroupToPerm.permission),)
2732
2733 perm_rows = []
2734 for _user_group in q.all():
2735 entry = AttributeDict(_user_group.users_group.get_dict())
2736 entry.permission = _user_group.permission.permission_name
2737 if with_members:
2738 entry.members = [x.user.get_dict()
2739 for x in _user_group.users_group.members]
2740 perm_rows.append(entry)
2741
2742 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2743 return perm_rows
2744
2745 def get_api_data(self):
2746 """
2747 Common function for generating api data
2748
2749 """
2750 group = self
2751 data = {
2752 'group_id': group.group_id,
2753 'group_name': group.group_name,
2754 'group_description': group.description_safe,
2755 'parent_group': group.parent_group.group_name if group.parent_group else None,
2756 'repositories': [x.repo_name for x in group.repositories],
2757 'owner': group.user.username,
2758 }
2759 return data
2760
2761
2762 class Permission(Base, BaseModel):
2763 __tablename__ = 'permissions'
2764 __table_args__ = (
2765 Index('p_perm_name_idx', 'permission_name'),
2766 base_table_args,
2767 )
2768
2769 PERMS = [
2770 ('hg.admin', _('RhodeCode Super Administrator')),
2771
2772 ('repository.none', _('Repository no access')),
2773 ('repository.read', _('Repository read access')),
2774 ('repository.write', _('Repository write access')),
2775 ('repository.admin', _('Repository admin access')),
2776
2777 ('group.none', _('Repository group no access')),
2778 ('group.read', _('Repository group read access')),
2779 ('group.write', _('Repository group write access')),
2780 ('group.admin', _('Repository group admin access')),
2781
2782 ('usergroup.none', _('User group no access')),
2783 ('usergroup.read', _('User group read access')),
2784 ('usergroup.write', _('User group write access')),
2785 ('usergroup.admin', _('User group admin access')),
2786
2787 ('branch.none', _('Branch no permissions')),
2788 ('branch.merge', _('Branch access by web merge')),
2789 ('branch.push', _('Branch access by push')),
2790 ('branch.push_force', _('Branch access by push with force')),
2791
2792 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2793 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2794
2795 ('hg.usergroup.create.false', _('User Group creation disabled')),
2796 ('hg.usergroup.create.true', _('User Group creation enabled')),
2797
2798 ('hg.create.none', _('Repository creation disabled')),
2799 ('hg.create.repository', _('Repository creation enabled')),
2800 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2801 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2802
2803 ('hg.fork.none', _('Repository forking disabled')),
2804 ('hg.fork.repository', _('Repository forking enabled')),
2805
2806 ('hg.register.none', _('Registration disabled')),
2807 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2808 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2809
2810 ('hg.password_reset.enabled', _('Password reset enabled')),
2811 ('hg.password_reset.hidden', _('Password reset hidden')),
2812 ('hg.password_reset.disabled', _('Password reset disabled')),
2813
2814 ('hg.extern_activate.manual', _('Manual activation of external account')),
2815 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2816
2817 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2818 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2819 ]
2820
2821 # definition of system default permissions for DEFAULT user, created on
2822 # system setup
2823 DEFAULT_USER_PERMISSIONS = [
2824 # object perms
2825 'repository.read',
2826 'group.read',
2827 'usergroup.read',
2828 # branch, for backward compat we need same value as before so forced pushed
2829 'branch.push_force',
2830 # global
2831 'hg.create.repository',
2832 'hg.repogroup.create.false',
2833 'hg.usergroup.create.false',
2834 'hg.create.write_on_repogroup.true',
2835 'hg.fork.repository',
2836 'hg.register.manual_activate',
2837 'hg.password_reset.enabled',
2838 'hg.extern_activate.auto',
2839 'hg.inherit_default_perms.true',
2840 ]
2841
2842 # defines which permissions are more important higher the more important
2843 # Weight defines which permissions are more important.
2844 # The higher number the more important.
2845 PERM_WEIGHTS = {
2846 'repository.none': 0,
2847 'repository.read': 1,
2848 'repository.write': 3,
2849 'repository.admin': 4,
2850
2851 'group.none': 0,
2852 'group.read': 1,
2853 'group.write': 3,
2854 'group.admin': 4,
2855
2856 'usergroup.none': 0,
2857 'usergroup.read': 1,
2858 'usergroup.write': 3,
2859 'usergroup.admin': 4,
2860
2861 'branch.none': 0,
2862 'branch.merge': 1,
2863 'branch.push': 3,
2864 'branch.push_force': 4,
2865
2866 'hg.repogroup.create.false': 0,
2867 'hg.repogroup.create.true': 1,
2868
2869 'hg.usergroup.create.false': 0,
2870 'hg.usergroup.create.true': 1,
2871
2872 'hg.fork.none': 0,
2873 'hg.fork.repository': 1,
2874 'hg.create.none': 0,
2875 'hg.create.repository': 1
2876 }
2877
2878 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2879 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2880 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2881
2882 def __unicode__(self):
2883 return u"<%s('%s:%s')>" % (
2884 self.__class__.__name__, self.permission_id, self.permission_name
2885 )
2886
2887 @classmethod
2888 def get_by_key(cls, key):
2889 return cls.query().filter(cls.permission_name == key).scalar()
2890
2891 @classmethod
2892 def get_default_repo_perms(cls, user_id, repo_id=None):
2893 q = Session().query(UserRepoToPerm, Repository, Permission)\
2894 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2895 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2896 .filter(UserRepoToPerm.user_id == user_id)
2897 if repo_id:
2898 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2899 return q.all()
2900
2901 @classmethod
2902 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
2903 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
2904 .join(
2905 Permission,
2906 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
2907 .join(
2908 UserRepoToPerm,
2909 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
2910 .filter(UserRepoToPerm.user_id == user_id)
2911
2912 if repo_id:
2913 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
2914 return q.order_by(UserToRepoBranchPermission.rule_order).all()
2915
2916 @classmethod
2917 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2918 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2919 .join(
2920 Permission,
2921 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2922 .join(
2923 Repository,
2924 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2925 .join(
2926 UserGroup,
2927 UserGroupRepoToPerm.users_group_id ==
2928 UserGroup.users_group_id)\
2929 .join(
2930 UserGroupMember,
2931 UserGroupRepoToPerm.users_group_id ==
2932 UserGroupMember.users_group_id)\
2933 .filter(
2934 UserGroupMember.user_id == user_id,
2935 UserGroup.users_group_active == true())
2936 if repo_id:
2937 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2938 return q.all()
2939
2940 @classmethod
2941 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
2942 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
2943 .join(
2944 Permission,
2945 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
2946 .join(
2947 UserGroupRepoToPerm,
2948 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
2949 .join(
2950 UserGroup,
2951 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
2952 .join(
2953 UserGroupMember,
2954 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
2955 .filter(
2956 UserGroupMember.user_id == user_id,
2957 UserGroup.users_group_active == true())
2958
2959 if repo_id:
2960 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
2961 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
2962
2963 @classmethod
2964 def get_default_group_perms(cls, user_id, repo_group_id=None):
2965 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2966 .join(
2967 Permission,
2968 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
2969 .join(
2970 RepoGroup,
2971 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
2972 .filter(UserRepoGroupToPerm.user_id == user_id)
2973 if repo_group_id:
2974 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2975 return q.all()
2976
2977 @classmethod
2978 def get_default_group_perms_from_user_group(
2979 cls, user_id, repo_group_id=None):
2980 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2981 .join(
2982 Permission,
2983 UserGroupRepoGroupToPerm.permission_id ==
2984 Permission.permission_id)\
2985 .join(
2986 RepoGroup,
2987 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2988 .join(
2989 UserGroup,
2990 UserGroupRepoGroupToPerm.users_group_id ==
2991 UserGroup.users_group_id)\
2992 .join(
2993 UserGroupMember,
2994 UserGroupRepoGroupToPerm.users_group_id ==
2995 UserGroupMember.users_group_id)\
2996 .filter(
2997 UserGroupMember.user_id == user_id,
2998 UserGroup.users_group_active == true())
2999 if repo_group_id:
3000 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3001 return q.all()
3002
3003 @classmethod
3004 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3005 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3006 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3007 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3008 .filter(UserUserGroupToPerm.user_id == user_id)
3009 if user_group_id:
3010 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3011 return q.all()
3012
3013 @classmethod
3014 def get_default_user_group_perms_from_user_group(
3015 cls, user_id, user_group_id=None):
3016 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3017 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3018 .join(
3019 Permission,
3020 UserGroupUserGroupToPerm.permission_id ==
3021 Permission.permission_id)\
3022 .join(
3023 TargetUserGroup,
3024 UserGroupUserGroupToPerm.target_user_group_id ==
3025 TargetUserGroup.users_group_id)\
3026 .join(
3027 UserGroup,
3028 UserGroupUserGroupToPerm.user_group_id ==
3029 UserGroup.users_group_id)\
3030 .join(
3031 UserGroupMember,
3032 UserGroupUserGroupToPerm.user_group_id ==
3033 UserGroupMember.users_group_id)\
3034 .filter(
3035 UserGroupMember.user_id == user_id,
3036 UserGroup.users_group_active == true())
3037 if user_group_id:
3038 q = q.filter(
3039 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3040
3041 return q.all()
3042
3043
3044 class UserRepoToPerm(Base, BaseModel):
3045 __tablename__ = 'repo_to_perm'
3046 __table_args__ = (
3047 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3048 base_table_args
3049 )
3050
3051 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3052 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3053 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3054 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3055
3056 user = relationship('User')
3057 repository = relationship('Repository')
3058 permission = relationship('Permission')
3059
3060 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete, delete-orphan", lazy='joined')
3061
3062 @classmethod
3063 def create(cls, user, repository, permission):
3064 n = cls()
3065 n.user = user
3066 n.repository = repository
3067 n.permission = permission
3068 Session().add(n)
3069 return n
3070
3071 def __unicode__(self):
3072 return u'<%s => %s >' % (self.user, self.repository)
3073
3074
3075 class UserUserGroupToPerm(Base, BaseModel):
3076 __tablename__ = 'user_user_group_to_perm'
3077 __table_args__ = (
3078 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3079 base_table_args
3080 )
3081
3082 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3083 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3084 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3085 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3086
3087 user = relationship('User')
3088 user_group = relationship('UserGroup')
3089 permission = relationship('Permission')
3090
3091 @classmethod
3092 def create(cls, user, user_group, permission):
3093 n = cls()
3094 n.user = user
3095 n.user_group = user_group
3096 n.permission = permission
3097 Session().add(n)
3098 return n
3099
3100 def __unicode__(self):
3101 return u'<%s => %s >' % (self.user, self.user_group)
3102
3103
3104 class UserToPerm(Base, BaseModel):
3105 __tablename__ = 'user_to_perm'
3106 __table_args__ = (
3107 UniqueConstraint('user_id', 'permission_id'),
3108 base_table_args
3109 )
3110
3111 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3112 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3113 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3114
3115 user = relationship('User')
3116 permission = relationship('Permission', lazy='joined')
3117
3118 def __unicode__(self):
3119 return u'<%s => %s >' % (self.user, self.permission)
3120
3121
3122 class UserGroupRepoToPerm(Base, BaseModel):
3123 __tablename__ = 'users_group_repo_to_perm'
3124 __table_args__ = (
3125 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3126 base_table_args
3127 )
3128
3129 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3130 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3131 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3132 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3133
3134 users_group = relationship('UserGroup')
3135 permission = relationship('Permission')
3136 repository = relationship('Repository')
3137 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3138
3139 @classmethod
3140 def create(cls, users_group, repository, permission):
3141 n = cls()
3142 n.users_group = users_group
3143 n.repository = repository
3144 n.permission = permission
3145 Session().add(n)
3146 return n
3147
3148 def __unicode__(self):
3149 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3150
3151
3152 class UserGroupUserGroupToPerm(Base, BaseModel):
3153 __tablename__ = 'user_group_user_group_to_perm'
3154 __table_args__ = (
3155 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3156 CheckConstraint('target_user_group_id != user_group_id'),
3157 base_table_args
3158 )
3159
3160 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3161 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3162 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3163 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3164
3165 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3166 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3167 permission = relationship('Permission')
3168
3169 @classmethod
3170 def create(cls, target_user_group, user_group, permission):
3171 n = cls()
3172 n.target_user_group = target_user_group
3173 n.user_group = user_group
3174 n.permission = permission
3175 Session().add(n)
3176 return n
3177
3178 def __unicode__(self):
3179 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3180
3181
3182 class UserGroupToPerm(Base, BaseModel):
3183 __tablename__ = 'users_group_to_perm'
3184 __table_args__ = (
3185 UniqueConstraint('users_group_id', 'permission_id',),
3186 base_table_args
3187 )
3188
3189 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3191 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3192
3193 users_group = relationship('UserGroup')
3194 permission = relationship('Permission')
3195
3196
3197 class UserRepoGroupToPerm(Base, BaseModel):
3198 __tablename__ = 'user_repo_group_to_perm'
3199 __table_args__ = (
3200 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3201 base_table_args
3202 )
3203
3204 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3205 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3206 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3207 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3208
3209 user = relationship('User')
3210 group = relationship('RepoGroup')
3211 permission = relationship('Permission')
3212
3213 @classmethod
3214 def create(cls, user, repository_group, permission):
3215 n = cls()
3216 n.user = user
3217 n.group = repository_group
3218 n.permission = permission
3219 Session().add(n)
3220 return n
3221
3222
3223 class UserGroupRepoGroupToPerm(Base, BaseModel):
3224 __tablename__ = 'users_group_repo_group_to_perm'
3225 __table_args__ = (
3226 UniqueConstraint('users_group_id', 'group_id'),
3227 base_table_args
3228 )
3229
3230 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3231 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3232 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3233 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3234
3235 users_group = relationship('UserGroup')
3236 permission = relationship('Permission')
3237 group = relationship('RepoGroup')
3238
3239 @classmethod
3240 def create(cls, user_group, repository_group, permission):
3241 n = cls()
3242 n.users_group = user_group
3243 n.group = repository_group
3244 n.permission = permission
3245 Session().add(n)
3246 return n
3247
3248 def __unicode__(self):
3249 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3250
3251
3252 class Statistics(Base, BaseModel):
3253 __tablename__ = 'statistics'
3254 __table_args__ = (
3255 base_table_args
3256 )
3257
3258 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3259 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3260 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3261 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3262 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3263 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3264
3265 repository = relationship('Repository', single_parent=True)
3266
3267
3268 class UserFollowing(Base, BaseModel):
3269 __tablename__ = 'user_followings'
3270 __table_args__ = (
3271 UniqueConstraint('user_id', 'follows_repository_id'),
3272 UniqueConstraint('user_id', 'follows_user_id'),
3273 base_table_args
3274 )
3275
3276 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3277 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3278 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3279 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3280 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3281
3282 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3283
3284 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3285 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3286
3287 @classmethod
3288 def get_repo_followers(cls, repo_id):
3289 return cls.query().filter(cls.follows_repo_id == repo_id)
3290
3291
3292 class CacheKey(Base, BaseModel):
3293 __tablename__ = 'cache_invalidation'
3294 __table_args__ = (
3295 UniqueConstraint('cache_key'),
3296 Index('key_idx', 'cache_key'),
3297 base_table_args,
3298 )
3299
3300 CACHE_TYPE_FEED = 'FEED'
3301 CACHE_TYPE_README = 'README'
3302 # namespaces used to register process/thread aware caches
3303 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3304 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3305
3306 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3307 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3308 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3309 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3310
3311 def __init__(self, cache_key, cache_args=''):
3312 self.cache_key = cache_key
3313 self.cache_args = cache_args
3314 self.cache_active = False
3315
3316 def __unicode__(self):
3317 return u"<%s('%s:%s[%s]')>" % (
3318 self.__class__.__name__,
3319 self.cache_id, self.cache_key, self.cache_active)
3320
3321 def _cache_key_partition(self):
3322 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3323 return prefix, repo_name, suffix
3324
3325 def get_prefix(self):
3326 """
3327 Try to extract prefix from existing cache key. The key could consist
3328 of prefix, repo_name, suffix
3329 """
3330 # this returns prefix, repo_name, suffix
3331 return self._cache_key_partition()[0]
3332
3333 def get_suffix(self):
3334 """
3335 get suffix that might have been used in _get_cache_key to
3336 generate self.cache_key. Only used for informational purposes
3337 in repo_edit.mako.
3338 """
3339 # prefix, repo_name, suffix
3340 return self._cache_key_partition()[2]
3341
3342 @classmethod
3343 def delete_all_cache(cls):
3344 """
3345 Delete all cache keys from database.
3346 Should only be run when all instances are down and all entries
3347 thus stale.
3348 """
3349 cls.query().delete()
3350 Session().commit()
3351
3352 @classmethod
3353 def set_invalidate(cls, cache_uid, delete=False):
3354 """
3355 Mark all caches of a repo as invalid in the database.
3356 """
3357
3358 try:
3359 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3360 if delete:
3361 qry.delete()
3362 log.debug('cache objects deleted for cache args %s',
3363 safe_str(cache_uid))
3364 else:
3365 qry.update({"cache_active": False})
3366 log.debug('cache objects marked as invalid for cache args %s',
3367 safe_str(cache_uid))
3368
3369 Session().commit()
3370 except Exception:
3371 log.exception(
3372 'Cache key invalidation failed for cache args %s',
3373 safe_str(cache_uid))
3374 Session().rollback()
3375
3376 @classmethod
3377 def get_active_cache(cls, cache_key):
3378 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3379 if inv_obj:
3380 return inv_obj
3381 return None
3382
3383
3384 class ChangesetComment(Base, BaseModel):
3385 __tablename__ = 'changeset_comments'
3386 __table_args__ = (
3387 Index('cc_revision_idx', 'revision'),
3388 base_table_args,
3389 )
3390
3391 COMMENT_OUTDATED = u'comment_outdated'
3392 COMMENT_TYPE_NOTE = u'note'
3393 COMMENT_TYPE_TODO = u'todo'
3394 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3395
3396 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3397 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3398 revision = Column('revision', String(40), nullable=True)
3399 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3400 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3401 line_no = Column('line_no', Unicode(10), nullable=True)
3402 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3403 f_path = Column('f_path', Unicode(1000), nullable=True)
3404 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3405 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3406 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3407 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3408 renderer = Column('renderer', Unicode(64), nullable=True)
3409 display_state = Column('display_state', Unicode(128), nullable=True)
3410
3411 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3412 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3413
3414 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3415 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3416
3417 author = relationship('User', lazy='joined')
3418 repo = relationship('Repository')
3419 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3420 pull_request = relationship('PullRequest', lazy='joined')
3421 pull_request_version = relationship('PullRequestVersion')
3422
3423 @classmethod
3424 def get_users(cls, revision=None, pull_request_id=None):
3425 """
3426 Returns user associated with this ChangesetComment. ie those
3427 who actually commented
3428
3429 :param cls:
3430 :param revision:
3431 """
3432 q = Session().query(User)\
3433 .join(ChangesetComment.author)
3434 if revision:
3435 q = q.filter(cls.revision == revision)
3436 elif pull_request_id:
3437 q = q.filter(cls.pull_request_id == pull_request_id)
3438 return q.all()
3439
3440 @classmethod
3441 def get_index_from_version(cls, pr_version, versions):
3442 num_versions = [x.pull_request_version_id for x in versions]
3443 try:
3444 return num_versions.index(pr_version) +1
3445 except (IndexError, ValueError):
3446 return
3447
3448 @property
3449 def outdated(self):
3450 return self.display_state == self.COMMENT_OUTDATED
3451
3452 def outdated_at_version(self, version):
3453 """
3454 Checks if comment is outdated for given pull request version
3455 """
3456 return self.outdated and self.pull_request_version_id != version
3457
3458 def older_than_version(self, version):
3459 """
3460 Checks if comment is made from previous version than given
3461 """
3462 if version is None:
3463 return self.pull_request_version_id is not None
3464
3465 return self.pull_request_version_id < version
3466
3467 @property
3468 def resolved(self):
3469 return self.resolved_by[0] if self.resolved_by else None
3470
3471 @property
3472 def is_todo(self):
3473 return self.comment_type == self.COMMENT_TYPE_TODO
3474
3475 @property
3476 def is_inline(self):
3477 return self.line_no and self.f_path
3478
3479 def get_index_version(self, versions):
3480 return self.get_index_from_version(
3481 self.pull_request_version_id, versions)
3482
3483 def __repr__(self):
3484 if self.comment_id:
3485 return '<DB:Comment #%s>' % self.comment_id
3486 else:
3487 return '<DB:Comment at %#x>' % id(self)
3488
3489 def get_api_data(self):
3490 comment = self
3491 data = {
3492 'comment_id': comment.comment_id,
3493 'comment_type': comment.comment_type,
3494 'comment_text': comment.text,
3495 'comment_status': comment.status_change,
3496 'comment_f_path': comment.f_path,
3497 'comment_lineno': comment.line_no,
3498 'comment_author': comment.author,
3499 'comment_created_on': comment.created_on,
3500 'comment_resolved_by': self.resolved
3501 }
3502 return data
3503
3504 def __json__(self):
3505 data = dict()
3506 data.update(self.get_api_data())
3507 return data
3508
3509
3510 class ChangesetStatus(Base, BaseModel):
3511 __tablename__ = 'changeset_statuses'
3512 __table_args__ = (
3513 Index('cs_revision_idx', 'revision'),
3514 Index('cs_version_idx', 'version'),
3515 UniqueConstraint('repo_id', 'revision', 'version'),
3516 base_table_args
3517 )
3518
3519 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3520 STATUS_APPROVED = 'approved'
3521 STATUS_REJECTED = 'rejected'
3522 STATUS_UNDER_REVIEW = 'under_review'
3523
3524 STATUSES = [
3525 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3526 (STATUS_APPROVED, _("Approved")),
3527 (STATUS_REJECTED, _("Rejected")),
3528 (STATUS_UNDER_REVIEW, _("Under Review")),
3529 ]
3530
3531 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3532 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3533 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3534 revision = Column('revision', String(40), nullable=False)
3535 status = Column('status', String(128), nullable=False, default=DEFAULT)
3536 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3537 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3538 version = Column('version', Integer(), nullable=False, default=0)
3539 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3540
3541 author = relationship('User', lazy='joined')
3542 repo = relationship('Repository')
3543 comment = relationship('ChangesetComment', lazy='joined')
3544 pull_request = relationship('PullRequest', lazy='joined')
3545
3546 def __unicode__(self):
3547 return u"<%s('%s[v%s]:%s')>" % (
3548 self.__class__.__name__,
3549 self.status, self.version, self.author
3550 )
3551
3552 @classmethod
3553 def get_status_lbl(cls, value):
3554 return dict(cls.STATUSES).get(value)
3555
3556 @property
3557 def status_lbl(self):
3558 return ChangesetStatus.get_status_lbl(self.status)
3559
3560 def get_api_data(self):
3561 status = self
3562 data = {
3563 'status_id': status.changeset_status_id,
3564 'status': status.status,
3565 }
3566 return data
3567
3568 def __json__(self):
3569 data = dict()
3570 data.update(self.get_api_data())
3571 return data
3572
3573
3574 class _SetState(object):
3575 """
3576 Context processor allowing changing state for sensitive operation such as
3577 pull request update or merge
3578 """
3579
3580 def __init__(self, pull_request, pr_state, back_state=None):
3581 self._pr = pull_request
3582 self._org_state = back_state or pull_request.pull_request_state
3583 self._pr_state = pr_state
3584
3585 def __enter__(self):
3586 log.debug('StateLock: entering set state context, setting state to: `%s`',
3587 self._pr_state)
3588 self._pr.pull_request_state = self._pr_state
3589 Session().add(self._pr)
3590 Session().commit()
3591
3592 def __exit__(self, exc_type, exc_val, exc_tb):
3593 log.debug('StateLock: exiting set state context, setting state to: `%s`',
3594 self._org_state)
3595 self._pr.pull_request_state = self._org_state
3596 Session().add(self._pr)
3597 Session().commit()
3598
3599
3600 class _PullRequestBase(BaseModel):
3601 """
3602 Common attributes of pull request and version entries.
3603 """
3604
3605 # .status values
3606 STATUS_NEW = u'new'
3607 STATUS_OPEN = u'open'
3608 STATUS_CLOSED = u'closed'
3609
3610 # available states
3611 STATE_CREATING = u'creating'
3612 STATE_UPDATING = u'updating'
3613 STATE_MERGING = u'merging'
3614 STATE_CREATED = u'created'
3615
3616 title = Column('title', Unicode(255), nullable=True)
3617 description = Column(
3618 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3619 nullable=True)
3620 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3621
3622 # new/open/closed status of pull request (not approve/reject/etc)
3623 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3624 created_on = Column(
3625 'created_on', DateTime(timezone=False), nullable=False,
3626 default=datetime.datetime.now)
3627 updated_on = Column(
3628 'updated_on', DateTime(timezone=False), nullable=False,
3629 default=datetime.datetime.now)
3630
3631 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3632
3633 @declared_attr
3634 def user_id(cls):
3635 return Column(
3636 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3637 unique=None)
3638
3639 # 500 revisions max
3640 _revisions = Column(
3641 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3642
3643 @declared_attr
3644 def source_repo_id(cls):
3645 # TODO: dan: rename column to source_repo_id
3646 return Column(
3647 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3648 nullable=False)
3649
3650 _source_ref = Column('org_ref', Unicode(255), nullable=False)
3651
3652 @hybrid_property
3653 def source_ref(self):
3654 return self._source_ref
3655
3656 @source_ref.setter
3657 def source_ref(self, val):
3658 parts = (val or '').split(':')
3659 if len(parts) != 3:
3660 raise ValueError(
3661 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3662 self._source_ref = safe_unicode(val)
3663
3664 _target_ref = Column('other_ref', Unicode(255), nullable=False)
3665
3666 @hybrid_property
3667 def target_ref(self):
3668 return self._target_ref
3669
3670 @target_ref.setter
3671 def target_ref(self, val):
3672 parts = (val or '').split(':')
3673 if len(parts) != 3:
3674 raise ValueError(
3675 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3676 self._target_ref = safe_unicode(val)
3677
3678 @declared_attr
3679 def target_repo_id(cls):
3680 # TODO: dan: rename column to target_repo_id
3681 return Column(
3682 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3683 nullable=False)
3684
3685 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3686
3687 # TODO: dan: rename column to last_merge_source_rev
3688 _last_merge_source_rev = Column(
3689 'last_merge_org_rev', String(40), nullable=True)
3690 # TODO: dan: rename column to last_merge_target_rev
3691 _last_merge_target_rev = Column(
3692 'last_merge_other_rev', String(40), nullable=True)
3693 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3694 merge_rev = Column('merge_rev', String(40), nullable=True)
3695
3696 reviewer_data = Column(
3697 'reviewer_data_json', MutationObj.as_mutable(
3698 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3699
3700 @property
3701 def reviewer_data_json(self):
3702 return json.dumps(self.reviewer_data)
3703
3704 @hybrid_property
3705 def description_safe(self):
3706 from rhodecode.lib import helpers as h
3707 return h.escape(self.description)
3708
3709 @hybrid_property
3710 def revisions(self):
3711 return self._revisions.split(':') if self._revisions else []
3712
3713 @revisions.setter
3714 def revisions(self, val):
3715 self._revisions = ':'.join(val)
3716
3717 @hybrid_property
3718 def last_merge_status(self):
3719 return safe_int(self._last_merge_status)
3720
3721 @last_merge_status.setter
3722 def last_merge_status(self, val):
3723 self._last_merge_status = val
3724
3725 @declared_attr
3726 def author(cls):
3727 return relationship('User', lazy='joined')
3728
3729 @declared_attr
3730 def source_repo(cls):
3731 return relationship(
3732 'Repository',
3733 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3734
3735 @property
3736 def source_ref_parts(self):
3737 return self.unicode_to_reference(self.source_ref)
3738
3739 @declared_attr
3740 def target_repo(cls):
3741 return relationship(
3742 'Repository',
3743 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3744
3745 @property
3746 def target_ref_parts(self):
3747 return self.unicode_to_reference(self.target_ref)
3748
3749 @property
3750 def shadow_merge_ref(self):
3751 return self.unicode_to_reference(self._shadow_merge_ref)
3752
3753 @shadow_merge_ref.setter
3754 def shadow_merge_ref(self, ref):
3755 self._shadow_merge_ref = self.reference_to_unicode(ref)
3756
3757 @staticmethod
3758 def unicode_to_reference(raw):
3759 """
3760 Convert a unicode (or string) to a reference object.
3761 If unicode evaluates to False it returns None.
3762 """
3763 if raw:
3764 refs = raw.split(':')
3765 return Reference(*refs)
3766 else:
3767 return None
3768
3769 @staticmethod
3770 def reference_to_unicode(ref):
3771 """
3772 Convert a reference object to unicode.
3773 If reference is None it returns None.
3774 """
3775 if ref:
3776 return u':'.join(ref)
3777 else:
3778 return None
3779
3780 def get_api_data(self, with_merge_state=True):
3781 from rhodecode.model.pull_request import PullRequestModel
3782
3783 pull_request = self
3784 if with_merge_state:
3785 merge_status = PullRequestModel().merge_status(pull_request)
3786 merge_state = {
3787 'status': merge_status[0],
3788 'message': safe_unicode(merge_status[1]),
3789 }
3790 else:
3791 merge_state = {'status': 'not_available',
3792 'message': 'not_available'}
3793
3794 merge_data = {
3795 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3796 'reference': (
3797 pull_request.shadow_merge_ref._asdict()
3798 if pull_request.shadow_merge_ref else None),
3799 }
3800
3801 data = {
3802 'pull_request_id': pull_request.pull_request_id,
3803 'url': PullRequestModel().get_url(pull_request),
3804 'title': pull_request.title,
3805 'description': pull_request.description,
3806 'status': pull_request.status,
3807 'state': pull_request.pull_request_state,
3808 'created_on': pull_request.created_on,
3809 'updated_on': pull_request.updated_on,
3810 'commit_ids': pull_request.revisions,
3811 'review_status': pull_request.calculated_review_status(),
3812 'mergeable': merge_state,
3813 'source': {
3814 'clone_url': pull_request.source_repo.clone_url(),
3815 'repository': pull_request.source_repo.repo_name,
3816 'reference': {
3817 'name': pull_request.source_ref_parts.name,
3818 'type': pull_request.source_ref_parts.type,
3819 'commit_id': pull_request.source_ref_parts.commit_id,
3820 },
3821 },
3822 'target': {
3823 'clone_url': pull_request.target_repo.clone_url(),
3824 'repository': pull_request.target_repo.repo_name,
3825 'reference': {
3826 'name': pull_request.target_ref_parts.name,
3827 'type': pull_request.target_ref_parts.type,
3828 'commit_id': pull_request.target_ref_parts.commit_id,
3829 },
3830 },
3831 'merge': merge_data,
3832 'author': pull_request.author.get_api_data(include_secrets=False,
3833 details='basic'),
3834 'reviewers': [
3835 {
3836 'user': reviewer.get_api_data(include_secrets=False,
3837 details='basic'),
3838 'reasons': reasons,
3839 'review_status': st[0][1].status if st else 'not_reviewed',
3840 }
3841 for obj, reviewer, reasons, mandatory, st in
3842 pull_request.reviewers_statuses()
3843 ]
3844 }
3845
3846 return data
3847
3848 def set_state(self, pull_request_state, final_state=None):
3849 """
3850 # goes from initial state to updating to initial state.
3851 # initial state can be changed by specifying back_state=
3852 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
3853 pull_request.merge()
3854
3855 :param pull_request_state:
3856 :param final_state:
3857
3858 """
3859
3860 return _SetState(self, pull_request_state, back_state=final_state)
3861
3862
3863 class PullRequest(Base, _PullRequestBase):
3864 __tablename__ = 'pull_requests'
3865 __table_args__ = (
3866 base_table_args,
3867 )
3868
3869 pull_request_id = Column(
3870 'pull_request_id', Integer(), nullable=False, primary_key=True)
3871
3872 def __repr__(self):
3873 if self.pull_request_id:
3874 return '<DB:PullRequest #%s>' % self.pull_request_id
3875 else:
3876 return '<DB:PullRequest at %#x>' % id(self)
3877
3878 reviewers = relationship('PullRequestReviewers',
3879 cascade="all, delete, delete-orphan")
3880 statuses = relationship('ChangesetStatus',
3881 cascade="all, delete, delete-orphan")
3882 comments = relationship('ChangesetComment',
3883 cascade="all, delete, delete-orphan")
3884 versions = relationship('PullRequestVersion',
3885 cascade="all, delete, delete-orphan",
3886 lazy='dynamic')
3887
3888 @classmethod
3889 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3890 internal_methods=None):
3891
3892 class PullRequestDisplay(object):
3893 """
3894 Special object wrapper for showing PullRequest data via Versions
3895 It mimics PR object as close as possible. This is read only object
3896 just for display
3897 """
3898
3899 def __init__(self, attrs, internal=None):
3900 self.attrs = attrs
3901 # internal have priority over the given ones via attrs
3902 self.internal = internal or ['versions']
3903
3904 def __getattr__(self, item):
3905 if item in self.internal:
3906 return getattr(self, item)
3907 try:
3908 return self.attrs[item]
3909 except KeyError:
3910 raise AttributeError(
3911 '%s object has no attribute %s' % (self, item))
3912
3913 def __repr__(self):
3914 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3915
3916 def versions(self):
3917 return pull_request_obj.versions.order_by(
3918 PullRequestVersion.pull_request_version_id).all()
3919
3920 def is_closed(self):
3921 return pull_request_obj.is_closed()
3922
3923 @property
3924 def pull_request_version_id(self):
3925 return getattr(pull_request_obj, 'pull_request_version_id', None)
3926
3927 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3928
3929 attrs.author = StrictAttributeDict(
3930 pull_request_obj.author.get_api_data())
3931 if pull_request_obj.target_repo:
3932 attrs.target_repo = StrictAttributeDict(
3933 pull_request_obj.target_repo.get_api_data())
3934 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3935
3936 if pull_request_obj.source_repo:
3937 attrs.source_repo = StrictAttributeDict(
3938 pull_request_obj.source_repo.get_api_data())
3939 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3940
3941 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3942 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3943 attrs.revisions = pull_request_obj.revisions
3944
3945 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3946 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3947 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3948
3949 return PullRequestDisplay(attrs, internal=internal_methods)
3950
3951 def is_closed(self):
3952 return self.status == self.STATUS_CLOSED
3953
3954 def __json__(self):
3955 return {
3956 'revisions': self.revisions,
3957 }
3958
3959 def calculated_review_status(self):
3960 from rhodecode.model.changeset_status import ChangesetStatusModel
3961 return ChangesetStatusModel().calculated_review_status(self)
3962
3963 def reviewers_statuses(self):
3964 from rhodecode.model.changeset_status import ChangesetStatusModel
3965 return ChangesetStatusModel().reviewers_statuses(self)
3966
3967 @property
3968 def workspace_id(self):
3969 from rhodecode.model.pull_request import PullRequestModel
3970 return PullRequestModel()._workspace_id(self)
3971
3972 def get_shadow_repo(self):
3973 workspace_id = self.workspace_id
3974 vcs_obj = self.target_repo.scm_instance()
3975 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3976 self.target_repo.repo_id, workspace_id)
3977 if os.path.isdir(shadow_repository_path):
3978 return vcs_obj._get_shadow_instance(shadow_repository_path)
3979
3980
3981 class PullRequestVersion(Base, _PullRequestBase):
3982 __tablename__ = 'pull_request_versions'
3983 __table_args__ = (
3984 base_table_args,
3985 )
3986
3987 pull_request_version_id = Column(
3988 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3989 pull_request_id = Column(
3990 'pull_request_id', Integer(),
3991 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3992 pull_request = relationship('PullRequest')
3993
3994 def __repr__(self):
3995 if self.pull_request_version_id:
3996 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3997 else:
3998 return '<DB:PullRequestVersion at %#x>' % id(self)
3999
4000 @property
4001 def reviewers(self):
4002 return self.pull_request.reviewers
4003
4004 @property
4005 def versions(self):
4006 return self.pull_request.versions
4007
4008 def is_closed(self):
4009 # calculate from original
4010 return self.pull_request.status == self.STATUS_CLOSED
4011
4012 def calculated_review_status(self):
4013 return self.pull_request.calculated_review_status()
4014
4015 def reviewers_statuses(self):
4016 return self.pull_request.reviewers_statuses()
4017
4018
4019 class PullRequestReviewers(Base, BaseModel):
4020 __tablename__ = 'pull_request_reviewers'
4021 __table_args__ = (
4022 base_table_args,
4023 )
4024
4025 @hybrid_property
4026 def reasons(self):
4027 if not self._reasons:
4028 return []
4029 return self._reasons
4030
4031 @reasons.setter
4032 def reasons(self, val):
4033 val = val or []
4034 if any(not isinstance(x, compat.string_types) for x in val):
4035 raise Exception('invalid reasons type, must be list of strings')
4036 self._reasons = val
4037
4038 pull_requests_reviewers_id = Column(
4039 'pull_requests_reviewers_id', Integer(), nullable=False,
4040 primary_key=True)
4041 pull_request_id = Column(
4042 "pull_request_id", Integer(),
4043 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4044 user_id = Column(
4045 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4046 _reasons = Column(
4047 'reason', MutationList.as_mutable(
4048 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4049
4050 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4051 user = relationship('User')
4052 pull_request = relationship('PullRequest')
4053
4054 rule_data = Column(
4055 'rule_data_json',
4056 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4057
4058 def rule_user_group_data(self):
4059 """
4060 Returns the voting user group rule data for this reviewer
4061 """
4062
4063 if self.rule_data and 'vote_rule' in self.rule_data:
4064 user_group_data = {}
4065 if 'rule_user_group_entry_id' in self.rule_data:
4066 # means a group with voting rules !
4067 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4068 user_group_data['name'] = self.rule_data['rule_name']
4069 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4070
4071 return user_group_data
4072
4073 def __unicode__(self):
4074 return u"<%s('id:%s')>" % (self.__class__.__name__,
4075 self.pull_requests_reviewers_id)
4076
4077
4078 class Notification(Base, BaseModel):
4079 __tablename__ = 'notifications'
4080 __table_args__ = (
4081 Index('notification_type_idx', 'type'),
4082 base_table_args,
4083 )
4084
4085 TYPE_CHANGESET_COMMENT = u'cs_comment'
4086 TYPE_MESSAGE = u'message'
4087 TYPE_MENTION = u'mention'
4088 TYPE_REGISTRATION = u'registration'
4089 TYPE_PULL_REQUEST = u'pull_request'
4090 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4091
4092 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4093 subject = Column('subject', Unicode(512), nullable=True)
4094 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4095 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4096 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4097 type_ = Column('type', Unicode(255))
4098
4099 created_by_user = relationship('User')
4100 notifications_to_users = relationship('UserNotification', lazy='joined',
4101 cascade="all, delete, delete-orphan")
4102
4103 @property
4104 def recipients(self):
4105 return [x.user for x in UserNotification.query()\
4106 .filter(UserNotification.notification == self)\
4107 .order_by(UserNotification.user_id.asc()).all()]
4108
4109 @classmethod
4110 def create(cls, created_by, subject, body, recipients, type_=None):
4111 if type_ is None:
4112 type_ = Notification.TYPE_MESSAGE
4113
4114 notification = cls()
4115 notification.created_by_user = created_by
4116 notification.subject = subject
4117 notification.body = body
4118 notification.type_ = type_
4119 notification.created_on = datetime.datetime.now()
4120
4121 # For each recipient link the created notification to his account
4122 for u in recipients:
4123 assoc = UserNotification()
4124 assoc.user_id = u.user_id
4125 assoc.notification = notification
4126
4127 # if created_by is inside recipients mark his notification
4128 # as read
4129 if u.user_id == created_by.user_id:
4130 assoc.read = True
4131 Session().add(assoc)
4132
4133 Session().add(notification)
4134
4135 return notification
4136
4137
4138 class UserNotification(Base, BaseModel):
4139 __tablename__ = 'user_to_notification'
4140 __table_args__ = (
4141 UniqueConstraint('user_id', 'notification_id'),
4142 base_table_args
4143 )
4144
4145 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4146 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4147 read = Column('read', Boolean, default=False)
4148 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4149
4150 user = relationship('User', lazy="joined")
4151 notification = relationship('Notification', lazy="joined",
4152 order_by=lambda: Notification.created_on.desc(),)
4153
4154 def mark_as_read(self):
4155 self.read = True
4156 Session().add(self)
4157
4158
4159 class Gist(Base, BaseModel):
4160 __tablename__ = 'gists'
4161 __table_args__ = (
4162 Index('g_gist_access_id_idx', 'gist_access_id'),
4163 Index('g_created_on_idx', 'created_on'),
4164 base_table_args
4165 )
4166
4167 GIST_PUBLIC = u'public'
4168 GIST_PRIVATE = u'private'
4169 DEFAULT_FILENAME = u'gistfile1.txt'
4170
4171 ACL_LEVEL_PUBLIC = u'acl_public'
4172 ACL_LEVEL_PRIVATE = u'acl_private'
4173
4174 gist_id = Column('gist_id', Integer(), primary_key=True)
4175 gist_access_id = Column('gist_access_id', Unicode(250))
4176 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4177 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4178 gist_expires = Column('gist_expires', Float(53), nullable=False)
4179 gist_type = Column('gist_type', Unicode(128), nullable=False)
4180 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4181 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4182 acl_level = Column('acl_level', Unicode(128), nullable=True)
4183
4184 owner = relationship('User')
4185
4186 def __repr__(self):
4187 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4188
4189 @hybrid_property
4190 def description_safe(self):
4191 from rhodecode.lib import helpers as h
4192 return h.escape(self.gist_description)
4193
4194 @classmethod
4195 def get_or_404(cls, id_):
4196 from pyramid.httpexceptions import HTTPNotFound
4197
4198 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4199 if not res:
4200 raise HTTPNotFound()
4201 return res
4202
4203 @classmethod
4204 def get_by_access_id(cls, gist_access_id):
4205 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4206
4207 def gist_url(self):
4208 from rhodecode.model.gist import GistModel
4209 return GistModel().get_url(self)
4210
4211 @classmethod
4212 def base_path(cls):
4213 """
4214 Returns base path when all gists are stored
4215
4216 :param cls:
4217 """
4218 from rhodecode.model.gist import GIST_STORE_LOC
4219 q = Session().query(RhodeCodeUi)\
4220 .filter(RhodeCodeUi.ui_key == URL_SEP)
4221 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4222 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4223
4224 def get_api_data(self):
4225 """
4226 Common function for generating gist related data for API
4227 """
4228 gist = self
4229 data = {
4230 'gist_id': gist.gist_id,
4231 'type': gist.gist_type,
4232 'access_id': gist.gist_access_id,
4233 'description': gist.gist_description,
4234 'url': gist.gist_url(),
4235 'expires': gist.gist_expires,
4236 'created_on': gist.created_on,
4237 'modified_at': gist.modified_at,
4238 'content': None,
4239 'acl_level': gist.acl_level,
4240 }
4241 return data
4242
4243 def __json__(self):
4244 data = dict(
4245 )
4246 data.update(self.get_api_data())
4247 return data
4248 # SCM functions
4249
4250 def scm_instance(self, **kwargs):
4251 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4252 return get_vcs_instance(
4253 repo_path=safe_str(full_repo_path), create=False)
4254
4255
4256 class ExternalIdentity(Base, BaseModel):
4257 __tablename__ = 'external_identities'
4258 __table_args__ = (
4259 Index('local_user_id_idx', 'local_user_id'),
4260 Index('external_id_idx', 'external_id'),
4261 base_table_args
4262 )
4263
4264 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4265 external_username = Column('external_username', Unicode(1024), default=u'')
4266 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4267 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4268 access_token = Column('access_token', String(1024), default=u'')
4269 alt_token = Column('alt_token', String(1024), default=u'')
4270 token_secret = Column('token_secret', String(1024), default=u'')
4271
4272 @classmethod
4273 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4274 """
4275 Returns ExternalIdentity instance based on search params
4276
4277 :param external_id:
4278 :param provider_name:
4279 :return: ExternalIdentity
4280 """
4281 query = cls.query()
4282 query = query.filter(cls.external_id == external_id)
4283 query = query.filter(cls.provider_name == provider_name)
4284 if local_user_id:
4285 query = query.filter(cls.local_user_id == local_user_id)
4286 return query.first()
4287
4288 @classmethod
4289 def user_by_external_id_and_provider(cls, external_id, provider_name):
4290 """
4291 Returns User instance based on search params
4292
4293 :param external_id:
4294 :param provider_name:
4295 :return: User
4296 """
4297 query = User.query()
4298 query = query.filter(cls.external_id == external_id)
4299 query = query.filter(cls.provider_name == provider_name)
4300 query = query.filter(User.user_id == cls.local_user_id)
4301 return query.first()
4302
4303 @classmethod
4304 def by_local_user_id(cls, local_user_id):
4305 """
4306 Returns all tokens for user
4307
4308 :param local_user_id:
4309 :return: ExternalIdentity
4310 """
4311 query = cls.query()
4312 query = query.filter(cls.local_user_id == local_user_id)
4313 return query
4314
4315 @classmethod
4316 def load_provider_plugin(cls, plugin_id):
4317 from rhodecode.authentication.base import loadplugin
4318 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4319 auth_plugin = loadplugin(_plugin_id)
4320 return auth_plugin
4321
4322
4323 class Integration(Base, BaseModel):
4324 __tablename__ = 'integrations'
4325 __table_args__ = (
4326 base_table_args
4327 )
4328
4329 integration_id = Column('integration_id', Integer(), primary_key=True)
4330 integration_type = Column('integration_type', String(255))
4331 enabled = Column('enabled', Boolean(), nullable=False)
4332 name = Column('name', String(255), nullable=False)
4333 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4334 default=False)
4335
4336 settings = Column(
4337 'settings_json', MutationObj.as_mutable(
4338 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4339 repo_id = Column(
4340 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4341 nullable=True, unique=None, default=None)
4342 repo = relationship('Repository', lazy='joined')
4343
4344 repo_group_id = Column(
4345 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4346 nullable=True, unique=None, default=None)
4347 repo_group = relationship('RepoGroup', lazy='joined')
4348
4349 @property
4350 def scope(self):
4351 if self.repo:
4352 return repr(self.repo)
4353 if self.repo_group:
4354 if self.child_repos_only:
4355 return repr(self.repo_group) + ' (child repos only)'
4356 else:
4357 return repr(self.repo_group) + ' (recursive)'
4358 if self.child_repos_only:
4359 return 'root_repos'
4360 return 'global'
4361
4362 def __repr__(self):
4363 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4364
4365
4366 class RepoReviewRuleUser(Base, BaseModel):
4367 __tablename__ = 'repo_review_rules_users'
4368 __table_args__ = (
4369 base_table_args
4370 )
4371
4372 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4373 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4374 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4375 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4376 user = relationship('User')
4377
4378 def rule_data(self):
4379 return {
4380 'mandatory': self.mandatory
4381 }
4382
4383
4384 class RepoReviewRuleUserGroup(Base, BaseModel):
4385 __tablename__ = 'repo_review_rules_users_groups'
4386 __table_args__ = (
4387 base_table_args
4388 )
4389
4390 VOTE_RULE_ALL = -1
4391
4392 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4393 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4394 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4395 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4396 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4397 users_group = relationship('UserGroup')
4398
4399 def rule_data(self):
4400 return {
4401 'mandatory': self.mandatory,
4402 'vote_rule': self.vote_rule
4403 }
4404
4405 @property
4406 def vote_rule_label(self):
4407 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4408 return 'all must vote'
4409 else:
4410 return 'min. vote {}'.format(self.vote_rule)
4411
4412
4413 class RepoReviewRule(Base, BaseModel):
4414 __tablename__ = 'repo_review_rules'
4415 __table_args__ = (
4416 base_table_args
4417 )
4418
4419 repo_review_rule_id = Column(
4420 'repo_review_rule_id', Integer(), primary_key=True)
4421 repo_id = Column(
4422 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4423 repo = relationship('Repository', backref='review_rules')
4424
4425 review_rule_name = Column('review_rule_name', String(255))
4426 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4427 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4428 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4429
4430 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4431 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4432 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4433 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4434
4435 rule_users = relationship('RepoReviewRuleUser')
4436 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4437
4438 def _validate_pattern(self, value):
4439 re.compile('^' + glob2re(value) + '$')
4440
4441 @hybrid_property
4442 def source_branch_pattern(self):
4443 return self._branch_pattern or '*'
4444
4445 @source_branch_pattern.setter
4446 def source_branch_pattern(self, value):
4447 self._validate_pattern(value)
4448 self._branch_pattern = value or '*'
4449
4450 @hybrid_property
4451 def target_branch_pattern(self):
4452 return self._target_branch_pattern or '*'
4453
4454 @target_branch_pattern.setter
4455 def target_branch_pattern(self, value):
4456 self._validate_pattern(value)
4457 self._target_branch_pattern = value or '*'
4458
4459 @hybrid_property
4460 def file_pattern(self):
4461 return self._file_pattern or '*'
4462
4463 @file_pattern.setter
4464 def file_pattern(self, value):
4465 self._validate_pattern(value)
4466 self._file_pattern = value or '*'
4467
4468 def matches(self, source_branch, target_branch, files_changed):
4469 """
4470 Check if this review rule matches a branch/files in a pull request
4471
4472 :param source_branch: source branch name for the commit
4473 :param target_branch: target branch name for the commit
4474 :param files_changed: list of file paths changed in the pull request
4475 """
4476
4477 source_branch = source_branch or ''
4478 target_branch = target_branch or ''
4479 files_changed = files_changed or []
4480
4481 branch_matches = True
4482 if source_branch or target_branch:
4483 if self.source_branch_pattern == '*':
4484 source_branch_match = True
4485 else:
4486 if self.source_branch_pattern.startswith('re:'):
4487 source_pattern = self.source_branch_pattern[3:]
4488 else:
4489 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4490 source_branch_regex = re.compile(source_pattern)
4491 source_branch_match = bool(source_branch_regex.search(source_branch))
4492 if self.target_branch_pattern == '*':
4493 target_branch_match = True
4494 else:
4495 if self.target_branch_pattern.startswith('re:'):
4496 target_pattern = self.target_branch_pattern[3:]
4497 else:
4498 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4499 target_branch_regex = re.compile(target_pattern)
4500 target_branch_match = bool(target_branch_regex.search(target_branch))
4501
4502 branch_matches = source_branch_match and target_branch_match
4503
4504 files_matches = True
4505 if self.file_pattern != '*':
4506 files_matches = False
4507 if self.file_pattern.startswith('re:'):
4508 file_pattern = self.file_pattern[3:]
4509 else:
4510 file_pattern = glob2re(self.file_pattern)
4511 file_regex = re.compile(file_pattern)
4512 for filename in files_changed:
4513 if file_regex.search(filename):
4514 files_matches = True
4515 break
4516
4517 return branch_matches and files_matches
4518
4519 @property
4520 def review_users(self):
4521 """ Returns the users which this rule applies to """
4522
4523 users = collections.OrderedDict()
4524
4525 for rule_user in self.rule_users:
4526 if rule_user.user.active:
4527 if rule_user.user not in users:
4528 users[rule_user.user.username] = {
4529 'user': rule_user.user,
4530 'source': 'user',
4531 'source_data': {},
4532 'data': rule_user.rule_data()
4533 }
4534
4535 for rule_user_group in self.rule_user_groups:
4536 source_data = {
4537 'user_group_id': rule_user_group.users_group.users_group_id,
4538 'name': rule_user_group.users_group.users_group_name,
4539 'members': len(rule_user_group.users_group.members)
4540 }
4541 for member in rule_user_group.users_group.members:
4542 if member.user.active:
4543 key = member.user.username
4544 if key in users:
4545 # skip this member as we have him already
4546 # this prevents from override the "first" matched
4547 # users with duplicates in multiple groups
4548 continue
4549
4550 users[key] = {
4551 'user': member.user,
4552 'source': 'user_group',
4553 'source_data': source_data,
4554 'data': rule_user_group.rule_data()
4555 }
4556
4557 return users
4558
4559 def user_group_vote_rule(self, user_id):
4560
4561 rules = []
4562 if not self.rule_user_groups:
4563 return rules
4564
4565 for user_group in self.rule_user_groups:
4566 user_group_members = [x.user_id for x in user_group.users_group.members]
4567 if user_id in user_group_members:
4568 rules.append(user_group)
4569 return rules
4570
4571 def __repr__(self):
4572 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4573 self.repo_review_rule_id, self.repo)
4574
4575
4576 class ScheduleEntry(Base, BaseModel):
4577 __tablename__ = 'schedule_entries'
4578 __table_args__ = (
4579 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4580 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4581 base_table_args,
4582 )
4583
4584 schedule_types = ['crontab', 'timedelta', 'integer']
4585 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4586
4587 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4588 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4589 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4590
4591 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4592 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4593
4594 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4595 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4596
4597 # task
4598 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4599 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4600 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4601 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4602
4603 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4604 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4605
4606 @hybrid_property
4607 def schedule_type(self):
4608 return self._schedule_type
4609
4610 @schedule_type.setter
4611 def schedule_type(self, val):
4612 if val not in self.schedule_types:
4613 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4614 val, self.schedule_type))
4615
4616 self._schedule_type = val
4617
4618 @classmethod
4619 def get_uid(cls, obj):
4620 args = obj.task_args
4621 kwargs = obj.task_kwargs
4622 if isinstance(args, JsonRaw):
4623 try:
4624 args = json.loads(args)
4625 except ValueError:
4626 args = tuple()
4627
4628 if isinstance(kwargs, JsonRaw):
4629 try:
4630 kwargs = json.loads(kwargs)
4631 except ValueError:
4632 kwargs = dict()
4633
4634 dot_notation = obj.task_dot_notation
4635 val = '.'.join(map(safe_str, [
4636 sorted(dot_notation), args, sorted(kwargs.items())]))
4637 return hashlib.sha1(val).hexdigest()
4638
4639 @classmethod
4640 def get_by_schedule_name(cls, schedule_name):
4641 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4642
4643 @classmethod
4644 def get_by_schedule_id(cls, schedule_id):
4645 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4646
4647 @property
4648 def task(self):
4649 return self.task_dot_notation
4650
4651 @property
4652 def schedule(self):
4653 from rhodecode.lib.celerylib.utils import raw_2_schedule
4654 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4655 return schedule
4656
4657 @property
4658 def args(self):
4659 try:
4660 return list(self.task_args or [])
4661 except ValueError:
4662 return list()
4663
4664 @property
4665 def kwargs(self):
4666 try:
4667 return dict(self.task_kwargs or {})
4668 except ValueError:
4669 return dict()
4670
4671 def _as_raw(self, val):
4672 if hasattr(val, 'de_coerce'):
4673 val = val.de_coerce()
4674 if val:
4675 val = json.dumps(val)
4676
4677 return val
4678
4679 @property
4680 def schedule_definition_raw(self):
4681 return self._as_raw(self.schedule_definition)
4682
4683 @property
4684 def args_raw(self):
4685 return self._as_raw(self.task_args)
4686
4687 @property
4688 def kwargs_raw(self):
4689 return self._as_raw(self.task_kwargs)
4690
4691 def __repr__(self):
4692 return '<DB:ScheduleEntry({}:{})>'.format(
4693 self.schedule_entry_id, self.schedule_name)
4694
4695
4696 @event.listens_for(ScheduleEntry, 'before_update')
4697 def update_task_uid(mapper, connection, target):
4698 target.task_uid = ScheduleEntry.get_uid(target)
4699
4700
4701 @event.listens_for(ScheduleEntry, 'before_insert')
4702 def set_task_uid(mapper, connection, target):
4703 target.task_uid = ScheduleEntry.get_uid(target)
4704
4705
4706 class _BaseBranchPerms(BaseModel):
4707 @classmethod
4708 def compute_hash(cls, value):
4709 return sha1_safe(value)
4710
4711 @hybrid_property
4712 def branch_pattern(self):
4713 return self._branch_pattern or '*'
4714
4715 @hybrid_property
4716 def branch_hash(self):
4717 return self._branch_hash
4718
4719 def _validate_glob(self, value):
4720 re.compile('^' + glob2re(value) + '$')
4721
4722 @branch_pattern.setter
4723 def branch_pattern(self, value):
4724 self._validate_glob(value)
4725 self._branch_pattern = value or '*'
4726 # set the Hash when setting the branch pattern
4727 self._branch_hash = self.compute_hash(self._branch_pattern)
4728
4729 def matches(self, branch):
4730 """
4731 Check if this the branch matches entry
4732
4733 :param branch: branch name for the commit
4734 """
4735
4736 branch = branch or ''
4737
4738 branch_matches = True
4739 if branch:
4740 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4741 branch_matches = bool(branch_regex.search(branch))
4742
4743 return branch_matches
4744
4745
4746 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
4747 __tablename__ = 'user_to_repo_branch_permissions'
4748 __table_args__ = (
4749 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4750 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4751 )
4752
4753 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
4754
4755 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
4756 repo = relationship('Repository', backref='user_branch_perms')
4757
4758 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
4759 permission = relationship('Permission')
4760
4761 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
4762 user_repo_to_perm = relationship('UserRepoToPerm')
4763
4764 rule_order = Column('rule_order', Integer(), nullable=False)
4765 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
4766 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
4767
4768 def __unicode__(self):
4769 return u'<UserBranchPermission(%s => %r)>' % (
4770 self.user_repo_to_perm, self.branch_pattern)
4771
4772
4773 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
4774 __tablename__ = 'user_group_to_repo_branch_permissions'
4775 __table_args__ = (
4776 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4777 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4778 )
4779
4780 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
4781
4782 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
4783 repo = relationship('Repository', backref='user_group_branch_perms')
4784
4785 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
4786 permission = relationship('Permission')
4787
4788 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
4789 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
4790
4791 rule_order = Column('rule_order', Integer(), nullable=False)
4792 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
4793 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
4794
4795 def __unicode__(self):
4796 return u'<UserBranchPermission(%s => %r)>' % (
4797 self.user_group_repo_to_perm, self.branch_pattern)
4798
4799
4800 class UserBookmark(Base, BaseModel):
4801 __tablename__ = 'user_bookmarks'
4802 __table_args__ = (
4803 UniqueConstraint('user_id', 'bookmark_repo_id'),
4804 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
4805 UniqueConstraint('user_id', 'bookmark_position'),
4806 base_table_args
4807 )
4808
4809 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
4810 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
4811 position = Column("bookmark_position", Integer(), nullable=False)
4812 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
4813 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
4814 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4815
4816 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
4817 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
4818
4819 user = relationship("User")
4820
4821 repository = relationship("Repository")
4822 repository_group = relationship("RepoGroup")
4823
4824 @classmethod
4825 def get_by_position_for_user(cls, position, user_id):
4826 return cls.query() \
4827 .filter(UserBookmark.user_id == user_id) \
4828 .filter(UserBookmark.position == position).scalar()
4829
4830 @classmethod
4831 def get_bookmarks_for_user(cls, user_id):
4832 return cls.query() \
4833 .filter(UserBookmark.user_id == user_id) \
4834 .options(joinedload(UserBookmark.repository)) \
4835 .options(joinedload(UserBookmark.repository_group)) \
4836 .order_by(UserBookmark.position.asc()) \
4837 .all()
4838
4839 def __unicode__(self):
4840 return u'<UserBookmark(%d @ %r)>' % (self.position, self.redirect_url)
4841
4842
4843 class FileStore(Base, BaseModel):
4844 __tablename__ = 'file_store'
4845 __table_args__ = (
4846 base_table_args
4847 )
4848
4849 file_store_id = Column('file_store_id', Integer(), primary_key=True)
4850 file_uid = Column('file_uid', String(1024), nullable=False)
4851 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
4852 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
4853 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
4854
4855 # sha256 hash
4856 file_hash = Column('file_hash', String(512), nullable=False)
4857 file_size = Column('file_size', Integer(), nullable=False)
4858
4859 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4860 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
4861 accessed_count = Column('accessed_count', Integer(), default=0)
4862
4863 enabled = Column('enabled', Boolean(), nullable=False, default=True)
4864
4865 # if repo/repo_group reference is set, check for permissions
4866 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
4867
4868 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4869 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
4870
4871 # scope limited to user, which requester have access to
4872 scope_user_id = Column(
4873 'scope_user_id', Integer(), ForeignKey('users.user_id'),
4874 nullable=True, unique=None, default=None)
4875 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
4876
4877 # scope limited to user group, which requester have access to
4878 scope_user_group_id = Column(
4879 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
4880 nullable=True, unique=None, default=None)
4881 user_group = relationship('UserGroup', lazy='joined')
4882
4883 # scope limited to repo, which requester have access to
4884 scope_repo_id = Column(
4885 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4886 nullable=True, unique=None, default=None)
4887 repo = relationship('Repository', lazy='joined')
4888
4889 # scope limited to repo group, which requester have access to
4890 scope_repo_group_id = Column(
4891 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
4892 nullable=True, unique=None, default=None)
4893 repo_group = relationship('RepoGroup', lazy='joined')
4894
4895 def __repr__(self):
4896 return '<FileStore({})>'.format(self.file_store_id)
4897
4898
4899 class DbMigrateVersion(Base, BaseModel):
4900 __tablename__ = 'db_migrate_version'
4901 __table_args__ = (
4902 base_table_args,
4903 )
4904
4905 repository_id = Column('repository_id', String(250), primary_key=True)
4906 repository_path = Column('repository_path', Text)
4907 version = Column('version', Integer)
4908
4909 @classmethod
4910 def set_version(cls, version):
4911 """
4912 Helper for forcing a different version, usually for debugging purposes via ishell.
4913 """
4914 ver = DbMigrateVersion.query().first()
4915 ver.version = version
4916 Session().commit()
4917
4918
4919 class DbSession(Base, BaseModel):
4920 __tablename__ = 'db_session'
4921 __table_args__ = (
4922 base_table_args,
4923 )
4924
4925 def __repr__(self):
4926 return '<DB:DbSession({})>'.format(self.id)
4927
4928 id = Column('id', Integer())
4929 namespace = Column('namespace', String(255), primary_key=True)
4930 accessed = Column('accessed', DateTime, nullable=False)
4931 created = Column('created', DateTime, nullable=False)
4932 data = Column('data', PickleType, nullable=False)
@@ -0,0 +1,37 b''
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_13_0_0 as db
18
19 pull_request = db.PullRequest.__table__
20 pull_request_version = db.PullRequestVersion.__table__
21
22 repo_state_1 = Column("pull_request_state", String(255), nullable=True)
23 repo_state_1.create(table=pull_request)
24
25 repo_state_2 = Column("pull_request_state", String(255), nullable=True)
26 repo_state_2.create(table=pull_request_version)
27
28 fixups(db, meta.Session)
29
30
31 def downgrade(migrate_engine):
32 meta = MetaData()
33 meta.bind = migrate_engine
34
35
36 def fixups(models, _SESSION):
37 pass
@@ -0,0 +1,41 b''
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_16_0_0 as db
18
19 fixups(db, meta.Session)
20
21
22 def downgrade(migrate_engine):
23 meta = MetaData()
24 meta.bind = migrate_engine
25
26
27 def fixups(models, _SESSION):
28 # move the builtin token to external tokens
29
30 log.info('Updating pull request pull_request_state to %s',
31 models.PullRequest.STATE_CREATED)
32 qry = _SESSION().query(models.PullRequest)
33 qry.update({"pull_request_state": models.PullRequest.STATE_CREATED})
34 _SESSION().commit()
35
36 log.info('Updating pull_request_version pull_request_state to %s',
37 models.PullRequest.STATE_CREATED)
38 qry = _SESSION().query(models.PullRequestVersion)
39 qry.update({"pull_request_state": models.PullRequest.STATE_CREATED})
40 _SESSION().commit()
41
@@ -0,0 +1,30 b''
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_16_0_1 as db
18
19 db.UserBookmark.__table__.create()
20
21 fixups(db, meta.Session)
22
23
24 def downgrade(migrate_engine):
25 meta = MetaData()
26 meta.bind = migrate_engine
27
28
29 def fixups(models, _SESSION):
30 pass
@@ -0,0 +1,30 b''
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_16_0_2 as db
18
19 db.FileStore.__table__.create()
20
21 fixups(db, meta.Session)
22
23
24 def downgrade(migrate_engine):
25 meta = MetaData()
26 meta.bind = migrate_engine
27
28
29 def fixups(models, _SESSION):
30 pass
@@ -0,0 +1,197 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import re
21
22 import pygments.filter
23 import pygments.filters
24 from pygments.token import Comment
25
26 HL_BEG_MARKER = '__RCSearchHLMarkBEG__'
27 HL_END_MARKER = '__RCSearchHLMarkEND__'
28 HL_MARKER_RE = '{}(.*?){}'.format(HL_BEG_MARKER, HL_END_MARKER)
29
30
31 class ElasticSearchHLFilter(pygments.filters.Filter):
32 _names = [HL_BEG_MARKER, HL_END_MARKER]
33
34 def __init__(self, **options):
35 pygments.filters.Filter.__init__(self, **options)
36
37 def filter(self, lexer, stream):
38 def tokenize(_value):
39 for token in re.split('({}|{})'.format(
40 self._names[0], self._names[1]), _value):
41 if token:
42 yield token
43
44 hl = False
45 for ttype, value in stream:
46
47 if self._names[0] in value or self._names[1] in value:
48 for item in tokenize(value):
49 if item == self._names[0]:
50 # skip marker, but start HL
51 hl = True
52 continue
53 elif item == self._names[1]:
54 hl = False
55 continue
56
57 if hl:
58 yield Comment.ElasticMatch, item
59 else:
60 yield ttype, item
61 else:
62 if hl:
63 yield Comment.ElasticMatch, value
64 else:
65 yield ttype, value
66
67
68 def extract_phrases(text_query):
69 """
70 Extracts phrases from search term string making sure phrases
71 contained in double quotes are kept together - and discarding empty values
72 or fully whitespace values eg.
73
74 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
75
76 """
77
78 in_phrase = False
79 buf = ''
80 phrases = []
81 for char in text_query:
82 if in_phrase:
83 if char == '"': # end phrase
84 phrases.append(buf)
85 buf = ''
86 in_phrase = False
87 continue
88 else:
89 buf += char
90 continue
91 else:
92 if char == '"': # start phrase
93 in_phrase = True
94 phrases.append(buf)
95 buf = ''
96 continue
97 elif char == ' ':
98 phrases.append(buf)
99 buf = ''
100 continue
101 else:
102 buf += char
103
104 phrases.append(buf)
105 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
106 return phrases
107
108
109 def get_matching_phrase_offsets(text, phrases):
110 """
111 Returns a list of string offsets in `text` that the list of `terms` match
112
113 >>> get_matching_phrase_offsets('some text here', ['some', 'here'])
114 [(0, 4), (10, 14)]
115
116 """
117 phrases = phrases or []
118 offsets = []
119
120 for phrase in phrases:
121 for match in re.finditer(phrase, text):
122 offsets.append((match.start(), match.end()))
123
124 return offsets
125
126
127 def get_matching_markers_offsets(text, markers=None):
128 """
129 Returns a list of string offsets in `text` that the are between matching markers
130
131 >>> get_matching_markers_offsets('$1some$2 text $1here$2 marked', ['\$1(.*?)\$2'])
132 [(0, 5), (16, 22)]
133
134 """
135 markers = markers or [HL_MARKER_RE]
136 offsets = []
137
138 if markers:
139 for mark in markers:
140 for match in re.finditer(mark, text):
141 offsets.append((match.start(), match.end()))
142
143 return offsets
144
145
146 def normalize_text_for_matching(x):
147 """
148 Replaces all non alfanum characters to spaces and lower cases the string,
149 useful for comparing two text strings without punctuation
150 """
151 return re.sub(r'[^\w]', ' ', x.lower())
152
153
154 def get_matching_line_offsets(lines, terms=None, markers=None):
155 """ Return a set of `lines` indices (starting from 1) matching a
156 text search query, along with `context` lines above/below matching lines
157
158 :param lines: list of strings representing lines
159 :param terms: search term string to match in lines eg. 'some text'
160 :param markers: instead of terms, use highlight markers instead that
161 mark beginning and end for matched item. eg. ['START(.*?)END']
162
163 eg.
164
165 text = '''
166 words words words
167 words words words
168 some text some
169 words words words
170 words words words
171 text here what
172 '''
173 get_matching_line_offsets(text, 'text', context=1)
174 6, {3: [(5, 9)], 6: [(0, 4)]]
175
176 """
177 matching_lines = {}
178 line_index = 0
179
180 if terms:
181 phrases = [normalize_text_for_matching(phrase)
182 for phrase in extract_phrases(terms)]
183
184 for line_index, line in enumerate(lines.splitlines(), start=1):
185 normalized_line = normalize_text_for_matching(line)
186 match_offsets = get_matching_phrase_offsets(normalized_line, phrases)
187 if match_offsets:
188 matching_lines[line_index] = match_offsets
189
190 else:
191 markers = markers or [HL_MARKER_RE]
192 for line_index, line in enumerate(lines.splitlines(), start=1):
193 match_offsets = get_matching_markers_offsets(line, markers=markers)
194 if match_offsets:
195 matching_lines[line_index] = match_offsets
196
197 return line_index, matching_lines
@@ -0,0 +1,222 b''
1 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
2
3 <%def name="form_item(count, position=None, title=None, redirect_url=None, repo=None, repo_group=None)">
4 <tr>
5 <td class="td-align-top" >
6 <div class="label">
7 <label for="position">${_('Position')}:</label>
8 </div>
9 <div class="input">
10 <input type="text" name="position" value="${position or count}" style="width: 40px"/>
11 </div>
12 </td>
13
14 <td>
15 <div class="label">
16 <label for="title">${_('Bookmark title (max 30 characters, optional)')}:</label>
17 </div>
18 <div class="input">
19 <input type="text" name="title" value="${title}" style="width: 300px" maxlength="30"/>
20
21 <div class="field pull-right">
22 <div>
23 <label class="btn-link btn-danger">${_('Clear')}:</label>
24 ${h.checkbox('remove', value=True)}
25 </div>
26 </div>
27 </div>
28
29 <div class="label">
30 <label for="redirect_url">${_('Redirect URL')}:</label>
31 </div>
32 <div class="input">
33 <input type="text" name="redirect_url" value="${redirect_url}" style="width: 600px"/>
34 </div>
35
36
37 <div class="select">
38 % if repo:
39 <div class="label">
40 <label for="redirect_url">${_('Repository template')}:</label>
41 </div>
42 ${dt.repo_name(name=repo.repo_name, rtype=repo.repo_type,rstate=None,private=None,archived=False,fork_of=False)}
43 ${h.hidden('bookmark_repo', repo.repo_id)}
44 % elif repo_group:
45 <div class="label">
46 <label for="redirect_url">${_('Repository group template')}:</label>
47 </div>
48 ${dt.repo_group_name(repo_group.group_name)}
49 ${h.hidden('bookmark_repo_group', repo_group.group_id)}
50 % else:
51 <div class="label">
52 <label for="redirect_url">${_('Template Repository or Repository group')}:</label>
53 </div>
54 ${h.hidden('bookmark_repo', class_='bookmark_repo')}
55 <span style="padding-right:15px">OR</span>
56 ${h.hidden('bookmark_repo_group', class_='bookmark_repo_group')}
57 % endif
58 </div>
59
60 <p class="help-block help-block-inline" >
61 % if repo:
62 ${_('Available as ${repo_url} e.g. Redirect url: ${repo_url}/changelog')}
63 % elif repo_group:
64 ${_('Available as ${repo_group_url} e.g. Redirect url: ${repo_group_url}')}
65 % else:
66 ${_('Available as full url variables in redirect url. i.e: ${repo_url}, ${repo_group_url}.')}
67 % endif
68 </p>
69 </td>
70
71 </tr>
72 </%def>
73
74 <div class="panel panel-default">
75 <div class="panel-heading">
76 <h3 class="panel-title">${_('Your Bookmarks')}</h3>
77 </div>
78
79 <div class="panel-body">
80 <p>
81 ${_('Store upto 10 bookmark links to favorite repositories, external issue tracker or CI server. ')}
82 <br/>
83 ${_('Bookmarks are accessible from your username dropdown or by keyboard shortcut `g 0-9`')}
84 </p>
85
86 ${h.secure_form(h.route_path('my_account_bookmarks_update'), request=request)}
87 <div class="form-vertical">
88 <table class="rctable">
89 ## generate always 10 entries
90 <input type="hidden" name="__start__" value="bookmarks:sequence"/>
91 % for cnt, item in enumerate((c.bookmark_items + [None for i in range(10)])[:10]):
92 <input type="hidden" name="__start__" value="bookmark:mapping"/>
93 % if item is None:
94 ## empty placehodlder
95 ${form_item(cnt)}
96 % else:
97 ## actual entry
98 ${form_item(cnt, position=item.position, title=item.title, redirect_url=item.redirect_url, repo=item.repository, repo_group=item.repository_group)}
99 % endif
100 <input type="hidden" name="__end__" value="bookmark:mapping"/>
101 % endfor
102 <input type="hidden" name="__end__" value="bookmarks:sequence"/>
103 </table>
104 <div class="buttons">
105 ${h.submit('save',_('Save'),class_="btn")}
106 </div>
107 </div>
108 ${h.end_form()}
109 </div>
110 </div>
111
112 <script>
113 $(document).ready(function(){
114
115
116 var repoFilter = function (data) {
117 var results = [];
118
119 if (!data.results[0]) {
120 return data
121 }
122
123 $.each(data.results[0].children, function () {
124 // replace name to ID for submision
125 this.id = this.repo_id;
126 results.push(this);
127 });
128
129 data.results[0].children = results;
130 return data;
131 };
132
133
134 $(".bookmark_repo").select2({
135 cachedDataSource: {},
136 minimumInputLength: 2,
137 placeholder: "${_('repository')}",
138 dropdownAutoWidth: true,
139 containerCssClass: "drop-menu",
140 dropdownCssClass: "drop-menu-dropdown",
141 formatResult: formatRepoResult,
142 query: $.debounce(250, function (query) {
143 self = this;
144 var cacheKey = query.term;
145 var cachedData = self.cachedDataSource[cacheKey];
146
147 if (cachedData) {
148 query.callback({results: cachedData.results});
149 } else {
150 $.ajax({
151 url: pyroutes.url('repo_list_data'),
152 data: {'query': query.term},
153 dataType: 'json',
154 type: 'GET',
155 success: function (data) {
156 data = repoFilter(data);
157 self.cachedDataSource[cacheKey] = data;
158 query.callback({results: data.results});
159 },
160 error: function (data, textStatus, errorThrown) {
161 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
162 }
163 })
164 }
165 }),
166 });
167
168 var repoGroupFilter = function (data) {
169 var results = [];
170
171 if (!data.results[0]) {
172 return data
173 }
174
175 $.each(data.results[0].children, function () {
176 // replace name to ID for submision
177 this.id = this.repo_group_id;
178 results.push(this);
179 });
180
181 data.results[0].children = results;
182 return data;
183 };
184
185 $(".bookmark_repo_group").select2({
186 cachedDataSource: {},
187 minimumInputLength: 2,
188 placeholder: "${_('repository group')}",
189 dropdownAutoWidth: true,
190 containerCssClass: "drop-menu",
191 dropdownCssClass: "drop-menu-dropdown",
192 formatResult: formatRepoGroupResult,
193 query: $.debounce(250, function (query) {
194 self = this;
195 var cacheKey = query.term;
196 var cachedData = self.cachedDataSource[cacheKey];
197
198 if (cachedData) {
199 query.callback({results: cachedData.results});
200 } else {
201 $.ajax({
202 url: pyroutes.url('repo_group_list_data'),
203 data: {'query': query.term},
204 dataType: 'json',
205 type: 'GET',
206 success: function (data) {
207 data = repoGroupFilter(data);
208 self.cachedDataSource[cacheKey] = data;
209 query.callback({results: data.results});
210 },
211 error: function (data, textStatus, errorThrown) {
212 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
213 }
214 })
215 }
216 })
217 });
218
219
220 });
221
222 </script>
@@ -0,0 +1,100 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import copy
22 import mock
23 import pytest
24
25 from rhodecode.lib.index import search_utils
26
27
28 @pytest.mark.parametrize('test_text, expected_output', [
29 ('some text', ['some', 'text']),
30 ('some text', ['some', 'text']),
31 ('some text "with a phrase"', ['some', 'text', 'with a phrase']),
32 ('"a phrase" "another phrase"', ['a phrase', 'another phrase']),
33 ('"justphrase"', ['justphrase']),
34 ('""', []),
35 ('', []),
36 (' ', []),
37 ('" "', []),
38 ])
39 def test_extract_phrases(test_text, expected_output):
40 assert search_utils.extract_phrases(test_text) == expected_output
41
42
43 @pytest.mark.parametrize('test_text, text_phrases, expected_output', [
44 ('some text here', ['some', 'here'], [(0, 4), (10, 14)]),
45 ('here here there', ['here'], [(0, 4), (5, 9), (11, 15)]),
46 ('irrelevant', ['not found'], []),
47 ('irrelevant', ['not found'], []),
48 ])
49 def test_get_matching_phrase_offsets(test_text, text_phrases, expected_output):
50 assert search_utils.get_matching_phrase_offsets(
51 test_text, text_phrases) == expected_output
52
53
54 @pytest.mark.parametrize('test_text, text_phrases, expected_output', [
55 ('__RCSearchHLMarkBEG__some__RCSearchHLMarkEND__ text __RCSearchHLMarkBEG__here__RCSearchHLMarkEND__', [], [(0, 46), (52, 98)]),
56 ('__RCSearchHLMarkBEG__here__RCSearchHLMarkEND__ __RCSearchHLMarkBEG__here__RCSearchHLMarkEND__ there', [], [(0, 46), (47, 93)]),
57 ('some text __RCSearchHLMarkBEG__here__RCSearchHLMarkEND__', [], [(10, 56)]),
58 ('__RCSearchHLMarkBEG__here__RCSearchHLMarkEND__ __RCSearchHLMarkBEG__here__RCSearchHLMarkEND__ __RCSearchHLMarkBEG__there__RCSearchHLMarkEND__', [], [(0, 46), (47, 93), (94, 141)]),
59 ('irrelevant', ['not found'], []),
60 ('irrelevant', ['not found'], []),
61 ])
62 def test_get_matching_marker_offsets(test_text, text_phrases, expected_output):
63
64 assert search_utils.get_matching_markers_offsets(test_text) == expected_output
65
66
67 def test_normalize_text_for_matching():
68 assert search_utils.normalize_text_for_matching(
69 'OJjfe)*#$*@)$JF*)3r2f80h') == 'ojjfe jf 3r2f80h'
70
71
72 def test_get_matching_line_offsets():
73 words = '\n'.join([
74 'words words words',
75 'words words words',
76 'some text some',
77 'words words words',
78 'words words words',
79 'text here what'
80 ])
81 total_lines, matched_offsets = \
82 search_utils.get_matching_line_offsets(words, terms='text')
83 assert total_lines == 6
84 assert matched_offsets == {3: [(5, 9)], 6: [(0, 4)]}
85
86
87 def test_get_matching_line_offsets_using_markers():
88 words = '\n'.join([
89 'words words words',
90 'words words words',
91 'some __1__text__2__ some',
92 'words words words',
93 'words words words',
94 '__1__text__2__ here what'
95 ])
96 total_lines, matched_offsets = \
97 search_utils.get_matching_line_offsets(words, terms=None,
98 markers=['__1__(.*?)__2__'])
99 assert total_lines == 6
100 assert matched_offsets == {3: [(5, 19)], 6: [(0, 14)]}
@@ -1,5 +1,5 b''
1 1 [bumpversion]
2 current_version = 4.15.2
2 current_version = 4.16.0
3 3 message = release: Bump version {current_version} to {new_version}
4 4
5 5 [bumpversion:file:rhodecode/VERSION]
@@ -5,25 +5,20 b' done = false'
5 5 done = true
6 6
7 7 [task:rc_tools_pinned]
8 done = true
9 8
10 9 [task:fixes_on_stable]
11 done = true
12 10
13 11 [task:pip2nix_generated]
14 done = true
15 12
16 13 [task:changelog_updated]
17 done = true
18 14
19 15 [task:generate_api_docs]
20 done = true
16
17 [task:updated_translation]
21 18
22 19 [release]
23 state = prepared
24 version = 4.15.2
25
26 [task:updated_translation]
20 state = in_progress
21 version = 4.16.0
27 22
28 23 [task:generate_js_routes]
29 24
@@ -12,8 +12,6 b' permission notice:'
12 12 file:licenses/msgpack_license.txt
13 13 Copyright (c) 2009 - tornado
14 14 file:licenses/tornado_license.txt
15 Copyright (c) 2015 - pygments-markdown-lexer
16 file:licenses/pygments_markdown_lexer_license.txt
17 15 Copyright 2006 - diff_match_patch
18 16 file:licenses/diff_match_patch_license.txt
19 17
@@ -1,5 +1,5 b''
1 1
2 .PHONY: clean docs docs-clean docs-cleanup test test-clean test-only test-only-postgres test-only-mysql web-build
2 .PHONY: clean docs docs-clean docs-cleanup test test-clean test-only test-only-postgres test-only-mysql web-build generate-pkgs
3 3
4 4 NODE_PATH=./node_modules
5 5 WEBPACK=./node_binaries/webpack
@@ -8,7 +8,7 b' GRUNT=./node_binaries/grunt'
8 8
9 9 clean:
10 10 make test-clean
11 find . -type f \( -iname '*.c' -o -iname '*.pyc' -o -iname '*.so' \) -exec rm '{}' ';'
11 find . -type f \( -iname '*.c' -o -iname '*.pyc' -o -iname '*.so' -o -iname '*.orig' \) -exec rm '{}' ';'
12 12
13 13 test:
14 14 make test-clean
@@ -51,3 +51,5 b' docs-cleanup:'
51 51 web-build:
52 52 NODE_PATH=$(NODE_PATH) $(GRUNT)
53 53
54 generate-pkgs:
55 nix-shell pkgs/shell-generate.nix --command "pip2nix generate --licenses"
@@ -53,7 +53,7 b' asyncore_use_poll = true'
53 53 ## run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
54 54
55 55 #use = egg:gunicorn#main
56 ## Sets the number of process workers. More workers means more concurent connections
56 ## Sets the number of process workers. More workers means more concurrent connections
57 57 ## RhodeCode can handle at the same time. Each additional worker also it increases
58 58 ## memory usage as each has it's own set of caches.
59 59 ## Recommended value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers, but no more
@@ -133,10 +133,10 b' rhodecode.api.url = /_admin/api'
133 133 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
134 134 #rhodecode.encrypted_values.strict = false
135 135
136 ## return gzipped responses from Rhodecode (static files/application)
136 ## return gzipped responses from RhodeCode (static files/application)
137 137 gzip_responses = false
138 138
139 ## autogenerate javascript routes file on startup
139 ## auto-generate javascript routes file on startup
140 140 generate_js_files = false
141 141
142 142 ## System global default language.
@@ -153,7 +153,7 b' startup.import_repos = false'
153 153 ## the repository.
154 154 #archive_cache_dir = /tmp/tarballcache
155 155
156 ## URL at which the application is running. This is used for bootstraping
156 ## URL at which the application is running. This is used for Bootstrapping
157 157 ## requests in context when no web request is available. Used in ishell, or
158 158 ## SSH calls. Set this for events to receive proper url for SSH calls.
159 159 app.base_url = http://rhodecode.local
@@ -203,7 +203,7 b' gist_alias_url ='
203 203 ## used for access.
204 204 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
205 205 ## came from the the logged in user who own this authentication token.
206 ## Additionally @TOKEN syntaxt can be used to bound the view to specific
206 ## Additionally @TOKEN syntax can be used to bound the view to specific
207 207 ## authentication token. Such view would be only accessible when used together
208 208 ## with this authentication token
209 209 ##
@@ -227,14 +227,14 b' default_encoding = UTF-8'
227 227
228 228 ## instance-id prefix
229 229 ## a prefix key for this instance used for cache invalidation when running
230 ## multiple instances of rhodecode, make sure it's globally unique for
231 ## all running rhodecode instances. Leave empty if you don't use it
230 ## multiple instances of RhodeCode, make sure it's globally unique for
231 ## all running RhodeCode instances. Leave empty if you don't use it
232 232 instance_id =
233 233
234 234 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
235 235 ## of an authentication plugin also if it is disabled by it's settings.
236 236 ## This could be useful if you are unable to log in to the system due to broken
237 ## authentication settings. Then you can enable e.g. the internal rhodecode auth
237 ## authentication settings. Then you can enable e.g. the internal RhodeCode auth
238 238 ## module to log in again and fix the settings.
239 239 ##
240 240 ## Available builtin plugin IDs (hash is part of the ID):
@@ -250,7 +250,7 b' instance_id ='
250 250 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
251 251 ## handling that causing a series of failed authentication calls.
252 252 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
253 ## This will be served instead of default 401 on bad authnetication
253 ## This will be served instead of default 401 on bad authentication
254 254 auth_ret_code =
255 255
256 256 ## use special detection method when serving auth_ret_code, instead of serving
@@ -284,6 +284,13 b' labs_settings_active = true'
284 284 ## This is used to store exception from RhodeCode in shared directory
285 285 #exception_tracker.store_path =
286 286
287 ## File store configuration. This is used to store and serve uploaded files
288 file_store.enabled = true
289 ## Storage backend, available options are: local
290 file_store.backend = local
291 ## path to store the uploaded binaries
292 file_store.storage_path = %(here)s/data/file_store
293
287 294
288 295 ####################################
289 296 ### CELERY CONFIG ####
@@ -325,6 +332,7 b' rc_cache.cache_perms.expiration_time = 3'
325 332 #rc_cache.cache_perms.arguments.host = localhost
326 333 #rc_cache.cache_perms.arguments.port = 6379
327 334 #rc_cache.cache_perms.arguments.db = 0
335 ## more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
328 336 #rc_cache.cache_perms.arguments.distributed_lock = true
329 337
330 338 ## `cache_repo` cache settings for FileTree, Readme, RSS FEEDS
@@ -340,6 +348,7 b' rc_cache.cache_repo.expiration_time = 25'
340 348 #rc_cache.cache_repo.arguments.host = localhost
341 349 #rc_cache.cache_repo.arguments.port = 6379
342 350 #rc_cache.cache_repo.arguments.db = 1
351 ## more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
343 352 #rc_cache.cache_repo.arguments.distributed_lock = true
344 353
345 354 ## cache settings for SQL queries, this needs to use memory type backend
@@ -424,7 +433,7 b' channelstream.server = 127.0.0.1:9800'
424 433 ## location of the channelstream server from outside world
425 434 ## use ws:// for http or wss:// for https. This address needs to be handled
426 435 ## by external HTTP server such as Nginx or Apache
427 ## see nginx/apache configuration examples in our docs
436 ## see Nginx/Apache configuration examples in our docs
428 437 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
429 438 channelstream.secret = secret
430 439 channelstream.history.location = %(here)s/channelstream_history
@@ -441,14 +450,14 b' channelstream.proxy_path = /_channelstre'
441 450 ## Appenlight is tailored to work with RhodeCode, see
442 451 ## http://appenlight.com for details how to obtain an account
443 452
444 ## appenlight integration enabled
453 ## Appenlight integration enabled
445 454 appenlight = false
446 455
447 456 appenlight.server_url = https://api.appenlight.com
448 457 appenlight.api_key = YOUR_API_KEY
449 458 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
450 459
451 # used for JS client
460 ## used for JS client
452 461 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
453 462
454 463 ## TWEAK AMOUNT OF INFO SENT HERE
@@ -473,7 +482,7 b' appenlight.logging.level = WARNING'
473 482 ## (saves API quota for intensive logging)
474 483 appenlight.logging_on_error = false
475 484
476 ## list of additonal keywords that should be grabbed from environ object
485 ## list of additional keywords that should be grabbed from environ object
477 486 ## can be string with comma separated list of words in lowercase
478 487 ## (by default client will always send following info:
479 488 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
@@ -533,7 +542,7 b' sqlalchemy.db1.convert_unicode = true'
533 542 vcs.server.enable = true
534 543 vcs.server = localhost:9900
535 544
536 ## Web server connectivity protocol, responsible for web based VCS operatations
545 ## Web server connectivity protocol, responsible for web based VCS operations
537 546 ## Available protocols are:
538 547 ## `http` - use http-rpc backend (default)
539 548 vcs.server.protocol = http
@@ -582,7 +591,8 b' svn.proxy.config_file_path = %(here)s/mo'
582 591 ## In most cases it should be set to `/`.
583 592 svn.proxy.location_root = /
584 593 ## Command to reload the mod dav svn configuration on change.
585 ## Example: `/etc/init.d/apache2 reload`
594 ## Example: `/etc/init.d/apache2 reload` or /home/USER/apache_reload.sh
595 ## Make sure user who runs RhodeCode process is allowed to reload Apache
586 596 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
587 597 ## If the timeout expires before the reload command finishes, the command will
588 598 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
@@ -593,7 +603,7 b' svn.proxy.location_root = /'
593 603 ############################################################
594 604
595 605 ## Defines if a custom authorized_keys file should be created and written on
596 ## any change user ssh keys. Setting this to false also disables posibility
606 ## any change user ssh keys. Setting this to false also disables possibility
597 607 ## of adding SSH keys by users from web interface. Super admins can still
598 608 ## manage SSH Keys.
599 609 ssh.generate_authorized_keyfile = false
@@ -601,13 +611,13 b' ssh.generate_authorized_keyfile = false'
601 611 ## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
602 612 # ssh.authorized_keys_ssh_opts =
603 613
604 ## Path to the authrozied_keys file where the generate entries are placed.
614 ## Path to the authorized_keys file where the generate entries are placed.
605 615 ## It is possible to have multiple key files specified in `sshd_config` e.g.
606 616 ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
607 617 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
608 618
609 619 ## Command to execute the SSH wrapper. The binary is available in the
610 ## rhodecode installation directory.
620 ## RhodeCode installation directory.
611 621 ## e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper
612 622 ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper
613 623
@@ -615,7 +625,7 b' ssh.wrapper_cmd = ~/.rccontrol/community'
615 625 ssh.wrapper_cmd_allow_shell = false
616 626
617 627 ## Enables logging, and detailed output send back to the client during SSH
618 ## operations. Usefull for debugging, shouldn't be used in production.
628 ## operations. Useful for debugging, shouldn't be used in production.
619 629 ssh.enable_debug_logging = true
620 630
621 631 ## Paths to binary executable, by default they are the names, but we can
@@ -624,6 +634,10 b' ssh.executable.hg = ~/.rccontrol/vcsserv'
624 634 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
625 635 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
626 636
637 ## Enables SSH key generator web interface. Disabling this still allows users
638 ## to add their own keys.
639 ssh.enable_ui_key_generator = true
640
627 641
628 642 ## Dummy marker to add new entries after.
629 643 ## Add any custom entries below. Please don't remove.
@@ -53,7 +53,7 b' port = 5000'
53 53 ## run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
54 54
55 55 use = egg:gunicorn#main
56 ## Sets the number of process workers. More workers means more concurent connections
56 ## Sets the number of process workers. More workers means more concurrent connections
57 57 ## RhodeCode can handle at the same time. Each additional worker also it increases
58 58 ## memory usage as each has it's own set of caches.
59 59 ## Recommended value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers, but no more
@@ -108,10 +108,10 b' use = egg:rhodecode-enterprise-ce'
108 108 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
109 109 #rhodecode.encrypted_values.strict = false
110 110
111 ## return gzipped responses from Rhodecode (static files/application)
111 ## return gzipped responses from RhodeCode (static files/application)
112 112 gzip_responses = false
113 113
114 ## autogenerate javascript routes file on startup
114 ## auto-generate javascript routes file on startup
115 115 generate_js_files = false
116 116
117 117 ## System global default language.
@@ -128,7 +128,7 b' startup.import_repos = false'
128 128 ## the repository.
129 129 #archive_cache_dir = /tmp/tarballcache
130 130
131 ## URL at which the application is running. This is used for bootstraping
131 ## URL at which the application is running. This is used for Bootstrapping
132 132 ## requests in context when no web request is available. Used in ishell, or
133 133 ## SSH calls. Set this for events to receive proper url for SSH calls.
134 134 app.base_url = http://rhodecode.local
@@ -178,7 +178,7 b' gist_alias_url ='
178 178 ## used for access.
179 179 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
180 180 ## came from the the logged in user who own this authentication token.
181 ## Additionally @TOKEN syntaxt can be used to bound the view to specific
181 ## Additionally @TOKEN syntax can be used to bound the view to specific
182 182 ## authentication token. Such view would be only accessible when used together
183 183 ## with this authentication token
184 184 ##
@@ -202,14 +202,14 b' default_encoding = UTF-8'
202 202
203 203 ## instance-id prefix
204 204 ## a prefix key for this instance used for cache invalidation when running
205 ## multiple instances of rhodecode, make sure it's globally unique for
206 ## all running rhodecode instances. Leave empty if you don't use it
205 ## multiple instances of RhodeCode, make sure it's globally unique for
206 ## all running RhodeCode instances. Leave empty if you don't use it
207 207 instance_id =
208 208
209 209 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
210 210 ## of an authentication plugin also if it is disabled by it's settings.
211 211 ## This could be useful if you are unable to log in to the system due to broken
212 ## authentication settings. Then you can enable e.g. the internal rhodecode auth
212 ## authentication settings. Then you can enable e.g. the internal RhodeCode auth
213 213 ## module to log in again and fix the settings.
214 214 ##
215 215 ## Available builtin plugin IDs (hash is part of the ID):
@@ -225,7 +225,7 b' instance_id ='
225 225 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
226 226 ## handling that causing a series of failed authentication calls.
227 227 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
228 ## This will be served instead of default 401 on bad authnetication
228 ## This will be served instead of default 401 on bad authentication
229 229 auth_ret_code =
230 230
231 231 ## use special detection method when serving auth_ret_code, instead of serving
@@ -259,6 +259,13 b' labs_settings_active = true'
259 259 ## This is used to store exception from RhodeCode in shared directory
260 260 #exception_tracker.store_path =
261 261
262 ## File store configuration. This is used to store and serve uploaded files
263 file_store.enabled = true
264 ## Storage backend, available options are: local
265 file_store.backend = local
266 ## path to store the uploaded binaries
267 file_store.storage_path = %(here)s/data/file_store
268
262 269
263 270 ####################################
264 271 ### CELERY CONFIG ####
@@ -300,6 +307,7 b' rc_cache.cache_perms.expiration_time = 3'
300 307 #rc_cache.cache_perms.arguments.host = localhost
301 308 #rc_cache.cache_perms.arguments.port = 6379
302 309 #rc_cache.cache_perms.arguments.db = 0
310 ## more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
303 311 #rc_cache.cache_perms.arguments.distributed_lock = true
304 312
305 313 ## `cache_repo` cache settings for FileTree, Readme, RSS FEEDS
@@ -315,6 +323,7 b' rc_cache.cache_repo.expiration_time = 25'
315 323 #rc_cache.cache_repo.arguments.host = localhost
316 324 #rc_cache.cache_repo.arguments.port = 6379
317 325 #rc_cache.cache_repo.arguments.db = 1
326 ## more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
318 327 #rc_cache.cache_repo.arguments.distributed_lock = true
319 328
320 329 ## cache settings for SQL queries, this needs to use memory type backend
@@ -399,7 +408,7 b' channelstream.server = 127.0.0.1:9800'
399 408 ## location of the channelstream server from outside world
400 409 ## use ws:// for http or wss:// for https. This address needs to be handled
401 410 ## by external HTTP server such as Nginx or Apache
402 ## see nginx/apache configuration examples in our docs
411 ## see Nginx/Apache configuration examples in our docs
403 412 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
404 413 channelstream.secret = secret
405 414 channelstream.history.location = %(here)s/channelstream_history
@@ -416,14 +425,14 b' channelstream.proxy_path = /_channelstre'
416 425 ## Appenlight is tailored to work with RhodeCode, see
417 426 ## http://appenlight.com for details how to obtain an account
418 427
419 ## appenlight integration enabled
428 ## Appenlight integration enabled
420 429 appenlight = false
421 430
422 431 appenlight.server_url = https://api.appenlight.com
423 432 appenlight.api_key = YOUR_API_KEY
424 433 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
425 434
426 # used for JS client
435 ## used for JS client
427 436 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
428 437
429 438 ## TWEAK AMOUNT OF INFO SENT HERE
@@ -448,7 +457,7 b' appenlight.logging.level = WARNING'
448 457 ## (saves API quota for intensive logging)
449 458 appenlight.logging_on_error = false
450 459
451 ## list of additonal keywords that should be grabbed from environ object
460 ## list of additional keywords that should be grabbed from environ object
452 461 ## can be string with comma separated list of words in lowercase
453 462 ## (by default client will always send following info:
454 463 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
@@ -506,7 +515,7 b' sqlalchemy.db1.convert_unicode = true'
506 515 vcs.server.enable = true
507 516 vcs.server = localhost:9900
508 517
509 ## Web server connectivity protocol, responsible for web based VCS operatations
518 ## Web server connectivity protocol, responsible for web based VCS operations
510 519 ## Available protocols are:
511 520 ## `http` - use http-rpc backend (default)
512 521 vcs.server.protocol = http
@@ -555,7 +564,8 b' svn.proxy.config_file_path = %(here)s/mo'
555 564 ## In most cases it should be set to `/`.
556 565 svn.proxy.location_root = /
557 566 ## Command to reload the mod dav svn configuration on change.
558 ## Example: `/etc/init.d/apache2 reload`
567 ## Example: `/etc/init.d/apache2 reload` or /home/USER/apache_reload.sh
568 ## Make sure user who runs RhodeCode process is allowed to reload Apache
559 569 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
560 570 ## If the timeout expires before the reload command finishes, the command will
561 571 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
@@ -566,7 +576,7 b' svn.proxy.location_root = /'
566 576 ############################################################
567 577
568 578 ## Defines if a custom authorized_keys file should be created and written on
569 ## any change user ssh keys. Setting this to false also disables posibility
579 ## any change user ssh keys. Setting this to false also disables possibility
570 580 ## of adding SSH keys by users from web interface. Super admins can still
571 581 ## manage SSH Keys.
572 582 ssh.generate_authorized_keyfile = false
@@ -574,13 +584,13 b' ssh.generate_authorized_keyfile = false'
574 584 ## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
575 585 # ssh.authorized_keys_ssh_opts =
576 586
577 ## Path to the authrozied_keys file where the generate entries are placed.
587 ## Path to the authorized_keys file where the generate entries are placed.
578 588 ## It is possible to have multiple key files specified in `sshd_config` e.g.
579 589 ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
580 590 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
581 591
582 592 ## Command to execute the SSH wrapper. The binary is available in the
583 ## rhodecode installation directory.
593 ## RhodeCode installation directory.
584 594 ## e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper
585 595 ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper
586 596
@@ -588,7 +598,7 b' ssh.wrapper_cmd = ~/.rccontrol/community'
588 598 ssh.wrapper_cmd_allow_shell = false
589 599
590 600 ## Enables logging, and detailed output send back to the client during SSH
591 ## operations. Usefull for debugging, shouldn't be used in production.
601 ## operations. Useful for debugging, shouldn't be used in production.
592 602 ssh.enable_debug_logging = false
593 603
594 604 ## Paths to binary executable, by default they are the names, but we can
@@ -597,6 +607,10 b' ssh.executable.hg = ~/.rccontrol/vcsserv'
597 607 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
598 608 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
599 609
610 ## Enables SSH key generator web interface. Disabling this still allows users
611 ## to add their own keys.
612 ssh.enable_ui_key_generator = true
613
600 614
601 615 ## Dummy marker to add new entries after.
602 616 ## Add any custom entries below. Please don't remove.
@@ -19,14 +19,15 b''
19 19 # }
20 20
21 21 args@
22 { pythonPackages ? "python27Packages"
22 { system ? builtins.currentSystem
23 , pythonPackages ? "python27Packages"
23 24 , pythonExternalOverrides ? self: super: {}
24 25 , doCheck ? false
25 26 , ...
26 27 }:
27 28
28 29 let
29 pkgs_ = (import <nixpkgs> {});
30 pkgs_ = args.pkgs or (import <nixpkgs> { inherit system; });
30 31 in
31 32
32 33 let
@@ -80,7 +81,8 b' let'
80 81
81 82 nodeEnv = import ./pkgs/node-default.nix {
82 83 inherit
83 pkgs;
84 pkgs
85 system;
84 86 };
85 87 nodeDependencies = nodeEnv.shell.nodeDependencies;
86 88
@@ -116,7 +116,7 b' Full-text Search Backup'
116 116
117 117 You may also have full text search set up, but the index can be rebuild from
118 118 re-imported |repos| if necessary. You will most likely want to backup your
119 :file:`mapping.ini` file if you've configured that. For more information, see
119 :file:`search_mapping.ini` file if you've configured that. For more information, see
120 120 the :ref:`indexing-ref` section.
121 121
122 122 Restoration Steps
@@ -140,7 +140,7 b' Post Restoration Steps'
140 140 Once you have restored your |RCE| instance to basic functionality, you can
141 141 then work on restoring any specific setup changes you had made.
142 142
143 * To recreate the |RCE| index, use the backed up :file:`mapping.ini` file if
143 * To recreate the |RCE| index, use the backed up :file:`search_mapping.ini` file if
144 144 you had made changes and rerun the indexer. See the
145 145 :ref:`indexing-ref` section for details.
146 146 * To reconfigure any extensions, copy the backed up extensions into the
@@ -23,9 +23,9 b' sections.'
23 23 * :ref:`increase-gunicorn`
24 24 * :ref:`x-frame`
25 25
26 \- **mapping.ini**
26 \- **search_mapping.ini**
27 27 Default location:
28 :file:`/home/{user}/.rccontrol/{instance-id}/mapping.ini`
28 :file:`/home/{user}/.rccontrol/{instance-id}/search_mapping.ini`
29 29
30 30 This file is used to control the |RCE| indexer. It comes configured
31 31 to index your instance. To change the default configuration, see
@@ -3,35 +3,41 b''
3 3 Full-text Search
4 4 ----------------
5 5
6 By default RhodeCode is configured to use `Whoosh`_ to index |repos| and
7 provide full-text search.
6 RhodeCode provides a full text search capabilities to search inside file content,
7 commit message, and file paths. Indexing is not enabled by default and to use
8 full text search building an index is a pre-requisite.
8 9
9 |RCE| also provides support for `Elasticsearch`_ as a backend for scalable
10 search. See :ref:`enable-elasticsearch` for details.
10 By default RhodeCode is configured to use `Whoosh`_ to index |repos| and
11 provide full-text search. `Whoosh`_ works well for a small amount of data and
12 shouldn't be used in case of large code-bases and lots of repositories.
13
14 |RCE| also provides support for `ElasticSearch 6`_ as a backend more for advanced
15 and scalable search. See :ref:`enable-elasticsearch` for details.
11 16
12 17 Indexing
13 18 ^^^^^^^^
14 19
15 To run the indexer you need to use an |authtoken| with admin rights to all
16 |repos|.
20 To run the indexer you need to have an |authtoken| with admin rights to all |repos|.
17 21
18 To index new content added, you have the option to set the indexer up in a
22 To index repositories stored in RhodeCode, you have the option to set the indexer up in a
19 23 number of ways, for example:
20 24
21 * Call the indexer via a cron job. We recommend running this nightly,
22 unless you need everything indexed immediately.
23 * Set the indexer to infinitely loop and reindex as soon as it has run its
24 cycle.
25 * Call the indexer via a cron job. We recommend running this once at night.
26 In case you need everything indexed immediately it's possible to index few
27 times during the day. Indexer has a special locking mechanism that won't allow
28 two instances of indexer running at once. It's safe to run it even every 1hr.
29 * Set the indexer to infinitely loop and reindex as soon as it has run its previous cycle.
25 30 * Hook the indexer up with your CI server to reindex after each push.
26 31
27 The indexer works by indexing new commits added since the last run. If you
28 wish to build a brand new index from scratch each time,
29 use the ``force`` option in the configuration file.
32 The indexer works by indexing new commits added since the last run, and comparing
33 file changes to index only new or modified files.
34 If you wish to build a brand new index from scratch each time, use the ``force``
35 option in the configuration file, or run it with --force flag.
30 36
31 37 .. important::
32 38
33 39 You need to have |RCT| installed, see :ref:`install-tools`. Since |RCE|
34 3.5.0 they are installed by default.
40 3.5.0 they are installed by default and available with community/enterprise installations.
35 41
36 42 To set up indexing, use the following steps:
37 43
@@ -45,6 +51,13 b' 4. :ref:`advanced-indexing`'
45 51 Configure the ``.rhoderc`` File
46 52 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47 53
54 .. note::
55
56 Optionally it's possible to use indexer without the ``.rhoderc``. Simply instead of
57 executing with `--instance-name=enterprise-1` execute providing the host and token
58 directly: `--api-host=http://127.0.0.1:10000 --api-key=<auth-token-goes-here>`
59
60
48 61 |RCT| uses the :file:`/home/{user}/.rhoderc` file for connection details
49 62 to |RCE| instances. If this file is not automatically created,
50 63 you can configure it using the following example. You need to configure the
@@ -58,9 +71,9 b' details for each instance you want to in'
58 71
59 72 - NAME: enterprise-1
60 73 - STATUS: RUNNING
61 - TYPE: Momentum
62 - VERSION: 1.5.0
63 - URL: http://127.0.0.1:10000
74 - TYPE: Enterprise
75 - VERSION: 4.1.0
76 - URL: http://127.0.0.1:10003
64 77
65 78 To get your API Token, on the |RCE| interface go to
66 79 :menuselection:`username --> My Account --> Auth tokens`
@@ -72,29 +85,35 b' To get your API Token, on the |RCE| inte'
72 85 [instance:enterprise-1]
73 86 api_host = http://127.0.0.1:10000
74 87 api_key = <auth token goes here>
75 repo_dir = /home/<username>/repos
88
76 89
77 90 .. _run-index:
78 91
79 92 Run the Indexer
80 93 ^^^^^^^^^^^^^^^
81 94
82 Run the indexer using the following command, and specify the instance you
83 want to index:
95 Run the indexer using the following command, and specify the instance you want to index:
84 96
85 97 .. code-block:: bash
86 98
87 # From inside a virtualevv
88 (venv)$ rhodecode-index --instance-name=enterprise-1
89
90 # Using default installation
99 # Using default simples indexing of all repositories
91 100 $ /home/user/.rccontrol/enterprise-1/profile/bin/rhodecode-index \
92 101 --instance-name=enterprise-1
93 102
94 # Using a custom mapping file
103 # Using a custom mapping file with indexing rules, and using elasticsearch 6 backend
95 104 $ /home/user/.rccontrol/enterprise-1/profile/bin/rhodecode-index \
96 105 --instance-name=enterprise-1 \
97 --mapping=/home/user/.rccontrol/enterprise-1/mapping.ini
106 --mapping=/home/user/.rccontrol/enterprise-1/search_mapping.ini \
107 --es-version=6 --engine-location=http://elasticsearch-host:9200
108
109 # Using a custom mapping file and invocation without ``.rhoderc``
110 $ /home/user/.rccontrol/enterprise-1/profile/bin/rhodecode-index \
111 --api-host=http://rhodecodecode.myserver.com --api-key=xxxxx \
112 --mapping=/home/user/.rccontrol/enterprise-1/search_mapping.ini
113
114 # From inside a virtualev on your local machine or CI server.
115 (venv)$ rhodecode-index --instance-name=enterprise-1
116
98 117
99 118 .. note::
100 119
@@ -136,119 +155,173 b' 3. Save the file.'
136 155 # using a specially configured mapping file
137 156 */15 * * * * ~/.rccontrol/enterprise-4/profile/bin/rhodecode-index \
138 157 --instance-name=enterprise-4 \
139 --mapping=/home/user/.rccontrol/enterprise-4/mapping.ini
158 --mapping=/home/user/.rccontrol/enterprise-4/search_mapping.ini
140 159
141 160 .. _advanced-indexing:
142 161
143 162 Advanced Indexing
144 163 ^^^^^^^^^^^^^^^^^
145 164
146 |RCT| indexes based on the :file:`mapping.ini` file. To configure your index,
147 you can specify different options in this file. The default location is:
165
166 Force Re-Indexing single repository
167 +++++++++++++++++++++++++++++++++++
168
169 Often it's required to re-index whole repository because of some repository changes,
170 or to remove some indexed secrets, or files. There's a special `--repo-name=` flag
171 for the indexer that limits execution to a single repository. For example to force-reindex
172 single repository such call can be made::
173
174 rhodecode-index --instance-name=enterprise-1 --force --repo-name=rhodecode-vcsserver
175
176
177 Removing repositories from index
178 ++++++++++++++++++++++++++++++++
148 179
149 * :file:`/home/{user}/.rccontrol/{instance-id}/mapping.ini`, using default
150 |RCT|.
180 The indexer automatically removes renamed repositories and builds index for new names.
181 In the same way if a listed repository in mapping.ini is not reported existing by the
182 server it's removed from the index.
183 In case that you wish to remove indexed repository manually such call would allow that::
184
185 rhodecode-index --instance-name=enterprise-1 --remove-only --repo-name=rhodecode-vcsserver
186
187
188 Using search_mapping.ini file for advanced index rules
189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
190
191 By default rhodecode-index runs for all repositories, all files with parsing limits
192 defined by the CLI default arguments. You can change those limits by calling with
193 different flags such as `--max-filesize=2048kb` or `--repo-limit=10`
194
195 For more advanced execution logic it's possible to use a configuration file that
196 would define detailed rules which repositories and how should be indexed.
197
198 |RCT| provides an example index configuration file called :file:`search_mapping.ini`.
199 This file is created by default during installation and is located at:
200
201 * :file:`/home/{user}/.rccontrol/{instance-id}/search_mapping.ini`, using default |RCT|.
151 202 * :file:`~/venv/lib/python2.7/site-packages/rhodecode_tools/templates/mapping.ini`,
152 203 when using ``virtualenv``.
153 204
154 205 .. note::
155 206
156 If you need to create the :file:`mapping.ini` file, use the |RCT|
157 ``rhodecode-index --create-mapping path/to/file`` API call. For details,
158 see the :ref:`tools-cli` section.
159
160 The indexer runs in a random order to prevent a failing |repo| from stopping
161 a build. To configure different indexing scenarios, set the following options
162 inside the :file:`mapping.ini` and specify the altered file using the
163 ``--mapping`` option.
207 If you need to create the :file:`search_mapping.ini` file manually, use the |RCT|
208 ``rhodecode-index --create-mapping path/to/search_mapping.ini`` API call.
209 For details, see the :ref:`tools-cli` section.
164 210
165 * ``index_files`` : Index the specified file types.
166 * ``skip_files`` : Do not index the specified file types.
167 * ``index_files_content`` : Index the content of the specified file types.
168 * ``skip_files_content`` : Do not index the content of the specified files.
169 * ``force`` : Create a fresh index on each run.
170 * ``max_filesize`` : Files larger than the set size will not be indexed.
171 * ``commit_parse_limit`` : Set the batch size when indexing commit messages.
172 Set to a lower number to lessen memory load.
173 * ``repo_limit`` : Set the maximum number or |repos| indexed per run.
174 * ``[INCLUDE]`` : Set |repos| you want indexed. This takes precedent over
175 ``[EXCLUDE]``.
176 * ``[EXCLUDE]`` : Set |repos| you do not want indexed. Exclude can be used to
177 not index branches, forks, or log |repos|.
211 To Run the indexer with mapping file provide it using `--mapping` flag::
178 212
179 At the end of the file you can specify conditions for specific |repos| that
180 will override the default values. To configure your indexer,
181 use the following example :file:`mapping.ini` file.
213 rhodecode-index --instance-name=enterprise-1 --mapping=/my/path/search_mapping.ini
214
215
216 Here's a detailed example of using :file:`search_mapping.ini` file.
182 217
183 218 .. code-block:: ini
184 219
185 220 [__DEFAULT__]
186 # default patterns for indexing files and content of files.
187 # Binary files are skipped by default.
221 ; Create index on commits data, and files data in this order. Available options
222 ; are `commits`, `files`
223 index_types = commits,files
224
225 ; Commit fetch limit. In what amount of chunks commits should be fetched
226 ; via api and parsed. This allows server to transfer smaller chunks and be less loaded
227 commit_fetch_limit = 1000
188 228
189 # Index python and markdown files
190 index_files = *.py, *.md
229 ; Commit process limit. Limit the number of commits indexer should fetch, and
230 ; store inside the full text search index. eg. if repo has 2000 commits, and
231 ; limit is 1000, on the first run it will process commits 0-1000 and on the
232 ; second 1000-2000 commits. Help reduce memory usage, default is 50000
233 ; (set -1 for unlimited)
234 commit_process_limit = 20000
191 235
192 # Do not index these file types
193 skip_files = *.svg, *.log, *.dump, *.txt
236 ; Limit of how many repositories each run can process, default is -1 (unlimited)
237 ; in case of 1000s of repositories it's better to execute in chunks to not overload
238 ; the server.
239 repo_limit = -1
194 240
195 # Index both file types and their content
196 index_files_content = *.cpp, *.ini, *.py
241 ; Default patterns for indexing files and content of files. Binary files
242 ; are skipped by default.
243
244 ; Add to index those comma separated files; globs syntax
245 ; e.g index_files = *.py, *.c, *.h, *.js
246 index_files = *,
247
248 ; Do not add to index those comma separated files, this excludes
249 ; both search by name and content; globs syntax
250 ; e.g index_files = *.key, *.sql, *.xml, *.pem, *.crt
251 skip_files = ,
197 252
198 # Index file names, but not file content
199 skip_files_content = *.svg,
253 ; Add to index content of those comma separated files; globs syntax
254 ; e.g index_files = *.h, *.obj
255 index_files_content = *,
200 256
201 # Force rebuilding an index from scratch. Each repository will be rebuild
202 # from scratch with a global flag. Use local flag to rebuild single repos
257 ; Do not add to index content of those comma separated files; globs syntax
258 ; Binary files are not indexed by default.
259 ; e.g index_files = *.min.js, *.xml, *.dump, *.log, *.dump
260 skip_files_content = ,
261
262 ; Force rebuilding an index from scratch. Each repository will be rebuild from
263 ; scratch with a global flag. Use --repo-name=NAME --force to rebuild single repo
203 264 force = false
204 265
205 # Do not index files larger than 385KB
206 max_filesize = 385KB
266 ; maximum file size that indexer will use, files above that limit are not going
267 ; to have they content indexed.
268 ; Possible options are KB (kilobytes), MB (megabytes), eg 1MB or 1024KB
269 max_filesize = 10MB
207 270
208 # Limit commit indexing to 500 per batch
209 commit_parse_limit = 500
210
211 # Limit each index run to 25 repos
212 repo_limit = 25
213 271
214 # __INCLUDE__ is more important that __EXCLUDE__.
215
216 [__INCLUDE__]
217 # Include all repos with these names
272 [__INDEX_RULES__]
273 ; Ordered match rules for repositories. A list of all repositories will be fetched
274 ; using API and this list will be filtered using those rules.
275 ; Syntax for entry: `glob_pattern_OR_full_repo_name = 0 OR 1` where 0=exclude, 1=include
276 ; When this ordered list is traversed first match will return the include/exclude marker
277 ; For example:
278 ; upstream/binary_repo = 0
279 ; upstream/subrepo/xml_files = 0
280 ; upstream/* = 1
281 ; special-repo = 1
282 ; * = 0
283 ; This will index all repositories under upstream/*, but skip upstream/binary_repo
284 ; and upstream/sub_repo/xml_files, last * = 0 means skip all other matches
218 285
219 docs/* = 1
220 lib/* = 1
221
222 [__EXCLUDE__]
223 # Do not include the following repo in index
224 286
225 dev-docs/* = 1
226 legacy-repos/* = 1
227 *-dev/* = 1
228
229 # Each repo that needs special indexing is a separate section below.
230 # In each section set the options to override the global configuration
231 # parameters above.
232 # If special settings are not configured, the global configuration values
233 # above are inherited. If no special repositories are
234 # defined here RhodeCode will use the API to ask for all repositories
287 ; == EXPLICIT REPOSITORY INDEXING ==
288 ; If defined this will skip using __INDEX_RULES__, and will not use API to fetch
289 ; list of repositories, it will explicitly take names defined with [NAME] format and
290 ; try to build the index, to build index just for repo_name_1 and special-repo use:
291 ; [repo_name_1]
292 ; [special-repo]
235 293
236 # For this repo use different settings
237 [special-repo]
238 commit_parse_limit = 20,
239 skip_files = *.idea, *.xml,
294 ; == PER REPOSITORY CONFIGURATION ==
295 ; This allows overriding the global configuration per repository.
296 ; example to set specific file limit, and skip certain files for repository special-repo
297 ; the CLI flags doesn't override the conf settings.
298 ; [conf:special-repo]
299 ; max_filesize = 5mb
300 ; skip_files = *.xml, *.sql
301
240 302
241 # For another repo use different settings
242 [another-special-repo]
243 index_files = *,
244 max_filesize = 800MB
245 commit_parse_limit = 20000
303
304 In case of 1000s of repositories it can be tricky to write the include/exclude rules at first.
305 There's a special flag to test the mapping file rules and list repositories that would
306 be indexed. Run the indexer with `--show-matched-repos` to list only the
307 match repositories defined in .ini file rules::
308
309 rhodecode-index --instance-name=enterprise-1 --show-matched-repos --mapping=/my/path/search_mapping.ini
310
246 311
247 312 .. _enable-elasticsearch:
248 313
249 Enabling Elasticsearch
314 Enabling ElasticSearch
250 315 ^^^^^^^^^^^^^^^^^^^^^^
251 316
317 ElasticSearch is available in EE edition only. It provides much scalable and more advanced
318 search capabilities. While Whoosh is fine for upto 1-2GB of data, beyond that amount it
319 starts slowing down, and can cause other problems.
320 New ElasticSearch 6 also provides much more advanced query language.
321 It allows advanced filtering by file paths, extensions, use OR statements, ranges etc.
322 Please check query language examples in the search field for some advanced query language usage.
323
324
252 325 1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The
253 326 default location is
254 327 :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
@@ -268,9 +341,19 b' and change it to:'
268 341 .. code-block:: ini
269 342
270 343 search.module = rc_elasticsearch
271 search.location = http://localhost:9200/
344 search.location = http://localhost:9200
345 ## specify Elastic Search version, 6 for latest or 2 for legacy
346 search.es_version = 6
347
348 where ``search.location`` points to the ElasticSearch server
349 by default running on port 9200.
272 350
273 where ``search.location`` points to the elasticsearch server.
351 Index invocation also needs change. Please provide --es-version= and
352 --engine-location= parameters to define ElasticSearch server location and it's version.
353 For example::
354
355 rhodecode-index --instace-name=enterprise-1 --es-version=6 --engine-location=http://localhost:9200
356
274 357
275 358 .. _Whoosh: https://pypi.python.org/pypi/Whoosh/
276 .. _Elasticsearch: https://www.elastic.co/ No newline at end of file
359 .. _ElasticSearch 6: https://www.elastic.co/
@@ -9,6 +9,9 b' Use the following example to configure N'
9 9 ## Rate limiter for certain pages to prevent brute force attacks
10 10 limit_req_zone $binary_remote_addr zone=req_limit:10m rate=1r/s;
11 11
12 ## cache zone
13 proxy_cache_path /etc/nginx/nginx_cache levels=1:2 use_temp_path=off keys_zone=cache_zone:10m inactive=720h max_size=10g;
14
12 15 ## Custom log format
13 16 log_format log_custom '$remote_addr - $remote_user [$time_local] '
14 17 '"$request" $status $body_bytes_sent '
@@ -141,6 +144,34 b' Use the following example to configure N'
141 144 try_files $uri @rhodecode_http;
142 145 }
143 146
147 ## Special Cache for file store, make sure you enable this intentionally as
148 ## it could bypass upload files permissions
149 # location /_file_store/download {
150 #
151 # proxy_cache cache_zone;
152 # # ignore Set-Cookie
153 # proxy_ignore_headers Set-Cookie;
154 # proxy_ignore_headers Cookie;
155 #
156 # proxy_cache_key $host$uri$is_args$args;
157 # proxy_cache_methods GET;
158 #
159 # proxy_cache_bypass $http_cache_control;
160 # proxy_cache_valid 200 302 720h;
161 #
162 # proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
163 #
164 # # returns cache status in headers
165 # add_header X-Proxy-Cache $upstream_cache_status;
166 # add_header Cache-Control "public";
167 #
168 # proxy_cache_lock on;
169 # proxy_cache_lock_age 5m;
170 #
171 # proxy_pass http://rc;
172 #
173 # }
174
144 175 location / {
145 176 try_files $uri @rhodecode_http;
146 177 }
@@ -35,7 +35,7 b' 2. On the |repo| group settings page you'
35 35
36 36 * :guilabel:`Owner`: Lets you change the group owner. Useful when users are
37 37 moving roles within an organisation.
38 * :guilabel:`Group parent`: Lets you add the |repo| group as a sub-group
38 * :guilabel:`Repository group`: Lets you add the |repo| group as a sub-group
39 39 of a larger group, i.e. :guilabel:`QA-Repos >> QA-Repos-Berlin`
40 40 * :guilabel:`Enable automatic locking`: For more information,
41 41 see :ref:`repo-locking`
@@ -30,3 +30,5 b' The following are the most common system'
30 30 enable-debug
31 31 admin-tricks
32 32 cleanup-cmds
33 restore-deleted-repositories
34
@@ -78,7 +78,7 b' Configuration Files'
78 78 -------------------
79 79
80 80 * :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
81 * :file:`/home/{user}/.rccontrol/{instance-id}/mapping.ini`
81 * :file:`/home/{user}/.rccontrol/{instance-id}/search_mapping.ini`
82 82 * :file:`/home/{user}/.rccontrol/{vcsserver-id}/vcsserver.ini`
83 83 * :file:`/home/{user}/.rccontrol/supervisor/supervisord.ini`
84 84 * :file:`/home/{user}/.rccontrol.ini`
@@ -196,7 +196,8 b' are not required in args.'
196 196 .. --- API DEFS MARKER ---
197 197 .. toctree::
198 198
199 methods/views
199 methods/repo-methods
200 methods/store-methods
200 201 methods/license-methods
201 202 methods/deprecated-methods
202 203 methods/gist-methods
@@ -83,7 +83,7 b' comment_pull_request'
83 83 create_pull_request
84 84 -------------------
85 85
86 .. py:function:: create_pull_request(apiuser, source_repo, target_repo, source_ref, target_ref, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>)
86 .. py:function:: create_pull_request(apiuser, source_repo, target_repo, source_ref, target_ref, owner=<Optional:<OptionalAttr:apiuser>>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>)
87 87
88 88 Creates a new pull request.
89 89
@@ -104,6 +104,8 b' create_pull_request'
104 104 :type source_ref: str
105 105 :param target_ref: Set the target ref name.
106 106 :type target_ref: str
107 :param owner: user_id or username
108 :type owner: Optional(str)
107 109 :param title: Optionally Set the pull request title, it's generated otherwise
108 110 :type title: str
109 111 :param description: Set the pull request description.
@@ -248,7 +250,7 b' get_pull_request_comments'
248 250 get_pull_requests
249 251 -----------------
250 252
251 .. py:function:: get_pull_requests(apiuser, repoid, status=<Optional:'new'>)
253 .. py:function:: get_pull_requests(apiuser, repoid, status=<Optional:'new'>, merge_state=<Optional:True>)
252 254
253 255 Get all pull requests from the repository specified in `repoid`.
254 256
@@ -262,6 +264,9 b' get_pull_requests'
262 264 * ``open``
263 265 * ``closed``
264 266 :type status: str
267 :param merge_state: Optional calculate merge state for each repository.
268 This could result in longer time to fetch the data
269 :type merge_state: bool
265 270
266 271 Example output:
267 272
@@ -358,6 +363,7 b' merge_pull_request'
358 363 "result": {
359 364 "executed": "<bool>",
360 365 "failure_reason": "<int>",
366 "merge_status_message": "<str>",
361 367 "merge_commit_id": "<merge_commit_id>",
362 368 "possible": "<bool>",
363 369 "merge_ref": {
@@ -394,6 +394,105 b' get_repo_changesets'
394 394 of changed files.
395 395
396 396
397 get_repo_comments
398 -----------------
399
400 .. py:function:: get_repo_comments(apiuser, repoid, commit_id=<Optional:None>, comment_type=<Optional:None>, userid=<Optional:None>)
401
402 Get all comments for a repository
403
404 :param apiuser: This is filled automatically from the |authtoken|.
405 :type apiuser: AuthUser
406 :param repoid: Set the repository name or repository ID.
407 :type repoid: str or int
408 :param commit_id: Optionally filter the comments by the commit_id
409 :type commit_id: Optional(str), default: None
410 :param comment_type: Optionally filter the comments by the comment_type
411 one of: 'note', 'todo'
412 :type comment_type: Optional(str), default: None
413 :param userid: Optionally filter the comments by the author of comment
414 :type userid: Optional(str or int), Default: None
415
416 Example error output:
417
418 .. code-block:: bash
419
420 {
421 "id" : <id_given_in_input>,
422 "result" : [
423 {
424 "comment_author": <USER_DETAILS>,
425 "comment_created_on": "2017-02-01T14:38:16.309",
426 "comment_f_path": "file.txt",
427 "comment_id": 282,
428 "comment_lineno": "n1",
429 "comment_resolved_by": null,
430 "comment_status": [],
431 "comment_text": "This file needs a header",
432 "comment_type": "todo"
433 }
434 ],
435 "error" : null
436 }
437
438
439 get_repo_file
440 -------------
441
442 .. py:function:: get_repo_file(apiuser, repoid, commit_id, file_path, max_file_bytes=<Optional:None>, details=<Optional:'basic'>, cache=<Optional:True>)
443
444 Returns a single file from repository at given revision.
445
446 This command can only be run using an |authtoken| with admin rights,
447 or users with at least read rights to |repos|.
448
449 :param apiuser: This is filled automatically from the |authtoken|.
450 :type apiuser: AuthUser
451 :param repoid: The repository name or repository ID.
452 :type repoid: str or int
453 :param commit_id: The revision for which listing should be done.
454 :type commit_id: str
455 :param file_path: The path from which to start displaying.
456 :type file_path: str
457 :param details: Returns different set of information about nodes.
458 The valid options are ``minimal`` ``basic`` and ``full``.
459 :type details: Optional(str)
460 :param max_file_bytes: Only return file content under this file size bytes
461 :type max_file_bytes: Optional(int)
462 :param cache: Use internal caches for fetching files. If disabled fetching
463 files is slower but more memory efficient
464 :type cache: Optional(bool)
465 Example output:
466
467 .. code-block:: bash
468
469 id : <id_given_in_input>
470 result: {
471 "binary": false,
472 "extension": "py",
473 "lines": 35,
474 "content": "....",
475 "md5": "76318336366b0f17ee249e11b0c99c41",
476 "mimetype": "text/x-python",
477 "name": "python.py",
478 "size": 817,
479 "type": "file",
480 }
481 error: null
482
483
484 get_repo_fts_tree
485 -----------------
486
487 .. py:function:: get_repo_fts_tree(apiuser, repoid, commit_id, root_path)
488
489 Returns a list of tree nodes for path at given revision. This api is built
490 strictly for usage in full text search building, and shouldn't be consumed
491
492 This command can only be run using an |authtoken| with admin rights,
493 or users with at least read rights to |repos|.
494
495
397 496 get_repo_nodes
398 497 --------------
399 498
@@ -419,8 +518,8 b' get_repo_nodes'
419 518 ``all`` (default), ``files`` and ``dirs``.
420 519 :type ret_type: Optional(str)
421 520 :param details: Returns extended information about nodes, such as
422 md5, binary, and or content. The valid options are ``basic`` and
423 ``full``.
521 md5, binary, and or content.
522 The valid options are ``basic`` and ``full``.
424 523 :type details: Optional(str)
425 524 :param max_file_bytes: Only return file content under this file size bytes
426 525 :type details: Optional(int)
@@ -432,10 +531,17 b' get_repo_nodes'
432 531 id : <id_given_in_input>
433 532 result: [
434 533 {
435 "name" : "<name>"
436 "type" : "<type>",
437 "binary": "<true|false>" (only in extended mode)
438 "md5" : "<md5 of file content>" (only in extended mode)
534 "binary": false,
535 "content": "File line
536 Line2
537 ",
538 "extension": "md",
539 "lines": 2,
540 "md5": "059fa5d29b19c0657e384749480f6422",
541 "mimetype": "text/x-minidsrc",
542 "name": "file.md",
543 "size": 580,
544 "type": "file"
439 545 },
440 546 ...
441 547 ]
@@ -103,7 +103,7 b' get_method'
103 103 :param apiuser: This is filled automatically from the |authtoken|.
104 104 :type apiuser: AuthUser
105 105 :param pattern: pattern to match method names against
106 :type older_then: Optional("*")
106 :type pattern: Optional("*")
107 107
108 108 Example output:
109 109
@@ -232,3 +232,37 b' rescan_repos'
232 232 }
233 233
234 234
235 store_exception
236 ---------------
237
238 .. py:function:: store_exception(apiuser, exc_data_json, prefix=<Optional:'rhodecode'>)
239
240 Stores sent exception inside the built-in exception tracker in |RCE| server.
241
242 This command can only be run using an |authtoken| with admin rights to
243 the specified repository.
244
245 This command takes the following options:
246
247 :param apiuser: This is filled automatically from the |authtoken|.
248 :type apiuser: AuthUser
249
250 :param exc_data_json: JSON data with exception e.g
251 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
252 :type exc_data_json: JSON data
253
254 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
255 :type prefix: Optional("rhodecode")
256
257 Example output:
258
259 .. code-block:: bash
260
261 id : <id_given_in_input>
262 "result": {
263 "exc_id": 139718459226384,
264 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
265 }
266 error : null
267
268
@@ -15,4 +15,5 b' Please check for reference two example p'
15 15
16 16 auth-saml-duosecurity
17 17 auth-saml-onelogin
18 auth-saml-bulk-enroll-users
18 19
@@ -73,6 +73,10 b' 2. Enable the SSH module on instance.'
73 73 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
74 74 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
75 75
76 ## Enables SSH key generator web interface. Disabling this still allows users
77 ## to add their own keys.
78 ssh.enable_ui_key_generator = true
79
76 80
77 81 3. Set base_url for instance to enable proper event handling (Optional):
78 82
@@ -52,6 +52,7 b' run::'
52 52 Followed by::
53 53
54 54 nix-channel --update
55 nix-env -i nix-2.0.4
55 56
56 57
57 58 Install required binaries
@@ -65,6 +66,18 b' run::'
65 66 nix-env -i nix-prefetch-git
66 67
67 68
69 Speed up JS build by installing PhantomJS
70 -----------------------------------------
71
72 PhantomJS will be downloaded each time nix-shell is invoked. To speed this by
73 setting already downloaded version do this::
74
75 nix-env -i phantomjs-2.1.1
76
77 # and set nix bin path
78 export PATH=$PATH:~/.nix-profile/bin
79
80
68 81 Clone the required repositories
69 82 -------------------------------
70 83
@@ -76,8 +89,8 b' you have it installed before continuing.'
76 89 To obtain the required sources, use the following commands::
77 90
78 91 mkdir rhodecode-develop && cd rhodecode-develop
79 hg clone https://code.rhodecode.com/rhodecode-enterprise-ce
80 hg clone https://code.rhodecode.com/rhodecode-vcsserver
92 hg clone -u default https://code.rhodecode.com/rhodecode-enterprise-ce
93 hg clone -u default https://code.rhodecode.com/rhodecode-vcsserver
81 94
82 95 .. note::
83 96
@@ -93,11 +106,15 b' need to install the following.'
93 106
94 107 required libraries::
95 108
109 # svn related
96 110 sudo apt-get install libapr1-dev libaprutil1-dev
97 111 sudo apt-get install libsvn-dev
112 # libcurl required too
113 sudo apt-get install libcurl4-openssl-dev
114 # mysql/pg server for development, optional
98 115 sudo apt-get install mysql-server libmysqlclient-dev
99 116 sudo apt-get install postgresql postgresql-contrib libpq-dev
100 sudo apt-get install libcurl4-openssl-dev
117
101 118
102 119
103 120 Enter the Development Shell
@@ -182,7 +199,7 b" To use the application's frontend and pr"
182 199 you will need to compile the CSS and JavaScript with Grunt.
183 200 This is easily done from within the nix-shell using the following command::
184 201
185 grunt
202 make web-build
186 203
187 204 When developing new features you will need to recompile following any
188 205 changes made to the CSS or JavaScript files when developing the code::
@@ -18,11 +18,11 b' Activating rcextensions'
18 18 To activate rcextensions simply copy or rename the created template rcextensions
19 19 into the path where the rhodecode.ini file is located::
20 20
21 pushd ~/rccontrol/enterprise-1/
21 pushd ~/.rccontrol/enterprise-1/
22 22 or
23 pushd ~/rccontrol/community-1/
23 pushd ~/.rccontrol/community-1/
24 24
25 mv etc/rcextensions.tmpl rcextensions
25 mv profile/etc/rcextensions.tmpl rcextensions
26 26
27 27
28 28 rcextensions are loaded when |RCE| starts. So a restart is required after activation or
@@ -104,7 +104,7 b' Upgrade notes'
104 104 - In this release, we're shipping a new `rcextensions`. The changes made are
105 105 backward incompatible. An update of `rcextensions` is required
106 106 prior to using them again. Please check the new `rcextensions.tmpl` directory
107 located in `etc/rcextensions.tmpl` in your instance installation path.
107 located in `profile/etc/rcextensions.tmpl` in your instance installation path.
108 108 Old code should be 100% portable by just copy&paste to the right function.
109 109
110 110 - Mailing: We introduced a new mailing library. The older options should be compatible and
@@ -9,6 +9,7 b' Release Notes'
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12 release-notes-4.16.0.rst
12 13 release-notes-4.15.2.rst
13 14 release-notes-4.15.1.rst
14 15 release-notes-4.15.0.rst
@@ -516,7 +516,7 b' Example usage:'
516 516 $ ~/.rccontrol/enterprise-4/profile/bin/rhodecode-index \
517 517 --instance-name=enterprise-4
518 518
519 # Run indexer based on mapping.ini file
519 # Run indexer based on search_mapping.ini file
520 520 # This is using pre-350 virtualenv
521 521 (venv)$ rhodecode-index --instance-name=enterprise-1
522 522
@@ -527,7 +527,7 b' Example usage:'
527 527
528 528 # Create the indexing mapping file
529 529 $ ~/.rccontrol/enterprise-4/profile/bin/rhodecode-index \
530 --create-mapping mapping.ini --instance-name=enterprise-4
530 --create-mapping search_mapping.ini --instance-name=enterprise-4
531 531
532 532 .. _tools-rhodecode-list-instance:
533 533
@@ -52,7 +52,7 b''
52 52 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
53 53 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
54 54 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
55 "<%= dirs.js.src %>/plugins/jquery.mark.js",
55 "<%= dirs.js.node_modules %>/mark.js/dist/jquery.mark.min.js",
56 56 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
57 57 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
58 58 "<%= dirs.js.src %>/select2/select2.js",
@@ -22,6 +22,7 b''
22 22 "grunt-contrib-watch": "^0.6.1",
23 23 "grunt-webpack": "^3.1.3",
24 24 "jquery": "1.11.3",
25 "mark.js": "8.11.1",
25 26 "jshint": "^2.9.1-rc3",
26 27 "moment": "^2.18.1",
27 28 "mousetrap": "^1.6.1",
This diff has been collapsed as it changes many lines, (696 lines changed) Show them Hide them
@@ -409,13 +409,13 b' let'
409 409 sha512 = "mJ3QKWtCchL1vhU/kZlJnLPuQZnlDOdZsyP0bbLWPGdYsQDnSBvyTLhzwBA3QAMlzEL9V4JHygEmK6/OTEyytA==";
410 410 };
411 411 };
412 "@webcomponents/shadycss-1.6.0" = {
412 "@webcomponents/shadycss-1.7.1" = {
413 413 name = "_at_webcomponents_slash_shadycss";
414 414 packageName = "@webcomponents/shadycss";
415 version = "1.6.0";
416 src = fetchurl {
417 url = "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.6.0.tgz";
418 sha512 = "iURGZZU6BaiRJtGgjMn208QxPkY11QwT/VmuHNa4Yb+kJxU/WODe4C8b0LDOtnk4KJzJg50hCfwvPRAjePEzbA==";
415 version = "1.7.1";
416 src = fetchurl {
417 url = "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.7.1.tgz";
418 sha512 = "6SZqLajRPWL0rrKDZOGF8PCBq5B9JqgFmE5rX5psk6i8WrqiMkSCuO8+rnirzViTsU5CqnjQPFC3OvG4YJdMrQ==";
419 419 };
420 420 };
421 421 "@webcomponents/webcomponentsjs-2.2.1" = {
@@ -499,13 +499,13 b' let'
499 499 sha1 = "82ffb02b29e662ae53bdc20af15947706739c536";
500 500 };
501 501 };
502 "ajv-6.6.1" = {
502 "ajv-6.6.2" = {
503 503 name = "ajv";
504 504 packageName = "ajv";
505 version = "6.6.1";
506 src = fetchurl {
507 url = "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz";
508 sha512 = "ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==";
505 version = "6.6.2";
506 src = fetchurl {
507 url = "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz";
508 sha512 = "FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==";
509 509 };
510 510 };
511 511 "ajv-keywords-3.2.0" = {
@@ -815,15 +815,6 b' let'
815 815 sha1 = "b6bbe0b0674b9d719708ca38de8c237cb526c3d1";
816 816 };
817 817 };
818 "async-1.0.0" = {
819 name = "async";
820 packageName = "async";
821 version = "1.0.0";
822 src = fetchurl {
823 url = "https://registry.npmjs.org/async/-/async-1.0.0.tgz";
824 sha1 = "f8fc04ca3a13784ade9e1641af98578cfbd647a9";
825 };
826 };
827 818 "async-2.6.1" = {
828 819 name = "async";
829 820 packageName = "async";
@@ -1445,6 +1436,15 b' let'
1445 1436 sha512 = "+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==";
1446 1437 };
1447 1438 };
1439 "big.js-5.2.2" = {
1440 name = "big.js";
1441 packageName = "big.js";
1442 version = "5.2.2";
1443 src = fetchurl {
1444 url = "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz";
1445 sha512 = "vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==";
1446 };
1447 };
1448 1448 "binary-extensions-1.12.0" = {
1449 1449 name = "binary-extensions";
1450 1450 packageName = "binary-extensions";
@@ -1679,22 +1679,22 b' let'
1679 1679 sha1 = "b534e7c734c4f81ec5fbe8aca2ad24354b962c6c";
1680 1680 };
1681 1681 };
1682 "caniuse-db-1.0.30000912" = {
1682 "caniuse-db-1.0.30000927" = {
1683 1683 name = "caniuse-db";
1684 1684 packageName = "caniuse-db";
1685 version = "1.0.30000912";
1686 src = fetchurl {
1687 url = "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000912.tgz";
1688 sha512 = "uiepPdHcJ06Na9t15L5l+pp3NWQU4IETbmleghD6tqCqbIYqhHSu7nVfbK2gqPjfy+9jl/wHF1UQlyTszh9tJQ==";
1689 };
1690 };
1691 "caniuse-lite-1.0.30000912" = {
1685 version = "1.0.30000927";
1686 src = fetchurl {
1687 url = "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000927.tgz";
1688 sha512 = "CX/QvLA8oh7kQ9cHCCzFm0UZW4KwSyQSRJ5A1XtH42HaMJQ0yh+9fEVWagMqv9I1vSCtaqA5Mb8k0uKfv7jhDw==";
1689 };
1690 };
1691 "caniuse-lite-1.0.30000927" = {
1692 1692 name = "caniuse-lite";
1693 1693 packageName = "caniuse-lite";
1694 version = "1.0.30000912";
1695 src = fetchurl {
1696 url = "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000912.tgz";
1697 sha512 = "M3zAtV36U+xw5mMROlTXpAHClmPAor6GPKAMD5Yi7glCB5sbMPFtnQ3rGpk4XqPdUrrTIaVYSJZxREZWNy8QJg==";
1694 version = "1.0.30000927";
1695 src = fetchurl {
1696 url = "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000927.tgz";
1697 sha512 = "ogq4NbUWf1uG/j66k0AmiO3GjqJAlQyF8n4w8a954cbCyFKmYGvRtgz6qkq2fWuduTXHibX7GyYL5Pg58Aks2g==";
1698 1698 };
1699 1699 };
1700 1700 "caseless-0.12.0" = {
@@ -1733,13 +1733,13 b' let'
1733 1733 sha1 = "a8115c55e4a702fe4d150abd3872822a7e09fc98";
1734 1734 };
1735 1735 };
1736 "chalk-2.4.1" = {
1736 "chalk-2.4.2" = {
1737 1737 name = "chalk";
1738 1738 packageName = "chalk";
1739 version = "2.4.1";
1740 src = fetchurl {
1741 url = "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz";
1742 sha512 = "ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==";
1739 version = "2.4.2";
1740 src = fetchurl {
1741 url = "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz";
1742 sha512 = "Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==";
1743 1743 };
1744 1744 };
1745 1745 "chokidar-2.0.4" = {
@@ -1958,15 +1958,6 b' let'
1958 1958 sha1 = "2423fe6678ac0c5dae8852e5d0e5be08c997abcc";
1959 1959 };
1960 1960 };
1961 "colors-1.0.3" = {
1962 name = "colors";
1963 packageName = "colors";
1964 version = "1.0.3";
1965 src = fetchurl {
1966 url = "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz";
1967 sha1 = "0433f44d809680fdeb60ed260f1b0c262e82a40b";
1968 };
1969 };
1970 1961 "colors-1.1.2" = {
1971 1962 name = "colors";
1972 1963 packageName = "colors";
@@ -1976,13 +1967,13 b' let'
1976 1967 sha1 = "168a4701756b6a7f51a12ce0c97bfa28c084ed63";
1977 1968 };
1978 1969 };
1979 "colors-1.3.2" = {
1970 "colors-1.3.3" = {
1980 1971 name = "colors";
1981 1972 packageName = "colors";
1982 version = "1.3.2";
1983 src = fetchurl {
1984 url = "https://registry.npmjs.org/colors/-/colors-1.3.2.tgz";
1985 sha512 = "rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ==";
1973 version = "1.3.3";
1974 src = fetchurl {
1975 url = "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz";
1976 sha512 = "mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==";
1986 1977 };
1987 1978 };
1988 1979 "combined-stream-1.0.7" = {
@@ -2102,13 +2093,13 b' let'
2102 2093 sha512 = "Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==";
2103 2094 };
2104 2095 };
2105 "core-js-2.5.7" = {
2096 "core-js-2.6.1" = {
2106 2097 name = "core-js";
2107 2098 packageName = "core-js";
2108 version = "2.5.7";
2109 src = fetchurl {
2110 url = "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz";
2111 sha512 = "RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==";
2099 version = "2.6.1";
2100 src = fetchurl {
2101 url = "https://registry.npmjs.org/core-js/-/core-js-2.6.1.tgz";
2102 sha512 = "L72mmmEayPJBejKIWe2pYtGis5r0tQ5NaJekdhyXgeMQTpJoBsH0NL4ElY2LfSoV15xeQWKQ+XTTOZdyero5Xg==";
2112 2103 };
2113 2104 };
2114 2105 "core-util-is-1.0.2" = {
@@ -2246,15 +2237,6 b' let'
2246 2237 sha1 = "ddd52c587033f49e94b71fc55569f252e8ff5f85";
2247 2238 };
2248 2239 };
2249 "cycle-1.0.3" = {
2250 name = "cycle";
2251 packageName = "cycle";
2252 version = "1.0.3";
2253 src = fetchurl {
2254 url = "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz";
2255 sha1 = "21e80b2be8580f98b468f379430662b046c34ad2";
2256 };
2257 };
2258 2240 "cyclist-0.2.2" = {
2259 2241 name = "cyclist";
2260 2242 packageName = "cyclist";
@@ -2489,13 +2471,13 b' let'
2489 2471 sha1 = "bd28773e2642881aec51544924299c5cd822185b";
2490 2472 };
2491 2473 };
2492 "domelementtype-1.3.0" = {
2474 "domelementtype-1.3.1" = {
2493 2475 name = "domelementtype";
2494 2476 packageName = "domelementtype";
2495 version = "1.3.0";
2496 src = fetchurl {
2497 url = "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz";
2498 sha1 = "b17aed82e8ab59e52dd9c19b1756e0fc187204c2";
2477 version = "1.3.1";
2478 src = fetchurl {
2479 url = "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz";
2480 sha512 = "BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==";
2499 2481 };
2500 2482 };
2501 2483 "domhandler-2.1.0" = {
@@ -2552,13 +2534,13 b' let'
2552 2534 sha1 = "3a83a904e54353287874c564b7549386849a98c9";
2553 2535 };
2554 2536 };
2555 "electron-to-chromium-1.3.85" = {
2537 "electron-to-chromium-1.3.98" = {
2556 2538 name = "electron-to-chromium";
2557 2539 packageName = "electron-to-chromium";
2558 version = "1.3.85";
2559 src = fetchurl {
2560 url = "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.85.tgz";
2561 sha512 = "kWSDVVF9t3mft2OHVZy4K85X2beP6c6mFm3teFS/mLSDJpQwuFIWHrULCX+w6H1E55ZYmFRlT+ATAFRwhrYzsw==";
2540 version = "1.3.98";
2541 src = fetchurl {
2542 url = "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.98.tgz";
2543 sha512 = "WIZdNuvE3dFr6kkPgv4d/cfswNZD6XbeLBM8baOIQTsnbf4xWrVEaLvp7oNnbnMWWXDqq7Tbv+H5JfciLTJm4Q==";
2562 2544 };
2563 2545 };
2564 2546 "elliptic-6.4.1" = {
@@ -2633,13 +2615,13 b' let'
2633 2615 sha512 = "MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==";
2634 2616 };
2635 2617 };
2636 "es-abstract-1.12.0" = {
2618 "es-abstract-1.13.0" = {
2637 2619 name = "es-abstract";
2638 2620 packageName = "es-abstract";
2639 version = "1.12.0";
2640 src = fetchurl {
2641 url = "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz";
2642 sha512 = "C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==";
2621 version = "1.13.0";
2622 src = fetchurl {
2623 url = "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz";
2624 sha512 = "vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==";
2643 2625 };
2644 2626 };
2645 2627 "es-to-primitive-1.2.0" = {
@@ -2651,15 +2633,6 b' let'
2651 2633 sha512 = "qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==";
2652 2634 };
2653 2635 };
2654 "es6-promise-4.2.5" = {
2655 name = "es6-promise";
2656 packageName = "es6-promise";
2657 version = "4.2.5";
2658 src = fetchurl {
2659 url = "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz";
2660 sha512 = "n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==";
2661 };
2662 };
2663 2636 "es6-templates-0.2.3" = {
2664 2637 name = "es6-templates";
2665 2638 packageName = "es6-templates";
@@ -2777,13 +2750,13 b' let'
2777 2750 sha512 = "/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==";
2778 2751 };
2779 2752 };
2780 "execa-0.10.0" = {
2753 "execa-1.0.0" = {
2781 2754 name = "execa";
2782 2755 packageName = "execa";
2783 version = "0.10.0";
2784 src = fetchurl {
2785 url = "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz";
2786 sha512 = "7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==";
2756 version = "1.0.0";
2757 src = fetchurl {
2758 url = "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz";
2759 sha512 = "adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==";
2787 2760 };
2788 2761 };
2789 2762 "exit-0.1.2" = {
@@ -2858,15 +2831,6 b' let'
2858 2831 sha512 = "Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==";
2859 2832 };
2860 2833 };
2861 "extract-zip-1.6.7" = {
2862 name = "extract-zip";
2863 packageName = "extract-zip";
2864 version = "1.6.7";
2865 src = fetchurl {
2866 url = "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz";
2867 sha1 = "a840b4b8af6403264c8db57f4f1a74333ef81fe9";
2868 };
2869 };
2870 2834 "extsprintf-1.3.0" = {
2871 2835 name = "extsprintf";
2872 2836 packageName = "extsprintf";
@@ -2876,15 +2840,6 b' let'
2876 2840 sha1 = "96918440e3041a7a414f8c52e3c574eb3c3e1e05";
2877 2841 };
2878 2842 };
2879 "eyes-0.1.8" = {
2880 name = "eyes";
2881 packageName = "eyes";
2882 version = "0.1.8";
2883 src = fetchurl {
2884 url = "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz";
2885 sha1 = "62cf120234c683785d902348a800ef3e0cc20bc0";
2886 };
2887 };
2888 2843 "fast-deep-equal-2.0.1" = {
2889 2844 name = "fast-deep-equal";
2890 2845 packageName = "fast-deep-equal";
@@ -2930,15 +2885,6 b' let'
2930 2885 sha1 = "c14c5b3bf14d7417ffbfd990c0a7495cd9f337bc";
2931 2886 };
2932 2887 };
2933 "fd-slicer-1.0.1" = {
2934 name = "fd-slicer";
2935 packageName = "fd-slicer";
2936 version = "1.0.1";
2937 src = fetchurl {
2938 url = "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz";
2939 sha1 = "8b5bcbd9ec327c5041bf9ab023fd6750f1177e65";
2940 };
2941 };
2942 2888 "file-sync-cmp-0.1.1" = {
2943 2889 name = "file-sync-cmp";
2944 2890 packageName = "file-sync-cmp";
@@ -3002,22 +2948,22 b' let'
3002 2948 sha1 = "9326b1488c22d1a6088650a86901b2d9a90a2cbc";
3003 2949 };
3004 2950 };
3005 "fined-1.1.0" = {
2951 "fined-1.1.1" = {
3006 2952 name = "fined";
3007 2953 packageName = "fined";
3008 version = "1.1.0";
3009 src = fetchurl {
3010 url = "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz";
3011 sha1 = "b37dc844b76a2f5e7081e884f7c0ae344f153476";
3012 };
3013 };
3014 "flagged-respawn-1.0.0" = {
2954 version = "1.1.1";
2955 src = fetchurl {
2956 url = "https://registry.npmjs.org/fined/-/fined-1.1.1.tgz";
2957 sha512 = "jQp949ZmEbiYHk3gkbdtpJ0G1+kgtLQBNdP5edFP7Fh+WAYceLQz6yO1SBj72Xkg8GVyTB3bBzAYrHJVh5Xd5g==";
2958 };
2959 };
2960 "flagged-respawn-1.0.1" = {
3015 2961 name = "flagged-respawn";
3016 2962 packageName = "flagged-respawn";
3017 version = "1.0.0";
3018 src = fetchurl {
3019 url = "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz";
3020 sha1 = "4e79ae9b2eb38bf86b3bb56bf3e0a56aa5fcabd7";
2963 version = "1.0.1";
2964 src = fetchurl {
2965 url = "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz";
2966 sha512 = "lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==";
3021 2967 };
3022 2968 };
3023 2969 "flatten-1.0.2" = {
@@ -3092,15 +3038,6 b' let'
3092 3038 sha1 = "8bfb5502bde4a4d36cfdeea007fcca21d7e382af";
3093 3039 };
3094 3040 };
3095 "fs-extra-1.0.0" = {
3096 name = "fs-extra";
3097 packageName = "fs-extra";
3098 version = "1.0.0";
3099 src = fetchurl {
3100 url = "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz";
3101 sha1 = "cd3ce5f7e7cb6145883fcae3191e9877f8587950";
3102 };
3103 };
3104 3041 "fs-write-stream-atomic-1.0.10" = {
3105 3042 name = "fs-write-stream-atomic";
3106 3043 packageName = "fs-write-stream-atomic";
@@ -3155,13 +3092,13 b' let'
3155 3092 sha512 = "3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==";
3156 3093 };
3157 3094 };
3158 "get-stream-3.0.0" = {
3095 "get-stream-4.1.0" = {
3159 3096 name = "get-stream";
3160 3097 packageName = "get-stream";
3161 version = "3.0.0";
3162 src = fetchurl {
3163 url = "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz";
3164 sha1 = "8e943d1358dc37555054ecbe2edb05aa174ede14";
3098 version = "4.1.0";
3099 src = fetchurl {
3100 url = "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz";
3101 sha512 = "GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==";
3165 3102 };
3166 3103 };
3167 3104 "get-value-2.0.6" = {
@@ -3236,13 +3173,13 b' let'
3236 3173 sha512 = "sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==";
3237 3174 };
3238 3175 };
3239 "global-modules-path-2.3.0" = {
3176 "global-modules-path-2.3.1" = {
3240 3177 name = "global-modules-path";
3241 3178 packageName = "global-modules-path";
3242 version = "2.3.0";
3243 src = fetchurl {
3244 url = "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.3.0.tgz";
3245 sha512 = "HchvMJNYh9dGSCy8pOQ2O8u/hoXaL+0XhnrwH0RyLiSXMMTl9W3N6KUU73+JFOg5PGjtzl6VZzUQsnrpm7Szag==";
3179 version = "2.3.1";
3180 src = fetchurl {
3181 url = "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.3.1.tgz";
3182 sha512 = "y+shkf4InI7mPRHSo2b/k6ix6+NLDtyccYv86whhxrSGX9wjPX1VMITmrDbE1eh7zkzhiWtW2sHklJYoQ62Cxg==";
3246 3183 };
3247 3184 };
3248 3185 "global-prefix-1.0.2" = {
@@ -3533,22 +3470,13 b' let'
3533 3470 sha1 = "5fc8686847ecd73499403319a6b0a3f3f6ae4918";
3534 3471 };
3535 3472 };
3536 "hash.js-1.1.5" = {
3473 "hash.js-1.1.7" = {
3537 3474 name = "hash.js";
3538 3475 packageName = "hash.js";
3539 version = "1.1.5";
3540 src = fetchurl {
3541 url = "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz";
3542 sha512 = "eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==";
3543 };
3544 };
3545 "hasha-2.2.0" = {
3546 name = "hasha";
3547 packageName = "hasha";
3548 version = "2.2.0";
3549 src = fetchurl {
3550 url = "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz";
3551 sha1 = "78d7cbfc1e6d66303fe79837365984517b2f6ee1";
3476 version = "1.1.7";
3477 src = fetchurl {
3478 url = "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz";
3479 sha512 = "taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==";
3552 3480 };
3553 3481 };
3554 3482 "hawk-3.1.3" = {
@@ -4217,13 +4145,13 b' let'
4217 4145 sha1 = "dd8b74278b27102d29df63eae28308a8cfa1b583";
4218 4146 };
4219 4147 };
4220 "js-base64-2.4.9" = {
4148 "js-base64-2.5.0" = {
4221 4149 name = "js-base64";
4222 4150 packageName = "js-base64";
4223 version = "2.4.9";
4224 src = fetchurl {
4225 url = "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz";
4226 sha512 = "xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==";
4151 version = "2.5.0";
4152 src = fetchurl {
4153 url = "https://registry.npmjs.org/js-base64/-/js-base64-2.5.0.tgz";
4154 sha512 = "wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g==";
4227 4155 };
4228 4156 };
4229 4157 "js-tokens-3.0.2" = {
@@ -4280,13 +4208,13 b' let'
4280 4208 sha1 = "46c3fec8c1892b12b0833db9bc7622176dbab34b";
4281 4209 };
4282 4210 };
4283 "jshint-2.9.6" = {
4211 "jshint-2.9.7" = {
4284 4212 name = "jshint";
4285 4213 packageName = "jshint";
4286 version = "2.9.6";
4287 src = fetchurl {
4288 url = "https://registry.npmjs.org/jshint/-/jshint-2.9.6.tgz";
4289 sha512 = "KO9SIAKTlJQOM4lE64GQUtGBRpTOuvbrRrSZw3AhUxMNG266nX9hK2cKA4SBhXOj0irJGyNyGSLT62HGOVDEOA==";
4214 version = "2.9.7";
4215 src = fetchurl {
4216 url = "https://registry.npmjs.org/jshint/-/jshint-2.9.7.tgz";
4217 sha512 = "Q8XN38hGsVQhdlM+4gd1Xl7OB1VieSuCJf+fEJjpo59JH99bVJhXRXAh26qQ15wfdd1VPMuDWNeSWoNl53T4YA==";
4290 4218 };
4291 4219 };
4292 4220 "json-parse-better-errors-1.0.2" = {
@@ -4343,13 +4271,13 b' let'
4343 4271 sha1 = "1eade7acc012034ad84e2396767ead9fa5495821";
4344 4272 };
4345 4273 };
4346 "jsonfile-2.4.0" = {
4347 name = "jsonfile";
4348 packageName = "jsonfile";
4349 version = "2.4.0";
4350 src = fetchurl {
4351 url = "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz";
4352 sha1 = "3736a2b428b87bbda0cc83b53fa3d633a35c2ae8";
4274 "json5-1.0.1" = {
4275 name = "json5";
4276 packageName = "json5";
4277 version = "1.0.1";
4278 src = fetchurl {
4279 url = "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz";
4280 sha512 = "aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==";
4353 4281 };
4354 4282 };
4355 4283 "jsonify-0.0.0" = {
@@ -4370,15 +4298,6 b' let'
4370 4298 sha1 = "313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2";
4371 4299 };
4372 4300 };
4373 "kew-0.7.0" = {
4374 name = "kew";
4375 packageName = "kew";
4376 version = "0.7.0";
4377 src = fetchurl {
4378 url = "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz";
4379 sha1 = "79d93d2d33363d6fdd2970b335d9141ad591d79b";
4380 };
4381 };
4382 4301 "kind-of-3.2.2" = {
4383 4302 name = "kind-of";
4384 4303 packageName = "kind-of";
@@ -4415,15 +4334,6 b' let'
4415 4334 sha512 = "s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==";
4416 4335 };
4417 4336 };
4418 "klaw-1.3.1" = {
4419 name = "klaw";
4420 packageName = "klaw";
4421 version = "1.3.1";
4422 src = fetchurl {
4423 url = "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz";
4424 sha1 = "4088433b46b3b1ba259d78785d8e96f73ba02439";
4425 };
4426 };
4427 4337 "lazy-cache-1.0.4" = {
4428 4338 name = "lazy-cache";
4429 4339 packageName = "lazy-cache";
@@ -4478,13 +4388,13 b' let'
4478 4388 sha1 = "f86e6374d43205a6e6c60e9196f17c0299bfb348";
4479 4389 };
4480 4390 };
4481 "loader-utils-1.1.0" = {
4391 "loader-utils-1.2.3" = {
4482 4392 name = "loader-utils";
4483 4393 packageName = "loader-utils";
4484 version = "1.1.0";
4485 src = fetchurl {
4486 url = "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz";
4487 sha1 = "c98aef488bcceda2ffb5e2de646d6a754429f5cd";
4394 version = "1.2.3";
4395 src = fetchurl {
4396 url = "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz";
4397 sha512 = "fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==";
4488 4398 };
4489 4399 };
4490 4400 "locate-path-2.0.0" = {
@@ -4676,6 +4586,15 b' let'
4676 4586 sha1 = "ecdca8f13144e660f1b5bd41f12f3479d98dfb8f";
4677 4587 };
4678 4588 };
4589 "mark.js-8.11.1" = {
4590 name = "mark.js";
4591 packageName = "mark.js";
4592 version = "8.11.1";
4593 src = fetchurl {
4594 url = "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz";
4595 sha1 = "180f1f9ebef8b0e638e4166ad52db879beb2ffc5";
4596 };
4597 };
4679 4598 "math-expression-evaluator-1.2.17" = {
4680 4599 name = "math-expression-evaluator";
4681 4600 packageName = "math-expression-evaluator";
@@ -4820,6 +4739,15 b' let'
4820 4739 sha1 = "857fcabfc3397d2625b8228262e86aa7a011b05d";
4821 4740 };
4822 4741 };
4742 "minimist-1.2.0" = {
4743 name = "minimist";
4744 packageName = "minimist";
4745 version = "1.2.0";
4746 src = fetchurl {
4747 url = "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz";
4748 sha1 = "a35008b20f41383eec1fb914f4cd5df79a264284";
4749 };
4750 };
4823 4751 "mississippi-2.0.0" = {
4824 4752 name = "mississippi";
4825 4753 packageName = "mississippi";
@@ -4847,13 +4775,13 b' let'
4847 4775 sha1 = "30057438eac6cf7f8c4767f38648d6697d75c903";
4848 4776 };
4849 4777 };
4850 "moment-2.22.2" = {
4778 "moment-2.23.0" = {
4851 4779 name = "moment";
4852 4780 packageName = "moment";
4853 version = "2.22.2";
4854 src = fetchurl {
4855 url = "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz";
4856 sha1 = "3c257f9839fc0e93ff53149632239eb90783ff66";
4781 version = "2.23.0";
4782 src = fetchurl {
4783 url = "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz";
4784 sha512 = "3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==";
4857 4785 };
4858 4786 };
4859 4787 "mousetrap-1.6.2" = {
@@ -4883,13 +4811,13 b' let'
4883 4811 sha1 = "5608aeadfc00be6c2901df5f9861788de0d597c8";
4884 4812 };
4885 4813 };
4886 "nan-2.11.1" = {
4814 "nan-2.12.1" = {
4887 4815 name = "nan";
4888 4816 packageName = "nan";
4889 version = "2.11.1";
4890 src = fetchurl {
4891 url = "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz";
4892 sha512 = "iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==";
4817 version = "2.12.1";
4818 src = fetchurl {
4819 url = "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz";
4820 sha512 = "JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==";
4893 4821 };
4894 4822 };
4895 4823 "nanomatch-1.2.13" = {
@@ -5144,13 +5072,13 b' let'
5144 5072 sha1 = "ffbc4988336e0e833de0c168c7ef152121aa7fb3";
5145 5073 };
5146 5074 };
5147 "os-locale-3.0.1" = {
5075 "os-locale-3.1.0" = {
5148 5076 name = "os-locale";
5149 5077 packageName = "os-locale";
5150 version = "3.0.1";
5151 src = fetchurl {
5152 url = "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz";
5153 sha512 = "7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==";
5078 version = "3.1.0";
5079 src = fetchurl {
5080 url = "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz";
5081 sha512 = "Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==";
5154 5082 };
5155 5083 };
5156 5084 "os-tmpdir-1.0.2" = {
@@ -5207,13 +5135,13 b' let'
5207 5135 sha512 = "vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==";
5208 5136 };
5209 5137 };
5210 "p-limit-2.0.0" = {
5138 "p-limit-2.1.0" = {
5211 5139 name = "p-limit";
5212 5140 packageName = "p-limit";
5213 version = "2.0.0";
5214 src = fetchurl {
5215 url = "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz";
5216 sha512 = "fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==";
5141 version = "2.1.0";
5142 src = fetchurl {
5143 url = "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz";
5144 sha512 = "NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==";
5217 5145 };
5218 5146 };
5219 5147 "p-locate-2.0.0" = {
@@ -5432,15 +5360,6 b' let'
5432 5360 sha512 = "U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==";
5433 5361 };
5434 5362 };
5435 "pend-1.2.0" = {
5436 name = "pend";
5437 packageName = "pend";
5438 version = "1.2.0";
5439 src = fetchurl {
5440 url = "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz";
5441 sha1 = "7a57eb550a6783f9115331fcf4663d5c8e007a50";
5442 };
5443 };
5444 5363 "performance-now-0.2.0" = {
5445 5364 name = "performance-now";
5446 5365 packageName = "performance-now";
@@ -5450,24 +5369,6 b' let'
5450 5369 sha1 = "33ef30c5c77d4ea21c5a53869d91b56d8f2555e5";
5451 5370 };
5452 5371 };
5453 "phantom-4.0.12" = {
5454 name = "phantom";
5455 packageName = "phantom";
5456 version = "4.0.12";
5457 src = fetchurl {
5458 url = "https://registry.npmjs.org/phantom/-/phantom-4.0.12.tgz";
5459 sha512 = "Tz82XhtPmwCk1FFPmecy7yRGZG2btpzY2KI9fcoPT7zT9det0CcMyfBFPp1S8DqzsnQnm8ZYEfdy528mwVtksA==";
5460 };
5461 };
5462 "phantomjs-prebuilt-2.1.16" = {
5463 name = "phantomjs-prebuilt";
5464 packageName = "phantomjs-prebuilt";
5465 version = "2.1.16";
5466 src = fetchurl {
5467 url = "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz";
5468 sha1 = "efd212a4a3966d3647684ea8ba788549be2aefef";
5469 };
5470 };
5471 5372 "pify-3.0.0" = {
5472 5373 name = "pify";
5473 5374 packageName = "pify";
@@ -5477,24 +5378,6 b' let'
5477 5378 sha1 = "e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176";
5478 5379 };
5479 5380 };
5480 "pinkie-2.0.4" = {
5481 name = "pinkie";
5482 packageName = "pinkie";
5483 version = "2.0.4";
5484 src = fetchurl {
5485 url = "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz";
5486 sha1 = "72556b80cfa0d48a974e80e77248e80ed4f7f870";
5487 };
5488 };
5489 "pinkie-promise-2.0.1" = {
5490 name = "pinkie-promise";
5491 packageName = "pinkie-promise";
5492 version = "2.0.1";
5493 src = fetchurl {
5494 url = "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz";
5495 sha1 = "2135d6dfa7a358c069ac9b178776288228450ffa";
5496 };
5497 };
5498 5381 "pkg-dir-2.0.0" = {
5499 5382 name = "pkg-dir";
5500 5383 packageName = "pkg-dir";
@@ -5882,15 +5765,6 b' let'
5882 5765 sha512 = "MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==";
5883 5766 };
5884 5767 };
5885 "progress-1.1.8" = {
5886 name = "progress";
5887 packageName = "progress";
5888 version = "1.1.8";
5889 src = fetchurl {
5890 url = "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz";
5891 sha1 = "e260c78f6161cdd9b0e56cc3e0a85de17c7a57be";
5892 };
5893 };
5894 5768 "promise-7.3.1" = {
5895 5769 name = "promise";
5896 5770 packageName = "promise";
@@ -5945,6 +5819,15 b' let'
5945 5819 sha512 = "ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==";
5946 5820 };
5947 5821 };
5822 "pump-3.0.0" = {
5823 name = "pump";
5824 packageName = "pump";
5825 version = "3.0.0";
5826 src = fetchurl {
5827 url = "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz";
5828 sha512 = "LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==";
5829 };
5830 };
5948 5831 "pumpify-1.5.1" = {
5949 5832 name = "pumpify";
5950 5833 packageName = "pumpify";
@@ -6278,15 +6161,6 b' let'
6278 6161 sha1 = "c6928946a0e06c5f8d6f8a9333469ffda46298a0";
6279 6162 };
6280 6163 };
6281 "request-progress-2.0.1" = {
6282 name = "request-progress";
6283 packageName = "request-progress";
6284 version = "2.0.1";
6285 src = fetchurl {
6286 url = "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz";
6287 sha1 = "5d36bb57961c673aa5b788dbc8141fdf23b44e08";
6288 };
6289 };
6290 6164 "require-directory-2.1.1" = {
6291 6165 name = "require-directory";
6292 6166 packageName = "require-directory";
@@ -6305,13 +6179,13 b' let'
6305 6179 sha1 = "97f717b69d48784f5f526a6c5aa8ffdda055a4d1";
6306 6180 };
6307 6181 };
6308 "resolve-1.8.1" = {
6182 "resolve-1.9.0" = {
6309 6183 name = "resolve";
6310 6184 packageName = "resolve";
6311 version = "1.8.1";
6312 src = fetchurl {
6313 url = "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz";
6314 sha512 = "AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==";
6185 version = "1.9.0";
6186 src = fetchurl {
6187 url = "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz";
6188 sha512 = "TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==";
6315 6189 };
6316 6190 };
6317 6191 "resolve-cwd-2.0.0" = {
@@ -6377,13 +6251,13 b' let'
6377 6251 sha1 = "e439be2aaee327321952730f99a8929e4fc50582";
6378 6252 };
6379 6253 };
6380 "rimraf-2.6.2" = {
6254 "rimraf-2.6.3" = {
6381 6255 name = "rimraf";
6382 6256 packageName = "rimraf";
6383 version = "2.6.2";
6384 src = fetchurl {
6385 url = "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz";
6386 sha512 = "lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==";
6257 version = "2.6.3";
6258 src = fetchurl {
6259 url = "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz";
6260 sha512 = "mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==";
6387 6261 };
6388 6262 };
6389 6263 "ripemd160-2.0.2" = {
@@ -6467,13 +6341,13 b' let'
6467 6341 sha512 = "RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==";
6468 6342 };
6469 6343 };
6470 "serialize-javascript-1.5.0" = {
6344 "serialize-javascript-1.6.1" = {
6471 6345 name = "serialize-javascript";
6472 6346 packageName = "serialize-javascript";
6473 version = "1.5.0";
6474 src = fetchurl {
6475 url = "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz";
6476 sha512 = "Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==";
6347 version = "1.6.1";
6348 src = fetchurl {
6349 url = "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.6.1.tgz";
6350 sha512 = "A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==";
6477 6351 };
6478 6352 };
6479 6353 "set-blocking-2.0.0" = {
@@ -6701,15 +6575,6 b' let'
6701 6575 sha1 = "3e935d7ddd73631b97659956d55128e87b5084a3";
6702 6576 };
6703 6577 };
6704 "split-1.0.1" = {
6705 name = "split";
6706 packageName = "split";
6707 version = "1.0.1";
6708 src = fetchurl {
6709 url = "https://registry.npmjs.org/split/-/split-1.0.1.tgz";
6710 sha512 = "mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==";
6711 };
6712 };
6713 6578 "split-string-3.1.0" = {
6714 6579 name = "split-string";
6715 6580 packageName = "split-string";
@@ -6728,13 +6593,13 b' let'
6728 6593 sha1 = "04e6926f662895354f3dd015203633b857297e2c";
6729 6594 };
6730 6595 };
6731 "sshpk-1.15.2" = {
6596 "sshpk-1.16.0" = {
6732 6597 name = "sshpk";
6733 6598 packageName = "sshpk";
6734 version = "1.15.2";
6735 src = fetchurl {
6736 url = "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz";
6737 sha512 = "Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==";
6599 version = "1.16.0";
6600 src = fetchurl {
6601 url = "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz";
6602 sha512 = "Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==";
6738 6603 };
6739 6604 };
6740 6605 "ssri-5.3.0" = {
@@ -6746,15 +6611,6 b' let'
6746 6611 sha512 = "XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==";
6747 6612 };
6748 6613 };
6749 "stack-trace-0.0.10" = {
6750 name = "stack-trace";
6751 packageName = "stack-trace";
6752 version = "0.0.10";
6753 src = fetchurl {
6754 url = "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz";
6755 sha1 = "547c70b347e8d32b4e108ea1a2a159e5fdde19c0";
6756 };
6757 };
6758 6614 "static-extend-0.1.2" = {
6759 6615 name = "static-extend";
6760 6616 packageName = "static-extend";
@@ -6989,15 +6845,6 b' let'
6989 6845 sha512 = "9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==";
6990 6846 };
6991 6847 };
6992 "throttleit-1.0.0" = {
6993 name = "throttleit";
6994 packageName = "throttleit";
6995 version = "1.0.0";
6996 src = fetchurl {
6997 url = "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz";
6998 sha1 = "9e785836daf46743145a5984b6268d828528ac6c";
6999 };
7000 };
7001 6848 "through-2.3.8" = {
7002 6849 name = "through";
7003 6850 packageName = "through";
@@ -7259,15 +7106,6 b' let'
7259 7106 sha1 = "8cdd8fbac4e2d2ea1e7e2e8097c42f442280f85b";
7260 7107 };
7261 7108 };
7262 "unicode-5.2.0-0.7.5" = {
7263 name = "unicode-5.2.0";
7264 packageName = "unicode-5.2.0";
7265 version = "0.7.5";
7266 src = fetchurl {
7267 url = "https://registry.npmjs.org/unicode-5.2.0/-/unicode-5.2.0-0.7.5.tgz";
7268 sha512 = "KVGLW1Bri30x00yv4HNM8kBxoqFXr0Sbo55735nvrlsx4PYBZol3UtoWgO492fSwmsetzPEZzy73rbU8OGXJcA==";
7269 };
7270 };
7271 7109 "union-value-1.0.0" = {
7272 7110 name = "union-value";
7273 7111 packageName = "union-value";
@@ -7439,13 +7277,13 b' let'
7439 7277 sha512 = "1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==";
7440 7278 };
7441 7279 };
7442 "v8flags-3.1.1" = {
7280 "v8flags-3.1.2" = {
7443 7281 name = "v8flags";
7444 7282 packageName = "v8flags";
7445 version = "3.1.1";
7446 src = fetchurl {
7447 url = "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz";
7448 sha512 = "iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==";
7283 version = "3.1.2";
7284 src = fetchurl {
7285 url = "https://registry.npmjs.org/v8flags/-/v8flags-3.1.2.tgz";
7286 sha512 = "MtivA7GF24yMPte9Rp/BWGCYQNaUj86zeYxV/x2RRJMKagImbbv3u8iJC57lNhWLPcGLJmHcHmFWkNsplbbLWw==";
7449 7287 };
7450 7288 };
7451 7289 "vendors-1.0.2" = {
@@ -7583,15 +7421,6 b' let'
7583 7421 sha1 = "5438cd2ea93b202efa3a19fe8887aee7c94f9c9d";
7584 7422 };
7585 7423 };
7586 "winston-2.4.4" = {
7587 name = "winston";
7588 packageName = "winston";
7589 version = "2.4.4";
7590 src = fetchurl {
7591 url = "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz";
7592 sha512 = "NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==";
7593 };
7594 };
7595 7424 "wordwrap-0.0.2" = {
7596 7425 name = "wordwrap";
7597 7426 packageName = "wordwrap";
@@ -7682,15 +7511,6 b' let'
7682 7511 sha512 = "C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==";
7683 7512 };
7684 7513 };
7685 "yauzl-2.4.1" = {
7686 name = "yauzl";
7687 packageName = "yauzl";
7688 version = "2.4.1";
7689 src = fetchurl {
7690 url = "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz";
7691 sha1 = "9528f442dab1b2284e58b4379bb194e22e0c4005";
7692 };
7693 };
7694 7514 };
7695 7515 args = {
7696 7516 name = "rhodecode-enterprise";
@@ -7743,7 +7563,7 b' let'
7743 7563 sources."@webassemblyjs/wasm-parser-1.7.10"
7744 7564 sources."@webassemblyjs/wast-parser-1.7.10"
7745 7565 sources."@webassemblyjs/wast-printer-1.7.10"
7746 sources."@webcomponents/shadycss-1.6.0"
7566 sources."@webcomponents/shadycss-1.7.1"
7747 7567 sources."@webcomponents/webcomponentsjs-2.2.1"
7748 7568 sources."@xtuc/ieee754-1.2.0"
7749 7569 sources."@xtuc/long-4.2.1"
@@ -7818,6 +7638,7 b' let'
7818 7638 })
7819 7639 (sources."babel-core-6.26.3" // {
7820 7640 dependencies = [
7641 sources."json5-0.5.1"
7821 7642 sources."lodash-4.17.11"
7822 7643 sources."minimatch-3.0.4"
7823 7644 ];
@@ -7916,7 +7737,7 b' let'
7916 7737 })
7917 7738 sources."base64-js-1.3.0"
7918 7739 sources."bcrypt-pbkdf-1.0.2"
7919 sources."big.js-3.2.0"
7740 sources."big.js-5.2.2"
7920 7741 sources."binary-extensions-1.12.0"
7921 7742 sources."bluebird-3.5.3"
7922 7743 sources."bn.js-4.11.8"
@@ -7946,7 +7767,7 b' let'
7946 7767 sources."graceful-fs-4.1.15"
7947 7768 sources."lru-cache-4.1.5"
7948 7769 sources."minimatch-3.0.4"
7949 sources."rimraf-2.6.2"
7770 sources."rimraf-2.6.3"
7950 7771 ];
7951 7772 })
7952 7773 sources."cache-base-1.0.1"
@@ -7957,8 +7778,8 b' let'
7957 7778 sources."browserslist-1.7.7"
7958 7779 ];
7959 7780 })
7960 sources."caniuse-db-1.0.30000912"
7961 sources."caniuse-lite-1.0.30000912"
7781 sources."caniuse-db-1.0.30000927"
7782 sources."caniuse-lite-1.0.30000927"
7962 7783 sources."caseless-0.12.0"
7963 7784 sources."center-align-0.1.3"
7964 7785 sources."chalk-0.5.1"
@@ -8045,7 +7866,7 b' let'
8045 7866 dependencies = [
8046 7867 sources."glob-7.1.3"
8047 7868 sources."minimatch-3.0.4"
8048 sources."rimraf-2.6.2"
7869 sources."rimraf-2.6.3"
8049 7870 ];
8050 7871 })
8051 7872 sources."copy-descriptor-0.1.1"
@@ -8055,7 +7876,7 b' let'
8055 7876 sources."minimatch-3.0.4"
8056 7877 ];
8057 7878 })
8058 sources."core-js-2.5.7"
7879 sources."core-js-2.6.1"
8059 7880 sources."core-util-is-1.0.2"
8060 7881 sources."create-ecdh-4.0.3"
8061 7882 sources."create-hash-1.2.0"
@@ -8079,7 +7900,6 b' let'
8079 7900 sources."cssesc-0.1.0"
8080 7901 sources."cssnano-3.10.0"
8081 7902 sources."csso-2.3.2"
8082 sources."cycle-1.0.3"
8083 7903 sources."cyclist-0.2.2"
8084 7904 (sources."dashdash-1.14.1" // {
8085 7905 dependencies = [
@@ -8116,7 +7936,7 b' let'
8116 7936 ];
8117 7937 })
8118 7938 sources."domain-browser-1.2.0"
8119 sources."domelementtype-1.3.0"
7939 sources."domelementtype-1.3.1"
8120 7940 sources."domhandler-2.3.0"
8121 7941 sources."domutils-1.5.1"
8122 7942 (sources."duplexify-3.6.1" // {
@@ -8126,7 +7946,7 b' let'
8126 7946 ];
8127 7947 })
8128 7948 sources."ecc-jsbn-0.1.2"
8129 sources."electron-to-chromium-1.3.85"
7949 sources."electron-to-chromium-1.3.98"
8130 7950 sources."elliptic-6.4.1"
8131 7951 sources."emojis-list-2.1.0"
8132 7952 sources."end-of-stream-1.4.1"
@@ -8137,9 +7957,8 b' let'
8137 7957 })
8138 7958 sources."entities-1.0.0"
8139 7959 sources."errno-0.1.7"
8140 sources."es-abstract-1.12.0"
7960 sources."es-abstract-1.13.0"
8141 7961 sources."es-to-primitive-1.2.0"
8142 sources."es6-promise-4.2.5"
8143 7962 sources."es6-templates-0.2.3"
8144 7963 sources."escape-string-regexp-1.0.5"
8145 7964 sources."eslint-scope-4.0.0"
@@ -8151,7 +7970,7 b' let'
8151 7970 sources."eventemitter2-0.4.14"
8152 7971 sources."events-1.1.1"
8153 7972 sources."evp_bytestokey-1.0.3"
8154 sources."execa-0.10.0"
7973 sources."execa-1.0.0"
8155 7974 sources."exit-0.1.2"
8156 7975 (sources."expand-brackets-2.1.4" // {
8157 7976 dependencies = [
@@ -8185,15 +8004,12 b' let'
8185 8004 sources."extend-shallow-2.0.1"
8186 8005 ];
8187 8006 })
8188 sources."extract-zip-1.6.7"
8189 8007 sources."extsprintf-1.3.0"
8190 sources."eyes-0.1.8"
8191 8008 sources."fast-deep-equal-2.0.1"
8192 8009 sources."fast-json-stable-stringify-2.0.0"
8193 8010 sources."fastparse-1.1.2"
8194 8011 sources."favico.js-0.3.10"
8195 8012 sources."faye-websocket-0.4.4"
8196 sources."fd-slicer-1.0.1"
8197 8013 sources."file-sync-cmp-0.1.1"
8198 8014 (sources."fill-range-4.0.0" // {
8199 8015 dependencies = [
@@ -8209,8 +8025,8 b' let'
8209 8025 sources."minimatch-0.3.0"
8210 8026 ];
8211 8027 })
8212 sources."fined-1.1.0"
8213 sources."flagged-respawn-1.0.0"
8028 sources."fined-1.1.1"
8029 sources."flagged-respawn-1.0.1"
8214 8030 sources."flatten-1.0.2"
8215 8031 (sources."flush-write-stream-1.0.3" // {
8216 8032 dependencies = [
@@ -8229,11 +8045,6 b' let'
8229 8045 sources."string_decoder-1.1.1"
8230 8046 ];
8231 8047 })
8232 (sources."fs-extra-1.0.0" // {
8233 dependencies = [
8234 sources."graceful-fs-4.1.15"
8235 ];
8236 })
8237 8048 (sources."fs-write-stream-atomic-1.0.10" // {
8238 8049 dependencies = [
8239 8050 sources."graceful-fs-4.1.15"
@@ -8244,7 +8055,11 b' let'
8244 8055 sources."function-bind-1.1.1"
8245 8056 sources."gaze-0.5.2"
8246 8057 sources."get-caller-file-1.0.3"
8247 sources."get-stream-3.0.0"
8058 (sources."get-stream-4.1.0" // {
8059 dependencies = [
8060 sources."pump-3.0.0"
8061 ];
8062 })
8248 8063 sources."get-value-2.0.6"
8249 8064 sources."getobject-0.1.0"
8250 8065 (sources."getpass-0.1.7" // {
@@ -8259,7 +8074,7 b' let'
8259 8074 })
8260 8075 sources."glob-parent-3.1.0"
8261 8076 sources."global-modules-1.0.0"
8262 sources."global-modules-path-2.3.0"
8077 sources."global-modules-path-2.3.1"
8263 8078 (sources."global-prefix-1.0.2" // {
8264 8079 dependencies = [
8265 8080 sources."which-1.3.1"
@@ -8351,8 +8166,7 b' let'
8351 8166 ];
8352 8167 })
8353 8168 sources."hash-base-3.0.4"
8354 sources."hash.js-1.1.5"
8355 sources."hasha-2.2.0"
8169 sources."hash.js-1.1.7"
8356 8170 sources."hawk-3.1.3"
8357 8171 sources."he-1.2.0"
8358 8172 sources."hmac-drbg-1.0.1"
@@ -8369,6 +8183,8 b' let'
8369 8183 })
8370 8184 (sources."html-webpack-plugin-3.2.0" // {
8371 8185 dependencies = [
8186 sources."big.js-3.2.0"
8187 sources."json5-0.5.1"
8372 8188 sources."loader-utils-0.2.17"
8373 8189 sources."lodash-4.17.11"
8374 8190 ];
@@ -8381,7 +8197,7 b' let'
8381 8197 (sources."icss-utils-2.1.0" // {
8382 8198 dependencies = [
8383 8199 sources."ansi-styles-3.2.1"
8384 sources."chalk-2.4.1"
8200 sources."chalk-2.4.2"
8385 8201 sources."postcss-6.0.23"
8386 8202 sources."source-map-0.6.1"
8387 8203 sources."supports-color-5.5.0"
@@ -8395,7 +8211,7 b' let'
8395 8211 dependencies = [
8396 8212 sources."find-up-3.0.0"
8397 8213 sources."locate-path-3.0.0"
8398 sources."p-limit-2.0.0"
8214 sources."p-limit-2.1.0"
8399 8215 sources."p-locate-3.0.0"
8400 8216 sources."p-try-2.0.0"
8401 8217 sources."pkg-dir-3.0.0"
@@ -8445,12 +8261,12 b' let'
8445 8261 sources."isobject-3.0.1"
8446 8262 sources."isstream-0.1.2"
8447 8263 sources."jquery-1.11.3"
8448 sources."js-base64-2.4.9"
8264 sources."js-base64-2.5.0"
8449 8265 sources."js-tokens-3.0.2"
8450 8266 sources."js-yaml-2.0.5"
8451 8267 sources."jsbn-0.1.1"
8452 8268 sources."jsesc-1.3.0"
8453 (sources."jshint-2.9.6" // {
8269 (sources."jshint-2.9.7" // {
8454 8270 dependencies = [
8455 8271 sources."lodash-4.17.11"
8456 8272 sources."minimatch-3.0.4"
@@ -8461,25 +8277,14 b' let'
8461 8277 sources."json-schema-traverse-0.4.1"
8462 8278 sources."json-stable-stringify-1.0.1"
8463 8279 sources."json-stringify-safe-5.0.1"
8464 sources."json5-0.5.1"
8465 (sources."jsonfile-2.4.0" // {
8466 dependencies = [
8467 sources."graceful-fs-4.1.15"
8468 ];
8469 })
8280 sources."json5-1.0.1"
8470 8281 sources."jsonify-0.0.0"
8471 8282 (sources."jsprim-1.4.1" // {
8472 8283 dependencies = [
8473 8284 sources."assert-plus-1.0.0"
8474 8285 ];
8475 8286 })
8476 sources."kew-0.7.0"
8477 8287 sources."kind-of-6.0.2"
8478 (sources."klaw-1.3.1" // {
8479 dependencies = [
8480 sources."graceful-fs-4.1.15"
8481 ];
8482 })
8483 8288 sources."lazy-cache-1.0.4"
8484 8289 sources."lcid-2.0.0"
8485 8290 (sources."less-2.7.3" // {
@@ -8493,7 +8298,7 b' let'
8493 8298 ];
8494 8299 })
8495 8300 sources."loader-runner-2.3.1"
8496 sources."loader-utils-1.1.0"
8301 sources."loader-utils-1.2.3"
8497 8302 sources."locate-path-2.0.0"
8498 8303 sources."lodash-0.9.2"
8499 8304 sources."lodash.camelcase-4.3.0"
@@ -8510,6 +8315,7 b' let'
8510 8315 sources."map-age-cleaner-0.1.3"
8511 8316 sources."map-cache-0.2.2"
8512 8317 sources."map-visit-1.0.0"
8318 sources."mark.js-8.11.1"
8513 8319 sources."math-expression-evaluator-1.2.17"
8514 8320 sources."md5.js-1.3.5"
8515 8321 sources."mem-4.0.0"
@@ -8528,25 +8334,29 b' let'
8528 8334 sources."minimalistic-assert-1.0.1"
8529 8335 sources."minimalistic-crypto-utils-1.0.1"
8530 8336 sources."minimatch-0.2.14"
8531 sources."minimist-0.0.8"
8337 sources."minimist-1.2.0"
8532 8338 sources."mississippi-2.0.0"
8533 8339 (sources."mixin-deep-1.3.1" // {
8534 8340 dependencies = [
8535 8341 sources."is-extendable-1.0.1"
8536 8342 ];
8537 8343 })
8538 sources."mkdirp-0.5.1"
8539 sources."moment-2.22.2"
8344 (sources."mkdirp-0.5.1" // {
8345 dependencies = [
8346 sources."minimist-0.0.8"
8347 ];
8348 })
8349 sources."moment-2.23.0"
8540 8350 sources."mousetrap-1.6.2"
8541 8351 (sources."move-concurrently-1.0.1" // {
8542 8352 dependencies = [
8543 8353 sources."glob-7.1.3"
8544 8354 sources."minimatch-3.0.4"
8545 sources."rimraf-2.6.2"
8355 sources."rimraf-2.6.3"
8546 8356 ];
8547 8357 })
8548 8358 sources."ms-2.0.0"
8549 sources."nan-2.11.1"
8359 sources."nan-2.12.1"
8550 8360 sources."nanomatch-1.2.13"
8551 8361 sources."neo-async-2.6.0"
8552 8362 sources."nice-try-1.0.5"
@@ -8598,7 +8408,7 b' let'
8598 8408 sources."once-1.4.0"
8599 8409 sources."os-browserify-0.3.0"
8600 8410 sources."os-homedir-1.0.2"
8601 sources."os-locale-3.0.1"
8411 sources."os-locale-3.1.0"
8602 8412 sources."os-tmpdir-1.0.2"
8603 8413 sources."osenv-0.1.5"
8604 8414 sources."p-defer-1.0.0"
@@ -8635,22 +8445,13 b' let'
8635 8445 sources."path-root-regex-0.1.2"
8636 8446 sources."path-type-3.0.0"
8637 8447 sources."pbkdf2-3.0.17"
8638 sources."pend-1.2.0"
8639 8448 sources."performance-now-0.2.0"
8640 sources."phantom-4.0.12"
8641 (sources."phantomjs-prebuilt-2.1.16" // {
8642 dependencies = [
8643 sources."which-1.3.1"
8644 ];
8645 })
8646 8449 sources."pify-3.0.0"
8647 sources."pinkie-2.0.4"
8648 sources."pinkie-promise-2.0.1"
8649 8450 sources."pkg-dir-2.0.0"
8650 8451 (sources."polymer-webpack-loader-2.0.3" // {
8651 8452 dependencies = [
8652 8453 sources."ansi-styles-3.2.1"
8653 sources."chalk-2.4.1"
8454 sources."chalk-2.4.2"
8654 8455 sources."html-loader-0.5.5"
8655 8456 (sources."postcss-6.0.23" // {
8656 8457 dependencies = [
@@ -8700,7 +8501,7 b' let'
8700 8501 (sources."postcss-modules-extract-imports-1.2.1" // {
8701 8502 dependencies = [
8702 8503 sources."ansi-styles-3.2.1"
8703 sources."chalk-2.4.1"
8504 sources."chalk-2.4.2"
8704 8505 sources."postcss-6.0.23"
8705 8506 sources."source-map-0.6.1"
8706 8507 sources."supports-color-5.5.0"
@@ -8709,7 +8510,7 b' let'
8709 8510 (sources."postcss-modules-local-by-default-1.2.0" // {
8710 8511 dependencies = [
8711 8512 sources."ansi-styles-3.2.1"
8712 sources."chalk-2.4.1"
8513 sources."chalk-2.4.2"
8713 8514 sources."postcss-6.0.23"
8714 8515 sources."source-map-0.6.1"
8715 8516 sources."supports-color-5.5.0"
@@ -8718,7 +8519,7 b' let'
8718 8519 (sources."postcss-modules-scope-1.1.0" // {
8719 8520 dependencies = [
8720 8521 sources."ansi-styles-3.2.1"
8721 sources."chalk-2.4.1"
8522 sources."chalk-2.4.2"
8722 8523 sources."postcss-6.0.23"
8723 8524 sources."source-map-0.6.1"
8724 8525 sources."supports-color-5.5.0"
@@ -8727,7 +8528,7 b' let'
8727 8528 (sources."postcss-modules-values-1.3.0" // {
8728 8529 dependencies = [
8729 8530 sources."ansi-styles-3.2.1"
8730 sources."chalk-2.4.1"
8531 sources."chalk-2.4.2"
8731 8532 sources."postcss-6.0.23"
8732 8533 sources."source-map-0.6.1"
8733 8534 sources."supports-color-5.5.0"
@@ -8749,7 +8550,6 b' let'
8749 8550 sources."private-0.1.8"
8750 8551 sources."process-0.11.10"
8751 8552 sources."process-nextick-args-2.0.0"
8752 sources."progress-1.1.8"
8753 8553 sources."promise-7.3.1"
8754 8554 sources."promise-inflight-1.0.1"
8755 8555 sources."prr-1.0.1"
@@ -8823,10 +8623,9 b' let'
8823 8623 sources."repeat-string-1.6.1"
8824 8624 sources."repeating-2.0.1"
8825 8625 sources."request-2.81.0"
8826 sources."request-progress-2.0.1"
8827 8626 sources."require-directory-2.1.1"
8828 8627 sources."require-main-filename-1.0.1"
8829 sources."resolve-1.8.1"
8628 sources."resolve-1.9.0"
8830 8629 sources."resolve-cwd-2.0.0"
8831 8630 sources."resolve-dir-1.0.1"
8832 8631 sources."resolve-from-3.0.0"
@@ -8842,12 +8641,12 b' let'
8842 8641 sources."sax-1.2.4"
8843 8642 (sources."schema-utils-0.4.7" // {
8844 8643 dependencies = [
8845 sources."ajv-6.6.1"
8644 sources."ajv-6.6.2"
8846 8645 ];
8847 8646 })
8848 8647 sources."select-1.1.2"
8849 8648 sources."semver-5.6.0"
8850 sources."serialize-javascript-1.5.0"
8649 sources."serialize-javascript-1.6.1"
8851 8650 sources."set-blocking-2.0.0"
8852 8651 (sources."set-value-2.0.0" // {
8853 8652 dependencies = [
@@ -8897,16 +8696,14 b' let'
8897 8696 sources."source-map-resolve-0.5.2"
8898 8697 sources."source-map-support-0.4.18"
8899 8698 sources."source-map-url-0.4.0"
8900 sources."split-1.0.1"
8901 8699 sources."split-string-3.1.0"
8902 8700 sources."sprintf-js-1.0.3"
8903 (sources."sshpk-1.15.2" // {
8701 (sources."sshpk-1.16.0" // {
8904 8702 dependencies = [
8905 8703 sources."assert-plus-1.0.0"
8906 8704 ];
8907 8705 })
8908 8706 sources."ssri-5.3.0"
8909 sources."stack-trace-0.0.10"
8910 8707 (sources."static-extend-0.1.2" // {
8911 8708 dependencies = [
8912 8709 sources."define-property-0.2.5"
@@ -8963,7 +8760,6 b' let'
8963 8760 ];
8964 8761 })
8965 8762 sources."tapable-1.1.1"
8966 sources."throttleit-1.0.0"
8967 8763 sources."through-2.3.8"
8968 8764 (sources."through2-2.0.5" // {
8969 8765 dependencies = [
@@ -8993,9 +8789,11 b' let'
8993 8789 sources."trim-right-1.0.1"
8994 8790 (sources."ts-loader-1.3.3" // {
8995 8791 dependencies = [
8996 sources."colors-1.3.2"
8792 sources."big.js-3.2.0"
8793 sources."colors-1.3.3"
8997 8794 sources."enhanced-resolve-3.4.1"
8998 8795 sources."graceful-fs-4.1.15"
8796 sources."json5-0.5.1"
8999 8797 sources."loader-utils-0.2.17"
9000 8798 sources."tapable-0.2.9"
9001 8799 ];
@@ -9025,7 +8823,6 b' let'
9025 8823 sources."unc-path-regex-0.1.2"
9026 8824 sources."underscore-1.7.0"
9027 8825 sources."underscore.string-2.2.1"
9028 sources."unicode-5.2.0-0.7.5"
9029 8826 (sources."union-value-1.0.0" // {
9030 8827 dependencies = [
9031 8828 sources."extend-shallow-2.0.1"
@@ -9066,7 +8863,7 b' let'
9066 8863 sources."utila-0.4.0"
9067 8864 sources."uuid-3.3.2"
9068 8865 sources."v8-compile-cache-2.0.2"
9069 sources."v8flags-3.1.1"
8866 sources."v8flags-3.1.2"
9070 8867 sources."vendors-1.0.2"
9071 8868 (sources."verror-1.10.0" // {
9072 8869 dependencies = [
@@ -9082,13 +8879,13 b' let'
9082 8879 sources."waypoints-4.0.1"
9083 8880 (sources."webpack-4.23.1" // {
9084 8881 dependencies = [
9085 sources."ajv-6.6.1"
8882 sources."ajv-6.6.2"
9086 8883 ];
9087 8884 })
9088 8885 (sources."webpack-cli-3.1.2" // {
9089 8886 dependencies = [
9090 8887 sources."ansi-styles-3.2.1"
9091 sources."chalk-2.4.1"
8888 sources."chalk-2.4.2"
9092 8889 sources."supports-color-5.5.0"
9093 8890 ];
9094 8891 })
@@ -9121,12 +8918,6 b' let'
9121 8918 sources."which-1.0.9"
9122 8919 sources."which-module-2.0.0"
9123 8920 sources."window-size-0.1.0"
9124 (sources."winston-2.4.4" // {
9125 dependencies = [
9126 sources."async-1.0.0"
9127 sources."colors-1.0.3"
9128 ];
9129 })
9130 8921 sources."wordwrap-0.0.2"
9131 8922 sources."worker-farm-1.6.0"
9132 8923 (sources."wrap-ansi-2.1.0" // {
@@ -9144,13 +8935,12 b' let'
9144 8935 dependencies = [
9145 8936 sources."find-up-3.0.0"
9146 8937 sources."locate-path-3.0.0"
9147 sources."p-limit-2.0.0"
8938 sources."p-limit-2.1.0"
9148 8939 sources."p-locate-3.0.0"
9149 8940 sources."p-try-2.0.0"
9150 8941 ];
9151 8942 })
9152 8943 sources."yargs-parser-11.1.1"
9153 sources."yauzl-2.4.1"
9154 8944 ];
9155 8945 buildInputs = globalBuildInputs;
9156 8946 meta = {
@@ -219,11 +219,11 b' self: super: {'
219 219 };
220 220 };
221 221 "click" = super.buildPythonPackage {
222 name = "click-6.6";
222 name = "click-7.0";
223 223 doCheck = false;
224 224 src = fetchurl {
225 url = "https://files.pythonhosted.org/packages/7a/00/c14926d8232b36b08218067bcd5853caefb4737cda3f0a47437151344792/click-6.6.tar.gz";
226 sha256 = "1sggipyz52crrybwbr9xvwxd4aqigvplf53k9w3ygxmzivd1jsnc";
225 url = "https://files.pythonhosted.org/packages/f8/5c/f60e9d8a1e77005f664b76ff8aeaee5bc05d0a91798afd7f53fc998dbc47/Click-7.0.tar.gz";
226 sha256 = "1mzjixd4vjbjvzb6vylki9w1556a9qmdh35kzmq6cign46av952v";
227 227 };
228 228 meta = {
229 229 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -260,11 +260,11 b' self: super: {'
260 260 };
261 261 };
262 262 "configparser" = super.buildPythonPackage {
263 name = "configparser-3.5.0";
263 name = "configparser-3.7.1";
264 264 doCheck = false;
265 265 src = fetchurl {
266 url = "https://files.pythonhosted.org/packages/7c/69/c2ce7e91c89dc073eb1aa74c0621c3eefbffe8216b3f9af9d3885265c01c/configparser-3.5.0.tar.gz";
267 sha256 = "0fi7vf09vi1588jd8f16a021m5y6ih2hy7rpbjb408xw45qb822k";
266 url = "https://files.pythonhosted.org/packages/b6/a6/eceea7c5a5dbcf56815bed411c38cabd8a879386be10717b160e7362b5a2/configparser-3.7.1.tar.gz";
267 sha256 = "0cnz213il9lhgda6x70fw7mfqr8da43s3wm343lwzhqx94mgmmav";
268 268 };
269 269 meta = {
270 270 license = [ pkgs.lib.licenses.mit ];
@@ -374,11 +374,14 b' self: super: {'
374 374 };
375 375 };
376 376 "dogpile.cache" = super.buildPythonPackage {
377 name = "dogpile.cache-0.6.7";
377 name = "dogpile.cache-0.7.1";
378 378 doCheck = false;
379 propagatedBuildInputs = [
380 self."decorator"
381 ];
379 382 src = fetchurl {
380 url = "https://files.pythonhosted.org/packages/ee/bd/440da735a11c6087eed7cc8747fc4b995cbac2464168682f8ee1c8e43844/dogpile.cache-0.6.7.tar.gz";
381 sha256 = "1aw8rx8vhb75y7zc6gi67g21sw057jdx7i8m3jq7kf3nqavxx9zw";
383 url = "https://files.pythonhosted.org/packages/84/3e/dbf1cfc5228f1d3dca80ef714db2c5aaec5cd9efaf54d7e3daef6bc48b19/dogpile.cache-0.7.1.tar.gz";
384 sha256 = "0caazmrzhnfqb5yrp8myhw61ny637jj69wcngrpbvi31jlcpy6v9";
382 385 };
383 386 meta = {
384 387 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -407,30 +410,75 b' self: super: {'
407 410 };
408 411 };
409 412 "elasticsearch" = super.buildPythonPackage {
410 name = "elasticsearch-2.3.0";
413 name = "elasticsearch-6.3.1";
411 414 doCheck = false;
412 415 propagatedBuildInputs = [
413 416 self."urllib3"
414 417 ];
415 418 src = fetchurl {
416 url = "https://files.pythonhosted.org/packages/10/35/5fd52c5f0b0ee405ed4b5195e8bce44c5e041787680dc7b94b8071cac600/elasticsearch-2.3.0.tar.gz";
417 sha256 = "10ad2dk73xsys9vajwsncibs69asa63w1hgwz6lz1prjpyi80c5y";
419 url = "https://files.pythonhosted.org/packages/9d/ce/c4664e8380e379a9402ecfbaf158e56396da90d520daba21cfa840e0eb71/elasticsearch-6.3.1.tar.gz";
420 sha256 = "12y93v0yn7a4xmf969239g8gb3l4cdkclfpbk1qc8hx5qkymrnma";
418 421 };
419 422 meta = {
420 423 license = [ pkgs.lib.licenses.asl20 ];
421 424 };
422 425 };
423 426 "elasticsearch-dsl" = super.buildPythonPackage {
424 name = "elasticsearch-dsl-2.2.0";
427 name = "elasticsearch-dsl-6.3.1";
425 428 doCheck = false;
426 429 propagatedBuildInputs = [
427 430 self."six"
428 431 self."python-dateutil"
429 432 self."elasticsearch"
433 self."ipaddress"
434 ];
435 src = fetchurl {
436 url = "https://files.pythonhosted.org/packages/4c/0d/1549f50c591db6bb4e66cbcc8d34a6e537c3d89aa426b167c244fd46420a/elasticsearch-dsl-6.3.1.tar.gz";
437 sha256 = "1gh8a0shqi105k325hgwb9avrpdjh0mc6mxwfg9ba7g6lssb702z";
438 };
439 meta = {
440 license = [ pkgs.lib.licenses.asl20 ];
441 };
442 };
443 "elasticsearch1" = super.buildPythonPackage {
444 name = "elasticsearch1-1.10.0";
445 doCheck = false;
446 propagatedBuildInputs = [
447 self."urllib3"
430 448 ];
431 449 src = fetchurl {
432 url = "https://files.pythonhosted.org/packages/66/2f/52a086968788e58461641570f45c3207a52d46ebbe9b77dc22b6a8ffda66/elasticsearch-dsl-2.2.0.tar.gz";
433 sha256 = "1g4kxzxsdwlsl2a9kscmx11pafgimhj7y8wrfksv8pgvpkfb9fwr";
450 url = "https://files.pythonhosted.org/packages/a6/eb/73e75f9681fa71e3157b8ee878534235d57f24ee64f0e77f8d995fb57076/elasticsearch1-1.10.0.tar.gz";
451 sha256 = "0g89444kd5zwql4vbvyrmi2m6l6dcj6ga98j4hqxyyyz6z20aki2";
452 };
453 meta = {
454 license = [ pkgs.lib.licenses.asl20 ];
455 };
456 };
457 "elasticsearch1-dsl" = super.buildPythonPackage {
458 name = "elasticsearch1-dsl-0.0.12";
459 doCheck = false;
460 propagatedBuildInputs = [
461 self."six"
462 self."python-dateutil"
463 self."elasticsearch1"
464 ];
465 src = fetchurl {
466 url = "https://files.pythonhosted.org/packages/eb/9d/785342775cb10eddc9b8d7457d618a423b4f0b89d8b2b2d1bc27190d71db/elasticsearch1-dsl-0.0.12.tar.gz";
467 sha256 = "0ig1ly39v93hba0z975wnhbmzwj28w6w1sqlr2g7cn5spp732bhk";
468 };
469 meta = {
470 license = [ pkgs.lib.licenses.asl20 ];
471 };
472 };
473 "elasticsearch2" = super.buildPythonPackage {
474 name = "elasticsearch2-2.5.0";
475 doCheck = false;
476 propagatedBuildInputs = [
477 self."urllib3"
478 ];
479 src = fetchurl {
480 url = "https://files.pythonhosted.org/packages/84/77/63cf63d4ba11d913b5278406f2a37b0712bec6fc85edfb6151a33eaeba25/elasticsearch2-2.5.0.tar.gz";
481 sha256 = "0ky0q16lbvz022yv6q3pix7aamf026p1y994537ccjf0p0dxnbxr";
434 482 };
435 483 meta = {
436 484 license = [ pkgs.lib.licenses.asl20 ];
@@ -517,14 +565,14 b' self: super: {'
517 565 };
518 566 };
519 567 "gevent" = super.buildPythonPackage {
520 name = "gevent-1.3.7";
568 name = "gevent-1.4.0";
521 569 doCheck = false;
522 570 propagatedBuildInputs = [
523 571 self."greenlet"
524 572 ];
525 573 src = fetchurl {
526 url = "https://files.pythonhosted.org/packages/10/c1/9499b146bfa43aa4f1e0ed1bab1bd3209a4861d25650c11725036c731cf5/gevent-1.3.7.tar.gz";
527 sha256 = "0b0fr04qdk1p4sniv87fh8z5psac60x01pv054kpgi94520g81iz";
574 url = "https://files.pythonhosted.org/packages/ed/27/6c49b70808f569b66ec7fac2e78f076e9b204db9cf5768740cff3d5a07ae/gevent-1.4.0.tar.gz";
575 sha256 = "1lchr4akw2jkm5v4kz7bdm4wv3knkfhbfn9vkkz4s5yrkcxzmdqy";
528 576 };
529 577 meta = {
530 578 license = [ pkgs.lib.licenses.mit ];
@@ -673,11 +721,11 b' self: super: {'
673 721 };
674 722 };
675 723 "iso8601" = super.buildPythonPackage {
676 name = "iso8601-0.1.11";
724 name = "iso8601-0.1.12";
677 725 doCheck = false;
678 726 src = fetchurl {
679 url = "https://files.pythonhosted.org/packages/c0/75/c9209ee4d1b5975eb8c2cba4428bde6b61bd55664a98290dd015cdb18e98/iso8601-0.1.11.tar.gz";
680 sha256 = "0c7gh3lsdjds262h0v1sqc66l7hqgfwbakn96qrhdbl0i3vm5yz8";
727 url = "https://files.pythonhosted.org/packages/45/13/3db24895497345fb44c4248c08b16da34a9eb02643cea2754b21b5ed08b0/iso8601-0.1.12.tar.gz";
728 sha256 = "10nyvvnrhw2w3p09v1ica4lgj6f4g9j3kkfx17qmraiq3w7b5i29";
681 729 };
682 730 meta = {
683 731 license = [ pkgs.lib.licenses.mit ];
@@ -768,14 +816,14 b' self: super: {'
768 816 };
769 817 };
770 818 "kombu" = super.buildPythonPackage {
771 name = "kombu-4.2.0";
819 name = "kombu-4.2.1";
772 820 doCheck = false;
773 821 propagatedBuildInputs = [
774 822 self."amqp"
775 823 ];
776 824 src = fetchurl {
777 url = "https://files.pythonhosted.org/packages/ab/b1/46a7a8babf5e60f3b2ca081a100af8edfcf132078a726375f52a054e70cf/kombu-4.2.0.tar.gz";
778 sha256 = "1yz19qlqf0inl1mnwlpq9j6kj9r67clpy0xg99phyg4329rw80fn";
825 url = "https://files.pythonhosted.org/packages/39/9f/556b988833abede4a80dbd18b2bdf4e8ff4486dd482ed45da961347e8ed2/kombu-4.2.1.tar.gz";
826 sha256 = "10lh3hncvw67fz0k5vgbx3yh9gjfpqdlia1f13i28cgnc1nfrbc6";
779 827 };
780 828 meta = {
781 829 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -818,11 +866,11 b' self: super: {'
818 866 };
819 867 };
820 868 "markupsafe" = super.buildPythonPackage {
821 name = "markupsafe-1.0";
869 name = "markupsafe-1.1.0";
822 870 doCheck = false;
823 871 src = fetchurl {
824 url = "https://files.pythonhosted.org/packages/4d/de/32d741db316d8fdb7680822dd37001ef7a448255de9699ab4bfcbdf4172b/MarkupSafe-1.0.tar.gz";
825 sha256 = "0rdn1s8x9ni7ss8rfiacj7x1085lx8mh2zdwqslnw8xc3l4nkgm6";
872 url = "https://files.pythonhosted.org/packages/ac/7e/1b4c2e05809a4414ebce0892fe1e32c14ace86ca7d50c70f00979ca9b3a3/MarkupSafe-1.1.0.tar.gz";
873 sha256 = "1lxirjypbdd3l9jl4vliilhfnhy7c7f2vlldqg1b0i74khn375sf";
826 874 };
827 875 meta = {
828 876 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -862,14 +910,14 b' self: super: {'
862 910 };
863 911 };
864 912 "more-itertools" = super.buildPythonPackage {
865 name = "more-itertools-4.3.0";
913 name = "more-itertools-5.0.0";
866 914 doCheck = false;
867 915 propagatedBuildInputs = [
868 916 self."six"
869 917 ];
870 918 src = fetchurl {
871 url = "https://files.pythonhosted.org/packages/88/ff/6d485d7362f39880810278bdc906c13300db05485d9c65971dec1142da6a/more-itertools-4.3.0.tar.gz";
872 sha256 = "17h3na0rdh8xq30w4b9pizgkdxmm51896bxw600x84jflg9vaxn4";
919 url = "https://files.pythonhosted.org/packages/dd/26/30fc0d541d9fdf55faf5ba4b0fd68f81d5bd2447579224820ad525934178/more-itertools-5.0.0.tar.gz";
920 sha256 = "1r12cm6mcdwdzz7d47a6g4l437xsvapdlgyhqay3i2nrlv03da9q";
873 921 };
874 922 meta = {
875 923 license = [ pkgs.lib.licenses.mit ];
@@ -960,32 +1008,32 b' self: super: {'
960 1008 };
961 1009 };
962 1010 "paste" = super.buildPythonPackage {
963 name = "paste-2.0.3";
1011 name = "paste-3.0.5";
964 1012 doCheck = false;
965 1013 propagatedBuildInputs = [
966 1014 self."six"
967 1015 ];
968 1016 src = fetchurl {
969 url = "https://files.pythonhosted.org/packages/30/c3/5c2f7c7a02e4f58d4454353fa1c32c94f79fa4e36d07a67c0ac295ea369e/Paste-2.0.3.tar.gz";
970 sha256 = "062jk0nlxf6lb2wwj6zc20rlvrwsnikpkh90y0dn8cjch93s6ii3";
1017 url = "https://files.pythonhosted.org/packages/d4/41/91bde422400786b1b06357c1e6e3a5379f54dc3002aeb337cb767233304e/Paste-3.0.5.tar.gz";
1018 sha256 = "1a6i8fh1fg8r4x800fvy9r82m15clwjim6yf2g9r4dff0y40dchv";
971 1019 };
972 1020 meta = {
973 1021 license = [ pkgs.lib.licenses.mit ];
974 1022 };
975 1023 };
976 1024 "pastedeploy" = super.buildPythonPackage {
977 name = "pastedeploy-1.5.2";
1025 name = "pastedeploy-2.0.1";
978 1026 doCheck = false;
979 1027 src = fetchurl {
980 url = "https://files.pythonhosted.org/packages/0f/90/8e20cdae206c543ea10793cbf4136eb9a8b3f417e04e40a29d72d9922cbd/PasteDeploy-1.5.2.tar.gz";
981 sha256 = "1jz3m4hq8v6hyhfjz9425nd3nvn52cvbfipdcd72krjmla4qz1fm";
1028 url = "https://files.pythonhosted.org/packages/19/a0/5623701df7e2478a68a1b685d1a84518024eef994cde7e4da8449a31616f/PasteDeploy-2.0.1.tar.gz";
1029 sha256 = "02imfbbx1mi2h546f3sr37m47dk9qizaqhzzlhx8bkzxa6fzn8yl";
982 1030 };
983 1031 meta = {
984 1032 license = [ pkgs.lib.licenses.mit ];
985 1033 };
986 1034 };
987 1035 "pastescript" = super.buildPythonPackage {
988 name = "pastescript-2.0.2";
1036 name = "pastescript-3.0.0";
989 1037 doCheck = false;
990 1038 propagatedBuildInputs = [
991 1039 self."paste"
@@ -993,23 +1041,23 b' self: super: {'
993 1041 self."six"
994 1042 ];
995 1043 src = fetchurl {
996 url = "https://files.pythonhosted.org/packages/e5/f0/78e766c3dcc61a4f3a6f71dd8c95168ae9c7a31722b5663d19c1fdf62cb6/PasteScript-2.0.2.tar.gz";
997 sha256 = "1h3nnhn45kf4pbcv669ik4faw04j58k8vbj1hwrc532k0nc28gy0";
1044 url = "https://files.pythonhosted.org/packages/08/2a/3797377a884ab9a064ad4d564ed612e54d26d7997caa8229c9c9df4eac31/PasteScript-3.0.0.tar.gz";
1045 sha256 = "1hvmyz1sbn7ws1syw567ph7km9fi0wi75r3vlyzx6sk0z26xkm6r";
998 1046 };
999 1047 meta = {
1000 1048 license = [ pkgs.lib.licenses.mit ];
1001 1049 };
1002 1050 };
1003 1051 "pathlib2" = super.buildPythonPackage {
1004 name = "pathlib2-2.3.2";
1052 name = "pathlib2-2.3.3";
1005 1053 doCheck = false;
1006 1054 propagatedBuildInputs = [
1007 1055 self."six"
1008 1056 self."scandir"
1009 1057 ];
1010 1058 src = fetchurl {
1011 url = "https://files.pythonhosted.org/packages/db/a8/7d6439c1aec525ed70810abee5b7d7f3aa35347f59bc28343e8f62019aa2/pathlib2-2.3.2.tar.gz";
1012 sha256 = "10yb0iv5x2hs631rcppkhbddx799d3h8pcwmkbh2a66ns3w71ccf";
1059 url = "https://files.pythonhosted.org/packages/bf/d7/a2568f4596b75d2c6e2b4094a7e64f620decc7887f69a1f2811931ea15b9/pathlib2-2.3.3.tar.gz";
1060 sha256 = "0hpp92vqqgcd8h92msm9slv161b1q160igjwnkf2ag6cx0c96695";
1013 1061 };
1014 1062 meta = {
1015 1063 license = [ pkgs.lib.licenses.mit ];
@@ -1084,11 +1132,11 b' self: super: {'
1084 1132 };
1085 1133 };
1086 1134 "pluggy" = super.buildPythonPackage {
1087 name = "pluggy-0.8.0";
1135 name = "pluggy-0.8.1";
1088 1136 doCheck = false;
1089 1137 src = fetchurl {
1090 url = "https://files.pythonhosted.org/packages/65/25/81d0de17cd00f8ca994a4e74e3c4baf7cd25072c0b831dad5c7d9d6138f8/pluggy-0.8.0.tar.gz";
1091 sha256 = "1580p47l2zqzsza8jcnw1h2wh3vvmygk6ly8bvi4w0g8j14sjys4";
1138 url = "https://files.pythonhosted.org/packages/38/e1/83b10c17688af7b2998fa5342fec58ecbd2a5a7499f31e606ae6640b71ac/pluggy-0.8.1.tar.gz";
1139 sha256 = "05l6g42p9ilmabw0hlbiyxy6gyzjri41m5l11a8dzgvi77q35p4d";
1092 1140 };
1093 1141 meta = {
1094 1142 license = [ pkgs.lib.licenses.mit ];
@@ -1110,11 +1158,11 b' self: super: {'
1110 1158 };
1111 1159 };
1112 1160 "psutil" = super.buildPythonPackage {
1113 name = "psutil-5.4.7";
1161 name = "psutil-5.4.8";
1114 1162 doCheck = false;
1115 1163 src = fetchurl {
1116 url = "https://files.pythonhosted.org/packages/7d/9a/1e93d41708f8ed2b564395edfa3389f0fd6d567597401c2e5e2775118d8b/psutil-5.4.7.tar.gz";
1117 sha256 = "0fsgmvzwbdbszkwfnqhib8jcxm4w6zyhvlxlcda0rfm5cyqj4qsv";
1164 url = "https://files.pythonhosted.org/packages/e3/58/0eae6e4466e5abf779d7e2b71fac7fba5f59e00ea36ddb3ed690419ccb0f/psutil-5.4.8.tar.gz";
1165 sha256 = "1hyna338sml2cl1mfb2gs89np18z27mvyhmq4ifh22x07n7mq9kf";
1118 1166 };
1119 1167 meta = {
1120 1168 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1180,25 +1228,25 b' self: super: {'
1180 1228 };
1181 1229 };
1182 1230 "pyasn1" = super.buildPythonPackage {
1183 name = "pyasn1-0.4.4";
1231 name = "pyasn1-0.4.5";
1184 1232 doCheck = false;
1185 1233 src = fetchurl {
1186 url = "https://files.pythonhosted.org/packages/10/46/059775dc8e50f722d205452bced4b3cc965d27e8c3389156acd3b1123ae3/pyasn1-0.4.4.tar.gz";
1187 sha256 = "0drilmx5j25aplfr5wrml0030cs5fgxp9yp94fhllxgx28yjm3zm";
1234 url = "https://files.pythonhosted.org/packages/46/60/b7e32f6ff481b8a1f6c8f02b0fd9b693d1c92ddd2efb038ec050d99a7245/pyasn1-0.4.5.tar.gz";
1235 sha256 = "1xqh3jh2nfi2bflk5a0vn59y3pp1vn54f3ksx652sid92gz2096s";
1188 1236 };
1189 1237 meta = {
1190 1238 license = [ pkgs.lib.licenses.bsdOriginal ];
1191 1239 };
1192 1240 };
1193 1241 "pyasn1-modules" = super.buildPythonPackage {
1194 name = "pyasn1-modules-0.2.2";
1242 name = "pyasn1-modules-0.2.4";
1195 1243 doCheck = false;
1196 1244 propagatedBuildInputs = [
1197 1245 self."pyasn1"
1198 1246 ];
1199 1247 src = fetchurl {
1200 url = "https://files.pythonhosted.org/packages/37/33/74ebdc52be534e683dc91faf263931bc00ae05c6073909fde53999088541/pyasn1-modules-0.2.2.tar.gz";
1201 sha256 = "0ivm850yi7ajjbi8j115qpsj95bgxdsx48nbjzg0zip788c3xkx0";
1248 url = "https://files.pythonhosted.org/packages/bd/a5/ef7bf693e8a8f015386c9167483199f54f8a8ec01d1c737e05524f16e792/pyasn1-modules-0.2.4.tar.gz";
1249 sha256 = "0z3w5dqrrvdplg9ma45j8n23xvyrj9ki8mg4ibqbn7l4qpl90855";
1202 1250 };
1203 1251 meta = {
1204 1252 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1238,30 +1286,16 b' self: super: {'
1238 1286 };
1239 1287 };
1240 1288 "pygments" = super.buildPythonPackage {
1241 name = "pygments-2.3.0";
1289 name = "pygments-2.3.1";
1242 1290 doCheck = false;
1243 1291 src = fetchurl {
1244 url = "https://files.pythonhosted.org/packages/63/a2/91c31c4831853dedca2a08a0f94d788fc26a48f7281c99a303769ad2721b/Pygments-2.3.0.tar.gz";
1245 sha256 = "1z34ms51dh4jq4h3cizp7vd1dmsxcbvffkjsd2xxfav22nn6lrl2";
1292 url = "https://files.pythonhosted.org/packages/64/69/413708eaf3a64a6abb8972644e0f20891a55e621c6759e2c3f3891e05d63/Pygments-2.3.1.tar.gz";
1293 sha256 = "0ji87g09jph8jqcvclgb02qvxasdnr9pzvk90rl66d90yqcxmyjz";
1246 1294 };
1247 1295 meta = {
1248 1296 license = [ pkgs.lib.licenses.bsdOriginal ];
1249 1297 };
1250 1298 };
1251 "pygments-markdown-lexer" = super.buildPythonPackage {
1252 name = "pygments-markdown-lexer-0.1.0.dev39";
1253 doCheck = false;
1254 propagatedBuildInputs = [
1255 self."pygments"
1256 ];
1257 src = fetchurl {
1258 url = "https://files.pythonhosted.org/packages/c3/12/674cdee66635d638cedb2c5d9c85ce507b7b2f91bdba29e482f1b1160ff6/pygments-markdown-lexer-0.1.0.dev39.zip";
1259 sha256 = "1pzb5wy23q3fhs0rqzasjnw6hdzwjngpakb73i98cn0b8lk8q4jc";
1260 };
1261 meta = {
1262 license = [ pkgs.lib.licenses.asl20 ];
1263 };
1264 };
1265 1299 "pymysql" = super.buildPythonPackage {
1266 1300 name = "pymysql-0.8.1";
1267 1301 doCheck = false;
@@ -1285,35 +1319,34 b' self: super: {'
1285 1319 };
1286 1320 };
1287 1321 "pyparsing" = super.buildPythonPackage {
1288 name = "pyparsing-1.5.7";
1322 name = "pyparsing-2.3.0";
1289 1323 doCheck = false;
1290 1324 src = fetchurl {
1291 url = "https://files.pythonhosted.org/packages/6f/2c/47457771c02a8ff0f302b695e094ec309e30452232bd79198ee94fda689f/pyparsing-1.5.7.tar.gz";
1292 sha256 = "17z7ws076z977sclj628fvwrp8y9j2rvdjcsq42v129n1gwi8vk4";
1325 url = "https://files.pythonhosted.org/packages/d0/09/3e6a5eeb6e04467b737d55f8bba15247ac0876f98fae659e58cd744430c6/pyparsing-2.3.0.tar.gz";
1326 sha256 = "14k5v7n3xqw8kzf42x06bzp184spnlkya2dpjyflax6l3yrallzk";
1293 1327 };
1294 1328 meta = {
1295 1329 license = [ pkgs.lib.licenses.mit ];
1296 1330 };
1297 1331 };
1298 1332 "pyramid" = super.buildPythonPackage {
1299 name = "pyramid-1.9.2";
1333 name = "pyramid-1.10.1";
1300 1334 doCheck = false;
1301 1335 propagatedBuildInputs = [
1302 self."setuptools"
1303 self."webob"
1304 self."repoze.lru"
1305 self."zope.interface"
1306 self."zope.deprecation"
1307 self."venusian"
1308 self."translationstring"
1309 self."pastedeploy"
1336 self."hupper"
1310 1337 self."plaster"
1311 1338 self."plaster-pastedeploy"
1312 self."hupper"
1339 self."setuptools"
1340 self."translationstring"
1341 self."venusian"
1342 self."webob"
1343 self."zope.deprecation"
1344 self."zope.interface"
1345 self."repoze.lru"
1313 1346 ];
1314 1347 src = fetchurl {
1315 url = "https://files.pythonhosted.org/packages/a0/c1/b321d07cfc4870541989ad131c86a1d593bfe802af0eca9718a0dadfb97a/pyramid-1.9.2.tar.gz";
1316 sha256 = "09drsl0346nchgxp2j7sa5hlk7mkhfld9wvbd0wicacrp26a92fg";
1348 url = "https://files.pythonhosted.org/packages/0a/3e/22e3ac9be1b70a01139adba8906ee4b8f628bb469fea3c52f6c97b73063c/pyramid-1.10.1.tar.gz";
1349 sha256 = "1h5105nfh6rsrfjiyw20aavyibj36la3hajy6vh1fa77xb4y3hrp";
1317 1350 };
1318 1351 meta = {
1319 1352 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@@ -1335,7 +1368,7 b' self: super: {'
1335 1368 };
1336 1369 };
1337 1370 "pyramid-debugtoolbar" = super.buildPythonPackage {
1338 name = "pyramid-debugtoolbar-4.4";
1371 name = "pyramid-debugtoolbar-4.5";
1339 1372 doCheck = false;
1340 1373 propagatedBuildInputs = [
1341 1374 self."pyramid"
@@ -1345,8 +1378,8 b' self: super: {'
1345 1378 self."ipaddress"
1346 1379 ];
1347 1380 src = fetchurl {
1348 url = "https://files.pythonhosted.org/packages/00/6f/c04eb4e715a7a5a4b24079ab7ffd1dceb1f70b2e24fc17686a2922dbac0a/pyramid_debugtoolbar-4.4.tar.gz";
1349 sha256 = "17p7nxvapvy2hab1rah3ndq2kbs4v83pixj8x2n4m7008ai9lxsz";
1381 url = "https://files.pythonhosted.org/packages/14/28/1f240239af340d19ee271ac62958158c79edb01a44ad8c9885508dd003d2/pyramid_debugtoolbar-4.5.tar.gz";
1382 sha256 = "0x2p3409pnx66n6dx5vc0mk2r1cp1ydr8mp120w44r9pwcngbibl";
1350 1383 };
1351 1384 meta = {
1352 1385 license = [ { fullName = "Repoze Public License"; } pkgs.lib.licenses.bsdOriginal ];
@@ -1519,11 +1552,11 b' self: super: {'
1519 1552 };
1520 1553 };
1521 1554 "python-editor" = super.buildPythonPackage {
1522 name = "python-editor-1.0.3";
1555 name = "python-editor-1.0.4";
1523 1556 doCheck = false;
1524 1557 src = fetchurl {
1525 url = "https://files.pythonhosted.org/packages/65/1e/adf6e000ea5dc909aa420352d6ba37f16434c8a3c2fa030445411a1ed545/python-editor-1.0.3.tar.gz";
1526 sha256 = "0rf5xz8vw93v7mhdcvind7fkykipzga430wkcd7wk892xsn6dh53";
1558 url = "https://files.pythonhosted.org/packages/0a/85/78f4a216d28343a67b7397c99825cff336330893f00601443f7c7b2f2234/python-editor-1.0.4.tar.gz";
1559 sha256 = "0yrjh8w72ivqxi4i7xsg5b1vz15x8fg51xra7c3bgfyxqnyadzai";
1527 1560 };
1528 1561 meta = {
1529 1562 license = [ pkgs.lib.licenses.asl20 { fullName = "Apache"; } ];
@@ -1657,7 +1690,7 b' self: super: {'
1657 1690 };
1658 1691 };
1659 1692 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1660 name = "rhodecode-enterprise-ce-4.15.2";
1693 name = "rhodecode-enterprise-ce-4.16.0";
1661 1694 buildInputs = [
1662 1695 self."pytest"
1663 1696 self."py"
@@ -1668,9 +1701,10 b' self: super: {'
1668 1701 self."pytest-timeout"
1669 1702 self."gprof2dot"
1670 1703 self."mock"
1671 self."webtest"
1672 1704 self."cov-core"
1673 1705 self."coverage"
1706 self."webtest"
1707 self."beautifulsoup4"
1674 1708 self."configobj"
1675 1709 ];
1676 1710 doCheck = true;
@@ -1723,7 +1757,6 b' self: super: {'
1723 1757 self."pycrypto"
1724 1758 self."pycurl"
1725 1759 self."pyflakes"
1726 self."pygments-markdown-lexer"
1727 1760 self."pygments"
1728 1761 self."pyparsing"
1729 1762 self."pyramid-beaker"
@@ -1794,9 +1827,10 b' self: super: {'
1794 1827 self."pytest-timeout"
1795 1828 self."gprof2dot"
1796 1829 self."mock"
1797 self."webtest"
1798 1830 self."cov-core"
1799 1831 self."coverage"
1832 self."webtest"
1833 self."beautifulsoup4"
1800 1834 ];
1801 1835 src = ./.;
1802 1836 meta = {
@@ -1804,7 +1838,7 b' self: super: {'
1804 1838 };
1805 1839 };
1806 1840 "rhodecode-tools" = super.buildPythonPackage {
1807 name = "rhodecode-tools-1.0.1";
1841 name = "rhodecode-tools-1.2.1";
1808 1842 doCheck = false;
1809 1843 propagatedBuildInputs = [
1810 1844 self."click"
@@ -1813,14 +1847,16 b' self: super: {'
1813 1847 self."mako"
1814 1848 self."markupsafe"
1815 1849 self."requests"
1816 self."elasticsearch"
1817 self."elasticsearch-dsl"
1818 1850 self."urllib3"
1819 1851 self."whoosh"
1852 self."elasticsearch"
1853 self."elasticsearch-dsl"
1854 self."elasticsearch2"
1855 self."elasticsearch1-dsl"
1820 1856 ];
1821 1857 src = fetchurl {
1822 url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v1.0.1.tar.gz?md5=ffb5d6bcb855305b93cfe23ad42e500b";
1823 sha256 = "0nr300s4sg685qs4wgbwlplwriawrwi6jq79z37frcnpyc89gpvm";
1858 url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v1.2.1.tar.gz?md5=25bc2f7de1da318e547236d3fb463d28";
1859 sha256 = "1k8l3s4mvshza1zay6dfxprq54fyb5dc85dqdva9wa3f466y0adk";
1824 1860 };
1825 1861 meta = {
1826 1862 license = [ { fullName = "Apache 2.0 and Proprietary"; } ];
@@ -1864,11 +1900,11 b' self: super: {'
1864 1900 };
1865 1901 };
1866 1902 "setuptools" = super.buildPythonPackage {
1867 name = "setuptools-40.6.2";
1903 name = "setuptools-40.8.0";
1868 1904 doCheck = false;
1869 1905 src = fetchurl {
1870 url = "https://files.pythonhosted.org/packages/b0/d1/8acb42f391cba52e35b131e442e80deffbb8d0676b93261d761b1f0ef8fb/setuptools-40.6.2.zip";
1871 sha256 = "0r2c5hapirlzm34h7pl1lgkm6gk7bcrlrdj28qgsvaqg3f74vfw6";
1906 url = "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip";
1907 sha256 = "0k9hifpgahnw2a26w3cr346iy733k6d3nwh3f7g9m13y6f8fqkkf";
1872 1908 };
1873 1909 meta = {
1874 1910 license = [ pkgs.lib.licenses.mit ];
@@ -1897,11 +1933,11 b' self: super: {'
1897 1933 };
1898 1934 };
1899 1935 "simplejson" = super.buildPythonPackage {
1900 name = "simplejson-3.11.1";
1936 name = "simplejson-3.16.0";
1901 1937 doCheck = false;
1902 1938 src = fetchurl {
1903 url = "https://files.pythonhosted.org/packages/08/48/c97b668d6da7d7bebe7ea1817a6f76394b0ec959cb04214ca833c34359df/simplejson-3.11.1.tar.gz";
1904 sha256 = "1rr58dppsq73p0qcd9bsw066cdd3v63sqv7j6sqni8frvm4jv8h1";
1939 url = "https://files.pythonhosted.org/packages/e3/24/c35fb1c1c315fc0fffe61ea00d3f88e85469004713dab488dee4f35b0aff/simplejson-3.16.0.tar.gz";
1940 sha256 = "19cws1syk8jzq2pw43878dv6fjkb0ifvjpx0i9aajix6kc9jkwxi";
1905 1941 };
1906 1942 meta = {
1907 1943 license = [ { fullName = "Academic Free License (AFL)"; } pkgs.lib.licenses.mit ];
@@ -1945,25 +1981,25 b' self: super: {'
1945 1981 };
1946 1982 };
1947 1983 "subprocess32" = super.buildPythonPackage {
1948 name = "subprocess32-3.5.2";
1984 name = "subprocess32-3.5.3";
1949 1985 doCheck = false;
1950 1986 src = fetchurl {
1951 url = "https://files.pythonhosted.org/packages/c3/5f/7117737fc7114061837a4f51670d863dd7f7f9c762a6546fa8a0dcfe61c8/subprocess32-3.5.2.tar.gz";
1952 sha256 = "11v62shwmdys48g7ncs3a8jwwnkcl8d4zcwy6dk73z1zy2f9hazb";
1987 url = "https://files.pythonhosted.org/packages/be/2b/beeba583e9877e64db10b52a96915afc0feabf7144dcbf2a0d0ea68bf73d/subprocess32-3.5.3.tar.gz";
1988 sha256 = "1hr5fan8i719hmlmz73hf8rhq74014w07d8ryg7krvvf6692kj3b";
1953 1989 };
1954 1990 meta = {
1955 1991 license = [ pkgs.lib.licenses.psfl ];
1956 1992 };
1957 1993 };
1958 1994 "supervisor" = super.buildPythonPackage {
1959 name = "supervisor-3.3.4";
1995 name = "supervisor-3.3.5";
1960 1996 doCheck = false;
1961 1997 propagatedBuildInputs = [
1962 1998 self."meld3"
1963 1999 ];
1964 2000 src = fetchurl {
1965 url = "https://files.pythonhosted.org/packages/44/60/698e54b4a4a9b956b2d709b4b7b676119c833d811d53ee2500f1b5e96dc3/supervisor-3.3.4.tar.gz";
1966 sha256 = "0wp62z9xprvz2krg02xnbwcnq6pxfq3byd8cxx8c2d8xznih28i1";
2001 url = "https://files.pythonhosted.org/packages/ba/65/92575a8757ed576beaee59251f64a3287bde82bdc03964b89df9e1d29e1b/supervisor-3.3.5.tar.gz";
2002 sha256 = "1w3ahridzbc6rxfpbyx8lij6pjlcgf2ymzyg53llkjqxalp6sk8v";
1967 2003 };
1968 2004 meta = {
1969 2005 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@@ -2059,11 +2095,11 b' self: super: {'
2059 2095 };
2060 2096 };
2061 2097 "urllib3" = super.buildPythonPackage {
2062 name = "urllib3-1.21";
2098 name = "urllib3-1.24.1";
2063 2099 doCheck = false;
2064 2100 src = fetchurl {
2065 url = "https://files.pythonhosted.org/packages/34/95/7b28259d0006ed681c424cd71a668363265eac92b67dddd018eb9a22bff8/urllib3-1.21.tar.gz";
2066 sha256 = "0irnj4wvh2y36s4q3l2vas9qr9m766w6w418nb490j3mf8a8zw6h";
2101 url = "https://files.pythonhosted.org/packages/b1/53/37d82ab391393565f2f831b8eedbffd57db5a718216f82f1a8b4d381a1c1/urllib3-1.24.1.tar.gz";
2102 sha256 = "08lwd9f3hqznyf32vnzwvp87pchx062nkbgyrf67rwlkgj0jk5fy";
2067 2103 };
2068 2104 meta = {
2069 2105 license = [ pkgs.lib.licenses.mit ];
@@ -2081,22 +2117,22 b' self: super: {'
2081 2117 };
2082 2118 };
2083 2119 "venusian" = super.buildPythonPackage {
2084 name = "venusian-1.1.0";
2120 name = "venusian-1.2.0";
2085 2121 doCheck = false;
2086 2122 src = fetchurl {
2087 url = "https://files.pythonhosted.org/packages/38/24/b4b470ab9e0a2e2e9b9030c7735828c8934b4c6b45befd1bb713ec2aeb2d/venusian-1.1.0.tar.gz";
2088 sha256 = "0zapz131686qm0gazwy8bh11vr57pr89jbwbl50s528sqy9f80lr";
2123 url = "https://files.pythonhosted.org/packages/7e/6f/40a9d43ac77cb51cb62be5b5662d170f43f8037bdc4eab56336c4ca92bb7/venusian-1.2.0.tar.gz";
2124 sha256 = "0ghyx66g8ikx9nx1mnwqvdcqm11i1vlq0hnvwl50s48bp22q5v34";
2089 2125 };
2090 2126 meta = {
2091 2127 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
2092 2128 };
2093 2129 };
2094 2130 "vine" = super.buildPythonPackage {
2095 name = "vine-1.1.4";
2131 name = "vine-1.2.0";
2096 2132 doCheck = false;
2097 2133 src = fetchurl {
2098 url = "https://files.pythonhosted.org/packages/32/23/36284986e011f3c130d802c3c66abd8f1aef371eae110ddf80c5ae22e1ff/vine-1.1.4.tar.gz";
2099 sha256 = "0wkskb2hb494v9gixqnf4bl972p4ibcmxdykzpwjlfa5picns4aj";
2134 url = "https://files.pythonhosted.org/packages/46/1a/c94317efa98040c5d50fe3cf9080cafb0372ff5afb0283dc018c751c6746/vine-1.2.0.tar.gz";
2135 sha256 = "0xjz2sjbr5jrpjk411b7alkghdskhphgsqqrbi7abqfh2pli6j7f";
2100 2136 };
2101 2137 meta = {
2102 2138 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -2182,18 +2218,18 b' self: super: {'
2182 2218 };
2183 2219 };
2184 2220 "webob" = super.buildPythonPackage {
2185 name = "webob-1.7.4";
2221 name = "webob-1.8.4";
2186 2222 doCheck = false;
2187 2223 src = fetchurl {
2188 url = "https://files.pythonhosted.org/packages/75/34/731e23f52371852dfe7490a61644826ba7fe70fd52a377aaca0f4956ba7f/WebOb-1.7.4.tar.gz";
2189 sha256 = "1na01ljg04z40il7vcrn8g29vaw7nvg1xvhk64cr4jys5wcay44d";
2224 url = "https://files.pythonhosted.org/packages/e4/6c/99e322c3d4cc11d9060a67a9bf2f7c9c581f40988c11fffe89bb8c36bc5e/WebOb-1.8.4.tar.gz";
2225 sha256 = "16cfg5y4n6sihz59vsmns2yqbfm0gfsn3l5xgz2g0pdhilaib0x4";
2190 2226 };
2191 2227 meta = {
2192 2228 license = [ pkgs.lib.licenses.mit ];
2193 2229 };
2194 2230 };
2195 2231 "webtest" = super.buildPythonPackage {
2196 name = "webtest-2.0.29";
2232 name = "webtest-2.0.32";
2197 2233 doCheck = false;
2198 2234 propagatedBuildInputs = [
2199 2235 self."six"
@@ -2202,8 +2238,8 b' self: super: {'
2202 2238 self."beautifulsoup4"
2203 2239 ];
2204 2240 src = fetchurl {
2205 url = "https://files.pythonhosted.org/packages/94/de/8f94738be649997da99c47b104aa3c3984ecec51a1d8153ed09638253d56/WebTest-2.0.29.tar.gz";
2206 sha256 = "0bcj1ica5lnmj5zbvk46x28kgphcsgh7sfnwjmn0cr94mhawrg6v";
2241 url = "https://files.pythonhosted.org/packages/27/9f/9e74449d272ffbef4fb3012e6dbc53c0b24822d545e7a33a342f80131e59/WebTest-2.0.32.tar.gz";
2242 sha256 = "0qp0nnbazzm4ibjiyqfcn6f230svk09i4g58zg2i9x1ga06h48a2";
2207 2243 };
2208 2244 meta = {
2209 2245 license = [ pkgs.lib.licenses.mit ];
@@ -1,12 +1,15 b''
1 1 # This file defines how to "build" for packaging.
2 2
3 { doCheck ? false
3 { pkgs ? import <nixpkgs> {}
4 , system ? builtins.currentSystem
5 , doCheck ? false
4 6 }:
5 7
6 8 let
7 9 enterprise_ce = import ./default.nix {
8 10 inherit
9 doCheck;
11 doCheck
12 system;
10 13
11 14 # disable checkPhase for build
12 15 checkPhase = ''
@@ -12,7 +12,7 b' bleach==3.0.2'
12 12 celery==4.1.1
13 13 chameleon==2.24
14 14 channelstream==0.5.2
15 click==6.6
15 click==7.0
16 16 colander==1.5.1
17 17 # our custom configobj
18 18 https://code.rhodecode.com/upstream/configobj/archive/a11ff0a0bd4fbda9e3a91267e720f88329efb4a6.tar.gz?md5=9916c524ea11a6c418217af6b28d4b3c#egg=configobj==5.0.6
@@ -20,7 +20,7 b' cssselect==1.0.3'
20 20 decorator==4.1.2
21 21 deform==2.0.7
22 22 docutils==0.14.0
23 dogpile.cache==0.6.7
23 dogpile.cache==0.7.1
24 24 dogpile.core==0.4.1
25 25 ecdsa==0.13
26 26 formencode==1.2.4
@@ -28,42 +28,41 b' future==0.14.3'
28 28 futures==3.0.2
29 29 gnureadline==6.3.8
30 30 infrae.cache==1.0.1
31 iso8601==0.1.11
31 iso8601==0.1.12
32 32 itsdangerous==0.24
33 33 jinja2==2.9.6
34 34 billiard==3.5.0.3
35 kombu==4.2.0
35 kombu==4.2.1
36 36 lxml==4.2.5
37 37 mako==1.0.7
38 38 markdown==2.6.11
39 markupsafe==1.0.0
39 markupsafe==1.1.0
40 40 msgpack-python==0.5.6
41 41 pyotp==2.2.7
42 42 packaging==15.2
43 paste==2.0.3
44 pastedeploy==1.5.2
45 pastescript==2.0.2
46 pathlib2==2.3.2
43 paste==3.0.5
44 pastedeploy==2.0.1
45 pastescript==3.0.0
46 pathlib2==2.3.3
47 47 peppercorn==0.6
48 psutil==5.4.7
48 psutil==5.4.8
49 49 py-bcrypt==0.4
50 50 pycrypto==2.6.1
51 51 pycurl==7.43.0.2
52 52 pyflakes==0.8.1
53 pygments-markdown-lexer==0.1.0.dev39
54 pygments==2.3.0
55 pyparsing==1.5.7
53 pygments==2.3.1
54 pyparsing==2.3.0
56 55 pyramid-beaker==0.8
57 pyramid-debugtoolbar==4.4.0
56 pyramid-debugtoolbar==4.5.0
58 57 pyramid-jinja2==2.7
59 58 pyramid-mako==1.0.2
60 pyramid==1.9.2
59 pyramid==1.10.1
61 60 pyramid_mailer==0.15.1
62 61 python-dateutil
63 62 python-ldap==3.1.0
64 63 python-memcached==1.59
65 64 python-pam==1.8.4
66 python-saml
65 python-saml==2.4.2
67 66 pytz==2018.4
68 67 tzlocal==1.5.1
69 68 pyzmq==14.6.0
@@ -72,21 +71,21 b' redis==2.10.6'
72 71 repoze.lru==0.7
73 72 requests==2.9.1
74 73 routes==2.4.1
75 simplejson==3.11.1
74 simplejson==3.16.0
76 75 six==1.11.0
77 76 sqlalchemy==1.1.18
78 77 sshpubkeys==2.2.0
79 subprocess32==3.5.2
80 supervisor==3.3.4
78 subprocess32==3.5.3
79 supervisor==3.3.5
81 80 tempita==0.5.2
82 81 translationstring==1.3
83 urllib3==1.21
82 urllib3==1.24.1
84 83 urlobject==2.4.3
85 venusian==1.1.0
84 venusian==1.2.0
86 85 weberror==0.10.3
87 86 webhelpers2==2.0
88 87 webhelpers==1.3
89 webob==1.7.4
88 webob==1.8.4
90 89 whoosh==2.7.4
91 90 wsgiref==0.1.2
92 91 zope.cachedescriptors==4.3.1
@@ -113,7 +112,7 b' invoke==0.13.0'
113 112 bumpversion==0.5.3
114 113
115 114 ## http servers
116 gevent==1.3.7
115 gevent==1.4.0
117 116 greenlet==0.4.15
118 117 gunicorn==19.9.0
119 118 waitress==1.1.0
@@ -124,7 +123,7 b' ipdb==0.11.0'
124 123 ipython==5.1.0
125 124
126 125 ## rhodecode-tools, special case
127 https://code.rhodecode.com/rhodecode-tools-ce/archive/v1.0.1.tar.gz?md5=ffb5d6bcb855305b93cfe23ad42e500b#egg=rhodecode-tools==1.0.1
126 https://code.rhodecode.com/rhodecode-tools-ce/archive/v1.2.1.tar.gz?md5=25bc2f7de1da318e547236d3fb463d28#egg=rhodecode-tools==1.2.1
128 127
129 128 ## appenlight
130 129 appenlight-client==0.6.26
@@ -9,6 +9,8 b' pytest-timeout==1.3.2'
9 9 gprof2dot==2017.9.19
10 10
11 11 mock==1.0.1
12 webtest==2.0.29
13 12 cov-core==1.15.0
14 13 coverage==4.5.1
14
15 webtest==2.0.32
16 beautifulsoup4==4.6.3
@@ -1,1 +1,1 b''
1 4.15.2 No newline at end of file
1 4.16.0 No newline at end of file
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -18,12 +18,6 b''
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 """
22
23 RhodeCode, a web based repository management software
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 """
26
27 21 import os
28 22 import sys
29 23 import platform
@@ -51,7 +45,7 b' PYRAMID_SETTINGS = {}'
51 45 EXTENSIONS = {}
52 46
53 47 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 91 # defines current db version for migrations
48 __dbversion__ = 95 # defines current db version for migrations
55 49 __platform__ = platform.system()
56 50 __license__ = 'AGPLv3, and Commercial License'
57 51 __author__ = 'RhodeCode GmbH'
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -21,6 +21,7 b''
21 21 import inspect
22 22 import itertools
23 23 import logging
24 import sys
24 25 import types
25 26 import fnmatch
26 27
@@ -38,6 +39,7 b' from rhodecode.api.exc import ('
38 39 from rhodecode.apps._base import TemplateArgs
39 40 from rhodecode.lib.auth import AuthUser
40 41 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
42 from rhodecode.lib.exc_tracking import store_exception
41 43 from rhodecode.lib.ext_json import json
42 44 from rhodecode.lib.utils2 import safe_str
43 45 from rhodecode.lib.plugins.utils import get_plugin_settings
@@ -140,15 +142,14 b' def jsonrpc_error(request, message, reti'
140 142 def exception_view(exc, request):
141 143 rpc_id = getattr(request, 'rpc_id', None)
142 144
143 fault_message = 'undefined error'
144 145 if isinstance(exc, JSONRPCError):
145 fault_message = exc.message
146 fault_message = safe_str(exc.message)
146 147 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
147 148 elif isinstance(exc, JSONRPCValidationError):
148 149 colander_exc = exc.colander_exception
149 150 # TODO(marcink): think maybe of nicer way to serialize errors ?
150 151 fault_message = colander_exc.asdict()
151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
152 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
152 153 elif isinstance(exc, JSONRPCForbidden):
153 154 fault_message = 'Access was denied to this resource.'
154 155 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
@@ -170,6 +171,10 b' def exception_view(exc, request):'
170 171
171 172 fault_message = "No such method: {}. Similar methods: {}".format(
172 173 method, similar)
174 else:
175 fault_message = 'undefined error'
176 exc_info = exc.exc_info()
177 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
173 178
174 179 return jsonrpc_error(request, fault_message, rpc_id)
175 180
@@ -292,8 +297,10 b' def request_view(request):'
292 297 raise
293 298 except Exception:
294 299 log.exception('Unhandled exception occurred on api call: %s', func)
295 return jsonrpc_error(request, retid=request.rpc_id,
296 message='Internal server error')
300 exc_info = sys.exc_info()
301 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
302 return jsonrpc_error(
303 request, retid=request.rpc_id, message='Internal server error')
297 304
298 305
299 306 def setup_request(request):
@@ -414,8 +421,7 b' def add_jsonrpc_method(config, view, **k'
414 421
415 422 if method is None:
416 423 raise ConfigurationError(
417 'Cannot register a JSON-RPC method without specifying the '
418 '"method"')
424 'Cannot register a JSON-RPC method without specifying the "method"')
419 425
420 426 # we define custom predicate, to enable to detect conflicting methods,
421 427 # those predicates are kind of "translation" from the decorator variables
@@ -524,6 +530,7 b' def includeme(config):'
524 530
525 531 # match filter by given method only
526 532 config.add_view_predicate('jsonrpc_method', MethodPredicate)
533 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
527 534
528 535 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 536 serializer=json.dumps, indent=4))
@@ -538,5 +545,4 b' def includeme(config):'
538 545 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 546 # register some exception handling view
540 547 config.add_view(exception_view, context=JSONRPCBaseError)
541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 548 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -86,7 +86,9 b' class TestApi(object):'
86 86 def test_api_non_existing_method_have_similar(self, request):
87 87 id_, params = build_data(self.apikey, 'comment', args='xx')
88 88 response = api_call(self.app, params)
89 expected = 'No such method: comment. Similar methods: changeset_comment, comment_pull_request, get_pull_request_comments, comment_commit'
89 expected = 'No such method: comment. ' \
90 'Similar methods: changeset_comment, comment_pull_request, ' \
91 'get_pull_request_comments, comment_commit, get_repo_comments'
90 92 assert_error(id_, expected, given=response.body)
91 93
92 94 def test_api_disabled_user(self, request):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -38,7 +38,7 b' class TestGetMethod(object):'
38 38 response = api_call(self.app, params)
39 39
40 40 expected = ['changeset_comment', 'comment_pull_request',
41 'get_pull_request_comments', 'comment_commit']
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
42 42 assert_ok(id_, expected, given=response.body)
43 43
44 44 def test_get_methods_on_single_match(self):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -65,6 +65,7 b' class TestGetPullRequest(object):'
65 65 'title': pull_request.title,
66 66 'description': pull_request.description,
67 67 'status': pull_request.status,
68 'state': pull_request.pull_request_state,
68 69 'created_on': pull_request.created_on,
69 70 'updated_on': pull_request.updated_on,
70 71 'commit_ids': pull_request.revisions,
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -59,6 +59,7 b' class TestGetPullRequestComments(object)'
59 59 'status_lbl': 'Under Review'},
60 60 'comment_text': 'Auto status change to |new_status|\n\n.. |new_status| replace:: *"Under Review"*',
61 61 'comment_type': 'note',
62 'comment_resolved_by': None,
62 63 'pull_request_version': None}
63 64 ]
64 65 assert_ok(id_, expected, response.body)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -29,11 +29,10 b' from rhodecode.api.tests.utils import ('
29 29
30 30 @pytest.mark.usefixtures("testuser_api", "app")
31 31 class TestMergePullRequest(object):
32
32 33 @pytest.mark.backends("git", "hg")
33 34 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
34 35 pull_request = pr_util.create_pull_request(mergeable=True)
35 author = pull_request.user_id
36 repo = pull_request.target_repo.repo_id
37 36 pull_request_id = pull_request.pull_request_id
38 37 pull_request_repo = pull_request.target_repo.repo_name
39 38
@@ -46,8 +45,7 b' class TestMergePullRequest(object):'
46 45
47 46 # The above api call detaches the pull request DB object from the
48 47 # session because of an unconditional transaction rollback in our
49 # middleware. Therefore we need to add it back here if we want to use
50 # it.
48 # middleware. Therefore we need to add it back here if we want to use it.
51 49 Session().add(pull_request)
52 50
53 51 expected = 'merge not possible for following reasons: ' \
@@ -55,6 +53,29 b' class TestMergePullRequest(object):'
55 53 assert_error(id_, expected, given=response.body)
56 54
57 55 @pytest.mark.backends("git", "hg")
56 def test_api_merge_pull_request_merge_failed_disallowed_state(
57 self, pr_util, no_notifications):
58 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
59 pull_request_id = pull_request.pull_request_id
60 pull_request_repo = pull_request.target_repo.repo_name
61
62 pr = PullRequest.get(pull_request_id)
63 pr.pull_request_state = pull_request.STATE_UPDATING
64 Session().add(pr)
65 Session().commit()
66
67 id_, params = build_data(
68 self.apikey, 'merge_pull_request',
69 repoid=pull_request_repo,
70 pullrequestid=pull_request_id)
71
72 response = api_call(self.app, params)
73 expected = 'Operation forbidden because pull request is in state {}, '\
74 'only state {} is allowed.'.format(PullRequest.STATE_UPDATING,
75 PullRequest.STATE_CREATED)
76 assert_error(id_, expected, given=response.body)
77
78 @pytest.mark.backends("git", "hg")
58 79 def test_api_merge_pull_request(self, pr_util, no_notifications):
59 80 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
60 81 author = pull_request.user_id
@@ -88,6 +109,7 b' class TestMergePullRequest(object):'
88 109 expected = {
89 110 'executed': True,
90 111 'failure_reason': 0,
112 'merge_status_message': 'This pull request can be automatically merged.',
91 113 'possible': True,
92 114 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
93 115 'merge_ref': pull_request.shadow_merge_ref._asdict()
@@ -112,6 +134,107 b' class TestMergePullRequest(object):'
112 134 assert_error(id_, expected, given=response.body)
113 135
114 136 @pytest.mark.backends("git", "hg")
137 def test_api_merge_pull_request_as_another_user_no_perms_to_merge(
138 self, pr_util, no_notifications, user_util):
139 merge_user = user_util.create_user()
140 merge_user_id = merge_user.user_id
141 merge_user_username = merge_user.username
142
143 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
144
145 pull_request_id = pull_request.pull_request_id
146 pull_request_repo = pull_request.target_repo.repo_name
147
148 id_, params = build_data(
149 self.apikey, 'comment_pull_request',
150 repoid=pull_request_repo,
151 pullrequestid=pull_request_id,
152 status='approved')
153
154 response = api_call(self.app, params)
155 expected = {
156 'comment_id': response.json.get('result', {}).get('comment_id'),
157 'pull_request_id': pull_request_id,
158 'status': {'given': 'approved', 'was_changed': True}
159 }
160 assert_ok(id_, expected, given=response.body)
161 id_, params = build_data(
162 self.apikey, 'merge_pull_request',
163 repoid=pull_request_repo,
164 pullrequestid=pull_request_id,
165 userid=merge_user_id
166 )
167
168 response = api_call(self.app, params)
169 expected = 'merge not possible for following reasons: User `{}` ' \
170 'not allowed to perform merge.'.format(merge_user_username)
171 assert_error(id_, expected, response.body)
172
173 @pytest.mark.backends("git", "hg")
174 def test_api_merge_pull_request_as_another_user(self, pr_util, no_notifications, user_util):
175 merge_user = user_util.create_user()
176 merge_user_id = merge_user.user_id
177 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
178 user_util.grant_user_permission_to_repo(
179 pull_request.target_repo, merge_user, 'repository.write')
180 author = pull_request.user_id
181 repo = pull_request.target_repo.repo_id
182 pull_request_id = pull_request.pull_request_id
183 pull_request_repo = pull_request.target_repo.repo_name
184
185 id_, params = build_data(
186 self.apikey, 'comment_pull_request',
187 repoid=pull_request_repo,
188 pullrequestid=pull_request_id,
189 status='approved')
190
191 response = api_call(self.app, params)
192 expected = {
193 'comment_id': response.json.get('result', {}).get('comment_id'),
194 'pull_request_id': pull_request_id,
195 'status': {'given': 'approved', 'was_changed': True}
196 }
197 assert_ok(id_, expected, given=response.body)
198
199 id_, params = build_data(
200 self.apikey, 'merge_pull_request',
201 repoid=pull_request_repo,
202 pullrequestid=pull_request_id,
203 userid=merge_user_id
204 )
205
206 response = api_call(self.app, params)
207
208 pull_request = PullRequest.get(pull_request_id)
209
210 expected = {
211 'executed': True,
212 'failure_reason': 0,
213 'merge_status_message': 'This pull request can be automatically merged.',
214 'possible': True,
215 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
216 'merge_ref': pull_request.shadow_merge_ref._asdict()
217 }
218
219 assert_ok(id_, expected, response.body)
220
221 journal = UserLog.query() \
222 .filter(UserLog.user_id == merge_user_id) \
223 .filter(UserLog.repository_id == repo) \
224 .order_by('user_log_id') \
225 .all()
226 assert journal[-2].action == 'repo.pull_request.merge'
227 assert journal[-1].action == 'repo.pull_request.close'
228
229 id_, params = build_data(
230 self.apikey, 'merge_pull_request',
231 repoid=pull_request_repo, pullrequestid=pull_request_id, userid=merge_user_id)
232 response = api_call(self.app, params)
233
234 expected = 'merge not possible for following reasons: This pull request is closed.'
235 assert_error(id_, expected, given=response.body)
236
237 @pytest.mark.backends("git", "hg")
115 238 def test_api_merge_pull_request_repo_error(self, pr_util):
116 239 pull_request = pr_util.create_pull_request()
117 240 id_, params = build_data(
@@ -123,8 +246,7 b' class TestMergePullRequest(object):'
123 246 assert_error(id_, expected, given=response.body)
124 247
125 248 @pytest.mark.backends("git", "hg")
126 def test_api_merge_pull_request_non_admin_with_userid_error(self,
127 pr_util):
249 def test_api_merge_pull_request_non_admin_with_userid_error(self, pr_util):
128 250 pull_request = pr_util.create_pull_request(mergeable=True)
129 251 id_, params = build_data(
130 252 self.apikey_regular, 'merge_pull_request',
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -28,6 +28,19 b' from rhodecode.lib.ext_json import json'
28 28 API_URL = '/_admin/api'
29 29
30 30
31 def assert_call_ok(id_, given):
32 expected = jsonify({
33 'id': id_,
34 'error': None,
35 'result': None
36 })
37 given = json.loads(given)
38
39 assert expected['id'] == given['id']
40 assert expected['error'] == given['error']
41 return given['result']
42
43
31 44 def assert_ok(id_, expected, given):
32 45 expected = jsonify({
33 46 'id': id_,
@@ -55,8 +68,6 b' def jsonify(obj):'
55 68 def build_data(apikey, method, **kw):
56 69 """
57 70 Builds API data with given random ID
58
59 :param random_id:
60 71 """
61 72 random_id = random.randrange(1, 9999)
62 73 return random_id, json.dumps({
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -30,7 +30,7 b' from rhodecode.lib.auth import ('
30 30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
31 31 from rhodecode.lib.utils import safe_unicode
32 32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 from rhodecode.controllers.utils import get_commit_from_ref_name
33 from rhodecode.lib.view_utils import get_commit_from_ref_name
34 34 from rhodecode.lib.utils2 import str2bool
35 35
36 36 log = logging.getLogger(__name__)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2015-2018 RhodeCode GmbH
3 # Copyright (C) 2015-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -26,13 +26,13 b' from rhodecode.api import jsonrpc_method'
26 26 from rhodecode.api.utils import (
27 27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error)
29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
30 30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 31 from rhodecode.lib.base import vcs_operation_context
32 32 from rhodecode.lib.utils2 import str2bool
33 33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 37 from rhodecode.model.settings import SettingsModel
38 38 from rhodecode.model.validation_schema import Invalid
@@ -128,16 +128,21 b' def get_pull_request(request, apiuser, p'
128 128 else:
129 129 repo = pull_request.target_repo
130 130
131 if not PullRequestModel().check_user_read(
132 pull_request, apiuser, api=True):
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
133 132 raise JSONRPCError('repository `%s` or pull request `%s` '
134 133 'does not exist' % (repoid, pullrequestid))
135 data = pull_request.get_api_data()
134
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
136 # otherwise we can lock the repo on calculation of merge state while update/merge
137 # is happening.
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
139 data = pull_request.get_api_data(with_merge_state=merge_state)
136 140 return data
137 141
138 142
139 143 @jsonrpc_method()
140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
145 merge_state=Optional(True)):
141 146 """
142 147 Get all pull requests from the repository specified in `repoid`.
143 148
@@ -151,6 +156,9 b' def get_pull_requests(request, apiuser, '
151 156 * ``open``
152 157 * ``closed``
153 158 :type status: str
159 :param merge_state: Optional calculate merge state for each repository.
160 This could result in longer time to fetch the data
161 :type merge_state: bool
154 162
155 163 Example output:
156 164
@@ -228,8 +236,10 b' def get_pull_requests(request, apiuser, '
228 236 validate_repo_permissions(apiuser, repoid, repo, _perms)
229 237
230 238 status = Optional.extract(status)
231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
232 data = [pr.get_api_data() for pr in pull_requests]
239 merge_state = Optional.extract(merge_state, binary=True)
240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
241 order_by='id', order_dir='desc')
242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
233 243 return data
234 244
235 245
@@ -259,6 +269,7 b' def merge_pull_request('
259 269 "result": {
260 270 "executed": "<bool>",
261 271 "failure_reason": "<int>",
272 "merge_status_message": "<str>",
262 273 "merge_commit_id": "<merge_commit_id>",
263 274 "possible": "<bool>",
264 275 "merge_ref": {
@@ -274,17 +285,25 b' def merge_pull_request('
274 285 repo = get_repo_or_error(repoid)
275 286 else:
276 287 repo = pull_request.target_repo
277
288 auth_user = apiuser
278 289 if not isinstance(userid, Optional):
279 290 if (has_superadmin_permission(apiuser) or
280 291 HasRepoPermissionAnyApi('repository.admin')(
281 292 user=apiuser, repo_name=repo.repo_name)):
282 293 apiuser = get_user_or_error(userid)
294 auth_user = apiuser.AuthUser()
283 295 else:
284 296 raise JSONRPCError('userid is not the same as your user')
285 297
286 check = MergeCheck.validate(
287 pull_request, auth_user=apiuser, translator=request.translate)
298 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
299 raise JSONRPCError(
300 'Operation forbidden because pull request is in state {}, '
301 'only state {} is allowed.'.format(
302 pull_request.pull_request_state, PullRequest.STATE_CREATED))
303
304 with pull_request.set_state(PullRequest.STATE_UPDATING):
305 check = MergeCheck.validate(pull_request, auth_user=auth_user,
306 translator=request.translate)
288 307 merge_possible = not check.failed
289 308
290 309 if not merge_possible:
@@ -300,20 +319,20 b' def merge_pull_request('
300 319 target_repo = pull_request.target_repo
301 320 extras = vcs_operation_context(
302 321 request.environ, repo_name=target_repo.repo_name,
303 username=apiuser.username, action='push',
322 username=auth_user.username, action='push',
304 323 scm=target_repo.repo_type)
324 with pull_request.set_state(PullRequest.STATE_UPDATING):
305 325 merge_response = PullRequestModel().merge_repo(
306 326 pull_request, apiuser, extras=extras)
307 327 if merge_response.executed:
308 PullRequestModel().close_pull_request(
309 pull_request.pull_request_id, apiuser)
328 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
310 329
311 330 Session().commit()
312 331
313 332 # In previous versions the merge response directly contained the merge
314 333 # commit id. It is now contained in the merge reference object. To be
315 334 # backwards compatible we have to extract it again.
316 merge_response = merge_response._asdict()
335 merge_response = merge_response.asdict()
317 336 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
318 337
319 338 return merge_response
@@ -474,14 +493,21 b' def comment_pull_request('
474 493 else:
475 494 repo = pull_request.target_repo
476 495
496 auth_user = apiuser
477 497 if not isinstance(userid, Optional):
478 498 if (has_superadmin_permission(apiuser) or
479 499 HasRepoPermissionAnyApi('repository.admin')(
480 500 user=apiuser, repo_name=repo.repo_name)):
481 501 apiuser = get_user_or_error(userid)
502 auth_user = apiuser.AuthUser()
482 503 else:
483 504 raise JSONRPCError('userid is not the same as your user')
484 505
506 if pull_request.is_closed():
507 raise JSONRPCError(
508 'pull request `%s` comment failed, pull request is closed' % (
509 pullrequestid,))
510
485 511 if not PullRequestModel().check_user_read(
486 512 pull_request, apiuser, api=True):
487 513 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
@@ -549,7 +575,7 b' def comment_pull_request('
549 575 renderer=renderer,
550 576 comment_type=comment_type,
551 577 resolves_comment_id=resolves_comment_id,
552 auth_user=apiuser
578 auth_user=auth_user
553 579 )
554 580
555 581 if allowed_to_change_status and status:
@@ -589,8 +615,8 b' def comment_pull_request('
589 615 @jsonrpc_method()
590 616 def create_pull_request(
591 617 request, apiuser, source_repo, target_repo, source_ref, target_ref,
592 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
593 reviewers=Optional(None)):
618 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
619 description_renderer=Optional(''), reviewers=Optional(None)):
594 620 """
595 621 Creates a new pull request.
596 622
@@ -611,6 +637,8 b' def create_pull_request('
611 637 :type source_ref: str
612 638 :param target_ref: Set the target ref name.
613 639 :type target_ref: str
640 :param owner: user_id or username
641 :type owner: Optional(str)
614 642 :param title: Optionally Set the pull request title, it's generated otherwise
615 643 :type title: str
616 644 :param description: Set the pull request description.
@@ -634,6 +662,8 b' def create_pull_request('
634 662 _perms = ('repository.admin', 'repository.write', 'repository.read',)
635 663 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
636 664
665 owner = validate_set_owner_permissions(apiuser, owner)
666
637 667 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
638 668 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
639 669
@@ -679,7 +709,7 b' def create_pull_request('
679 709
680 710 # recalculate reviewers logic, to make sure we can validate this
681 711 reviewer_rules = get_default_reviewers_data(
682 apiuser.get_instance(), source_db_repo,
712 owner, source_db_repo,
683 713 source_commit, target_db_repo, target_commit)
684 714
685 715 # now MERGE our given with the calculated
@@ -706,7 +736,7 b' def create_pull_request('
706 736 description_renderer = Optional.extract(description_renderer) or default_system_renderer
707 737
708 738 pull_request = PullRequestModel().create(
709 created_by=apiuser.user_id,
739 created_by=owner.user_id,
710 740 source_repo=source_repo,
711 741 source_ref=full_source_ref,
712 742 target_repo=target_repo,
@@ -844,9 +874,16 b' def update_pull_request('
844 874
845 875 commit_changes = {"added": [], "common": [], "removed": []}
846 876 if str2bool(Optional.extract(update_commits)):
877
878 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
879 raise JSONRPCError(
880 'Operation forbidden because pull request is in state {}, '
881 'only state {} is allowed.'.format(
882 pull_request.pull_request_state, PullRequest.STATE_CREATED))
883
884 with pull_request.set_state(PullRequest.STATE_UPDATING):
847 885 if PullRequestModel().has_valid_update_type(pull_request):
848 update_response = PullRequestModel().update_commits(
849 pull_request)
886 update_response = PullRequestModel().update_commits(pull_request)
850 887 commit_changes = update_response.changes or commit_changes
851 888 Session().commit()
852 889
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -29,13 +29,15 b' from rhodecode.api.utils import ('
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger, rc_cache
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 35 from rhodecode.lib.celerylib.utils import get_task_id
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 from rhodecode.lib.vcs import RepositoryError
40 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
39 41 from rhodecode.model.changeset_status import ChangesetStatusModel
40 42 from rhodecode.model.comment import CommentsModel
41 43 from rhodecode.model.db import (
@@ -380,7 +382,7 b' def get_repo_changesets(request, apiuser'
380 382
381 383 try:
382 384 commits = vcs_repo.get_commits(
383 start_id=start_rev, pre_load=pre_load)
385 start_id=start_rev, pre_load=pre_load, translate_tags=False)
384 386 except TypeError as e:
385 387 raise JSONRPCError(safe_str(e))
386 388 except Exception:
@@ -428,8 +430,8 b' def get_repo_nodes(request, apiuser, rep'
428 430 ``all`` (default), ``files`` and ``dirs``.
429 431 :type ret_type: Optional(str)
430 432 :param details: Returns extended information about nodes, such as
431 md5, binary, and or content. The valid options are ``basic`` and
432 ``full``.
433 md5, binary, and or content.
434 The valid options are ``basic`` and ``full``.
433 435 :type details: Optional(str)
434 436 :param max_file_bytes: Only return file content under this file size bytes
435 437 :type details: Optional(int)
@@ -441,10 +443,15 b' def get_repo_nodes(request, apiuser, rep'
441 443 id : <id_given_in_input>
442 444 result: [
443 445 {
444 "name" : "<name>"
445 "type" : "<type>",
446 "binary": "<true|false>" (only in extended mode)
447 "md5" : "<md5 of file content>" (only in extended mode)
446 "binary": false,
447 "content": "File line\nLine2\n",
448 "extension": "md",
449 "lines": 2,
450 "md5": "059fa5d29b19c0657e384749480f6422",
451 "mimetype": "text/x-minidsrc",
452 "name": "file.md",
453 "size": 580,
454 "type": "file"
448 455 },
449 456 ...
450 457 ]
@@ -453,16 +460,14 b' def get_repo_nodes(request, apiuser, rep'
453 460
454 461 repo = get_repo_or_error(repoid)
455 462 if not has_superadmin_permission(apiuser):
456 _perms = (
457 'repository.admin', 'repository.write', 'repository.read',)
463 _perms = ('repository.admin', 'repository.write', 'repository.read',)
458 464 validate_repo_permissions(apiuser, repoid, repo, _perms)
459 465
460 466 ret_type = Optional.extract(ret_type)
461 467 details = Optional.extract(details)
462 468 _extended_types = ['basic', 'full']
463 469 if details not in _extended_types:
464 raise JSONRPCError(
465 'ret_type must be one of %s' % (','.join(_extended_types)))
470 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
466 471 extended_info = False
467 472 content = False
468 473 if details == 'basic':
@@ -499,6 +504,149 b' def get_repo_nodes(request, apiuser, rep'
499 504
500 505
501 506 @jsonrpc_method()
507 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
508 max_file_bytes=Optional(None), details=Optional('basic'),
509 cache=Optional(True)):
510 """
511 Returns a single file from repository at given revision.
512
513 This command can only be run using an |authtoken| with admin rights,
514 or users with at least read rights to |repos|.
515
516 :param apiuser: This is filled automatically from the |authtoken|.
517 :type apiuser: AuthUser
518 :param repoid: The repository name or repository ID.
519 :type repoid: str or int
520 :param commit_id: The revision for which listing should be done.
521 :type commit_id: str
522 :param file_path: The path from which to start displaying.
523 :type file_path: str
524 :param details: Returns different set of information about nodes.
525 The valid options are ``minimal`` ``basic`` and ``full``.
526 :type details: Optional(str)
527 :param max_file_bytes: Only return file content under this file size bytes
528 :type max_file_bytes: Optional(int)
529 :param cache: Use internal caches for fetching files. If disabled fetching
530 files is slower but more memory efficient
531 :type cache: Optional(bool)
532 Example output:
533
534 .. code-block:: bash
535
536 id : <id_given_in_input>
537 result: {
538 "binary": false,
539 "extension": "py",
540 "lines": 35,
541 "content": "....",
542 "md5": "76318336366b0f17ee249e11b0c99c41",
543 "mimetype": "text/x-python",
544 "name": "python.py",
545 "size": 817,
546 "type": "file",
547 }
548 error: null
549 """
550
551 repo = get_repo_or_error(repoid)
552 if not has_superadmin_permission(apiuser):
553 _perms = ('repository.admin', 'repository.write', 'repository.read',)
554 validate_repo_permissions(apiuser, repoid, repo, _perms)
555
556 cache = Optional.extract(cache, binary=True)
557 details = Optional.extract(details)
558 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
559 if details not in _extended_types:
560 raise JSONRPCError(
561 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
562 extended_info = False
563 content = False
564
565 if details == 'minimal':
566 extended_info = False
567
568 elif details == 'basic':
569 extended_info = True
570
571 elif details == 'full':
572 extended_info = content = True
573
574 try:
575 # check if repo is not empty by any chance, skip quicker if it is.
576 _scm = repo.scm_instance()
577 if _scm.is_empty():
578 return None
579
580 node = ScmModel().get_node(
581 repo, commit_id, file_path, extended_info=extended_info,
582 content=content, max_file_bytes=max_file_bytes, cache=cache)
583 except NodeDoesNotExistError:
584 raise JSONRPCError('There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
585 repo.repo_name, file_path, commit_id))
586 except Exception:
587 log.exception("Exception occurred while trying to get repo %s file",
588 repo.repo_name)
589 raise JSONRPCError('failed to get repo: `{}` file at path {}'.format(
590 repo.repo_name, file_path))
591
592 return node
593
594
595 @jsonrpc_method()
596 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
597 """
598 Returns a list of tree nodes for path at given revision. This api is built
599 strictly for usage in full text search building, and shouldn't be consumed
600
601 This command can only be run using an |authtoken| with admin rights,
602 or users with at least read rights to |repos|.
603
604 """
605
606 repo = get_repo_or_error(repoid)
607 if not has_superadmin_permission(apiuser):
608 _perms = ('repository.admin', 'repository.write', 'repository.read',)
609 validate_repo_permissions(apiuser, repoid, repo, _perms)
610
611 repo_id = repo.repo_id
612 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
613 cache_on = cache_seconds > 0
614
615 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
616 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
617
618 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
619 condition=cache_on)
620 def compute_fts_tree(repo_id, commit_id, root_path, cache_ver):
621 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
622
623 try:
624 # check if repo is not empty by any chance, skip quicker if it is.
625 _scm = repo.scm_instance()
626 if _scm.is_empty():
627 return []
628 except RepositoryError:
629 log.exception("Exception occurred while trying to get repo nodes")
630 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
631
632 try:
633 # we need to resolve commit_id to a FULL sha for cache to work correctly.
634 # sending 'master' is a pointer that needs to be translated to current commit.
635 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
636 log.debug(
637 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
638 'with caching: %s[TTL: %ss]' % (
639 repo_id, commit_id, cache_on, cache_seconds or 0))
640
641 tree_files = compute_fts_tree(repo_id, commit_id, root_path, 'v1')
642 return tree_files
643
644 except Exception:
645 log.exception("Exception occurred while trying to get repo nodes")
646 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
647
648
649 @jsonrpc_method()
502 650 def get_repo_refs(request, apiuser, repoid):
503 651 """
504 652 Returns a dictionary of current references. It returns
@@ -1506,6 +1654,73 b' def comment_commit('
1506 1654
1507 1655
1508 1656 @jsonrpc_method()
1657 def get_repo_comments(request, apiuser, repoid,
1658 commit_id=Optional(None), comment_type=Optional(None),
1659 userid=Optional(None)):
1660 """
1661 Get all comments for a repository
1662
1663 :param apiuser: This is filled automatically from the |authtoken|.
1664 :type apiuser: AuthUser
1665 :param repoid: Set the repository name or repository ID.
1666 :type repoid: str or int
1667 :param commit_id: Optionally filter the comments by the commit_id
1668 :type commit_id: Optional(str), default: None
1669 :param comment_type: Optionally filter the comments by the comment_type
1670 one of: 'note', 'todo'
1671 :type comment_type: Optional(str), default: None
1672 :param userid: Optionally filter the comments by the author of comment
1673 :type userid: Optional(str or int), Default: None
1674
1675 Example error output:
1676
1677 .. code-block:: bash
1678
1679 {
1680 "id" : <id_given_in_input>,
1681 "result" : [
1682 {
1683 "comment_author": <USER_DETAILS>,
1684 "comment_created_on": "2017-02-01T14:38:16.309",
1685 "comment_f_path": "file.txt",
1686 "comment_id": 282,
1687 "comment_lineno": "n1",
1688 "comment_resolved_by": null,
1689 "comment_status": [],
1690 "comment_text": "This file needs a header",
1691 "comment_type": "todo"
1692 }
1693 ],
1694 "error" : null
1695 }
1696
1697 """
1698 repo = get_repo_or_error(repoid)
1699 if not has_superadmin_permission(apiuser):
1700 _perms = ('repository.read', 'repository.write', 'repository.admin')
1701 validate_repo_permissions(apiuser, repoid, repo, _perms)
1702
1703 commit_id = Optional.extract(commit_id)
1704
1705 userid = Optional.extract(userid)
1706 if userid:
1707 user = get_user_or_error(userid)
1708 else:
1709 user = None
1710
1711 comment_type = Optional.extract(comment_type)
1712 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1713 raise JSONRPCError(
1714 'comment_type must be one of `{}` got {}'.format(
1715 ChangesetComment.COMMENT_TYPES, comment_type)
1716 )
1717
1718 comments = CommentsModel().get_repository_comments(
1719 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1720 return comments
1721
1722
1723 @jsonrpc_method()
1509 1724 def grant_user_permission(request, apiuser, repoid, userid, perm):
1510 1725 """
1511 1726 Grant permissions for the specified user on the given repository,
@@ -1543,9 +1758,18 b' def grant_user_permission(request, apius'
1543 1758 _perms = ('repository.admin',)
1544 1759 validate_repo_permissions(apiuser, repoid, repo, _perms)
1545 1760
1761 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1546 1762 try:
1763 changes = RepoModel().update_permissions(
1764 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1547 1765
1548 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1766 action_data = {
1767 'added': changes['added'],
1768 'updated': changes['updated'],
1769 'deleted': changes['deleted'],
1770 }
1771 audit_logger.store_api(
1772 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1549 1773
1550 1774 Session().commit()
1551 1775 return {
@@ -1555,8 +1779,7 b' def grant_user_permission(request, apius'
1555 1779 'success': True
1556 1780 }
1557 1781 except Exception:
1558 log.exception(
1559 "Exception occurred while trying edit permissions for repo")
1782 log.exception("Exception occurred while trying edit permissions for repo")
1560 1783 raise JSONRPCError(
1561 1784 'failed to edit permission for user: `%s` in repo: `%s`' % (
1562 1785 userid, repoid
@@ -1597,8 +1820,19 b' def revoke_user_permission(request, apiu'
1597 1820 _perms = ('repository.admin',)
1598 1821 validate_repo_permissions(apiuser, repoid, repo, _perms)
1599 1822
1823 perm_deletions = [[user.user_id, None, "user"]]
1600 1824 try:
1601 RepoModel().revoke_user_permission(repo=repo, user=user)
1825 changes = RepoModel().update_permissions(
1826 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1827
1828 action_data = {
1829 'added': changes['added'],
1830 'updated': changes['updated'],
1831 'deleted': changes['deleted'],
1832 }
1833 audit_logger.store_api(
1834 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1835
1602 1836 Session().commit()
1603 1837 return {
1604 1838 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
@@ -1607,8 +1841,7 b' def revoke_user_permission(request, apiu'
1607 1841 'success': True
1608 1842 }
1609 1843 except Exception:
1610 log.exception(
1611 "Exception occurred while trying revoke permissions to repo")
1844 log.exception("Exception occurred while trying revoke permissions to repo")
1612 1845 raise JSONRPCError(
1613 1846 'failed to edit permission for user: `%s` in repo: `%s`' % (
1614 1847 userid, repoid
@@ -1674,9 +1907,17 b' def grant_user_group_permission(request,'
1674 1907 raise JSONRPCError(
1675 1908 'user group `%s` does not exist' % (usergroupid,))
1676 1909
1910 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1677 1911 try:
1678 RepoModel().grant_user_group_permission(
1679 repo=repo, group_name=user_group, perm=perm)
1912 changes = RepoModel().update_permissions(
1913 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1914 action_data = {
1915 'added': changes['added'],
1916 'updated': changes['updated'],
1917 'deleted': changes['deleted'],
1918 }
1919 audit_logger.store_api(
1920 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1680 1921
1681 1922 Session().commit()
1682 1923 return {
@@ -1739,9 +1980,17 b' def revoke_user_group_permission(request'
1739 1980 raise JSONRPCError(
1740 1981 'user group `%s` does not exist' % (usergroupid,))
1741 1982
1983 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
1742 1984 try:
1743 RepoModel().revoke_user_group_permission(
1744 repo=repo, group_name=user_group)
1985 changes = RepoModel().update_permissions(
1986 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
1987 action_data = {
1988 'added': changes['added'],
1989 'updated': changes['updated'],
1990 'deleted': changes['deleted'],
1991 }
1992 audit_logger.store_api(
1993 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1745 1994
1746 1995 Session().commit()
1747 1996 return {
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -453,10 +453,19 b' def grant_user_permission_to_repo_group('
453 453
454 454 perm_additions = [[user.user_id, perm.permission_name, "user"]]
455 455 try:
456 RepoGroupModel().update_permissions(repo_group=repo_group,
457 perm_additions=perm_additions,
458 recursive=apply_to_children,
459 cur_user=apiuser)
456 changes = RepoGroupModel().update_permissions(
457 repo_group=repo_group, perm_additions=perm_additions,
458 recursive=apply_to_children, cur_user=apiuser)
459
460 action_data = {
461 'added': changes['added'],
462 'updated': changes['updated'],
463 'deleted': changes['deleted'],
464 }
465 audit_logger.store_api(
466 'repo_group.edit.permissions', action_data=action_data,
467 user=apiuser)
468
460 469 Session().commit()
461 470 return {
462 471 'msg': 'Granted perm: `%s` (recursive:%s) for user: '
@@ -527,10 +536,19 b' def revoke_user_permission_from_repo_gro'
527 536
528 537 perm_deletions = [[user.user_id, None, "user"]]
529 538 try:
530 RepoGroupModel().update_permissions(repo_group=repo_group,
531 perm_deletions=perm_deletions,
532 recursive=apply_to_children,
533 cur_user=apiuser)
539 changes = RepoGroupModel().update_permissions(
540 repo_group=repo_group, perm_deletions=perm_deletions,
541 recursive=apply_to_children, cur_user=apiuser)
542
543 action_data = {
544 'added': changes['added'],
545 'updated': changes['updated'],
546 'deleted': changes['deleted'],
547 }
548 audit_logger.store_api(
549 'repo_group.edit.permissions', action_data=action_data,
550 user=apiuser)
551
534 552 Session().commit()
535 553 return {
536 554 'msg': 'Revoked perm (recursive:%s) for user: '
@@ -611,10 +629,19 b' def grant_user_group_permission_to_repo_'
611 629
612 630 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
613 631 try:
614 RepoGroupModel().update_permissions(repo_group=repo_group,
615 perm_additions=perm_additions,
616 recursive=apply_to_children,
617 cur_user=apiuser)
632 changes = RepoGroupModel().update_permissions(
633 repo_group=repo_group, perm_additions=perm_additions,
634 recursive=apply_to_children, cur_user=apiuser)
635
636 action_data = {
637 'added': changes['added'],
638 'updated': changes['updated'],
639 'deleted': changes['deleted'],
640 }
641 audit_logger.store_api(
642 'repo_group.edit.permissions', action_data=action_data,
643 user=apiuser)
644
618 645 Session().commit()
619 646 return {
620 647 'msg': 'Granted perm: `%s` (recursive:%s) '
@@ -694,10 +721,19 b' def revoke_user_group_permission_from_re'
694 721
695 722 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
696 723 try:
697 RepoGroupModel().update_permissions(repo_group=repo_group,
698 perm_deletions=perm_deletions,
699 recursive=apply_to_children,
700 cur_user=apiuser)
724 changes = RepoGroupModel().update_permissions(
725 repo_group=repo_group, perm_deletions=perm_deletions,
726 recursive=apply_to_children, cur_user=apiuser)
727
728 action_data = {
729 'added': changes['added'],
730 'updated': changes['updated'],
731 'deleted': changes['deleted'],
732 }
733 audit_logger.store_api(
734 'repo_group.edit.permissions', action_data=action_data,
735 user=apiuser)
736
701 737 Session().commit()
702 738 return {
703 739 'msg': 'Revoked perm (recursive:%s) for user group: '
@@ -716,4 +752,3 b' def revoke_user_group_permission_from_re'
716 752 user_group.users_group_name, repo_group.name
717 753 )
718 754 )
719
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -21,6 +21,9 b''
21 21 import inspect
22 22 import logging
23 23 import itertools
24 import base64
25
26 from pyramid import compat
24 27
25 28 from rhodecode.api import (
26 29 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
@@ -30,10 +33,15 b' from rhodecode.api.utils import ('
30 33 from rhodecode.lib.utils import repo2db_mapper
31 34 from rhodecode.lib import system_info
32 35 from rhodecode.lib import user_sessions
36 from rhodecode.lib import exc_tracking
37 from rhodecode.lib.ext_json import json
33 38 from rhodecode.lib.utils2 import safe_int
34 39 from rhodecode.model.db import UserIpMap
35 40 from rhodecode.model.scm import ScmModel
36 41 from rhodecode.model.settings import VcsSettingsModel
42 from rhodecode.apps.file_store import utils
43 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
44 FileOverSizeException
37 45
38 46 log = logging.getLogger(__name__)
39 47
@@ -293,7 +301,7 b' def get_method(request, apiuser, pattern'
293 301 :param apiuser: This is filled automatically from the |authtoken|.
294 302 :type apiuser: AuthUser
295 303 :param pattern: pattern to match method names against
296 :type older_then: Optional("*")
304 :type pattern: Optional("*")
297 305
298 306 Example output:
299 307
@@ -349,3 +357,63 b' def get_method(request, apiuser, pattern'
349 357 args_desc.append(func_kwargs)
350 358
351 359 return matches.keys() + args_desc
360
361
362 @jsonrpc_method()
363 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
364 """
365 Stores sent exception inside the built-in exception tracker in |RCE| server.
366
367 This command can only be run using an |authtoken| with admin rights to
368 the specified repository.
369
370 This command takes the following options:
371
372 :param apiuser: This is filled automatically from the |authtoken|.
373 :type apiuser: AuthUser
374
375 :param exc_data_json: JSON data with exception e.g
376 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
377 :type exc_data_json: JSON data
378
379 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
380 :type prefix: Optional("rhodecode")
381
382 Example output:
383
384 .. code-block:: bash
385
386 id : <id_given_in_input>
387 "result": {
388 "exc_id": 139718459226384,
389 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
390 }
391 error : null
392 """
393 if not has_superadmin_permission(apiuser):
394 raise JSONRPCForbidden()
395
396 prefix = Optional.extract(prefix)
397 exc_id = exc_tracking.generate_id()
398
399 try:
400 exc_data = json.loads(exc_data_json)
401 except Exception:
402 log.error('Failed to parse JSON: %r', exc_data_json)
403 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
404 'Please make sure it contains a valid JSON.')
405
406 try:
407 exc_traceback = exc_data['exc_traceback']
408 exc_type_name = exc_data['exc_type_name']
409 except KeyError as err:
410 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
411 'in exc_data_json field. Missing: {}'.format(err))
412
413 exc_tracking._store_exception(
414 exc_id=exc_id, exc_traceback=exc_traceback,
415 exc_type_name=exc_type_name, prefix=prefix)
416
417 exc_url = request.route_url(
418 'admin_settings_exception_tracker_show', exception_id=exc_id)
419 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -19,6 +19,7 b''
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 from pyramid import compat
22 23
23 24 from rhodecode.api import (
24 25 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
@@ -241,7 +242,7 b' def create_user(request, apiuser, userna'
241 242 # generate temporary password if user is external
242 243 password = PasswordGenerator().gen_password(length=16)
243 244 create_repo_group = Optional.extract(create_personal_repo_group)
244 if isinstance(create_repo_group, basestring):
245 if isinstance(create_repo_group, compat.string_types):
245 246 create_repo_group = str2bool(create_repo_group)
246 247
247 248 username = Optional.extract(username)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -22,11 +22,12 b' import time'
22 22 import logging
23 23 import operator
24 24
25 from pyramid import compat
25 26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26 27
27 28 from rhodecode.lib import helpers as h, diffs
28 29 from rhodecode.lib.utils2 import (
29 StrictAttributeDict, safe_int, datetime_to_time, safe_unicode)
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
30 31 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
31 32 from rhodecode.model import repo
32 33 from rhodecode.model import repo_group
@@ -249,6 +250,12 b' class RepoAppView(BaseAppView):'
249 250 else: # redirect if we don't show missing requirements
250 251 raise HTTPFound(h.route_path('home'))
251 252
253 c.has_origin_repo_read_perm = False
254 if self.db_repo.fork:
255 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
256 'repository.write', 'repository.read', 'repository.admin')(
257 self.db_repo.fork.repo_name, 'summary fork link')
258
252 259 return c
253 260
254 261 def _get_f_path_unchecked(self, matchdict, default=None):
@@ -271,6 +278,13 b' class RepoAppView(BaseAppView):'
271 278 settings = settings_model.get_general_settings()
272 279 return settings.get(settings_key, default)
273 280
281 def get_recache_flag(self):
282 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
283 flag_val = self.request.GET.get(flag_name)
284 if str2bool(flag_val):
285 return True
286 return False
287
274 288
275 289 class PathFilter(object):
276 290
@@ -327,6 +341,13 b' class RepoGroupAppView(BaseAppView):'
327 341 self.db_repo_group = request.db_repo_group
328 342 self.db_repo_group_name = self.db_repo_group.group_name
329 343
344 def _get_local_tmpl_context(self, include_app_defaults=True):
345 _ = self.request.translate
346 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
347 include_app_defaults=include_app_defaults)
348 c.repo_group = self.db_repo_group
349 return c
350
330 351 def _revoke_perms_on_yourself(self, form_result):
331 352 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
332 353 form_result['perm_updates'])
@@ -389,7 +410,7 b' class DataGridAppView(object):'
389 410 return draw, start, length
390 411
391 412 def _get_order_col(self, order_by, model):
392 if isinstance(order_by, basestring):
413 if isinstance(order_by, compat.string_types):
393 414 try:
394 415 return operator.attrgetter(order_by)(model)
395 416 except AttributeError:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -280,8 +280,12 b' def admin_routes(config):'
280 280 pattern='/users/{user_id:\d+}/delete',
281 281 user_route=True)
282 282 config.add_route(
283 name='user_force_password_reset',
284 pattern='/users/{user_id:\d+}/password_reset',
283 name='user_enable_force_password_reset',
284 pattern='/users/{user_id:\d+}/password_reset_enable',
285 user_route=True)
286 config.add_route(
287 name='user_disable_force_password_reset',
288 pattern='/users/{user_id:\d+}/password_reset_disable',
285 289 user_route=True)
286 290 config.add_route(
287 291 name='user_create_personal_repo_group',
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -59,8 +59,6 b' def route_path(name, params=None, **kwar'
59 59 ADMIN_PREFIX + '/users/{user_id}/update',
60 60 'user_delete':
61 61 ADMIN_PREFIX + '/users/{user_id}/delete',
62 'user_force_password_reset':
63 ADMIN_PREFIX + '/users/{user_id}/password_reset',
64 62 'user_create_personal_repo_group':
65 63 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
66 64
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2018-2018 RhodeCode GmbH
3 # Copyright (C) 2018-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -119,8 +119,7 b' class AdminProcessManagementView(BaseApp'
119 119 result = []
120 120
121 121 def on_terminate(proc):
122 msg = "process `PID:{}` terminated with exit code {}".format(
123 proc.pid, proc.returncode or 0)
122 msg = "terminated"
124 123 result.append(msg)
125 124
126 125 procs = []
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -666,8 +666,8 b' class AdminSettingsView(BaseAppView):'
666 666 c = self.load_default_context()
667 667 c.active = 'search'
668 668
669 searcher = searcher_from_config(self.request.registry.settings)
670 c.statistics = searcher.statistics(self.request.translate)
669 c.searcher = searcher_from_config(self.request.registry.settings)
670 c.statistics = c.searcher.statistics(self.request.translate)
671 671
672 672 return self._get_template_context(c)
673 673
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -604,12 +604,9 b' class UsersView(UserAppView):'
604 604 @HasPermissionAllDecorator('hg.admin')
605 605 @CSRFRequired()
606 606 @view_config(
607 route_name='user_force_password_reset', request_method='POST',
607 route_name='user_enable_force_password_reset', request_method='POST',
608 608 renderer='rhodecode:templates/admin/users/user_edit.mako')
609 def user_force_password_reset(self):
610 """
611 toggle reset password flag for this user
612 """
609 def user_enable_force_password_reset(self):
613 610 _ = self.request.translate
614 611 c = self.load_default_context()
615 612
@@ -617,19 +614,41 b' class UsersView(UserAppView):'
617 614 c.user = self.db_user
618 615
619 616 try:
620 old_value = c.user.user_data.get('force_password_change')
621 c.user.update_userdata(force_password_change=not old_value)
617 c.user.update_userdata(force_password_change=True)
618
619 msg = _('Force password change enabled for user')
620 audit_logger.store_web('user.edit.password_reset.enabled',
621 user=c.rhodecode_user)
622
623 Session().commit()
624 h.flash(msg, category='success')
625 except Exception:
626 log.exception("Exception during password reset for user")
627 h.flash(_('An error occurred during password reset for user'),
628 category='error')
629
630 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
622 631
623 if old_value:
632 @LoginRequired()
633 @HasPermissionAllDecorator('hg.admin')
634 @CSRFRequired()
635 @view_config(
636 route_name='user_disable_force_password_reset', request_method='POST',
637 renderer='rhodecode:templates/admin/users/user_edit.mako')
638 def user_disable_force_password_reset(self):
639 _ = self.request.translate
640 c = self.load_default_context()
641
642 user_id = self.db_user_id
643 c.user = self.db_user
644
645 try:
646 c.user.update_userdata(force_password_change=False)
647
624 648 msg = _('Force password change disabled for user')
625 649 audit_logger.store_web(
626 650 'user.edit.password_reset.disabled',
627 651 user=c.rhodecode_user)
628 else:
629 msg = _('Force password change enabled for user')
630 audit_logger.store_web(
631 'user.edit.password_reset.enabled',
632 user=c.rhodecode_user)
633 652
634 653 Session().commit()
635 654 h.flash(msg, category='success')
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -56,6 +56,10 b' def includeme(config):'
56 56 pattern='/_repos')
57 57
58 58 config.add_route(
59 name='repo_group_list_data',
60 pattern='/_repo_groups')
61
62 config.add_route(
59 63 name='goto_switcher_data',
60 64 pattern='/_goto_data')
61 65
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -31,7 +31,14 b' def assert_and_get_main_filter_content(r'
31 31 assert data_item['url']
32 32
33 33 if data_item['type'] == 'search':
34 assert data_item['value_display'].startswith('Full text search for:')
34 display_val = data_item['value_display']
35 if data_item['id'] == -1:
36 assert 'File search for:' in display_val, display_val
37 elif data_item['id'] == -2:
38 assert 'Commit search for:' in display_val, display_val
39 else:
40 assert False, 'No Proper ID returned {}'.format(data_item['id'])
41
35 42 elif data_item['type'] == 'repo':
36 43 repos.append(data_item)
37 44 elif data_item['type'] == 'repo_group':
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -19,7 +19,7 b''
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 # -*- coding: utf-8 -*-
21 21
22 # Copyright (C) 2016-2018 RhodeCode GmbH
22 # Copyright (C) 2016-2019 RhodeCode GmbH
23 23 #
24 24 # This program is free software: you can redistribute it and/or modify
25 25 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -172,7 +172,9 b' class HomeView(BaseAppView):'
172 172 'id': obj.group_name,
173 173 'value': org_query,
174 174 'value_display': obj.group_name,
175 'text': obj.group_name,
175 176 'type': 'repo_group',
177 'repo_group_id': obj.group_id,
176 178 'url': h.route_path(
177 179 'repo_group_home', repo_group_name=obj.group_name)
178 180 }
@@ -246,9 +248,9 b' class HomeView(BaseAppView):'
246 248 }
247 249 for obj in acl_iter]
248 250
249 def _get_hash_commit_list(self, auth_user, query):
251 def _get_hash_commit_list(self, auth_user, searcher, query):
250 252 org_query = query
251 if not query or len(query) < 3:
253 if not query or len(query) < 3 or not searcher:
252 254 return []
253 255
254 256 commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query)
@@ -257,24 +259,34 b' class HomeView(BaseAppView):'
257 259 return []
258 260 commit_hash = commit_hashes[0]
259 261
260 searcher = searcher_from_config(self.request.registry.settings)
261 262 result = searcher.search(
262 'commit_id:%s*' % commit_hash, 'commit', auth_user,
263 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
263 264 raise_on_exc=False)
264 265
265 return [
266 {
266 commits = []
267 for entry in result['results']:
268 repo_data = {
269 'repository_id': entry.get('repository_id'),
270 'repository_type': entry.get('repo_type'),
271 'repository_name': entry.get('repository'),
272 }
273
274 commit_entry = {
267 275 'id': entry['commit_id'],
268 276 'value': org_query,
269 'value_display': 'repo `{}` commit: {}'.format(
277 'value_display': '`{}` commit: {}'.format(
270 278 entry['repository'], entry['commit_id']),
271 279 'type': 'commit',
272 280 'repo': entry['repository'],
281 'repo_data': repo_data,
282
273 283 'url': h.route_path(
274 284 'repo_commit',
275 285 repo_name=entry['repository'], commit_id=entry['commit_id'])
276 286 }
277 for entry in result['results']]
287
288 commits.append(commit_entry)
289 return commits
278 290
279 291 @LoginRequired()
280 292 @view_config(
@@ -305,6 +317,144 b' class HomeView(BaseAppView):'
305 317
306 318 @LoginRequired()
307 319 @view_config(
320 route_name='repo_group_list_data', request_method='GET',
321 renderer='json_ext', xhr=True)
322 def repo_group_list_data(self):
323 _ = self.request.translate
324 self.load_default_context()
325
326 query = self.request.GET.get('query')
327
328 log.debug('generating repo group list, query:%s',
329 query)
330
331 res = []
332 repo_groups = self._get_repo_group_list(query)
333 if repo_groups:
334 res.append({
335 'text': _('Repository Groups'),
336 'children': repo_groups
337 })
338
339 data = {
340 'more': False,
341 'results': res
342 }
343 return data
344
345 def _get_default_search_queries(self, search_context, searcher, query):
346 if not searcher:
347 return []
348
349 is_es_6 = searcher.is_es_6
350
351 queries = []
352 repo_group_name, repo_name, repo_context = None, None, None
353
354 # repo group context
355 if search_context.get('search_context[repo_group_name]'):
356 repo_group_name = search_context.get('search_context[repo_group_name]')
357 if search_context.get('search_context[repo_name]'):
358 repo_name = search_context.get('search_context[repo_name]')
359 repo_context = search_context.get('search_context[repo_view_type]')
360
361 if is_es_6 and repo_name:
362 # files
363 def query_modifier():
364 qry = query
365 return {'q': qry, 'type': 'content'}
366 label = u'File search for `{}` in this repository.'.format(query)
367 queries.append(
368 {
369 'id': -10,
370 'value': query,
371 'value_display': label,
372 'type': 'search',
373 'url': h.route_path('search_repo',
374 repo_name=repo_name,
375 _query=query_modifier())
376 }
377 )
378
379 # commits
380 def query_modifier():
381 qry = query
382 return {'q': qry, 'type': 'commit'}
383
384 label = u'Commit search for `{}` in this repository.'.format(query)
385 queries.append(
386 {
387 'id': -20,
388 'value': query,
389 'value_display': label,
390 'type': 'search',
391 'url': h.route_path('search_repo',
392 repo_name=repo_name,
393 _query=query_modifier())
394 }
395 )
396
397 elif is_es_6 and repo_group_name:
398 # files
399 def query_modifier():
400 qry = query
401 return {'q': qry, 'type': 'content'}
402
403 label = u'File search for `{}` in this repository group'.format(query)
404 queries.append(
405 {
406 'id': -30,
407 'value': query,
408 'value_display': label,
409 'type': 'search',
410 'url': h.route_path('search_repo_group',
411 repo_group_name=repo_group_name,
412 _query=query_modifier())
413 }
414 )
415
416 # commits
417 def query_modifier():
418 qry = query
419 return {'q': qry, 'type': 'commit'}
420
421 label = u'Commit search for `{}` in this repository group'.format(query)
422 queries.append(
423 {
424 'id': -40,
425 'value': query,
426 'value_display': label,
427 'type': 'search',
428 'url': h.route_path('search_repo_group',
429 repo_group_name=repo_group_name,
430 _query=query_modifier())
431 }
432 )
433
434 if not queries:
435 queries.append(
436 {
437 'id': -1,
438 'value': query,
439 'value_display': u'File search for: `{}`'.format(query),
440 'type': 'search',
441 'url': h.route_path('search',
442 _query={'q': query, 'type': 'content'})
443 })
444 queries.append(
445 {
446 'id': -2,
447 'value': query,
448 'value_display': u'Commit search for: `{}`'.format(query),
449 'type': 'search',
450 'url': h.route_path('search',
451 _query={'q': query, 'type': 'commit'})
452 })
453
454 return queries
455
456 @LoginRequired()
457 @view_config(
308 458 route_name='goto_switcher_data', request_method='GET',
309 459 renderer='json_ext', xhr=True)
310 460 def goto_switcher_data(self):
@@ -315,26 +465,21 b' class HomeView(BaseAppView):'
315 465 query = self.request.GET.get('query')
316 466 log.debug('generating main filter data, query %s', query)
317 467
318 default_search_val = u'Full text search for: `{}`'.format(query)
319 468 res = []
320 469 if not query:
321 470 return {'suggestions': res}
322 471
323 res.append({
324 'id': -1,
325 'value': query,
326 'value_display': default_search_val,
327 'type': 'search',
328 'url': h.route_path(
329 'search', _query={'q': query})
330 })
331 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
472 searcher = searcher_from_config(self.request.registry.settings)
473 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
474 res.append(_q)
475
476 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
332 477 if repo_group_id:
333 478 repo_group = RepoGroup.get(repo_group_id)
334 479 composed_hint = '{}/{}'.format(repo_group.group_name, query)
335 480 show_hint = not query.startswith(repo_group.group_name)
336 481 if repo_group and show_hint:
337 hint = u'Group search: `{}`'.format(composed_hint)
482 hint = u'Repository search inside: `{}`'.format(composed_hint)
338 483 res.append({
339 484 'id': -1,
340 485 'value': composed_hint,
@@ -351,7 +496,7 b' class HomeView(BaseAppView):'
351 496 for serialized_repo in repos:
352 497 res.append(serialized_repo)
353 498
354 # TODO(marcink): permissions for that ?
499 # TODO(marcink): should all logged in users be allowed to search others?
355 500 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
356 501 if allowed_user_search:
357 502 users = self._get_user_list(query)
@@ -362,7 +507,7 b' class HomeView(BaseAppView):'
362 507 for serialized_user_group in user_groups:
363 508 res.append(serialized_user_group)
364 509
365 commits = self._get_hash_commit_list(c.auth_user, query)
510 commits = self._get_hash_commit_list(c.auth_user, searcher, query)
366 511 if commits:
367 512 unique_repos = collections.OrderedDict()
368 513 for commit in commits:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -107,6 +107,26 b' class TestLoginController(object):'
107 107
108 108 response.mustcontain('/%s' % HG_REPO)
109 109
110 def test_login_regular_forbidden_when_super_admin_restriction(self):
111 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
112 with fixture.auth_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 response = self.app.post(route_path('login'),
114 {'username': 'test_regular',
115 'password': 'test12'})
116
117 response.mustcontain('invalid user name')
118 response.mustcontain('invalid password')
119
120 def test_login_regular_forbidden_when_scope_restriction(self):
121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 with fixture.scope_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
123 response = self.app.post(route_path('login'),
124 {'username': 'test_regular',
125 'password': 'test12'})
126
127 response.mustcontain('invalid user name')
128 response.mustcontain('invalid password')
129
110 130 def test_login_ok_came_from(self):
111 131 test_came_from = '/_admin/users?branch=stable'
112 132 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -74,20 +74,17 b' class TestPasswordReset(TestController):'
74 74 'default_password_reset': pwd_reset_setting,
75 75 'default_extern_activate': 'hg.extern_activate.auto',
76 76 }
77 resp = self.app.post(route_path('admin_permissions_application_update'), params=params)
77 resp = self.app.post(
78 route_path('admin_permissions_application_update'), params=params)
78 79 self.logout_user()
79 80
80 81 login_page = self.app.get(route_path('login'))
81 82 asr_login = AssertResponse(login_page)
82 index_page = self.app.get(h.route_path('home'))
83 asr_index = AssertResponse(index_page)
84 83
85 84 if show_link:
86 85 asr_login.one_element_exists('a.pwd_reset')
87 asr_index.one_element_exists('a.pwd_reset')
88 86 else:
89 87 asr_login.no_element_exists('a.pwd_reset')
90 asr_index.no_element_exists('a.pwd_reset')
91 88
92 89 response = self.app.get(route_path('reset_password'))
93 90
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -95,6 +95,18 b' def includeme(config):'
95 95 pattern=ADMIN_PREFIX + '/my_account/watched')
96 96
97 97 config.add_route(
98 name='my_account_bookmarks',
99 pattern=ADMIN_PREFIX + '/my_account/bookmarks')
100
101 config.add_route(
102 name='my_account_bookmarks_update',
103 pattern=ADMIN_PREFIX + '/my_account/bookmarks/update')
104
105 config.add_route(
106 name='my_account_goto_bookmark',
107 pattern=ADMIN_PREFIX + '/my_account/bookmark/{bookmark_id}')
108
109 config.add_route(
98 110 name='my_account_perms',
99 111 pattern=ADMIN_PREFIX + '/my_account/perms')
100 112
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -19,7 +19,7 b''
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 # -*- coding: utf-8 -*-
21 21
22 # Copyright (C) 2016-2018 RhodeCode GmbH
22 # Copyright (C) 2016-2019 RhodeCode GmbH
23 23 #
24 24 # This program is free software: you can redistribute it and/or modify
25 25 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -20,29 +20,30 b''
20 20
21 21 import logging
22 22 import datetime
23 import string
23 24
24 25 import formencode
25 26 import formencode.htmlfill
27 import peppercorn
26 28 from pyramid.httpexceptions import HTTPFound
27 29 from pyramid.view import view_config
28 from pyramid.renderers import render
29 from pyramid.response import Response
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, \
37 HasRepoPermissionAny, HasRepoGroupPermissionAny
37 38 from rhodecode.lib.channelstream import (
38 39 channelstream_request, ChannelstreamException)
39 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 41 from rhodecode.model.auth_token import AuthTokenModel
41 42 from rhodecode.model.comment import CommentsModel
42 43 from rhodecode.model.db import (
43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
44 PullRequest)
45 from rhodecode.model.forms import UserForm, UserExtraEmailForm
44 IntegrityError, joinedload,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup)
46 47 from rhodecode.model.meta import Session
47 48 from rhodecode.model.pull_request import PullRequestModel
48 49 from rhodecode.model.scm import RepoList
@@ -392,6 +393,140 b' class MyAccountView(BaseAppView, DataGri'
392 393 @LoginRequired()
393 394 @NotAnonymous()
394 395 @view_config(
396 route_name='my_account_bookmarks', request_method='GET',
397 renderer='rhodecode:templates/admin/my_account/my_account.mako')
398 def my_account_bookmarks(self):
399 c = self.load_default_context()
400 c.active = 'bookmarks'
401 return self._get_template_context(c)
402
403 def _process_entry(self, entry, user_id):
404 position = safe_int(entry.get('position'))
405 if position is None:
406 return
407
408 # check if this is an existing entry
409 is_new = False
410 db_entry = UserBookmark().get_by_position_for_user(position, user_id)
411
412 if db_entry and str2bool(entry.get('remove')):
413 log.debug('Marked bookmark %s for deletion', db_entry)
414 Session().delete(db_entry)
415 return
416
417 if not db_entry:
418 # new
419 db_entry = UserBookmark()
420 is_new = True
421
422 should_save = False
423 default_redirect_url = ''
424
425 # save repo
426 if entry.get('bookmark_repo'):
427 repo = Repository.get(entry['bookmark_repo'])
428 perm_check = HasRepoPermissionAny(
429 'repository.read', 'repository.write', 'repository.admin')
430 if repo and perm_check(repo_name=repo.repo_name):
431 db_entry.repository = repo
432 should_save = True
433 default_redirect_url = '${repo_url}'
434 # save repo group
435 elif entry.get('bookmark_repo_group'):
436 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
437 perm_check = HasRepoGroupPermissionAny(
438 'group.read', 'group.write', 'group.admin')
439
440 if repo_group and perm_check(group_name=repo_group.group_name):
441 db_entry.repository_group = repo_group
442 should_save = True
443 default_redirect_url = '${repo_group_url}'
444 # save generic info
445 elif entry.get('title') and entry.get('redirect_url'):
446 should_save = True
447
448 if should_save:
449 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
450 # mark user and position
451 db_entry.user_id = user_id
452 db_entry.position = position
453 db_entry.title = entry.get('title')
454 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
455
456 Session().add(db_entry)
457
458 @LoginRequired()
459 @NotAnonymous()
460 @CSRFRequired()
461 @view_config(
462 route_name='my_account_bookmarks_update', request_method='POST')
463 def my_account_bookmarks_update(self):
464 _ = self.request.translate
465 c = self.load_default_context()
466 c.active = 'bookmarks'
467
468 controls = peppercorn.parse(self.request.POST.items())
469 user_id = c.user.user_id
470
471 try:
472 for entry in controls.get('bookmarks', []):
473 self._process_entry(entry, user_id)
474
475 Session().commit()
476 h.flash(_("Update Bookmarks"), category='success')
477 except IntegrityError:
478 h.flash(_("Failed to update bookmarks. "
479 "Make sure an unique position is used"), category='error')
480
481 return HTTPFound(h.route_path('my_account_bookmarks'))
482
483 @LoginRequired()
484 @NotAnonymous()
485 @view_config(
486 route_name='my_account_goto_bookmark', request_method='GET',
487 renderer='rhodecode:templates/admin/my_account/my_account.mako')
488 def my_account_goto_bookmark(self):
489
490 bookmark_id = self.request.matchdict['bookmark_id']
491 user_bookmark = UserBookmark().query()\
492 .filter(UserBookmark.user_id == self.request.user.user_id) \
493 .filter(UserBookmark.position == bookmark_id).scalar()
494
495 redirect_url = h.route_path('my_account_bookmarks')
496 if not user_bookmark:
497 raise HTTPFound(redirect_url)
498
499 if user_bookmark.repository:
500 repo_name = user_bookmark.repository.repo_name
501 base_redirect_url = h.route_path(
502 'repo_summary', repo_name=repo_name)
503 if user_bookmark.redirect_url and \
504 '${repo_url}' in user_bookmark.redirect_url:
505 redirect_url = string.Template(user_bookmark.redirect_url)\
506 .safe_substitute({'repo_url': base_redirect_url})
507 else:
508 redirect_url = base_redirect_url
509
510 elif user_bookmark.repository_group:
511 repo_group_name = user_bookmark.repository_group.group_name
512 base_redirect_url = h.route_path(
513 'repo_group_home', repo_group_name=repo_group_name)
514 if user_bookmark.redirect_url and \
515 '${repo_group_url}' in user_bookmark.redirect_url:
516 redirect_url = string.Template(user_bookmark.redirect_url)\
517 .safe_substitute({'repo_group_url': base_redirect_url})
518 else:
519 redirect_url = base_redirect_url
520
521 elif user_bookmark.redirect_url:
522 redirect_url = user_bookmark.redirect_url
523
524 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
525 raise HTTPFound(redirect_url)
526
527 @LoginRequired()
528 @NotAnonymous()
529 @view_config(
395 530 route_name='my_account_perms', request_method='GET',
396 531 renderer='rhodecode:templates/admin/my_account/my_account.mako')
397 532 def my_account_perms(self):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -71,6 +71,7 b' class MyAccountSshKeysView(BaseAppView, '
71 71 c = self.load_default_context()
72 72
73 73 c.active = 'ssh_keys_generate'
74 if c.ssh_key_generator_enabled:
74 75 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
75 76 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
76 77 c.target_form_url = h.route_path(
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -49,9 +49,13 b' class OpsView(BaseAppView):'
49 49 'instance': self.request.registry.settings.get('instance_id'),
50 50 }
51 51 if getattr(self.request, 'user'):
52 caller_name = 'anonymous'
53 if self.request.user.user_id:
54 caller_name = self.request.user.username
55
52 56 data.update({
53 57 'caller_ip': self.request.user.ip_addr,
54 'caller_name': self.request.user.username,
58 'caller_name': caller_name,
55 59 })
56 60 return {'ok': data}
57 61
@@ -65,11 +69,13 b' class OpsView(BaseAppView):'
65 69 """
66 70 Test exception handling and emails on errors
67 71 """
72
68 73 class TestException(Exception):
69 74 pass
70
75 # add timeout so we add some sort of rate limiter
76 time.sleep(2)
71 77 msg = ('RhodeCode Enterprise test exception. '
72 'Generation time: {}'.format(time.time()))
78 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
73 79 raise TestException(msg)
74 80
75 81 @view_config(
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -271,8 +271,8 b' def includeme(config):'
271 271 repo_route=True)
272 272
273 273 config.add_route(
274 name='pullrequest_repo_destinations',
275 pattern='/{repo_name:.*?[^/]}/pull-request/repo-destinations',
274 name='pullrequest_repo_targets',
275 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
276 276 repo_route=True)
277 277
278 278 config.add_route(
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -32,7 +32,6 b' from rhodecode.model.pull_request import'
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.tests import (
34 34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
36 35
37 36
38 37 def route_path(name, params=None, **kwargs):
@@ -45,7 +44,7 b' def route_path(name, params=None, **kwar'
45 44 'pullrequest_show_all': '/{repo_name}/pull-request',
46 45 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 46 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
47 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
49 48 'pullrequest_new': '/{repo_name}/pull-request/new',
50 49 'pullrequest_create': '/{repo_name}/pull-request/create',
51 50 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
@@ -233,8 +232,7 b' class TestPullrequestsView(object):'
233 232 route_path('pullrequest_update',
234 233 repo_name=pull_request.target_repo.repo_name,
235 234 pull_request_id=pull_request_id),
236 params={'update_commits': 'true',
237 'csrf_token': csrf_token})
235 params={'update_commits': 'true', 'csrf_token': csrf_token})
238 236
239 237 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 238 UpdateFailureReason.MISSING_SOURCE_REF])
@@ -244,7 +242,8 b' class TestPullrequestsView(object):'
244 242 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 243 pull_request = pr_util.create_pull_request(
246 244 approved=True, mergeable=True)
247 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
245 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
246 pull_request.target_ref = unicode_reference
248 247 Session().add(pull_request)
249 248 Session().commit()
250 249
@@ -255,12 +254,12 b' class TestPullrequestsView(object):'
255 254 pull_request_id=pull_request_id)
256 255
257 256 response = self.app.get(pull_request_url)
258
259 assertr = AssertResponse(response)
260 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
261 MergeFailureReason.MISSING_TARGET_REF]
262 assertr.element_contains(
263 'span[data-role="merge-message"]', str(expected_msg))
257 target_ref_id = 'invalid-branch'
258 merge_resp = MergeResponse(
259 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
260 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
261 response.assert_response().element_contains(
262 'span[data-role="merge-message"]', merge_resp.merge_status_message)
264 263
265 264 def test_comment_and_close_pull_request_custom_message_approved(
266 265 self, pr_util, csrf_token, xhr_header):
@@ -608,8 +607,7 b' class TestPullrequestsView(object):'
608 607
609 608 response = self.app.post(
610 609 route_path('pullrequest_merge',
611 repo_name=repo_name,
612 pull_request_id=pull_request_id),
610 repo_name=repo_name, pull_request_id=pull_request_id),
613 611 params={'csrf_token': csrf_token}).follow()
614 612
615 613 assert response.status_int == 200
@@ -624,10 +622,13 b' class TestPullrequestsView(object):'
624 622 pull_request_id = pull_request.pull_request_id
625 623 repo_name = pull_request.target_repo.scm_instance().name
626 624
625 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
626 MergeFailureReason.PUSH_FAILED,
627 metadata={'target': 'shadow repo',
628 'merge_commit': 'xxx'})
627 629 model_patcher = mock.patch.multiple(
628 630 PullRequestModel,
629 merge_repo=mock.Mock(return_value=MergeResponse(
630 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
631 merge_repo=mock.Mock(return_value=merge_resp),
631 632 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
632 633
633 634 with model_patcher:
@@ -637,8 +638,10 b' class TestPullrequestsView(object):'
637 638 pull_request_id=pull_request_id),
638 639 params={'csrf_token': csrf_token}, status=302)
639 640
640 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
641 MergeFailureReason.PUSH_FAILED])
641 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
642 metadata={'target': 'shadow repo',
643 'merge_commit': 'xxx'})
644 assert_session_flash(response, merge_resp.merge_status_message)
642 645
643 646 def test_update_source_revision(self, backend, csrf_token):
644 647 commits = [
@@ -652,20 +655,20 b' class TestPullrequestsView(object):'
652 655
653 656 # create pr from a in source to A in target
654 657 pull_request = PullRequest()
658
655 659 pull_request.source_repo = source
656 # TODO: johbo: Make sure that we write the source ref this way!
657 660 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
658 661 branch=backend.default_branch_name, commit_id=commit_ids['change'])
662
659 663 pull_request.target_repo = target
660
661 664 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
662 branch=backend.default_branch_name,
663 commit_id=commit_ids['ancestor'])
665 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
666
664 667 pull_request.revisions = [commit_ids['change']]
665 668 pull_request.title = u"Test"
666 669 pull_request.description = u"Description"
667 pull_request.author = UserModel().get_by_username(
668 TEST_USER_ADMIN_LOGIN)
670 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
671 pull_request.pull_request_state = PullRequest.STATE_CREATED
669 672 Session().add(pull_request)
670 673 Session().commit()
671 674 pull_request_id = pull_request.pull_request_id
@@ -676,23 +679,21 b' class TestPullrequestsView(object):'
676 679 # update PR
677 680 self.app.post(
678 681 route_path('pullrequest_update',
682 repo_name=target.repo_name, pull_request_id=pull_request_id),
683 params={'update_commits': 'true', 'csrf_token': csrf_token})
684
685 response = self.app.get(
686 route_path('pullrequest_show',
679 687 repo_name=target.repo_name,
680 pull_request_id=pull_request_id),
681 params={'update_commits': 'true',
682 'csrf_token': csrf_token})
688 pull_request_id=pull_request.pull_request_id))
689
690 assert response.status_int == 200
691 assert 'Pull request updated to' in response.body
692 assert 'with 1 added, 0 removed commits.' in response.body
683 693
684 694 # check that we have now both revisions
685 695 pull_request = PullRequest.get(pull_request_id)
686 assert pull_request.revisions == [
687 commit_ids['change-2'], commit_ids['change']]
688
689 # TODO: johbo: this should be a test on its own
690 response = self.app.get(route_path(
691 'pullrequest_new',
692 repo_name=target.repo_name))
693 assert response.status_int == 200
694 assert 'Pull request updated to' in response.body
695 assert 'with 1 added, 0 removed commits.' in response.body
696 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
696 697
697 698 def test_update_target_revision(self, backend, csrf_token):
698 699 commits = [
@@ -707,21 +708,21 b' class TestPullrequestsView(object):'
707 708
708 709 # create pr from a in source to A in target
709 710 pull_request = PullRequest()
711
710 712 pull_request.source_repo = source
711 # TODO: johbo: Make sure that we write the source ref this way!
712 713 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
713 714 branch=backend.default_branch_name, commit_id=commit_ids['change'])
715
714 716 pull_request.target_repo = target
715 # TODO: johbo: Target ref should be branch based, since tip can jump
716 # from branch to branch
717 717 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
718 branch=backend.default_branch_name,
719 commit_id=commit_ids['ancestor'])
718 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
719
720 720 pull_request.revisions = [commit_ids['change']]
721 721 pull_request.title = u"Test"
722 722 pull_request.description = u"Description"
723 pull_request.author = UserModel().get_by_username(
724 TEST_USER_ADMIN_LOGIN)
723 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
724 pull_request.pull_request_state = PullRequest.STATE_CREATED
725
725 726 Session().add(pull_request)
726 727 Session().commit()
727 728 pull_request_id = pull_request.pull_request_id
@@ -736,21 +737,19 b' class TestPullrequestsView(object):'
736 737 route_path('pullrequest_update',
737 738 repo_name=target.repo_name,
738 739 pull_request_id=pull_request_id),
739 params={'update_commits': 'true',
740 'csrf_token': csrf_token},
740 params={'update_commits': 'true', 'csrf_token': csrf_token},
741 741 status=200)
742 742
743 743 # check that we have now both revisions
744 744 pull_request = PullRequest.get(pull_request_id)
745 745 assert pull_request.revisions == [commit_ids['change-rebased']]
746 746 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
747 branch=backend.default_branch_name,
748 commit_id=commit_ids['ancestor-new'])
747 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
749 748
750 # TODO: johbo: This should be a test on its own
751 response = self.app.get(route_path(
752 'pullrequest_new',
753 repo_name=target.repo_name))
749 response = self.app.get(
750 route_path('pullrequest_show',
751 repo_name=target.repo_name,
752 pull_request_id=pull_request.pull_request_id))
754 753 assert response.status_int == 200
755 754 assert 'Pull request updated to' in response.body
756 755 assert 'with 1 added, 1 removed commits.' in response.body
@@ -772,17 +771,14 b' class TestPullrequestsView(object):'
772 771 # create pr from a in source to A in target
773 772 pull_request = PullRequest()
774 773 pull_request.source_repo = source
775 # TODO: johbo: Make sure that we write the source ref this way!
774
776 775 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
777 776 branch=backend.default_branch_name,
778 777 commit_id=commit_ids['master-commit-3-change-2'])
779 778
780 779 pull_request.target_repo = target
781 # TODO: johbo: Target ref should be branch based, since tip can jump
782 # from branch to branch
783 780 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
784 branch=backend.default_branch_name,
785 commit_id=commit_ids['feat-commit-2'])
781 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
786 782
787 783 pull_request.revisions = [
788 784 commit_ids['feat-commit-1'],
@@ -790,8 +786,8 b' class TestPullrequestsView(object):'
790 786 ]
791 787 pull_request.title = u"Test"
792 788 pull_request.description = u"Description"
793 pull_request.author = UserModel().get_by_username(
794 TEST_USER_ADMIN_LOGIN)
789 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
790 pull_request.pull_request_state = PullRequest.STATE_CREATED
795 791 Session().add(pull_request)
796 792 Session().commit()
797 793 pull_request_id = pull_request.pull_request_id
@@ -807,13 +803,10 b' class TestPullrequestsView(object):'
807 803 route_path('pullrequest_update',
808 804 repo_name=target.repo_name,
809 805 pull_request_id=pull_request_id),
810 params={'update_commits': 'true',
811 'csrf_token': csrf_token},
806 params={'update_commits': 'true', 'csrf_token': csrf_token},
812 807 status=200)
813 808
814 response = self.app.get(route_path(
815 'pullrequest_new',
816 repo_name=target.repo_name))
809 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
817 810 assert response.status_int == 200
818 811 response.mustcontain('Pull request updated to')
819 812 response.mustcontain('with 0 added, 0 removed commits.')
@@ -833,21 +826,17 b' class TestPullrequestsView(object):'
833 826 # create pr from a in source to A in target
834 827 pull_request = PullRequest()
835 828 pull_request.source_repo = source
836 # TODO: johbo: Make sure that we write the source ref this way!
829
837 830 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
838 branch=backend.default_branch_name,
839 commit_id=commit_ids['change'])
831 branch=backend.default_branch_name, commit_id=commit_ids['change'])
840 832 pull_request.target_repo = target
841 # TODO: johbo: Target ref should be branch based, since tip can jump
842 # from branch to branch
843 833 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
844 branch=backend.default_branch_name,
845 commit_id=commit_ids['ancestor'])
834 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
846 835 pull_request.revisions = [commit_ids['change']]
847 836 pull_request.title = u"Test"
848 837 pull_request.description = u"Description"
849 pull_request.author = UserModel().get_by_username(
850 TEST_USER_ADMIN_LOGIN)
838 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
839 pull_request.pull_request_state = PullRequest.STATE_CREATED
851 840 Session().add(pull_request)
852 841 Session().commit()
853 842 pull_request_id = pull_request.pull_request_id
@@ -860,10 +849,8 b' class TestPullrequestsView(object):'
860 849 # update PR
861 850 self.app.post(
862 851 route_path('pullrequest_update',
863 repo_name=target.repo_name,
864 pull_request_id=pull_request_id),
865 params={'update_commits': 'true',
866 'csrf_token': csrf_token},
852 repo_name=target.repo_name, pull_request_id=pull_request_id),
853 params={'update_commits': 'true', 'csrf_token': csrf_token},
867 854 status=200)
868 855
869 856 # Expect the target reference to be updated correctly
@@ -890,13 +877,12 b' class TestPullrequestsView(object):'
890 877 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
891 878 branch=branch_name, commit_id=commit_ids['new-feature'])
892 879 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
893 branch=backend_git.default_branch_name,
894 commit_id=commit_ids['old-feature'])
880 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
895 881 pull_request.revisions = [commit_ids['new-feature']]
896 882 pull_request.title = u"Test"
897 883 pull_request.description = u"Description"
898 pull_request.author = UserModel().get_by_username(
899 TEST_USER_ADMIN_LOGIN)
884 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
885 pull_request.pull_request_state = PullRequest.STATE_CREATED
900 886 Session().add(pull_request)
901 887 Session().commit()
902 888
@@ -909,11 +895,11 b' class TestPullrequestsView(object):'
909 895 pull_request_id=pull_request.pull_request_id))
910 896
911 897 assert response.status_int == 200
912 assert_response = AssertResponse(response)
913 assert_response.element_contains(
898
899 response.assert_response().element_contains(
914 900 '#changeset_compare_view_content .alert strong',
915 901 'Missing commits')
916 assert_response.element_contains(
902 response.assert_response().element_contains(
917 903 '#changeset_compare_view_content .alert',
918 904 'This pull request cannot be displayed, because one or more'
919 905 ' commits no longer exist in the source repository.')
@@ -941,15 +927,15 b' class TestPullrequestsView(object):'
941 927 pull_request_id=pull_request.pull_request_id))
942 928
943 929 assert response.status_int == 200
944 assert_response = AssertResponse(response)
945 assert_response.element_contains(
930
931 response.assert_response().element_contains(
946 932 '#changeset_compare_view_content .alert strong',
947 933 'Missing commits')
948 assert_response.element_contains(
934 response.assert_response().element_contains(
949 935 '#changeset_compare_view_content .alert',
950 936 'This pull request cannot be displayed, because one or more'
951 937 ' commits no longer exist in the source repository.')
952 assert_response.element_contains(
938 response.assert_response().element_contains(
953 939 '#update_commits',
954 940 'Update commits')
955 941
@@ -987,8 +973,7 b' class TestPullrequestsView(object):'
987 973 pull_request_id=pull_request.pull_request_id))
988 974
989 975 assert response.status_int == 200
990 assert_response = AssertResponse(response)
991 assert_response.element_contains(
976 response.assert_response().element_contains(
992 977 '#changeset_compare_view_content .alert strong',
993 978 'Missing commits')
994 979
@@ -1004,12 +989,11 b' class TestPullrequestsView(object):'
1004 989 repo_name=pull_request.target_repo.scm_instance().name,
1005 990 pull_request_id=pull_request.pull_request_id))
1006 991 assert response.status_int == 200
1007 assert_response = AssertResponse(response)
1008 992
1009 origin = assert_response.get_element('.pr-origininfo .tag')
993 origin = response.assert_response().get_element('.pr-origininfo .tag')
1010 994 origin_children = origin.getchildren()
1011 995 assert len(origin_children) == 1
1012 target = assert_response.get_element('.pr-targetinfo .tag')
996 target = response.assert_response().get_element('.pr-targetinfo .tag')
1013 997 target_children = target.getchildren()
1014 998 assert len(target_children) == 1
1015 999
@@ -1038,13 +1022,12 b' class TestPullrequestsView(object):'
1038 1022 repo_name=pull_request.target_repo.scm_instance().name,
1039 1023 pull_request_id=pull_request.pull_request_id))
1040 1024 assert response.status_int == 200
1041 assert_response = AssertResponse(response)
1042 1025
1043 origin = assert_response.get_element('.pr-origininfo .tag')
1026 origin = response.assert_response().get_element('.pr-origininfo .tag')
1044 1027 assert origin.text.strip() == 'bookmark: origin'
1045 1028 assert origin.getchildren() == []
1046 1029
1047 target = assert_response.get_element('.pr-targetinfo .tag')
1030 target = response.assert_response().get_element('.pr-targetinfo .tag')
1048 1031 assert target.text.strip() == 'bookmark: target'
1049 1032 assert target.getchildren() == []
1050 1033
@@ -1060,13 +1043,12 b' class TestPullrequestsView(object):'
1060 1043 repo_name=pull_request.target_repo.scm_instance().name,
1061 1044 pull_request_id=pull_request.pull_request_id))
1062 1045 assert response.status_int == 200
1063 assert_response = AssertResponse(response)
1064 1046
1065 origin = assert_response.get_element('.pr-origininfo .tag')
1047 origin = response.assert_response().get_element('.pr-origininfo .tag')
1066 1048 assert origin.text.strip() == 'tag: origin'
1067 1049 assert origin.getchildren() == []
1068 1050
1069 target = assert_response.get_element('.pr-targetinfo .tag')
1051 target = response.assert_response().get_element('.pr-targetinfo .tag')
1070 1052 assert target.text.strip() == 'tag: target'
1071 1053 assert target.getchildren() == []
1072 1054
@@ -1090,12 +1072,13 b' class TestPullrequestsView(object):'
1090 1072 repo_name=target_repo.name,
1091 1073 pull_request_id=pr_id))
1092 1074
1093 assertr = AssertResponse(response)
1094 1075 if mergeable:
1095 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1096 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1076 response.assert_response().element_value_contains(
1077 'input.pr-mergeinfo', shadow_url)
1078 response.assert_response().element_value_contains(
1079 'input.pr-mergeinfo ', 'pr-merge')
1097 1080 else:
1098 assertr.no_element_exists('.pr-mergeinfo')
1081 response.assert_response().no_element_exists('.pr-mergeinfo')
1099 1082
1100 1083
1101 1084 @pytest.mark.usefixtures('app')
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -203,7 +203,6 b' class RepoChangelogView(RepoAppView):'
203 203 pre_load = self._get_preload_attrs()
204 204
205 205 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
206
207 206 try:
208 207 if f_path:
209 208 log.debug('generating changelog for path %s', f_path)
@@ -231,7 +230,7 b' class RepoChangelogView(RepoAppView):'
231 230 else:
232 231 collection = self.rhodecode_vcs_repo.get_commits(
233 232 branch_name=branch_name, show_hidden=show_hidden,
234 pre_load=pre_load)
233 pre_load=pre_load, translate_tags=False)
235 234
236 235 self._load_changelog_data(
237 236 c, collection, p, chunk_size, c.branch_name,
@@ -320,7 +319,8 b' class RepoChangelogView(RepoAppView):'
320 319 collection = list(reversed(collection))
321 320 else:
322 321 collection = self.rhodecode_vcs_repo.get_commits(
323 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load)
322 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
323 translate_tags=False)
324 324
325 325 p = safe_int(self.request.GET.get('page', 1), 1)
326 326 try:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -97,7 +97,7 b' class RepoCommitsView(RepoAppView):'
97 97 if len(commit_range) == 2:
98 98 commits = self.rhodecode_vcs_repo.get_commits(
99 99 start_id=commit_range[0], end_id=commit_range[1],
100 pre_load=pre_load)
100 pre_load=pre_load, translate_tags=False)
101 101 commits = list(commits)
102 102 else:
103 103 commits = [self.rhodecode_vcs_repo.get_commit(
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -27,12 +27,13 b' from pyramid.renderers import render'
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from rhodecode.lib.utils import safe_str
35 35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
36 37 from rhodecode.lib.vcs.exceptions import (
37 38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 39 NodeDoesNotExistError)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,9 +34,10 b' from pyramid.response import Response'
34 34 import rhodecode
35 35 from rhodecode.apps._base import RepoAppView
36 36
37 from rhodecode.controllers.utils import parse_path_ref
37
38 38 from rhodecode.lib import diffs, helpers as h, rc_cache
39 39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.view_utils import parse_path_ref
40 41 from rhodecode.lib.exceptions import NonRelativePathError
41 42 from rhodecode.lib.codeblocks import (
42 43 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
@@ -83,6 +84,7 b' class RepoFilesView(RepoAppView):'
83 84 def load_default_context(self):
84 85 c = self._get_local_tmpl_context(include_app_defaults=True)
85 86 c.rhodecode_repo = self.rhodecode_vcs_repo
87 c.enable_downloads = self.db_repo.enable_downloads
86 88 return c
87 89
88 90 def _ensure_not_locked(self):
@@ -227,10 +229,11 b' class RepoFilesView(RepoAppView):'
227 229 self, c, commit_id, f_path, full_load=False):
228 230
229 231 repo_id = self.db_repo.repo_id
232 force_recache = self.get_recache_flag()
230 233
231 234 cache_seconds = safe_int(
232 235 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
233 cache_on = cache_seconds > 0
236 cache_on = not force_recache and cache_seconds > 0
234 237 log.debug(
235 238 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
236 239 'with caching: %s[TTL: %ss]' % (
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -50,8 +50,13 b' class RepoSettingsPermissionsView(RepoAp'
50 50 route_name='edit_repo_perms', request_method='GET',
51 51 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
52 52 def edit_permissions(self):
53 _ = self.request.translate
53 54 c = self.load_default_context()
54 55 c.active = 'permissions'
56 if self.request.GET.get('branch_permissions'):
57 h.flash(_('Explicitly add user or user group with write+ '
58 'permission to modify their branch permissions.'),
59 category='notice')
55 60 return self._get_template_context(c)
56 61
57 62 @LoginRequired()
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -137,13 +137,6 b' class RepoPullRequestsView(RepoAppView, '
137 137 })
138 138 return data
139 139
140 def get_recache_flag(self):
141 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
142 flag_val = self.request.GET.get(flag_name)
143 if str2bool(flag_val):
144 return True
145 return False
146
147 140 @LoginRequired()
148 141 @HasRepoPermissionAnyDecorator(
149 142 'repository.read', 'repository.write', 'repository.admin')
@@ -272,9 +265,22 b' class RepoPullRequestsView(RepoAppView, '
272 265 route_name='pullrequest_show', request_method='GET',
273 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
274 267 def pull_request_show(self):
275 pull_request_id = self.request.matchdict['pull_request_id']
268 _ = self.request.translate
269 c = self.load_default_context()
270
271 pull_request = PullRequest.get_or_404(
272 self.request.matchdict['pull_request_id'])
273 pull_request_id = pull_request.pull_request_id
276 274
277 c = self.load_default_context()
275 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
276 log.debug('show: forbidden because pull request is in state %s',
277 pull_request.pull_request_state)
278 msg = _(u'Cannot show pull requests in state other than `{}`. '
279 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
280 pull_request.pull_request_state)
281 h.flash(msg, category='error')
282 raise HTTPFound(h.route_path('pullrequest_show_all',
283 repo_name=self.db_repo_name))
278 284
279 285 version = self.request.GET.get('version')
280 286 from_version = self.request.GET.get('from_version') or version
@@ -754,7 +760,7 b' class RepoPullRequestsView(RepoAppView, '
754 760
755 761 default_target_repo = source_repo
756 762
757 if source_repo.parent:
763 if source_repo.parent and c.has_origin_repo_read_perm:
758 764 parent_vcs_obj = source_repo.parent.scm_instance()
759 765 if parent_vcs_obj and not parent_vcs_obj.is_empty():
760 766 # change default if we have a parent repo
@@ -811,37 +817,51 b' class RepoPullRequestsView(RepoAppView, '
811 817 @HasRepoPermissionAnyDecorator(
812 818 'repository.read', 'repository.write', 'repository.admin')
813 819 @view_config(
814 route_name='pullrequest_repo_destinations', request_method='GET',
820 route_name='pullrequest_repo_targets', request_method='GET',
815 821 renderer='json_ext', xhr=True)
816 def pull_request_repo_destinations(self):
822 def pullrequest_repo_targets(self):
817 823 _ = self.request.translate
818 824 filter_query = self.request.GET.get('query')
819 825
820 query = Repository.query() \
826 # get the parents
827 parent_target_repos = []
828 if self.db_repo.parent:
829 parents_query = Repository.query() \
821 830 .order_by(func.length(Repository.repo_name)) \
822 .filter(
823 or_(Repository.repo_name == self.db_repo.repo_name,
824 Repository.fork_id == self.db_repo.repo_id))
831 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
825 832
826 833 if filter_query:
827 834 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
828 query = query.filter(
835 parents_query = parents_query.filter(
829 836 Repository.repo_name.ilike(ilike_expression))
837 parents = parents_query.limit(20).all()
838
839 for parent in parents:
840 parent_vcs_obj = parent.scm_instance()
841 if parent_vcs_obj and not parent_vcs_obj.is_empty():
842 parent_target_repos.append(parent)
830 843
831 add_parent = False
832 if self.db_repo.parent:
833 if filter_query in self.db_repo.parent.repo_name:
834 parent_vcs_obj = self.db_repo.parent.scm_instance()
835 if parent_vcs_obj and not parent_vcs_obj.is_empty():
836 add_parent = True
844 # get other forks, and repo itself
845 query = Repository.query() \
846 .order_by(func.length(Repository.repo_name)) \
847 .filter(
848 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
849 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
850 ) \
851 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
837 852
838 limit = 20 - 1 if add_parent else 20
839 all_repos = query.limit(limit).all()
840 if add_parent:
841 all_repos += [self.db_repo.parent]
853 if filter_query:
854 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
855 query = query.filter(Repository.repo_name.ilike(ilike_expression))
856
857 limit = max(20 - len(parent_target_repos), 5) # not less then 5
858 target_repos = query.limit(limit).all()
859
860 all_target_repos = target_repos + parent_target_repos
842 861
843 862 repos = []
844 for obj in ScmModel().get_repos(all_repos):
863 # This checks permissions to the repositories
864 for obj in ScmModel().get_repos(all_target_repos):
845 865 repos.append({
846 866 'id': obj['name'],
847 867 'text': obj['name'],
@@ -904,12 +924,17 b' class RepoPullRequestsView(RepoAppView, '
904 924 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
905 925 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
906 926
927 if not (source_db_repo or target_db_repo):
928 h.flash(_('source_repo or target repo not found'), category='error')
929 raise HTTPFound(
930 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
931
907 932 # re-check permissions again here
908 933 # source_repo we must have read permissions
909 934
910 935 source_perm = HasRepoPermissionAny(
911 'repository.read',
912 'repository.write', 'repository.admin')(source_db_repo.repo_name)
936 'repository.read', 'repository.write', 'repository.admin')(
937 source_db_repo.repo_name)
913 938 if not source_perm:
914 939 msg = _('Not Enough permissions to source repo `{}`.'.format(
915 940 source_db_repo.repo_name))
@@ -923,8 +948,8 b' class RepoPullRequestsView(RepoAppView, '
923 948 # target repo we must have read permissions, and also later on
924 949 # we want to check branch permissions here
925 950 target_perm = HasRepoPermissionAny(
926 'repository.read',
927 'repository.write', 'repository.admin')(target_db_repo.repo_name)
951 'repository.read', 'repository.write', 'repository.admin')(
952 target_db_repo.repo_name)
928 953 if not target_perm:
929 954 msg = _('Not Enough permissions to target repo `{}`.'.format(
930 955 target_db_repo.repo_name))
@@ -1027,6 +1052,15 b' class RepoPullRequestsView(RepoAppView, '
1027 1052 h.flash(msg, category='error')
1028 1053 return True
1029 1054
1055 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1056 log.debug('update: forbidden because pull request is in state %s',
1057 pull_request.pull_request_state)
1058 msg = _(u'Cannot update pull requests in state other than `{}`. '
1059 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1060 pull_request.pull_request_state)
1061 h.flash(msg, category='error')
1062 return True
1063
1030 1064 # only owner or admin can update it
1031 1065 allowed_to_update = PullRequestModel().check_user_update(
1032 1066 pull_request, self._rhodecode_user)
@@ -1069,6 +1103,8 b' class RepoPullRequestsView(RepoAppView, '
1069 1103
1070 1104 def _update_commits(self, pull_request):
1071 1105 _ = self.request.translate
1106
1107 with pull_request.set_state(PullRequest.STATE_UPDATING):
1072 1108 resp = PullRequestModel().update_commits(pull_request)
1073 1109
1074 1110 if resp.executed:
@@ -1082,8 +1118,7 b' class RepoPullRequestsView(RepoAppView, '
1082 1118 else:
1083 1119 changed = 'nothing'
1084 1120
1085 msg = _(
1086 u'Pull request updated to "{source_commit_id}" with '
1121 msg = _(u'Pull request updated to "{source_commit_id}" with '
1087 1122 u'{count_added} added, {count_removed} removed commits. '
1088 1123 u'Source of changes: {change_source}')
1089 1124 msg = msg.format(
@@ -1094,8 +1129,7 b' class RepoPullRequestsView(RepoAppView, '
1094 1129 h.flash(msg, category='success')
1095 1130
1096 1131 channel = '/repo${}$/pr/{}'.format(
1097 pull_request.target_repo.repo_name,
1098 pull_request.pull_request_id)
1132 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1099 1133 message = msg + (
1100 1134 ' - <a onclick="window.location.reload()">'
1101 1135 '<strong>{}</strong></a>'.format(_('Reload page')))
@@ -1128,8 +1162,23 b' class RepoPullRequestsView(RepoAppView, '
1128 1162 """
1129 1163 pull_request = PullRequest.get_or_404(
1130 1164 self.request.matchdict['pull_request_id'])
1165 _ = self.request.translate
1166
1167 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1168 log.debug('show: forbidden because pull request is in state %s',
1169 pull_request.pull_request_state)
1170 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1171 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1172 pull_request.pull_request_state)
1173 h.flash(msg, category='error')
1174 raise HTTPFound(
1175 h.route_path('pullrequest_show',
1176 repo_name=pull_request.target_repo.repo_name,
1177 pull_request_id=pull_request.pull_request_id))
1131 1178
1132 1179 self.load_default_context()
1180
1181 with pull_request.set_state(PullRequest.STATE_UPDATING):
1133 1182 check = MergeCheck.validate(
1134 1183 pull_request, auth_user=self._rhodecode_user,
1135 1184 translator=self.request.translate)
@@ -1144,6 +1193,7 b' class RepoPullRequestsView(RepoAppView, '
1144 1193 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1145 1194 username=self._rhodecode_db_user.username, action='push',
1146 1195 scm=pull_request.target_repo.repo_type)
1196 with pull_request.set_state(PullRequest.STATE_UPDATING):
1147 1197 self._merge_pull_request(
1148 1198 pull_request, self._rhodecode_db_user, extras)
1149 1199 else:
@@ -1167,10 +1217,8 b' class RepoPullRequestsView(RepoAppView, '
1167 1217 h.flash(msg, category='success')
1168 1218 else:
1169 1219 log.debug(
1170 "The merge was not successful. Merge response: %s",
1171 merge_resp)
1172 msg = PullRequestModel().merge_status_message(
1173 merge_resp.failure_reason)
1220 "The merge was not successful. Merge response: %s", merge_resp)
1221 msg = merge_resp.merge_status_message
1174 1222 h.flash(msg, category='error')
1175 1223
1176 1224 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -73,6 +73,8 b' class RepoSettingsView(RepoAppView):'
73 73 'repository.write', 'repository.read', 'repository.admin')(
74 74 self.db_repo.fork.repo_name, 'repo set as fork page')
75 75
76 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
77
76 78 return self._get_template_context(c)
77 79
78 80 @LoginRequired()
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -24,7 +24,7 b' import rhodecode'
24 24
25 25 from pyramid.view import view_config
26 26
27 from rhodecode.controllers import utils
27 from rhodecode.lib.view_utils import get_format_ref_id
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
30 30 from rhodecode.lib import helpers as h, rc_cache
@@ -141,7 +141,8 b' class RepoSummaryView(RepoAppView):'
141 141
142 142 pre_load = ['author', 'branch', 'date', 'message']
143 143 try:
144 collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
144 collection = self.rhodecode_vcs_repo.get_commits(
145 pre_load=pre_load, translate_tags=False)
145 146 except EmptyRepositoryError:
146 147 collection = self.rhodecode_vcs_repo
147 148
@@ -351,7 +352,7 b' class RepoSummaryView(RepoAppView):'
351 352 return data
352 353
353 354 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
354 format_ref_id = utils.get_format_ref_id(repo)
355 format_ref_id = get_format_ref_id(repo)
355 356
356 357 result = []
357 358 for title, refs, ref_type in refs_to_create:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -28,7 +28,16 b' def includeme(config):'
28 28
29 29 config.add_route(
30 30 name='search_repo',
31 pattern='/{repo_name:.*?[^/]}/_search', repo_route=True)
32
33 config.add_route(
34 name='search_repo_alt',
31 35 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
32 36
37 config.add_route(
38 name='search_repo_group',
39 pattern='/{repo_group_name:.*?[^/]}/_search',
40 repo_group_route=True)
41
33 42 # Scan module for configuration decorators.
34 43 config.scan('.views', ignore='.tests')
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -23,8 +23,9 b' import urllib'
23 23 from pyramid.view import view_config
24 24 from webhelpers.util import update_params
25 25
26 from rhodecode.apps._base import BaseAppView, RepoAppView
27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
27 from rhodecode.lib.auth import (
28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
28 29 from rhodecode.lib.helpers import Page
29 30 from rhodecode.lib.utils2 import safe_str
30 31 from rhodecode.lib.index import searcher_from_config
@@ -34,22 +35,25 b' from rhodecode.model.validation_schema.s'
34 35 log = logging.getLogger(__name__)
35 36
36 37
37 def search(request, tmpl_context, repo_name):
38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
38 39 searcher = searcher_from_config(request.registry.settings)
39 40 formatted_results = []
40 41 execution_time = ''
41 42
42 43 schema = search_schema.SearchParamsSchema()
43
44 search_tags = []
44 45 search_params = {}
45 46 errors = []
46 47 try:
47 48 search_params = schema.deserialize(
48 dict(search_query=request.GET.get('q'),
49 dict(
50 search_query=request.GET.get('q'),
49 51 search_type=request.GET.get('type'),
50 52 search_sort=request.GET.get('sort'),
53 search_max_lines=request.GET.get('max_lines'),
51 54 page_limit=request.GET.get('page_limit'),
52 requested_page=request.GET.get('page'))
55 requested_page=request.GET.get('page'),
56 )
53 57 )
54 58 except validation_schema.Invalid as e:
55 59 errors = e.children
@@ -57,20 +61,22 b' def search(request, tmpl_context, repo_n'
57 61 def url_generator(**kw):
58 62 q = urllib.quote(safe_str(search_query))
59 63 return update_params(
60 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
64 "?q=%s&type=%s&max_lines=%s" % (
65 q, safe_str(search_type), search_max_lines), **kw)
61 66
62 67 c = tmpl_context
63 68 search_query = search_params.get('search_query')
64 69 search_type = search_params.get('search_type')
65 70 search_sort = search_params.get('search_sort')
71 search_max_lines = search_params.get('search_max_lines')
66 72 if search_params.get('search_query'):
67 73 page_limit = search_params['page_limit']
68 74 requested_page = search_params['requested_page']
69 75
70 76 try:
71 77 search_result = searcher.search(
72 search_query, search_type, c.auth_user, repo_name,
73 requested_page, page_limit, search_sort)
78 search_query, search_type, c.auth_user, repo_name, repo_group_name,
79 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
74 80
75 81 formatted_results = Page(
76 82 search_result['results'], page=requested_page,
@@ -79,6 +85,8 b' def search(request, tmpl_context, repo_n'
79 85 finally:
80 86 searcher.cleanup()
81 87
88 search_tags = searcher.extract_search_tags(search_query)
89
82 90 if not search_result['error']:
83 91 execution_time = '%s results (%.3f seconds)' % (
84 92 search_result['count'],
@@ -90,6 +98,7 b' def search(request, tmpl_context, repo_n'
90 98
91 99 c.perm_user = c.auth_user
92 100 c.repo_name = repo_name
101 c.repo_group_name = repo_group_name
93 102 c.sort = search_sort
94 103 c.url_generator = url_generator
95 104 c.errors = errors
@@ -98,12 +107,12 b' def search(request, tmpl_context, repo_n'
98 107 c.cur_query = search_query
99 108 c.search_type = search_type
100 109 c.searcher = searcher
110 c.search_tags = search_tags
101 111
102 112
103 113 class SearchView(BaseAppView):
104 114 def load_default_context(self):
105 115 c = self._get_local_tmpl_context()
106
107 116 return c
108 117
109 118 @LoginRequired()
@@ -112,14 +121,14 b' class SearchView(BaseAppView):'
112 121 renderer='rhodecode:templates/search/search.mako')
113 122 def search(self):
114 123 c = self.load_default_context()
115 search(self.request, c, repo_name=None)
124 perform_search(self.request, c)
116 125 return self._get_template_context(c)
117 126
118 127
119 128 class SearchRepoView(RepoAppView):
120 129 def load_default_context(self):
121 130 c = self._get_local_tmpl_context()
122
131 c.active = 'search'
123 132 return c
124 133
125 134 @LoginRequired()
@@ -128,7 +137,28 b' class SearchRepoView(RepoAppView):'
128 137 @view_config(
129 138 route_name='search_repo', request_method='GET',
130 139 renderer='rhodecode:templates/search/search.mako')
140 @view_config(
141 route_name='search_repo_alt', request_method='GET',
142 renderer='rhodecode:templates/search/search.mako')
131 143 def search_repo(self):
132 144 c = self.load_default_context()
133 search(self.request, c, repo_name=self.db_repo_name)
145 perform_search(self.request, c, repo_name=self.db_repo_name)
134 146 return self._get_template_context(c)
147
148
149 class SearchRepoGroupView(RepoGroupAppView):
150 def load_default_context(self):
151 c = self._get_local_tmpl_context()
152 c.active = 'search'
153 return c
154
155 @LoginRequired()
156 @HasRepoGroupPermissionAnyDecorator(
157 'group.read', 'group.write', 'group.admin')
158 @view_config(
159 route_name='search_repo_group', request_method='GET',
160 renderer='rhodecode:templates/search/search.mako')
161 def search_repo_group(self):
162 c = self.load_default_context()
163 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
164 return self._get_template_context(c)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -36,6 +36,7 b' def _sanitize_settings_and_apply_default'
36 36 _bool_setting(settings, config_keys.generate_authorized_keyfile, 'false')
37 37 _bool_setting(settings, config_keys.wrapper_allow_shell, 'false')
38 38 _bool_setting(settings, config_keys.enable_debug_logging, 'false')
39 _bool_setting(settings, config_keys.ssh_key_generator_enabled, 'true')
39 40
40 41 _string_setting(settings, config_keys.authorized_keys_file_path,
41 42 '~/.ssh/authorized_keys_rhodecode',
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -24,6 +24,7 b''
24 24 generate_authorized_keyfile = 'ssh.generate_authorized_keyfile'
25 25 authorized_keys_file_path = 'ssh.authorized_keys_file_path'
26 26 authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
27 ssh_key_generator_enabled = 'ssh.enable_ui_key_generator'
27 28 wrapper_cmd = 'ssh.wrapper_cmd'
28 29 wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
29 30 enable_debug_logging = 'ssh.enable_debug_logging'
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -17,10 +17,10 b''
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20 import os
21 21 import logging
22 import os
23 22 import shlex
23 from pyramid import compat
24 24
25 25 # Do not use `from rhodecode import events` here, it will be overridden by the
26 26 # events module in this package due to pythons import mechanism.
@@ -85,6 +85,6 b' def _append_path_sep(path):'
85 85 """
86 86 Append the path separator if missing.
87 87 """
88 if isinstance(path, basestring) and not path.endswith(os.path.sep):
88 if isinstance(path, compat.string_types) and not path.endswith(os.path.sep):
89 89 path += os.path.sep
90 90 return path
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -24,12 +24,15 b' RhodeCode authentication plugin for buil'
24 24
25 25 import logging
26 26
27 from rhodecode.translation import _
27 import colander
28 28
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 from rhodecode.translation import _
31 30 from rhodecode.lib.utils2 import safe_str
32 31 from rhodecode.model.db import User
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.base import (
34 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 36
34 37 log = logging.getLogger(__name__)
35 38
@@ -45,6 +48,11 b' class RhodecodeAuthnResource(AuthnPlugin'
45 48
46 49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 50 uid = 'rhodecode'
51 AUTH_RESTRICTION_NONE = 'user_all'
52 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
53 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
54 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
55 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
48 56
49 57 def includeme(self, config):
50 58 config.add_authn_plugin(self)
@@ -64,6 +72,9 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
64 72 route_name='auth_home',
65 73 context=RhodecodeAuthnResource)
66 74
75 def get_settings_schema(self):
76 return RhodeCodeSettingsSchema()
77
67 78 def get_display_name(self):
68 79 return _('RhodeCode Internal')
69 80
@@ -94,12 +105,36 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
94 105 if not userobj:
95 106 log.debug('userobj was:%s skipping', userobj)
96 107 return None
108
97 109 if userobj.extern_type != self.name:
98 log.warning(
99 "userobj:%s extern_type mismatch got:`%s` expected:`%s`",
110 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
100 111 userobj, userobj.extern_type, self.name)
101 112 return None
102 113
114 # check scope of auth
115 scope_restriction = settings.get('scope_restriction', '')
116
117 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
118 and self.auth_type != HTTP_TYPE:
119 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
120 userobj, self.auth_type, scope_restriction)
121 return None
122
123 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
124 and self.auth_type != VCS_TYPE:
125 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
126 userobj, self.auth_type, scope_restriction)
127 return None
128
129 # check super-admin restriction
130 auth_restriction = settings.get('auth_restriction', '')
131
132 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
133 and userobj.admin is False:
134 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
135 userobj, auth_restriction)
136 return None
137
103 138 user_attrs = {
104 139 "username": userobj.username,
105 140 "firstname": userobj.firstname,
@@ -131,23 +166,55 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
131 166 user_attrs['_hash_migrate'] = new_hash
132 167
133 168 if userobj.username == User.DEFAULT_USER and userobj.active:
134 log.info(
135 'user `%s` authenticated correctly as anonymous user', userobj.username)
169 log.info('user `%s` authenticated correctly as anonymous user',
170 userobj.username)
136 171 return user_attrs
137 172
138 173 elif userobj.username == username and password_match:
139 174 log.info('user `%s` authenticated correctly', userobj.username)
140 175 return user_attrs
141 log.warn("user `%s` used a wrong password when "
176 log.warning("user `%s` used a wrong password when "
142 177 "authenticating on this plugin", userobj.username)
143 178 return None
144 179 else:
145 log.warning(
146 'user `%s` failed to authenticate via %s, reason: account not '
180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
147 181 'active.', username, self.name)
148 182 return None
149 183
150 184
185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
186
187 auth_restriction_choices = [
188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
189 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
190 ]
191
192 auth_scope_choices = [
193 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
194 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
195 ]
196
197 auth_restriction = colander.SchemaNode(
198 colander.String(),
199 default=auth_restriction_choices[0],
200 description=_('Allowed user types for authentication using this plugin.'),
201 title=_('User restriction'),
202 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
203 widget='select_with_labels',
204 choices=auth_restriction_choices
205 )
206 scope_restriction = colander.SchemaNode(
207 colander.String(),
208 default=auth_scope_choices[0],
209 description=_('Allowed protocols for authentication using this plugin. '
210 'VCS means GIT/HG/SVN. HTTP is web based login.'),
211 title=_('Scope restriction'),
212 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
213 widget='select_with_labels',
214 choices=auth_scope_choices
215 )
216
217
151 218 def includeme(config):
152 219 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
153 220 plugin_factory(plugin_id).includeme(config)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -23,7 +23,9 b' RhodeCode authentication token plugin fo'
23 23 """
24 24
25 25 import logging
26 import colander
26 27
28 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
27 29 from rhodecode.translation import _
28 30 from rhodecode.authentication.base import (
29 31 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
@@ -48,6 +50,7 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
48 50 Enables usage of authentication tokens for vcs operations.
49 51 """
50 52 uid = 'token'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
51 54
52 55 def includeme(self, config):
53 56 config.add_authn_plugin(self)
@@ -67,6 +70,9 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
67 70 route_name='auth_home',
68 71 context=RhodecodeAuthnResource)
69 72
73 def get_settings_schema(self):
74 return RhodeCodeSettingsSchema()
75
70 76 def get_display_name(self):
71 77 return _('Rhodecode Token')
72 78
@@ -142,12 +148,10 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
142 148 'user `%s` successfully authenticated via %s',
143 149 user_attrs['username'], self.name)
144 150 return user_attrs
145 log.warn(
146 'user `%s` failed to authenticate via %s, reason: bad or '
151 log.warning('user `%s` failed to authenticate via %s, reason: bad or '
147 152 'inactive token.', username, self.name)
148 153 else:
149 log.warning(
150 'user `%s` failed to authenticate via %s, reason: account not '
154 log.warning('user `%s` failed to authenticate via %s, reason: account not '
151 155 'active.', username, self.name)
152 156 return None
153 157
@@ -155,3 +159,19 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
155 159 def includeme(config):
156 160 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
157 161 plugin_factory(plugin_id).includeme(config)
162
163
164 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
165 auth_scope_choices = [
166 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS, 'VCS only'),
167 ]
168
169 scope_restriction = colander.SchemaNode(
170 colander.String(),
171 default=auth_scope_choices[0],
172 description=_('Choose operation scope restriction when authenticating.'),
173 title=_('Scope restriction'),
174 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
175 widget='select_with_labels',
176 choices=auth_scope_choices
177 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -98,7 +98,7 b' def make_pyramid_app(global_config, **se'
98 98 global_config = _substitute_values(global_config, environ)
99 99 settings = _substitute_values(settings, environ)
100 100
101 sanitize_settings_and_apply_defaults(settings)
101 sanitize_settings_and_apply_defaults(global_config, settings)
102 102
103 103 config = Configurator(settings=settings)
104 104
@@ -165,7 +165,7 b' def error_handler(exception, request):'
165 165
166 166 error_explanation = base_response.explanation or str(base_response)
167 167 if base_response.status_code == 404:
168 error_explanation += " Or you don't have permission to access it."
168 error_explanation += " Optionally you don't have permission to access this page."
169 169 c = AttributeDict()
170 170 c.error_message = base_response.status
171 171 c.error_explanation = error_explanation
@@ -281,6 +281,7 b' def includeme(config):'
281 281 config.include('rhodecode.apps.ops')
282 282 config.include('rhodecode.apps.admin')
283 283 config.include('rhodecode.apps.channelstream')
284 config.include('rhodecode.apps.file_store')
284 285 config.include('rhodecode.apps.login')
285 286 config.include('rhodecode.apps.home')
286 287 config.include('rhodecode.apps.journal')
@@ -381,7 +382,7 b' def wrap_app_in_wsgi_middlewares(pyramid'
381 382 return pyramid_app_with_cleanup
382 383
383 384
384 def sanitize_settings_and_apply_defaults(settings):
385 def sanitize_settings_and_apply_defaults(global_config, settings):
385 386 """
386 387 Applies settings defaults and does all type conversion.
387 388
@@ -420,6 +421,7 b' def sanitize_settings_and_apply_defaults'
420 421 # TODO: johbo: Re-think this, usually the call to config.include
421 422 # should allow to pass in a prefix.
422 423 settings.setdefault('rhodecode.api.url', '/_admin/api')
424 settings.setdefault('__file__', global_config.get('__file__'))
423 425
424 426 # Sanitize generic settings.
425 427 _list_setting(settings, 'default_encoding', 'UTF-8')
@@ -708,18 +710,29 b' def _string_setting(settings, name, defa'
708 710
709 711
710 712 def _substitute_values(mapping, substitutions):
713 result = {}
711 714
712 715 try:
713 result = {
716 for key, value in mapping.items():
717 # initialize without substitution first
718 result[key] = value
719
714 720 # Note: Cannot use regular replacements, since they would clash
715 721 # with the implementation of ConfigParser. Using "format" instead.
716 key: value.format(**substitutions)
717 for key, value in mapping.items()
718 }
722 try:
723 result[key] = value.format(**substitutions)
719 724 except KeyError as e:
720 raise ValueError(
721 'Failed to substitute env variable: {}. '
722 'Make sure you have specified this env variable without ENV_ prefix'.format(e))
725 env_var = '{}'.format(e.args[0])
726
727 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
728 'Make sure your environment has {var} set, or remove this ' \
729 'variable from config file'.format(key=key, var=env_var)
730
731 if env_var.startswith('ENV_'):
732 raise ValueError(msg)
733 else:
734 log.warning(msg)
735
723 736 except ValueError as e:
724 737 log.warning('Failed to substitute ENV variable: %s', e)
725 738 result = mapping
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -14,19 +14,6 b''
14 14 'hook_type': '',
15 15 'user_agent': 'Client user agent, e.g git or mercurial CLI version',
16 16 })
17 @has_kwargs({
18 'server_url': 'url of instance that triggered this hook',
19 'config': 'path to .ini config used',
20 'scm': 'type of version control "git", "hg", "svn"',
21 'username': 'username of actor who triggered this event',
22 'ip': 'ip address of actor who triggered this hook',
23 'action': '',
24 'repository': 'repository name',
25 'repo_store_path': 'full path to where repositories are stored',
26 'commit_ids': 'pre transaction metadata for commit ids',
27 'hook_type': '',
28 'user_agent': 'Client user agent, e.g git or mercurial CLI version',
29 })
30 17 def _pre_push_hook(*args, **kwargs):
31 18 """
32 19 Post push hook
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,5 +1,5 b''
1 1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2016-2018 RhodeCode GmbH
2 # Copyright (C) 2016-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,5 +1,5 b''
1 1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2016-2018 RhodeCode GmbH
2 # Copyright (C) 2016-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,5 +1,5 b''
1 1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2016-2018 RhodeCode GmbH
2 # Copyright (C) 2016-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,5 +1,5 b''
1 1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2016-2018 RhodeCode GmbH
2 # Copyright (C) 2016-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -7457,7 +7457,7 b' msgid ""'
7457 7457 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7458 7458 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7459 7459 " {user} current user username,\n"
7460 " {sys_user} current system user running this process, usefull for ssh,\n"
7460 " {sys_user} current system user running this process, Useful for ssh,\n"
7461 7461 " {hostname} hostname of this server running RhodeCode,\n"
7462 7462 " {netloc} network location/server host of running RhodeCode server,\n"
7463 7463 " {repo} full repository name,\n"
@@ -7455,7 +7455,7 b' msgid ""'
7455 7455 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7456 7456 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7457 7457 " {user} current user username,\n"
7458 " {sys_user} current system user running this process, usefull for ssh,\n"
7458 " {sys_user} current system user running this process, Useful for ssh,\n"
7459 7459 " {hostname} hostname of this server running RhodeCode,\n"
7460 7460 " {netloc} network location/server host of running RhodeCode server,\n"
7461 7461 " {repo} full repository name,\n"
@@ -7456,7 +7456,7 b' msgid ""'
7456 7456 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7457 7457 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7458 7458 " {user} current user username,\n"
7459 " {sys_user} current system user running this process, usefull for ssh,\n"
7459 " {sys_user} current system user running this process, Useful for ssh,\n"
7460 7460 " {hostname} hostname of this server running RhodeCode,\n"
7461 7461 " {netloc} network location/server host of running RhodeCode server,\n"
7462 7462 " {repo} full repository name,\n"
@@ -7456,7 +7456,7 b' msgid ""'
7456 7456 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7457 7457 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7458 7458 " {user} current user username,\n"
7459 " {sys_user} current system user running this process, usefull for ssh,\n"
7459 " {sys_user} current system user running this process, Useful for ssh,\n"
7460 7460 " {hostname} hostname of this server running RhodeCode,\n"
7461 7461 " {netloc} network location/server host of running RhodeCode server,\n"
7462 7462 " {repo} full repository name,\n"
@@ -7463,7 +7463,7 b' msgid ""'
7463 7463 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7464 7464 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7465 7465 " {user} current user username,\n"
7466 " {sys_user} current system user running this process, usefull for ssh,\n"
7466 " {sys_user} current system user running this process, Useful for ssh,\n"
7467 7467 " {hostname} hostname of this server running RhodeCode,\n"
7468 7468 " {netloc} network location/server host of running RhodeCode server,\n"
7469 7469 " {repo} full repository name,\n"
@@ -7461,7 +7461,7 b' msgid ""'
7461 7461 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7462 7462 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7463 7463 " {user} current user username,\n"
7464 " {sys_user} current system user running this process, usefull for ssh,\n"
7464 " {sys_user} current system user running this process, Useful for ssh,\n"
7465 7465 " {hostname} hostname of this server running RhodeCode,\n"
7466 7466 " {netloc} network location/server host of running RhodeCode server,\n"
7467 7467 " {repo} full repository name,\n"
@@ -7457,7 +7457,7 b' msgid ""'
7457 7457 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7458 7458 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7459 7459 " {user} current user username,\n"
7460 " {sys_user} current system user running this process, usefull for ssh,\n"
7460 " {sys_user} current system user running this process, Useful for ssh,\n"
7461 7461 " {hostname} hostname of this server running RhodeCode,\n"
7462 7462 " {netloc} network location/server host of running RhodeCode server,\n"
7463 7463 " {repo} full repository name,\n"
@@ -7497,7 +7497,7 b' msgid ""'
7497 7497 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7498 7498 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7499 7499 " {user} current user username,\n"
7500 " {sys_user} current system user running this process, usefull for ssh,\n"
7500 " {sys_user} current system user running this process, Useful for ssh,\n"
7501 7501 " {hostname} hostname of this server running RhodeCode,\n"
7502 7502 " {netloc} network location/server host of running RhodeCode server,\n"
7503 7503 " {repo} full repository name,\n"
@@ -7474,7 +7474,7 b' msgid ""'
7474 7474 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7475 7475 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7476 7476 " {user} current user username,\n"
7477 " {sys_user} current system user running this process, usefull for ssh,\n"
7477 " {sys_user} current system user running this process, Useful for ssh,\n"
7478 7478 " {hostname} hostname of this server running RhodeCode,\n"
7479 7479 " {netloc} network location/server host of running RhodeCode server,\n"
7480 7480 " {repo} full repository name,\n"
@@ -7458,7 +7458,7 b' msgid ""'
7458 7458 "Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:\n"
7459 7459 " {scheme} 'http' or 'https' sent from running RhodeCode server,\n"
7460 7460 " {user} current user username,\n"
7461 " {sys_user} current system user running this process, usefull for ssh,\n"
7461 " {sys_user} current system user running this process, Useful for ssh,\n"
7462 7462 " {hostname} hostname of this server running RhodeCode,\n"
7463 7463 " {netloc} network location/server host of running RhodeCode server,\n"
7464 7464 " {repo} full repository name,\n"
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,5 +1,5 b''
1 1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2012-2018 RhodeCode GmbH
2 # Copyright (C) 2012-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -23,17 +23,27 b' import string'
23 23 import collections
24 24 import logging
25 25 import requests
26 import urllib
26 27 from requests.adapters import HTTPAdapter
27 28 from requests.packages.urllib3.util.retry import Retry
28 29
29 30 from mako import exceptions
30 31
32 from rhodecode.lib.utils2 import safe_str
31 33 from rhodecode.translation import _
32 34
33 35
34 36 log = logging.getLogger(__name__)
35 37
36 38
39 class UrlTmpl(string.Template):
40
41 def safe_substitute(self, **kws):
42 # url encode the kw for usage in url
43 kws = {k: urllib.quote(safe_str(v)) for k, v in kws.items()}
44 return super(UrlTmpl, self).safe_substitute(**kws)
45
46
37 47 class IntegrationTypeBase(object):
38 48 """ Base class for IntegrationType plugins """
39 49 is_dummy = False
@@ -217,7 +227,9 b' class WebhookDataHandler(CommitParsingDa'
217 227 common_vars.update(extra_vars)
218 228
219 229 template_url = self.template_url.replace('${extra:', '${extra__')
220 return string.Template(template_url).safe_substitute(**common_vars)
230 for k, v in common_vars.items():
231 template_url = UrlTmpl(template_url).safe_substitute(**{k: v})
232 return template_url
221 233
222 234 def repo_push_event_handler(self, event, data):
223 235 url = self.get_base_parsed_template(data)
@@ -228,20 +240,18 b' class WebhookDataHandler(CommitParsingDa'
228 240 if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
229 241 # call it multiple times, for each branch if used in variables
230 242 for branch, commit_ids in branches_commits.items():
231 branch_url = string.Template(url).safe_substitute(branch=branch)
243 branch_url = UrlTmpl(url).safe_substitute(branch=branch)
232 244
233 245 if '${branch_head}' in branch_url:
234 246 # last commit in the aggregate is the head of the branch
235 247 branch_head = commit_ids['branch_head']
236 branch_url = string.Template(branch_url).safe_substitute(
237 branch_head=branch_head)
248 branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head)
238 249
239 250 # call further down for each commit if used
240 251 if '${commit_id}' in branch_url:
241 252 for commit_data in commit_ids['commits']:
242 253 commit_id = commit_data['raw_id']
243 commit_url = string.Template(branch_url).safe_substitute(
244 commit_id=commit_id)
254 commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id)
245 255 # register per-commit call
246 256 log.debug(
247 257 'register %s call(%s) to url %s',
@@ -251,36 +261,34 b' class WebhookDataHandler(CommitParsingDa'
251 261
252 262 else:
253 263 # register per-branch call
254 log.debug(
255 'register %s call(%s) to url %s',
264 log.debug('register %s call(%s) to url %s',
256 265 self.name, event, branch_url)
257 url_calls.append(
258 (branch_url, self.headers, data))
266 url_calls.append((branch_url, self.headers, data))
259 267
260 268 else:
261 log.debug(
262 'register %s call(%s) to url %s', self.name, event, url)
269 log.debug('register %s call(%s) to url %s', self.name, event, url)
263 270 url_calls.append((url, self.headers, data))
264 271
265 272 return url_calls
266 273
267 274 def repo_create_event_handler(self, event, data):
268 275 url = self.get_base_parsed_template(data)
269 log.debug(
270 'register %s call(%s) to url %s', self.name, event, url)
276 log.debug('register %s call(%s) to url %s', self.name, event, url)
271 277 return [(url, self.headers, data)]
272 278
273 279 def pull_request_event_handler(self, event, data):
274 280 url = self.get_base_parsed_template(data)
275 log.debug(
276 'register %s call(%s) to url %s', self.name, event, url)
277 url = string.Template(url).safe_substitute(
278 pull_request_id=data['pullrequest']['pull_request_id'],
279 pull_request_title=data['pullrequest']['title'],
280 pull_request_url=data['pullrequest']['url'],
281 pull_request_shadow_url=data['pullrequest']['shadow_url'],
282 pull_request_commits_uid=data['pullrequest']['commits_uid'],
283 )
281 log.debug('register %s call(%s) to url %s', self.name, event, url)
282 pr_vars = [
283 ('pull_request_id', data['pullrequest']['pull_request_id']),
284 ('pull_request_title', data['pullrequest']['title']),
285 ('pull_request_url', data['pullrequest']['url']),
286 ('pull_request_shadow_url', data['pullrequest']['shadow_url']),
287 ('pull_request_commits_uid', data['pullrequest']['commits_uid']),
288 ]
289 for k, v in pr_vars:
290 url = UrlTmpl(url).safe_substitute(**{k: v})
291
284 292 return [(url, self.headers, data)]
285 293
286 294 def __call__(self, event, data):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -122,8 +122,10 b' class HipchatIntegrationType(Integration'
122 122 log.debug('event not valid: %r', event)
123 123 return
124 124
125 if event.name not in self.settings['events']:
126 log.debug('event ignored: %r', event)
125 allowed_events = self.settings['events']
126 if event.name not in allowed_events:
127 log.debug('event ignored: %r event %s not in allowed events %s',
128 event, event.name, allowed_events)
127 129 return
128 130
129 131 data = event.as_dict()
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -138,8 +138,10 b' class SlackIntegrationType(IntegrationTy'
138 138 log.debug('event not valid: %r', event)
139 139 return
140 140
141 if event.name not in self.settings['events']:
142 log.debug('event ignored: %r', event)
141 allowed_events = self.settings['events']
142 if event.name not in allowed_events:
143 log.debug('event ignored: %r event %s not in allowed events %s',
144 event, event.name, allowed_events)
143 145 return
144 146
145 147 data = event.as_dict()
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -118,12 +118,11 b' class WebhookSettingsSchema(colander.Sch'
118 118 method_type = colander.SchemaNode(
119 119 colander.String(),
120 120 title=_('Call Method'),
121 description=_('Select if the Webhook call should be made '
122 'with POST or GET.'),
121 description=_('Select a HTTP method to use when calling the Webhook.'),
123 122 default='post',
124 123 missing='',
125 124 widget=deform.widget.RadioChoiceWidget(
126 values=[('get', 'GET'), ('post', 'POST')],
125 values=[('get', 'GET'), ('post', 'POST'), ('put', 'PUT')],
127 126 inline=True
128 127 ),
129 128 )
@@ -171,8 +170,10 b' class WebhookIntegrationType(Integration'
171 170 log.debug('event not valid: %r', event)
172 171 return
173 172
174 if event.name not in self.settings['events']:
175 log.debug('event ignored: %r', event)
173 allowed_events = self.settings['events']
174 if event.name not in allowed_events:
175 log.debug('event ignored: %r event %s not in allowed events %s',
176 event, event.name, allowed_events)
176 177 return
177 178
178 179 data = event.as_dict()
@@ -187,8 +188,7 b' class WebhookIntegrationType(Integration'
187 188 handler = WebhookDataHandler(template_url, headers)
188 189
189 190 url_calls = handler(event, data)
190 log.debug('webhook: calling following urls: %s',
191 [x[0] for x in url_calls])
191 log.debug('Webhook: calling following urls: %s', [x[0] for x in url_calls])
192 192
193 193 run_task(post_to_webhook, url_calls, self.settings)
194 194
@@ -214,7 +214,9 b' def post_to_webhook(url_calls, settings)'
214 214 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
215 215 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
216 216 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
217 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
217 'refs': {'bookmarks': [],
218 'branches': [u'default'],
219 'tags': [u'tip']},
218 220 'reviewers': [],
219 221 'revision': 9L,
220 222 'short_id': 'a815cc738b96',
@@ -228,11 +230,11 b' def post_to_webhook(url_calls, settings)'
228 230 'url': u'http://rc.local:8080/hg-repo'},
229 231 'server_url': u'http://rc.local:8080',
230 232 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
233 }
234 """
231 235
232 """
233 236 call_headers = {
234 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
235 rhodecode.__version__)
237 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(rhodecode.__version__)
236 238 } # updated below with custom ones, allows override
237 239
238 240 auth = get_auth(settings)
@@ -247,8 +249,7 b' def post_to_webhook(url_calls, settings)'
247 249 headers = headers or {}
248 250 call_headers.update(headers)
249 251
250 log.debug('calling Webhook with method: %s, and auth:%s',
251 call_method, auth)
252 log.debug('calling Webhook with method: %s, and auth:%s', call_method, auth)
252 253 if settings.get('log_data'):
253 254 log.debug('calling webhook with data: %s', data)
254 255 resp = call_method(url, json={
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -35,6 +35,7 b' from paste.httpexceptions import HTTPUna'
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 from rhodecode.apps._base import TemplateArgs
38 39 from rhodecode.authentication.base import VCS_TYPE
39 40 from rhodecode.lib import auth, utils2
40 41 from rhodecode.lib import helpers as h
@@ -43,7 +44,7 b' from rhodecode.lib.exceptions import Use'
43 44 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
44 45 from rhodecode.lib.utils2 import (
45 46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
46 from rhodecode.model.db import Repository, User, ChangesetComment
47 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
47 48 from rhodecode.model.notification import NotificationModel
48 49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
49 50
@@ -281,7 +282,7 b' def get_current_lang(request):'
281 282 return getattr(request, '_LOCALE_', request.locale_name)
282 283
283 284
284 def attach_context_attributes(context, request, user_id):
285 def attach_context_attributes(context, request, user_id=None):
285 286 """
286 287 Attach variables into template context called `c`.
287 288 """
@@ -312,6 +313,10 b' def attach_context_attributes(context, r'
312 313 rc_config.get('rhodecode_dashboard_items', 100))
313 314 context.visual.admin_grid_items = safe_int(
314 315 rc_config.get('rhodecode_admin_grid_items', 100))
316 context.visual.show_revision_number = str2bool(
317 rc_config.get('rhodecode_show_revision_number', True))
318 context.visual.show_sha_length = safe_int(
319 rc_config.get('rhodecode_show_sha_length', 100))
315 320 context.visual.repository_fields = str2bool(
316 321 rc_config.get('rhodecode_repository_fields'))
317 322 context.visual.show_version = str2bool(
@@ -343,6 +348,8 b' def attach_context_attributes(context, r'
343 348 config.get('labs_settings_active', 'false'))
344 349 context.ssh_enabled = str2bool(
345 350 config.get('ssh.generate_authorized_keyfile', 'false'))
351 context.ssh_key_generator_enabled = str2bool(
352 config.get('ssh.enable_ui_key_generator', 'true'))
346 353
347 354 context.visual.allow_repo_location_change = str2bool(
348 355 config.get('allow_repo_location_change', True))
@@ -417,7 +424,13 b' def attach_context_attributes(context, r'
417 424 context.csrf_token = auth.get_csrf_token(session=request.session)
418 425 context.backends = rhodecode.BACKENDS.keys()
419 426 context.backends.sort()
420 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
427 unread_count = 0
428 user_bookmark_list = []
429 if user_id:
430 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
431 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
432 context.unread_notifications = unread_count
433 context.bookmark_items = user_bookmark_list
421 434
422 435 # web case
423 436 if hasattr(request, 'user'):
@@ -551,7 +564,11 b' def bootstrap_request(**kwargs):'
551 564 from rhodecode.lib.partial_renderer import get_partial_renderer
552 565 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
553 566
554 _call_context = {}
567 _call_context = TemplateArgs()
568 _call_context.visual = TemplateArgs()
569 _call_context.visual.show_sha_length = 12
570 _call_context.visual.show_revision_number = True
571
555 572 @property
556 573 def call_context(self):
557 574 return self._call_context
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -40,7 +40,7 b' from pyramid.threadlocal import get_curr'
40 40 import rhodecode
41 41
42 42 from rhodecode.lib.auth import AuthUser
43 from rhodecode.lib.celerylib.utils import get_ini_config, parse_ini_vars
43 from rhodecode.lib.celerylib.utils import get_ini_config, parse_ini_vars, ping_db
44 44 from rhodecode.lib.ext_json import json
45 45 from rhodecode.lib.pyramid_utils import bootstrap, setup_logging, prepare_request
46 46 from rhodecode.lib.utils2 import str2bool
@@ -144,6 +144,11 b' def on_preload_parsed(options, **kwargs)'
144 144 rhodecode.CELERY_ENABLED = True
145 145
146 146
147 @signals.task_prerun.connect
148 def task_prerun_signal(task_id, task, args, **kwargs):
149 ping_db()
150
151
147 152 @signals.task_success.connect
148 153 def task_success_signal(result, **kwargs):
149 154 meta.Session.commit()
@@ -170,7 +175,7 b' def task_failure_signal('
170 175
171 176 # simulate sys.exc_info()
172 177 exc_info = (einfo.type, einfo.exception, einfo.tb)
173 store_exception(id(exc_info), exc_info, prefix='celery_rhodecode')
178 store_exception(id(exc_info), exc_info, prefix='rhodecode-celery')
174 179
175 180 closer = celery_app.conf['PYRAMID_CLOSER']
176 181 if closer:
@@ -227,8 +232,7 b' def maybe_prepare_env(req):'
227 232 environ.update({
228 233 'PATH_INFO': req.environ['PATH_INFO'],
229 234 'SCRIPT_NAME': req.environ['SCRIPT_NAME'],
230 'HTTP_HOST':
231 req.environ.get('HTTP_HOST', req.environ['SERVER_NAME']),
235 'HTTP_HOST':req.environ.get('HTTP_HOST', req.environ['SERVER_NAME']),
232 236 'SERVER_NAME': req.environ['SERVER_NAME'],
233 237 'SERVER_PORT': req.environ['SERVER_PORT'],
234 238 'wsgi.url_scheme': req.environ['wsgi.url_scheme'],
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -26,6 +26,7 b' by celery daemon'
26 26 import os
27 27 import time
28 28
29 from pyramid import compat
29 30 from pyramid_mailer.mailer import Mailer
30 31 from pyramid_mailer.message import Message
31 32
@@ -62,7 +63,7 b' def send_email(recipients, subject, body'
62 63 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
63 64
64 65 if recipients:
65 if isinstance(recipients, basestring):
66 if isinstance(recipients, compat.string_types):
66 67 recipients = recipients.split(',')
67 68 else:
68 69 # if recipients are not defined we send to email_config + all admins
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -22,6 +22,7 b' import os'
22 22 import json
23 23 import logging
24 24 import datetime
25 import time
25 26
26 27 from functools import partial
27 28
@@ -30,7 +31,6 b' from celery.result import AsyncResult'
30 31 import celery.loaders.base
31 32 import celery.schedules
32 33
33
34 34 log = logging.getLogger(__name__)
35 35
36 36
@@ -167,3 +167,21 b' def parse_ini_vars(ini_vars):'
167 167 key, value = pairs.split('=')
168 168 options[key] = value
169 169 return options
170
171
172 def ping_db():
173 from rhodecode.model import meta
174 from rhodecode.model.db import DbMigrateVersion
175 log.info('Testing DB connection...')
176
177 for test in range(10):
178 try:
179 scalar = DbMigrateVersion.query().scalar()
180 log.debug('DB PING %s@%s', scalar, scalar.version)
181 break
182 except Exception:
183 retry = 1
184 log.debug('DB not ready, next try in %ss', retry)
185 time.sleep(retry)
186 finally:
187 meta.Session.remove()
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -26,6 +26,7 b' from pygments import lex'
26 26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 27 from pygments.lexers.special import TextLexer, Token
28 28 from pygments.lexers import get_lexer_by_name
29 from pyramid import compat
29 30
30 31 from rhodecode.lib.helpers import (
31 32 get_lexer_for_filenode, html_escape, get_custom_lexer)
@@ -48,8 +49,9 b' def filenode_as_lines_tokens(filenode, l'
48 49 lexer = lexer or get_lexer_for_filenode(filenode)
49 50 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
50 51 lexer, filenode, org_lexer)
51 tokens = tokenize_string(filenode.content, lexer)
52 lines = split_token_stream(tokens)
52 content = filenode.content
53 tokens = tokenize_string(content, lexer)
54 lines = split_token_stream(tokens, content)
53 55 rv = list(lines)
54 56 return rv
55 57
@@ -73,7 +75,7 b' def tokenize_string(content, lexer):'
73 75 yield pygment_token_class(token_type), token_text
74 76
75 77
76 def split_token_stream(tokens):
78 def split_token_stream(tokens, content):
77 79 """
78 80 Take a list of (TokenType, text) tuples and split them by a string
79 81
@@ -82,18 +84,23 b' def split_token_stream(tokens):'
82 84 (TEXT, 'more'), (TEXT, 'text')]
83 85 """
84 86
85 buffer = []
87 token_buffer = []
86 88 for token_class, token_text in tokens:
87 89 parts = token_text.split('\n')
88 90 for part in parts[:-1]:
89 buffer.append((token_class, part))
90 yield buffer
91 buffer = []
91 token_buffer.append((token_class, part))
92 yield token_buffer
93 token_buffer = []
94
95 token_buffer.append((token_class, parts[-1]))
92 96
93 buffer.append((token_class, parts[-1]))
94
95 if buffer:
96 yield buffer
97 if token_buffer:
98 yield token_buffer
99 elif content:
100 # this is a special case, we have the content, but tokenization didn't produce
101 # any results. THis can happen if know file extensions like .css have some bogus
102 # unicode content without any newline characters
103 yield [(pygment_token_class(Token.Text), content)]
97 104
98 105
99 106 def filenode_as_annotated_lines_tokens(filenode):
@@ -695,7 +702,7 b' class DiffSet(object):'
695 702 filenode = None
696 703 filename = None
697 704
698 if isinstance(input_file, basestring):
705 if isinstance(input_file, compat.string_types):
699 706 filename = input_file
700 707 elif isinstance(input_file, FileNode):
701 708 filenode = input_file
@@ -720,7 +727,11 b' class DiffSet(object):'
720 727 if filenode not in self.highlighted_filenodes:
721 728 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
722 729 self.highlighted_filenodes[filenode] = tokenized_lines
730
731 try:
723 732 return self.highlighted_filenodes[filenode][line_number - 1]
733 except Exception:
734 return [('', u'rhodecode diff rendering error')]
724 735
725 736 def action_to_op(self, action):
726 737 return {
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -18,13 +18,15 b''
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 from pyramid import compat
22
21 23
22 24 def strip_whitespace(value):
23 25 """
24 26 Removes leading/trailing whitespace, newlines, and tabs from the value.
25 27 Implements the `colander.interface.Preparer` interface.
26 28 """
27 if isinstance(value, basestring):
29 if isinstance(value, compat.string_types):
28 30 return value.strip(' \t\n\r')
29 31 else:
30 32 return value
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -156,8 +156,11 b' class DbManage(object):'
156 156 'rhodecode/lib/dbmigrate')
157 157 db_uri = self.dburi
158 158
159 if version:
160 DbMigrateVersion.set_version(version)
161
159 162 try:
160 curr_version = version or api.db_version(db_uri, repository_path)
163 curr_version = api.db_version(db_uri, repository_path)
161 164 msg = ('Found current database db_uri under version '
162 165 'control with version {}'.format(curr_version))
163 166
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -9,6 +9,7 b' import sqlalchemy'
9 9
10 10 from sqlalchemy.schema import ForeignKeyConstraint
11 11 from sqlalchemy.schema import UniqueConstraint
12 from pyramid import compat
12 13
13 14 from rhodecode.lib.dbmigrate.migrate.exceptions import *
14 15 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07, SQLA_08
@@ -229,7 +230,7 b' class ColumnDelta(DictMixin, sqlalchemy.'
229 230 diffs = self.compare_1_column(*p, **kw)
230 231 else:
231 232 # Zero columns specified
232 if not len(p) or not isinstance(p[0], basestring):
233 if not len(p) or not isinstance(p[0], compat.string_types):
233 234 raise ValueError("First argument must be column name")
234 235 diffs = self.compare_parameters(*p, **kw)
235 236
@@ -338,7 +339,7 b' class ColumnDelta(DictMixin, sqlalchemy.'
338 339 """Extracts data from p and modifies diffs"""
339 340 p = list(p)
340 341 while len(p):
341 if isinstance(p[0], basestring):
342 if isinstance(p[0], compat.string_types):
342 343 k.setdefault('name', p.pop(0))
343 344 elif isinstance(p[0], sqlalchemy.types.TypeEngine):
344 345 k.setdefault('type', p.pop(0))
@@ -376,7 +377,7 b' class ColumnDelta(DictMixin, sqlalchemy.'
376 377 return getattr(self, '_table', None)
377 378
378 379 def _set_table(self, table):
379 if isinstance(table, basestring):
380 if isinstance(table, compat.string_types):
380 381 if self.alter_metadata:
381 382 if not self.meta:
382 383 raise ValueError("metadata must be specified for table"
@@ -593,7 +594,7 b' populated with defaults'
593 594 if isinstance(cons,(ForeignKeyConstraint,
594 595 UniqueConstraint)):
595 596 for col_name in cons.columns:
596 if not isinstance(col_name,basestring):
597 if not isinstance(col_name, compat.string_types):
597 598 col_name = col_name.name
598 599 if self.name==col_name:
599 600 to_drop.add(cons)
@@ -628,7 +629,7 b' populated with defaults'
628 629 if (getattr(self, name[:-5]) and not obj):
629 630 raise InvalidConstraintError("Column.create() accepts index_name,"
630 631 " primary_key_name and unique_name to generate constraints")
631 if not isinstance(obj, basestring) and obj is not None:
632 if not isinstance(obj, compat.string_types) and obj is not None:
632 633 raise InvalidConstraintError(
633 634 "%s argument for column must be constraint name" % name)
634 635
@@ -9,6 +9,7 b' from sqlalchemy import (Table, Column, M'
9 9 from sqlalchemy.sql import and_
10 10 from sqlalchemy import exc as sa_exceptions
11 11 from sqlalchemy.sql import bindparam
12 from pyramid import compat
12 13
13 14 from rhodecode.lib.dbmigrate.migrate import exceptions
14 15 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07
@@ -25,7 +26,7 b' class ControlledSchema(object):'
25 26 """A database under version control"""
26 27
27 28 def __init__(self, engine, repository):
28 if isinstance(repository, basestring):
29 if isinstance(repository, compat.string_types):
29 30 repository = Repository(repository)
30 31 self.engine = engine
31 32 self.repository = repository
@@ -134,7 +135,7 b' class ControlledSchema(object):'
134 135 """
135 136 # Confirm that the version # is valid: positive, integer,
136 137 # exists in repos
137 if isinstance(repository, basestring):
138 if isinstance(repository, compat.string_types):
138 139 repository = Repository(repository)
139 140 version = cls._validate_version(repository, version)
140 141 table = cls._create_table_version(engine, repository, version)
@@ -199,7 +200,7 b' class ControlledSchema(object):'
199 200 """
200 201 Compare the current model against the current database.
201 202 """
202 if isinstance(repository, basestring):
203 if isinstance(repository, compat.string_types):
203 204 repository = Repository(repository)
204 205 model = load_model(model)
205 206
@@ -212,7 +213,7 b' class ControlledSchema(object):'
212 213 """
213 214 Dump the current database as a Python model.
214 215 """
215 if isinstance(repository, basestring):
216 if isinstance(repository, compat.string_types):
216 217 repository = Repository(repository)
217 218
218 219 diff = schemadiff.getDiffOfModelAgainstDatabase(
@@ -7,6 +7,7 b' import logging'
7 7 import inspect
8 8 from StringIO import StringIO
9 9
10 from pyramid import compat
10 11 from rhodecode.lib.dbmigrate import migrate
11 12 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
12 13 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
@@ -51,7 +52,7 b' class PythonScript(base.BaseScript):'
51 52 :rtype: string
52 53 """
53 54
54 if isinstance(repository, basestring):
55 if isinstance(repository, compat.string_types):
55 56 # oh dear, an import cycle!
56 57 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
57 58 repository = Repository(repository)
@@ -11,6 +11,7 b' from sqlalchemy import create_engine'
11 11 from sqlalchemy.engine import Engine
12 12 from sqlalchemy.pool import StaticPool
13 13
14 from pyramid import compat
14 15 from rhodecode.lib.dbmigrate.migrate import exceptions
15 16 from rhodecode.lib.dbmigrate.migrate.versioning.util.keyedinstance import KeyedInstance
16 17 from rhodecode.lib.dbmigrate.migrate.versioning.util.importpath import import_path
@@ -18,6 +19,7 b' from rhodecode.lib.dbmigrate.migrate.ver'
18 19
19 20 log = logging.getLogger(__name__)
20 21
22
21 23 def load_model(dotted_name):
22 24 """Import module and use module-level variable".
23 25
@@ -26,7 +28,7 b' def load_model(dotted_name):'
26 28 .. versionchanged:: 0.5.4
27 29
28 30 """
29 if isinstance(dotted_name, basestring):
31 if isinstance(dotted_name, compat.string_types):
30 32 if ':' not in dotted_name:
31 33 # backwards compatibility
32 34 warnings.warn('model should be in form of module.model:User '
@@ -39,7 +41,7 b' def load_model(dotted_name):'
39 41
40 42 def asbool(obj):
41 43 """Do everything to use object as bool"""
42 if isinstance(obj, basestring):
44 if isinstance(obj, compat.string_types):
43 45 obj = obj.strip().lower()
44 46 if obj in ['true', 'yes', 'on', 'y', 't', '1']:
45 47 return True
@@ -112,7 +114,7 b' def construct_engine(engine, **opts):'
112 114 """
113 115 if isinstance(engine, Engine):
114 116 return engine
115 elif not isinstance(engine, basestring):
117 elif not isinstance(engine, compat.string_types):
116 118 raise ValueError("you need to pass either an existing engine or a database uri")
117 119
118 120 # get options for create_engine
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -28,6 +28,7 b' from sqlalchemy import *'
28 28 from sqlalchemy.ext.hybrid import hybrid_property
29 29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
30 30 from beaker.cache import cache_region, region_invalidate
31 from pyramid import compat
31 32
32 33 from rhodecode.lib.vcs import get_backend
33 34 from rhodecode.lib.vcs.utils.helpers import get_scm
@@ -413,7 +414,7 b' class UserGroup(Base, BaseModel):'
413 414 Session.flush()
414 415 members_list = []
415 416 if v:
416 v = [v] if isinstance(v, basestring) else v
417 v = [v] if isinstance(v, compat.string_types) else v
417 418 for u_id in set(v):
418 419 member = UserGroupMember(users_group_id, u_id)
419 420 members_list.append(member)
@@ -679,7 +680,7 b' class Repository(Base, BaseModel):'
679 680 class Group(Base, BaseModel):
680 681 __tablename__ = 'groups'
681 682 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
682 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
683 {'extend_existing':True},)
683 684 __mapper_args__ = {'order_by':'group_name'}
684 685
685 686 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -699,7 +699,6 b' class RepoGroup(Base, BaseModel):'
699 699 __tablename__ = 'groups'
700 700 __table_args__ = (
701 701 UniqueConstraint('group_name', 'group_parent_id'),
702 CheckConstraint('group_id != group_parent_id'),
703 702 {'extend_existing': True, 'mysql_engine':'InnoDB',
704 703 'mysql_charset': 'utf8'},
705 704 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -513,7 +513,6 b' class RepoGroup(Base, BaseModel):'
513 513 __tablename__ = 'groups'
514 514 __table_args__ = (
515 515 UniqueConstraint('group_name', 'group_parent_id'),
516 CheckConstraint('group_id != group_parent_id'),
517 516 {'extend_existing': True, 'mysql_engine': 'InnoDB',
518 517 'mysql_charset': 'utf8'},
519 518 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -526,7 +526,6 b' class RepoGroup(Base, BaseModel):'
526 526 __tablename__ = 'groups'
527 527 __table_args__ = (
528 528 UniqueConstraint('group_name', 'group_parent_id'),
529 CheckConstraint('group_id != group_parent_id'),
530 529 {'extend_existing': True, 'mysql_engine': 'InnoDB',
531 530 'mysql_charset': 'utf8'},
532 531 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -543,7 +543,6 b' class RepoGroup(Base, BaseModel):'
543 543 __tablename__ = 'groups'
544 544 __table_args__ = (
545 545 UniqueConstraint('group_name', 'group_parent_id'),
546 CheckConstraint('group_id != group_parent_id'),
547 546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
548 547 'mysql_charset': 'utf8'},
549 548 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -626,7 +626,6 b' class RepoGroup(Base, BaseModel):'
626 626 __tablename__ = 'groups'
627 627 __table_args__ = (
628 628 UniqueConstraint('group_name', 'group_parent_id'),
629 CheckConstraint('group_id != group_parent_id'),
630 629 {'extend_existing': True, 'mysql_engine': 'InnoDB',
631 630 'mysql_charset': 'utf8'},
632 631 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -597,7 +597,6 b' class RepoGroup(Base, BaseModel):'
597 597 __tablename__ = 'groups'
598 598 __table_args__ = (
599 599 UniqueConstraint('group_name', 'group_parent_id'),
600 CheckConstraint('group_id != group_parent_id'),
601 600 {'extend_existing': True, 'mysql_engine': 'InnoDB',
602 601 'mysql_charset': 'utf8'},
603 602 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -599,7 +599,6 b' class RepoGroup(Base, BaseModel):'
599 599 __tablename__ = 'groups'
600 600 __table_args__ = (
601 601 UniqueConstraint('group_name', 'group_parent_id'),
602 CheckConstraint('group_id != group_parent_id'),
603 602 {'extend_existing': True, 'mysql_engine': 'InnoDB',
604 603 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
605 604 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -622,7 +622,6 b' class RepoGroup(Base, BaseModel):'
622 622 __tablename__ = 'groups'
623 623 __table_args__ = (
624 624 UniqueConstraint('group_name', 'group_parent_id'),
625 CheckConstraint('group_id != group_parent_id'),
626 625 {'extend_existing': True, 'mysql_engine': 'InnoDB',
627 626 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
628 627 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -621,7 +621,6 b' class RepoGroup(Base, BaseModel):'
621 621 __tablename__ = 'groups'
622 622 __table_args__ = (
623 623 UniqueConstraint('group_name', 'group_parent_id'),
624 CheckConstraint('group_id != group_parent_id'),
625 624 {'extend_existing': True, 'mysql_engine': 'InnoDB',
626 625 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
627 626 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -639,7 +639,6 b' class RepoGroup(Base, BaseModel):'
639 639 __tablename__ = 'groups'
640 640 __table_args__ = (
641 641 UniqueConstraint('group_name', 'group_parent_id'),
642 CheckConstraint('group_id != group_parent_id'),
643 642 {'extend_existing': True, 'mysql_engine': 'InnoDB',
644 643 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
645 644 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -656,7 +656,6 b' class RepoGroup(Base, BaseModel):'
656 656 __tablename__ = 'groups'
657 657 __table_args__ = (
658 658 UniqueConstraint('group_name', 'group_parent_id'),
659 CheckConstraint('group_id != group_parent_id'),
660 659 {'extend_existing': True, 'mysql_engine': 'InnoDB',
661 660 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
662 661 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -672,7 +672,6 b' class RepoGroup(Base, BaseModel):'
672 672 __tablename__ = 'groups'
673 673 __table_args__ = (
674 674 UniqueConstraint('group_name', 'group_parent_id'),
675 CheckConstraint('group_id != group_parent_id'),
676 675 {'extend_existing': True, 'mysql_engine': 'InnoDB',
677 676 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
678 677 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -675,7 +675,6 b' class RepoGroup(Base, BaseModel):'
675 675 __tablename__ = 'groups'
676 676 __table_args__ = (
677 677 UniqueConstraint('group_name', 'group_parent_id'),
678 CheckConstraint('group_id != group_parent_id'),
679 678 {'extend_existing': True, 'mysql_engine': 'InnoDB',
680 679 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
681 680 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -679,7 +679,6 b' class RepoGroup(Base, BaseModel):'
679 679 __tablename__ = 'groups'
680 680 __table_args__ = (
681 681 UniqueConstraint('group_name', 'group_parent_id'),
682 CheckConstraint('group_id != group_parent_id'),
683 682 {'extend_existing': True, 'mysql_engine': 'InnoDB',
684 683 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
685 684 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -679,7 +679,6 b' class RepoGroup(Base, BaseModel):'
679 679 __tablename__ = 'groups'
680 680 __table_args__ = (
681 681 UniqueConstraint('group_name', 'group_parent_id'),
682 CheckConstraint('group_id != group_parent_id'),
683 682 {'extend_existing': True, 'mysql_engine': 'InnoDB',
684 683 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
685 684 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -707,7 +707,6 b' class RepoGroup(Base, BaseModel):'
707 707 __tablename__ = 'groups'
708 708 __table_args__ = (
709 709 UniqueConstraint('group_name', 'group_parent_id'),
710 CheckConstraint('group_id != group_parent_id'),
711 710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
712 711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
713 712 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -717,7 +717,6 b' class RepoGroup(Base, BaseModel):'
717 717 __tablename__ = 'groups'
718 718 __table_args__ = (
719 719 UniqueConstraint('group_name', 'group_parent_id'),
720 CheckConstraint('group_id != group_parent_id'),
721 720 {'extend_existing': True, 'mysql_engine': 'InnoDB',
722 721 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
723 722 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -760,7 +760,6 b' class RepoGroup(Base, BaseModel):'
760 760 __tablename__ = 'groups'
761 761 __table_args__ = (
762 762 UniqueConstraint('group_name', 'group_parent_id'),
763 CheckConstraint('group_id != group_parent_id'),
764 763 {'extend_existing': True, 'mysql_engine': 'InnoDB',
765 764 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
766 765 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -758,7 +758,6 b' class RepoGroup(Base, BaseModel):'
758 758 __tablename__ = 'groups'
759 759 __table_args__ = (
760 760 UniqueConstraint('group_name', 'group_parent_id'),
761 CheckConstraint('group_id != group_parent_id'),
762 761 {'extend_existing': True, 'mysql_engine': 'InnoDB',
763 762 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
764 763 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -759,7 +759,6 b' class RepoGroup(Base, BaseModel):'
759 759 __tablename__ = 'groups'
760 760 __table_args__ = (
761 761 UniqueConstraint('group_name', 'group_parent_id'),
762 CheckConstraint('group_id != group_parent_id'),
763 762 {'extend_existing': True, 'mysql_engine': 'InnoDB',
764 763 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
765 764 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -759,7 +759,6 b' class RepoGroup(Base, BaseModel):'
759 759 __tablename__ = 'groups'
760 760 __table_args__ = (
761 761 UniqueConstraint('group_name', 'group_parent_id'),
762 CheckConstraint('group_id != group_parent_id'),
763 762 {'extend_existing': True, 'mysql_engine': 'InnoDB',
764 763 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
765 764 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -765,7 +765,6 b' class RepoGroup(Base, BaseModel):'
765 765 __tablename__ = 'groups'
766 766 __table_args__ = (
767 767 UniqueConstraint('group_name', 'group_parent_id'),
768 CheckConstraint('group_id != group_parent_id'),
769 768 {'extend_existing': True, 'mysql_engine': 'InnoDB',
770 769 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
771 770 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -887,7 +887,6 b' class RepoGroup(Base, BaseModel):'
887 887 __tablename__ = 'groups'
888 888 __table_args__ = (
889 889 UniqueConstraint('group_name', 'group_parent_id'),
890 CheckConstraint('group_id != group_parent_id'),
891 890 {'extend_existing': True, 'mysql_engine': 'InnoDB',
892 891 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
893 892 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -912,7 +912,6 b' class RepoGroup(Base, BaseModel):'
912 912 __tablename__ = 'groups'
913 913 __table_args__ = (
914 914 UniqueConstraint('group_name', 'group_parent_id'),
915 CheckConstraint('group_id != group_parent_id'),
916 915 {'extend_existing': True, 'mysql_engine': 'InnoDB',
917 916 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
918 917 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -49,7 +49,7 b' from sqlalchemy.exc import IntegrityErro'
49 49 from sqlalchemy.dialects.mysql import LONGTEXT
50 50 from beaker.cache import cache_region
51 51 from zope.cachedescriptors.property import Lazy as LazyProperty
52
52 from pyramid import compat
53 53 from pyramid.threadlocal import get_current_request
54 54
55 55 from rhodecode.translation import _
@@ -2110,7 +2110,7 b' class Repository(Base, BaseModel):'
2110 2110 warnings.warn("Use get_commit", DeprecationWarning)
2111 2111 commit_id = None
2112 2112 commit_idx = None
2113 if isinstance(rev, basestring):
2113 if isinstance(rev, compat.string_types):
2114 2114 commit_id = rev
2115 2115 else:
2116 2116 commit_idx = rev
@@ -2295,7 +2295,6 b' class RepoGroup(Base, BaseModel):'
2295 2295 __tablename__ = 'groups'
2296 2296 __table_args__ = (
2297 2297 UniqueConstraint('group_name', 'group_parent_id'),
2298 CheckConstraint('group_id != group_parent_id'),
2299 2298 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2300 2299 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2301 2300 )
@@ -3726,7 +3725,7 b' class PullRequestReviewers(Base, BaseMod'
3726 3725 @reasons.setter
3727 3726 def reasons(self, val):
3728 3727 val = val or []
3729 if any(not isinstance(x, basestring) for x in val):
3728 if any(not isinstance(x, compat.string_types) for x in val):
3730 3729 raise Exception('invalid reasons type, must be list of strings')
3731 3730 self._reasons = val
3732 3731
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -49,7 +49,7 b' from sqlalchemy.exc import IntegrityErro'
49 49 from sqlalchemy.dialects.mysql import LONGTEXT
50 50 from beaker.cache import cache_region
51 51 from zope.cachedescriptors.property import Lazy as LazyProperty
52
52 from pyramid import compat
53 53 from pyramid.threadlocal import get_current_request
54 54
55 55 from rhodecode.translation import _
@@ -2176,7 +2176,7 b' class Repository(Base, BaseModel):'
2176 2176 warnings.warn("Use get_commit", DeprecationWarning)
2177 2177 commit_id = None
2178 2178 commit_idx = None
2179 if isinstance(rev, basestring):
2179 if isinstance(rev, compat.string_types):
2180 2180 commit_id = rev
2181 2181 else:
2182 2182 commit_idx = rev
@@ -2361,7 +2361,6 b' class RepoGroup(Base, BaseModel):'
2361 2361 __tablename__ = 'groups'
2362 2362 __table_args__ = (
2363 2363 UniqueConstraint('group_name', 'group_parent_id'),
2364 CheckConstraint('group_id != group_parent_id'),
2365 2364 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2366 2365 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2367 2366 )
@@ -3809,7 +3808,7 b' class PullRequestReviewers(Base, BaseMod'
3809 3808 @reasons.setter
3810 3809 def reasons(self, val):
3811 3810 val = val or []
3812 if any(not isinstance(x, basestring) for x in val):
3811 if any(not isinstance(x, compat.string_types) for x in val):
3813 3812 raise Exception('invalid reasons type, must be list of strings')
3814 3813 self._reasons = val
3815 3814
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,7 +34,6 b' import functools'
34 34 import traceback
35 35 import collections
36 36
37
38 37 from sqlalchemy import *
39 38 from sqlalchemy.exc import IntegrityError
40 39 from sqlalchemy.ext.declarative import declared_attr
@@ -45,6 +44,7 b' from sqlalchemy.sql.expression import tr'
45 44 from beaker.cache import cache_region, region_invalidate
46 45 from webob.exc import HTTPNotFound
47 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 from pyramid import compat
48 48
49 49 # replace pylons with fake url for migration
50 50 from rhodecode.lib.dbmigrate.schema import url
@@ -1811,7 +1811,7 b' class Repository(Base, BaseModel):'
1811 1811 warnings.warn("Use get_commit", DeprecationWarning)
1812 1812 commit_id = None
1813 1813 commit_idx = None
1814 if isinstance(rev, basestring):
1814 if isinstance(rev, compat.string_types):
1815 1815 commit_id = rev
1816 1816 else:
1817 1817 commit_idx = rev
@@ -2007,7 +2007,6 b' class RepoGroup(Base, BaseModel):'
2007 2007 __tablename__ = 'groups'
2008 2008 __table_args__ = (
2009 2009 UniqueConstraint('group_name', 'group_parent_id'),
2010 CheckConstraint('group_id != group_parent_id'),
2011 2010 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2012 2011 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2013 2012 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,7 +34,6 b' import functools'
34 34 import traceback
35 35 import collections
36 36
37
38 37 from sqlalchemy import *
39 38 from sqlalchemy.exc import IntegrityError
40 39 from sqlalchemy.ext.declarative import declared_attr
@@ -45,6 +44,7 b' from sqlalchemy.sql.expression import tr'
45 44 from beaker.cache import cache_region, region_invalidate
46 45 from webob.exc import HTTPNotFound
47 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 from pyramid import compat
48 48
49 49 # replace pylons with fake url for migration
50 50 from rhodecode.lib.dbmigrate.schema import url
@@ -1814,7 +1814,7 b' class Repository(Base, BaseModel):'
1814 1814 warnings.warn("Use get_commit", DeprecationWarning)
1815 1815 commit_id = None
1816 1816 commit_idx = None
1817 if isinstance(rev, basestring):
1817 if isinstance(rev, compat.string_types):
1818 1818 commit_id = rev
1819 1819 else:
1820 1820 commit_idx = rev
@@ -1999,7 +1999,6 b' class RepoGroup(Base, BaseModel):'
1999 1999 __tablename__ = 'groups'
2000 2000 __table_args__ = (
2001 2001 UniqueConstraint('group_name', 'group_parent_id'),
2002 CheckConstraint('group_id != group_parent_id'),
2003 2002 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2004 2003 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2005 2004 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,7 +34,6 b' import functools'
34 34 import traceback
35 35 import collections
36 36
37
38 37 from sqlalchemy import *
39 38 from sqlalchemy.exc import IntegrityError
40 39 from sqlalchemy.ext.declarative import declared_attr
@@ -45,7 +44,7 b' from sqlalchemy.sql.expression import tr'
45 44 from beaker.cache import cache_region, region_invalidate
46 45 from webob.exc import HTTPNotFound
47 46 from zope.cachedescriptors.property import Lazy as LazyProperty
48
47 from pyramid import compat
49 48 # replace pylons with fake url for migration
50 49 from rhodecode.lib.dbmigrate.schema import url
51 50 from rhodecode.translation import _
@@ -1814,7 +1813,7 b' class Repository(Base, BaseModel):'
1814 1813 warnings.warn("Use get_commit", DeprecationWarning)
1815 1814 commit_id = None
1816 1815 commit_idx = None
1817 if isinstance(rev, basestring):
1816 if isinstance(rev, compat.string_types):
1818 1817 commit_id = rev
1819 1818 else:
1820 1819 commit_idx = rev
@@ -1999,7 +1998,6 b' class RepoGroup(Base, BaseModel):'
1999 1998 __tablename__ = 'groups'
2000 1999 __table_args__ = (
2001 2000 UniqueConstraint('group_name', 'group_parent_id'),
2002 CheckConstraint('group_id != group_parent_id'),
2003 2001 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2004 2002 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2005 2003 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -35,7 +35,6 b' import functools'
35 35 import traceback
36 36 import collections
37 37
38
39 38 from sqlalchemy import *
40 39 from sqlalchemy.exc import IntegrityError
41 40 from sqlalchemy.ext.declarative import declared_attr
@@ -46,7 +45,7 b' from sqlalchemy.sql.expression import tr'
46 45 from beaker.cache import cache_region, region_invalidate
47 46 from webob.exc import HTTPNotFound
48 47 from zope.cachedescriptors.property import Lazy as LazyProperty
49
48 from pyramid import compat
50 49 # replace pylons with fake url for migration
51 50 from rhodecode.lib.dbmigrate.schema import url
52 51 from rhodecode.translation import _
@@ -1816,7 +1815,7 b' class Repository(Base, BaseModel):'
1816 1815 warnings.warn("Use get_commit", DeprecationWarning)
1817 1816 commit_id = None
1818 1817 commit_idx = None
1819 if isinstance(rev, basestring):
1818 if isinstance(rev, compat.string_types):
1820 1819 commit_id = rev
1821 1820 else:
1822 1821 commit_idx = rev
@@ -2001,7 +2000,6 b' class RepoGroup(Base, BaseModel):'
2001 2000 __tablename__ = 'groups'
2002 2001 __table_args__ = (
2003 2002 UniqueConstraint('group_name', 'group_parent_id'),
2004 CheckConstraint('group_id != group_parent_id'),
2005 2003 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2006 2004 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2007 2005 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -35,7 +35,6 b' import functools'
35 35 import traceback
36 36 import collections
37 37
38
39 38 from sqlalchemy import *
40 39 from sqlalchemy.exc import IntegrityError
41 40 from sqlalchemy.ext.declarative import declared_attr
@@ -46,7 +45,7 b' from sqlalchemy.sql.expression import tr'
46 45 from beaker.cache import cache_region, region_invalidate
47 46 from webob.exc import HTTPNotFound
48 47 from zope.cachedescriptors.property import Lazy as LazyProperty
49
48 from pyramid import compat
50 49 # replace pylons with fake url for migration
51 50 from rhodecode.lib.dbmigrate.schema import url
52 51 from rhodecode.translation import _
@@ -1816,7 +1815,7 b' class Repository(Base, BaseModel):'
1816 1815 warnings.warn("Use get_commit", DeprecationWarning)
1817 1816 commit_id = None
1818 1817 commit_idx = None
1819 if isinstance(rev, basestring):
1818 if isinstance(rev, compat.string_types):
1820 1819 commit_id = rev
1821 1820 else:
1822 1821 commit_idx = rev
@@ -2001,7 +2000,6 b' class RepoGroup(Base, BaseModel):'
2001 2000 __tablename__ = 'groups'
2002 2001 __table_args__ = (
2003 2002 UniqueConstraint('group_name', 'group_parent_id'),
2004 CheckConstraint('group_id != group_parent_id'),
2005 2003 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2006 2004 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2007 2005 )
@@ -3180,7 +3178,7 b' class PullRequestReviewers(Base, BaseMod'
3180 3178 @reasons.setter
3181 3179 def reasons(self, val):
3182 3180 val = val or []
3183 if any(not isinstance(x, basestring) for x in val):
3181 if any(not isinstance(x, compat.string_types) for x in val):
3184 3182 raise Exception('invalid reasons type, must be list of strings')
3185 3183 self._reasons = val
3186 3184
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,7 +34,6 b' import functools'
34 34 import traceback
35 35 import collections
36 36
37
38 37 from sqlalchemy import *
39 38 from sqlalchemy.ext.declarative import declared_attr
40 39 from sqlalchemy.ext.hybrid import hybrid_property
@@ -44,6 +43,7 b' from sqlalchemy.sql.expression import tr'
44 43 from beaker.cache import cache_region
45 44 from webob.exc import HTTPNotFound
46 45 from zope.cachedescriptors.property import Lazy as LazyProperty
46 from pyramid import compat
47 47
48 48 # replace pylons with fake url for migration
49 49 from rhodecode.lib.dbmigrate.schema import url
@@ -1858,7 +1858,7 b' class Repository(Base, BaseModel):'
1858 1858 warnings.warn("Use get_commit", DeprecationWarning)
1859 1859 commit_id = None
1860 1860 commit_idx = None
1861 if isinstance(rev, basestring):
1861 if isinstance(rev, compat.string_types):
1862 1862 commit_id = rev
1863 1863 else:
1864 1864 commit_idx = rev
@@ -2043,7 +2043,6 b' class RepoGroup(Base, BaseModel):'
2043 2043 __tablename__ = 'groups'
2044 2044 __table_args__ = (
2045 2045 UniqueConstraint('group_name', 'group_parent_id'),
2046 CheckConstraint('group_id != group_parent_id'),
2047 2046 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2048 2047 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2049 2048 )
@@ -3407,7 +3406,7 b' class PullRequestReviewers(Base, BaseMod'
3407 3406 @reasons.setter
3408 3407 def reasons(self, val):
3409 3408 val = val or []
3410 if any(not isinstance(x, basestring) for x in val):
3409 if any(not isinstance(x, compat.string_types) for x in val):
3411 3410 raise Exception('invalid reasons type, must be list of strings')
3412 3411 self._reasons = val
3413 3412
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,7 +34,6 b' import functools'
34 34 import traceback
35 35 import collections
36 36
37
38 37 from sqlalchemy import *
39 38 from sqlalchemy.ext.declarative import declared_attr
40 39 from sqlalchemy.ext.hybrid import hybrid_property
@@ -44,6 +43,7 b' from sqlalchemy.sql.expression import tr'
44 43 from beaker.cache import cache_region
45 44 from webob.exc import HTTPNotFound
46 45 from zope.cachedescriptors.property import Lazy as LazyProperty
46 from pyramid import compat
47 47
48 48 # replace pylons with fake url for migration
49 49 from rhodecode.lib.dbmigrate.schema import url
@@ -1859,7 +1859,7 b' class Repository(Base, BaseModel):'
1859 1859 warnings.warn("Use get_commit", DeprecationWarning)
1860 1860 commit_id = None
1861 1861 commit_idx = None
1862 if isinstance(rev, basestring):
1862 if isinstance(rev, compat.string_types):
1863 1863 commit_id = rev
1864 1864 else:
1865 1865 commit_idx = rev
@@ -2044,7 +2044,6 b' class RepoGroup(Base, BaseModel):'
2044 2044 __tablename__ = 'groups'
2045 2045 __table_args__ = (
2046 2046 UniqueConstraint('group_name', 'group_parent_id'),
2047 CheckConstraint('group_id != group_parent_id'),
2048 2047 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2049 2048 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2050 2049 )
@@ -3408,7 +3407,7 b' class PullRequestReviewers(Base, BaseMod'
3408 3407 @reasons.setter
3409 3408 def reasons(self, val):
3410 3409 val = val or []
3411 if any(not isinstance(x, basestring) for x in val):
3410 if any(not isinstance(x, compat.string_types) for x in val):
3412 3411 raise Exception('invalid reasons type, must be list of strings')
3413 3412 self._reasons = val
3414 3413
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,7 +34,6 b' import functools'
34 34 import traceback
35 35 import collections
36 36
37
38 37 from sqlalchemy import *
39 38 from sqlalchemy.ext.declarative import declared_attr
40 39 from sqlalchemy.ext.hybrid import hybrid_property
@@ -44,7 +43,7 b' from sqlalchemy.sql.expression import tr'
44 43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 44 from beaker.cache import cache_region
46 45 from zope.cachedescriptors.property import Lazy as LazyProperty
47
46 from pyramid import compat
48 47 from pyramid.threadlocal import get_current_request
49 48
50 49 from rhodecode.translation import _
@@ -2047,7 +2046,7 b' class Repository(Base, BaseModel):'
2047 2046 warnings.warn("Use get_commit", DeprecationWarning)
2048 2047 commit_id = None
2049 2048 commit_idx = None
2050 if isinstance(rev, basestring):
2049 if isinstance(rev, compat.string_types):
2051 2050 commit_id = rev
2052 2051 else:
2053 2052 commit_idx = rev
@@ -2232,7 +2231,6 b' class RepoGroup(Base, BaseModel):'
2232 2231 __tablename__ = 'groups'
2233 2232 __table_args__ = (
2234 2233 UniqueConstraint('group_name', 'group_parent_id'),
2235 CheckConstraint('group_id != group_parent_id'),
2236 2234 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2237 2235 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2238 2236 )
@@ -3662,7 +3660,7 b' class PullRequestReviewers(Base, BaseMod'
3662 3660 @reasons.setter
3663 3661 def reasons(self, val):
3664 3662 val = val or []
3665 if any(not isinstance(x, basestring) for x in val):
3663 if any(not isinstance(x, compat.string_types) for x in val):
3666 3664 raise Exception('invalid reasons type, must be list of strings')
3667 3665 self._reasons = val
3668 3666
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -2,6 +2,7 b' import logging'
2 2 import datetime
3 3
4 4 from sqlalchemy import *
5 from pyramid import compat
5 6
6 7 from rhodecode.lib.utils2 import safe_str
7 8 from rhodecode.model import meta
@@ -12,7 +13,7 b' log = logging.getLogger(__name__)'
12 13
13 14 def time_to_datetime(tm):
14 15 if tm:
15 if isinstance(tm, basestring):
16 if isinstance(tm, compat.string_types):
16 17 try:
17 18 tm = float(tm)
18 19 except ValueError:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,3 b''
1 #!/usr/bin/python2.4
2 1
3 2 from __future__ import division
4 3
@@ -33,6 +32,8 b' import re'
33 32 import sys
34 33 import time
35 34 import urllib
35 from pyramid import compat
36
36 37
37 38 class diff_match_patch:
38 39 """Class containing the diff, match and patch methods.
@@ -1438,7 +1439,7 b' class diff_match_patch:'
1438 1439 text1 = None
1439 1440 diffs = None
1440 1441 # Note that texts may arrive as 'str' or 'unicode'.
1441 if isinstance(a, basestring) and isinstance(b, basestring) and c is None:
1442 if isinstance(a, compat.string_types) and isinstance(b, compat.string_types) and c is None:
1442 1443 # Method 1: text1, text2
1443 1444 # Compute diffs from text1 and text2.
1444 1445 text1 = a
@@ -1451,11 +1452,11 b' class diff_match_patch:'
1451 1452 # Compute text1 from diffs.
1452 1453 diffs = a
1453 1454 text1 = self.diff_text1(diffs)
1454 elif isinstance(a, basestring) and isinstance(b, list) and c is None:
1455 elif isinstance(a, compat.string_types) and isinstance(b, list) and c is None:
1455 1456 # Method 3: text1, diffs
1456 1457 text1 = a
1457 1458 diffs = b
1458 elif (isinstance(a, basestring) and isinstance(b, basestring) and
1459 elif (isinstance(a, compat.string_types) and isinstance(b, compat.string_types) and
1459 1460 isinstance(c, list)):
1460 1461 # Method 4: text1, text2, diffs
1461 1462 # text2 is not used.
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -67,14 +67,13 b' def get_exc_store():'
67 67 return _exc_store_path
68 68
69 69
70 def _store_exception(exc_id, exc_info, prefix):
71 exc_type, exc_value, exc_traceback = exc_info
72 tb = ''.join(traceback.format_exception(
73 exc_type, exc_value, exc_traceback, None))
70 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix):
71 """
72 Low level function to store exception in the exception tracker
73 """
74 74
75 exc_type_name = exc_type.__name__
76 75 exc_store_path = get_exc_store()
77 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
76 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name)
78 77 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
79 78 if not os.path.isdir(exc_store_path):
80 79 os.makedirs(exc_store_path)
@@ -84,6 +83,16 b' def _store_exception(exc_id, exc_info, p'
84 83 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
85 84
86 85
86 def _prepare_exception(exc_info):
87 exc_type, exc_value, exc_traceback = exc_info
88 exc_type_name = exc_type.__name__
89
90 tb = ''.join(traceback.format_exception(
91 exc_type, exc_value, exc_traceback, None))
92
93 return exc_type_name, tb
94
95
87 96 def store_exception(exc_id, exc_info, prefix=global_prefix):
88 97 """
89 98 Example usage::
@@ -93,7 +102,9 b' def store_exception(exc_id, exc_info, pr'
93 102 """
94 103
95 104 try:
96 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
105 exc_type_name, exc_traceback = _prepare_exception(exc_info)
106 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
107 exc_traceback=exc_traceback, prefix=prefix)
97 108 except Exception:
98 109 log.exception('Failed to store exception `%s` information', exc_id)
99 110 # there's no way this can fail, it will crash server badly if it does.
@@ -149,3 +160,7 b' def delete_exception(exc_id, prefix=glob'
149 160 log.exception('Failed to remove exception `%s` information', exc_id)
150 161 # there's no way this can fail, it will crash server badly if it does.
151 162 pass
163
164
165 def generate_id():
166 return id(object())
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,7 +34,6 b' import urllib'
34 34 import math
35 35 import logging
36 36 import re
37 import urlparse
38 37 import time
39 38 import string
40 39 import hashlib
@@ -45,10 +44,10 b' import itertools'
45 44 import fnmatch
46 45 import bleach
47 46
47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 from pygments import highlight as code_highlight
52 51 from pygments.lexers import (
53 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54 53
@@ -81,12 +80,14 b' from rhodecode.lib.utils2 import str2boo'
81 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
82 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
83 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90
90 91 log = logging.getLogger(__name__)
91 92
92 93
@@ -260,6 +261,21 b' def files_breadcrumbs(repo_name, commit_'
260 261 return literal('/'.join(url_segments))
261 262
262 263
264 def code_highlight(code, lexer, formatter, use_hl_filter=False):
265 """
266 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
267
268 If ``outfile`` is given and a valid file object (an object
269 with a ``write`` method), the result will be written to it, otherwise
270 it is returned as a string.
271 """
272 if use_hl_filter:
273 # add HL filter
274 from rhodecode.lib.index import search_utils
275 lexer.add_filter(search_utils.ElasticSearchHLFilter())
276 return pygments.format(pygments.lex(code, lexer), formatter)
277
278
263 279 class CodeHtmlFormatter(HtmlFormatter):
264 280 """
265 281 My code Html Formatter for source codes
@@ -386,110 +402,9 b' class SearchContentCodeHtmlFormatter(Cod'
386 402
387 403 current_line_number += 1
388 404
389
390 405 yield 0, '</table>'
391 406
392 407
393 def extract_phrases(text_query):
394 """
395 Extracts phrases from search term string making sure phrases
396 contained in double quotes are kept together - and discarding empty values
397 or fully whitespace values eg.
398
399 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
400
401 """
402
403 in_phrase = False
404 buf = ''
405 phrases = []
406 for char in text_query:
407 if in_phrase:
408 if char == '"': # end phrase
409 phrases.append(buf)
410 buf = ''
411 in_phrase = False
412 continue
413 else:
414 buf += char
415 continue
416 else:
417 if char == '"': # start phrase
418 in_phrase = True
419 phrases.append(buf)
420 buf = ''
421 continue
422 elif char == ' ':
423 phrases.append(buf)
424 buf = ''
425 continue
426 else:
427 buf += char
428
429 phrases.append(buf)
430 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
431 return phrases
432
433
434 def get_matching_offsets(text, phrases):
435 """
436 Returns a list of string offsets in `text` that the list of `terms` match
437
438 >>> get_matching_offsets('some text here', ['some', 'here'])
439 [(0, 4), (10, 14)]
440
441 """
442 offsets = []
443 for phrase in phrases:
444 for match in re.finditer(phrase, text):
445 offsets.append((match.start(), match.end()))
446
447 return offsets
448
449
450 def normalize_text_for_matching(x):
451 """
452 Replaces all non alnum characters to spaces and lower cases the string,
453 useful for comparing two text strings without punctuation
454 """
455 return re.sub(r'[^\w]', ' ', x.lower())
456
457
458 def get_matching_line_offsets(lines, terms):
459 """ Return a set of `lines` indices (starting from 1) matching a
460 text search query, along with `context` lines above/below matching lines
461
462 :param lines: list of strings representing lines
463 :param terms: search term string to match in lines eg. 'some text'
464 :param context: number of lines above/below a matching line to add to result
465 :param max_lines: cut off for lines of interest
466 eg.
467
468 text = '''
469 words words words
470 words words words
471 some text some
472 words words words
473 words words words
474 text here what
475 '''
476 get_matching_line_offsets(text, 'text', context=1)
477 {3: [(5, 9)], 6: [(0, 4)]]
478
479 """
480 matching_lines = {}
481 phrases = [normalize_text_for_matching(phrase)
482 for phrase in extract_phrases(terms)]
483
484 for line_index, line in enumerate(lines, start=1):
485 match_offsets = get_matching_offsets(
486 normalize_text_for_matching(line), phrases)
487 if match_offsets:
488 matching_lines[line_index] = match_offsets
489
490 return matching_lines
491
492
493 408 def hsv_to_rgb(h, s, v):
494 409 """ Convert hsv color values to rgb """
495 410
@@ -735,10 +650,9 b' flash = Flash()'
735 650 # SCM FILTERS available via h.
736 651 #==============================================================================
737 652 from rhodecode.lib.vcs.utils import author_name, author_email
738 from rhodecode.lib.utils2 import credentials_filter, age as _age
653 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
739 654 from rhodecode.model.db import User, ChangesetStatus
740 655
741 age = _age
742 656 capitalize = lambda x: x.capitalize()
743 657 email = author_email
744 658 short_id = lambda x: x[:12]
@@ -769,23 +683,25 b' def age_component(datetime_iso, value=No'
769 683 datetime_iso, title, tzinfo))
770 684
771 685
772 def _shorten_commit_id(commit_id):
773 from rhodecode import CONFIG
774 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
775 return commit_id[:def_len]
686 def _shorten_commit_id(commit_id, commit_len=None):
687 if commit_len is None:
688 request = get_current_request()
689 commit_len = request.call_context.visual.show_sha_length
690 return commit_id[:commit_len]
776 691
777 692
778 def show_id(commit):
693 def show_id(commit, show_idx=None, commit_len=None):
779 694 """
780 695 Configurable function that shows ID
781 696 by default it's r123:fffeeefffeee
782 697
783 698 :param commit: commit instance
784 699 """
785 from rhodecode import CONFIG
786 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
700 if show_idx is None:
701 request = get_current_request()
702 show_idx = request.call_context.visual.show_revision_number
787 703
788 raw_id = _shorten_commit_id(commit.raw_id)
704 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
789 705 if show_idx:
790 706 return 'r%s:%s' % (commit.idx, raw_id)
791 707 else:
@@ -821,6 +737,7 b' class _RepoChecker(object):'
821 737 _type = repository
822 738 return _type == self._backend_alias
823 739
740
824 741 is_git = _RepoChecker('git')
825 742 is_hg = _RepoChecker('hg')
826 743 is_svn = _RepoChecker('svn')
@@ -828,6 +745,7 b" is_svn = _RepoChecker('svn')"
828 745
829 746 def get_repo_type_by_name(repo_name):
830 747 repo = Repository.get_by_repo_name(repo_name)
748 if repo:
831 749 return repo.repo_type
832 750
833 751
@@ -1577,6 +1495,28 b' def breadcrumb_repo_link(repo):'
1577 1495 return literal(' &raquo; '.join(path))
1578 1496
1579 1497
1498 def breadcrumb_repo_group_link(repo_group):
1499 """
1500 Makes a breadcrumbs path link to repo
1501
1502 ex::
1503 group >> subgroup
1504
1505 :param repo_group: a Repository Group instance
1506 """
1507
1508 path = [
1509 link_to(group.name,
1510 route_path('repo_group_home', repo_group_name=group.group_name))
1511 for group in repo_group.parents
1512 ] + [
1513 link_to(repo_group.name,
1514 route_path('repo_group_home', repo_group_name=repo_group.group_name))
1515 ]
1516
1517 return literal(' &raquo; '.join(path))
1518
1519
1580 1520 def format_byte_size_binary(file_size):
1581 1521 """
1582 1522 Formats file/folder sizes to standard.
@@ -1629,8 +1569,7 b' def urlify_commits(text_, repository):'
1629 1569 return tmpl % {
1630 1570 'pref': pref,
1631 1571 'cls': 'revision-link',
1632 'url': route_url('repo_commit', repo_name=repository,
1633 commit_id=commit_id),
1572 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1634 1573 'commit_id': commit_id,
1635 1574 'suf': suf
1636 1575 }
@@ -1661,8 +1600,7 b' def _process_url_func(match_obj, repo_na'
1661 1600 raise ValueError('Bad link_format:{}'.format(link_format))
1662 1601
1663 1602 (repo_name_cleaned,
1664 parent_group_name) = RepoGroupModel().\
1665 _get_group_name_and_parent(repo_name)
1603 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1666 1604
1667 1605 # variables replacement
1668 1606 named_vars = {
@@ -1675,10 +1613,14 b' def _process_url_func(match_obj, repo_na'
1675 1613 named_vars.update(match_obj.groupdict())
1676 1614 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1677 1615
1616 def quote_cleaner(input_str):
1617 """Remove quotes as it's HTML"""
1618 return input_str.replace('"', '')
1619
1678 1620 data = {
1679 1621 'pref': pref,
1680 'cls': 'issue-tracker-link',
1681 'url': _url,
1622 'cls': quote_cleaner('issue-tracker-link'),
1623 'url': quote_cleaner(_url),
1682 1624 'id-repr': issue_id,
1683 1625 'issue-prefix': entry['pref'],
1684 1626 'serv': entry['url'],
@@ -1703,8 +1645,7 b' def get_active_pattern_entries(repo_name'
1703 1645 return active_entries
1704 1646
1705 1647
1706 def process_patterns(text_string, repo_name, link_format='html',
1707 active_entries=None):
1648 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1708 1649
1709 1650 allowed_formats = ['html', 'rst', 'markdown']
1710 1651 if link_format not in allowed_formats:
@@ -1750,8 +1691,7 b' def process_patterns(text_string, repo_n'
1750 1691 return newtext, issues_data
1751 1692
1752 1693
1753 def urlify_commit_message(commit_text, repository=None,
1754 active_pattern_entries=None):
1694 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1755 1695 """
1756 1696 Parses given text message and makes proper links.
1757 1697 issues are linked to given issue-server, and rest is a commit link
@@ -1904,25 +1844,6 b' def journal_filter_help(request):'
1904 1844 ).format(actions=actions)
1905 1845
1906 1846
1907 def search_filter_help(searcher, request):
1908 _ = request.translate
1909
1910 terms = ''
1911 return _(
1912 'Example filter terms for `{searcher}` search:\n' +
1913 '{terms}\n' +
1914 'Generate wildcards using \'*\' character:\n' +
1915 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1916 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1917 '\n' +
1918 'Optional AND / OR operators in queries\n' +
1919 ' "repo_name:vcs OR repo_name:test"\n' +
1920 ' "owner:test AND repo_name:test*"\n' +
1921 'More: {search_doc}'
1922 ).format(searcher=searcher.name,
1923 terms=terms, search_doc=searcher.query_lang_doc)
1924
1925
1926 1847 def not_mapped_error(repo_name):
1927 1848 from rhodecode.translation import _
1928 1849 flash(_('%s repository is not mapped to db perhaps'
@@ -2107,3 +2028,15 b' def go_import_header(request, db_repo=No'
2107 2028 def reviewer_as_json(*args, **kwargs):
2108 2029 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2109 2030 return _reviewer_as_json(*args, **kwargs)
2031
2032
2033 def get_repo_view_type(request):
2034 route_name = request.matched_route.name
2035 route_to_view_type = {
2036 'repo_changelog': 'changelog',
2037 'repo_files': 'files',
2038 'repo_summary': 'summary',
2039 'repo_commit': 'commit'
2040
2041 }
2042 return route_to_view_type.get(route_name)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -284,17 +284,16 b' def post_push(extras):'
284 284 output += _http_ret.title
285 285
286 286 if extras.new_refs:
287 tmpl = \
288 extras.server_url + '/' + \
289 extras.repository + \
287 tmpl = extras.server_url + '/' + extras.repository + \
290 288 "/pull-request/new?{ref_type}={ref_name}"
289
291 290 for branch_name in extras.new_refs['branches']:
292 291 output += 'RhodeCode: open pull request link: {}\n'.format(
293 tmpl.format(ref_type='branch', ref_name=branch_name))
292 tmpl.format(ref_type='branch', ref_name=safe_str(branch_name)))
294 293
295 294 for book_name in extras.new_refs['bookmarks']:
296 295 output += 'RhodeCode: open pull request link: {}\n'.format(
297 tmpl.format(ref_type='bookmark', ref_name=book_name))
296 tmpl.format(ref_type='bookmark', ref_name=safe_str(book_name)))
298 297
299 298 hook_response = ''
300 299 if not is_shadow_repo(extras):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -25,15 +25,27 b' Index schema for RhodeCode'
25 25 import importlib
26 26 import logging
27 27
28 from rhodecode.lib.index.search_utils import normalize_text_for_matching
29
28 30 log = logging.getLogger(__name__)
29 31
30 32 # leave defaults for backward compat
31 33 default_searcher = 'rhodecode.lib.index.whoosh'
32 34 default_location = '%(here)s/data/index'
33 35
36 ES_VERSION_2 = '2'
37 ES_VERSION_6 = '6'
38 # for legacy reasons we keep 2 compat as default
39 DEFAULT_ES_VERSION = ES_VERSION_2
34 40
35 class BaseSearch(object):
41 from rhodecode_tools.lib.fts_index.elasticsearch_engine_6 import \
42 ES_CONFIG # pragma: no cover
43
44
45 class BaseSearcher(object):
36 46 query_lang_doc = ''
47 es_version = None
48 name = None
37 49
38 50 def __init__(self):
39 51 pass
@@ -41,19 +53,51 b' class BaseSearch(object):'
41 53 def cleanup(self):
42 54 pass
43 55
44 def search(self, query, document_type, search_user, repo_name=None,
56 def search(self, query, document_type, search_user,
57 repo_name=None, repo_group_name=None,
45 58 raise_on_exc=True):
46 59 raise Exception('NotImplemented')
47 60
61 @staticmethod
62 def query_to_mark(query, default_field=None):
63 """
64 Formats the query to mark token for jquery.mark.js highlighting. ES could
65 have a different format optionally.
48 66
49 def searcher_from_config(config, prefix='search.'):
67 :param default_field:
68 :param query:
69 """
70 return ' '.join(normalize_text_for_matching(query).split())
71
72 @property
73 def is_es_6(self):
74 return self.es_version == ES_VERSION_6
75
76 def get_handlers(self):
77 return {}
78
79 @staticmethod
80 def extract_search_tags(query):
81 return []
82
83
84 def search_config(config, prefix='search.'):
50 85 _config = {}
51 86 for key in config.keys():
52 87 if key.startswith(prefix):
53 88 _config[key[len(prefix):]] = config[key]
89 return _config
90
91
92 def searcher_from_config(config, prefix='search.'):
93 _config = search_config(config, prefix)
54 94
55 95 if 'location' not in _config:
56 96 _config['location'] = default_location
97 if 'es_version' not in _config:
98 # use old legacy ES version set to 2
99 _config['es_version'] = '2'
100
57 101 imported = importlib.import_module(_config.get('module', default_searcher))
58 searcher = imported.Search(config=_config)
102 searcher = imported.Searcher(config=_config)
59 103 return searcher
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -33,7 +33,7 b' from whoosh.index import create_in, open'
33 33 from whoosh.qparser import QueryParser, QueryParserError
34 34
35 35 import rhodecode.lib.helpers as h
36 from rhodecode.lib.index import BaseSearch
36 from rhodecode.lib.index import BaseSearcher
37 37 from rhodecode.lib.utils2 import safe_unicode
38 38
39 39 log = logging.getLogger(__name__)
@@ -59,13 +59,13 b' FRAGMENTER = ContextFragmenter(200)'
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 class Search(BaseSearch):
62 class WhooshSearcher(BaseSearcher):
63 63 # this also shows in UI
64 64 query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html'
65 65 name = 'whoosh'
66 66
67 67 def __init__(self, config):
68 super(Search, self).__init__()
68 super(Searcher, self).__init__()
69 69 self.config = config
70 70 if not os.path.isdir(self.config['location']):
71 71 os.makedirs(self.config['location'])
@@ -100,8 +100,8 b' class Search(BaseSearch):'
100 100 return query
101 101
102 102 def search(self, query, document_type, search_user,
103 repo_name=None, requested_page=1, page_limit=10, sort=None,
104 raise_on_exc=True):
103 repo_name=None, repo_group_name=None,
104 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
105 105
106 106 original_query = query
107 107 query = self._extend_query(query)
@@ -162,16 +162,17 b' class Search(BaseSearch):'
162 162 _ = translator
163 163 stats = [
164 164 {'key': _('Index Type'), 'value': 'Whoosh'},
165 {'sep': True},
166
165 167 {'key': _('File Index'), 'value': str(self.file_index)},
166 {'key': _('Indexed documents'),
167 'value': self.file_index.doc_count()},
168 {'key': _('Last update'),
169 'value': h.time_to_datetime(self.file_index.last_modified())},
168 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
169 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
170
171 {'sep': True},
172
170 173 {'key': _('Commit index'), 'value': str(self.commit_index)},
171 {'key': _('Indexed documents'),
172 'value': str(self.commit_index.doc_count())},
173 {'key': _('Last update'),
174 'value': h.time_to_datetime(self.commit_index.last_modified())}
174 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
175 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
175 176 ]
176 177 return stats
177 178
@@ -227,6 +228,9 b' class Search(BaseSearch):'
227 228 return self.searcher
228 229
229 230
231 Searcher = WhooshSearcher
232
233
230 234 class WhooshResultWrapper(object):
231 235 def __init__(self, search_type, total_hits, results):
232 236 self.search_type = search_type
@@ -263,6 +267,8 b' class WhooshResultWrapper(object):'
263 267 # TODO: marcink: this feels like an overkill, there's a lot of data
264 268 # inside hit object, and we don't need all
265 269 res = dict(hit)
270 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
271 res['content_highlight'] = ''
266 272
267 273 f_path = '' # pragma: no cover
268 274 if self.search_type in ['content', 'path']:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -357,7 +357,9 b' class MarkupRenderer(object):'
357 357 if leading_newline:
358 358 source += '<br />'
359 359 source += rendered_source.replace("\n", '<br />')
360 return source
360
361 rendered = cls.bleach_clean(source)
362 return rendered
361 363
362 364 @classmethod
363 365 def markdown(cls, source, safe=True, flavored=True, mentions=False,
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -45,8 +45,8 b' class RequestWrapperTween(object):'
45 45 end = time.time()
46 46 total = end - start
47 47 log.info(
48 'IP: %s Request to %s time: %.3fs [%s]',
49 get_ip_addr(request.environ),
48 'IP: %s %s Request to %s time: %.3fs [%s]',
49 get_ip_addr(request.environ), request.environ.get('REQUEST_METHOD'),
50 50 safe_str(get_access_path(request.environ)), total,
51 51 get_user_agent(request. environ)
52 52 )
@@ -57,4 +57,4 b' class RequestWrapperTween(object):'
57 57 def includeme(config):
58 58 config.add_tween(
59 59 'rhodecode.lib.middleware.request_wrapper.RequestWrapperTween',
60 ) No newline at end of file
60 )
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2015-2018 RhodeCode GmbH
3 # Copyright (C) 2015-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2015-2018 RhodeCode GmbH
3 # Copyright (C) 2015-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -98,7 +98,7 b' class CustomLockFactory(FileLock):'
98 98 # set non-blocking, this will cause an exception if we cannot acquire a lock
99 99 operation |= fcntl.LOCK_NB
100 100 start_lock_time = time.time()
101 timeout = 60 * 5 # 5min
101 timeout = 60 * 15 # 15min
102 102 while True:
103 103 try:
104 104 flock_org(fd, operation)
@@ -203,3 +203,12 b' class RedisPickleBackend(Serializer, red'
203 203 for key, value in mapping.items():
204 204 pipe.setex(key, self.redis_expiration_time, value)
205 205 pipe.execute()
206
207 def get_mutex(self, key):
208 u = redis_backend.u
209 if self.distributed_lock:
210 lock_key = u('_lock_{0}').format(key)
211 log.debug('Trying to acquire Redis lock for key %s', lock_key)
212 return self.client.lock(lock_key, self.lock_timeout, self.lock_sleep)
213 else:
214 return None
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2015-2018 RhodeCode GmbH
3 # Copyright (C) 2015-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2015-2018 RhodeCode GmbH
3 # Copyright (C) 2015-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -102,6 +102,7 b' class RhodeCodeCacheRegion(CacheRegion):'
102 102 decorate.get = get
103 103 decorate.original = fn
104 104 decorate.key_generator = key_generator
105 decorate.__wrapped__ = fn
105 106
106 107 return decorate
107 108
@@ -120,7 +121,7 b' def get_default_cache_settings(settings,'
120 121 if key.startswith(prefix):
121 122 name = key.split(prefix)[1].strip()
122 123 val = settings[key]
123 if isinstance(val, basestring):
124 if isinstance(val, compat.string_types):
124 125 val = val.strip()
125 126 cache_settings[name] = val
126 127 return cache_settings
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -25,6 +25,7 b' import pyramid.paster'
25 25
26 26 from rhodecode.lib.pyramid_utils import bootstrap
27 27 from rhodecode.lib.db_manage import DbManage
28 from rhodecode.lib.utils2 import safe_int
28 29
29 30 log = logging.getLogger(__name__)
30 31
@@ -33,11 +34,13 b' log = logging.getLogger(__name__)'
33 34 @click.argument('ini_path', type=click.Path(exists=True))
34 35 @click.option('--force-yes/--force-no', default=None,
35 36 help="Force yes/no to every question")
36 def main(ini_path, force_yes):
37 return command(ini_path, force_yes)
37 @click.option('--force-version', default=None,
38 help="Force upgrade from version")
39 def main(ini_path, force_yes, force_version):
40 return command(ini_path, force_yes, force_version)
38 41
39 42
40 def command(ini_path, force_yes):
43 def command(ini_path, force_yes, force_version):
41 44 pyramid.paster.setup_logging(ini_path)
42 45
43 46 with bootstrap(ini_path, env={'RC_CMD_UPGRADE_DB': '1'}) as env:
@@ -49,4 +52,4 b' def command(ini_path, force_yes):'
49 52 dbmanage = DbManage(
50 53 log_sql=True, dbconf=db_uri, root='.', tests=False,
51 54 cli_args=options)
52 dbmanage.upgrade()
55 dbmanage.upgrade(version=safe_int(force_version))
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -67,8 +67,7 b' def get_storage_size(storage_path):'
67 67 try:
68 68 sizes.append(os.path.getsize(storage_file))
69 69 except OSError:
70 log.exception('Failed to get size of storage file %s',
71 storage_file)
70 log.exception('Failed to get size of storage file %s', storage_file)
72 71 pass
73 72
74 73 return sum(sizes)
@@ -81,6 +80,16 b' def get_resource(resource_type):'
81 80 return 'NOT_SUPPORTED'
82 81
83 82
83 def get_cert_path(ini_path):
84 default = '/etc/ssl/certs/ca-certificates.crt'
85 control_ca_bundle = os.path.join(
86 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(ini_path)))),
87 '.rccontrol-profile/etc/ca-bundle.crt')
88 if os.path.isfile(control_ca_bundle):
89 default = control_ca_bundle
90
91 return default
92
84 93 class SysInfoRes(object):
85 94 def __init__(self, value, state=None, human_value=None):
86 95 self.value = value
@@ -604,6 +613,7 b' def rhodecode_config():'
604 613 import rhodecode
605 614 path = rhodecode.CONFIG.get('__file__')
606 615 rhodecode_ini_safe = rhodecode.CONFIG.copy()
616 cert_path = get_cert_path(path)
607 617
608 618 try:
609 619 config = configparser.ConfigParser()
@@ -615,10 +625,6 b' def rhodecode_config():'
615 625 log.exception('Failed to read .ini file for display')
616 626 parsed_ini = {}
617 627
618 cert_path = os.path.join(
619 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(path)))),
620 '.rccontrol-profile/etc/ca-bundle.crt')
621
622 628 rhodecode_ini_safe['server:main'] = parsed_ini
623 629
624 630 blacklist = [
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -43,6 +43,7 b' import sqlalchemy.exc'
43 43 import sqlalchemy.sql
44 44 import webob
45 45 import pyramid.threadlocal
46 from pyramid import compat
46 47 from pyramid.settings import asbool
47 48
48 49 import rhodecode
@@ -261,7 +262,7 b' def safe_str(unicode_, to_encoding=None)'
261 262 """
262 263
263 264 # if it's not basestr cast to str
264 if not isinstance(unicode_, basestring):
265 if not isinstance(unicode_, compat.string_types):
265 266 return str(unicode_)
266 267
267 268 if isinstance(unicode_, str):
@@ -564,6 +565,12 b' def age(prevdate, now=None, show_short_v'
564 565 return _(u'just now')
565 566
566 567
568 def age_from_seconds(seconds):
569 seconds = safe_int(seconds) or 0
570 prevdate = time_to_datetime(time.time() + seconds)
571 return age(prevdate, show_suffix=False, show_short_version=True)
572
573
567 574 def cleaned_uri(uri):
568 575 """
569 576 Quotes '[' and ']' from uri if there is only one of them.
@@ -681,7 +688,7 b' def datetime_to_time(dt):'
681 688
682 689 def time_to_datetime(tm):
683 690 if tm:
684 if isinstance(tm, basestring):
691 if isinstance(tm, compat.string_types):
685 692 try:
686 693 tm = float(tm)
687 694 except ValueError:
@@ -691,7 +698,7 b' def time_to_datetime(tm):'
691 698
692 699 def time_to_utcdatetime(tm):
693 700 if tm:
694 if isinstance(tm, basestring):
701 if isinstance(tm, compat.string_types):
695 702 try:
696 703 tm = float(tm)
697 704 except ValueError:
@@ -1009,3 +1016,14 b' def glob2re(pat):'
1009 1016 else:
1010 1017 res = res + re.escape(c)
1011 1018 return res + '\Z(?ms)'
1019
1020
1021 def parse_byte_string(size_str):
1022 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1023 if not match:
1024 raise ValueError('Given size:%s is invalid, please make sure '
1025 'to use format of <num>(MB|KB)' % size_str)
1026
1027 _parts = match.groups()
1028 num, type_ = _parts
1029 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -21,20 +21,21 b''
21 21 """
22 22 Base module for all VCS systems
23 23 """
24
25 import collections
24 import os
25 import re
26 import time
27 import shutil
26 28 import datetime
27 29 import fnmatch
28 30 import itertools
29 31 import logging
30 import os
31 import re
32 import time
32 import collections
33 33 import warnings
34 import shutil
35 34
36 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 from pyramid import compat
37 37
38 from rhodecode.translation import lazy_ugettext
38 39 from rhodecode.lib.utils2 import safe_str, safe_unicode
39 40 from rhodecode.lib.vcs import connection
40 41 from rhodecode.lib.vcs.utils import author_name, author_email
@@ -54,9 +55,6 b' FILEMODE_DEFAULT = 0o100644'
54 55 FILEMODE_EXECUTABLE = 0o100755
55 56
56 57 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
57 MergeResponse = collections.namedtuple(
58 'MergeResponse',
59 ('possible', 'executed', 'merge_ref', 'failure_reason'))
60 58
61 59
62 60 class MergeFailureReason(object):
@@ -142,6 +140,93 b' class UpdateFailureReason(object):'
142 140 MISSING_SOURCE_REF = 5
143 141
144 142
143 class MergeResponse(object):
144
145 # uses .format(**metadata) for variables
146 MERGE_STATUS_MESSAGES = {
147 MergeFailureReason.NONE: lazy_ugettext(
148 u'This pull request can be automatically merged.'),
149 MergeFailureReason.UNKNOWN: lazy_ugettext(
150 u'This pull request cannot be merged because of an unhandled exception. '
151 u'{exception}'),
152 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
153 u'This pull request cannot be merged because of merge conflicts.'),
154 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
155 u'This pull request could not be merged because push to '
156 u'target:`{target}@{merge_commit}` failed.'),
157 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
158 u'This pull request cannot be merged because the target '
159 u'`{target_ref.name}` is not a head.'),
160 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
161 u'This pull request cannot be merged because the source contains '
162 u'more branches than the target.'),
163 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
164 u'This pull request cannot be merged because the target '
165 u'has multiple heads: `{heads}`.'),
166 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
167 u'This pull request cannot be merged because the target repository is '
168 u'locked by {locked_by}.'),
169
170 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
171 u'This pull request cannot be merged because the target '
172 u'reference `{target_ref.name}` is missing.'),
173 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
174 u'This pull request cannot be merged because the source '
175 u'reference `{source_ref.name}` is missing.'),
176 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
177 u'This pull request cannot be merged because of conflicts related '
178 u'to sub repositories.'),
179
180 # Deprecations
181 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
182 u'This pull request cannot be merged because the target or the '
183 u'source reference is missing.'),
184
185 }
186
187 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
188 self.possible = possible
189 self.executed = executed
190 self.merge_ref = merge_ref
191 self.failure_reason = failure_reason
192 self.metadata = metadata or {}
193
194 def __repr__(self):
195 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
196
197 def __eq__(self, other):
198 same_instance = isinstance(other, self.__class__)
199 return same_instance \
200 and self.possible == other.possible \
201 and self.executed == other.executed \
202 and self.failure_reason == other.failure_reason
203
204 @property
205 def label(self):
206 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
207 not k.startswith('_'))
208 return label_dict.get(self.failure_reason)
209
210 @property
211 def merge_status_message(self):
212 """
213 Return a human friendly error message for the given merge status code.
214 """
215 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
216 try:
217 return msg.format(**self.metadata)
218 except Exception:
219 log.exception('Failed to format %s message', self)
220 return msg
221
222 def asdict(self):
223 data = {}
224 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
225 'merge_status_message']:
226 data[k] = getattr(self, k)
227 return data
228
229
145 230 class BaseRepository(object):
146 231 """
147 232 Base Repository for final backends
@@ -316,7 +401,7 b' class BaseRepository(object):'
316 401 # COMMITS
317 402 # ==========================================================================
318 403
319 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
404 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
320 405 """
321 406 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
322 407 are both None, most recent commit is returned.
@@ -333,7 +418,7 b' class BaseRepository(object):'
333 418
334 419 def get_commits(
335 420 self, start_id=None, end_id=None, start_date=None, end_date=None,
336 branch_name=None, show_hidden=False, pre_load=None):
421 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
337 422 """
338 423 Returns iterator of `BaseCommit` objects from start to end
339 424 not inclusive. This should behave just like a list, ie. end is not
@@ -346,6 +431,7 b' class BaseRepository(object):'
346 431 :param branch_name:
347 432 :param show_hidden:
348 433 :param pre_load:
434 :param translate_tags:
349 435 """
350 436 raise NotImplementedError
351 437
@@ -501,12 +587,11 b' class BaseRepository(object):'
501 587 repo_id, workspace_id, target_ref, source_repo,
502 588 source_ref, message, user_name, user_email, dry_run=dry_run,
503 589 use_rebase=use_rebase, close_branch=close_branch)
504 except RepositoryError:
505 log.exception(
506 'Unexpected failure when running merge, dry-run=%s',
507 dry_run)
590 except RepositoryError as exc:
591 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
508 592 return MergeResponse(
509 False, False, None, MergeFailureReason.UNKNOWN)
593 False, False, None, MergeFailureReason.UNKNOWN,
594 metadata={'exception': str(exc)})
510 595
511 596 def _merge_repo(self, repo_id, workspace_id, target_ref,
512 597 source_repo, source_ref, merge_message,
@@ -610,7 +695,7 b' class BaseRepository(object):'
610 695 (commit, self, commit.repository))
611 696
612 697 def _validate_commit_id(self, commit_id):
613 if not isinstance(commit_id, basestring):
698 if not isinstance(commit_id, compat.string_types):
614 699 raise TypeError("commit_id must be a string value")
615 700
616 701 def _validate_commit_idx(self, commit_idx):
@@ -647,7 +732,7 b' class BaseRepository(object):'
647 732 warnings.warn("Use get_commit instead", DeprecationWarning)
648 733 commit_id = None
649 734 commit_idx = None
650 if isinstance(revision, basestring):
735 if isinstance(revision, compat.string_types):
651 736 commit_id = revision
652 737 else:
653 738 commit_idx = revision
@@ -674,7 +759,7 b' class BaseRepository(object):'
674 759 if revision is None:
675 760 return revision
676 761
677 if isinstance(revision, basestring):
762 if isinstance(revision, compat.string_types):
678 763 commit_id = revision
679 764 else:
680 765 commit_id = self.commit_ids[revision]
@@ -697,6 +782,9 b' class BaseRepository(object):'
697 782 def install_hooks(self, force=False):
698 783 return self._remote.install_hooks(force)
699 784
785 def get_hooks_info(self):
786 return self._remote.get_hooks_info()
787
700 788
701 789 class BaseCommit(object):
702 790 """
@@ -1559,12 +1647,13 b' class EmptyRepository(BaseRepository):'
1559 1647
1560 1648 class CollectionGenerator(object):
1561 1649
1562 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1650 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1563 1651 self.repo = repo
1564 1652 self.commit_ids = commit_ids
1565 1653 # TODO: (oliver) this isn't currently hooked up
1566 1654 self.collection_size = None
1567 1655 self.pre_load = pre_load
1656 self.translate_tag = translate_tag
1568 1657
1569 1658 def __len__(self):
1570 1659 if self.collection_size is not None:
@@ -1580,8 +1669,9 b' class CollectionGenerator(object):'
1580 1669 """
1581 1670 Allows backends to override the way commits are generated.
1582 1671 """
1583 return self.repo.get_commit(commit_id=commit_id,
1584 pre_load=self.pre_load)
1672 return self.repo.get_commit(
1673 commit_id=commit_id, pre_load=self.pre_load,
1674 translate_tag=self.translate_tag)
1585 1675
1586 1676 def __getslice__(self, i, j):
1587 1677 """
@@ -1589,7 +1679,8 b' class CollectionGenerator(object):'
1589 1679 """
1590 1680 commit_ids = self.commit_ids[i:j]
1591 1681 return self.__class__(
1592 self.repo, commit_ids, pre_load=self.pre_load)
1682 self.repo, commit_ids, pre_load=self.pre_load,
1683 translate_tag=self.translate_tag)
1593 1684
1594 1685 def __repr__(self):
1595 1686 return '<CollectionGenerator[len:%s]>' % (self.__len__())
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -269,7 +269,8 b' class GitCommit(base.BaseCommit):'
269 269
270 270 def _make_commits(self, commit_ids, pre_load=None):
271 271 return [
272 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
272 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load,
273 translate_tag=False)
273 274 for commit_id in commit_ids]
274 275
275 276 def get_file_mode(self, path):
@@ -309,6 +310,10 b' class GitCommit(base.BaseCommit):'
309 310 self._get_filectx(path)
310 311 f_path = safe_str(path)
311 312
313 # optimize for n==1, rev-list is much faster for that use-case
314 if limit == 1:
315 cmd = ['rev-list', '-1', self.raw_id, '--', f_path]
316 else:
312 317 cmd = ['log']
313 318 if limit:
314 319 cmd.extend(['-n', str(safe_int(limit, 0))])
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -426,7 +426,7 b' class GitRepository(BaseRepository):'
426 426 except Exception:
427 427 return
428 428
429 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
429 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=True):
430 430 """
431 431 Returns `GitCommit` object representing commit from git repository
432 432 at the given `commit_id` or head (most recent commit) if None given.
@@ -438,6 +438,7 b' class GitRepository(BaseRepository):'
438 438 commit_id = commit_idx
439 439 commit_id = self._get_commit_id(commit_id)
440 440 try:
441 if translate_tag:
441 442 # Need to call remote to translate id for tagging scenario
442 443 commit_id = self._remote.get_object(commit_id)["commit_id"]
443 444 idx = self._commit_ids[commit_id]
@@ -448,7 +449,7 b' class GitRepository(BaseRepository):'
448 449
449 450 def get_commits(
450 451 self, start_id=None, end_id=None, start_date=None, end_date=None,
451 branch_name=None, show_hidden=False, pre_load=None):
452 branch_name=None, show_hidden=False, pre_load=None, translate_tags=True):
452 453 """
453 454 Returns generator of `GitCommit` objects from start to end (both
454 455 are inclusive), in ascending date order.
@@ -528,7 +529,8 b' class GitRepository(BaseRepository):'
528 529 if start_pos or end_pos:
529 530 commit_ids = commit_ids[start_pos: end_pos]
530 531
531 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
532 return CollectionGenerator(self, commit_ids, pre_load=pre_load,
533 translate_tag=translate_tags)
532 534
533 535 def get_diff(
534 536 self, commit1, commit2, path='', ignore_whitespace=False,
@@ -911,11 +913,15 b' class GitRepository(BaseRepository):'
911 913 source_repo, source_ref, merge_message,
912 914 merger_name, merger_email, dry_run=False,
913 915 use_rebase=False, close_branch=False):
916
917 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
918 'rebase' if use_rebase else 'merge', dry_run)
914 919 if target_ref.commit_id != self.branches[target_ref.name]:
915 920 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
916 921 target_ref.commit_id, self.branches[target_ref.name])
917 922 return MergeResponse(
918 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
923 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
924 metadata={'target_ref': target_ref})
919 925
920 926 shadow_repository_path = self._maybe_prepare_merge_workspace(
921 927 repo_id, workspace_id, target_ref, source_ref)
@@ -943,7 +949,8 b' class GitRepository(BaseRepository):'
943 949 target_ref, target_ref.commit_id,
944 950 shadow_repo.branches[target_ref.name])
945 951 return MergeResponse(
946 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
952 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
953 metadata={'target_ref': target_ref})
947 954
948 955 # calculate new branch
949 956 pr_branch = shadow_repo._get_new_pr_branch(
@@ -954,12 +961,15 b' class GitRepository(BaseRepository):'
954 961 try:
955 962 shadow_repo._local_fetch(source_repo.path, source_ref.name)
956 963 except RepositoryError:
957 log.exception('Failure when doing local fetch on git shadow repo')
964 log.exception('Failure when doing local fetch on '
965 'shadow repo: %s', shadow_repo)
958 966 return MergeResponse(
959 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
967 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
968 metadata={'source_ref': source_ref})
960 969
961 970 merge_ref = None
962 971 merge_failure_reason = MergeFailureReason.NONE
972 metadata = {}
963 973 try:
964 974 shadow_repo._local_merge(merge_message, merger_name, merger_email,
965 975 [source_ref.commit_id])
@@ -988,12 +998,15 b' class GitRepository(BaseRepository):'
988 998 merge_succeeded = True
989 999 except RepositoryError:
990 1000 log.exception(
991 'Failure when doing local push on git shadow repo')
1001 'Failure when doing local push from the shadow '
1002 'repository to the target repository at %s.', self.path)
992 1003 merge_succeeded = False
993 1004 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1005 metadata['target'] = 'git shadow repo'
1006 metadata['merge_commit'] = pr_branch
994 1007 else:
995 1008 merge_succeeded = False
996 1009
997 1010 return MergeResponse(
998 merge_possible, merge_succeeded, merge_ref,
999 merge_failure_reason)
1011 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
1012 metadata=metadata)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -303,10 +303,10 b' class MercurialCommit(base.BaseCommit):'
303 303
304 304 alias = self.repository.alias
305 305 for k, vals in self._submodules.iteritems():
306 if vcspath.dirname(k) == path:
306 307 loc = vals[0]
307 308 commit = vals[1]
308 dirnodes.append(
309 SubModuleNode(k, url=loc, commit=commit, alias=alias))
309 dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias))
310 310 nodes = dirnodes + filenodes
311 311 # cache nodes
312 312 for node in nodes:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -414,7 +414,7 b' class MercurialRepository(BaseRepository'
414 414 """
415 415 return os.path.join(self.path, '.hg', '.hgrc')
416 416
417 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
417 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
418 418 """
419 419 Returns ``MercurialCommit`` object representing repository's
420 420 commit at the given `commit_id` or `commit_idx`.
@@ -456,7 +456,7 b' class MercurialRepository(BaseRepository'
456 456
457 457 def get_commits(
458 458 self, start_id=None, end_id=None, start_date=None, end_date=None,
459 branch_name=None, show_hidden=False, pre_load=None):
459 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
460 460 """
461 461 Returns generator of ``MercurialCommit`` objects from start to end
462 462 (both are inclusive)
@@ -610,7 +610,7 b' class MercurialRepository(BaseRepository'
610 610 Returns the commit id of the merge and a boolean indicating if the
611 611 commit needs to be pushed.
612 612 """
613 self._update(target_ref.commit_id)
613 self._update(target_ref.commit_id, clean=True)
614 614
615 615 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
616 616 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
@@ -631,7 +631,7 b' class MercurialRepository(BaseRepository'
631 631 self._remote.rebase(
632 632 source=source_ref.commit_id, dest=target_ref.commit_id)
633 633 self._remote.invalidate_vcs_cache()
634 self._update(bookmark_name)
634 self._update(bookmark_name, clean=True)
635 635 return self._identify(), True
636 636 except RepositoryError:
637 637 # The rebase-abort may raise another exception which 'hides'
@@ -710,18 +710,21 b' class MercurialRepository(BaseRepository'
710 710 'rebase' if use_rebase else 'merge', dry_run)
711 711 if target_ref.commit_id not in self._heads():
712 712 return MergeResponse(
713 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
713 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
714 metadata={'target_ref': target_ref})
714 715
715 716 try:
716 if (target_ref.type == 'branch' and
717 len(self._heads(target_ref.name)) != 1):
717 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
718 heads = ','.join(self._heads(target_ref.name))
718 719 return MergeResponse(
719 720 False, False, None,
720 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
721 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
722 metadata={'heads': heads})
721 723 except CommitDoesNotExistError:
722 724 log.exception('Failure when looking up branch heads on hg target')
723 725 return MergeResponse(
724 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
726 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
727 metadata={'target_ref': target_ref})
725 728
726 729 shadow_repository_path = self._maybe_prepare_merge_workspace(
727 730 repo_id, workspace_id, target_ref, source_ref)
@@ -730,6 +733,7 b' class MercurialRepository(BaseRepository'
730 733 log.debug('Pulling in target reference %s', target_ref)
731 734 self._validate_pull_reference(target_ref)
732 735 shadow_repo._local_pull(self.path, target_ref)
736
733 737 try:
734 738 log.debug('Pulling in source reference %s', source_ref)
735 739 source_repo._validate_pull_reference(source_ref)
@@ -737,12 +741,14 b' class MercurialRepository(BaseRepository'
737 741 except CommitDoesNotExistError:
738 742 log.exception('Failure when doing local pull on hg shadow repo')
739 743 return MergeResponse(
740 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
744 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
745 metadata={'source_ref': source_ref})
741 746
742 747 merge_ref = None
743 748 merge_commit_id = None
744 749 close_commit_id = None
745 750 merge_failure_reason = MergeFailureReason.NONE
751 metadata = {}
746 752
747 753 # enforce that close branch should be used only in case we source from
748 754 # an actual Branch
@@ -758,8 +764,8 b' class MercurialRepository(BaseRepository'
758 764 target_ref, merger_name, merger_email, source_ref)
759 765 merge_possible = True
760 766 except RepositoryError:
761 log.exception(
762 'Failure when doing close branch on hg shadow repo')
767 log.exception('Failure when doing close branch on '
768 'shadow repo: %s', shadow_repo)
763 769 merge_possible = False
764 770 merge_failure_reason = MergeFailureReason.MERGE_FAILED
765 771 else:
@@ -824,19 +830,21 b' class MercurialRepository(BaseRepository'
824 830 except RepositoryError:
825 831 log.exception(
826 832 'Failure when doing local push from the shadow '
827 'repository to the target repository.')
833 'repository to the target repository at %s.', self.path)
828 834 merge_succeeded = False
829 835 merge_failure_reason = MergeFailureReason.PUSH_FAILED
836 metadata['target'] = 'hg shadow repo'
837 metadata['merge_commit'] = merge_commit_id
830 838 else:
831 839 merge_succeeded = True
832 840 else:
833 841 merge_succeeded = False
834 842
835 843 return MergeResponse(
836 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
844 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
845 metadata=metadata)
837 846
838 def _get_shadow_instance(
839 self, shadow_repository_path, enable_hooks=False):
847 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
840 848 config = self.config.copy()
841 849 if not enable_hooks:
842 850 config.clear_section('hooks')
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -250,7 +250,7 b' class SubversionRepository(base.BaseRepo'
250 250 """
251 251 return os.path.join(self.path, 'hooks')
252 252
253 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
253 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
254 254 if self.is_empty():
255 255 raise EmptyRepositoryError("There are no commits yet")
256 256 if commit_id is not None:
@@ -268,7 +268,7 b' class SubversionRepository(base.BaseRepo'
268 268
269 269 def get_commits(
270 270 self, start_id=None, end_id=None, start_date=None, end_date=None,
271 branch_name=None, show_hidden=False, pre_load=None):
271 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
272 272 if self.is_empty():
273 273 raise EmptyRepositoryError("There are no commit_ids yet")
274 274 self._validate_branch_name(branch_name)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -35,7 +35,9 b' import msgpack'
35 35 import requests
36 36 from requests.packages.urllib3.util.retry import Retry
37 37
38 from . import exceptions, CurlSession
38 import rhodecode
39 from rhodecode.lib.system_info import get_cert_path
40 from rhodecode.lib.vcs import exceptions, CurlSession
39 41
40 42
41 43 log = logging.getLogger(__name__)
@@ -121,6 +123,8 b' class RemoteRepo(object):'
121 123 if log.isEnabledFor(logging.DEBUG):
122 124 self._call = self._call_with_logging
123 125
126 self.cert_dir = get_cert_path(rhodecode.CONFIG.get('__file__'))
127
124 128 def __getattr__(self, name):
125 129 def f(*args, **kwargs):
126 130 return self._call(name, *args, **kwargs)
@@ -132,6 +136,8 b' class RemoteRepo(object):'
132 136 # config object is being changed for hooking scenarios
133 137 wire = copy.deepcopy(self._wire)
134 138 wire["config"] = wire["config"].serialize()
139
140 wire["config"].append(('vcs', 'ssl_dir', self.cert_dir))
135 141 payload = {
136 142 'id': str(uuid.uuid4()),
137 143 'method': name,
@@ -231,6 +237,8 b' def _remote_call(url, payload, exception'
231 237
232 238 try:
233 239 exc._vcs_server_traceback = error['traceback']
240 exc._vcs_server_org_exc_name = error['org_exc']
241 exc._vcs_server_org_exc_tb = error['org_exc_tb']
234 242 except KeyError:
235 243 pass
236 244
@@ -243,8 +251,6 b' class VcsHttpProxy(object):'
243 251 CHUNK_SIZE = 16384
244 252
245 253 def __init__(self, server_and_port, backend_endpoint):
246
247
248 254 retries = Retry(total=5, connect=None, read=None, redirect=None)
249 255
250 256 adapter = requests.adapters.HTTPAdapter(max_retries=retries)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -24,6 +24,8 b' Custom vcs exceptions module.'
24 24 import logging
25 25 import functools
26 26 import urllib2
27 import rhodecode
28 from pyramid import compat
27 29
28 30 log = logging.getLogger(__name__)
29 31
@@ -180,17 +182,26 b' def map_vcs_exceptions(func):'
180 182 try:
181 183 return func(*args, **kwargs)
182 184 except Exception as e:
185 from rhodecode.lib.utils2 import str2bool
186 debug = str2bool(rhodecode.CONFIG.get('debug'))
187
183 188 # The error middleware adds information if it finds
184 189 # __traceback_info__ in a frame object. This way the remote
185 190 # traceback information is made available in error reports.
186 191 remote_tb = getattr(e, '_vcs_server_traceback', None)
192 org_remote_tb = getattr(e, '_vcs_server_org_exc_tb', '')
187 193 __traceback_info__ = None
188 194 if remote_tb:
189 if isinstance(remote_tb, basestring):
195 if isinstance(remote_tb, compat.string_types):
190 196 remote_tb = [remote_tb]
191 197 __traceback_info__ = (
192 'Found VCSServer remote traceback information:\n\n' +
193 '\n'.join(remote_tb))
198 'Found VCSServer remote traceback information:\n'
199 '{}\n'
200 '+++ BEG SOURCE EXCEPTION +++\n\n'
201 '{}\n'
202 '+++ END SOURCE EXCEPTION +++\n'
203 ''.format('\n'.join(remote_tb), org_remote_tb)
204 )
194 205
195 206 # Avoid that remote_tb also appears in the frame
196 207 del remote_tb
@@ -205,9 +216,9 b' def map_vcs_exceptions(func):'
205 216 args = e.args
206 217 else:
207 218 args = [__traceback_info__ or 'unhandledException']
208 if __traceback_info__ and kind not in ['unhandled', 'lookup']:
219 if debug or __traceback_info__ and kind not in ['unhandled', 'lookup']:
209 220 # for other than unhandled errors also log the traceback
210 # can be usefull for debugging
221 # can be useful for debugging
211 222 log.error(__traceback_info__)
212 223 raise _EXCEPTION_MAP[kind](*args)
213 224 else:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -200,8 +200,12 b' class Node(object):'
200 200 """
201 201 Comparator using name of the node, needed for quick list sorting.
202 202 """
203
203 204 kind_cmp = cmp(self.kind, other.kind)
204 205 if kind_cmp:
206 if isinstance(self, SubModuleNode):
207 # we make submodules equal to dirnode for "sorting" purposes
208 return NodeKind.DIR
205 209 return kind_cmp
206 210 return cmp(self.name, other.name)
207 211
@@ -372,6 +376,31 b' class FileNode(Node):'
372 376 """
373 377 return md5(self.raw_bytes)
374 378
379 def metadata_uncached(self):
380 """
381 Returns md5, binary flag of the file node, without any cache usage.
382 """
383
384 content = self.content_uncached()
385
386 is_binary = content and '\0' in content
387 size = 0
388 if content:
389 size = len(content)
390
391 return is_binary, md5(content), size, content
392
393 def content_uncached(self):
394 """
395 Returns lazily content of the FileNode. If possible, would try to
396 decode content from UTF-8.
397 """
398 if self.commit:
399 content = self.commit.get_file_content(self.path)
400 else:
401 content = self._content
402 return content
403
375 404 @LazyProperty
376 405 def content(self):
377 406 """
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -58,7 +58,7 b' def author_name(author):'
58 58 to get the username
59 59 """
60 60
61 if not author or not '@' in author:
61 if not author or '@' not in author:
62 62 return author
63 63 else:
64 64 return author.replace(author_email(author), '').replace('<', '')\
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -125,7 +125,35 b' class CommentsModel(BaseModel):'
125 125
126 126 return comment_versions
127 127
128 def get_unresolved_todos(self, pull_request, show_outdated=True):
128 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
129 qry = Session().query(ChangesetComment) \
130 .filter(ChangesetComment.repo == repo)
131
132 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
133 qry = qry.filter(ChangesetComment.comment_type == comment_type)
134
135 if user:
136 user = self._get_user(user)
137 if user:
138 qry = qry.filter(ChangesetComment.user_id == user.user_id)
139
140 if commit_id:
141 qry = qry.filter(ChangesetComment.revision == commit_id)
142
143 qry = qry.order_by(ChangesetComment.created_on)
144 return qry.all()
145
146 def get_repository_unresolved_todos(self, repo):
147 todos = Session().query(ChangesetComment) \
148 .filter(ChangesetComment.repo == repo) \
149 .filter(ChangesetComment.resolved_by == None) \
150 .filter(ChangesetComment.comment_type
151 == ChangesetComment.COMMENT_TYPE_TODO)
152 todos = todos.all()
153
154 return todos
155
156 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
129 157
130 158 todos = Session().query(ChangesetComment) \
131 159 .filter(ChangesetComment.pull_request == pull_request) \
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -48,7 +48,7 b' from sqlalchemy.ext.hybrid import hybrid'
48 48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 49 from sqlalchemy.dialects.mysql import LONGTEXT
50 50 from zope.cachedescriptors.property import Lazy as LazyProperty
51
51 from pyramid import compat
52 52 from pyramid.threadlocal import get_current_request
53 53
54 54 from rhodecode.translation import _
@@ -732,8 +732,6 b' class User(Base, BaseModel):'
732 732 if not auth_token:
733 733 return False
734 734
735 crypto_backend = auth.crypto_backend()
736
737 735 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
738 736 tokens_q = UserApiKeys.query()\
739 737 .filter(UserApiKeys.user_id == self.user_id)\
@@ -742,39 +740,42 b' class User(Base, BaseModel):'
742 740
743 741 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
744 742
745 plain_tokens = []
746 hash_tokens = []
747
748 user_tokens = tokens_q.all()
749 log.debug('Found %s user tokens to check for authentication', len(user_tokens))
750 for token in user_tokens:
751 log.debug('AUTH_TOKEN: checking if user token with id `%s` matches',
752 token.user_api_key_id)
753 # verify scope first, since it's way faster than hash calculation of
754 # encrypted tokens
755 if token.repo_id:
756 # token has a scope, we need to verify it
757 if scope_repo_id != token.repo_id:
743 crypto_backend = auth.crypto_backend()
744 enc_token_map = {}
745 plain_token_map = {}
746 for token in tokens_q:
747 if token.api_key.startswith(crypto_backend.ENC_PREF):
748 enc_token_map[token.api_key] = token
749 else:
750 plain_token_map[token.api_key] = token
751 log.debug(
752 'Found %s plain and %s encrypted user tokens to check for authentication',
753 len(plain_token_map), len(enc_token_map))
754
755 # plain token match comes first
756 match = plain_token_map.get(auth_token)
757
758 # check encrypted tokens now
759 if not match:
760 for token_hash, token in enc_token_map.items():
761 # NOTE(marcink): this is expensive to calculate, but most secure
762 if crypto_backend.hash_check(auth_token, token_hash):
763 match = token
764 break
765
766 if match:
767 log.debug('Found matching token %s', match)
768 if match.repo_id:
769 log.debug('Found scope, checking for scope match of token %s', match)
770 if match.repo_id == scope_repo_id:
771 return True
772 else:
758 773 log.debug(
759 774 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
760 775 'and calling scope is:%s, skipping further checks',
761 token.repo, scope_repo_id)
762 # token has a scope, and it doesn't match, skip token
763 continue
764
765 if token.api_key.startswith(crypto_backend.ENC_PREF):
766 hash_tokens.append(token.api_key)
776 match.repo, scope_repo_id)
777 return False
767 778 else:
768 plain_tokens.append(token.api_key)
769
770 is_plain_match = auth_token in plain_tokens
771 if is_plain_match:
772 return True
773
774 for hashed in hash_tokens:
775 # NOTE(marcink): this is expensive to calculate, but most secure
776 match = crypto_backend.hash_check(auth_token, hashed)
777 if match:
778 779 return True
779 780
780 781 return False
@@ -1130,10 +1131,10 b' class UserApiKeys(Base, BaseModel):'
1130 1131
1131 1132 def _get_scope(self):
1132 1133 if self.repo:
1133 return repr(self.repo)
1134 return 'Repository: {}'.format(self.repo.repo_name)
1134 1135 if self.repo_group:
1135 return repr(self.repo_group) + ' (recursive)'
1136 return 'global'
1136 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1137 return 'Global'
1137 1138
1138 1139 @property
1139 1140 def scope_humanized(self):
@@ -2240,7 +2241,7 b' class Repository(Base, BaseModel):'
2240 2241 warnings.warn("Use get_commit", DeprecationWarning)
2241 2242 commit_id = None
2242 2243 commit_idx = None
2243 if isinstance(rev, basestring):
2244 if isinstance(rev, compat.string_types):
2244 2245 commit_id = rev
2245 2246 else:
2246 2247 commit_idx = rev
@@ -2276,7 +2277,7 b' class Repository(Base, BaseModel):'
2276 2277 # use no-cache version here
2277 2278 scm_repo = self.scm_instance(cache=False, config=config)
2278 2279
2279 empty = scm_repo.is_empty()
2280 empty = not scm_repo or scm_repo.is_empty()
2280 2281 if not empty:
2281 2282 cs_cache = scm_repo.get_commit(
2282 2283 pre_load=["author", "date", "message", "parents"])
@@ -2459,7 +2460,6 b' class RepoGroup(Base, BaseModel):'
2459 2460 __tablename__ = 'groups'
2460 2461 __table_args__ = (
2461 2462 UniqueConstraint('group_name', 'group_parent_id'),
2462 CheckConstraint('group_id != group_parent_id'),
2463 2463 base_table_args,
2464 2464 )
2465 2465 __mapper_args__ = {'order_by': 'group_name'}
@@ -2480,8 +2480,7 b' class RepoGroup(Base, BaseModel):'
2480 2480 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2481 2481 parent_group = relationship('RepoGroup', remote_side=group_id)
2482 2482 user = relationship('User')
2483 integrations = relationship('Integration',
2484 cascade="all, delete, delete-orphan")
2483 integrations = relationship('Integration', cascade="all, delete, delete-orphan")
2485 2484
2486 2485 def __init__(self, group_name='', parent_group=None):
2487 2486 self.group_name = group_name
@@ -2491,6 +2490,16 b' class RepoGroup(Base, BaseModel):'
2491 2490 return u"<%s('id:%s:%s')>" % (
2492 2491 self.__class__.__name__, self.group_id, self.group_name)
2493 2492
2493 @validates('group_parent_id')
2494 def validate_group_parent_id(self, key, val):
2495 """
2496 Check cycle references for a parent group to self
2497 """
2498 if self.group_id and val:
2499 assert val != self.group_id
2500
2501 return val
2502
2494 2503 @hybrid_property
2495 2504 def description_safe(self):
2496 2505 from rhodecode.lib import helpers as h
@@ -3411,7 +3420,10 b' class ChangesetComment(Base, BaseModel):'
3411 3420
3412 3421 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3413 3422 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3414 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3423
3424 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3425 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3426
3415 3427 author = relationship('User', lazy='joined')
3416 3428 repo = relationship('Repository')
3417 3429 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
@@ -3494,7 +3506,8 b' class ChangesetComment(Base, BaseModel):'
3494 3506 'comment_f_path': comment.f_path,
3495 3507 'comment_lineno': comment.line_no,
3496 3508 'comment_author': comment.author,
3497 'comment_created_on': comment.created_on
3509 'comment_created_on': comment.created_on,
3510 'comment_resolved_by': self.resolved
3498 3511 }
3499 3512 return data
3500 3513
@@ -3568,6 +3581,32 b' class ChangesetStatus(Base, BaseModel):'
3568 3581 return data
3569 3582
3570 3583
3584 class _SetState(object):
3585 """
3586 Context processor allowing changing state for sensitive operation such as
3587 pull request update or merge
3588 """
3589
3590 def __init__(self, pull_request, pr_state, back_state=None):
3591 self._pr = pull_request
3592 self._org_state = back_state or pull_request.pull_request_state
3593 self._pr_state = pr_state
3594
3595 def __enter__(self):
3596 log.debug('StateLock: entering set state context, setting state to: `%s`',
3597 self._pr_state)
3598 self._pr.pull_request_state = self._pr_state
3599 Session().add(self._pr)
3600 Session().commit()
3601
3602 def __exit__(self, exc_type, exc_val, exc_tb):
3603 log.debug('StateLock: exiting set state context, setting state to: `%s`',
3604 self._org_state)
3605 self._pr.pull_request_state = self._org_state
3606 Session().add(self._pr)
3607 Session().commit()
3608
3609
3571 3610 class _PullRequestBase(BaseModel):
3572 3611 """
3573 3612 Common attributes of pull request and version entries.
@@ -3578,6 +3617,12 b' class _PullRequestBase(BaseModel):'
3578 3617 STATUS_OPEN = u'open'
3579 3618 STATUS_CLOSED = u'closed'
3580 3619
3620 # available states
3621 STATE_CREATING = u'creating'
3622 STATE_UPDATING = u'updating'
3623 STATE_MERGING = u'merging'
3624 STATE_CREATED = u'created'
3625
3581 3626 title = Column('title', Unicode(255), nullable=True)
3582 3627 description = Column(
3583 3628 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
@@ -3593,6 +3638,8 b' class _PullRequestBase(BaseModel):'
3593 3638 'updated_on', DateTime(timezone=False), nullable=False,
3594 3639 default=datetime.datetime.now)
3595 3640
3641 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3642
3596 3643 @declared_attr
3597 3644 def user_id(cls):
3598 3645 return Column(
@@ -3610,7 +3657,33 b' class _PullRequestBase(BaseModel):'
3610 3657 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3611 3658 nullable=False)
3612 3659
3613 source_ref = Column('org_ref', Unicode(255), nullable=False)
3660 _source_ref = Column('org_ref', Unicode(255), nullable=False)
3661
3662 @hybrid_property
3663 def source_ref(self):
3664 return self._source_ref
3665
3666 @source_ref.setter
3667 def source_ref(self, val):
3668 parts = (val or '').split(':')
3669 if len(parts) != 3:
3670 raise ValueError(
3671 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3672 self._source_ref = safe_unicode(val)
3673
3674 _target_ref = Column('other_ref', Unicode(255), nullable=False)
3675
3676 @hybrid_property
3677 def target_ref(self):
3678 return self._target_ref
3679
3680 @target_ref.setter
3681 def target_ref(self, val):
3682 parts = (val or '').split(':')
3683 if len(parts) != 3:
3684 raise ValueError(
3685 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3686 self._target_ref = safe_unicode(val)
3614 3687
3615 3688 @declared_attr
3616 3689 def target_repo_id(cls):
@@ -3619,7 +3692,6 b' class _PullRequestBase(BaseModel):'
3619 3692 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3620 3693 nullable=False)
3621 3694
3622 target_ref = Column('other_ref', Unicode(255), nullable=False)
3623 3695 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3624 3696
3625 3697 # TODO: dan: rename column to last_merge_source_rev
@@ -3692,7 +3764,8 b' class _PullRequestBase(BaseModel):'
3692 3764 def shadow_merge_ref(self, ref):
3693 3765 self._shadow_merge_ref = self.reference_to_unicode(ref)
3694 3766
3695 def unicode_to_reference(self, raw):
3767 @staticmethod
3768 def unicode_to_reference(raw):
3696 3769 """
3697 3770 Convert a unicode (or string) to a reference object.
3698 3771 If unicode evaluates to False it returns None.
@@ -3703,7 +3776,8 b' class _PullRequestBase(BaseModel):'
3703 3776 else:
3704 3777 return None
3705 3778
3706 def reference_to_unicode(self, ref):
3779 @staticmethod
3780 def reference_to_unicode(ref):
3707 3781 """
3708 3782 Convert a reference object to unicode.
3709 3783 If reference is None it returns None.
@@ -3740,6 +3814,7 b' class _PullRequestBase(BaseModel):'
3740 3814 'title': pull_request.title,
3741 3815 'description': pull_request.description,
3742 3816 'status': pull_request.status,
3817 'state': pull_request.pull_request_state,
3743 3818 'created_on': pull_request.created_on,
3744 3819 'updated_on': pull_request.updated_on,
3745 3820 'commit_ids': pull_request.revisions,
@@ -3780,6 +3855,20 b' class _PullRequestBase(BaseModel):'
3780 3855
3781 3856 return data
3782 3857
3858 def set_state(self, pull_request_state, final_state=None):
3859 """
3860 # goes from initial state to updating to initial state.
3861 # initial state can be changed by specifying back_state=
3862 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
3863 pull_request.merge()
3864
3865 :param pull_request_state:
3866 :param final_state:
3867
3868 """
3869
3870 return _SetState(self, pull_request_state, back_state=final_state)
3871
3783 3872
3784 3873 class PullRequest(Base, _PullRequestBase):
3785 3874 __tablename__ = 'pull_requests'
@@ -3952,7 +4041,7 b' class PullRequestReviewers(Base, BaseMod'
3952 4041 @reasons.setter
3953 4042 def reasons(self, val):
3954 4043 val = val or []
3955 if any(not isinstance(x, basestring) for x in val):
4044 if any(not isinstance(x, compat.string_types) for x in val):
3956 4045 raise Exception('invalid reasons type, must be list of strings')
3957 4046 self._reasons = val
3958 4047
@@ -4718,6 +4807,135 b' class UserGroupToRepoBranchPermission(Ba'
4718 4807 self.user_group_repo_to_perm, self.branch_pattern)
4719 4808
4720 4809
4810 class UserBookmark(Base, BaseModel):
4811 __tablename__ = 'user_bookmarks'
4812 __table_args__ = (
4813 UniqueConstraint('user_id', 'bookmark_repo_id'),
4814 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
4815 UniqueConstraint('user_id', 'bookmark_position'),
4816 base_table_args
4817 )
4818
4819 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
4820 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
4821 position = Column("bookmark_position", Integer(), nullable=False)
4822 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
4823 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
4824 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4825
4826 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
4827 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
4828
4829 user = relationship("User")
4830
4831 repository = relationship("Repository")
4832 repository_group = relationship("RepoGroup")
4833
4834 @classmethod
4835 def get_by_position_for_user(cls, position, user_id):
4836 return cls.query() \
4837 .filter(UserBookmark.user_id == user_id) \
4838 .filter(UserBookmark.position == position).scalar()
4839
4840 @classmethod
4841 def get_bookmarks_for_user(cls, user_id):
4842 return cls.query() \
4843 .filter(UserBookmark.user_id == user_id) \
4844 .options(joinedload(UserBookmark.repository)) \
4845 .options(joinedload(UserBookmark.repository_group)) \
4846 .order_by(UserBookmark.position.asc()) \
4847 .all()
4848
4849 def __unicode__(self):
4850 return u'<UserBookmark(%d @ %r)>' % (self.position, self.redirect_url)
4851
4852
4853 class FileStore(Base, BaseModel):
4854 __tablename__ = 'file_store'
4855 __table_args__ = (
4856 base_table_args
4857 )
4858
4859 file_store_id = Column('file_store_id', Integer(), primary_key=True)
4860 file_uid = Column('file_uid', String(1024), nullable=False)
4861 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
4862 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
4863 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
4864
4865 # sha256 hash
4866 file_hash = Column('file_hash', String(512), nullable=False)
4867 file_size = Column('file_size', Integer(), nullable=False)
4868
4869 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4870 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
4871 accessed_count = Column('accessed_count', Integer(), default=0)
4872
4873 enabled = Column('enabled', Boolean(), nullable=False, default=True)
4874
4875 # if repo/repo_group reference is set, check for permissions
4876 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
4877
4878 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4879 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
4880
4881 # scope limited to user, which requester have access to
4882 scope_user_id = Column(
4883 'scope_user_id', Integer(), ForeignKey('users.user_id'),
4884 nullable=True, unique=None, default=None)
4885 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
4886
4887 # scope limited to user group, which requester have access to
4888 scope_user_group_id = Column(
4889 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
4890 nullable=True, unique=None, default=None)
4891 user_group = relationship('UserGroup', lazy='joined')
4892
4893 # scope limited to repo, which requester have access to
4894 scope_repo_id = Column(
4895 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4896 nullable=True, unique=None, default=None)
4897 repo = relationship('Repository', lazy='joined')
4898
4899 # scope limited to repo group, which requester have access to
4900 scope_repo_group_id = Column(
4901 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
4902 nullable=True, unique=None, default=None)
4903 repo_group = relationship('RepoGroup', lazy='joined')
4904
4905 @classmethod
4906 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
4907 file_description='', enabled=True, check_acl=True,
4908 user_id=None, scope_repo_id=None, scope_repo_group_id=None):
4909
4910 store_entry = FileStore()
4911 store_entry.file_uid = file_uid
4912 store_entry.file_display_name = file_display_name
4913 store_entry.file_org_name = filename
4914 store_entry.file_size = file_size
4915 store_entry.file_hash = file_hash
4916 store_entry.file_description = file_description
4917
4918 store_entry.check_acl = check_acl
4919 store_entry.enabled = enabled
4920
4921 store_entry.user_id = user_id
4922 store_entry.scope_repo_id = scope_repo_id
4923 store_entry.scope_repo_group_id = scope_repo_group_id
4924 return store_entry
4925
4926 @classmethod
4927 def bump_access_counter(cls, file_uid, commit=True):
4928 FileStore().query()\
4929 .filter(FileStore.file_uid == file_uid)\
4930 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
4931 FileStore.accessed_on: datetime.datetime.now()})
4932 if commit:
4933 Session().commit()
4934
4935 def __repr__(self):
4936 return '<FileStore({})>'.format(self.file_store_id)
4937
4938
4721 4939 class DbMigrateVersion(Base, BaseModel):
4722 4940 __tablename__ = 'db_migrate_version'
4723 4941 __table_args__ = (
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -30,10 +30,11 b' import datetime'
30 30 import urllib
31 31 import collections
32 32
33 from pyramid import compat
33 34 from pyramid.threadlocal import get_current_request
34 35
35 36 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
37 from rhodecode.translation import lazy_ugettext
37 38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 39 from rhodecode.lib import audit_logger
39 40 from rhodecode.lib.compat import OrderedDict
@@ -75,43 +76,6 b' class PullRequestModel(BaseModel):'
75 76
76 77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
77 78
78 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
88 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
91 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
106 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
109 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
113 }
114
115 79 UPDATE_STATUS_MESSAGES = {
116 80 UpdateFailureReason.NONE: lazy_ugettext(
117 81 'Pull request update successful.'),
@@ -175,7 +139,7 b' class PullRequestModel(BaseModel):'
175 139
176 140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
177 141 opened_by=None, order_by=None,
178 order_dir='desc'):
142 order_dir='desc', only_created=True):
179 143 repo = None
180 144 if repo_name:
181 145 repo = self._get_repo(repo_name)
@@ -196,9 +160,14 b' class PullRequestModel(BaseModel):'
196 160 if opened_by:
197 161 q = q.filter(PullRequest.user_id.in_(opened_by))
198 162
163 # only get those that are in "created" state
164 if only_created:
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
166
199 167 if order_by:
200 168 order_map = {
201 169 'name_raw': PullRequest.pull_request_id,
170 'id': PullRequest.pull_request_id,
202 171 'title': PullRequest.title,
203 172 'updated_on_raw': PullRequest.updated_on,
204 173 'target_repo': PullRequest.target_repo_id
@@ -466,7 +435,7 b' class PullRequestModel(BaseModel):'
466 435 pull_request.description_renderer = description_renderer
467 436 pull_request.author = created_by_user
468 437 pull_request.reviewer_data = reviewer_data
469
438 pull_request.pull_request_state = pull_request.STATE_CREATING
470 439 Session().add(pull_request)
471 440 Session().flush()
472 441
@@ -534,7 +503,14 b' class PullRequestModel(BaseModel):'
534 503 # that for large repos could be long resulting in long row locks
535 504 Session().commit()
536 505
537 # prepare workspace, and run initial merge simulation
506 # prepare workspace, and run initial merge simulation. Set state during that
507 # operation
508 pull_request = PullRequest.get(pull_request.pull_request_id)
509
510 # set as merging, for simulation, and if finished to created so we mark
511 # simulation is working fine
512 with pull_request.set_state(PullRequest.STATE_MERGING,
513 final_state=PullRequest.STATE_CREATED):
538 514 MergeCheck.validate(
539 515 pull_request, auth_user=auth_user, translator=translator)
540 516
@@ -602,8 +578,7 b' class PullRequestModel(BaseModel):'
602 578 extras['user_agent'] = 'internal-merge'
603 579 merge_state = self._merge_pull_request(pull_request, user, extras)
604 580 if merge_state.executed:
605 log.debug(
606 "Merge was successful, updating the pull request comments.")
581 log.debug("Merge was successful, updating the pull request comments.")
607 582 self._comment_and_close_pr(pull_request, user, merge_state)
608 583
609 584 self._log_audit_action(
@@ -698,8 +673,7 b' class PullRequestModel(BaseModel):'
698 673 target_ref_id = pull_request.target_ref_parts.commit_id
699 674
700 675 if not self.has_valid_update_type(pull_request):
701 log.debug(
702 "Skipping update of pull request %s due to ref type: %s",
676 log.debug("Skipping update of pull request %s due to ref type: %s",
703 677 pull_request, source_ref_type)
704 678 return UpdateResponse(
705 679 executed=False,
@@ -858,6 +832,7 b' class PullRequestModel(BaseModel):'
858 832 version.title = pull_request.title
859 833 version.description = pull_request.description
860 834 version.status = pull_request.status
835 version.pull_request_state = pull_request.pull_request_state
861 836 version.created_on = datetime.datetime.now()
862 837 version.updated_on = pull_request.updated_on
863 838 version.user_id = pull_request.user_id
@@ -1028,7 +1003,7 b' class PullRequestModel(BaseModel):'
1028 1003
1029 1004 reviewers = {}
1030 1005 for user_id, reasons, mandatory, rules in reviewer_data:
1031 if isinstance(user_id, (int, basestring)):
1006 if isinstance(user_id, (int, compat.string_types)):
1032 1007 user_id = self._get_user(user_id).user_id
1033 1008 reviewers[user_id] = {
1034 1009 'reasons': reasons, 'mandatory': mandatory}
@@ -1263,8 +1238,7 b' class PullRequestModel(BaseModel):'
1263 1238 pull_request,
1264 1239 force_shadow_repo_refresh=force_shadow_repo_refresh)
1265 1240 log.debug("Merge response: %s", resp)
1266 status = resp.possible, self.merge_status_message(
1267 resp.failure_reason)
1241 status = resp.possible, resp.merge_status_message
1268 1242 except NotImplementedError:
1269 1243 status = False, _('Pull request merging is not supported.')
1270 1244
@@ -1306,21 +1280,23 b' class PullRequestModel(BaseModel):'
1306 1280 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1307 1281 pull_request.pull_request_id, force_shadow_repo_refresh)
1308 1282 target_vcs = pull_request.target_repo.scm_instance()
1309
1310 1283 # Refresh the target reference.
1311 1284 try:
1312 1285 target_ref = self._refresh_reference(
1313 1286 pull_request.target_ref_parts, target_vcs)
1314 1287 except CommitDoesNotExistError:
1315 1288 merge_state = MergeResponse(
1316 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1289 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1290 metadata={'target_ref': pull_request.target_ref_parts})
1317 1291 return merge_state
1318 1292
1319 1293 target_locked = pull_request.target_repo.locked
1320 1294 if target_locked and target_locked[0]:
1321 log.debug("The target repository is locked.")
1295 locked_by = 'user:{}'.format(target_locked[0])
1296 log.debug("The target repository is locked by %s.", locked_by)
1322 1297 merge_state = MergeResponse(
1323 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1298 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1299 metadata={'locked_by': locked_by})
1324 1300 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1325 1301 pull_request, target_ref):
1326 1302 log.debug("Refreshing the merge status of the repository.")
@@ -1378,12 +1354,6 b' class PullRequestModel(BaseModel):'
1378 1354 workspace_id = 'pr-%s' % pull_request.pull_request_id
1379 1355 return workspace_id
1380 1356
1381 def merge_status_message(self, status_code):
1382 """
1383 Return a human friendly error message for the given merge status code.
1384 """
1385 return self.MERGE_STATUS_MESSAGES[status_code]
1386
1387 1357 def generate_repo_data(self, repo, commit_id=None, branch=None,
1388 1358 bookmark=None, translator=None):
1389 1359 from rhodecode.model.repo import RepoModel
@@ -1664,17 +1634,16 b' class MergeCheck(object):'
1664 1634
1665 1635 msg = _('Pull request reviewer approval is pending.')
1666 1636
1667 merge_check.push_error(
1668 'warning', msg, cls.REVIEW_CHECK, review_status)
1637 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1669 1638
1670 1639 if fail_early:
1671 1640 return merge_check
1672 1641
1673 1642 # left over TODOs
1674 todos = CommentsModel().get_unresolved_todos(pull_request)
1643 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1675 1644 if todos:
1676 1645 log.debug("MergeCheck: cannot merge, {} "
1677 "unresolved todos left.".format(len(todos)))
1646 "unresolved TODOs left.".format(len(todos)))
1678 1647
1679 1648 if len(todos) == 1:
1680 1649 msg = _('Cannot merge, {} TODO still not resolved.').format(
@@ -1695,8 +1664,7 b' class MergeCheck(object):'
1695 1664 merge_check.merge_possible = merge_status
1696 1665 merge_check.merge_msg = msg
1697 1666 if not merge_status:
1698 log.debug(
1699 "MergeCheck: cannot merge, pull request merge not possible.")
1667 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1700 1668 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1701 1669
1702 1670 if fail_early:
@@ -1727,6 +1695,7 b' class MergeCheck(object):'
1727 1695 close_branch = model._close_branch_before_merging(pull_request)
1728 1696 if close_branch:
1729 1697 repo_type = pull_request.target_repo.repo_type
1698 close_msg = ''
1730 1699 if repo_type == 'hg':
1731 1700 close_msg = _('Source branch will be closed after merge.')
1732 1701 elif repo_type == 'git':
@@ -1739,6 +1708,7 b' class MergeCheck(object):'
1739 1708
1740 1709 return merge_details
1741 1710
1711
1742 1712 ChangeTuple = collections.namedtuple(
1743 1713 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1744 1714
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -462,8 +462,7 b' class RepoModel(BaseModel):'
462 462 UserGroupRepoToPerm.create(
463 463 perm.users_group, new_repo, perm.permission)
464 464 # in case we copy permissions and also set this repo to private
465 # override the default user permission to make it a private
466 # repo
465 # override the default user permission to make it a private repo
467 466 if private:
468 467 RepoModel(self.sa).grant_user_permission(
469 468 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
@@ -485,8 +484,7 b' class RepoModel(BaseModel):'
485 484 perm_name = perm.permission.permission_name.replace(
486 485 'group.', 'repository.')
487 486 perm_obj = Permission.get_by_key(perm_name)
488 UserGroupRepoToPerm.create(
489 perm.users_group, new_repo, perm_obj)
487 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
490 488
491 489 if private:
492 490 RepoModel(self.sa).grant_user_permission(
@@ -497,8 +495,7 b' class RepoModel(BaseModel):'
497 495 self.sa.add(perm_obj)
498 496
499 497 # now automatically start following this repository as owner
500 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
501 owner.user_id)
498 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id)
502 499
503 500 # we need to flush here, in order to check if database won't
504 501 # throw any exceptions, create filesystem dirs at the very end
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -513,6 +513,8 b' class ScmModel(BaseModel):'
513 513 :param commit_id: commit id for which to list nodes
514 514 :param root_path: root path to list
515 515 :param flat: return as a list, if False returns a dict with description
516 :param extended_info: show additional info such as md5, binary, size etc
517 :param content: add nodes content to the return data
516 518 :param max_file_bytes: will not return file contents over this limit
517 519
518 520 """
@@ -523,15 +525,14 b' class ScmModel(BaseModel):'
523 525 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
524 526 root_path = root_path.lstrip('/')
525 527 for __, dirs, files in commit.walk(root_path):
528
526 529 for f in files:
527 530 _content = None
528 _data = f.unicode_path
529 over_size_limit = (max_file_bytes is not None
530 and f.size > max_file_bytes)
531 _data = f_name = f.unicode_path
531 532
532 533 if not flat:
533 534 _data = {
534 "name": h.escape(f.unicode_path),
535 "name": h.escape(f_name),
535 536 "type": "file",
536 537 }
537 538 if extended_info:
@@ -545,6 +546,8 b' class ScmModel(BaseModel):'
545 546 })
546 547
547 548 if content:
549 over_size_limit = (max_file_bytes is not None
550 and f.size > max_file_bytes)
548 551 full_content = None
549 552 if not f.is_binary and not over_size_limit:
550 553 full_content = safe_str(f.content)
@@ -553,11 +556,12 b' class ScmModel(BaseModel):'
553 556 "content": full_content,
554 557 })
555 558 _files.append(_data)
559
556 560 for d in dirs:
557 _data = d.unicode_path
561 _data = d_name = d.unicode_path
558 562 if not flat:
559 563 _data = {
560 "name": h.escape(d.unicode_path),
564 "name": h.escape(d_name),
561 565 "type": "dir",
562 566 }
563 567 if extended_info:
@@ -573,11 +577,114 b' class ScmModel(BaseModel):'
573 577 })
574 578 _dirs.append(_data)
575 579 except RepositoryError:
576 log.debug("Exception in get_nodes", exc_info=True)
580 log.exception("Exception in get_nodes")
577 581 raise
578 582
579 583 return _dirs, _files
580 584
585 def get_node(self, repo_name, commit_id, file_path,
586 extended_info=False, content=False, max_file_bytes=None, cache=True):
587 """
588 retrieve single node from commit
589 """
590 try:
591
592 _repo = self._get_repo(repo_name)
593 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
594
595 file_node = commit.get_node(file_path)
596 if file_node.is_dir():
597 raise RepositoryError('The given path is a directory')
598
599 _content = None
600 f_name = file_node.unicode_path
601
602 file_data = {
603 "name": h.escape(f_name),
604 "type": "file",
605 }
606
607 if extended_info:
608 file_data.update({
609 "extension": file_node.extension,
610 "mimetype": file_node.mimetype,
611 })
612
613 if cache:
614 md5 = file_node.md5
615 is_binary = file_node.is_binary
616 size = file_node.size
617 else:
618 is_binary, md5, size, _content = file_node.metadata_uncached()
619
620 file_data.update({
621 "md5": md5,
622 "binary": is_binary,
623 "size": size,
624 })
625
626 if content and cache:
627 # get content + cache
628 size = file_node.size
629 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
630 full_content = None
631 if not file_node.is_binary and not over_size_limit:
632 full_content = safe_unicode(file_node.content)
633
634 file_data.update({
635 "content": full_content,
636 })
637 elif content:
638 # get content *without* cache
639 if _content is None:
640 is_binary, md5, size, _content = file_node.metadata_uncached()
641
642 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
643 full_content = None
644 if not is_binary and not over_size_limit:
645 full_content = safe_unicode(_content)
646
647 file_data.update({
648 "content": full_content,
649 })
650
651 except RepositoryError:
652 log.exception("Exception in get_node")
653 raise
654
655 return file_data
656
657 def get_fts_data(self, repo_name, commit_id, root_path='/'):
658 """
659 Fetch node tree for usage in full text search
660 """
661
662 tree_info = list()
663
664 try:
665 _repo = self._get_repo(repo_name)
666 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
667 root_path = root_path.lstrip('/')
668 for __, dirs, files in commit.walk(root_path):
669
670 for f in files:
671 is_binary, md5, size, _content = f.metadata_uncached()
672 _data = {
673 "name": f.unicode_path,
674 "md5": md5,
675 "extension": f.extension,
676 "binary": is_binary,
677 "size": size
678 }
679
680 tree_info.append(_data)
681
682 except RepositoryError:
683 log.exception("Exception in get_nodes")
684 raise
685
686 return tree_info
687
581 688 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
582 689 author=None, trigger_push_hook=True):
583 690 """
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -350,18 +350,26 b' class IssueTrackerSettingsModel(object):'
350 350 uid = k[len(prefix_match):]
351 351 issuetracker_entries[uid] = None
352 352
353 def url_cleaner(input_str):
354 input_str = input_str.replace('"', '').replace("'", '')
355 input_str = bleach.clean(input_str, strip=True)
356 return input_str
357
353 358 # populate
354 359 for uid in issuetracker_entries:
360 url_data = qs.get(self._get_keyname('url', uid, 'rhodecode_'))
361
355 362 issuetracker_entries[uid] = AttributeDict({
356 363 'pat': qs.get(
357 364 self._get_keyname('pat', uid, 'rhodecode_')),
358 'url': bleach.clean(
365 'url': url_cleaner(
359 366 qs.get(self._get_keyname('url', uid, 'rhodecode_')) or ''),
360 367 'pref': bleach.clean(
361 368 qs.get(self._get_keyname('pref', uid, 'rhodecode_')) or ''),
362 369 'desc': qs.get(
363 370 self._get_keyname('desc', uid, 'rhodecode_')),
364 371 })
372
365 373 return issuetracker_entries
366 374
367 375 def get_global_settings(self, cache=False):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2013-2018 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -20,6 +20,7 b''
20 20
21 21 import logging
22 22 import traceback
23 from pyramid import compat
23 24
24 25 from rhodecode.lib.utils2 import safe_str, safe_unicode
25 26 from rhodecode.lib.exceptions import (
@@ -247,7 +248,7 b' class UserGroupModel(BaseModel):'
247 248 # handle owner change
248 249 if 'user' in form_data:
249 250 owner = form_data['user']
250 if isinstance(owner, basestring):
251 if isinstance(owner, compat.string_types):
251 252 owner = User.get_by_username(form_data['user'])
252 253
253 254 if not isinstance(owner, User):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -19,6 +19,7 b''
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import unicodedata
22 from pyramid import compat
22 23
23 24
24 25 def strip_preparer(value):
@@ -83,6 +84,6 b' def unique_list_from_str_preparer(value)'
83 84 """
84 85 from rhodecode.lib.utils2 import aslist
85 86
86 if isinstance(value, basestring):
87 if isinstance(value, compat.string_types):
87 88 value = aslist(value, ',')
88 89 return unique_list_preparer(value) No newline at end of file
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -34,6 +34,9 b' class SearchParamsSchema(colander.Mappin'
34 34 colander.String(),
35 35 missing='newfirst',
36 36 validator=colander.OneOf(['oldfirst', 'newfirst']))
37 search_max_lines = colander.SchemaNode(
38 colander.Integer(),
39 missing=10)
37 40 page_limit = colander.SchemaNode(
38 41 colander.Integer(),
39 42 missing=10,
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -21,6 +21,8 b''
21 21 import re
22 22
23 23 import colander
24 from pyramid import compat
25
24 26 from rhodecode.model.validation_schema import preparers
25 27 from rhodecode.model.db import User, UserGroup
26 28
@@ -106,7 +108,7 b' class StringBooleanType(colander.String)'
106 108 if isinstance(cstruct, bool):
107 109 return cstruct
108 110
109 if not isinstance(cstruct, basestring):
111 if not isinstance(cstruct, compat.string_types):
110 112 raise colander.Invalid(node, '%r is not a string' % cstruct)
111 113
112 114 value = cstruct.lower()
@@ -190,7 +192,7 b' class UserGroupType(UserOrUserGroupType)'
190 192
191 193 class StrOrIntType(colander.String):
192 194 def deserialize(self, node, cstruct):
193 if isinstance(cstruct, basestring):
195 if isinstance(cstruct, compat.string_types):
194 196 return super(StrOrIntType, self).deserialize(node, cstruct)
195 197 else:
196 198 return colander.Integer().deserialize(node, cstruct)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -37,6 +37,7 b' from formencode.validators import ('
37 37
38 38 from sqlalchemy.sql.expression import true
39 39 from sqlalchemy.util import OrderedSet
40 from pyramid import compat
40 41
41 42 from rhodecode.authentication import (
42 43 legacy_plugin_prefix, _import_legacy_plugin)
@@ -125,7 +126,7 b' def UniqueListFromString(localizer):'
125 126
126 127 class _validator(UniqueList(localizer)):
127 128 def _to_python(self, value, state):
128 if isinstance(value, basestring):
129 if isinstance(value, compat.string_types):
129 130 value = aslist(value, ',')
130 131 return super(_validator, self)._to_python(value, state)
131 132 return _validator
@@ -572,6 +572,7 b' div.annotatediv { margin-left: 2px; marg'
572 572 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
573 573 /* This can be generated with `pygmentize -S default -f html` */
574 574 .codehilite {
575 .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
575 576 .hll { background-color: #ffffcc }
576 577 .c { color: #408080; font-style: italic } /* Comment */
577 578 .err, .codehilite .err { border: none } /* Error */
@@ -640,6 +641,7 b' div.annotatediv { margin-left: 2px; marg'
640 641 .vi { color: #19177C } /* Name.Variable.Instance */
641 642 .vm { color: #19177C } /* Name.Variable.Magic */
642 643 .il { color: #666666 } /* Literal.Number.Integer.Long */
644
643 645 }
644 646
645 647 /* customized pre blocks for markdown/rst */
@@ -73,9 +73,6 b''
73 73 }
74 74
75 75 .sign-in-title {
76 h1 {
77 margin: 0;
78 }
79 76
80 77 h4 {
81 78 margin: @padding*2 0;
@@ -109,6 +106,12 b''
109 106 width: 100%;
110 107 margin: @padding 0;
111 108 }
109 .pwd_reset {
110 font-weight: normal;
111 }
112 .new_account {
113 font-weight: bold;
114 }
112 115 }
113 116 .register_message,
114 117 .activation_msg {
@@ -197,7 +200,27 b''
197 200 .user-menu.submenu {
198 201 right: 0;
199 202 left: auto;
203 min-width: 290px;
200 204 }
205
206
207 .user-menu {
208 .bookmark-items {
209 padding: 4px 2px;
210 color: @grey3;
211 border-bottom: @grey3;
212
213 a {
214 padding: 0 !important;
215 color: @rcblue !important;
216 }
217 }
218 a.bookmark-item {
219 color: @rcblue !important;
220 }
221 }
222
223
201 224 #quick_login {
202 225 left: auto;
203 226 right: 0;
@@ -2236,6 +2236,10 b' h3.files_location{'
2236 2236 clear: both;
2237 2237 margin: 0 0 @padding;
2238 2238 }
2239
2240 .search-tags {
2241 padding: 5px 0;
2242 }
2239 2243 }
2240 2244
2241 2245 div.search-feedback-items {
@@ -335,6 +335,7 b''
335 335 }
336 336 }
337 337 }
338
338 339 }
339 340
340 341
@@ -635,8 +636,10 b' ul#context-pages {'
635 636 border-bottom: 1px solid @grey4;
636 637 display: inline-block;
637 638 vertical-align: top;
638 margin-left: -7px;
639 background: @grey3;
639 background: inherit;
640 position: absolute;
641 right: 8px;
642 top: 9px;
640 643 }
641 644
642 645 .main_filter_input_box {
@@ -651,20 +654,32 b' ul#context-pages {'
651 654 background: @grey3;
652 655 border: 1px solid black;
653 656 position: absolute;
654 white-space: pre-wrap;
657 white-space: pre;
655 658 z-index: 9999;
656 659 color: @nav-grey;
657 660 margin: 1px 7px;
658 padding: 0 2px;
661 padding: 0 10px;
659 662 }
660 663
661 664 .main_filter_input {
662 665 padding: 5px;
663 min-width: 220px;
666 min-width: 260px;
664 667 color: @nav-grey;
665 668 background: @grey3;
666 669 min-height: 18px;
670
671
672 &:active {
673 color: @grey2 !important;
674 background: white !important;
667 675 }
676 &:focus {
677 color: @grey2 !important;
678 background: white !important;
679 }
680 }
681
682
668 683
669 684 .main_filter_input::placeholder {
670 685 color: @nav-grey;
@@ -255,7 +255,6 b''
255 255
256 256 .stats {
257 257 float: left;
258 width: 50%;
259 258 }
260 259 .stats-filename {
261 260 font-size: 120%;
@@ -266,10 +265,15 b''
266 265
267 266 .buttons {
268 267 float: right;
269 width: 50%;
270 268 text-align: right;
271 269 color: @grey4;
272 270 }
271
272 .file-container {
273 display: inline-block;
274 width: 100%;
275 }
276
273 277 }
274 278
275 279 #summary-menu-stats {
@@ -172,7 +172,9 b' table.dataTable {'
172 172 &.td-buttons {
173 173 padding: .3em 0;
174 174 }
175
175 &.td-align-top {
176 vertical-align: text-top
177 }
176 178 &.td-action {
177 179 // this is for the remove/delete/edit buttons
178 180 padding-right: 0;
@@ -166,7 +166,6 b' small,'
166 166
167 167 mark,
168 168 .mark {
169 background-color: @rclightblue;
170 169 padding: .2em;
171 170 }
172 171
@@ -301,6 +300,10 b' mark,'
301 300 margin-top: @padding;
302 301 }
303 302 }
303
304 .repo-group-desc {
305 padding: 8px 0px 0px 0px;
306 }
304 307 }
305 308
306 309 .title-main {
@@ -527,6 +530,10 b' address {'
527 530 font-family: @text-regular;
528 531 }
529 532
533 .help-block-inline {
534 margin: 0;
535 }
536
530 537 // help block text
531 538 .help-block {
532 539 display: block;
@@ -51,6 +51,38 b' function setRCMouseBindings(repoName, re'
51 51 Mousetrap.bind(['g G'], function(e) {
52 52 window.location = pyroutes.url('gists_show', {'public': 1});
53 53 });
54
55 Mousetrap.bind(['g 0'], function(e) {
56 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 0});
57 });
58 Mousetrap.bind(['g 1'], function(e) {
59 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 1});
60 });
61 Mousetrap.bind(['g 2'], function(e) {
62 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 2});
63 });
64 Mousetrap.bind(['g 3'], function(e) {
65 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 3});
66 });
67 Mousetrap.bind(['g 4'], function(e) {
68 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 4});
69 });
70 Mousetrap.bind(['g 5'], function(e) {
71 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 5});
72 });
73 Mousetrap.bind(['g 6'], function(e) {
74 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 6});
75 });
76 Mousetrap.bind(['g 7'], function(e) {
77 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 7});
78 });
79 Mousetrap.bind(['g 8'], function(e) {
80 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 8});
81 });
82 Mousetrap.bind(['g 9'], function(e) {
83 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 9});
84 });
85
54 86 Mousetrap.bind(['n g'], function(e) {
55 87 window.location = pyroutes.url('gists_new');
56 88 });
@@ -58,7 +90,7 b' function setRCMouseBindings(repoName, re'
58 90 window.location = pyroutes.url('repo_new');
59 91 });
60 92
61 if (repoName && repoName != '') {
93 if (repoName && repoName !== '') {
62 94 // nav in repo context
63 95 Mousetrap.bind(['g s'], function(e) {
64 96 window.location = pyroutes.url(
@@ -102,7 +102,8 b' function registerRCRoutes() {'
102 102 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
103 103 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
104 104 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
105 pyroutes.register('user_force_password_reset', '/_admin/users/%(user_id)s/password_reset', ['user_id']);
105 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
106 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
106 107 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
107 108 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
108 109 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
@@ -135,6 +136,8 b' function registerRCRoutes() {'
135 136 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
136 137 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
137 138 pyroutes.register('channelstream_proxy', '/_channelstream', []);
139 pyroutes.register('upload_file', '/_file_store/upload', []);
140 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
138 141 pyroutes.register('logout', '/_admin/logout', []);
139 142 pyroutes.register('reset_password', '/_admin/password_reset', []);
140 143 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
@@ -142,6 +145,7 b' function registerRCRoutes() {'
142 145 pyroutes.register('user_autocomplete_data', '/_users', []);
143 146 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
144 147 pyroutes.register('repo_list_data', '/_repos', []);
148 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
145 149 pyroutes.register('goto_switcher_data', '/_goto_data', []);
146 150 pyroutes.register('markup_preview', '/_markup_preview', []);
147 151 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
@@ -212,7 +216,7 b' function registerRCRoutes() {'
212 216 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
213 217 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
214 218 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
215 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
219 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
216 220 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
217 221 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
218 222 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
@@ -277,7 +281,9 b' function registerRCRoutes() {'
277 281 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
278 282 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
279 283 pyroutes.register('search', '/_admin/search', []);
280 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
284 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
285 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
286 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
281 287 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
282 288 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
283 289 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
@@ -296,6 +302,9 b' function registerRCRoutes() {'
296 302 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
297 303 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
298 304 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
305 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
306 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
307 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
299 308 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
300 309 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
301 310 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2016-2018 RhodeCode GmbH
1 // # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -76,6 +76,7 b' var showRepoSize = function(target, repo'
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 container.show();
79 80 if (!container.hasClass('loaded')) {
80 81 $.ajax({url: url})
81 82 .complete(function (data) {
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2016-2018 RhodeCode GmbH
1 // # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -137,11 +137,12 b' var fileBrowserListeners = function(node'
137 137 var new_url = url_base.replace('__FPATH__',n);
138 138
139 139 var typeObj = {
140 dir: 'icon-folder browser-dir',
141 file: 'icon-file browser-file'
140 dir: 'icon-directory browser-dir',
141 file: 'icon-file-text browser-file'
142 142 };
143
143 144 var typeIcon = '<i class="{0}"></i>'.format(typeObj[t]);
144 match.push('<tr class="browser-result"><td><a class="browser-{0} pjax-link" href="{1}">{2}{3}</a></td><td colspan="5"></td></tr>'.format(t,new_url,typeIcon, n_hl));
145 match.push('<tr class="browser-result"><td><a class="pjax-link" href="{0}">{1}{2}</a></td><td colspan="5"></td></tr>'.format(new_url,typeIcon, n_hl));
145 146 }
146 147 }
147 148 if(results.length > limit){
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 // # Copyright (C) 2010-2018 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -66,6 +66,8 b''
66 66 <div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
67 67 %elif node.widget == "select":
68 68 ${h.select(node.name, defaults.get(node.name), node.validator.choices, class_="select2AuthSetting")}
69 %elif node.widget == "select_with_labels":
70 ${h.select(node.name, defaults.get(node.name), node.choices, class_="select2AuthSetting")}
69 71 %elif node.widget == "textarea":
70 72 <div class="textarea" style="margin-left: 0px">${h.textarea(node.name, defaults.get(node.name), rows=10)}</div>
71 73 %elif node.widget == "readonly":
@@ -4,17 +4,9 b''
4 4
5 5 <%def name="breadcrumbs_links()">
6 6 %if c.repo:
7 ${h.link_to('Settings',h.route_path('edit_repo', repo_name=c.repo.repo_name))}
8 &raquo;
9 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))}
7 ${_('Settings')}
10 8 %elif c.repo_group:
11 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
12 &raquo;
13 ${h.link_to(_('Repository Groups'),h.route_path('repo_groups'))}
14 &raquo;
15 ${h.link_to(c.repo_group.group_name,h.route_path('edit_repo_group', repo_group_name=c.repo_group.group_name))}
16 &raquo;
17 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))}
9 ${_('Settings')}
18 10 %else:
19 11 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
20 12 &raquo;
@@ -28,6 +28,7 b''
28 28 <ul class="nav nav-pills nav-stacked">
29 29 <li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
30 30 <li class="${'active' if c.active=='password' else ''}"><a href="${h.route_path('my_account_password')}">${_('Password')}</a></li>
31 <li class="${'active' if c.active=='bookmarks' else ''}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
31 32 <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
32 33 <li class="${'active' if c.active in ['ssh_keys', 'ssh_keys_generate'] else ''}"><a href="${h.route_path('my_account_ssh_keys')}">${_('SSH Keys')}</a></li>
33 34 <li class="${'active' if c.active=='user_group_membership' else ''}"><a href="${h.route_path('my_account_user_group_membership')}">${_('User Group Membership')}</a></li>
@@ -5,31 +5,32 b''
5 5 <div class="panel-body">
6 6 <div class="apikeys_wrap">
7 7 <p>
8 ${_('Each token can have a role. Token with a role can be used only in given context, '
8 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
9 'Each token can have a role. Token with a role can be used only in given context, '
9 10 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
10 11 </p>
11 12 <table class="rctable auth_tokens">
12 13 <tr>
13 14 <th>${_('Token')}</th>
14 <th>${_('Scope')}</th>
15 15 <th>${_('Description')}</th>
16 16 <th>${_('Role')}</th>
17 <th>${_('Repository Scope')}</th>
17 18 <th>${_('Expiration')}</th>
18 19 <th>${_('Action')}</th>
19 20 </tr>
20 21 %if c.user_auth_tokens:
21 22 %for auth_token in c.user_auth_tokens:
22 <tr class="${'expired' if auth_token.expired else ''}">
23 <tr class="${('expired' if auth_token.expired else '')}">
23 24 <td class="truncate-wrap td-authtoken">
24 25 <div class="user_auth_tokens truncate autoexpand">
25 26 <code>${auth_token.api_key}</code>
26 27 </div>
27 28 </td>
28 <td class="td">${auth_token.scope_humanized}</td>
29 29 <td class="td-wrap">${auth_token.description}</td>
30 30 <td class="td-tags">
31 31 <span class="tag disabled">${auth_token.role_humanized}</span>
32 32 </td>
33 <td class="td">${auth_token.scope_humanized}</td>
33 34 <td class="td-exp">
34 35 %if auth_token.expires == -1:
35 36 ${_('never')}
@@ -92,6 +93,7 b''
92 93 </div>
93 94 </div>
94 95 </div>
96
95 97 <script>
96 98 $(document).ready(function(){
97 99
@@ -55,7 +55,9 b''
55 55 </div>
56 56 <div class="input">
57 57 ${h.text('description', class_='medium', placeholder=_('Description'))}
58 % if c.ssh_key_generator_enabled:
58 59 <a href="${h.route_path('my_account_ssh_keys_generate')}">${_('Generate random RSA key')}</a>
60 % endif
59 61 </div>
60 62 </div>
61 63
@@ -70,7 +72,7 b''
70 72 ${h.reset('reset',_('Reset'),class_="btn")}
71 73 </div>
72 74 % if c.default_key:
73 ${_('Click add to use this generate SSH key')}
75 ${_('Click add to use this generated SSH key')}
74 76 % endif
75 77 </div>
76 78 </div>
@@ -33,7 +33,7 b''
33 33 <div class="fields">
34 34 <div class="field">
35 35 <div class="label">
36 <label for="group_name">${_('Group Name')}:</label>
36 <label for="group_name">${_('Group name')}:</label>
37 37 </div>
38 38 <div class="input">
39 39 ${h.text('group_name', class_="medium")}
@@ -42,6 +42,15 b''
42 42
43 43 <div class="field">
44 44 <div class="label">
45 <label for="group_parent_id">${_('Repository group')}:</label>
46 </div>
47 <div class="select">
48 ${h.select('group_parent_id',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
49 </div>
50 </div>
51
52 <div class="field">
53 <div class="label">
45 54 <label for="group_description">${_('Description')}:</label>
46 55 </div>
47 56 <div class="textarea editor">
@@ -55,22 +64,13 b''
55 64 </div>
56 65 </div>
57 66
58 <div class="field">
59 <div class="label">
60 <label for="group_parent_id">${_('Group Parent')}:</label>
61 </div>
62 <div class="select">
63 ${h.select('group_parent_id',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
64 </div>
65 </div>
66
67 67 <div id="copy_perms" class="field">
68 68 <div class="label label-checkbox">
69 69 <label for="group_copy_permissions">${_('Copy Parent Group Permissions')}:</label>
70 70 </div>
71 71 <div class="checkboxes">
72 72 ${h.checkbox('group_copy_permissions', value="True", checked="checked")}
73 <span class="help-block">${_('Copy permission settings from parent repository group.')}</span>
73 <span class="help-block">${_('Copy permissions from parent repository group.')}</span>
74 74 </div>
75 75 </div>
76 76
@@ -84,22 +84,22 b''
84 84 <script>
85 85 $(document).ready(function(){
86 86 var setCopyPermsOption = function(group_val){
87 if(group_val != "-1"){
87 if(group_val !== "-1"){
88 88 $('#copy_perms').show()
89 89 }
90 90 else{
91 91 $('#copy_perms').hide();
92 92 }
93 }
93 };
94 94 $("#group_parent_id").select2({
95 95 'containerCssClass': "drop-menu",
96 96 'dropdownCssClass': "drop-menu-dropdown",
97 97 'dropdownAutoWidth': true
98 98 });
99 setCopyPermsOption($('#group_parent_id').val())
99 setCopyPermsOption($('#group_parent_id').val());
100 100 $("#group_parent_id").on("change", function(e) {
101 101 setCopyPermsOption(e.val)
102 })
102 });
103 103 $('#group_name').focus();
104 104 })
105 105 </script>
@@ -8,26 +8,12 b''
8 8 %endif
9 9 </%def>
10 10
11 <%def name="breadcrumbs_links()">
12 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
13 &raquo;
14 ${h.link_to(_('Repository Groups'),h.route_path('repo_groups'))}
15 %if c.repo_group.parent_group:
16 &raquo; ${h.link_to(c.repo_group.parent_group.name, h.route_path('repo_group_home', repo_group_name=c.repo_group.parent_group.group_name))}
17 %endif
18 &raquo; ${c.repo_group.name}
11 <%def name="menu_bar_nav()">
12 ${self.menu_items(active='admin')}
19 13 </%def>
20 14
21 <%def name="breadcrumbs_side_links()">
22 <ul class="links">
23 <li>
24 <a href="${h.route_path('repo_group_new', _query=dict(parent_group=c.repo_group.group_id))}" class="btn btn-small btn-success">${_(u'Add Child Group')}</a>
25 </li>
26 </ul>
27 </%def>
28
29 <%def name="menu_bar_nav()">
30 ${self.menu_items(active='admin')}
15 <%def name="menu_bar_subnav()">
16 ${self.repo_group_menu(active='options')}
31 17 </%def>
32 18
33 19 <%def name="main_content()">
@@ -35,10 +21,10 b''
35 21 </%def>
36 22
37 23 <%def name="main()">
24
38 25 <div class="box">
39 26 <div class="title">
40 ${self.breadcrumbs()}
41 ${self.breadcrumbs_side_links()}
27 ${self.repo_group_page_title(c.repo_group)}
42 28 </div>
43 29
44 30 <div class="sidebar-col-wrapper">
@@ -58,4 +44,5 b''
58 44
59 45 </div>
60 46 </div>
47
61 48 </%def>
@@ -2,6 +2,7 b''
2 2
3 3 <%
4 4 elems = [
5 (_('Repository Group ID'), c.repo_group.group_id, '', ''),
5 6 (_('Owner'), lambda:base.gravatar_with_user(c.repo_group.user.email), '', ''),
6 7 (_('Created on'), h.format_date(c.repo_group.created_on), '', ''),
7 8 (_('Is Personal Group'), c.repo_group.personal or False, '', ''),
@@ -133,7 +133,7 b''
133 133 %endfor
134 134
135 135 ## USER GROUPS
136 %for _user_group in c.repo_group.permission_user_groups():
136 %for _user_group in c.repo_group.permission_user_groups(with_members=True):
137 137 <tr id="id${id(_user_group.users_group_name)}">
138 138 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.none', checked=_user_group.permission=='group.none')}</td>
139 139 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.read', checked=_user_group.permission=='group.read')}</td>
@@ -148,6 +148,7 b''
148 148 %else:
149 149 ${h.link_to_group(_user_group.users_group_name)}
150 150 %endif
151 (${_('members')}: ${len(_user_group.members)})
151 152 </td>
152 153 <td class="td-action">
153 154 <span class="btn btn-link btn-danger revoke_perm"
@@ -12,7 +12,7 b''
12 12 <div class="fields">
13 13 <div class="field">
14 14 <div class="label">
15 <label for="group_name">${_('Group Name')}:</label>
15 <label for="group_name">${_('Group name')}:</label>
16 16 </div>
17 17 <div class="input">
18 18 ${c.form['repo_group_name'].render(css_class='medium', oid='group_name')|n}
@@ -22,7 +22,7 b''
22 22
23 23 <div class="field">
24 24 <div class="label">
25 <label for="repo_group">${_('Group parent')}:</label>
25 <label for="repo_group">${_('Repository group')}:</label>
26 26 </div>
27 27 <div class="select">
28 28 ${c.form['repo_group'].render(css_class='medium', oid='repo_group')|n}
@@ -6,7 +6,7 b''
6 6 <div class="fields">
7 7 <div class="field">
8 8 <div class="label">
9 <label for="repo_name">${_('Name')}:</label>
9 <label for="repo_name">${_('Repository name')}:</label>
10 10 </div>
11 11 <div class="input">
12 12 ${h.text('repo_name', class_="medium")}
@@ -49,21 +49,7 b''
49 49 </div>
50 50 <div class="field">
51 51 <div class="label">
52 <label for="repo_description">${_('Description')}:</label>
53 </div>
54 <div class="textarea editor">
55 ${h.textarea('repo_description')}
56 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
57 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
58 <span id="meta-tags-desc" style="display: none">
59 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
60 ${dt.metatags_help()}
61 </span>
62 </div>
63 </div>
64 <div class="field">
65 <div class="label">
66 <label for="repo_group">${_('Repository Group')}:</label>
52 <label for="repo_group">${_('Repository group')}:</label>
67 53 </div>
68 54 <div class="select">
69 55 ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
@@ -77,6 +63,20 b''
77 63 </div>
78 64 <div class="field">
79 65 <div class="label">
66 <label for="repo_description">${_('Description')}:</label>
67 </div>
68 <div class="textarea editor">
69 ${h.textarea('repo_description',cols=23,rows=5,class_="medium")}
70 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
71 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
72 <span id="meta-tags-desc" style="display: none">
73 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
74 ${dt.metatags_help()}
75 </span>
76 </div>
77 </div>
78 <div class="field">
79 <div class="label">
80 80 <label for="repo_landing_rev">${_('Landing commit')}:</label>
81 81 </div>
82 82 <div class="select">
@@ -90,7 +90,7 b''
90 90 </div>
91 91 <div class="checkboxes">
92 92 ${h.checkbox('repo_copy_permissions', value="True", checked="checked")}
93 <span class="help-block">${_('Copy permission set from the parent repository group.')}</span>
93 <span class="help-block">${_('Copy permissions from parent repository group.')}</span>
94 94 </div>
95 95 </div>
96 96 <div class="field">
@@ -36,7 +36,7 b''
36 36 <div class="box">
37 37 <div class="title">
38 38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 ${self.breadcrumbs()}
39
40 40 </div>
41 41
42 42 <div class="sidebar-col-wrapper scw-small">
@@ -2,6 +2,7 b''
2 2
3 3 <%
4 4 elems = [
5 (_('Repository ID'), c.rhodecode_db_repo.repo_id, '', ''),
5 6 (_('Owner'), lambda:base.gravatar_with_user(c.rhodecode_db_repo.user.email), '', ''),
6 7 (_('Created on'), h.format_date(c.rhodecode_db_repo.created_on), '', ''),
7 8 (_('Updated on'), h.format_date(c.rhodecode_db_repo.updated_on), '', ''),
@@ -112,6 +113,40 b''
112 113 </div>
113 114
114 115
116 <div class="panel panel-default">
117 <div class="panel-heading" id="advanced-hooks">
118 <h3 class="panel-title">${_('Hooks')} <a class="permalink" href="#advanced-hooks"></a></h3>
119 </div>
120 <div class="panel-body">
121 <table class="rctable">
122 <th>${_('Hook type')}</th>
123 <th>${_('Hook version')}</th>
124 <th>${_('Current version')}</th>
125 % if c.ver_info_dict:
126 <tr>
127 <td>${_('PRE HOOK')}</td>
128 <td>${c.ver_info_dict['pre_version']}</td>
129 <td>${c.rhodecode_version}</td>
130 </tr>
131 <tr>
132 <td>${_('POST HOOK')}</td>
133 <td>${c.ver_info_dict['post_version']}</td>
134 <td>${c.rhodecode_version}</td>
135 </tr>
136 % else:
137 <tr>
138 <td>${_('Unable to read hook information from VCS Server')}</td>
139 </tr>
140 % endif
141 </table>
142
143 <a href="${h.route_path('edit_repo_advanced_hooks', repo_name=c.repo_name)}"
144 onclick="return confirm('${_('Confirm to reinstall hooks for this repository.')}');">
145 ${_('Update Hooks')}
146 </a>
147 </div>
148 </div>
149
115 150 <div class="panel panel-warning">
116 151 <div class="panel-heading" id="advanced-archive">
117 152 <h3 class="panel-title">${_('Archive repository')} <a class="permalink" href="#advanced-archive"></a></h3>
@@ -128,7 +128,7 b''
128 128 %endfor
129 129
130 130 ## USER GROUPS
131 %for _user_group in c.rhodecode_db_repo.permission_user_groups():
131 %for _user_group in c.rhodecode_db_repo.permission_user_groups(with_members=True):
132 132 <tr>
133 133 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.none', checked=_user_group.permission=='repository.none')}</td>
134 134 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.read', checked=_user_group.permission=='repository.read')}</td>
@@ -143,6 +143,7 b''
143 143 %else:
144 144 ${h.link_to_group(_user_group.users_group_name)}
145 145 %endif
146 (${_('members')}: ${len(_user_group.members)})
146 147 </td>
147 148 <td class="td-action">
148 149 <span class="btn btn-link btn-danger revoke_perm"
@@ -143,7 +143,7 b''
143 143 <div class="select">
144 144 ${c.form['repo_landing_commit_ref'].render(css_class='medium', oid='repo_landing_commit_ref')|n}
145 145 ${c.form.render_error(request, c.form['repo_landing_commit_ref'])|n}
146 <p class="help-block">${_('Default commit for files page, downloads, full text search index and readme')}</p>
146 <p class="help-block">${_('The default commit for file pages, downloads, full text search index, and README generation.')}</p>
147 147 </div>
148 148 </div>
149 149
@@ -5,8 +5,13 b''
5 5 <div class="panel-body">
6 6 <dl class="dl-horizontal">
7 7 % for stat in c.statistics:
8 % if stat.get('sep'):
9 <dt></dt>
10 <dd>--</dd>
11 % else:
8 12 <dt>${stat['key']}</dt>
9 13 <dd>${stat['value']}</dd>
14 % endif
10 15 % endfor
11 16 </dl>
12 17 </div>
@@ -181,7 +181,7 b''
181 181 ${_('''Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:
182 182 {scheme} 'http' or 'https' sent from running RhodeCode server,
183 183 {user} current user username,
184 {sys_user} current system user running this process, usefull for ssh,
184 {sys_user} current system user running this process, Useful for ssh,
185 185 {hostname} hostname of this server running RhodeCode,
186 186 {netloc} network location/server host of running RhodeCode server,
187 187 {repo} full repository name,
@@ -2,6 +2,7 b''
2 2
3 3 <%
4 4 elems = [
5 (_('User Group ID'), c.user_group.users_group_id, '', ''),
5 6 (_('Owner'), lambda:base.gravatar_with_user(c.user_group.user.email), '', ''),
6 7 (_('Created on'), h.format_date(c.user_group.created_on), '', '',),
7 8
@@ -135,7 +135,7 b''
135 135 %endfor
136 136
137 137 ## USER GROUPS
138 %for _user_group in c.user_group.permission_user_groups():
138 %for _user_group in c.user_group.permission_user_groups(with_members=True):
139 139 <tr>
140 140 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.none')}</td>
141 141 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.read')}</td>
@@ -150,6 +150,7 b''
150 150 %else:
151 151 ${h.link_to_group(_user_group.users_group_name)}
152 152 %endif
153 (${_('members')}: ${len(_user_group.members)})
153 154 </td>
154 155 <td class="td-action">
155 156 <span class="btn btn-link btn-danger revoke_perm"
@@ -2,6 +2,7 b''
2 2
3 3 <%
4 4 elems = [
5 (_('User ID'), c.user.user_id, '', ''),
5 6 (_('Created on'), h.format_date(c.user.created_on), '', ''),
6 7 (_('Source of Record'), c.user.extern_type, '', ''),
7 8
@@ -34,15 +35,23 b''
34 35 <h3 class="panel-title">${_('Force Password Reset')}</h3>
35 36 </div>
36 37 <div class="panel-body">
37 ${h.secure_form(h.route_path('user_force_password_reset', user_id=c.user.user_id), request=request)}
38 ${h.secure_form(h.route_path('user_disable_force_password_reset', user_id=c.user.user_id), request=request)}
38 39 <div class="field">
39 40 <button class="btn btn-default" type="submit">
40 <i class="icon-lock"></i>
41 %if c.user.user_data.get('force_password_change'):
42 ${_('Disable forced password reset')}
43 %else:
44 ${_('Enable forced password reset')}
45 %endif
41 <i class="icon-unlock"></i> ${_('Disable forced password reset')}
42 </button>
43 </div>
44 <div class="field">
45 <span class="help-block">
46 ${_("Clear the forced password change flag.")}
47 </span>
48 </div>
49 ${h.end_form()}
50
51 ${h.secure_form(h.route_path('user_enable_force_password_reset', user_id=c.user.user_id), request=request)}
52 <div class="field">
53 <button class="btn btn-default" type="submit" onclick="return confirm('${_('Confirm to enable forced password change')}');">
54 <i class="icon-lock"></i> ${_('Enable forced password reset')}
46 55 </button>
47 56 </div>
48 57 <div class="field">
@@ -51,6 +60,7 b''
51 60 </span>
52 61 </div>
53 62 ${h.end_form()}
63
54 64 </div>
55 65 </div>
56 66
@@ -86,20 +96,16 b''
86 96 <div class="panel-body">
87 97 ${h.secure_form(h.route_path('user_delete', user_id=c.user.user_id), request=request)}
88 98
89 <table class="display">
99 <table class="display rctable">
90 100 <tr>
91 101 <td>
92 102 ${_ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
93 103 </td>
94 104 <td>
95 %if len(c.user.repositories) > 0:
96 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked"/> <label for="user_repos_1">${_('Detach repositories')}</label>
97 %endif
105 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_1">${_('Detach repositories')}</label>
98 106 </td>
99 107 <td>
100 %if len(c.user.repositories) > 0:
101 <input type="radio" id="user_repos_2" name="user_repos" value="delete" /> <label for="user_repos_2">${_('Delete repositories')}</label>
102 %endif
108 <input type="radio" id="user_repos_2" name="user_repos" value="delete" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_2">${_('Delete repositories')}</label>
103 109 </td>
104 110 </tr>
105 111
@@ -108,14 +114,10 b''
108 114 ${_ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
109 115 </td>
110 116 <td>
111 %if len(c.user.repository_groups) > 0:
112 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked"/> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
113 %endif
117 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''} /> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
114 118 </td>
115 119 <td>
116 %if len(c.user.repository_groups) > 0:
117 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" /> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
118 %endif
120 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''}/> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
119 121 </td>
120 122 </tr>
121 123
@@ -124,18 +126,37 b''
124 126 ${_ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
125 127 </td>
126 128 <td>
127 %if len(c.user.user_groups) > 0:
128 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked"/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
129 %endif
129 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
130 130 </td>
131 131 <td>
132 %if len(c.user.user_groups) > 0:
133 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" /> <label for="user_user_groups_2">${_('Delete repositories')}</label>
134 %endif
132 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_2">${_('Delete repositories')}</label>
135 133 </td>
136 134 </tr>
137 135 </table>
138 136 <div style="margin: 0 0 20px 0" class="fake-space"></div>
137 <div class="pull-left">
138 % if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
139 % endif
140
141 <span style="padding: 0 5px 0 0">${_('New owner for detached objects')}:</span>
142 <div class="pull-right">${base.gravatar_with_user(c.first_admin.email, 16)}</div>
143 </div>
144 <div style="clear: both">
145
146 <div>
147 <p class="help-block">
148 ${_("When selecting the detach option, the depending objects owned by this user will be assigned to the above user.")}
149 <br/>
150 ${_("The delete option will delete the user and all his owned objects!")}
151 </p>
152 </div>
153
154 % if c.can_delete_user_message:
155 <p class="pre-formatting">${c.can_delete_user_message}</p>
156 % endif
157 </div>
158
159 <div style="margin: 0 0 20px 0" class="fake-space"></div>
139 160
140 161 <div class="field">
141 162 <button class="btn btn-small btn-danger" type="submit"
@@ -144,17 +165,6 b''
144 165 ${_('Delete this user')}
145 166 </button>
146 167 </div>
147 % if c.can_delete_user_message:
148 <p class="help-block pre-formatting">${c.can_delete_user_message}</p>
149 % endif
150
151 <div class="field">
152 <span class="help-block">
153 %if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
154 <p class="help-block">${_("When selecting the detach option, the depending objects owned by this user will be assigned to the `%s` super admin in the system. The delete option will delete the user's repositories!") % (c.first_admin.full_name)}</p>
155 %endif
156 </span>
157 </div>
158 168
159 169 ${h.end_form()}
160 170 </div>
@@ -5,27 +5,32 b''
5 5 <div class="panel-body">
6 6 <div class="apikeys_wrap">
7 7 <p>
8 ${_('Each token can have a role. Token with a role can be used only in given context, '
8 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
9 'Each token can have a role. Token with a role can be used only in given context, '
9 10 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
10 11 </p>
11 12 <table class="rctable auth_tokens">
12 13 <tr>
13 14 <th>${_('Token')}</th>
14 <th>${_('Scope')}</th>
15 15 <th>${_('Description')}</th>
16 16 <th>${_('Role')}</th>
17 <th>${_('Repository Scope')}</th>
17 18 <th>${_('Expiration')}</th>
18 19 <th>${_('Action')}</th>
19 20 </tr>
20 21 %if c.user_auth_tokens:
21 22 %for auth_token in c.user_auth_tokens:
22 <tr class="${'expired' if auth_token.expired else ''}">
23 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
24 <td class="td">${auth_token.scope_humanized}</td>
23 <tr class="${('expired' if auth_token.expired else '')}">
24 <td class="truncate-wrap td-authtoken">
25 <div class="user_auth_tokens truncate autoexpand">
26 <code>${auth_token.api_key}</code>
27 </div>
28 </td>
25 29 <td class="td-wrap">${auth_token.description}</td>
26 30 <td class="td-tags">
27 31 <span class="tag disabled">${auth_token.role_humanized}</span>
28 32 </td>
33 <td class="td">${auth_token.scope_humanized}</td>
29 34 <td class="td-exp">
30 35 %if auth_token.expires == -1:
31 36 ${_('never')}
@@ -50,7 +50,9 b''
50 50 </div>
51 51 <div class="input">
52 52 ${h.text('description', class_='medium', placeholder=_('Description'))}
53 % if c.ssh_key_generator_enabled:
53 54 <a href="${h.route_path('edit_user_ssh_keys_generate_keypair', user_id=c.user.user_id)}">${_('Generate random RSA key')}</a>
55 % endif
54 56 </div>
55 57 </div>
56 58
@@ -1,8 +1,9 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 <h3 class="panel-title">${_('New SSH Key generated')}</h3>
3 <h3 class="panel-title">${_('New SSH Key generation')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 %if c.ssh_enabled and c.ssh_key_generator_enabled:
6 7 <p>
7 8 ${_('Below is a 2048 bit generated SSH RSA key.')}<br/>
8 9 ${_('If You wish to use it to access RhodeCode via the SSH please save the private key and click `Use this generated key` at the bottom.')}
@@ -40,6 +41,11 b' e.g chmod 0600 /home/{username}/.ssh/id_'
40 41 % endif
41 42 ${_('Confirmation required on the next screen')}.
42 43 </p>
44 % else:
45 <h2>
46 ${_('SSH key generator has been disabled.')}
47 </h2>
48 % endif
43 49 </div>
44 50 </div>
45 51
@@ -81,7 +81,7 b''
81 81 { data: {"_": "active",
82 82 "sort": "active"}, title: "${_('Active')}", className: "td-active" },
83 83 { data: {"_": "admin",
84 "sort": "admin"}, title: "${_('Admin')}", className: "td-admin" },
84 "sort": "admin"}, title: "${_('Super admin')}", className: "td-admin" },
85 85 { data: {"_": "extern_type",
86 86 "sort": "extern_type"}, title: "${_('Auth type')}", className: "td-type" },
87 87 { data: {"_": "action",
@@ -52,7 +52,7 b''
52 52 <p class="server-instance" style="display:${sid}">
53 53 ## display hidden instance ID if specially defined
54 54 % if c.rhodecode_instanceid:
55 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
55 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
56 56 % endif
57 57 </p>
58 58 </div>
@@ -180,7 +180,7 b''
180 180 %endif
181 181
182 182 ## repo name with group name
183 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
183 ${h.breadcrumb_repo_link(repo_instance)}
184 184
185 185 </div>
186 186
@@ -188,7 +188,7 b''
188 188 %if repo_instance.fork:
189 189 <p>
190 190 <i class="icon-code-fork"></i> ${_('Fork of')}
191 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
191 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
192 192 </p>
193 193 %endif
194 194
@@ -231,6 +231,8 b''
231 231 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
232 232 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
233 233 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
234 <li class="${is_active('search')}"><a class="menulink" href="${h.route_path('search_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Search')}</div></a></li>
235
234 236 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 237 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 238 <li class="${is_active('showpullrequest')}">
@@ -242,13 +244,14 b''
242 244 </a>
243 245 </li>
244 246 %endif
247
245 248 <li class="${is_active('options')}">
246 249 <a class="menulink dropdown">
247 250 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
248 251 </a>
249 252 <ul class="submenu">
250 253 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
251 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
254 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Repository Settings')}</a></li>
252 255 %endif
253 256 %if c.rhodecode_db_repo.fork:
254 257 <li>
@@ -266,8 +269,6 b''
266 269 </li>
267 270 %endif
268 271
269 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
270
271 272 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
272 273 %if c.rhodecode_db_repo.locked[0]:
273 274 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
@@ -296,62 +297,96 b''
296 297
297 298 </%def>
298 299
300 <%def name="repo_group_page_title(repo_group_instance)">
301 <div class="title-content">
302 <div class="title-main">
303 ## Repository Group icon
304 <i class="icon-folder-close"></i>
305
306 ## repo name with group name
307 ${h.breadcrumb_repo_group_link(repo_group_instance)}
308 </div>
309
310 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
311 <div class="repo-group-desc">
312 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
313 </div>
314
315 </div>
316 </%def>
317
318 <%def name="repo_group_menu(active=None)">
319 <%
320 def is_active(selected):
321 if selected == active:
322 return "active"
323
324 is_admin = h.HasPermissionAny('hg.admin')('can create repos index page')
325
326 gr_name = c.repo_group.group_name if c.repo_group else None
327 # create repositories with write permission on group is set to true
328 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
329 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
330 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
331
332 %>
333
334 <!--- CONTEXT BAR -->
335 <div id="context-bar">
336 <div class="wrapper">
337 <ul id="context-pages" class="navigation horizontal-list">
338 <li class="${is_active('home')}"><a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a></li>
339 <li class="${is_active('search')}"><a class="menulink" href="${h.route_path('search_repo_group', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Search')}</div></a></li>
340
341 <li class="${is_active('options')}">
342 <a class="menulink dropdown">
343 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
344 </a>
345 <ul class="submenu">
346 %if is_admin or group_admin:
347 <li><a href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}">${_('Group Settings')}</a></li>
348 %endif
349 %if is_admin or group_admin or (group_write and create_on_write):
350 <li><a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('Add Repository')}</a></li>
351 %endif
352 %if is_admin or group_admin:
353 <li><a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'Add Parent Group')}</a></li>
354 %endif
355 </ul>
356 </li>
357 </ul>
358 </div>
359 <div class="clear"></div>
360 </div>
361
362 <!--- END CONTEXT BAR -->
363
364 </%def>
365
366
299 367 <%def name="usermenu(active=False)">
300 368 ## USER MENU
301 369 <li id="quick_login_li" class="${'active' if active else ''}">
370 % if c.rhodecode_user.username == h.DEFAULT_USER:
371 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
372 ${gravatar(c.rhodecode_user.email, 20)}
373 <span class="user">
374 <span>${_('Sign in')}</span>
375 </span>
376 </a>
377 % else:
378 ## logged in user
302 379 <a id="quick_login_link" class="menulink childs">
303 380 ${gravatar(c.rhodecode_user.email, 20)}
304 381 <span class="user">
305 %if c.rhodecode_user.username != h.DEFAULT_USER:
306 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
307 %else:
308 <span>${_('Sign in')}</span>
309 %endif
382 <span class="menu_link_user">${c.rhodecode_user.username}</span>
383 <div class="show_more"></div>
310 384 </span>
311 385 </a>
312
386 ## subnav with menu for logged in user
313 387 <div class="user-menu submenu">
314 388 <div id="quick_login">
315 %if c.rhodecode_user.username == h.DEFAULT_USER:
316 <h4>${_('Sign in to your account')}</h4>
317 ${h.form(h.route_path('login', _query={'came_from': h.current_route_path(request)}), needs_csrf_token=False)}
318 <div class="form form-vertical">
319 <div class="fields">
320 <div class="field">
321 <div class="label">
322 <label for="username">${_('Username')}:</label>
323 </div>
324 <div class="input">
325 ${h.text('username',class_='focus',tabindex=1)}
326 </div>
327
328 </div>
329 <div class="field">
330 <div class="label">
331 <label for="password">${_('Password')}:</label>
332 %if h.HasPermissionAny('hg.password_reset.enabled')():
333 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
334 %endif
335 </div>
336 <div class="input">
337 ${h.password('password',class_='focus',tabindex=2)}
338 </div>
339 </div>
340 <div class="buttons">
341 <div class="register">
342 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
343 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
344 %endif
345 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
346 </div>
347 <div class="submit">
348 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
349 </div>
350 </div>
351 </div>
352 </div>
353 ${h.end_form()}
354 %else:
389 %if c.rhodecode_user.username != h.DEFAULT_USER:
355 390 <div class="">
356 391 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
357 392 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
@@ -364,6 +399,50 b''
364 399 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
365 400 % endif
366 401 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
402 ## bookmark-items
403 <li class="bookmark-items">
404 ${_('Bookmarks')}
405 <div class="pull-right">
406 <a href="${h.route_path('my_account_bookmarks')}">${_('Manage')}</a>
407 </div>
408 </li>
409 % if not c.bookmark_items:
410 <li>
411 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
412 </li>
413 % endif
414 % for item in c.bookmark_items:
415 <li>
416 % if item.repository:
417 <div>
418 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
419 <code>${item.position}</code>
420 % if item.repository.repo_type == 'hg':
421 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
422 % elif item.repository.repo_type == 'git':
423 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
424 % elif item.repository.repo_type == 'svn':
425 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
426 % endif
427 ${(item.title or h.shorter(item.repository.repo_name, 30))}
428 </a>
429 </div>
430 % elif item.repository_group:
431 <div>
432 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
433 <code>${item.position}</code>
434 <i class="icon-folder-close" title="${_('Repository group')}" style="font-size: 16px"></i>
435 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
436 </a>
437 </div>
438 % else:
439 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
440 <code>${item.position}</code>
441 ${item.title}
442 </a>
443 % endif
444 </li>
445 % endfor
367 446
368 447 <li class="logout">
369 448 ${h.secure_form(h.route_path('logout'), request=request)}
@@ -375,7 +454,7 b''
375 454 %endif
376 455 </div>
377 456 </div>
378 %if c.rhodecode_user.username != h.DEFAULT_USER:
457 ## unread counter
379 458 <div class="pill_container">
380 459 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
381 460 </div>
@@ -413,17 +492,17 b''
413 492 </div>
414 493
415 494 <div id="main_filter_help" style="display: none">
416 Use '/' key to quickly access this field.
417 Enter name of repository, or repository group for quick search.
495 - Use '/' key to quickly access this field.
418 496
419 Prefix query to allow special search:
497 - Enter a name of repository, or repository group for quick search.
498
499 - Prefix query to allow special search:
420 500
421 501 user:admin, to search for usernames
422 502
423 503 user_group:devops, to search for user groups
424 504
425 505 commit:efced4, to search for commits
426
427 506 </div>
428 507 </li>
429 508
@@ -516,6 +595,26 b' commit:efced4, to search for commits'
516 595 }(result, escapeMarkup);
517 596 };
518 597
598 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
599 return function(data, escapeMarkup) {
600 if (!data.repo_group_id){
601 return data.text; // optgroup text Repositories
602 }
603
604 var tmpl = '';
605 var repoGroupName = data['text'];
606
607 if(data){
608
609 tmpl += '<i class="icon-folder-close"></i> ';
610
611 }
612 tmpl += escapeMarkup(repoGroupName);
613 return tmpl;
614
615 }(result, escapeMarkup);
616 };
617
519 618
520 619 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
521 620
@@ -531,6 +630,19 b' commit:efced4, to search for commits'
531 630 };
532 631 var pattern = '(' + escapeRegExChars(value) + ')';
533 632
633 var getRepoIcon = function(repo_type) {
634 if (repo_type === 'hg') {
635 return '<i class="icon-hg"></i> ';
636 }
637 else if (repo_type === 'git') {
638 return '<i class="icon-git"></i> ';
639 }
640 else if (repo_type === 'svn') {
641 return '<i class="icon-svn"></i> ';
642 }
643 return ''
644 };
645
534 646 // highlight match
535 647 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
536 648 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
@@ -540,19 +652,16 b' commit:efced4, to search for commits'
540 652 if (searchType === 'hint') {
541 653 icon += '<i class="icon-folder-close"></i> ';
542 654 }
655 // full text search
543 656 else if (searchType === 'search') {
544 657 icon += '<i class="icon-more"></i> ';
545 658 }
659 // repository
546 660 else if (searchType === 'repo') {
547 if (data['repo_type'] === 'hg') {
548 icon += '<i class="icon-hg"></i> ';
549 }
550 else if (data['repo_type'] === 'git') {
551 icon += '<i class="icon-git"></i> ';
552 }
553 else if (data['repo_type'] === 'svn') {
554 icon += '<i class="icon-svn"></i> ';
555 }
661
662 var repoIcon = getRepoIcon(data['repo_type']);
663 icon += repoIcon;
664
556 665 if (data['private']) {
557 666 icon += '<i class="icon-lock" ></i> ';
558 667 }
@@ -560,18 +669,27 b' commit:efced4, to search for commits'
560 669 icon += '<i class="icon-unlock-alt"></i> ';
561 670 }
562 671 }
672 // repository groups
563 673 else if (searchType === 'repo_group') {
564 674 icon += '<i class="icon-folder-close"></i> ';
565 675 }
676 // user group
566 677 else if (searchType === 'user_group') {
567 678 icon += '<i class="icon-group"></i> ';
568 679 }
569 680 else if (searchType === 'user') {
570 681 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
571 682 }
683 // commit
572 684 else if (searchType === 'commit') {
685 var repo_data = data['repo_data'];
686 var repoIcon = getRepoIcon(repo_data['repository_type']);
687 if (repoIcon) {
688 icon += repoIcon;
689 } else {
573 690 icon += '<i class="icon-tag"></i>';
574 691 }
692 }
575 693
576 694 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
577 695 return tmpl.format(icon, valueDisplay);
@@ -594,7 +712,7 b' commit:efced4, to search for commits'
594 712
595 713 $('#main_filter').autocomplete({
596 714 serviceUrl: pyroutes.url('goto_switcher_data'),
597 params: {"repo_group_id": templateContext.repo_group_id},
715 params: {"search_context": templateContext.search_context},
598 716 minChars:2,
599 717 maxHeight:400,
600 718 deferRequestBy: 300, //miliseconds
@@ -605,12 +723,18 b' commit:efced4, to search for commits'
605 723 onSelect: function(element, suggestion){
606 724 handleSelect(element, suggestion);
607 725 return false;
726 },
727 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
728 if (jqXHR !== 'abort') {
729 alert("Error during search.\nError code: {0}".format(textStatus));
730 window.location = '';
731 }
608 732 }
609 733 });
610 734
611 735 showMainFilterBox = function () {
612 736 $('#main_filter_help').toggle();
613 }
737 };
614 738
615 739 </script>
616 740 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
@@ -637,6 +761,7 b' commit:efced4, to search for commits'
637 761 ('g h', 'Goto home page'),
638 762 ('g g', 'Goto my private gists page'),
639 763 ('g G', 'Goto my public gists page'),
764 ('g 0-9', 'Goto bookmarked items from 0-9'),
640 765 ('n r', 'New repository page'),
641 766 ('n g', 'New gist page'),
642 767 ]
@@ -7,9 +7,12 b" go_import_header = ''"
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 ## check repo context
11 c.template_context['repo_view_type'] = h.get_repo_view_type(request)
10 12
11 13 if getattr(c, 'repo_group', None):
12 14 c.template_context['repo_group_id'] = c.repo_group.group_id
15 c.template_context['repo_group_name'] = c.repo_group.group_name
13 16
14 17 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
15 18 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
@@ -23,6 +26,12 b" c.template_context['default_user'] = {"
23 26 'username': h.DEFAULT_USER,
24 27 'user_id': 1
25 28 }
29 c.template_context['search_context'] = {
30 'repo_group_id': c.template_context.get('repo_group_id'),
31 'repo_group_name': c.template_context.get('repo_group_name'),
32 'repo_name': c.template_context.get('repo_name'),
33 'repo_view_type': c.template_context.get('repo_view_type'),
34 }
26 35
27 36 %>
28 37 <html xmlns="http://www.w3.org/1999/xhtml">
@@ -520,7 +520,7 b' def get_comments_for(diff_type, comments'
520 520 if hasattr(filename, 'unicode_path'):
521 521 filename = filename.unicode_path
522 522
523 if not isinstance(filename, basestring):
523 if not isinstance(filename, (unicode, str)):
524 524 return None
525 525
526 526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
@@ -88,9 +88,9 b''
88 88 %endif
89 89
90 90 ##PRIVATE/PUBLIC
91 %if private and c.visual.show_private_icon:
91 %if private is True and c.visual.show_private_icon:
92 92 <i class="icon-lock" title="${_('Private repository')}"></i>
93 %elif not private and c.visual.show_public_icon:
93 %elif private is False and c.visual.show_public_icon:
94 94 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
95 95 %else:
96 96 <span></span>
@@ -314,7 +314,7 b''
314 314 $(document).ready(function() {
315 315 callbacks();
316 316 var search_GET = "${request.GET.get('search','')}";
317 if (search_GET == "1") {
317 if (search_GET === "1") {
318 318 _NODEFILTER.initFilter();
319 319 }
320 320 });
@@ -27,6 +27,14 b''
27 27 ${_('Add File')}</a>
28 28 </div>
29 29 % endif
30 % if c.enable_downloads:
31 <% at_path = '{}.zip'.format(request.GET.get('at') or c.commit.raw_id[:6]) %>
32 <div title="${_('Download tree at {}').format(at_path)}" class="btn btn-default new-file">
33 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname=c.commit.raw_id)}">
34 ${_('Download tree at {}').format(at_path)}
35 </a>
36 </div>
37 % endif
30 38 </div>
31 39
32 40 <div class="browser-search">
@@ -106,4 +106,25 b''
106 106 %endif
107 107 %endif
108 108 </div>
109 </div> No newline at end of file
109 </div>
110
111 <script type="text/javascript">
112 % if request.GET.get('mark'):
113
114 $(function(){
115 $(".codehilite").mark(
116 "${request.GET.get('mark')}",
117 {
118 "className": 'match',
119 "accuracy": "complementary",
120 "ignorePunctuation": ":._(){}[]!'+=".split(""),
121 "each": function(el) {
122 // and also highlight lines !
123 $($(el).closest('tr')).find('td.cb-lineno').addClass('cb-line-selected');
124 }
125 }
126 );
127
128 });
129 % endif
130 </script>
@@ -8,9 +8,7 b''
8 8 %endif
9 9 </%def>
10 10
11 <%def name="breadcrumbs_links()">
12 ${_('New Fork')}
13 </%def>
11 <%def name="breadcrumbs_links()"></%def>
14 12
15 13 <%def name="menu_bar_nav()">
16 14 ${self.menu_items(active='repositories')}
@@ -24,7 +22,6 b''
24 22 <div class="box">
25 23 <div class="title">
26 24 ${self.repo_page_title(c.rhodecode_db_repo)}
27 ${self.breadcrumbs()}
28 25 </div>
29 26
30 27 ${h.secure_form(h.route_path('repo_fork_create',repo_name=c.rhodecode_db_repo.repo_name), request=request)}
@@ -44,21 +41,6 b''
44 41 </div>
45 42
46 43 <div class="field">
47 <div class="label label-textarea">
48 <label for="description">${_('Description')}:</label>
49 </div>
50 <div class="textarea-repo textarea text-area editor">
51 ${h.textarea('description')}
52 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
53 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
54 <span id="meta-tags-desc" style="display: none">
55 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
56 ${dt.metatags_help()}
57 </span>
58 </div>
59 </div>
60
61 <div class="field">
62 44 <div class="label">
63 45 <label for="repo_group">${_('Repository group')}:</label>
64 46 </div>
@@ -74,12 +56,27 b''
74 56 </div>
75 57
76 58 <div class="field">
59 <div class="label label-textarea">
60 <label for="description">${_('Description')}:</label>
61 </div>
62 <div class="textarea editor">
63 ${h.textarea('description',cols=23,rows=5,class_="medium")}
64 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
65 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
66 <span id="meta-tags-desc" style="display: none">
67 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
68 ${dt.metatags_help()}
69 </span>
70 </div>
71 </div>
72
73 <div class="field">
77 74 <div class="label">
78 75 <label for="landing_rev">${_('Landing commit')}:</label>
79 76 </div>
80 77 <div class="select">
81 78 ${h.select('landing_rev','',c.landing_revs,class_="medium")}
82 <span class="help-block">${_('Default commit for files page, downloads, whoosh and readme')}</span>
79 <span class="help-block">${_('The default commit for file pages, downloads, full text search index, and README generation.')}</span>
83 80 </div>
84 81 </div>
85 82
@@ -89,7 +86,7 b''
89 86 </div>
90 87 <div class="checkboxes">
91 88 ${h.checkbox('copy_permissions',value="True", checked="checked")}
92 <span class="help-block">${_('Copy permissions from forked repository')}</span>
89 <span class="help-block">${_('Copy permissions from parent repository.')}</span>
93 90 </div>
94 91 </div>
95 92
@@ -1,13 +1,27 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3
4 <%def name="menu_bar_subnav()">
5 % if c.repo_group:
6 ${self.repo_group_menu(active='home')}
7 % endif
8 </%def>
9
10
3 11 <%def name="main()">
4 12 <div class="box">
5 13 <!-- box / title -->
6 14 <div class="title">
7 <div class="block-left breadcrumbs">
8 ${self.breadcrumbs()}
9 <span id="match_container" style="display:none"><span id="match_count">0</span> ${_('matches')}</span>
15 % if c.repo_group:
16 ${self.repo_group_page_title(c.repo_group)}
17 ## context actions
18 <div>
19 <ul class="links icon-only-links block-right">
20 <li></li>
21 </ul>
10 22 </div>
23 % endif
24
11 25 %if c.rhodecode_user.username != h.DEFAULT_USER:
12 26 <div class="block-right">
13 27 <%
@@ -15,12 +29,6 b''
15 29 create_repo = h.HasPermissionAny('hg.create.repository')('can create repository index page')
16 30 create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')('can create repository groups index page')
17 31 create_user_group = h.HasPermissionAny('hg.usergroup.create.true')('can create user groups index page')
18
19 gr_name = c.repo_group.group_name if c.repo_group else None
20 # create repositories with write permission on group is set to true
21 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
22 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
23 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
24 32 %>
25 33
26 34 %if not c.repo_group:
@@ -32,17 +40,6 b''
32 40 %if is_admin or create_repo_group:
33 41 <a href="${h.route_path('repo_group_new')}" class="btn btn-small btn-default">${_(u'Add Repository Group')}</a>
34 42 %endif
35 %else:
36 ##we're inside other repository group other terms apply
37 %if is_admin or group_admin or (group_write and create_on_write):
38 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}" class="btn btn-small btn-success btn-primary">${_('Add Repository')}</a>
39 %endif
40 %if is_admin or group_admin:
41 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}" class="btn btn-small btn-default">${_(u'Add Repository Group')}</a>
42 %endif
43 %if is_admin or group_admin:
44 <a href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}" class="btn btn-small btn-primary">${_('Edit Repository Group')}</a>
45 %endif
46 43 %endif
47 44 </div>
48 45 %endif
@@ -25,17 +25,16 b''
25 25
26 26 <div class="loginwrapper">
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28
28 29 <div class="left-column">
29 30 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
30 31 </div>
32
31 33 <%block name="above_login_button" />
32 34 <div id="login" class="right-column">
33 35 <!-- login -->
34 36 <div class="sign-in-title">
35 <h1>${_('Sign In')}</h1>
36 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
37 <h4>${h.link_to(_("Go to the registration page to create a new account."), request.route_path('register'))}</h4>
38 %endif
37 <h1>${_('Sign In using username/password')}</h1>
39 38 </div>
40 39 <div class="inner form">
41 40 ${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
@@ -47,7 +46,12 b''
47 46 <br />
48 47 %endif
49 48
50 <label for="password">${_('Password')}:</label>
49 <label for="password">${_('Password')}:
50 %if h.HasPermissionAny('hg.password_reset.enabled')():
51 <div class="pull-right">${h.link_to(_('Forgot your password?'), h.route_path('reset_password'), class_='pwd_reset', tabindex="-1")}</div>
52 %endif
53
54 </label>
51 55 ${h.password('password', class_='focus')}
52 56 %if 'password' in errors:
53 57 <span class="error-message">${errors.get('password')}</span>
@@ -55,15 +59,25 b''
55 59 %endif
56 60
57 61 ${h.checkbox('remember', value=True, checked=defaults.get('remember'))}
58 <label class="checkbox" for="remember">${_('Remember me')}</label>
62 <% timeout = request.registry.settings.get('beaker.session.timeout', '0') %>
63 % if timeout == '0':
64 <% remember_label = _('Remember my indefinitely') %>
65 % else:
66 <% remember_label = _('Remember me for {}').format(h.age_from_seconds(timeout)) %>
67 % endif
68 <label class="checkbox" for="remember">${remember_label}</label>
59 69
60 %if h.HasPermissionAny('hg.password_reset.enabled')():
61 70 <p class="links">
62 ${h.link_to(_('Forgot your password?'), h.route_path('reset_password'), class_='pwd_reset')}
71 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
72 ${h.link_to(_("Create a new account."), request.route_path('register'), class_='new_account')}
73 %endif
63 74 </p>
64 %elif h.HasPermissionAny('hg.password_reset.hidden')():
75
76 %if not h.HasPermissionAny('hg.password_reset.enabled')():
77 ## password reset hidden or disabled.
65 78 <p class="help-block">
66 ${_('Password reset is disabled. Please contact ')}
79 ${_('Password reset is disabled.')} <br/>
80 ${_('Please contact ')}
67 81 % if c.visual.rhodecode_support_url:
68 82 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
69 83 ${_('or')}
@@ -72,18 +86,18 b''
72 86 </p>
73 87 %endif
74 88
75 ${h.submit('sign_in', _('Sign In'), class_="btn sign-in")}
76 <p class="help-block pull-right">
77 RhodeCode ${c.rhodecode_edition}
78 </p>
89 ${h.submit('sign_in', _('Sign In'), class_="btn sign-in", title=_('Sign in to {}').format(c.rhodecode_edition))}
90
79 91 ${h.end_form()}
80 92 <script type="text/javascript">
81 93 $(document).ready(function(){
82 94 $('#username').focus();
83 95 })
84 96 </script>
97
85 98 </div>
86 99 <!-- end login -->
100
87 101 <%block name="below_login_button" />
88 102 </div>
89 103 </div>
@@ -33,7 +33,7 b''
33 33
34 34
35 35 <div class="pr-details-title">
36 ${_('Pull request summary')}
36 ${_('Summary')}
37 37 </div>
38 38
39 39 <div class="form" style="padding-top: 10px">
@@ -257,7 +257,7 b''
257 257 query.callback({results: cachedData.results});
258 258 } else {
259 259 $.ajax({
260 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
260 url: pyroutes.url('pullrequest_repo_targets', {'repo_name': templateContext.repo_name}),
261 261 data: {query: query.term},
262 262 dataType: 'json',
263 263 type: 'GET',
@@ -118,10 +118,7 b''
118 118 ${register_message|n}
119 119 </p>
120 120
121 ${h.submit('sign_up',_('Create Account'),class_="btn sign-in")}
122 <p class="help-block pull-right">
123 RhodeCode ${c.rhodecode_edition}
124 </p>
121 ${h.submit('sign_up',_('Create Account'), class_="btn sign-in", title=_('Create Account in {}').format(c.rhodecode_edition))}
125 122 ${h.end_form()}
126 123 </div>
127 124 <%block name="below_register_button" />
@@ -3,7 +3,9 b''
3 3
4 4 <%def name="title()">
5 5 %if c.repo_name:
6 ${_('Search inside repository %(repo_name)s') % {'repo_name': c.repo_name}}
6 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
7 %elif c.repo_group_name:
8 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
7 9 %else:
8 10 ${_('Search inside all accessible repositories')}
9 11 %endif
@@ -14,19 +16,20 b''
14 16
15 17 <%def name="breadcrumbs_links()">
16 18 %if c.repo_name:
17 ${_('Search inside repository %(repo_name)s') % {'repo_name': c.repo_name}}
19 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
20 %elif c.repo_group_name:
21 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
18 22 %else:
19 23 ${_('Search inside all accessible repositories')}
20 24 %endif
21 %if c.cur_query:
22 &raquo;
23 ${c.cur_query}
24 %endif
25
25 26 </%def>
26 27
27 28 <%def name="menu_bar_nav()">
28 29 %if c.repo_name:
29 ${self.menu_items(active='repositories')}
30 ${self.menu_items(active='search')}
31 %elif c.repo_group_name:
32 ${self.menu_items(active='search')}
30 33 %else:
31 34 ${self.menu_items(active='search')}
32 35 %endif
@@ -34,8 +37,26 b''
34 37
35 38 <%def name="menu_bar_subnav()">
36 39 %if c.repo_name:
37 ${self.repo_menu(active='options')}
40 ${self.repo_menu(active='search')}
41 %elif c.repo_group_name:
42 ${self.repo_group_menu(active='search')}
43 %endif
44 </%def>
45
46 <%def name="repo_icon(db_repo)">
47 %if h.is_hg(db_repo):
48 <i class="icon-hg"></i>
38 49 %endif
50 %if h.is_git(db_repo):
51 <i class="icon-git"></i>
52 %endif
53 %if h.is_svn(db_repo):
54 <i class="icon-svn"></i>
55 %endif
56 </%def>
57
58 <%def name="repo_group_icon()">
59 <i class="icon-folder-close"></i>
39 60 </%def>
40 61
41 62 <%def name="main()">
@@ -46,6 +67,12 b''
46 67 ${self.repo_page_title(c.rhodecode_db_repo)}
47 68 </div>
48 69 ${h.form(h.route_path('search_repo',repo_name=c.repo_name),method='get')}
70 %elif c.repo_group_name:
71 <!-- box / title -->
72 <div class="title">
73 ${self.repo_group_page_title(c.repo_group)}
74 </div>
75 ${h.form(h.route_path('search_repo_group',repo_group_name=c.repo_group_name),method='get')}
49 76 %else:
50 77 <!-- box / title -->
51 78 <div class="title">
@@ -57,12 +84,48 b''
57 84 %endif
58 85 <div class="form search-form">
59 86 <div class="fields">
87
60 88 ${h.text('q', c.cur_query, placeholder="Enter query...")}
61 89
62 ${h.select('type',c.search_type,[('content',_('File contents')), ('commit',_('Commit messages')), ('path',_('File names')),],id='id_search_type')}
90 ${h.select('type',c.search_type,[('content',_('Files')), ('path',_('File path')),('commit',_('Commits'))],id='id_search_type')}
91 ${h.hidden('max_lines', '10')}
92
63 93 <input type="submit" value="${_('Search')}" class="btn"/>
64 94 <br/>
65 95
96 <div class="search-tags">
97 <span class="tag tag8">
98 %if c.repo_name:
99 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
100 %elif c.repo_group_name:
101 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
102 % else:
103 ${_('Global Search')}
104 %endif
105 </span>
106
107 %if c.repo_name:
108 »
109 <span class="tag tag8">
110 ${repo_icon(c.rhodecode_db_repo)}
111 ${c.repo_name}
112 </span>
113
114 %elif c.repo_group_name:
115 »
116 <span class="tag tag8">
117 ${repo_group_icon()}
118 ${c.repo_group_name}
119 </span>
120 %endif
121
122
123 % for search_tag in c.search_tags:
124 <br/><span class="tag disabled" style="margin-top: 3px">${search_tag}</span>
125 % endfor
126
127 </div>
128
66 129 <div class="search-feedback-items">
67 130 % for error in c.errors:
68 131 <span class="error-message">
@@ -72,8 +135,54 b''
72 135 </span>
73 136 % endfor
74 137 <div class="field">
75 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Example Queries')}</p>
76 <pre id="search-help" style="display: none">${h.tooltip(h.search_filter_help(c.searcher, request))}</pre>
138 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Langague examples')}</p>
139 <pre id="search-help" style="display: none">\
140
141 % if c.searcher.name == 'whoosh':
142 Example filter terms for `Whoosh` search:
143 query lang: <a href="${c.searcher.query_lang_doc}">Whoosh Query Language</a>
144 Whoosh has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
145
146 Generate wildcards using '*' character:
147 "repo_name:vcs*" - search everything starting with 'vcs'
148 "repo_name:*vcs*" - search for repository containing 'vcs'
149
150 Optional AND / OR operators in queries
151 "repo_name:vcs OR repo_name:test"
152 "owner:test AND repo_name:test*" AND extension:py
153
154 Move advanced search is available via ElasticSearch6 backend in EE edition.
155 % elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
156 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
157 ElasticSearch-2 has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
158
159 search type: content (File Content)
160 indexed fields: content
161
162 # search for `fix` string in all files
163 fix
164
165 search type: commit (Commit message)
166 indexed fields: message
167
168 search type: path (File name)
169 indexed fields: path
170
171 % else:
172 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
173 query lang: <a href="${c.searcher.query_lang_doc}">ES 6 Query Language</a>
174 The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
175 % for handler in c.searcher.get_handlers().values():
176
177 search type: ${handler.search_type_label}
178 *indexed fields*: ${', '.join( [('\n ' if x[0]%4==0 else '')+x[1] for x in enumerate(handler.es_6_field_names)])}
179 % for entry in handler.es_6_example_queries:
180 ${entry.rstrip()}
181 % endfor
182 % endfor
183
184 % endif
185 </pre>
77 186 </div>
78 187
79 188 <div class="field">${c.runtime}</div>
@@ -102,6 +211,12 b''
102 211 'dropdownAutoWidth': true,
103 212 'minimumResultsForSearch': -1
104 213 });
214
215 $('#q').autoGrowInput({maxWidth: 920});
216
217 setTimeout(function() {
218 $('#q').keyup()
219 }, 1);
105 220 })
106 221 </script>
107 222 </%def>
@@ -1,4 +1,7 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="search" file="/search/search.mako"/>
3
4 % if c.formatted_results:
2 5
3 6 <table class="rctable search-results">
4 7 <tr>
@@ -17,16 +20,11 b''
17 20 </tr>
18 21 %for entry in c.formatted_results:
19 22 ## search results are additionally filtered, and this check is just a safe gate
20 % if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results commit check'):
23 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results commit check'):
21 24 <tr class="body">
22 25 <td class="td-componentname">
23 %if h.get_repo_type_by_name(entry.get('repository')) == 'hg':
24 <i class="icon-hg"></i>
25 %elif h.get_repo_type_by_name(entry.get('repository')) == 'git':
26 <i class="icon-git"></i>
27 %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn':
28 <i class="icon-svn"></i>
29 %endif
26 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
27 ${search.repo_icon(repo_type)}
30 28 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
31 29 </td>
32 30 <td class="td-commit">
@@ -50,14 +48,20 b''
50 48 </td>
51 49
52 50 <td class="td-user author">
53 ${base.gravatar_with_user(entry['author'])}
51 <%
52 ## es6 stores this as object
53 author = entry['author']
54 if isinstance(author, dict):
55 author = author['email']
56 %>
57 ${base.gravatar_with_user(author)}
54 58 </td>
55 59 </tr>
56 60 % endif
57 61 %endfor
58 62 </table>
59 63
60 %if c.cur_query and c.formatted_results:
64 %if c.cur_query:
61 65 <div class="pagination-wh pagination-left">
62 66 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
63 67 </div>
@@ -69,14 +73,26 b''
69 73 var cid = target_expand.data('commit-id');
70 74
71 75 if (target_expand.hasClass('open')){
72 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'})
73 $('#t-'+cid).css({'height': 'auto', 'line-height': '.9em', 'text-overflow': 'ellipsis', 'overflow':'hidden'})
76 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
77 $('#t-'+cid).css({'height': 'auto', 'line-height': '.9em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
74 78 target_expand.removeClass('open');
75 79 }
76 80 else {
77 $('#c-'+cid).css({'height': 'auto', 'white-space': 'normal', 'text-overflow': 'initial', 'overflow':'visible'})
78 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible'})
81 $('#c-'+cid).css({'height': 'auto', 'white-space': 'normal', 'text-overflow': 'initial', 'overflow':'visible'});
82 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible'});
79 83 target_expand.addClass('open');
80 84 }
81 85 });
86
87 $(".message.td-description").mark(
88 "${c.searcher.query_to_mark(c.cur_query, 'message')}",
89 {
90 "className": 'match',
91 "accuracy": "complementary",
92 "ignorePunctuation": ":._(){}[]!'+=".split("")
93 }
94 );
95
82 96 </script>
97
98 % endif
@@ -1,33 +1,11 b''
1 <%def name="highlight_text_file(terms, text, url, line_context=3,
2 max_lines=10,
3 mimetype=None, filepath=None)">
4 <%
5 lines = text.split('\n')
6 lines_of_interest = set()
7 matching_lines = h.get_matching_line_offsets(lines, terms)
8 shown_matching_lines = 0
1 <%namespace name="search" file="/search/search.mako"/>
9 2
10 for line_number in matching_lines:
11 if len(lines_of_interest) < max_lines:
12 lines_of_interest |= set(range(
13 max(line_number - line_context, 0),
14 min(line_number + line_context, len(lines) + 1)))
15 shown_matching_lines += 1
16
17 %>
18 ${h.code_highlight(
19 text,
20 h.get_lexer_safe(
21 mimetype=mimetype,
22 filepath=filepath,
23 ),
24 h.SearchContentCodeHtmlFormatter(
25 linenos=True,
26 cssclass="code-highlight",
27 url=url,
28 query_terms=terms,
29 only_line_numbers=lines_of_interest
30 ))|n}
3 <%def name="highlight_text_file(has_matched_content, file_content, lexer, html_formatter, matching_lines, shown_matching_lines, url, use_hl_filter)">
4 % if has_matched_content:
5 ${h.code_highlight(file_content, lexer, html_formatter, use_hl_filter=use_hl_filter)|n}
6 % else:
7 ${_('No content matched')} <br/>
8 % endif
31 9
32 10 %if len(matching_lines) > shown_matching_lines:
33 11 <a href="${url}">
@@ -37,33 +15,72 b' for line_number in matching_lines:'
37 15 </%def>
38 16
39 17 <div class="search-results">
18 <% query_mark = c.searcher.query_to_mark(c.cur_query, 'content') %>
19
40 20 %for entry in c.formatted_results:
21
22 <%
23 file_content = entry['content_highlight'] or entry['content']
24 mimetype = entry.get('mimetype')
25 filepath = entry.get('path')
26 max_lines = h.safe_int(request.GET.get('max_lines', '10'))
27 line_context = h.safe_int(request.GET.get('line_contenxt', '3'))
28
29 match_file_url=h.route_path('repo_files',repo_name=entry['repository'], commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path'], _query={"mark": query_mark})
30 terms = c.cur_query
31
32 if c.searcher.is_es_6:
33 # use empty terms so we default to markers usage
34 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms=None)
35 else:
36 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms)
37
38 shown_matching_lines = 0
39 lines_of_interest = set()
40 for line_number in matching_lines:
41 if len(lines_of_interest) < max_lines:
42 lines_of_interest |= set(range(
43 max(line_number - line_context, 0),
44 min(line_number + line_context, total_lines + 1)))
45 shown_matching_lines += 1
46 lexer = h.get_lexer_safe(mimetype=mimetype, filepath=filepath)
47
48 html_formatter = h.SearchContentCodeHtmlFormatter(
49 linenos=True,
50 cssclass="code-highlight",
51 url=match_file_url,
52 query_terms=terms,
53 only_line_numbers=lines_of_interest
54 )
55
56 has_matched_content = len(lines_of_interest) >= 1
57
58 %>
41 59 ## search results are additionally filtered, and this check is just a safe gate
42 % if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
60 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
43 61 <div id="codeblock" class="codeblock">
44 62 <div class="codeblock-header">
45 <h2>
46 %if h.get_repo_type_by_name(entry.get('repository')) == 'hg':
47 <i class="icon-hg"></i>
48 %elif h.get_repo_type_by_name(entry.get('repository')) == 'git':
49 <i class="icon-git"></i>
50 %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn':
51 <i class="icon-svn"></i>
52 %endif
63 <h1>
64 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
65 ${search.repo_icon(repo_type)}
53 66 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
54 </h2>
55 <div class="stats">
67 </h1>
68 ## level 1
69 <div class="file-container">
70
71 <div class="pull-left">
72 <span class="stats-filename">
73 <strong>
74 <i class="icon-file-text"></i>
56 75 ${h.link_to(h.literal(entry['f_path']), h.route_path('repo_files',repo_name=entry['repository'],commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path']))}
57 %if entry.get('lines'):
58 | ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
59 %endif
60 %if entry.get('size'):
61 | ${h.format_byte_size_binary(entry['size'])}
62 %endif
63 %if entry.get('mimetype'):
64 | ${entry.get('mimetype', "unknown mimetype")}
65 %endif
76 </strong>
77 </span>
78 <span class="item last">
79 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${entry['f_path']}" title="${_('Copy the full path')}"></i>
80 </span>
66 81 </div>
82
83 <div class="pull-right">
67 84 <div class="buttons">
68 85 <a id="file_history_overview_full" href="${h.route_path('repo_changelog_file',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
69 86 ${_('Show Full History')}
@@ -73,11 +90,70 b' for line_number in matching_lines:'
73 90 | ${h.link_to(_('Download'), h.route_path('repo_file_download',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
74 91 </div>
75 92 </div>
93
94 </div>
95 ## level 2
96 <div class="file-container">
97
98 <div class="pull-left">
99 <span class="stats-first-item">
100 %if entry.get('lines'):
101 ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
102 (${len(matching_lines)} ${_ungettext('matched', 'matched', len(matching_lines))})
103 %endif
104 </span>
105
106 <span>
107 %if entry.get('size'):
108 | ${h.format_byte_size_binary(entry['size'])}
109 %endif
110 </span>
111
112 <span>
113 %if entry.get('mimetype'):
114 | ${entry.get('mimetype', "unknown mimetype")}
115 %endif
116 </span>
117 </div>
118
119 <div class="pull-right">
120 <div class="search-tags">
121
122 <% repo_group = entry.get('repository_group')%>
123 ## hiden if in repo group view
124 % if repo_group and not c.repo_group_name:
125 <span class="tag tag8">
126 ${search.repo_group_icon()}
127 <a href="${h.route_path('search_repo_group', repo_group_name=repo_group, _query={'q': c.cur_query})}">${_('Narrow to this repository group')}</a>
128 </span>
129 % endif
130 ## hiden if in repo view
131 % if not c.repo_name:
132 <span class="tag tag8">
133 ${search.repo_icon(repo_type)}
134 <a href="${h.route_path('search_repo', repo_name=entry.get('repo_name'), _query={'q': c.cur_query})}">${_('Narrow to this repository')}</a>
135 </span>
136 % endif
137 </div>
138 </div>
139
140 </div>
141
142 </div>
76 143 <div class="code-body search-code-body">
77 ${highlight_text_file(c.cur_query, entry['content'],
78 url=h.route_path('repo_files',repo_name=entry['repository'],commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path']),
79 mimetype=entry.get('mimetype'), filepath=entry.get('path'))}
144
145 ${highlight_text_file(
146 has_matched_content=has_matched_content,
147 file_content=file_content,
148 lexer=lexer,
149 html_formatter=html_formatter,
150 matching_lines=matching_lines,
151 shown_matching_lines=shown_matching_lines,
152 url=match_file_url,
153 use_hl_filter=c.searcher.is_es_6
154 )}
80 155 </div>
156
81 157 </div>
82 158 % endif
83 159 %endfor
@@ -91,10 +167,14 b' for line_number in matching_lines:'
91 167 %if c.cur_query:
92 168 <script type="text/javascript">
93 169 $(function(){
94 $(".code").mark(
95 '${' '.join(h.normalize_text_for_matching(c.cur_query).split())}',
96 {"className": 'match',
97 });
170 $(".search-code-body").mark(
171 "${query_mark}",
172 {
173 "className": 'match',
174 "accuracy": "complementary",
175 "ignorePunctuation": ":._(){}[]!'+=".split("")
176 }
177 );
98 178 })
99 179 </script>
100 %endif No newline at end of file
180 %endif
@@ -1,34 +1,46 b''
1 <%namespace name="search" file="/search/search.mako"/>
2
3 % if c.formatted_results:
4
1 5 <table class="rctable search-results">
2 6 <tr>
3 7 <th>${_('Repository')}</th>
4 8 <th>${_('File')}</th>
5 ##TODO: add 'Last Change' and 'Author' here
9 <th>${_('Size')}</th>
10 <th>${_('Lines')}</th>
6 11 </tr>
7 12 %for entry in c.formatted_results:
8 13 ## search results are additionally filtered, and this check is just a safe gate
9 % if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results path check'):
14 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results path check'):
10 15 <tr class="body">
11 16 <td class="td-componentname">
12 %if h.get_repo_type_by_name(entry.get('repository')) == 'hg':
13 <i class="icon-hg"></i>
14 %elif h.get_repo_type_by_name(entry.get('repository')) == 'git':
15 <i class="icon-git"></i>
16 %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn':
17 <i class="icon-svn"></i>
18 %endif
17 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
18 ${search.repo_icon(repo_type)}
19 19 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
20 20 </td>
21 21 <td class="td-componentname">
22 22 ${h.link_to(h.literal(entry['f_path']),
23 23 h.route_path('repo_files',repo_name=entry['repository'],commit_id='tip',f_path=entry['f_path']))}
24 24 </td>
25 <td>
26 %if entry.get('size'):
27 ${h.format_byte_size_binary(entry['size'])}
28 %endif
29 </td>
30 <td>
31 %if entry.get('lines'):
32 ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
33 %endif
34 </td>
25 35 </tr>
26 36 % endif
27 37 %endfor
28 38 </table>
29 39
30 %if c.cur_query and c.formatted_results:
40 %if c.cur_query:
31 41 <div class="pagination-wh pagination-left">
32 42 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
33 43 </div>
34 %endif No newline at end of file
44 %endif
45
46 % endif
@@ -83,9 +83,9 b''
83 83 </div>
84 84 </div>
85 85
86 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
86 <div class="fieldset">
87 87 <div class="left-label-summary">
88 ${_('Information')}:
88 &nbsp;
89 89 </div>
90 90 <div class="right-content">
91 91 <div class="commit-info">
@@ -105,28 +105,42 b''
105 105 ## commits
106 106 <span class="tag">
107 107 % if commit_rev == -1:
108 ${_ungettext('%(num)s Commit', '%(num)s Commits', 0) % {'num': 0}},
108 ${_ungettext('%(num)s Commit', '%(num)s Commits', 0) % {'num': 0}}
109 109 % else:
110 110 <a href="${h.route_path('repo_changelog', repo_name=c.repo_name)}">
111 ${_ungettext('%(num)s Commit', '%(num)s Commits', commit_rev) % {'num': commit_rev}}</a>,
111 ${_ungettext('%(num)s Commit', '%(num)s Commits', commit_rev) % {'num': commit_rev}}</a>
112 112 % endif
113 113 </span>
114 114
115 115 ## forks
116 116 <span class="tag">
117 117 <a title="${_('Number of Repository Forks')}" href="${h.route_path('repo_forks_show_all', repo_name=c.repo_name)}">
118 ${c.repository_forks} ${_ungettext('Fork', 'Forks', c.repository_forks)}</a>,
118 ${c.repository_forks} ${_ungettext('Fork', 'Forks', c.repository_forks)}</a>
119 119 </span>
120 120
121 </div>
122 </div>
123 </div>
124 </div>
125
126 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
127 <div class="left-label-summary">
128 ${_('Repository size')}:
129 </div>
130 <div class="right-content">
131 <div class="commit-info">
132 <div class="tags">
121 133 ## repo size
122 134 % if commit_rev == -1:
123 135 <span class="stats-bullet">0 B</span>
124 136 % else:
125 <span class="stats-bullet" id="repo_size_container">
137 <span>
138 <a href="#showSize" onclick="calculateSize(); $(this).hide(); return false" id="show-repo-size">Show repository size</a>
139 </span>
140 <span class="stats-bullet" id="repo_size_container" style="display:none">
126 141 ${_('Calculating Repository Size...')}
127 142 </span>
128 143 % endif
129
130 144 </div>
131 145 </div>
132 146 </div>
@@ -11,16 +11,19 b''
11 11
12 12 <div class="title">
13 13 ${self.repo_page_title(c.rhodecode_db_repo)}
14 ## Context Action
15 <div>
14 16 <ul class="links icon-only-links block-right">
15 17 <li>
16 18 %if c.rhodecode_user.username != h.DEFAULT_USER:
17 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
19 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
18 20 %else:
19 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name)}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
21 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
20 22 %endif
21 23 </li>
22 24 </ul>
23 25 </div>
26 </div>
24 27
25 28 <div id="repo-summary" class="summary">
26 29 ${components.summary_detail(breadcrumbs_links=self.breadcrumbs_links(), show_downloads=True)}
@@ -106,8 +109,8 b''
106 109 });
107 110
108 111
109 // load details on summary page expand
110 $('#summary_details_expand').on('click', function() {
112 // calculate size of repository
113 calculateSize = function () {
111 114
112 115 var callback = function (data) {
113 116 % if c.show_stats:
@@ -115,13 +118,9 b''
115 118 % endif
116 119 };
117 120
118 showRepoSize(
119 'repo_size_container',
120 templateContext.repo_name,
121 templateContext.repo_landing_commit,
122 callback);
121 showRepoSize('repo_size_container', templateContext.repo_name, templateContext.repo_landing_commit, callback);
123 122
124 })
123 }
125 124
126 125 })
127 126 </script>
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -19,6 +19,7 b''
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 from pyramid import compat
22 23
23 24 from rhodecode.config.utils import set_instance_id
24 25
@@ -31,6 +32,6 b' def test_set_instance_id(instance_id):'
31 32 if instance_id == 'custom-id':
32 33 assert config['instance_id'] == instance_id
33 34 else:
34 assert isinstance(config['instance_id'], basestring)
35 assert isinstance(config['instance_id'], compat.string_types)
35 36 assert len(config['instance_id'])
36 37 assert instance_id != config['instance_id']
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -38,6 +38,8 b' from rhodecode.model.repo_group import R'
38 38 from rhodecode.model.user_group import UserGroupModel
39 39 from rhodecode.model.gist import GistModel
40 40 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.authentication.plugins.auth_rhodecode import \
42 RhodeCodeAuthPlugin
41 43
42 44 dn = os.path.dirname
43 45 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'tests', 'fixtures')
@@ -120,6 +122,70 b' class Fixture(object):'
120 122
121 123 return context()
122 124
125 def auth_restriction(self, auth_restriction):
126 """
127 Context process for changing the builtin rhodecode plugin auth restrictions.
128 Use like:
129 fixture = Fixture()
130 with fixture.auth_restriction('super_admin'):
131 #tests
132
133 after this block auth restriction will be taken off
134 """
135
136 class context(object):
137 def _get_pluing(self):
138 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(
139 RhodeCodeAuthPlugin.uid)
140 plugin = RhodeCodeAuthPlugin(plugin_id)
141 return plugin
142
143 def __enter__(self):
144 plugin = self._get_pluing()
145 plugin.create_or_update_setting(
146 'auth_restriction', auth_restriction)
147 Session().commit()
148
149 def __exit__(self, exc_type, exc_val, exc_tb):
150 plugin = self._get_pluing()
151 plugin.create_or_update_setting(
152 'auth_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE)
153 Session().commit()
154
155 return context()
156
157 def scope_restriction(self, scope_restriction):
158 """
159 Context process for changing the builtin rhodecode plugin scope restrictions.
160 Use like:
161 fixture = Fixture()
162 with fixture.scope_restriction('scope_http'):
163 #tests
164
165 after this block scope restriction will be taken off
166 """
167
168 class context(object):
169 def _get_pluing(self):
170 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(
171 RhodeCodeAuthPlugin.uid)
172 plugin = RhodeCodeAuthPlugin(plugin_id)
173 return plugin
174
175 def __enter__(self):
176 plugin = self._get_pluing()
177 plugin.create_or_update_setting(
178 'scope_restriction', scope_restriction)
179 Session().commit()
180
181 def __exit__(self, exc_type, exc_val, exc_tb):
182 plugin = self._get_pluing()
183 plugin.create_or_update_setting(
184 'scope_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL)
185 Session().commit()
186
187 return context()
188
123 189 def _get_repo_create_params(self, **custom):
124 190 defs = {
125 191 'repo_name': None,
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -78,15 +78,18 b' def test_webook_parse_url_for_create_eve'
78 78 ('http://server.com/${repo_name}/${pull_request_id}',
79 79 ['http://server.com/foo/999']),
80 80 ('http://server.com/${repo_name}/${pull_request_url}',
81 ['http://server.com/foo/http://pr-url.com']),
81 ['http://server.com/foo/http%3A//pr-url.com']),
82 ('http://server.com/${repo_name}/${pull_request_url}/?TITLE=${pull_request_title}',
83 ['http://server.com/foo/http%3A//pr-url.com/?TITLE=example-pr-title%20Ticket%20%23123']),
84 ('http://server.com/${repo_name}/?SHADOW_URL=${pull_request_shadow_url}',
85 ['http://server.com/foo/?SHADOW_URL=http%3A//pr-url.com/repository']),
82 86 ])
83 def test_webook_parse_url_for_pull_request_event(
84 base_data, template, expected_urls):
87 def test_webook_parse_url_for_pull_request_event(base_data, template, expected_urls):
85 88
86 89 base_data['pullrequest'] = {
87 90 'pull_request_id': 999,
88 91 'url': 'http://pr-url.com',
89 'title': 'example-pr-title',
92 'title': 'example-pr-title Ticket #123',
90 93 'commits_uid': 'abcdefg1234',
91 94 'shadow_url': 'http://pr-url.com/repository'
92 95 }
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -81,8 +81,9 b' class TestTokenizeString(object):'
81 81 class TestSplitTokenStream(object):
82 82
83 83 def test_split_token_stream(self):
84 lines = list(split_token_stream(
85 [('type1', 'some\ntext'), ('type2', 'more\n')]))
84 tokens = [('type1', 'some\ntext'), ('type2', 'more\n')]
85 content = [x + y for x, y in tokens]
86 lines = list(split_token_stream(tokens, content))
86 87
87 88 assert lines == [
88 89 [('type1', u'some')],
@@ -91,18 +92,18 b' class TestSplitTokenStream(object):'
91 92 ]
92 93
93 94 def test_split_token_stream_single(self):
94 lines = list(split_token_stream(
95 [('type1', '\n')]))
96
95 tokens = [('type1', '\n')]
96 content = [x + y for x, y in tokens]
97 lines = list(split_token_stream(tokens, content))
97 98 assert lines == [
98 99 [('type1', '')],
99 100 [('type1', '')],
100 101 ]
101 102
102 103 def test_split_token_stream_single_repeat(self):
103 lines = list(split_token_stream(
104 [('type1', '\n\n\n')]))
105
104 tokens = [('type1', '\n\n\n')]
105 content = [x + y for x, y in tokens]
106 lines = list(split_token_stream(tokens, content))
106 107 assert lines == [
107 108 [('type1', '')],
108 109 [('type1', '')],
@@ -111,9 +112,10 b' class TestSplitTokenStream(object):'
111 112 ]
112 113
113 114 def test_split_token_stream_multiple_repeat(self):
114 lines = list(split_token_stream(
115 [('type1', '\n\n'), ('type2', '\n\n')]))
115 tokens = [('type1', '\n\n'), ('type2', '\n\n')]
116 content = [x + y for x, y in tokens]
116 117
118 lines = list(split_token_stream(tokens, content))
117 119 assert lines == [
118 120 [('type1', '')],
119 121 [('type1', '')],
@@ -122,6 +124,27 b' class TestSplitTokenStream(object):'
122 124 [('type2', '')],
123 125 ]
124 126
127 def test_no_tokens_by_content(self):
128 tokens = []
129 content = u'\ufeff'
130 lines = list(split_token_stream(tokens, content))
131 assert lines == [
132 [('', content)],
133 ]
134
135 def test_no_tokens_by_valid_content(self):
136 from pygments.lexers.css import CssLexer
137 content = u'\ufeff table.dataTable'
138 tokens = tokenize_string(content, CssLexer())
139
140 lines = list(split_token_stream(tokens, content))
141 assert lines == [
142 [('', u' '),
143 ('nt', u'table'),
144 ('p', u'.'),
145 ('nc', u'dataTable')],
146 ]
147
125 148
126 149 class TestRollupTokens(object):
127 150
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -208,44 +208,3 b' def test_get_visual_attr(baseapp):'
208 208 def test_chop_at(test_text, inclusive, expected_text):
209 209 assert helpers.chop_at_smart(
210 210 test_text, '\n', inclusive, '...') == expected_text
211
212
213 @pytest.mark.parametrize('test_text, expected_output', [
214 ('some text', ['some', 'text']),
215 ('some text', ['some', 'text']),
216 ('some text "with a phrase"', ['some', 'text', 'with a phrase']),
217 ('"a phrase" "another phrase"', ['a phrase', 'another phrase']),
218 ('"justphrase"', ['justphrase']),
219 ('""', []),
220 ('', []),
221 (' ', []),
222 ('" "', []),
223 ])
224 def test_extract_phrases(test_text, expected_output):
225 assert helpers.extract_phrases(test_text) == expected_output
226
227
228 @pytest.mark.parametrize('test_text, text_phrases, expected_output', [
229 ('some text here', ['some', 'here'], [(0, 4), (10, 14)]),
230 ('here here there', ['here'], [(0, 4), (5, 9), (11, 15)]),
231 ('irrelevant', ['not found'], []),
232 ('irrelevant', ['not found'], []),
233 ])
234 def test_get_matching_offsets(test_text, text_phrases, expected_output):
235 assert helpers.get_matching_offsets(
236 test_text, text_phrases) == expected_output
237
238
239 def test_normalize_text_for_matching():
240 assert helpers.normalize_text_for_matching(
241 'OJjfe)*#$*@)$JF*)3r2f80h') == 'ojjfe jf 3r2f80h'
242
243
244 def test_get_matching_line_offsets():
245 assert helpers.get_matching_line_offsets([
246 'words words words',
247 'words words words',
248 'some text some',
249 'words words words',
250 'words words words',
251 'text here what'], 'text') == {3: [(5, 9)], 6: [(0, 4)]}
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,7 +1,7 b''
1 1 import collections
2 2 # -*- coding: utf-8 -*-
3 3
4 # Copyright (C) 2010-2018 RhodeCode GmbH
4 # Copyright (C) 2010-2019 RhodeCode GmbH
5 5 #
6 6 # This program is free software: you can redistribute it and/or modify
7 7 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -50,9 +50,11 b' class TestPullRequestModel(object):'
50 50 A pull request combined with multiples patches.
51 51 """
52 52 BackendClass = get_backend(backend.alias)
53 merge_resp = MergeResponse(
54 False, False, None, MergeFailureReason.UNKNOWN,
55 metadata={'exception': 'MockError'})
53 56 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
57 BackendClass, 'merge', return_value=merge_resp)
56 58 self.workspace_remove_patcher = mock.patch.object(
57 59 BackendClass, 'cleanup_merge_workspace')
58 60
@@ -162,7 +164,7 b' class TestPullRequestModel(object):'
162 164
163 165 status, msg = PullRequestModel().merge_status(pull_request)
164 166 assert status is True
165 assert msg.eval() == 'This pull request can be automatically merged.'
167 assert msg == 'This pull request can be automatically merged.'
166 168 self.merge_mock.assert_called_with(
167 169 self.repo_id, self.workspace_id,
168 170 pull_request.target_ref_parts,
@@ -177,7 +179,7 b' class TestPullRequestModel(object):'
177 179 self.merge_mock.reset_mock()
178 180 status, msg = PullRequestModel().merge_status(pull_request)
179 181 assert status is True
180 assert msg.eval() == 'This pull request can be automatically merged.'
182 assert msg == 'This pull request can be automatically merged.'
181 183 assert self.merge_mock.called is False
182 184
183 185 def test_merge_status_known_failure(self, pull_request):
@@ -190,9 +192,7 b' class TestPullRequestModel(object):'
190 192
191 193 status, msg = PullRequestModel().merge_status(pull_request)
192 194 assert status is False
193 assert (
194 msg.eval() ==
195 'This pull request cannot be merged because of merge conflicts.')
195 assert msg == 'This pull request cannot be merged because of merge conflicts.'
196 196 self.merge_mock.assert_called_with(
197 197 self.repo_id, self.workspace_id,
198 198 pull_request.target_ref_parts,
@@ -208,14 +208,13 b' class TestPullRequestModel(object):'
208 208 self.merge_mock.reset_mock()
209 209 status, msg = PullRequestModel().merge_status(pull_request)
210 210 assert status is False
211 assert (
212 msg.eval() ==
213 'This pull request cannot be merged because of merge conflicts.')
211 assert msg == 'This pull request cannot be merged because of merge conflicts.'
214 212 assert self.merge_mock.called is False
215 213
216 214 def test_merge_status_unknown_failure(self, pull_request):
217 215 self.merge_mock.return_value = MergeResponse(
218 False, False, None, MergeFailureReason.UNKNOWN)
216 False, False, None, MergeFailureReason.UNKNOWN,
217 metadata={'exception': 'MockError'})
219 218
220 219 assert pull_request._last_merge_source_rev is None
221 220 assert pull_request._last_merge_target_rev is None
@@ -223,9 +222,9 b' class TestPullRequestModel(object):'
223 222
224 223 status, msg = PullRequestModel().merge_status(pull_request)
225 224 assert status is False
226 assert msg.eval() == (
227 'This pull request cannot be merged because of an unhandled'
228 ' exception.')
225 assert msg == (
226 'This pull request cannot be merged because of an unhandled exception. '
227 'MockError')
229 228 self.merge_mock.assert_called_with(
230 229 self.repo_id, self.workspace_id,
231 230 pull_request.target_ref_parts,
@@ -240,26 +239,25 b' class TestPullRequestModel(object):'
240 239 self.merge_mock.reset_mock()
241 240 status, msg = PullRequestModel().merge_status(pull_request)
242 241 assert status is False
243 assert msg.eval() == (
244 'This pull request cannot be merged because of an unhandled'
245 ' exception.')
242 assert msg == (
243 'This pull request cannot be merged because of an unhandled exception. '
244 'MockError')
246 245 assert self.merge_mock.called is True
247 246
248 247 def test_merge_status_when_target_is_locked(self, pull_request):
249 248 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
250 249 status, msg = PullRequestModel().merge_status(pull_request)
251 250 assert status is False
252 assert msg.eval() == (
251 assert msg == (
253 252 'This pull request cannot be merged because the target repository'
254 ' is locked.')
253 'is locked by user:1.')
255 254
256 255 def test_merge_status_requirements_check_target(self, pull_request):
257 256
258 257 def has_largefiles(self, repo):
259 258 return repo == pull_request.source_repo
260 259
261 patcher = mock.patch.object(
262 PullRequestModel, '_has_largefiles', has_largefiles)
260 patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles)
263 261 with patcher:
264 262 status, msg = PullRequestModel().merge_status(pull_request)
265 263
@@ -271,8 +269,7 b' class TestPullRequestModel(object):'
271 269 def has_largefiles(self, repo):
272 270 return repo == pull_request.target_repo
273 271
274 patcher = mock.patch.object(
275 PullRequestModel, '_has_largefiles', has_largefiles)
272 patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles)
276 273 with patcher:
277 274 status, msg = PullRequestModel().merge_status(pull_request)
278 275
@@ -315,9 +312,50 b' class TestPullRequestModel(object):'
315 312 self.pull_request, self.pull_request.author, 'merge')
316 313
317 314 pull_request = PullRequest.get(pull_request.pull_request_id)
318 assert (
319 pull_request.merge_rev ==
320 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
315 assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6'
316
317 def test_merge_with_status_lock(self, pull_request, merge_extras):
318 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
319 merge_ref = Reference(
320 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
321 self.merge_mock.return_value = MergeResponse(
322 True, True, merge_ref, MergeFailureReason.NONE)
323
324 merge_extras['repository'] = pull_request.target_repo.repo_name
325
326 with pull_request.set_state(PullRequest.STATE_UPDATING):
327 assert pull_request.pull_request_state == PullRequest.STATE_UPDATING
328 PullRequestModel().merge_repo(
329 pull_request, pull_request.author, extras=merge_extras)
330
331 assert pull_request.pull_request_state == PullRequest.STATE_CREATED
332
333 message = (
334 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
335 u'\n\n {pr_title}'.format(
336 pr_id=pull_request.pull_request_id,
337 source_repo=safe_unicode(
338 pull_request.source_repo.scm_instance().name),
339 source_ref_name=pull_request.source_ref_parts.name,
340 pr_title=safe_unicode(pull_request.title)
341 )
342 )
343 self.merge_mock.assert_called_with(
344 self.repo_id, self.workspace_id,
345 pull_request.target_ref_parts,
346 pull_request.source_repo.scm_instance(),
347 pull_request.source_ref_parts,
348 user_name=user.short_contact, user_email=user.email, message=message,
349 use_rebase=False, close_branch=False
350 )
351 self.invalidation_mock.assert_called_once_with(
352 pull_request.target_repo.repo_name)
353
354 self.hook_mock.assert_called_with(
355 self.pull_request, self.pull_request.author, 'merge')
356
357 pull_request = PullRequest.get(pull_request.pull_request_id)
358 assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6'
321 359
322 360 def test_merge_failed(self, pull_request, merge_extras):
323 361 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
@@ -460,6 +498,46 b' def test_outdated_comments('
460 498 outdated_comment_mock.assert_called_with(pull_request)
461 499
462 500
501 @pytest.mark.parametrize('mr_type, expected_msg', [
502 (MergeFailureReason.NONE,
503 'This pull request can be automatically merged.'),
504 (MergeFailureReason.UNKNOWN,
505 'This pull request cannot be merged because of an unhandled exception. CRASH'),
506 (MergeFailureReason.MERGE_FAILED,
507 'This pull request cannot be merged because of merge conflicts.'),
508 (MergeFailureReason.PUSH_FAILED,
509 'This pull request could not be merged because push to target:`some-repo@merge_commit` failed.'),
510 (MergeFailureReason.TARGET_IS_NOT_HEAD,
511 'This pull request cannot be merged because the target `ref_name` is not a head.'),
512 (MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES,
513 'This pull request cannot be merged because the source contains more branches than the target.'),
514 (MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
515 'This pull request cannot be merged because the target has multiple heads: `a,b,c`.'),
516 (MergeFailureReason.TARGET_IS_LOCKED,
517 'This pull request cannot be merged because the target repository is locked by user:123.'),
518 (MergeFailureReason.MISSING_TARGET_REF,
519 'This pull request cannot be merged because the target reference `ref_name` is missing.'),
520 (MergeFailureReason.MISSING_SOURCE_REF,
521 'This pull request cannot be merged because the source reference `ref_name` is missing.'),
522 (MergeFailureReason.SUBREPO_MERGE_FAILED,
523 'This pull request cannot be merged because of conflicts related to sub repositories.'),
524
525 ])
526 def test_merge_response_message(mr_type, expected_msg):
527 merge_ref = Reference('type', 'ref_name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
528 metadata = {
529 'exception': "CRASH",
530 'target': 'some-repo',
531 'merge_commit': 'merge_commit',
532 'target_ref': merge_ref,
533 'source_ref': merge_ref,
534 'heads': ','.join(['a', 'b', 'c']),
535 'locked_by': 'user:123'}
536
537 merge_response = MergeResponse(True, True, merge_ref, mr_type, metadata=metadata)
538 assert merge_response.merge_status_message == expected_msg
539
540
463 541 @pytest.fixture
464 542 def merge_extras(user_regular):
465 543 """
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -20,7 +20,7 b''
20 20
21 21 import pytest
22 22
23 from rhodecode.controllers import utils
23 from rhodecode.lib import view_utils
24 24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 25 import mock
26 26
@@ -34,11 +34,11 b' def test_parse_path_ref_understands_form'
34 34 repo = mock.Mock(alias=alias)
35 35
36 36 # Formatting of reference ids as it is used by controllers
37 format_ref_id = utils.get_format_ref_id(repo)
37 format_ref_id = view_utils.get_format_ref_id(repo)
38 38 formatted_ref_id = format_ref_id(name='name', raw_id='raw_id')
39 39
40 40 # Parsing such a reference back as it is used by controllers
41 result = utils.parse_path_ref(formatted_ref_id)
41 result = view_utils.parse_path_ref(formatted_ref_id)
42 42
43 43 assert list(result) == expected
44 44
@@ -50,7 +50,7 b' def test_parse_path_ref_understands_form'
50 50 ('p@a', None, ('p', 'a')),
51 51 ])
52 52 def test_parse_path_ref(ref, default_path, expected):
53 result = utils.parse_path_ref(ref, default_path)
53 result = view_utils.parse_path_ref(ref, default_path)
54 54 assert list(result) == list(expected)
55 55
56 56
@@ -61,7 +61,7 b' def test_parse_path_ref(ref, default_pat'
61 61 ])
62 62 def test_format_ref_id(alias, expected):
63 63 repo = mock.Mock(alias=alias)
64 format_ref_id = utils.get_format_ref_id(repo)
64 format_ref_id = view_utils.get_format_ref_id(repo)
65 65 result = format_ref_id(name='name', raw_id='raw_id')
66 66 assert result == expected
67 67
@@ -77,7 +77,7 b' class TestGetCommit(object):'
77 77 scm_instance.bookmarks = {ref_name: 'a_book_id'}
78 78
79 79 scm_instance.get_commit.return_value = 'test'
80 commit = utils.get_commit_from_ref_name(repo, ref_name, ref_type)
80 commit = view_utils.get_commit_from_ref_name(repo, ref_name, ref_type)
81 81 scm_instance.get_commit.assert_called_once_with('a_%s_id' % ref_type)
82 82 assert commit == 'test'
83 83
@@ -90,4 +90,4 b' class TestGetCommit(object):'
90 90 repo.scm_instance().tags = {}
91 91 repo.scm_instance().bookmarks = {}
92 92 with pytest.raises(RepositoryError):
93 utils.get_commit_from_ref_name(repo, ref_name, ref_type)
93 view_utils.get_commit_from_ref_name(repo, ref_name, ref_type)
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -661,8 +661,7 b' class Backend(object):'
661 661
662 662 def _next_repo_name(self):
663 663 return u"%s_%s" % (
664 self.invalid_repo_name.sub(u'_', self._test_name),
665 len(self._cleanup_repos))
664 self.invalid_repo_name.sub(u'_', self._test_name), len(self._cleanup_repos))
666 665
667 666 def ensure_file(self, filename, content='Test content\n'):
668 667 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -502,7 +502,7 b' vcs.hooks.protocol = http'
502 502 vcs.hooks.host = 127.0.0.1
503 503
504 504 vcs.server.log_level = debug
505 ## Start VCSServer with this instance as a subprocess, usefull for development
505 ## Start VCSServer with this instance as a subprocess, Useful for development
506 506 vcs.start_server = false
507 507
508 508 ## List of enabled VCS backends, available options are:
@@ -562,7 +562,7 b' ssh.wrapper_cmd = ~/.rccontrol/community'
562 562 ## Allow shell when executing the ssh-wrapper command
563 563 ssh.wrapper_cmd_allow_shell = false
564 564
565 ## Enables logging, and detailed output send back to the client. Usefull for
565 ## Enables logging, and detailed output send back to the client. Useful for
566 566 ## debugging, shouldn't be used in production.
567 567 ssh.enable_debug_logging = false
568 568
@@ -572,6 +572,10 b' ssh.executable.hg = ~/.rccontrol/vcsserv'
572 572 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
573 573 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
574 574
575 ## Enables SSH key generator web interface. Disabling this still allows users
576 ## to add their own keys.
577 ssh.enable_ui_key_generator = true
578
575 579
576 580 ## Dummy marker to add new entries after.
577 581 ## Add any custom entries below. Please don't remove.
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -35,7 +35,7 b' import os'
35 35 import sys
36 36 from os.path import join as jn
37 37 from os.path import dirname as dn
38
38 from pyramid import compat
39 39 from sqlalchemy.util import OrderedSet
40 40
41 41 __here__ = os.path.abspath(__file__)
@@ -77,7 +77,7 b' urllib2.install_opener(o)'
77 77
78 78
79 79 def _get_repo(proj):
80 if isinstance(proj, basestring):
80 if isinstance(proj, compat.string_types):
81 81 repo = vcs.get_repo(jn(PROJECT_PATH, proj))
82 82 proj = proj
83 83 else:
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -101,8 +101,7 b' class CustomTestResponse(TestResponse):'
101 101 """
102 102
103 103 from pyramid_beaker import session_factory_from_settings
104 session = session_factory_from_settings(
105 self.test_app.app.config.get_settings())
104 session = session_factory_from_settings(self.test_app._pyramid_settings)
106 105 return session(self.request)
107 106
108 107
@@ -118,7 +117,7 b' class TestRequest(webob.BaseRequest):'
118 117
119 118 class CustomTestApp(TestApp):
120 119 """
121 Custom app to make mustcontain more usefull, and extract special methods
120 Custom app to make mustcontain more Useful, and extract special methods
122 121 """
123 122 RequestClass = TestRequest
124 123 rc_login_data = {}
@@ -140,6 +139,14 b' class CustomTestApp(TestApp):'
140 139 def csrf_token(self):
141 140 return self.rc_login_data['csrf_token']
142 141
142 @property
143 def _pyramid_registry(self):
144 return self.app.config.registry
145
146 @property
147 def _pyramid_settings(self):
148 return self._pyramid_registry.settings
149
143 150
144 151 def set_anonymous_access(enabled):
145 152 """(Dis)allows anonymous access depending on parameter `enabled`"""
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -681,9 +681,11 b' TODO: To be written...'
681 681 workspace_id = 'test-merge'
682 682
683 683 assert len(target_repo._heads(branch='default')) == 2
684 heads = target_repo._heads(branch='default')
684 685 expected_merge_response = MergeResponse(
685 686 False, False, None,
686 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
687 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
688 metadata={'heads': heads})
687 689 repo_id = repo_id_generator(target_repo.path)
688 690 merge_response = target_repo.merge(
689 691 repo_id, workspace_id, target_ref, source_repo, source_ref,
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -284,11 +284,9 b' class TestRepositoryMerge(object):'
284 284 self.source_commit = self.source_repo.get_commit()
285 285 # This only works for Git and Mercurial
286 286 default_branch = self.target_repo.DEFAULT_BRANCH_NAME
287 self.target_ref = Reference(
288 'branch', default_branch, self.target_commit.raw_id)
289 self.source_ref = Reference(
290 'branch', default_branch, self.source_commit.raw_id)
291 self.workspace_id = 'test-merge'
287 self.target_ref = Reference('branch', default_branch, self.target_commit.raw_id)
288 self.source_ref = Reference('branch', default_branch, self.source_commit.raw_id)
289 self.workspace_id = 'test-merge-{}'.format(vcsbackend.alias)
292 290 self.repo_id = repo_id_generator(self.target_repo.path)
293 291
294 292 def prepare_for_conflict(self, vcsbackend):
@@ -300,11 +298,9 b' class TestRepositoryMerge(object):'
300 298 self.source_commit = self.source_repo.get_commit()
301 299 # This only works for Git and Mercurial
302 300 default_branch = self.target_repo.DEFAULT_BRANCH_NAME
303 self.target_ref = Reference(
304 'branch', default_branch, self.target_commit.raw_id)
305 self.source_ref = Reference(
306 'branch', default_branch, self.source_commit.raw_id)
307 self.workspace_id = 'test-merge'
301 self.target_ref = Reference('branch', default_branch, self.target_commit.raw_id)
302 self.source_ref = Reference('branch', default_branch, self.source_commit.raw_id)
303 self.workspace_id = 'test-merge-{}'.format(vcsbackend.alias)
308 304 self.repo_id = repo_id_generator(self.target_repo.path)
309 305
310 306 def test_merge_success(self, vcsbackend):
@@ -367,10 +363,11 b' class TestRepositoryMerge(object):'
367 363
368 364 # Multiple merges may differ in their commit id. Therefore we set the
369 365 # commit id to `None` before comparing the merge responses.
370 merge_response = merge_response._replace(
371 merge_ref=merge_response.merge_ref._replace(commit_id=None))
372 merge_response_update = merge_response_update._replace(
373 merge_ref=merge_response_update.merge_ref._replace(commit_id=None))
366 new_merge_ref = merge_response.merge_ref._replace(commit_id=None)
367 merge_response.merge_ref = new_merge_ref
368
369 new_update_merge_ref = merge_response_update.merge_ref._replace(commit_id=None)
370 merge_response_update.merge_ref = new_update_merge_ref
374 371
375 372 assert merge_response == merge_response_update
376 373 assert merge_response.possible is True
@@ -381,6 +378,7 b' class TestRepositoryMerge(object):'
381 378 @pytest.mark.parametrize('dry_run', [True, False])
382 379 def test_merge_conflict(self, vcsbackend, dry_run):
383 380 self.prepare_for_conflict(vcsbackend)
381
384 382 expected_merge_response = MergeResponse(
385 383 False, False, None, MergeFailureReason.MERGE_FAILED)
386 384
@@ -399,12 +397,11 b' class TestRepositoryMerge(object):'
399 397
400 398 def test_merge_target_is_not_head(self, vcsbackend):
401 399 self.prepare_for_success(vcsbackend)
402 expected_merge_response = MergeResponse(
403 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
404
405 400 target_ref = Reference(
406 401 self.target_ref.type, self.target_ref.name, '0' * 40)
407
402 expected_merge_response = MergeResponse(
403 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
404 metadata={'target_ref': target_ref})
408 405 merge_response = self.target_repo.merge(
409 406 self.repo_id, self.workspace_id, target_ref, self.source_repo,
410 407 self.source_ref, dry_run=True)
@@ -413,11 +410,12 b' class TestRepositoryMerge(object):'
413 410
414 411 def test_merge_missing_source_reference(self, vcsbackend):
415 412 self.prepare_for_success(vcsbackend)
416 expected_merge_response = MergeResponse(
417 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
418 413
419 414 source_ref = Reference(
420 415 self.source_ref.type, 'not_existing', self.source_ref.commit_id)
416 expected_merge_response = MergeResponse(
417 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
418 metadata={'source_ref': source_ref})
421 419
422 420 merge_response = self.target_repo.merge(
423 421 self.repo_id, self.workspace_id, self.target_ref,
@@ -429,7 +427,8 b' class TestRepositoryMerge(object):'
429 427 def test_merge_raises_exception(self, vcsbackend):
430 428 self.prepare_for_success(vcsbackend)
431 429 expected_merge_response = MergeResponse(
432 False, False, None, MergeFailureReason.UNKNOWN)
430 False, False, None, MergeFailureReason.UNKNOWN,
431 metadata={'exception': 'ErrorForTest'})
433 432
434 433 with mock.patch.object(self.target_repo, '_merge_repo',
435 434 side_effect=RepositoryError()):
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,4 +1,4 b''
1 # Copyright (C) 2016-2018 RhodeCode GmbH
1 # Copyright (C) 2016-2019 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -1,6 +1,6 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
@@ -166,7 +166,6 b' setup('
166 166 'enterprise=rhodecode.tests.plugin',
167 167 ],
168 168 'console_scripts': [
169 'rc-server=rhodecode.rcserver:main',
170 169 'rc-setup-app=rhodecode.lib.rc_commands.setup_rc:main',
171 170 'rc-upgrade-db=rhodecode.lib.rc_commands.upgrade_db:main',
172 171 'rc-ishell=rhodecode.lib.rc_commands.ishell:main',
@@ -1,48 +0,0 b''
1 .. _views-ref:
2
3 views
4 =====
5
6 push (EE only)
7 --------------
8
9 .. py:function:: push(apiuser, repoid, remote_uri=<Optional:None>)
10
11 Triggers a push on the given repository from a remote location. You
12 can use this to keep remote repositories up-to-date.
13
14 This command can only be run using an |authtoken| with admin
15 rights to the specified repository. For more information,
16 see :ref:`config-token-ref`.
17
18 This command takes the following options:
19
20 :param apiuser: This is filled automatically from the |authtoken|.
21 :type apiuser: AuthUser
22 :param repoid: The repository name or repository ID.
23 :type repoid: str or int
24 :param remote_uri: Optional remote URI to pass in for push
25 :type remote_uri: str
26
27 Example output:
28
29 .. code-block:: bash
30
31 id : <id_given_in_input>
32 result : {
33 "msg": "Pushed to url `<remote_url>` on repo `<repository name>`"
34 "repository": "<repository name>"
35 }
36 error : null
37
38 Example error output:
39
40 .. code-block:: bash
41
42 id : <id_given_in_input>
43 result : null
44 error : {
45 "Unable to push changes to `<remote_url>`"
46 }
47
48
@@ -1,19 +0,0 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,490 +0,0 b''
1 /*!***************************************************
2 * mark.js v6.1.0
3 * https://github.com/julmot/mark.js
4 * Copyright (c) 2014–2016, Julian Motz
5 * Released under the MIT license https://git.io/vwTVl
6 *****************************************************/
7
8 "use strict";
9
10 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
11
12 var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
13
14 var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
15
16 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
17
18 (function (factory, window, document) {
19 if (typeof define === "function" && define.amd) {
20 define(["jquery"], function (jQuery) {
21 return factory(window, document, jQuery);
22 });
23 } else if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === "object") {
24 factory(window, document, require("jquery"));
25 } else {
26 factory(window, document, jQuery);
27 }
28 })(function (window, document, $) {
29 var Mark = function () {
30 function Mark(ctx) {
31 _classCallCheck(this, Mark);
32
33 this.ctx = ctx;
34 }
35
36 _createClass(Mark, [{
37 key: "log",
38 value: function log(msg) {
39 var level = arguments.length <= 1 || arguments[1] === undefined ? "debug" : arguments[1];
40
41 var log = this.opt.log;
42 if (!this.opt.debug) {
43 return;
44 }
45 if ((typeof log === "undefined" ? "undefined" : _typeof(log)) === "object" && typeof log[level] === "function") {
46 log[level]("mark.js: " + msg);
47 }
48 }
49 }, {
50 key: "escapeStr",
51 value: function escapeStr(str) {
52 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
53 }
54 }, {
55 key: "createRegExp",
56 value: function createRegExp(str) {
57 str = this.escapeStr(str);
58 if (Object.keys(this.opt.synonyms).length) {
59 str = this.createSynonymsRegExp(str);
60 }
61 if (this.opt.diacritics) {
62 str = this.createDiacriticsRegExp(str);
63 }
64 str = this.createAccuracyRegExp(str);
65 return str;
66 }
67 }, {
68 key: "createSynonymsRegExp",
69 value: function createSynonymsRegExp(str) {
70 var syn = this.opt.synonyms;
71 for (var index in syn) {
72 if (syn.hasOwnProperty(index)) {
73 var value = syn[index],
74 k1 = this.escapeStr(index),
75 k2 = this.escapeStr(value);
76 str = str.replace(new RegExp("(" + k1 + "|" + k2 + ")", "gmi"), "(" + k1 + "|" + k2 + ")");
77 }
78 }
79 return str;
80 }
81 }, {
82 key: "createDiacriticsRegExp",
83 value: function createDiacriticsRegExp(str) {
84 var dct = ["aÀÁÂÃÄÅàáâãäåĀāąĄ", "cÇçćĆčČ", "dđĐďĎ", "eÈÉÊËèéêëěĚĒēęĘ", "iÌÍÎÏìíîïĪī", "lłŁ", "nÑñňŇńŃ", "oÒÓÔÕÕÖØòóôõöøŌō", "rřŘ", "sŠšśŚ", "tťŤ", "uÙÚÛÜùúûüůŮŪū", "yŸÿýÝ", "zŽžżŻźŹ"];
85 var handled = [];
86 str.split("").forEach(function (ch) {
87 dct.every(function (dct) {
88 if (dct.indexOf(ch) !== -1) {
89 if (handled.indexOf(dct) > -1) {
90 return false;
91 }
92
93 str = str.replace(new RegExp("[" + dct + "]", "gmi"), "[" + dct + "]");
94 handled.push(dct);
95 }
96 return true;
97 });
98 });
99 return str;
100 }
101 }, {
102 key: "createAccuracyRegExp",
103 value: function createAccuracyRegExp(str) {
104 switch (this.opt.accuracy) {
105 case "partially":
106 return "()(" + str + ")";
107 case "complementary":
108 return "()(\\S*" + str + "\\S*)";
109 case "exactly":
110 return "(^|\\s)(" + str + ")(?=\\s|$)";
111 }
112 }
113 }, {
114 key: "getSeparatedKeywords",
115 value: function getSeparatedKeywords(sv) {
116 var _this = this;
117
118 var stack = [];
119 sv.forEach(function (kw) {
120 if (!_this.opt.separateWordSearch) {
121 if (kw.trim()) {
122 stack.push(kw);
123 }
124 } else {
125 kw.split(" ").forEach(function (kwSplitted) {
126 if (kwSplitted.trim()) {
127 stack.push(kwSplitted);
128 }
129 });
130 }
131 });
132 return {
133 "keywords": stack,
134 "length": stack.length
135 };
136 }
137 }, {
138 key: "getElements",
139 value: function getElements() {
140 var ctx = void 0,
141 stack = [];
142 if (typeof this.ctx === "undefined") {
143 ctx = [];
144 } else if (this.ctx instanceof HTMLElement) {
145 ctx = [this.ctx];
146 } else if (Array.isArray(this.ctx)) {
147 ctx = this.ctx;
148 } else {
149 ctx = Array.prototype.slice.call(this.ctx);
150 }
151 ctx.forEach(function (ctx) {
152 stack.push(ctx);
153 var childs = ctx.querySelectorAll("*");
154 if (childs.length) {
155 stack = stack.concat(Array.prototype.slice.call(childs));
156 }
157 });
158 if (!ctx.length) {
159 this.log("Empty context", "warn");
160 }
161 return {
162 "elements": stack,
163 "length": stack.length
164 };
165 }
166 }, {
167 key: "matches",
168 value: function matches(el, selector) {
169 return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
170 }
171 }, {
172 key: "matchesFilter",
173 value: function matchesFilter(el, exclM) {
174 var _this2 = this;
175
176 var remain = true;
177 var fltr = this.opt.filter.concat(["script", "style", "title"]);
178 if (!this.opt.iframes) {
179 fltr = fltr.concat(["iframe"]);
180 }
181 if (exclM) {
182 fltr = fltr.concat(["*[data-markjs='true']"]);
183 }
184 fltr.every(function (filter) {
185 if (_this2.matches(el, filter)) {
186 return remain = false;
187 }
188 return true;
189 });
190 return !remain;
191 }
192 }, {
193 key: "onIframeReady",
194 value: function onIframeReady(ifr, successFn, errorFn) {
195 try {
196 (function () {
197 var ifrWin = ifr.contentWindow,
198 bl = "about:blank",
199 compl = "complete";
200 var callCallback = function callCallback() {
201 try {
202 if (ifrWin.document === null) {
203 throw new Error("iframe inaccessible");
204 }
205 successFn(ifrWin.document);
206 } catch (e) {
207 errorFn();
208 }
209 };
210 var isBlank = function isBlank() {
211 var src = ifr.getAttribute("src").trim(),
212 href = ifrWin.location.href;
213 return href === bl && src !== bl && src;
214 };
215 var observeOnload = function observeOnload() {
216 var listener = function listener() {
217 try {
218 if (!isBlank()) {
219 ifr.removeEventListener("load", listener);
220 callCallback();
221 }
222 } catch (e) {
223 errorFn();
224 }
225 };
226 ifr.addEventListener("load", listener);
227 };
228 if (ifrWin.document.readyState === compl) {
229 if (isBlank()) {
230 observeOnload();
231 } else {
232 callCallback();
233 }
234 } else {
235 observeOnload();
236 }
237 })();
238 } catch (e) {
239 errorFn();
240 }
241 }
242 }, {
243 key: "forEachElementInIframe",
244 value: function forEachElementInIframe(ifr, cb) {
245 var _this3 = this;
246
247 var end = arguments.length <= 2 || arguments[2] === undefined ? function () {} : arguments[2];
248
249 var open = 0;
250 var checkEnd = function checkEnd() {
251 if (--open < 1) {
252 end();
253 }
254 };
255 this.onIframeReady(ifr, function (con) {
256 var stack = Array.prototype.slice.call(con.querySelectorAll("*"));
257 if ((open = stack.length) === 0) {
258 checkEnd();
259 }
260 stack.forEach(function (el) {
261 if (el.tagName.toLowerCase() === "iframe") {
262 (function () {
263 var j = 0;
264 _this3.forEachElementInIframe(el, function (iel, len) {
265 cb(iel, len);
266 if (len - 1 === j) {
267 checkEnd();
268 }
269 j++;
270 }, checkEnd);
271 })();
272 } else {
273 cb(el, stack.length);
274 checkEnd();
275 }
276 });
277 }, function () {
278 var src = ifr.getAttribute("src");
279 _this3.log("iframe '" + src + "' could not be accessed", "warn");
280 checkEnd();
281 });
282 }
283 }, {
284 key: "forEachElement",
285 value: function forEachElement(cb) {
286 var _this4 = this;
287
288 var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1];
289 var exclM = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];
290
291 var _getElements = this.getElements();
292
293 var stack = _getElements.elements;
294 var open = _getElements.length;
295
296 var checkEnd = function checkEnd() {
297 if (--open === 0) {
298 end();
299 }
300 };
301 checkEnd(++open);
302 stack.forEach(function (el) {
303 if (!_this4.matchesFilter(el, exclM)) {
304 if (el.tagName.toLowerCase() === "iframe") {
305 _this4.forEachElementInIframe(el, function (iel) {
306 if (!_this4.matchesFilter(iel, exclM)) {
307 cb(iel);
308 }
309 }, checkEnd);
310 return;
311 } else {
312 cb(el);
313 }
314 }
315 checkEnd();
316 });
317 }
318 }, {
319 key: "forEachNode",
320 value: function forEachNode(cb) {
321 var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1];
322
323 this.forEachElement(function (n) {
324 for (n = n.firstChild; n; n = n.nextSibling) {
325 if (n.nodeType === 3 && n.textContent.trim()) {
326 cb(n);
327 }
328 }
329 }, end);
330 }
331 }, {
332 key: "wrapMatches",
333 value: function wrapMatches(node, regex, custom, cb) {
334 var hEl = !this.opt.element ? "mark" : this.opt.element,
335 index = custom ? 0 : 2;
336 var match = void 0;
337 while ((match = regex.exec(node.textContent)) !== null) {
338 var pos = match.index;
339 if (!custom) {
340 pos += match[index - 1].length;
341 }
342 var startNode = node.splitText(pos);
343
344 node = startNode.splitText(match[index].length);
345 if (startNode.parentNode !== null) {
346 var repl = document.createElement(hEl);
347 repl.setAttribute("data-markjs", "true");
348 if (this.opt.className) {
349 repl.setAttribute("class", this.opt.className);
350 }
351 repl.textContent = match[index];
352 startNode.parentNode.replaceChild(repl, startNode);
353 cb(repl);
354 }
355 regex.lastIndex = 0;
356 }
357 }
358 }, {
359 key: "unwrapMatches",
360 value: function unwrapMatches(node) {
361 var parent = node.parentNode;
362 var docFrag = document.createDocumentFragment();
363 while (node.firstChild) {
364 docFrag.appendChild(node.removeChild(node.firstChild));
365 }
366 parent.replaceChild(docFrag, node);
367 parent.normalize();
368 }
369 }, {
370 key: "markRegExp",
371 value: function markRegExp(regexp, opt) {
372 var _this5 = this;
373
374 this.opt = opt;
375 this.log("Searching with expression \"" + regexp + "\"");
376 var found = false;
377 var eachCb = function eachCb(element) {
378 found = true;
379 _this5.opt.each(element);
380 };
381 this.forEachNode(function (node) {
382 _this5.wrapMatches(node, regexp, true, eachCb);
383 }, function () {
384 if (!found) {
385 _this5.opt.noMatch(regexp);
386 }
387 _this5.opt.complete();
388 _this5.opt.done();
389 });
390 }
391 }, {
392 key: "mark",
393 value: function mark(sv, opt) {
394 var _this6 = this;
395
396 this.opt = opt;
397 sv = typeof sv === "string" ? [sv] : sv;
398
399 var _getSeparatedKeywords = this.getSeparatedKeywords(sv);
400
401 var kwArr = _getSeparatedKeywords.keywords;
402 var kwArrLen = _getSeparatedKeywords.length;
403
404 if (kwArrLen === 0) {
405 this.opt.complete();
406 this.opt.done();
407 }
408 kwArr.forEach(function (kw) {
409 var regex = new RegExp(_this6.createRegExp(kw), "gmi"),
410 found = false;
411 var eachCb = function eachCb(element) {
412 found = true;
413 _this6.opt.each(element);
414 };
415 _this6.log("Searching with expression \"" + regex + "\"");
416 _this6.forEachNode(function (node) {
417 _this6.wrapMatches(node, regex, false, eachCb);
418 }, function () {
419 if (!found) {
420 _this6.opt.noMatch(kw);
421 }
422 if (kwArr[kwArrLen - 1] === kw) {
423 _this6.opt.complete();
424 _this6.opt.done();
425 }
426 });
427 });
428 }
429 }, {
430 key: "unmark",
431 value: function unmark(opt) {
432 var _this7 = this;
433
434 this.opt = opt;
435 var sel = this.opt.element ? this.opt.element : "*";
436 sel += "[data-markjs]";
437 if (this.opt.className) {
438 sel += "." + this.opt.className;
439 }
440 this.log("Removal selector \"" + sel + "\"");
441 this.forEachElement(function (el) {
442 if (_this7.matches(el, sel)) {
443 _this7.unwrapMatches(el);
444 }
445 }, function () {
446 _this7.opt.complete();
447 _this7.opt.done();
448 }, false);
449 }
450 }, {
451 key: "opt",
452 set: function set(val) {
453 this._opt = _extends({}, {
454 "element": "",
455 "className": "",
456 "filter": [],
457 "iframes": false,
458 "separateWordSearch": true,
459 "diacritics": true,
460 "synonyms": {},
461 "accuracy": "partially",
462 "each": function each() {},
463 "noMatch": function noMatch() {},
464 "done": function done() {},
465 "complete": function complete() {},
466 "debug": false,
467 "log": window.console
468 }, val);
469 },
470 get: function get() {
471 return this._opt;
472 }
473 }]);
474
475 return Mark;
476 }();
477
478 $.fn.mark = function (sv, opt) {
479 new Mark(this).mark(sv, opt);
480 return this;
481 };
482 $.fn.markRegExp = function (regexp, opt) {
483 new Mark(this).markRegExp(regexp, opt);
484 return this;
485 };
486 $.fn.unmark = function (opt) {
487 new Mark(this).unmark(opt);
488 return this;
489 };
490 }, window, document);
This diff has been collapsed as it changes many lines, (1024 lines changed) Show them Hide them
@@ -1,1024 +0,0 b''
1 # (c) 2005 Ian Bicking and contributors; written for Paste
2 # (http://pythonpaste.org) Licensed under the MIT license:
3 # http://www.opensource.org/licenses/mit-license.php
4 #
5 # For discussion of daemonizing:
6 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731
7 #
8 # Code taken also from QP: http://www.mems-exchange.org/software/qp/ From
9 # lib/site.py
10
11 import atexit
12 import errno
13 import fnmatch
14 import logging
15 import optparse
16 import os
17 import re
18 import subprocess32
19 import sys
20 import textwrap
21 import threading
22 import time
23 import traceback
24
25 from logging.config import fileConfig
26 import ConfigParser as configparser
27 from paste.deploy import loadserver
28 from paste.deploy import loadapp
29
30 import rhodecode
31 from rhodecode.lib.compat import kill
32
33
34 def make_web_build_callback(filename):
35 p = subprocess32.Popen('make web-build', shell=True,
36 stdout=subprocess32.PIPE,
37 stderr=subprocess32.PIPE,
38 cwd=os.path.dirname(os.path.dirname(__file__)))
39 stdout, stderr = p.communicate()
40 stdout = ''.join(stdout)
41 stderr = ''.join(stderr)
42 if stdout:
43 print(stdout)
44 if stderr:
45 print('%s %s %s' % ('-' * 20, 'ERRORS', '-' * 20))
46 print(stderr)
47
48
49 MAXFD = 1024
50 HERE = os.path.dirname(os.path.abspath(__file__))
51 SERVER_RUNNING_FILE = None
52
53
54 # watch those extra files for changes, server gets restarted if file changes
55 GLOBAL_EXTRA_FILES = {
56 'rhodecode/public/css/*.less': make_web_build_callback,
57 'rhodecode/public/js/src/**/*.js': make_web_build_callback,
58 }
59
60
61
62 ## HOOKS - inspired by gunicorn #
63
64 def when_ready(server):
65 """
66 Called just after the server is started.
67 """
68
69 def _remove_server_running_file():
70 if os.path.isfile(SERVER_RUNNING_FILE):
71 os.remove(SERVER_RUNNING_FILE)
72
73 if SERVER_RUNNING_FILE:
74 with open(SERVER_RUNNING_FILE, 'wb') as f:
75 f.write(str(os.getpid()))
76 # register cleanup of that file when server exits
77 atexit.register(_remove_server_running_file)
78
79
80 def setup_logging(config_uri, fileConfig=fileConfig,
81 configparser=configparser):
82 """
83 Set up logging via the logging module's fileConfig function with the
84 filename specified via ``config_uri`` (a string in the form
85 ``filename#sectionname``).
86
87 ConfigParser defaults are specified for the special ``__file__``
88 and ``here`` variables, similar to PasteDeploy config loading.
89 """
90 path, _ = _getpathsec(config_uri, None)
91 parser = configparser.ConfigParser()
92 parser.read([path])
93 if parser.has_section('loggers'):
94 config_file = os.path.abspath(path)
95 return fileConfig(
96 config_file,
97 {'__file__': config_file, 'here': os.path.dirname(config_file)}
98 )
99
100
101 def set_rhodecode_is_test(config_uri):
102 """If is_test is defined in the config file sets rhodecode.is_test."""
103 path, _ = _getpathsec(config_uri, None)
104 parser = configparser.ConfigParser()
105 parser.read(path)
106 rhodecode.is_test = (
107 parser.has_option('app:main', 'is_test') and
108 parser.getboolean('app:main', 'is_test'))
109
110
111 def _getpathsec(config_uri, name):
112 if '#' in config_uri:
113 path, section = config_uri.split('#', 1)
114 else:
115 path, section = config_uri, 'main'
116 if name:
117 section = name
118 return path, section
119
120
121 def parse_vars(args):
122 """
123 Given variables like ``['a=b', 'c=d']`` turns it into ``{'a':
124 'b', 'c': 'd'}``
125 """
126 result = {}
127 for arg in args:
128 if '=' not in arg:
129 raise ValueError(
130 'Variable assignment %r invalid (no "=")'
131 % arg)
132 name, value = arg.split('=', 1)
133 result[name] = value
134 return result
135
136
137 def _match_pattern(filename):
138 for pattern in GLOBAL_EXTRA_FILES:
139 if fnmatch.fnmatch(filename, pattern):
140 return pattern
141 return False
142
143
144 def generate_extra_file_list():
145
146 extra_list = []
147 for root, dirs, files in os.walk(HERE, topdown=True):
148 for fname in files:
149 stripped_src = os.path.join(
150 'rhodecode', os.path.relpath(os.path.join(root, fname), HERE))
151
152 if _match_pattern(stripped_src):
153 extra_list.append(stripped_src)
154
155 return extra_list
156
157
158 def run_callback_for_pattern(filename):
159 pattern = _match_pattern(filename)
160 if pattern:
161 _file_callback = GLOBAL_EXTRA_FILES.get(pattern)
162 if callable(_file_callback):
163 _file_callback(filename)
164
165
166 class DaemonizeException(Exception):
167 pass
168
169
170 class RcServerCommand(object):
171
172 usage = '%prog config_uri [start|stop|restart|status] [var=value]'
173 description = """\
174 This command serves a web application that uses a PasteDeploy
175 configuration file for the server and application.
176
177 If start/stop/restart is given, then --daemon is implied, and it will
178 start (normal operation), stop (--stop-daemon), or do both.
179
180 You can also include variable assignments like 'http_port=8080'
181 and then use %(http_port)s in your config files.
182 """
183 default_verbosity = 1
184
185 parser = optparse.OptionParser(
186 usage,
187 description=textwrap.dedent(description)
188 )
189 parser.add_option(
190 '-n', '--app-name',
191 dest='app_name',
192 metavar='NAME',
193 help="Load the named application (default main)")
194 parser.add_option(
195 '-s', '--server',
196 dest='server',
197 metavar='SERVER_TYPE',
198 help="Use the named server.")
199 parser.add_option(
200 '--server-name',
201 dest='server_name',
202 metavar='SECTION_NAME',
203 help=("Use the named server as defined in the configuration file "
204 "(default: main)"))
205 parser.add_option(
206 '--with-vcsserver',
207 dest='vcs_server',
208 action='store_true',
209 help=("Start the vcsserver instance together with the RhodeCode server"))
210 if hasattr(os, 'fork'):
211 parser.add_option(
212 '--daemon',
213 dest="daemon",
214 action="store_true",
215 help="Run in daemon (background) mode")
216 parser.add_option(
217 '--pid-file',
218 dest='pid_file',
219 metavar='FILENAME',
220 help=("Save PID to file (default to pyramid.pid if running in "
221 "daemon mode)"))
222 parser.add_option(
223 '--running-file',
224 dest='running_file',
225 metavar='RUNNING_FILE',
226 help="Create a running file after the server is initalized with "
227 "stored PID of process")
228 parser.add_option(
229 '--log-file',
230 dest='log_file',
231 metavar='LOG_FILE',
232 help="Save output to the given log file (redirects stdout)")
233 parser.add_option(
234 '--reload',
235 dest='reload',
236 action='store_true',
237 help="Use auto-restart file monitor")
238 parser.add_option(
239 '--reload-interval',
240 dest='reload_interval',
241 default=1,
242 help=("Seconds between checking files (low number can cause "
243 "significant CPU usage)"))
244 parser.add_option(
245 '--monitor-restart',
246 dest='monitor_restart',
247 action='store_true',
248 help="Auto-restart server if it dies")
249 parser.add_option(
250 '--status',
251 action='store_true',
252 dest='show_status',
253 help="Show the status of the (presumably daemonized) server")
254 parser.add_option(
255 '-v', '--verbose',
256 default=default_verbosity,
257 dest='verbose',
258 action='count',
259 help="Set verbose level (default "+str(default_verbosity)+")")
260 parser.add_option(
261 '-q', '--quiet',
262 action='store_const',
263 const=0,
264 dest='verbose',
265 help="Suppress verbose output")
266
267 if hasattr(os, 'setuid'):
268 # I don't think these are available on Windows
269 parser.add_option(
270 '--user',
271 dest='set_user',
272 metavar="USERNAME",
273 help="Set the user (usually only possible when run as root)")
274 parser.add_option(
275 '--group',
276 dest='set_group',
277 metavar="GROUP",
278 help="Set the group (usually only possible when run as root)")
279
280 parser.add_option(
281 '--stop-daemon',
282 dest='stop_daemon',
283 action='store_true',
284 help=('Stop a daemonized server (given a PID file, or default '
285 'pyramid.pid file)'))
286
287 _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)
288
289 _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
290 _monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN'
291
292 possible_subcommands = ('start', 'stop', 'restart', 'status')
293
294 def __init__(self, argv, quiet=False):
295 self.options, self.args = self.parser.parse_args(argv[1:])
296 if quiet:
297 self.options.verbose = 0
298
299 def out(self, msg): # pragma: no cover
300 if self.options.verbose > 0:
301 print(msg)
302
303 def get_options(self):
304 if (len(self.args) > 1
305 and self.args[1] in self.possible_subcommands):
306 restvars = self.args[2:]
307 else:
308 restvars = self.args[1:]
309
310 return parse_vars(restvars)
311
312 def run(self): # pragma: no cover
313 if self.options.stop_daemon:
314 return self.stop_daemon()
315
316 if not hasattr(self.options, 'set_user'):
317 # Windows case:
318 self.options.set_user = self.options.set_group = None
319
320 # @@: Is this the right stage to set the user at?
321 self.change_user_group(
322 self.options.set_user, self.options.set_group)
323
324 if not self.args:
325 self.out('Please provide configuration file as first argument, '
326 'most likely it should be production.ini')
327 return 2
328 app_spec = self.args[0]
329
330 if (len(self.args) > 1
331 and self.args[1] in self.possible_subcommands):
332 cmd = self.args[1]
333 else:
334 cmd = None
335
336 if self.options.reload:
337 if os.environ.get(self._reloader_environ_key):
338 if self.options.verbose > 1:
339 self.out('Running reloading file monitor')
340
341 install_reloader(int(self.options.reload_interval),
342 [app_spec] + generate_extra_file_list())
343 # if self.requires_config_file:
344 # watch_file(self.args[0])
345 else:
346 return self.restart_with_reloader()
347
348 if cmd not in (None, 'start', 'stop', 'restart', 'status'):
349 self.out(
350 'Error: must give start|stop|restart (not %s)' % cmd)
351 return 2
352
353 if cmd == 'status' or self.options.show_status:
354 return self.show_status()
355
356 if cmd == 'restart' or cmd == 'stop':
357 result = self.stop_daemon()
358 if result:
359 if cmd == 'restart':
360 self.out("Could not stop daemon; aborting")
361 else:
362 self.out("Could not stop daemon")
363 return result
364 if cmd == 'stop':
365 return result
366 self.options.daemon = True
367
368 if cmd == 'start':
369 self.options.daemon = True
370
371 app_name = self.options.app_name
372
373 vars = self.get_options()
374
375 if self.options.vcs_server:
376 vars['vcs.start_server'] = 'true'
377
378 if self.options.running_file:
379 global SERVER_RUNNING_FILE
380 SERVER_RUNNING_FILE = self.options.running_file
381
382 if not self._scheme_re.search(app_spec):
383 app_spec = 'config:' + app_spec
384 server_name = self.options.server_name
385 if self.options.server:
386 server_spec = 'egg:pyramid'
387 assert server_name is None
388 server_name = self.options.server
389 else:
390 server_spec = app_spec
391 base = os.getcwd()
392
393 if getattr(self.options, 'daemon', False):
394 if not self.options.pid_file:
395 self.options.pid_file = 'pyramid.pid'
396 if not self.options.log_file:
397 self.options.log_file = 'pyramid.log'
398
399 # Ensure the log file is writeable
400 if self.options.log_file:
401 try:
402 writeable_log_file = open(self.options.log_file, 'a')
403 except IOError as ioe:
404 msg = 'Error: Unable to write to log file: %s' % ioe
405 raise ValueError(msg)
406 writeable_log_file.close()
407
408 # Ensure the pid file is writeable
409 if self.options.pid_file:
410 try:
411 writeable_pid_file = open(self.options.pid_file, 'a')
412 except IOError as ioe:
413 msg = 'Error: Unable to write to pid file: %s' % ioe
414 raise ValueError(msg)
415 writeable_pid_file.close()
416
417
418 if getattr(self.options, 'daemon', False):
419 try:
420 self.daemonize()
421 except DaemonizeException as ex:
422 if self.options.verbose > 0:
423 self.out(str(ex))
424 return 2
425
426 if (self.options.monitor_restart
427 and not os.environ.get(self._monitor_environ_key)):
428 return self.restart_with_monitor()
429
430 if self.options.pid_file:
431 self.record_pid(self.options.pid_file)
432
433 if self.options.log_file:
434 stdout_log = LazyWriter(self.options.log_file, 'a')
435 sys.stdout = stdout_log
436 sys.stderr = stdout_log
437 logging.basicConfig(stream=stdout_log)
438
439 log_fn = app_spec
440 if log_fn.startswith('config:'):
441 log_fn = app_spec[len('config:'):]
442 elif log_fn.startswith('egg:'):
443 log_fn = None
444 if log_fn:
445 log_fn = os.path.join(base, log_fn)
446 setup_logging(log_fn)
447 set_rhodecode_is_test(log_fn)
448
449 server = self.loadserver(server_spec, name=server_name,
450 relative_to=base, global_conf=vars)
451 # starting hooks
452 app = self.loadapp(app_spec, name=app_name, relative_to=base,
453 global_conf=vars)
454
455 if self.options.verbose > 0:
456 if hasattr(os, 'getpid'):
457 msg = 'Starting %s in PID %i.' % (__name__, os.getpid())
458 else:
459 msg = 'Starting %s.' % (__name__,)
460 self.out(msg)
461 if SERVER_RUNNING_FILE:
462 self.out('PID file written as %s' % (SERVER_RUNNING_FILE, ))
463 elif not self.options.pid_file:
464 self.out('No PID file written by default.')
465
466 try:
467 when_ready(server)
468 server(app)
469 except (SystemExit, KeyboardInterrupt) as e:
470 if self.options.verbose > 1:
471 raise
472 if str(e):
473 msg = ' ' + str(e)
474 else:
475 msg = ''
476 self.out('Exiting%s (-v to see traceback)' % msg)
477
478 def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover
479 return loadapp(app_spec, name=name, relative_to=relative_to, **kw)
480
481 def loadserver(self, server_spec, name, relative_to, **kw): # pragma: no cover
482 return loadserver(
483 server_spec, name=name, relative_to=relative_to, **kw)
484
485 def quote_first_command_arg(self, arg): # pragma: no cover
486 """
487 There's a bug in Windows when running an executable that's
488 located inside a path with a space in it. This method handles
489 that case, or on non-Windows systems or an executable with no
490 spaces, it just leaves well enough alone.
491 """
492 if sys.platform != 'win32' or ' ' not in arg:
493 # Problem does not apply:
494 return arg
495 try:
496 import win32api
497 except ImportError:
498 raise ValueError(
499 "The executable %r contains a space, and in order to "
500 "handle this issue you must have the win32api module "
501 "installed" % arg)
502 arg = win32api.GetShortPathName(arg)
503 return arg
504
505 def daemonize(self): # pragma: no cover
506 pid = live_pidfile(self.options.pid_file)
507 if pid:
508 raise DaemonizeException(
509 "Daemon is already running (PID: %s from PID file %s)"
510 % (pid, self.options.pid_file))
511
512 if self.options.verbose > 0:
513 self.out('Entering daemon mode')
514 pid = os.fork()
515 if pid:
516 # The forked process also has a handle on resources, so we
517 # *don't* want proper termination of the process, we just
518 # want to exit quick (which os._exit() does)
519 os._exit(0)
520 # Make this the session leader
521 os.setsid()
522 # Fork again for good measure!
523 pid = os.fork()
524 if pid:
525 os._exit(0)
526
527 # @@: Should we set the umask and cwd now?
528
529 import resource # Resource usage information.
530 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
531 if maxfd == resource.RLIM_INFINITY:
532 maxfd = MAXFD
533 # Iterate through and close all file descriptors.
534 for fd in range(0, maxfd):
535 try:
536 os.close(fd)
537 except OSError: # ERROR, fd wasn't open to begin with (ignored)
538 pass
539
540 if hasattr(os, "devnull"):
541 REDIRECT_TO = os.devnull
542 else:
543 REDIRECT_TO = "/dev/null"
544 os.open(REDIRECT_TO, os.O_RDWR) # standard input (0)
545 # Duplicate standard input to standard output and standard error.
546 os.dup2(0, 1) # standard output (1)
547 os.dup2(0, 2) # standard error (2)
548
549 def _remove_pid_file(self, written_pid, filename, verbosity):
550 current_pid = os.getpid()
551 if written_pid != current_pid:
552 # A forked process must be exiting, not the process that
553 # wrote the PID file
554 return
555 if not os.path.exists(filename):
556 return
557 with open(filename) as f:
558 content = f.read().strip()
559 try:
560 pid_in_file = int(content)
561 except ValueError:
562 pass
563 else:
564 if pid_in_file != current_pid:
565 msg = "PID file %s contains %s, not expected PID %s"
566 self.out(msg % (filename, pid_in_file, current_pid))
567 return
568 if verbosity > 0:
569 self.out("Removing PID file %s" % filename)
570 try:
571 os.unlink(filename)
572 return
573 except OSError as e:
574 # Record, but don't give traceback
575 self.out("Cannot remove PID file: (%s)" % e)
576 # well, at least lets not leave the invalid PID around...
577 try:
578 with open(filename, 'w') as f:
579 f.write('')
580 except OSError as e:
581 self.out('Stale PID left in file: %s (%s)' % (filename, e))
582 else:
583 self.out('Stale PID removed')
584
585 def record_pid(self, pid_file):
586 pid = os.getpid()
587 if self.options.verbose > 1:
588 self.out('Writing PID %s to %s' % (pid, pid_file))
589 with open(pid_file, 'w') as f:
590 f.write(str(pid))
591 atexit.register(self._remove_pid_file, pid, pid_file, self.options.verbose)
592
593 def stop_daemon(self): # pragma: no cover
594 pid_file = self.options.pid_file or 'pyramid.pid'
595 if not os.path.exists(pid_file):
596 self.out('No PID file exists in %s' % pid_file)
597 return 1
598 pid = read_pidfile(pid_file)
599 if not pid:
600 self.out("Not a valid PID file in %s" % pid_file)
601 return 1
602 pid = live_pidfile(pid_file)
603 if not pid:
604 self.out("PID in %s is not valid (deleting)" % pid_file)
605 try:
606 os.unlink(pid_file)
607 except (OSError, IOError) as e:
608 self.out("Could not delete: %s" % e)
609 return 2
610 return 1
611 for j in range(10):
612 if not live_pidfile(pid_file):
613 break
614 import signal
615 kill(pid, signal.SIGTERM)
616 time.sleep(1)
617 else:
618 self.out("failed to kill web process %s" % pid)
619 return 3
620 if os.path.exists(pid_file):
621 os.unlink(pid_file)
622 return 0
623
624 def show_status(self): # pragma: no cover
625 pid_file = self.options.pid_file or 'pyramid.pid'
626 if not os.path.exists(pid_file):
627 self.out('No PID file %s' % pid_file)
628 return 1
629 pid = read_pidfile(pid_file)
630 if not pid:
631 self.out('No PID in file %s' % pid_file)
632 return 1
633 pid = live_pidfile(pid_file)
634 if not pid:
635 self.out('PID %s in %s is not running' % (pid, pid_file))
636 return 1
637 self.out('Server running in PID %s' % pid)
638 return 0
639
640 def restart_with_reloader(self): # pragma: no cover
641 self.restart_with_monitor(reloader=True)
642
643 def restart_with_monitor(self, reloader=False): # pragma: no cover
644 if self.options.verbose > 0:
645 if reloader:
646 self.out('Starting subprocess with file monitor')
647 else:
648 self.out('Starting subprocess with monitor parent')
649 while 1:
650 args = [self.quote_first_command_arg(sys.executable)] + sys.argv
651 new_environ = os.environ.copy()
652 if reloader:
653 new_environ[self._reloader_environ_key] = 'true'
654 else:
655 new_environ[self._monitor_environ_key] = 'true'
656 proc = None
657 try:
658 try:
659 _turn_sigterm_into_systemexit()
660 proc = subprocess32.Popen(args, env=new_environ)
661 exit_code = proc.wait()
662 proc = None
663 except KeyboardInterrupt:
664 self.out('^C caught in monitor process')
665 if self.options.verbose > 1:
666 raise
667 return 1
668 finally:
669 if proc is not None:
670 import signal
671 try:
672 kill(proc.pid, signal.SIGTERM)
673 except (OSError, IOError):
674 pass
675
676 if reloader:
677 # Reloader always exits with code 3; but if we are
678 # a monitor, any exit code will restart
679 if exit_code != 3:
680 return exit_code
681 if self.options.verbose > 0:
682 self.out('%s %s %s' % ('-' * 20, 'Restarting', '-' * 20))
683
684 def change_user_group(self, user, group): # pragma: no cover
685 if not user and not group:
686 return
687 import pwd
688 import grp
689 uid = gid = None
690 if group:
691 try:
692 gid = int(group)
693 group = grp.getgrgid(gid).gr_name
694 except ValueError:
695 try:
696 entry = grp.getgrnam(group)
697 except KeyError:
698 raise ValueError(
699 "Bad group: %r; no such group exists" % group)
700 gid = entry.gr_gid
701 try:
702 uid = int(user)
703 user = pwd.getpwuid(uid).pw_name
704 except ValueError:
705 try:
706 entry = pwd.getpwnam(user)
707 except KeyError:
708 raise ValueError(
709 "Bad username: %r; no such user exists" % user)
710 if not gid:
711 gid = entry.pw_gid
712 uid = entry.pw_uid
713 if self.options.verbose > 0:
714 self.out('Changing user to %s:%s (%s:%s)' % (
715 user, group or '(unknown)', uid, gid))
716 if gid:
717 os.setgid(gid)
718 if uid:
719 os.setuid(uid)
720
721
722 class LazyWriter(object):
723
724 """
725 File-like object that opens a file lazily when it is first written
726 to.
727 """
728
729 def __init__(self, filename, mode='w'):
730 self.filename = filename
731 self.fileobj = None
732 self.lock = threading.Lock()
733 self.mode = mode
734
735 def open(self):
736 if self.fileobj is None:
737 with self.lock:
738 self.fileobj = open(self.filename, self.mode)
739 return self.fileobj
740
741 def close(self):
742 fileobj = self.fileobj
743 if fileobj is not None:
744 fileobj.close()
745
746 def __del__(self):
747 self.close()
748
749 def write(self, text):
750 fileobj = self.open()
751 fileobj.write(text)
752 fileobj.flush()
753
754 def writelines(self, text):
755 fileobj = self.open()
756 fileobj.writelines(text)
757 fileobj.flush()
758
759 def flush(self):
760 self.open().flush()
761
762
763 def live_pidfile(pidfile): # pragma: no cover
764 """
765 (pidfile:str) -> int | None
766 Returns an int found in the named file, if there is one,
767 and if there is a running process with that process id.
768 Return None if no such process exists.
769 """
770 pid = read_pidfile(pidfile)
771 if pid:
772 try:
773 kill(int(pid), 0)
774 return pid
775 except OSError as e:
776 if e.errno == errno.EPERM:
777 return pid
778 return None
779
780
781 def read_pidfile(filename):
782 if os.path.exists(filename):
783 try:
784 with open(filename) as f:
785 content = f.read()
786 return int(content.strip())
787 except (ValueError, IOError):
788 return None
789 else:
790 return None
791
792
793 def ensure_port_cleanup(
794 bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover
795 """
796 This makes sure any open ports are closed.
797
798 Does this by connecting to them until they give connection
799 refused. Servers should call like::
800
801 ensure_port_cleanup([80, 443])
802 """
803 atexit.register(_cleanup_ports, bound_addresses, maxtries=maxtries,
804 sleeptime=sleeptime)
805
806
807 def _cleanup_ports(
808 bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover
809 # Wait for the server to bind to the port.
810 import socket
811 import errno
812 for bound_address in bound_addresses:
813 for attempt in range(maxtries):
814 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
815 try:
816 sock.connect(bound_address)
817 except socket.error as e:
818 if e.args[0] != errno.ECONNREFUSED:
819 raise
820 break
821 else:
822 time.sleep(sleeptime)
823 else:
824 raise SystemExit('Timeout waiting for port.')
825 sock.close()
826
827
828 def _turn_sigterm_into_systemexit(): # pragma: no cover
829 """
830 Attempts to turn a SIGTERM exception into a SystemExit exception.
831 """
832 try:
833 import signal
834 except ImportError:
835 return
836 def handle_term(signo, frame):
837 raise SystemExit
838 signal.signal(signal.SIGTERM, handle_term)
839
840
841 def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover
842 """
843 Install the reloading monitor.
844
845 On some platforms server threads may not terminate when the main
846 thread does, causing ports to remain open/locked. The
847 ``raise_keyboard_interrupt`` option creates a unignorable signal
848 which causes the whole application to shut-down (rudely).
849 """
850 mon = Monitor(poll_interval=poll_interval)
851 if extra_files is None:
852 extra_files = []
853 mon.extra_files.extend(extra_files)
854 t = threading.Thread(target=mon.periodic_reload)
855 t.setDaemon(True)
856 t.start()
857
858
859 class classinstancemethod(object):
860 """
861 Acts like a class method when called from a class, like an
862 instance method when called by an instance. The method should
863 take two arguments, 'self' and 'cls'; one of these will be None
864 depending on how the method was called.
865 """
866
867 def __init__(self, func):
868 self.func = func
869 self.__doc__ = func.__doc__
870
871 def __get__(self, obj, type=None):
872 return _methodwrapper(self.func, obj=obj, type=type)
873
874
875 class _methodwrapper(object):
876
877 def __init__(self, func, obj, type):
878 self.func = func
879 self.obj = obj
880 self.type = type
881
882 def __call__(self, *args, **kw):
883 assert not 'self' in kw and not 'cls' in kw, (
884 "You cannot use 'self' or 'cls' arguments to a "
885 "classinstancemethod")
886 return self.func(*((self.obj, self.type) + args), **kw)
887
888
889 class Monitor(object): # pragma: no cover
890 """
891 A file monitor and server restarter.
892
893 Use this like:
894
895 ..code-block:: Python
896
897 install_reloader()
898
899 Then make sure your server is installed with a shell script like::
900
901 err=3
902 while test "$err" -eq 3 ; do
903 python server.py
904 err="$?"
905 done
906
907 or is run from this .bat file (if you use Windows)::
908
909 @echo off
910 :repeat
911 python server.py
912 if %errorlevel% == 3 goto repeat
913
914 or run a monitoring process in Python (``pserve --reload`` does
915 this).
916
917 Use the ``watch_file(filename)`` function to cause a reload/restart for
918 other non-Python files (e.g., configuration files). If you have
919 a dynamic set of files that grows over time you can use something like::
920
921 def watch_config_files():
922 return CONFIG_FILE_CACHE.keys()
923 add_file_callback(watch_config_files)
924
925 Then every time the reloader polls files it will call
926 ``watch_config_files`` and check all the filenames it returns.
927 """
928 instances = []
929 global_extra_files = []
930 global_file_callbacks = []
931
932 def __init__(self, poll_interval):
933 self.module_mtimes = {}
934 self.keep_running = True
935 self.poll_interval = poll_interval
936 self.extra_files = list(self.global_extra_files)
937 self.instances.append(self)
938 self.file_callbacks = list(self.global_file_callbacks)
939
940 def _exit(self):
941 # use os._exit() here and not sys.exit() since within a
942 # thread sys.exit() just closes the given thread and
943 # won't kill the process; note os._exit does not call
944 # any atexit callbacks, nor does it do finally blocks,
945 # flush open files, etc. In otherwords, it is rude.
946 os._exit(3)
947
948 def periodic_reload(self):
949 while True:
950 if not self.check_reload():
951 self._exit()
952 break
953 time.sleep(self.poll_interval)
954
955 def check_reload(self):
956 filenames = list(self.extra_files)
957 for file_callback in self.file_callbacks:
958 try:
959 filenames.extend(file_callback())
960 except:
961 print(
962 "Error calling reloader callback %r:" % file_callback)
963 traceback.print_exc()
964 for module in list(sys.modules.values()):
965 try:
966 filename = module.__file__
967 except (AttributeError, ImportError):
968 continue
969 if filename is not None:
970 filenames.append(filename)
971
972 for filename in filenames:
973 try:
974 stat = os.stat(filename)
975 if stat:
976 mtime = stat.st_mtime
977 else:
978 mtime = 0
979 except (OSError, IOError):
980 continue
981 if filename.endswith('.pyc') and os.path.exists(filename[:-1]):
982 mtime = max(os.stat(filename[:-1]).st_mtime, mtime)
983 if not filename in self.module_mtimes:
984 self.module_mtimes[filename] = mtime
985 elif self.module_mtimes[filename] < mtime:
986 print("%s changed; reloading..." % filename)
987 run_callback_for_pattern(filename)
988 return False
989 return True
990
991 def watch_file(self, cls, filename):
992 """Watch the named file for changes"""
993 filename = os.path.abspath(filename)
994 if self is None:
995 for instance in cls.instances:
996 instance.watch_file(filename)
997 cls.global_extra_files.append(filename)
998 else:
999 self.extra_files.append(filename)
1000
1001 watch_file = classinstancemethod(watch_file)
1002
1003 def add_file_callback(self, cls, callback):
1004 """Add a callback -- a function that takes no parameters -- that will
1005 return a list of filenames to watch for changes."""
1006 if self is None:
1007 for instance in cls.instances:
1008 instance.add_file_callback(callback)
1009 cls.global_file_callbacks.append(callback)
1010 else:
1011 self.file_callbacks.append(callback)
1012
1013 add_file_callback = classinstancemethod(add_file_callback)
1014
1015 watch_file = Monitor.watch_file
1016 add_file_callback = Monitor.add_file_callback
1017
1018
1019 def main(argv=sys.argv, quiet=False):
1020 command = RcServerCommand(argv, quiet=quiet)
1021 return command.run()
1022
1023 if __name__ == '__main__': # pragma: no cover
1024 sys.exit(main() or 0)
General Comments 0
You need to be logged in to leave comments. Login now