##// END OF EJS Templates
comments: added immutable parameter to forbid editing/deleting certain comments
marcink -
r4327:da58ea77 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,43 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8 from sqlalchemy import BigInteger
9
10 from rhodecode.lib.dbmigrate.versions import _reset_base
11 from rhodecode.model import init_model_encryption
12
13
14 log = logging.getLogger(__name__)
15
16
17 def upgrade(migrate_engine):
18 """
19 Upgrade operations go here.
20 Don't create your own engine; bind migrate_engine to your metadata
21 """
22 _reset_base(migrate_engine)
23 from rhodecode.lib.dbmigrate.schema import db_4_18_0_1 as db
24
25 init_model_encryption(db)
26
27 context = MigrationContext.configure(migrate_engine.connect())
28 op = Operations(context)
29
30 comments = db.ChangesetComment.__table__
31
32 with op.batch_alter_table(comments.name) as batch_op:
33 new_column = Column('immutable_state', Unicode(128), nullable=True)
34 batch_op.add_column(new_column)
35
36
37 def downgrade(migrate_engine):
38 meta = MetaData()
39 meta.bind = migrate_engine
40
41
42 def fixups(models, _SESSION):
43 pass
@@ -1,60 +1,60 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 from collections import OrderedDict
22 from collections import OrderedDict
23
23
24 import sys
24 import sys
25 import platform
25 import platform
26
26
27 VERSION = tuple(open(os.path.join(
27 VERSION = tuple(open(os.path.join(
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
29
29
30 BACKENDS = OrderedDict()
30 BACKENDS = OrderedDict()
31
31
32 BACKENDS['hg'] = 'Mercurial repository'
32 BACKENDS['hg'] = 'Mercurial repository'
33 BACKENDS['git'] = 'Git repository'
33 BACKENDS['git'] = 'Git repository'
34 BACKENDS['svn'] = 'Subversion repository'
34 BACKENDS['svn'] = 'Subversion repository'
35
35
36
36
37 CELERY_ENABLED = False
37 CELERY_ENABLED = False
38 CELERY_EAGER = False
38 CELERY_EAGER = False
39
39
40 # link to config for pyramid
40 # link to config for pyramid
41 CONFIG = {}
41 CONFIG = {}
42
42
43 # Populated with the settings dictionary from application init in
43 # Populated with the settings dictionary from application init in
44 # rhodecode.conf.environment.load_pyramid_environment
44 # rhodecode.conf.environment.load_pyramid_environment
45 PYRAMID_SETTINGS = {}
45 PYRAMID_SETTINGS = {}
46
46
47 # Linked module for extensions
47 # Linked module for extensions
48 EXTENSIONS = {}
48 EXTENSIONS = {}
49
49
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 105 # defines current db version for migrations
51 __dbversion__ = 106 # defines current db version for migrations
52 __platform__ = platform.system()
52 __platform__ = platform.system()
53 __license__ = 'AGPLv3, and Commercial License'
53 __license__ = 'AGPLv3, and Commercial License'
54 __author__ = 'RhodeCode GmbH'
54 __author__ = 'RhodeCode GmbH'
55 __url__ = 'https://code.rhodecode.com'
55 __url__ = 'https://code.rhodecode.com'
56
56
57 is_windows = __platform__ in ['Windows']
57 is_windows = __platform__ in ['Windows']
58 is_unix = not is_windows
58 is_unix = not is_windows
59 is_test = False
59 is_test = False
60 disable_error_handler = False
60 disable_error_handler = False
@@ -1,402 +1,402 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import logging
22 import logging
23 import datetime
23 import datetime
24
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render_to_response
26 from pyramid.renderers import render_to_response
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.celerylib import run_task, tasks
28 from rhodecode.lib.celerylib import run_task, tasks
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.model.db import User
30 from rhodecode.model.db import User
31 from rhodecode.model.notification import EmailNotificationModel
31 from rhodecode.model.notification import EmailNotificationModel
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class DebugStyleView(BaseAppView):
36 class DebugStyleView(BaseAppView):
37 def load_default_context(self):
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
39
39
40 return c
40 return c
41
41
42 @view_config(
42 @view_config(
43 route_name='debug_style_home', request_method='GET',
43 route_name='debug_style_home', request_method='GET',
44 renderer=None)
44 renderer=None)
45 def index(self):
45 def index(self):
46 c = self.load_default_context()
46 c = self.load_default_context()
47 c.active = 'index'
47 c.active = 'index'
48
48
49 return render_to_response(
49 return render_to_response(
50 'debug_style/index.html', self._get_template_context(c),
50 'debug_style/index.html', self._get_template_context(c),
51 request=self.request)
51 request=self.request)
52
52
53 @view_config(
53 @view_config(
54 route_name='debug_style_email', request_method='GET',
54 route_name='debug_style_email', request_method='GET',
55 renderer=None)
55 renderer=None)
56 @view_config(
56 @view_config(
57 route_name='debug_style_email_plain_rendered', request_method='GET',
57 route_name='debug_style_email_plain_rendered', request_method='GET',
58 renderer=None)
58 renderer=None)
59 def render_email(self):
59 def render_email(self):
60 c = self.load_default_context()
60 c = self.load_default_context()
61 email_id = self.request.matchdict['email_id']
61 email_id = self.request.matchdict['email_id']
62 c.active = 'emails'
62 c.active = 'emails'
63
63
64 pr = AttributeDict(
64 pr = AttributeDict(
65 pull_request_id=123,
65 pull_request_id=123,
66 title='digital_ocean: fix redis, elastic search start on boot, '
66 title='digital_ocean: fix redis, elastic search start on boot, '
67 'fix fd limits on supervisor, set postgres 11 version',
67 'fix fd limits on supervisor, set postgres 11 version',
68 description='''
68 description='''
69 Check if we should use full-topic or mini-topic.
69 Check if we should use full-topic or mini-topic.
70
70
71 - full topic produces some problems with merge states etc
71 - full topic produces some problems with merge states etc
72 - server-mini-topic needs probably tweeks.
72 - server-mini-topic needs probably tweeks.
73 ''',
73 ''',
74 repo_name='foobar',
74 repo_name='foobar',
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 )
77 )
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
81 # file/commit changes for PR update
81 # file/commit changes for PR update
82 commit_changes = AttributeDict({
82 commit_changes = AttributeDict({
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 'removed': ['eeeeeeeeeee'],
84 'removed': ['eeeeeeeeeee'],
85 })
85 })
86 file_changes = AttributeDict({
86 file_changes = AttributeDict({
87 'added': ['a/file1.md', 'file2.py'],
87 'added': ['a/file1.md', 'file2.py'],
88 'modified': ['b/modified_file.rst'],
88 'modified': ['b/modified_file.rst'],
89 'removed': ['.idea'],
89 'removed': ['.idea'],
90 })
90 })
91
91
92 exc_traceback = {
92 exc_traceback = {
93 'exc_utc_date': '2020-03-26T12:54:50.683281',
93 'exc_utc_date': '2020-03-26T12:54:50.683281',
94 'exc_id': 139638856342656,
94 'exc_id': 139638856342656,
95 'exc_timestamp': '1585227290.683288',
95 'exc_timestamp': '1585227290.683288',
96 'version': 'v1',
96 'version': 'v1',
97 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
97 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
98 'exc_type': 'AttributeError'
98 'exc_type': 'AttributeError'
99 }
99 }
100 email_kwargs = {
100 email_kwargs = {
101 'test': {},
101 'test': {},
102 'message': {
102 'message': {
103 'body': 'message body !'
103 'body': 'message body !'
104 },
104 },
105 'email_test': {
105 'email_test': {
106 'user': user,
106 'user': user,
107 'date': datetime.datetime.now(),
107 'date': datetime.datetime.now(),
108 },
108 },
109 'exception': {
109 'exception': {
110 'email_prefix': '[RHODECODE ERROR]',
110 'email_prefix': '[RHODECODE ERROR]',
111 'exc_id': exc_traceback['exc_id'],
111 'exc_id': exc_traceback['exc_id'],
112 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
112 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
113 'exc_type_name': 'NameError',
113 'exc_type_name': 'NameError',
114 'exc_traceback': exc_traceback,
114 'exc_traceback': exc_traceback,
115 },
115 },
116 'password_reset': {
116 'password_reset': {
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
118
118
119 'user': user,
119 'user': user,
120 'date': datetime.datetime.now(),
120 'date': datetime.datetime.now(),
121 'email': 'test@rhodecode.com',
121 'email': 'test@rhodecode.com',
122 'first_admin_email': User.get_first_super_admin().email
122 'first_admin_email': User.get_first_super_admin().email
123 },
123 },
124 'password_reset_confirmation': {
124 'password_reset_confirmation': {
125 'new_password': 'new-password-example',
125 'new_password': 'new-password-example',
126 'user': user,
126 'user': user,
127 'date': datetime.datetime.now(),
127 'date': datetime.datetime.now(),
128 'email': 'test@rhodecode.com',
128 'email': 'test@rhodecode.com',
129 'first_admin_email': User.get_first_super_admin().email
129 'first_admin_email': User.get_first_super_admin().email
130 },
130 },
131 'registration': {
131 'registration': {
132 'user': user,
132 'user': user,
133 'date': datetime.datetime.now(),
133 'date': datetime.datetime.now(),
134 },
134 },
135
135
136 'pull_request_comment': {
136 'pull_request_comment': {
137 'user': user,
137 'user': user,
138
138
139 'status_change': None,
139 'status_change': None,
140 'status_change_type': None,
140 'status_change_type': None,
141
141
142 'pull_request': pr,
142 'pull_request': pr,
143 'pull_request_commits': [],
143 'pull_request_commits': [],
144
144
145 'pull_request_target_repo': target_repo,
145 'pull_request_target_repo': target_repo,
146 'pull_request_target_repo_url': 'http://target-repo/url',
146 'pull_request_target_repo_url': 'http://target-repo/url',
147
147
148 'pull_request_source_repo': source_repo,
148 'pull_request_source_repo': source_repo,
149 'pull_request_source_repo_url': 'http://source-repo/url',
149 'pull_request_source_repo_url': 'http://source-repo/url',
150
150
151 'pull_request_url': 'http://localhost/pr1',
151 'pull_request_url': 'http://localhost/pr1',
152 'pr_comment_url': 'http://comment-url',
152 'pr_comment_url': 'http://comment-url',
153 'pr_comment_reply_url': 'http://comment-url#reply',
153 'pr_comment_reply_url': 'http://comment-url#reply',
154
154
155 'comment_file': None,
155 'comment_file': None,
156 'comment_line': None,
156 'comment_line': None,
157 'comment_type': 'note',
157 'comment_type': 'note',
158 'comment_body': 'This is my comment body. *I like !*',
158 'comment_body': 'This is my comment body. *I like !*',
159 'comment_id': 2048,
159 'comment_id': 2048,
160 'renderer_type': 'markdown',
160 'renderer_type': 'markdown',
161 'mention': True,
161 'mention': True,
162
162
163 },
163 },
164 'pull_request_comment+status': {
164 'pull_request_comment+status': {
165 'user': user,
165 'user': user,
166
166
167 'status_change': 'approved',
167 'status_change': 'approved',
168 'status_change_type': 'approved',
168 'status_change_type': 'approved',
169
169
170 'pull_request': pr,
170 'pull_request': pr,
171 'pull_request_commits': [],
171 'pull_request_commits': [],
172
172
173 'pull_request_target_repo': target_repo,
173 'pull_request_target_repo': target_repo,
174 'pull_request_target_repo_url': 'http://target-repo/url',
174 'pull_request_target_repo_url': 'http://target-repo/url',
175
175
176 'pull_request_source_repo': source_repo,
176 'pull_request_source_repo': source_repo,
177 'pull_request_source_repo_url': 'http://source-repo/url',
177 'pull_request_source_repo_url': 'http://source-repo/url',
178
178
179 'pull_request_url': 'http://localhost/pr1',
179 'pull_request_url': 'http://localhost/pr1',
180 'pr_comment_url': 'http://comment-url',
180 'pr_comment_url': 'http://comment-url',
181 'pr_comment_reply_url': 'http://comment-url#reply',
181 'pr_comment_reply_url': 'http://comment-url#reply',
182
182
183 'comment_type': 'todo',
183 'comment_type': 'todo',
184 'comment_file': None,
184 'comment_file': None,
185 'comment_line': None,
185 'comment_line': None,
186 'comment_body': '''
186 'comment_body': '''
187 I think something like this would be better
187 I think something like this would be better
188
188
189 ```py
189 ```py
190
190
191 def db():
191 def db():
192 global connection
192 global connection
193 return connection
193 return connection
194
194
195 ```
195 ```
196
196
197 ''',
197 ''',
198 'comment_id': 2048,
198 'comment_id': 2048,
199 'renderer_type': 'markdown',
199 'renderer_type': 'markdown',
200 'mention': True,
200 'mention': True,
201
201
202 },
202 },
203 'pull_request_comment+file': {
203 'pull_request_comment+file': {
204 'user': user,
204 'user': user,
205
205
206 'status_change': None,
206 'status_change': None,
207 'status_change_type': None,
207 'status_change_type': None,
208
208
209 'pull_request': pr,
209 'pull_request': pr,
210 'pull_request_commits': [],
210 'pull_request_commits': [],
211
211
212 'pull_request_target_repo': target_repo,
212 'pull_request_target_repo': target_repo,
213 'pull_request_target_repo_url': 'http://target-repo/url',
213 'pull_request_target_repo_url': 'http://target-repo/url',
214
214
215 'pull_request_source_repo': source_repo,
215 'pull_request_source_repo': source_repo,
216 'pull_request_source_repo_url': 'http://source-repo/url',
216 'pull_request_source_repo_url': 'http://source-repo/url',
217
217
218 'pull_request_url': 'http://localhost/pr1',
218 'pull_request_url': 'http://localhost/pr1',
219
219
220 'pr_comment_url': 'http://comment-url',
220 'pr_comment_url': 'http://comment-url',
221 'pr_comment_reply_url': 'http://comment-url#reply',
221 'pr_comment_reply_url': 'http://comment-url#reply',
222
222
223 'comment_file': 'rhodecode/model/db.py',
223 'comment_file': 'rhodecode/model/get_flow_commits',
224 'comment_line': 'o1210',
224 'comment_line': 'o1210',
225 'comment_type': 'todo',
225 'comment_type': 'todo',
226 'comment_body': '''
226 'comment_body': '''
227 I like this !
227 I like this !
228
228
229 But please check this code::
229 But please check this code::
230
230
231 def main():
231 def main():
232 print 'ok'
232 print 'ok'
233
233
234 This should work better !
234 This should work better !
235 ''',
235 ''',
236 'comment_id': 2048,
236 'comment_id': 2048,
237 'renderer_type': 'rst',
237 'renderer_type': 'rst',
238 'mention': True,
238 'mention': True,
239
239
240 },
240 },
241
241
242 'pull_request_update': {
242 'pull_request_update': {
243 'updating_user': user,
243 'updating_user': user,
244
244
245 'status_change': None,
245 'status_change': None,
246 'status_change_type': None,
246 'status_change_type': None,
247
247
248 'pull_request': pr,
248 'pull_request': pr,
249 'pull_request_commits': [],
249 'pull_request_commits': [],
250
250
251 'pull_request_target_repo': target_repo,
251 'pull_request_target_repo': target_repo,
252 'pull_request_target_repo_url': 'http://target-repo/url',
252 'pull_request_target_repo_url': 'http://target-repo/url',
253
253
254 'pull_request_source_repo': source_repo,
254 'pull_request_source_repo': source_repo,
255 'pull_request_source_repo_url': 'http://source-repo/url',
255 'pull_request_source_repo_url': 'http://source-repo/url',
256
256
257 'pull_request_url': 'http://localhost/pr1',
257 'pull_request_url': 'http://localhost/pr1',
258
258
259 # update comment links
259 # update comment links
260 'pr_comment_url': 'http://comment-url',
260 'pr_comment_url': 'http://comment-url',
261 'pr_comment_reply_url': 'http://comment-url#reply',
261 'pr_comment_reply_url': 'http://comment-url#reply',
262 'ancestor_commit_id': 'f39bd443',
262 'ancestor_commit_id': 'f39bd443',
263 'added_commits': commit_changes.added,
263 'added_commits': commit_changes.added,
264 'removed_commits': commit_changes.removed,
264 'removed_commits': commit_changes.removed,
265 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
265 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
266 'added_files': file_changes.added,
266 'added_files': file_changes.added,
267 'modified_files': file_changes.modified,
267 'modified_files': file_changes.modified,
268 'removed_files': file_changes.removed,
268 'removed_files': file_changes.removed,
269 },
269 },
270
270
271 'cs_comment': {
271 'cs_comment': {
272 'user': user,
272 'user': user,
273 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
273 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
274 'status_change': None,
274 'status_change': None,
275 'status_change_type': None,
275 'status_change_type': None,
276
276
277 'commit_target_repo_url': 'http://foo.example.com/#comment1',
277 'commit_target_repo_url': 'http://foo.example.com/#comment1',
278 'repo_name': 'test-repo',
278 'repo_name': 'test-repo',
279 'comment_type': 'note',
279 'comment_type': 'note',
280 'comment_file': None,
280 'comment_file': None,
281 'comment_line': None,
281 'comment_line': None,
282 'commit_comment_url': 'http://comment-url',
282 'commit_comment_url': 'http://comment-url',
283 'commit_comment_reply_url': 'http://comment-url#reply',
283 'commit_comment_reply_url': 'http://comment-url#reply',
284 'comment_body': 'This is my comment body. *I like !*',
284 'comment_body': 'This is my comment body. *I like !*',
285 'comment_id': 2048,
285 'comment_id': 2048,
286 'renderer_type': 'markdown',
286 'renderer_type': 'markdown',
287 'mention': True,
287 'mention': True,
288 },
288 },
289 'cs_comment+status': {
289 'cs_comment+status': {
290 'user': user,
290 'user': user,
291 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
291 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
292 'status_change': 'approved',
292 'status_change': 'approved',
293 'status_change_type': 'approved',
293 'status_change_type': 'approved',
294
294
295 'commit_target_repo_url': 'http://foo.example.com/#comment1',
295 'commit_target_repo_url': 'http://foo.example.com/#comment1',
296 'repo_name': 'test-repo',
296 'repo_name': 'test-repo',
297 'comment_type': 'note',
297 'comment_type': 'note',
298 'comment_file': None,
298 'comment_file': None,
299 'comment_line': None,
299 'comment_line': None,
300 'commit_comment_url': 'http://comment-url',
300 'commit_comment_url': 'http://comment-url',
301 'commit_comment_reply_url': 'http://comment-url#reply',
301 'commit_comment_reply_url': 'http://comment-url#reply',
302 'comment_body': '''
302 'comment_body': '''
303 Hello **world**
303 Hello **world**
304
304
305 This is a multiline comment :)
305 This is a multiline comment :)
306
306
307 - list
307 - list
308 - list2
308 - list2
309 ''',
309 ''',
310 'comment_id': 2048,
310 'comment_id': 2048,
311 'renderer_type': 'markdown',
311 'renderer_type': 'markdown',
312 'mention': True,
312 'mention': True,
313 },
313 },
314 'cs_comment+file': {
314 'cs_comment+file': {
315 'user': user,
315 'user': user,
316 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
316 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
317 'status_change': None,
317 'status_change': None,
318 'status_change_type': None,
318 'status_change_type': None,
319
319
320 'commit_target_repo_url': 'http://foo.example.com/#comment1',
320 'commit_target_repo_url': 'http://foo.example.com/#comment1',
321 'repo_name': 'test-repo',
321 'repo_name': 'test-repo',
322
322
323 'comment_type': 'note',
323 'comment_type': 'note',
324 'comment_file': 'test-file.py',
324 'comment_file': 'test-file.py',
325 'comment_line': 'n100',
325 'comment_line': 'n100',
326
326
327 'commit_comment_url': 'http://comment-url',
327 'commit_comment_url': 'http://comment-url',
328 'commit_comment_reply_url': 'http://comment-url#reply',
328 'commit_comment_reply_url': 'http://comment-url#reply',
329 'comment_body': 'This is my comment body. *I like !*',
329 'comment_body': 'This is my comment body. *I like !*',
330 'comment_id': 2048,
330 'comment_id': 2048,
331 'renderer_type': 'markdown',
331 'renderer_type': 'markdown',
332 'mention': True,
332 'mention': True,
333 },
333 },
334
334
335 'pull_request': {
335 'pull_request': {
336 'user': user,
336 'user': user,
337 'pull_request': pr,
337 'pull_request': pr,
338 'pull_request_commits': [
338 'pull_request_commits': [
339 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
339 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
340 my-account: moved email closer to profile as it's similar data just moved outside.
340 my-account: moved email closer to profile as it's similar data just moved outside.
341 '''),
341 '''),
342 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
342 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
343 users: description edit fixes
343 users: description edit fixes
344
344
345 - tests
345 - tests
346 - added metatags info
346 - added metatags info
347 '''),
347 '''),
348 ],
348 ],
349
349
350 'pull_request_target_repo': target_repo,
350 'pull_request_target_repo': target_repo,
351 'pull_request_target_repo_url': 'http://target-repo/url',
351 'pull_request_target_repo_url': 'http://target-repo/url',
352
352
353 'pull_request_source_repo': source_repo,
353 'pull_request_source_repo': source_repo,
354 'pull_request_source_repo_url': 'http://source-repo/url',
354 'pull_request_source_repo_url': 'http://source-repo/url',
355
355
356 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
356 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
357 }
357 }
358
358
359 }
359 }
360
360
361 template_type = email_id.split('+')[0]
361 template_type = email_id.split('+')[0]
362 (c.subject, c.headers, c.email_body,
362 (c.subject, c.headers, c.email_body,
363 c.email_body_plaintext) = EmailNotificationModel().render_email(
363 c.email_body_plaintext) = EmailNotificationModel().render_email(
364 template_type, **email_kwargs.get(email_id, {}))
364 template_type, **email_kwargs.get(email_id, {}))
365
365
366 test_email = self.request.GET.get('email')
366 test_email = self.request.GET.get('email')
367 if test_email:
367 if test_email:
368 recipients = [test_email]
368 recipients = [test_email]
369 run_task(tasks.send_email, recipients, c.subject,
369 run_task(tasks.send_email, recipients, c.subject,
370 c.email_body_plaintext, c.email_body)
370 c.email_body_plaintext, c.email_body)
371
371
372 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
372 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
373 template = 'debug_style/email_plain_rendered.mako'
373 template = 'debug_style/email_plain_rendered.mako'
374 else:
374 else:
375 template = 'debug_style/email.mako'
375 template = 'debug_style/email.mako'
376 return render_to_response(
376 return render_to_response(
377 template, self._get_template_context(c),
377 template, self._get_template_context(c),
378 request=self.request)
378 request=self.request)
379
379
380 @view_config(
380 @view_config(
381 route_name='debug_style_template', request_method='GET',
381 route_name='debug_style_template', request_method='GET',
382 renderer=None)
382 renderer=None)
383 def template(self):
383 def template(self):
384 t_path = self.request.matchdict['t_path']
384 t_path = self.request.matchdict['t_path']
385 c = self.load_default_context()
385 c = self.load_default_context()
386 c.active = os.path.splitext(t_path)[0]
386 c.active = os.path.splitext(t_path)[0]
387 c.came_from = ''
387 c.came_from = ''
388 c.email_types = {
388 c.email_types = {
389 'cs_comment+file': {},
389 'cs_comment+file': {},
390 'cs_comment+status': {},
390 'cs_comment+status': {},
391
391
392 'pull_request_comment+file': {},
392 'pull_request_comment+file': {},
393 'pull_request_comment+status': {},
393 'pull_request_comment+status': {},
394
394
395 'pull_request_update': {},
395 'pull_request_update': {},
396 }
396 }
397 c.email_types.update(EmailNotificationModel.email_types)
397 c.email_types.update(EmailNotificationModel.email_types)
398
398
399 return render_to_response(
399 return render_to_response(
400 'debug_style/' + t_path, self._get_template_context(c),
400 'debug_style/' + t_path, self._get_template_context(c),
401 request=self.request)
401 request=self.request)
402
402
@@ -1,320 +1,348 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests import TestController
23 from rhodecode.tests import TestController
24
24
25 from rhodecode.model.db import (
25 from rhodecode.model.db import ChangesetComment, Notification
26 ChangesetComment, Notification, UserNotification)
27 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
28 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
29
28
30
29
31 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
32 import urllib
31 import urllib
33
32
34 base_url = {
33 base_url = {
35 'repo_commit': '/{repo_name}/changeset/{commit_id}',
34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
36 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
37 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
38 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
39 }[name].format(**kwargs)
38 }[name].format(**kwargs)
40
39
41 if params:
40 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 return base_url
42 return base_url
44
43
45
44
46 @pytest.mark.backends("git", "hg", "svn")
45 @pytest.mark.backends("git", "hg", "svn")
47 class TestRepoCommitCommentsView(TestController):
46 class TestRepoCommitCommentsView(TestController):
48
47
49 @pytest.fixture(autouse=True)
48 @pytest.fixture(autouse=True)
50 def prepare(self, request, baseapp):
49 def prepare(self, request, baseapp):
51 for x in ChangesetComment.query().all():
50 for x in ChangesetComment.query().all():
52 Session().delete(x)
51 Session().delete(x)
53 Session().commit()
52 Session().commit()
54
53
55 for x in Notification.query().all():
54 for x in Notification.query().all():
56 Session().delete(x)
55 Session().delete(x)
57 Session().commit()
56 Session().commit()
58
57
59 request.addfinalizer(self.cleanup)
58 request.addfinalizer(self.cleanup)
60
59
61 def cleanup(self):
60 def cleanup(self):
62 for x in ChangesetComment.query().all():
61 for x in ChangesetComment.query().all():
63 Session().delete(x)
62 Session().delete(x)
64 Session().commit()
63 Session().commit()
65
64
66 for x in Notification.query().all():
65 for x in Notification.query().all():
67 Session().delete(x)
66 Session().delete(x)
68 Session().commit()
67 Session().commit()
69
68
70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
69 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 def test_create(self, comment_type, backend):
70 def test_create(self, comment_type, backend):
72 self.log_user()
71 self.log_user()
73 commit = backend.repo.get_commit('300')
72 commit = backend.repo.get_commit('300')
74 commit_id = commit.raw_id
73 commit_id = commit.raw_id
75 text = u'CommentOnCommit'
74 text = u'CommentOnCommit'
76
75
77 params = {'text': text, 'csrf_token': self.csrf_token,
76 params = {'text': text, 'csrf_token': self.csrf_token,
78 'comment_type': comment_type}
77 'comment_type': comment_type}
79 self.app.post(
78 self.app.post(
80 route_path('repo_commit_comment_create',
79 route_path('repo_commit_comment_create',
81 repo_name=backend.repo_name, commit_id=commit_id),
80 repo_name=backend.repo_name, commit_id=commit_id),
82 params=params)
81 params=params)
83
82
84 response = self.app.get(
83 response = self.app.get(
85 route_path('repo_commit',
84 route_path('repo_commit',
86 repo_name=backend.repo_name, commit_id=commit_id))
85 repo_name=backend.repo_name, commit_id=commit_id))
87
86
88 # test DB
87 # test DB
89 assert ChangesetComment.query().count() == 1
88 assert ChangesetComment.query().count() == 1
90 assert_comment_links(response, ChangesetComment.query().count(), 0)
89 assert_comment_links(response, ChangesetComment.query().count(), 0)
91
90
92 assert Notification.query().count() == 1
91 assert Notification.query().count() == 1
93 assert ChangesetComment.query().count() == 1
92 assert ChangesetComment.query().count() == 1
94
93
95 notification = Notification.query().all()[0]
94 notification = Notification.query().all()[0]
96
95
97 comment_id = ChangesetComment.query().first().comment_id
96 comment_id = ChangesetComment.query().first().comment_id
98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
97 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99
98
100 author = notification.created_by_user.username_and_name
99 author = notification.created_by_user.username_and_name
101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
100 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 author, comment_type, h.show_id(commit), backend.repo_name)
101 author, comment_type, h.show_id(commit), backend.repo_name)
103 assert sbj == notification.subject
102 assert sbj == notification.subject
104
103
105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
104 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 backend.repo_name, commit_id, comment_id))
105 backend.repo_name, commit_id, comment_id))
107 assert lnk in notification.body
106 assert lnk in notification.body
108
107
109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
108 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 def test_create_inline(self, comment_type, backend):
109 def test_create_inline(self, comment_type, backend):
111 self.log_user()
110 self.log_user()
112 commit = backend.repo.get_commit('300')
111 commit = backend.repo.get_commit('300')
113 commit_id = commit.raw_id
112 commit_id = commit.raw_id
114 text = u'CommentOnCommit'
113 text = u'CommentOnCommit'
115 f_path = 'vcs/web/simplevcs/views/repository.py'
114 f_path = 'vcs/web/simplevcs/views/repository.py'
116 line = 'n1'
115 line = 'n1'
117
116
118 params = {'text': text, 'f_path': f_path, 'line': line,
117 params = {'text': text, 'f_path': f_path, 'line': line,
119 'comment_type': comment_type,
118 'comment_type': comment_type,
120 'csrf_token': self.csrf_token}
119 'csrf_token': self.csrf_token}
121
120
122 self.app.post(
121 self.app.post(
123 route_path('repo_commit_comment_create',
122 route_path('repo_commit_comment_create',
124 repo_name=backend.repo_name, commit_id=commit_id),
123 repo_name=backend.repo_name, commit_id=commit_id),
125 params=params)
124 params=params)
126
125
127 response = self.app.get(
126 response = self.app.get(
128 route_path('repo_commit',
127 route_path('repo_commit',
129 repo_name=backend.repo_name, commit_id=commit_id))
128 repo_name=backend.repo_name, commit_id=commit_id))
130
129
131 # test DB
130 # test DB
132 assert ChangesetComment.query().count() == 1
131 assert ChangesetComment.query().count() == 1
133 assert_comment_links(response, 0, ChangesetComment.query().count())
132 assert_comment_links(response, 0, ChangesetComment.query().count())
134
133
135 if backend.alias == 'svn':
134 if backend.alias == 'svn':
136 response.mustcontain(
135 response.mustcontain(
137 '''data-f-path="vcs/commands/summary.py" '''
136 '''data-f-path="vcs/commands/summary.py" '''
138 '''data-anchor-id="c-300-ad05457a43f8"'''
137 '''data-anchor-id="c-300-ad05457a43f8"'''
139 )
138 )
140 if backend.alias == 'git':
139 if backend.alias == 'git':
141 response.mustcontain(
140 response.mustcontain(
142 '''data-f-path="vcs/backends/hg.py" '''
141 '''data-f-path="vcs/backends/hg.py" '''
143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
142 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 )
143 )
145
144
146 if backend.alias == 'hg':
145 if backend.alias == 'hg':
147 response.mustcontain(
146 response.mustcontain(
148 '''data-f-path="vcs/backends/hg.py" '''
147 '''data-f-path="vcs/backends/hg.py" '''
149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
148 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 )
149 )
151
150
152 assert Notification.query().count() == 1
151 assert Notification.query().count() == 1
153 assert ChangesetComment.query().count() == 1
152 assert ChangesetComment.query().count() == 1
154
153
155 notification = Notification.query().all()[0]
154 notification = Notification.query().all()[0]
156 comment = ChangesetComment.query().first()
155 comment = ChangesetComment.query().first()
157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
156 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158
157
159 assert comment.revision == commit_id
158 assert comment.revision == commit_id
160
159
161 author = notification.created_by_user.username_and_name
160 author = notification.created_by_user.username_and_name
162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
161 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
162 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164
163
165 assert sbj == notification.subject
164 assert sbj == notification.subject
166
165
167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
166 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 backend.repo_name, commit_id, comment.comment_id))
167 backend.repo_name, commit_id, comment.comment_id))
169 assert lnk in notification.body
168 assert lnk in notification.body
170 assert 'on line n1' in notification.body
169 assert 'on line n1' in notification.body
171
170
172 def test_create_with_mention(self, backend):
171 def test_create_with_mention(self, backend):
173 self.log_user()
172 self.log_user()
174
173
175 commit_id = backend.repo.get_commit('300').raw_id
174 commit_id = backend.repo.get_commit('300').raw_id
176 text = u'@test_regular check CommentOnCommit'
175 text = u'@test_regular check CommentOnCommit'
177
176
178 params = {'text': text, 'csrf_token': self.csrf_token}
177 params = {'text': text, 'csrf_token': self.csrf_token}
179 self.app.post(
178 self.app.post(
180 route_path('repo_commit_comment_create',
179 route_path('repo_commit_comment_create',
181 repo_name=backend.repo_name, commit_id=commit_id),
180 repo_name=backend.repo_name, commit_id=commit_id),
182 params=params)
181 params=params)
183
182
184 response = self.app.get(
183 response = self.app.get(
185 route_path('repo_commit',
184 route_path('repo_commit',
186 repo_name=backend.repo_name, commit_id=commit_id))
185 repo_name=backend.repo_name, commit_id=commit_id))
187 # test DB
186 # test DB
188 assert ChangesetComment.query().count() == 1
187 assert ChangesetComment.query().count() == 1
189 assert_comment_links(response, ChangesetComment.query().count(), 0)
188 assert_comment_links(response, ChangesetComment.query().count(), 0)
190
189
191 notification = Notification.query().one()
190 notification = Notification.query().one()
192
191
193 assert len(notification.recipients) == 2
192 assert len(notification.recipients) == 2
194 users = [x.username for x in notification.recipients]
193 users = [x.username for x in notification.recipients]
195
194
196 # test_regular gets notification by @mention
195 # test_regular gets notification by @mention
197 assert sorted(users) == [u'test_admin', u'test_regular']
196 assert sorted(users) == [u'test_admin', u'test_regular']
198
197
199 def test_create_with_status_change(self, backend):
198 def test_create_with_status_change(self, backend):
200 self.log_user()
199 self.log_user()
201 commit = backend.repo.get_commit('300')
200 commit = backend.repo.get_commit('300')
202 commit_id = commit.raw_id
201 commit_id = commit.raw_id
203 text = u'CommentOnCommit'
202 text = u'CommentOnCommit'
204 f_path = 'vcs/web/simplevcs/views/repository.py'
203 f_path = 'vcs/web/simplevcs/views/repository.py'
205 line = 'n1'
204 line = 'n1'
206
205
207 params = {'text': text, 'changeset_status': 'approved',
206 params = {'text': text, 'changeset_status': 'approved',
208 'csrf_token': self.csrf_token}
207 'csrf_token': self.csrf_token}
209
208
210 self.app.post(
209 self.app.post(
211 route_path(
210 route_path(
212 'repo_commit_comment_create',
211 'repo_commit_comment_create',
213 repo_name=backend.repo_name, commit_id=commit_id),
212 repo_name=backend.repo_name, commit_id=commit_id),
214 params=params)
213 params=params)
215
214
216 response = self.app.get(
215 response = self.app.get(
217 route_path('repo_commit',
216 route_path('repo_commit',
218 repo_name=backend.repo_name, commit_id=commit_id))
217 repo_name=backend.repo_name, commit_id=commit_id))
219
218
220 # test DB
219 # test DB
221 assert ChangesetComment.query().count() == 1
220 assert ChangesetComment.query().count() == 1
222 assert_comment_links(response, ChangesetComment.query().count(), 0)
221 assert_comment_links(response, ChangesetComment.query().count(), 0)
223
222
224 assert Notification.query().count() == 1
223 assert Notification.query().count() == 1
225 assert ChangesetComment.query().count() == 1
224 assert ChangesetComment.query().count() == 1
226
225
227 notification = Notification.query().all()[0]
226 notification = Notification.query().all()[0]
228
227
229 comment_id = ChangesetComment.query().first().comment_id
228 comment_id = ChangesetComment.query().first().comment_id
230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
229 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231
230
232 author = notification.created_by_user.username_and_name
231 author = notification.created_by_user.username_and_name
233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
232 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 author, h.show_id(commit), backend.repo_name)
233 author, h.show_id(commit), backend.repo_name)
235 assert sbj == notification.subject
234 assert sbj == notification.subject
236
235
237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
236 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 backend.repo_name, commit_id, comment_id))
237 backend.repo_name, commit_id, comment_id))
239 assert lnk in notification.body
238 assert lnk in notification.body
240
239
241 def test_delete(self, backend):
240 def test_delete(self, backend):
242 self.log_user()
241 self.log_user()
243 commit_id = backend.repo.get_commit('300').raw_id
242 commit_id = backend.repo.get_commit('300').raw_id
244 text = u'CommentOnCommit'
243 text = u'CommentOnCommit'
245
244
246 params = {'text': text, 'csrf_token': self.csrf_token}
245 params = {'text': text, 'csrf_token': self.csrf_token}
247 self.app.post(
246 self.app.post(
248 route_path(
247 route_path(
249 'repo_commit_comment_create',
248 'repo_commit_comment_create',
250 repo_name=backend.repo_name, commit_id=commit_id),
249 repo_name=backend.repo_name, commit_id=commit_id),
251 params=params)
250 params=params)
252
251
253 comments = ChangesetComment.query().all()
252 comments = ChangesetComment.query().all()
254 assert len(comments) == 1
253 assert len(comments) == 1
255 comment_id = comments[0].comment_id
254 comment_id = comments[0].comment_id
256
255
257 self.app.post(
256 self.app.post(
258 route_path('repo_commit_comment_delete',
257 route_path('repo_commit_comment_delete',
259 repo_name=backend.repo_name,
258 repo_name=backend.repo_name,
260 commit_id=commit_id,
259 commit_id=commit_id,
261 comment_id=comment_id),
260 comment_id=comment_id),
262 params={'csrf_token': self.csrf_token})
261 params={'csrf_token': self.csrf_token})
263
262
264 comments = ChangesetComment.query().all()
263 comments = ChangesetComment.query().all()
265 assert len(comments) == 0
264 assert len(comments) == 0
266
265
267 response = self.app.get(
266 response = self.app.get(
268 route_path('repo_commit',
267 route_path('repo_commit',
269 repo_name=backend.repo_name, commit_id=commit_id))
268 repo_name=backend.repo_name, commit_id=commit_id))
270 assert_comment_links(response, 0, 0)
269 assert_comment_links(response, 0, 0)
271
270
272 @pytest.mark.parametrize('renderer, input, output', [
271 def test_delete_forbidden_for_immutable_comments(self, backend):
272 self.log_user()
273 commit_id = backend.repo.get_commit('300').raw_id
274 text = u'CommentOnCommit'
275
276 params = {'text': text, 'csrf_token': self.csrf_token}
277 self.app.post(
278 route_path(
279 'repo_commit_comment_create',
280 repo_name=backend.repo_name, commit_id=commit_id),
281 params=params)
282
283 comments = ChangesetComment.query().all()
284 assert len(comments) == 1
285 comment_id = comments[0].comment_id
286
287 comment = ChangesetComment.get(comment_id)
288 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
289 Session().add(comment)
290 Session().commit()
291
292 self.app.post(
293 route_path('repo_commit_comment_delete',
294 repo_name=backend.repo_name,
295 commit_id=commit_id,
296 comment_id=comment_id),
297 params={'csrf_token': self.csrf_token},
298 status=403)
299
300 @pytest.mark.parametrize('renderer, text_input, output', [
273 ('rst', 'plain text', '<p>plain text</p>'),
301 ('rst', 'plain text', '<p>plain text</p>'),
274 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
302 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
275 ('rst', '*italics*', '<em>italics</em>'),
303 ('rst', '*italics*', '<em>italics</em>'),
276 ('rst', '**bold**', '<strong>bold</strong>'),
304 ('rst', '**bold**', '<strong>bold</strong>'),
277 ('markdown', 'plain text', '<p>plain text</p>'),
305 ('markdown', 'plain text', '<p>plain text</p>'),
278 ('markdown', '# header', '<h1>header</h1>'),
306 ('markdown', '# header', '<h1>header</h1>'),
279 ('markdown', '*italics*', '<em>italics</em>'),
307 ('markdown', '*italics*', '<em>italics</em>'),
280 ('markdown', '**bold**', '<strong>bold</strong>'),
308 ('markdown', '**bold**', '<strong>bold</strong>'),
281 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
309 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
282 'md-header', 'md-italics', 'md-bold', ])
310 'md-header', 'md-italics', 'md-bold', ])
283 def test_preview(self, renderer, input, output, backend, xhr_header):
311 def test_preview(self, renderer, text_input, output, backend, xhr_header):
284 self.log_user()
312 self.log_user()
285 params = {
313 params = {
286 'renderer': renderer,
314 'renderer': renderer,
287 'text': input,
315 'text': text_input,
288 'csrf_token': self.csrf_token
316 'csrf_token': self.csrf_token
289 }
317 }
290 commit_id = '0' * 16 # fake this for tests
318 commit_id = '0' * 16 # fake this for tests
291 response = self.app.post(
319 response = self.app.post(
292 route_path('repo_commit_comment_preview',
320 route_path('repo_commit_comment_preview',
293 repo_name=backend.repo_name, commit_id=commit_id,),
321 repo_name=backend.repo_name, commit_id=commit_id,),
294 params=params,
322 params=params,
295 extra_environ=xhr_header)
323 extra_environ=xhr_header)
296
324
297 response.mustcontain(output)
325 response.mustcontain(output)
298
326
299
327
300 def assert_comment_links(response, comments, inline_comments):
328 def assert_comment_links(response, comments, inline_comments):
301 if comments == 1:
329 if comments == 1:
302 comments_text = "%d General" % comments
330 comments_text = "%d General" % comments
303 else:
331 else:
304 comments_text = "%d General" % comments
332 comments_text = "%d General" % comments
305
333
306 if inline_comments == 1:
334 if inline_comments == 1:
307 inline_comments_text = "%d Inline" % inline_comments
335 inline_comments_text = "%d Inline" % inline_comments
308 else:
336 else:
309 inline_comments_text = "%d Inline" % inline_comments
337 inline_comments_text = "%d Inline" % inline_comments
310
338
311 if comments:
339 if comments:
312 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
340 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
313 else:
341 else:
314 response.mustcontain(comments_text)
342 response.mustcontain(comments_text)
315
343
316 if inline_comments:
344 if inline_comments:
317 response.mustcontain(
345 response.mustcontain(
318 'id="inline-comments-counter">%s' % inline_comments_text)
346 'id="inline-comments-counter">%s' % inline_comments_text)
319 else:
347 else:
320 response.mustcontain(inline_comments_text)
348 response.mustcontain(inline_comments_text)
@@ -1,606 +1,610 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37
37
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.settings import VcsSettingsModel
52 from rhodecode.model.settings import VcsSettingsModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, request):
57 def _update_with_GET(params, request):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += request.GET.getall(k)
59 params[k] += request.GET.getall(k)
60
60
61
61
62 class RepoCommitsView(RepoAppView):
62 class RepoCommitsView(RepoAppView):
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c.rhodecode_repo = self.rhodecode_vcs_repo
65 c.rhodecode_repo = self.rhodecode_vcs_repo
66
66
67 return c
67 return c
68
68
69 def _is_diff_cache_enabled(self, target_repo):
69 def _is_diff_cache_enabled(self, target_repo):
70 caching_enabled = self._get_general_setting(
70 caching_enabled = self._get_general_setting(
71 target_repo, 'rhodecode_diff_cache')
71 target_repo, 'rhodecode_diff_cache')
72 log.debug('Diff caching enabled: %s', caching_enabled)
72 log.debug('Diff caching enabled: %s', caching_enabled)
73 return caching_enabled
73 return caching_enabled
74
74
75 def _commit(self, commit_id_range, method):
75 def _commit(self, commit_id_range, method):
76 _ = self.request.translate
76 _ = self.request.translate
77 c = self.load_default_context()
77 c = self.load_default_context()
78 c.fulldiff = self.request.GET.get('fulldiff')
78 c.fulldiff = self.request.GET.get('fulldiff')
79
79
80 # fetch global flags of ignore ws or context lines
80 # fetch global flags of ignore ws or context lines
81 diff_context = get_diff_context(self.request)
81 diff_context = get_diff_context(self.request)
82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83
83
84 # diff_limit will cut off the whole diff if the limit is applied
84 # diff_limit will cut off the whole diff if the limit is applied
85 # otherwise it will just hide the big files from the front-end
85 # otherwise it will just hide the big files from the front-end
86 diff_limit = c.visual.cut_off_limit_diff
86 diff_limit = c.visual.cut_off_limit_diff
87 file_limit = c.visual.cut_off_limit_file
87 file_limit = c.visual.cut_off_limit_file
88
88
89 # get ranges of commit ids if preset
89 # get ranges of commit ids if preset
90 commit_range = commit_id_range.split('...')[:2]
90 commit_range = commit_id_range.split('...')[:2]
91
91
92 try:
92 try:
93 pre_load = ['affected_files', 'author', 'branch', 'date',
93 pre_load = ['affected_files', 'author', 'branch', 'date',
94 'message', 'parents']
94 'message', 'parents']
95 if self.rhodecode_vcs_repo.alias == 'hg':
95 if self.rhodecode_vcs_repo.alias == 'hg':
96 pre_load += ['hidden', 'obsolete', 'phase']
96 pre_load += ['hidden', 'obsolete', 'phase']
97
97
98 if len(commit_range) == 2:
98 if len(commit_range) == 2:
99 commits = self.rhodecode_vcs_repo.get_commits(
99 commits = self.rhodecode_vcs_repo.get_commits(
100 start_id=commit_range[0], end_id=commit_range[1],
100 start_id=commit_range[0], end_id=commit_range[1],
101 pre_load=pre_load, translate_tags=False)
101 pre_load=pre_load, translate_tags=False)
102 commits = list(commits)
102 commits = list(commits)
103 else:
103 else:
104 commits = [self.rhodecode_vcs_repo.get_commit(
104 commits = [self.rhodecode_vcs_repo.get_commit(
105 commit_id=commit_id_range, pre_load=pre_load)]
105 commit_id=commit_id_range, pre_load=pre_load)]
106
106
107 c.commit_ranges = commits
107 c.commit_ranges = commits
108 if not c.commit_ranges:
108 if not c.commit_ranges:
109 raise RepositoryError('The commit range returned an empty result')
109 raise RepositoryError('The commit range returned an empty result')
110 except CommitDoesNotExistError as e:
110 except CommitDoesNotExistError as e:
111 msg = _('No such commit exists. Org exception: `{}`').format(e)
111 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 h.flash(msg, category='error')
112 h.flash(msg, category='error')
113 raise HTTPNotFound()
113 raise HTTPNotFound()
114 except Exception:
114 except Exception:
115 log.exception("General failure")
115 log.exception("General failure")
116 raise HTTPNotFound()
116 raise HTTPNotFound()
117
117
118 c.changes = OrderedDict()
118 c.changes = OrderedDict()
119 c.lines_added = 0
119 c.lines_added = 0
120 c.lines_deleted = 0
120 c.lines_deleted = 0
121
121
122 # auto collapse if we have more than limit
122 # auto collapse if we have more than limit
123 collapse_limit = diffs.DiffProcessor._collapse_commits_over
123 collapse_limit = diffs.DiffProcessor._collapse_commits_over
124 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
124 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
125
125
126 c.commit_statuses = ChangesetStatus.STATUSES
126 c.commit_statuses = ChangesetStatus.STATUSES
127 c.inline_comments = []
127 c.inline_comments = []
128 c.files = []
128 c.files = []
129
129
130 c.statuses = []
130 c.statuses = []
131 c.comments = []
131 c.comments = []
132 c.unresolved_comments = []
132 c.unresolved_comments = []
133 c.resolved_comments = []
133 c.resolved_comments = []
134 if len(c.commit_ranges) == 1:
134 if len(c.commit_ranges) == 1:
135 commit = c.commit_ranges[0]
135 commit = c.commit_ranges[0]
136 c.comments = CommentsModel().get_comments(
136 c.comments = CommentsModel().get_comments(
137 self.db_repo.repo_id,
137 self.db_repo.repo_id,
138 revision=commit.raw_id)
138 revision=commit.raw_id)
139 c.statuses.append(ChangesetStatusModel().get_status(
139 c.statuses.append(ChangesetStatusModel().get_status(
140 self.db_repo.repo_id, commit.raw_id))
140 self.db_repo.repo_id, commit.raw_id))
141 # comments from PR
141 # comments from PR
142 statuses = ChangesetStatusModel().get_statuses(
142 statuses = ChangesetStatusModel().get_statuses(
143 self.db_repo.repo_id, commit.raw_id,
143 self.db_repo.repo_id, commit.raw_id,
144 with_revisions=True)
144 with_revisions=True)
145 prs = set(st.pull_request for st in statuses
145 prs = set(st.pull_request for st in statuses
146 if st.pull_request is not None)
146 if st.pull_request is not None)
147 # from associated statuses, check the pull requests, and
147 # from associated statuses, check the pull requests, and
148 # show comments from them
148 # show comments from them
149 for pr in prs:
149 for pr in prs:
150 c.comments.extend(pr.comments)
150 c.comments.extend(pr.comments)
151
151
152 c.unresolved_comments = CommentsModel()\
152 c.unresolved_comments = CommentsModel()\
153 .get_commit_unresolved_todos(commit.raw_id)
153 .get_commit_unresolved_todos(commit.raw_id)
154 c.resolved_comments = CommentsModel()\
154 c.resolved_comments = CommentsModel()\
155 .get_commit_resolved_todos(commit.raw_id)
155 .get_commit_resolved_todos(commit.raw_id)
156
156
157 diff = None
157 diff = None
158 # Iterate over ranges (default commit view is always one commit)
158 # Iterate over ranges (default commit view is always one commit)
159 for commit in c.commit_ranges:
159 for commit in c.commit_ranges:
160 c.changes[commit.raw_id] = []
160 c.changes[commit.raw_id] = []
161
161
162 commit2 = commit
162 commit2 = commit
163 commit1 = commit.first_parent
163 commit1 = commit.first_parent
164
164
165 if method == 'show':
165 if method == 'show':
166 inline_comments = CommentsModel().get_inline_comments(
166 inline_comments = CommentsModel().get_inline_comments(
167 self.db_repo.repo_id, revision=commit.raw_id)
167 self.db_repo.repo_id, revision=commit.raw_id)
168 c.inline_cnt = CommentsModel().get_inline_comments_count(
168 c.inline_cnt = CommentsModel().get_inline_comments_count(
169 inline_comments)
169 inline_comments)
170 c.inline_comments = inline_comments
170 c.inline_comments = inline_comments
171
171
172 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
172 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
173 self.db_repo)
173 self.db_repo)
174 cache_file_path = diff_cache_exist(
174 cache_file_path = diff_cache_exist(
175 cache_path, 'diff', commit.raw_id,
175 cache_path, 'diff', commit.raw_id,
176 hide_whitespace_changes, diff_context, c.fulldiff)
176 hide_whitespace_changes, diff_context, c.fulldiff)
177
177
178 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
178 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
179 force_recache = str2bool(self.request.GET.get('force_recache'))
179 force_recache = str2bool(self.request.GET.get('force_recache'))
180
180
181 cached_diff = None
181 cached_diff = None
182 if caching_enabled:
182 if caching_enabled:
183 cached_diff = load_cached_diff(cache_file_path)
183 cached_diff = load_cached_diff(cache_file_path)
184
184
185 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
185 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
186 if not force_recache and has_proper_diff_cache:
186 if not force_recache and has_proper_diff_cache:
187 diffset = cached_diff['diff']
187 diffset = cached_diff['diff']
188 else:
188 else:
189 vcs_diff = self.rhodecode_vcs_repo.get_diff(
189 vcs_diff = self.rhodecode_vcs_repo.get_diff(
190 commit1, commit2,
190 commit1, commit2,
191 ignore_whitespace=hide_whitespace_changes,
191 ignore_whitespace=hide_whitespace_changes,
192 context=diff_context)
192 context=diff_context)
193
193
194 diff_processor = diffs.DiffProcessor(
194 diff_processor = diffs.DiffProcessor(
195 vcs_diff, format='newdiff', diff_limit=diff_limit,
195 vcs_diff, format='newdiff', diff_limit=diff_limit,
196 file_limit=file_limit, show_full_diff=c.fulldiff)
196 file_limit=file_limit, show_full_diff=c.fulldiff)
197
197
198 _parsed = diff_processor.prepare()
198 _parsed = diff_processor.prepare()
199
199
200 diffset = codeblocks.DiffSet(
200 diffset = codeblocks.DiffSet(
201 repo_name=self.db_repo_name,
201 repo_name=self.db_repo_name,
202 source_node_getter=codeblocks.diffset_node_getter(commit1),
202 source_node_getter=codeblocks.diffset_node_getter(commit1),
203 target_node_getter=codeblocks.diffset_node_getter(commit2))
203 target_node_getter=codeblocks.diffset_node_getter(commit2))
204
204
205 diffset = self.path_filter.render_patchset_filtered(
205 diffset = self.path_filter.render_patchset_filtered(
206 diffset, _parsed, commit1.raw_id, commit2.raw_id)
206 diffset, _parsed, commit1.raw_id, commit2.raw_id)
207
207
208 # save cached diff
208 # save cached diff
209 if caching_enabled:
209 if caching_enabled:
210 cache_diff(cache_file_path, diffset, None)
210 cache_diff(cache_file_path, diffset, None)
211
211
212 c.limited_diff = diffset.limited_diff
212 c.limited_diff = diffset.limited_diff
213 c.changes[commit.raw_id] = diffset
213 c.changes[commit.raw_id] = diffset
214 else:
214 else:
215 # TODO(marcink): no cache usage here...
215 # TODO(marcink): no cache usage here...
216 _diff = self.rhodecode_vcs_repo.get_diff(
216 _diff = self.rhodecode_vcs_repo.get_diff(
217 commit1, commit2,
217 commit1, commit2,
218 ignore_whitespace=hide_whitespace_changes, context=diff_context)
218 ignore_whitespace=hide_whitespace_changes, context=diff_context)
219 diff_processor = diffs.DiffProcessor(
219 diff_processor = diffs.DiffProcessor(
220 _diff, format='newdiff', diff_limit=diff_limit,
220 _diff, format='newdiff', diff_limit=diff_limit,
221 file_limit=file_limit, show_full_diff=c.fulldiff)
221 file_limit=file_limit, show_full_diff=c.fulldiff)
222 # downloads/raw we only need RAW diff nothing else
222 # downloads/raw we only need RAW diff nothing else
223 diff = self.path_filter.get_raw_patch(diff_processor)
223 diff = self.path_filter.get_raw_patch(diff_processor)
224 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
224 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
225
225
226 # sort comments by how they were generated
226 # sort comments by how they were generated
227 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
227 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
228
228
229 if len(c.commit_ranges) == 1:
229 if len(c.commit_ranges) == 1:
230 c.commit = c.commit_ranges[0]
230 c.commit = c.commit_ranges[0]
231 c.parent_tmpl = ''.join(
231 c.parent_tmpl = ''.join(
232 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
232 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
233
233
234 if method == 'download':
234 if method == 'download':
235 response = Response(diff)
235 response = Response(diff)
236 response.content_type = 'text/plain'
236 response.content_type = 'text/plain'
237 response.content_disposition = (
237 response.content_disposition = (
238 'attachment; filename=%s.diff' % commit_id_range[:12])
238 'attachment; filename=%s.diff' % commit_id_range[:12])
239 return response
239 return response
240 elif method == 'patch':
240 elif method == 'patch':
241 c.diff = safe_unicode(diff)
241 c.diff = safe_unicode(diff)
242 patch = render(
242 patch = render(
243 'rhodecode:templates/changeset/patch_changeset.mako',
243 'rhodecode:templates/changeset/patch_changeset.mako',
244 self._get_template_context(c), self.request)
244 self._get_template_context(c), self.request)
245 response = Response(patch)
245 response = Response(patch)
246 response.content_type = 'text/plain'
246 response.content_type = 'text/plain'
247 return response
247 return response
248 elif method == 'raw':
248 elif method == 'raw':
249 response = Response(diff)
249 response = Response(diff)
250 response.content_type = 'text/plain'
250 response.content_type = 'text/plain'
251 return response
251 return response
252 elif method == 'show':
252 elif method == 'show':
253 if len(c.commit_ranges) == 1:
253 if len(c.commit_ranges) == 1:
254 html = render(
254 html = render(
255 'rhodecode:templates/changeset/changeset.mako',
255 'rhodecode:templates/changeset/changeset.mako',
256 self._get_template_context(c), self.request)
256 self._get_template_context(c), self.request)
257 return Response(html)
257 return Response(html)
258 else:
258 else:
259 c.ancestor = None
259 c.ancestor = None
260 c.target_repo = self.db_repo
260 c.target_repo = self.db_repo
261 html = render(
261 html = render(
262 'rhodecode:templates/changeset/changeset_range.mako',
262 'rhodecode:templates/changeset/changeset_range.mako',
263 self._get_template_context(c), self.request)
263 self._get_template_context(c), self.request)
264 return Response(html)
264 return Response(html)
265
265
266 raise HTTPBadRequest()
266 raise HTTPBadRequest()
267
267
268 @LoginRequired()
268 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
269 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
270 'repository.read', 'repository.write', 'repository.admin')
271 @view_config(
271 @view_config(
272 route_name='repo_commit', request_method='GET',
272 route_name='repo_commit', request_method='GET',
273 renderer=None)
273 renderer=None)
274 def repo_commit_show(self):
274 def repo_commit_show(self):
275 commit_id = self.request.matchdict['commit_id']
275 commit_id = self.request.matchdict['commit_id']
276 return self._commit(commit_id, method='show')
276 return self._commit(commit_id, method='show')
277
277
278 @LoginRequired()
278 @LoginRequired()
279 @HasRepoPermissionAnyDecorator(
279 @HasRepoPermissionAnyDecorator(
280 'repository.read', 'repository.write', 'repository.admin')
280 'repository.read', 'repository.write', 'repository.admin')
281 @view_config(
281 @view_config(
282 route_name='repo_commit_raw', request_method='GET',
282 route_name='repo_commit_raw', request_method='GET',
283 renderer=None)
283 renderer=None)
284 @view_config(
284 @view_config(
285 route_name='repo_commit_raw_deprecated', request_method='GET',
285 route_name='repo_commit_raw_deprecated', request_method='GET',
286 renderer=None)
286 renderer=None)
287 def repo_commit_raw(self):
287 def repo_commit_raw(self):
288 commit_id = self.request.matchdict['commit_id']
288 commit_id = self.request.matchdict['commit_id']
289 return self._commit(commit_id, method='raw')
289 return self._commit(commit_id, method='raw')
290
290
291 @LoginRequired()
291 @LoginRequired()
292 @HasRepoPermissionAnyDecorator(
292 @HasRepoPermissionAnyDecorator(
293 'repository.read', 'repository.write', 'repository.admin')
293 'repository.read', 'repository.write', 'repository.admin')
294 @view_config(
294 @view_config(
295 route_name='repo_commit_patch', request_method='GET',
295 route_name='repo_commit_patch', request_method='GET',
296 renderer=None)
296 renderer=None)
297 def repo_commit_patch(self):
297 def repo_commit_patch(self):
298 commit_id = self.request.matchdict['commit_id']
298 commit_id = self.request.matchdict['commit_id']
299 return self._commit(commit_id, method='patch')
299 return self._commit(commit_id, method='patch')
300
300
301 @LoginRequired()
301 @LoginRequired()
302 @HasRepoPermissionAnyDecorator(
302 @HasRepoPermissionAnyDecorator(
303 'repository.read', 'repository.write', 'repository.admin')
303 'repository.read', 'repository.write', 'repository.admin')
304 @view_config(
304 @view_config(
305 route_name='repo_commit_download', request_method='GET',
305 route_name='repo_commit_download', request_method='GET',
306 renderer=None)
306 renderer=None)
307 def repo_commit_download(self):
307 def repo_commit_download(self):
308 commit_id = self.request.matchdict['commit_id']
308 commit_id = self.request.matchdict['commit_id']
309 return self._commit(commit_id, method='download')
309 return self._commit(commit_id, method='download')
310
310
311 @LoginRequired()
311 @LoginRequired()
312 @NotAnonymous()
312 @NotAnonymous()
313 @HasRepoPermissionAnyDecorator(
313 @HasRepoPermissionAnyDecorator(
314 'repository.read', 'repository.write', 'repository.admin')
314 'repository.read', 'repository.write', 'repository.admin')
315 @CSRFRequired()
315 @CSRFRequired()
316 @view_config(
316 @view_config(
317 route_name='repo_commit_comment_create', request_method='POST',
317 route_name='repo_commit_comment_create', request_method='POST',
318 renderer='json_ext')
318 renderer='json_ext')
319 def repo_commit_comment_create(self):
319 def repo_commit_comment_create(self):
320 _ = self.request.translate
320 _ = self.request.translate
321 commit_id = self.request.matchdict['commit_id']
321 commit_id = self.request.matchdict['commit_id']
322
322
323 c = self.load_default_context()
323 c = self.load_default_context()
324 status = self.request.POST.get('changeset_status', None)
324 status = self.request.POST.get('changeset_status', None)
325 text = self.request.POST.get('text')
325 text = self.request.POST.get('text')
326 comment_type = self.request.POST.get('comment_type')
326 comment_type = self.request.POST.get('comment_type')
327 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
327 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
328
328
329 if status:
329 if status:
330 text = text or (_('Status change %(transition_icon)s %(status)s')
330 text = text or (_('Status change %(transition_icon)s %(status)s')
331 % {'transition_icon': '>',
331 % {'transition_icon': '>',
332 'status': ChangesetStatus.get_status_lbl(status)})
332 'status': ChangesetStatus.get_status_lbl(status)})
333
333
334 multi_commit_ids = []
334 multi_commit_ids = []
335 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
335 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
336 if _commit_id not in ['', None, EmptyCommit.raw_id]:
336 if _commit_id not in ['', None, EmptyCommit.raw_id]:
337 if _commit_id not in multi_commit_ids:
337 if _commit_id not in multi_commit_ids:
338 multi_commit_ids.append(_commit_id)
338 multi_commit_ids.append(_commit_id)
339
339
340 commit_ids = multi_commit_ids or [commit_id]
340 commit_ids = multi_commit_ids or [commit_id]
341
341
342 comment = None
342 comment = None
343 for current_id in filter(None, commit_ids):
343 for current_id in filter(None, commit_ids):
344 comment = CommentsModel().create(
344 comment = CommentsModel().create(
345 text=text,
345 text=text,
346 repo=self.db_repo.repo_id,
346 repo=self.db_repo.repo_id,
347 user=self._rhodecode_db_user.user_id,
347 user=self._rhodecode_db_user.user_id,
348 commit_id=current_id,
348 commit_id=current_id,
349 f_path=self.request.POST.get('f_path'),
349 f_path=self.request.POST.get('f_path'),
350 line_no=self.request.POST.get('line'),
350 line_no=self.request.POST.get('line'),
351 status_change=(ChangesetStatus.get_status_lbl(status)
351 status_change=(ChangesetStatus.get_status_lbl(status)
352 if status else None),
352 if status else None),
353 status_change_type=status,
353 status_change_type=status,
354 comment_type=comment_type,
354 comment_type=comment_type,
355 resolves_comment_id=resolves_comment_id,
355 resolves_comment_id=resolves_comment_id,
356 auth_user=self._rhodecode_user
356 auth_user=self._rhodecode_user
357 )
357 )
358
358
359 # get status if set !
359 # get status if set !
360 if status:
360 if status:
361 # if latest status was from pull request and it's closed
361 # if latest status was from pull request and it's closed
362 # disallow changing status !
362 # disallow changing status !
363 # dont_allow_on_closed_pull_request = True !
363 # dont_allow_on_closed_pull_request = True !
364
364
365 try:
365 try:
366 ChangesetStatusModel().set_status(
366 ChangesetStatusModel().set_status(
367 self.db_repo.repo_id,
367 self.db_repo.repo_id,
368 status,
368 status,
369 self._rhodecode_db_user.user_id,
369 self._rhodecode_db_user.user_id,
370 comment,
370 comment,
371 revision=current_id,
371 revision=current_id,
372 dont_allow_on_closed_pull_request=True
372 dont_allow_on_closed_pull_request=True
373 )
373 )
374 except StatusChangeOnClosedPullRequestError:
374 except StatusChangeOnClosedPullRequestError:
375 msg = _('Changing the status of a commit associated with '
375 msg = _('Changing the status of a commit associated with '
376 'a closed pull request is not allowed')
376 'a closed pull request is not allowed')
377 log.exception(msg)
377 log.exception(msg)
378 h.flash(msg, category='warning')
378 h.flash(msg, category='warning')
379 raise HTTPFound(h.route_path(
379 raise HTTPFound(h.route_path(
380 'repo_commit', repo_name=self.db_repo_name,
380 'repo_commit', repo_name=self.db_repo_name,
381 commit_id=current_id))
381 commit_id=current_id))
382
382
383 commit = self.db_repo.get_commit(current_id)
383 commit = self.db_repo.get_commit(current_id)
384 CommentsModel().trigger_commit_comment_hook(
384 CommentsModel().trigger_commit_comment_hook(
385 self.db_repo, self._rhodecode_user, 'create',
385 self.db_repo, self._rhodecode_user, 'create',
386 data={'comment': comment, 'commit': commit})
386 data={'comment': comment, 'commit': commit})
387
387
388 # finalize, commit and redirect
388 # finalize, commit and redirect
389 Session().commit()
389 Session().commit()
390
390
391 data = {
391 data = {
392 'target_id': h.safeid(h.safe_unicode(
392 'target_id': h.safeid(h.safe_unicode(
393 self.request.POST.get('f_path'))),
393 self.request.POST.get('f_path'))),
394 }
394 }
395 if comment:
395 if comment:
396 c.co = comment
396 c.co = comment
397 rendered_comment = render(
397 rendered_comment = render(
398 'rhodecode:templates/changeset/changeset_comment_block.mako',
398 'rhodecode:templates/changeset/changeset_comment_block.mako',
399 self._get_template_context(c), self.request)
399 self._get_template_context(c), self.request)
400
400
401 data.update(comment.get_dict())
401 data.update(comment.get_dict())
402 data.update({'rendered_text': rendered_comment})
402 data.update({'rendered_text': rendered_comment})
403
403
404 return data
404 return data
405
405
406 @LoginRequired()
406 @LoginRequired()
407 @NotAnonymous()
407 @NotAnonymous()
408 @HasRepoPermissionAnyDecorator(
408 @HasRepoPermissionAnyDecorator(
409 'repository.read', 'repository.write', 'repository.admin')
409 'repository.read', 'repository.write', 'repository.admin')
410 @CSRFRequired()
410 @CSRFRequired()
411 @view_config(
411 @view_config(
412 route_name='repo_commit_comment_preview', request_method='POST',
412 route_name='repo_commit_comment_preview', request_method='POST',
413 renderer='string', xhr=True)
413 renderer='string', xhr=True)
414 def repo_commit_comment_preview(self):
414 def repo_commit_comment_preview(self):
415 # Technically a CSRF token is not needed as no state changes with this
415 # Technically a CSRF token is not needed as no state changes with this
416 # call. However, as this is a POST is better to have it, so automated
416 # call. However, as this is a POST is better to have it, so automated
417 # tools don't flag it as potential CSRF.
417 # tools don't flag it as potential CSRF.
418 # Post is required because the payload could be bigger than the maximum
418 # Post is required because the payload could be bigger than the maximum
419 # allowed by GET.
419 # allowed by GET.
420
420
421 text = self.request.POST.get('text')
421 text = self.request.POST.get('text')
422 renderer = self.request.POST.get('renderer') or 'rst'
422 renderer = self.request.POST.get('renderer') or 'rst'
423 if text:
423 if text:
424 return h.render(text, renderer=renderer, mentions=True,
424 return h.render(text, renderer=renderer, mentions=True,
425 repo_name=self.db_repo_name)
425 repo_name=self.db_repo_name)
426 return ''
426 return ''
427
427
428 @LoginRequired()
428 @LoginRequired()
429 @NotAnonymous()
429 @NotAnonymous()
430 @HasRepoPermissionAnyDecorator(
430 @HasRepoPermissionAnyDecorator(
431 'repository.read', 'repository.write', 'repository.admin')
431 'repository.read', 'repository.write', 'repository.admin')
432 @CSRFRequired()
432 @CSRFRequired()
433 @view_config(
433 @view_config(
434 route_name='repo_commit_comment_attachment_upload', request_method='POST',
434 route_name='repo_commit_comment_attachment_upload', request_method='POST',
435 renderer='json_ext', xhr=True)
435 renderer='json_ext', xhr=True)
436 def repo_commit_comment_attachment_upload(self):
436 def repo_commit_comment_attachment_upload(self):
437 c = self.load_default_context()
437 c = self.load_default_context()
438 upload_key = 'attachment'
438 upload_key = 'attachment'
439
439
440 file_obj = self.request.POST.get(upload_key)
440 file_obj = self.request.POST.get(upload_key)
441
441
442 if file_obj is None:
442 if file_obj is None:
443 self.request.response.status = 400
443 self.request.response.status = 400
444 return {'store_fid': None,
444 return {'store_fid': None,
445 'access_path': None,
445 'access_path': None,
446 'error': '{} data field is missing'.format(upload_key)}
446 'error': '{} data field is missing'.format(upload_key)}
447
447
448 if not hasattr(file_obj, 'filename'):
448 if not hasattr(file_obj, 'filename'):
449 self.request.response.status = 400
449 self.request.response.status = 400
450 return {'store_fid': None,
450 return {'store_fid': None,
451 'access_path': None,
451 'access_path': None,
452 'error': 'filename cannot be read from the data field'}
452 'error': 'filename cannot be read from the data field'}
453
453
454 filename = file_obj.filename
454 filename = file_obj.filename
455 file_display_name = filename
455 file_display_name = filename
456
456
457 metadata = {
457 metadata = {
458 'user_uploaded': {'username': self._rhodecode_user.username,
458 'user_uploaded': {'username': self._rhodecode_user.username,
459 'user_id': self._rhodecode_user.user_id,
459 'user_id': self._rhodecode_user.user_id,
460 'ip': self._rhodecode_user.ip_addr}}
460 'ip': self._rhodecode_user.ip_addr}}
461
461
462 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
462 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
463 allowed_extensions = [
463 allowed_extensions = [
464 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
464 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
465 '.pptx', '.txt', '.xlsx', '.zip']
465 '.pptx', '.txt', '.xlsx', '.zip']
466 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
466 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
467
467
468 try:
468 try:
469 storage = store_utils.get_file_storage(self.request.registry.settings)
469 storage = store_utils.get_file_storage(self.request.registry.settings)
470 store_uid, metadata = storage.save_file(
470 store_uid, metadata = storage.save_file(
471 file_obj.file, filename, extra_metadata=metadata,
471 file_obj.file, filename, extra_metadata=metadata,
472 extensions=allowed_extensions, max_filesize=max_file_size)
472 extensions=allowed_extensions, max_filesize=max_file_size)
473 except FileNotAllowedException:
473 except FileNotAllowedException:
474 self.request.response.status = 400
474 self.request.response.status = 400
475 permitted_extensions = ', '.join(allowed_extensions)
475 permitted_extensions = ', '.join(allowed_extensions)
476 error_msg = 'File `{}` is not allowed. ' \
476 error_msg = 'File `{}` is not allowed. ' \
477 'Only following extensions are permitted: {}'.format(
477 'Only following extensions are permitted: {}'.format(
478 filename, permitted_extensions)
478 filename, permitted_extensions)
479 return {'store_fid': None,
479 return {'store_fid': None,
480 'access_path': None,
480 'access_path': None,
481 'error': error_msg}
481 'error': error_msg}
482 except FileOverSizeException:
482 except FileOverSizeException:
483 self.request.response.status = 400
483 self.request.response.status = 400
484 limit_mb = h.format_byte_size_binary(max_file_size)
484 limit_mb = h.format_byte_size_binary(max_file_size)
485 return {'store_fid': None,
485 return {'store_fid': None,
486 'access_path': None,
486 'access_path': None,
487 'error': 'File {} is exceeding allowed limit of {}.'.format(
487 'error': 'File {} is exceeding allowed limit of {}.'.format(
488 filename, limit_mb)}
488 filename, limit_mb)}
489
489
490 try:
490 try:
491 entry = FileStore.create(
491 entry = FileStore.create(
492 file_uid=store_uid, filename=metadata["filename"],
492 file_uid=store_uid, filename=metadata["filename"],
493 file_hash=metadata["sha256"], file_size=metadata["size"],
493 file_hash=metadata["sha256"], file_size=metadata["size"],
494 file_display_name=file_display_name,
494 file_display_name=file_display_name,
495 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
495 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
496 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
496 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
497 scope_repo_id=self.db_repo.repo_id
497 scope_repo_id=self.db_repo.repo_id
498 )
498 )
499 Session().add(entry)
499 Session().add(entry)
500 Session().commit()
500 Session().commit()
501 log.debug('Stored upload in DB as %s', entry)
501 log.debug('Stored upload in DB as %s', entry)
502 except Exception:
502 except Exception:
503 log.exception('Failed to store file %s', filename)
503 log.exception('Failed to store file %s', filename)
504 self.request.response.status = 400
504 self.request.response.status = 400
505 return {'store_fid': None,
505 return {'store_fid': None,
506 'access_path': None,
506 'access_path': None,
507 'error': 'File {} failed to store in DB.'.format(filename)}
507 'error': 'File {} failed to store in DB.'.format(filename)}
508
508
509 Session().commit()
509 Session().commit()
510
510
511 return {
511 return {
512 'store_fid': store_uid,
512 'store_fid': store_uid,
513 'access_path': h.route_path(
513 'access_path': h.route_path(
514 'download_file', fid=store_uid),
514 'download_file', fid=store_uid),
515 'fqn_access_path': h.route_url(
515 'fqn_access_path': h.route_url(
516 'download_file', fid=store_uid),
516 'download_file', fid=store_uid),
517 'repo_access_path': h.route_path(
517 'repo_access_path': h.route_path(
518 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
518 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
519 'repo_fqn_access_path': h.route_url(
519 'repo_fqn_access_path': h.route_url(
520 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
520 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
521 }
521 }
522
522
523 @LoginRequired()
523 @LoginRequired()
524 @NotAnonymous()
524 @NotAnonymous()
525 @HasRepoPermissionAnyDecorator(
525 @HasRepoPermissionAnyDecorator(
526 'repository.read', 'repository.write', 'repository.admin')
526 'repository.read', 'repository.write', 'repository.admin')
527 @CSRFRequired()
527 @CSRFRequired()
528 @view_config(
528 @view_config(
529 route_name='repo_commit_comment_delete', request_method='POST',
529 route_name='repo_commit_comment_delete', request_method='POST',
530 renderer='json_ext')
530 renderer='json_ext')
531 def repo_commit_comment_delete(self):
531 def repo_commit_comment_delete(self):
532 commit_id = self.request.matchdict['commit_id']
532 commit_id = self.request.matchdict['commit_id']
533 comment_id = self.request.matchdict['comment_id']
533 comment_id = self.request.matchdict['comment_id']
534
534
535 comment = ChangesetComment.get_or_404(comment_id)
535 comment = ChangesetComment.get_or_404(comment_id)
536 if not comment:
536 if not comment:
537 log.debug('Comment with id:%s not found, skipping', comment_id)
537 log.debug('Comment with id:%s not found, skipping', comment_id)
538 # comment already deleted in another call probably
538 # comment already deleted in another call probably
539 return True
539 return True
540
540
541 if comment.immutable:
542 # don't allow deleting comments that are immutable
543 raise HTTPForbidden()
544
541 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
545 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
542 super_admin = h.HasPermissionAny('hg.admin')()
546 super_admin = h.HasPermissionAny('hg.admin')()
543 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
547 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
544 is_repo_comment = comment.repo.repo_name == self.db_repo_name
548 is_repo_comment = comment.repo.repo_name == self.db_repo_name
545 comment_repo_admin = is_repo_admin and is_repo_comment
549 comment_repo_admin = is_repo_admin and is_repo_comment
546
550
547 if super_admin or comment_owner or comment_repo_admin:
551 if super_admin or comment_owner or comment_repo_admin:
548 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
552 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
549 Session().commit()
553 Session().commit()
550 return True
554 return True
551 else:
555 else:
552 log.warning('No permissions for user %s to delete comment_id: %s',
556 log.warning('No permissions for user %s to delete comment_id: %s',
553 self._rhodecode_db_user, comment_id)
557 self._rhodecode_db_user, comment_id)
554 raise HTTPNotFound()
558 raise HTTPNotFound()
555
559
556 @LoginRequired()
560 @LoginRequired()
557 @HasRepoPermissionAnyDecorator(
561 @HasRepoPermissionAnyDecorator(
558 'repository.read', 'repository.write', 'repository.admin')
562 'repository.read', 'repository.write', 'repository.admin')
559 @view_config(
563 @view_config(
560 route_name='repo_commit_data', request_method='GET',
564 route_name='repo_commit_data', request_method='GET',
561 renderer='json_ext', xhr=True)
565 renderer='json_ext', xhr=True)
562 def repo_commit_data(self):
566 def repo_commit_data(self):
563 commit_id = self.request.matchdict['commit_id']
567 commit_id = self.request.matchdict['commit_id']
564 self.load_default_context()
568 self.load_default_context()
565
569
566 try:
570 try:
567 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
571 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
568 except CommitDoesNotExistError as e:
572 except CommitDoesNotExistError as e:
569 return EmptyCommit(message=str(e))
573 return EmptyCommit(message=str(e))
570
574
571 @LoginRequired()
575 @LoginRequired()
572 @HasRepoPermissionAnyDecorator(
576 @HasRepoPermissionAnyDecorator(
573 'repository.read', 'repository.write', 'repository.admin')
577 'repository.read', 'repository.write', 'repository.admin')
574 @view_config(
578 @view_config(
575 route_name='repo_commit_children', request_method='GET',
579 route_name='repo_commit_children', request_method='GET',
576 renderer='json_ext', xhr=True)
580 renderer='json_ext', xhr=True)
577 def repo_commit_children(self):
581 def repo_commit_children(self):
578 commit_id = self.request.matchdict['commit_id']
582 commit_id = self.request.matchdict['commit_id']
579 self.load_default_context()
583 self.load_default_context()
580
584
581 try:
585 try:
582 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
586 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
583 children = commit.children
587 children = commit.children
584 except CommitDoesNotExistError:
588 except CommitDoesNotExistError:
585 children = []
589 children = []
586
590
587 result = {"results": children}
591 result = {"results": children}
588 return result
592 return result
589
593
590 @LoginRequired()
594 @LoginRequired()
591 @HasRepoPermissionAnyDecorator(
595 @HasRepoPermissionAnyDecorator(
592 'repository.read', 'repository.write', 'repository.admin')
596 'repository.read', 'repository.write', 'repository.admin')
593 @view_config(
597 @view_config(
594 route_name='repo_commit_parents', request_method='GET',
598 route_name='repo_commit_parents', request_method='GET',
595 renderer='json_ext')
599 renderer='json_ext')
596 def repo_commit_parents(self):
600 def repo_commit_parents(self):
597 commit_id = self.request.matchdict['commit_id']
601 commit_id = self.request.matchdict['commit_id']
598 self.load_default_context()
602 self.load_default_context()
599
603
600 try:
604 try:
601 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
605 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
602 parents = commit.parents
606 parents = commit.parents
603 except CommitDoesNotExistError:
607 except CommitDoesNotExistError:
604 parents = []
608 parents = []
605 result = {"results": parents}
609 result = {"results": parents}
606 return result
610 return result
@@ -1,1508 +1,1512 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, EmptyRepositoryError)
44 RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 # backward compat., we use for OLD PRs a plain renderer
63 # backward compat., we use for OLD PRs a plain renderer
64 c.renderer = 'plain'
64 c.renderer = 'plain'
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, search_q=search_q, source=source, statuses=statuses,
83 repo_name, search_q=search_q, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, search_q=search_q, source=source, statuses=statuses,
100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.pull_request_state,
111 pr.pull_request_id, pr.pull_request_state,
112 pr.work_in_progress, pr.target_repo.repo_name),
112 pr.work_in_progress, pr.target_repo.repo_name),
113 'name_raw': pr.pull_request_id,
113 'name_raw': pr.pull_request_id,
114 'status': _render('pullrequest_status',
114 'status': _render('pullrequest_status',
115 pr.calculated_review_status()),
115 pr.calculated_review_status()),
116 'title': _render('pullrequest_title', pr.title, pr.description),
116 'title': _render('pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
117 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
118 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
119 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
121 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
122 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'state': pr.pull_request_state,
124 'state': pr.pull_request_state,
125 'author': _render('pullrequest_author',
125 'author': _render('pullrequest_author',
126 pr.author.full_contact, ),
126 pr.author.full_contact, ),
127 'author_raw': pr.author.full_name,
127 'author_raw': pr.author.full_name,
128 'comments': _render('pullrequest_comments', len(comments)),
128 'comments': _render('pullrequest_comments', len(comments)),
129 'comments_raw': len(comments),
129 'comments_raw': len(comments),
130 'closed': pr.is_closed(),
130 'closed': pr.is_closed(),
131 })
131 })
132
132
133 data = ({
133 data = ({
134 'draw': draw,
134 'draw': draw,
135 'data': data,
135 'data': data,
136 'recordsTotal': pull_requests_total_count,
136 'recordsTotal': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
138 })
138 })
139 return data
139 return data
140
140
141 @LoginRequired()
141 @LoginRequired()
142 @HasRepoPermissionAnyDecorator(
142 @HasRepoPermissionAnyDecorator(
143 'repository.read', 'repository.write', 'repository.admin')
143 'repository.read', 'repository.write', 'repository.admin')
144 @view_config(
144 @view_config(
145 route_name='pullrequest_show_all', request_method='GET',
145 route_name='pullrequest_show_all', request_method='GET',
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 def pull_request_list(self):
147 def pull_request_list(self):
148 c = self.load_default_context()
148 c = self.load_default_context()
149
149
150 req_get = self.request.GET
150 req_get = self.request.GET
151 c.source = str2bool(req_get.get('source'))
151 c.source = str2bool(req_get.get('source'))
152 c.closed = str2bool(req_get.get('closed'))
152 c.closed = str2bool(req_get.get('closed'))
153 c.my = str2bool(req_get.get('my'))
153 c.my = str2bool(req_get.get('my'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156
156
157 c.active = 'open'
157 c.active = 'open'
158 if c.my:
158 if c.my:
159 c.active = 'my'
159 c.active = 'my'
160 if c.closed:
160 if c.closed:
161 c.active = 'closed'
161 c.active = 'closed'
162 if c.awaiting_review and not c.source:
162 if c.awaiting_review and not c.source:
163 c.active = 'awaiting'
163 c.active = 'awaiting'
164 if c.source and not c.awaiting_review:
164 if c.source and not c.awaiting_review:
165 c.active = 'source'
165 c.active = 'source'
166 if c.awaiting_my_review:
166 if c.awaiting_my_review:
167 c.active = 'awaiting_my'
167 c.active = 'awaiting_my'
168
168
169 return self._get_template_context(c)
169 return self._get_template_context(c)
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @HasRepoPermissionAnyDecorator(
172 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
173 'repository.read', 'repository.write', 'repository.admin')
174 @view_config(
174 @view_config(
175 route_name='pullrequest_show_all_data', request_method='GET',
175 route_name='pullrequest_show_all_data', request_method='GET',
176 renderer='json_ext', xhr=True)
176 renderer='json_ext', xhr=True)
177 def pull_request_list_data(self):
177 def pull_request_list_data(self):
178 self.load_default_context()
178 self.load_default_context()
179
179
180 # additional filters
180 # additional filters
181 req_get = self.request.GET
181 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
182 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
183 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
184 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
187
188 filter_type = 'awaiting_review' if awaiting_review \
188 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
189 else 'awaiting_my_review' if awaiting_my_review \
190 else None
190 else None
191
191
192 opened_by = None
192 opened_by = None
193 if my:
193 if my:
194 opened_by = [self._rhodecode_user.user_id]
194 opened_by = [self._rhodecode_user.user_id]
195
195
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
197 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
198 statuses = [PullRequest.STATUS_CLOSED]
199
199
200 data = self._get_pull_requests_list(
200 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
201 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
203
204 return data
204 return data
205
205
206 def _is_diff_cache_enabled(self, target_repo):
206 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
207 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
208 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
209 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
210 return caching_enabled
211
211
212 def _get_diffset(self, source_repo_name, source_repo,
212 def _get_diffset(self, source_repo_name, source_repo,
213 source_ref_id, target_ref_id,
213 source_ref_id, target_ref_id,
214 target_commit, source_commit, diff_limit, file_limit,
214 target_commit, source_commit, diff_limit, file_limit,
215 fulldiff, hide_whitespace_changes, diff_context):
215 fulldiff, hide_whitespace_changes, diff_context):
216
216
217 vcs_diff = PullRequestModel().get_diff(
217 vcs_diff = PullRequestModel().get_diff(
218 source_repo, source_ref_id, target_ref_id,
218 source_repo, source_ref_id, target_ref_id,
219 hide_whitespace_changes, diff_context)
219 hide_whitespace_changes, diff_context)
220
220
221 diff_processor = diffs.DiffProcessor(
221 diff_processor = diffs.DiffProcessor(
222 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 vcs_diff, format='newdiff', diff_limit=diff_limit,
223 file_limit=file_limit, show_full_diff=fulldiff)
223 file_limit=file_limit, show_full_diff=fulldiff)
224
224
225 _parsed = diff_processor.prepare()
225 _parsed = diff_processor.prepare()
226
226
227 diffset = codeblocks.DiffSet(
227 diffset = codeblocks.DiffSet(
228 repo_name=self.db_repo_name,
228 repo_name=self.db_repo_name,
229 source_repo_name=source_repo_name,
229 source_repo_name=source_repo_name,
230 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 source_node_getter=codeblocks.diffset_node_getter(target_commit),
231 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 target_node_getter=codeblocks.diffset_node_getter(source_commit),
232 )
232 )
233 diffset = self.path_filter.render_patchset_filtered(
233 diffset = self.path_filter.render_patchset_filtered(
234 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
235
235
236 return diffset
236 return diffset
237
237
238 def _get_range_diffset(self, source_scm, source_repo,
238 def _get_range_diffset(self, source_scm, source_repo,
239 commit1, commit2, diff_limit, file_limit,
239 commit1, commit2, diff_limit, file_limit,
240 fulldiff, hide_whitespace_changes, diff_context):
240 fulldiff, hide_whitespace_changes, diff_context):
241 vcs_diff = source_scm.get_diff(
241 vcs_diff = source_scm.get_diff(
242 commit1, commit2,
242 commit1, commit2,
243 ignore_whitespace=hide_whitespace_changes,
243 ignore_whitespace=hide_whitespace_changes,
244 context=diff_context)
244 context=diff_context)
245
245
246 diff_processor = diffs.DiffProcessor(
246 diff_processor = diffs.DiffProcessor(
247 vcs_diff, format='newdiff', diff_limit=diff_limit,
247 vcs_diff, format='newdiff', diff_limit=diff_limit,
248 file_limit=file_limit, show_full_diff=fulldiff)
248 file_limit=file_limit, show_full_diff=fulldiff)
249
249
250 _parsed = diff_processor.prepare()
250 _parsed = diff_processor.prepare()
251
251
252 diffset = codeblocks.DiffSet(
252 diffset = codeblocks.DiffSet(
253 repo_name=source_repo.repo_name,
253 repo_name=source_repo.repo_name,
254 source_node_getter=codeblocks.diffset_node_getter(commit1),
254 source_node_getter=codeblocks.diffset_node_getter(commit1),
255 target_node_getter=codeblocks.diffset_node_getter(commit2))
255 target_node_getter=codeblocks.diffset_node_getter(commit2))
256
256
257 diffset = self.path_filter.render_patchset_filtered(
257 diffset = self.path_filter.render_patchset_filtered(
258 diffset, _parsed, commit1.raw_id, commit2.raw_id)
258 diffset, _parsed, commit1.raw_id, commit2.raw_id)
259
259
260 return diffset
260 return diffset
261
261
262 @LoginRequired()
262 @LoginRequired()
263 @HasRepoPermissionAnyDecorator(
263 @HasRepoPermissionAnyDecorator(
264 'repository.read', 'repository.write', 'repository.admin')
264 'repository.read', 'repository.write', 'repository.admin')
265 @view_config(
265 @view_config(
266 route_name='pullrequest_show', request_method='GET',
266 route_name='pullrequest_show', request_method='GET',
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
268 def pull_request_show(self):
268 def pull_request_show(self):
269 _ = self.request.translate
269 _ = self.request.translate
270 c = self.load_default_context()
270 c = self.load_default_context()
271
271
272 pull_request = PullRequest.get_or_404(
272 pull_request = PullRequest.get_or_404(
273 self.request.matchdict['pull_request_id'])
273 self.request.matchdict['pull_request_id'])
274 pull_request_id = pull_request.pull_request_id
274 pull_request_id = pull_request.pull_request_id
275
275
276 c.state_progressing = pull_request.is_state_changing()
276 c.state_progressing = pull_request.is_state_changing()
277
277
278 _new_state = {
278 _new_state = {
279 'created': PullRequest.STATE_CREATED,
279 'created': PullRequest.STATE_CREATED,
280 }.get(self.request.GET.get('force_state'))
280 }.get(self.request.GET.get('force_state'))
281 if c.is_super_admin and _new_state:
281 if c.is_super_admin and _new_state:
282 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
282 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
283 h.flash(
283 h.flash(
284 _('Pull Request state was force changed to `{}`').format(_new_state),
284 _('Pull Request state was force changed to `{}`').format(_new_state),
285 category='success')
285 category='success')
286 Session().commit()
286 Session().commit()
287
287
288 raise HTTPFound(h.route_path(
288 raise HTTPFound(h.route_path(
289 'pullrequest_show', repo_name=self.db_repo_name,
289 'pullrequest_show', repo_name=self.db_repo_name,
290 pull_request_id=pull_request_id))
290 pull_request_id=pull_request_id))
291
291
292 version = self.request.GET.get('version')
292 version = self.request.GET.get('version')
293 from_version = self.request.GET.get('from_version') or version
293 from_version = self.request.GET.get('from_version') or version
294 merge_checks = self.request.GET.get('merge_checks')
294 merge_checks = self.request.GET.get('merge_checks')
295 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
295 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
296
296
297 # fetch global flags of ignore ws or context lines
297 # fetch global flags of ignore ws or context lines
298 diff_context = diffs.get_diff_context(self.request)
298 diff_context = diffs.get_diff_context(self.request)
299 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
299 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
300
300
301 force_refresh = str2bool(self.request.GET.get('force_refresh'))
301 force_refresh = str2bool(self.request.GET.get('force_refresh'))
302
302
303 (pull_request_latest,
303 (pull_request_latest,
304 pull_request_at_ver,
304 pull_request_at_ver,
305 pull_request_display_obj,
305 pull_request_display_obj,
306 at_version) = PullRequestModel().get_pr_version(
306 at_version) = PullRequestModel().get_pr_version(
307 pull_request_id, version=version)
307 pull_request_id, version=version)
308 pr_closed = pull_request_latest.is_closed()
308 pr_closed = pull_request_latest.is_closed()
309
309
310 if pr_closed and (version or from_version):
310 if pr_closed and (version or from_version):
311 # not allow to browse versions
311 # not allow to browse versions
312 raise HTTPFound(h.route_path(
312 raise HTTPFound(h.route_path(
313 'pullrequest_show', repo_name=self.db_repo_name,
313 'pullrequest_show', repo_name=self.db_repo_name,
314 pull_request_id=pull_request_id))
314 pull_request_id=pull_request_id))
315
315
316 versions = pull_request_display_obj.versions()
316 versions = pull_request_display_obj.versions()
317 # used to store per-commit range diffs
317 # used to store per-commit range diffs
318 c.changes = collections.OrderedDict()
318 c.changes = collections.OrderedDict()
319 c.range_diff_on = self.request.GET.get('range-diff') == "1"
319 c.range_diff_on = self.request.GET.get('range-diff') == "1"
320
320
321 c.at_version = at_version
321 c.at_version = at_version
322 c.at_version_num = (at_version
322 c.at_version_num = (at_version
323 if at_version and at_version != 'latest'
323 if at_version and at_version != 'latest'
324 else None)
324 else None)
325 c.at_version_pos = ChangesetComment.get_index_from_version(
325 c.at_version_pos = ChangesetComment.get_index_from_version(
326 c.at_version_num, versions)
326 c.at_version_num, versions)
327
327
328 (prev_pull_request_latest,
328 (prev_pull_request_latest,
329 prev_pull_request_at_ver,
329 prev_pull_request_at_ver,
330 prev_pull_request_display_obj,
330 prev_pull_request_display_obj,
331 prev_at_version) = PullRequestModel().get_pr_version(
331 prev_at_version) = PullRequestModel().get_pr_version(
332 pull_request_id, version=from_version)
332 pull_request_id, version=from_version)
333
333
334 c.from_version = prev_at_version
334 c.from_version = prev_at_version
335 c.from_version_num = (prev_at_version
335 c.from_version_num = (prev_at_version
336 if prev_at_version and prev_at_version != 'latest'
336 if prev_at_version and prev_at_version != 'latest'
337 else None)
337 else None)
338 c.from_version_pos = ChangesetComment.get_index_from_version(
338 c.from_version_pos = ChangesetComment.get_index_from_version(
339 c.from_version_num, versions)
339 c.from_version_num, versions)
340
340
341 # define if we're in COMPARE mode or VIEW at version mode
341 # define if we're in COMPARE mode or VIEW at version mode
342 compare = at_version != prev_at_version
342 compare = at_version != prev_at_version
343
343
344 # pull_requests repo_name we opened it against
344 # pull_requests repo_name we opened it against
345 # ie. target_repo must match
345 # ie. target_repo must match
346 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
346 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
347 raise HTTPNotFound()
347 raise HTTPNotFound()
348
348
349 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
349 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
350 pull_request_at_ver)
350 pull_request_at_ver)
351
351
352 c.pull_request = pull_request_display_obj
352 c.pull_request = pull_request_display_obj
353 c.renderer = pull_request_at_ver.description_renderer or c.renderer
353 c.renderer = pull_request_at_ver.description_renderer or c.renderer
354 c.pull_request_latest = pull_request_latest
354 c.pull_request_latest = pull_request_latest
355
355
356 if compare or (at_version and not at_version == 'latest'):
356 if compare or (at_version and not at_version == 'latest'):
357 c.allowed_to_change_status = False
357 c.allowed_to_change_status = False
358 c.allowed_to_update = False
358 c.allowed_to_update = False
359 c.allowed_to_merge = False
359 c.allowed_to_merge = False
360 c.allowed_to_delete = False
360 c.allowed_to_delete = False
361 c.allowed_to_comment = False
361 c.allowed_to_comment = False
362 c.allowed_to_close = False
362 c.allowed_to_close = False
363 else:
363 else:
364 can_change_status = PullRequestModel().check_user_change_status(
364 can_change_status = PullRequestModel().check_user_change_status(
365 pull_request_at_ver, self._rhodecode_user)
365 pull_request_at_ver, self._rhodecode_user)
366 c.allowed_to_change_status = can_change_status and not pr_closed
366 c.allowed_to_change_status = can_change_status and not pr_closed
367
367
368 c.allowed_to_update = PullRequestModel().check_user_update(
368 c.allowed_to_update = PullRequestModel().check_user_update(
369 pull_request_latest, self._rhodecode_user) and not pr_closed
369 pull_request_latest, self._rhodecode_user) and not pr_closed
370 c.allowed_to_merge = PullRequestModel().check_user_merge(
370 c.allowed_to_merge = PullRequestModel().check_user_merge(
371 pull_request_latest, self._rhodecode_user) and not pr_closed
371 pull_request_latest, self._rhodecode_user) and not pr_closed
372 c.allowed_to_delete = PullRequestModel().check_user_delete(
372 c.allowed_to_delete = PullRequestModel().check_user_delete(
373 pull_request_latest, self._rhodecode_user) and not pr_closed
373 pull_request_latest, self._rhodecode_user) and not pr_closed
374 c.allowed_to_comment = not pr_closed
374 c.allowed_to_comment = not pr_closed
375 c.allowed_to_close = c.allowed_to_merge and not pr_closed
375 c.allowed_to_close = c.allowed_to_merge and not pr_closed
376
376
377 c.forbid_adding_reviewers = False
377 c.forbid_adding_reviewers = False
378 c.forbid_author_to_review = False
378 c.forbid_author_to_review = False
379 c.forbid_commit_author_to_review = False
379 c.forbid_commit_author_to_review = False
380
380
381 if pull_request_latest.reviewer_data and \
381 if pull_request_latest.reviewer_data and \
382 'rules' in pull_request_latest.reviewer_data:
382 'rules' in pull_request_latest.reviewer_data:
383 rules = pull_request_latest.reviewer_data['rules'] or {}
383 rules = pull_request_latest.reviewer_data['rules'] or {}
384 try:
384 try:
385 c.forbid_adding_reviewers = rules.get(
385 c.forbid_adding_reviewers = rules.get(
386 'forbid_adding_reviewers')
386 'forbid_adding_reviewers')
387 c.forbid_author_to_review = rules.get(
387 c.forbid_author_to_review = rules.get(
388 'forbid_author_to_review')
388 'forbid_author_to_review')
389 c.forbid_commit_author_to_review = rules.get(
389 c.forbid_commit_author_to_review = rules.get(
390 'forbid_commit_author_to_review')
390 'forbid_commit_author_to_review')
391 except Exception:
391 except Exception:
392 pass
392 pass
393
393
394 # check merge capabilities
394 # check merge capabilities
395 _merge_check = MergeCheck.validate(
395 _merge_check = MergeCheck.validate(
396 pull_request_latest, auth_user=self._rhodecode_user,
396 pull_request_latest, auth_user=self._rhodecode_user,
397 translator=self.request.translate,
397 translator=self.request.translate,
398 force_shadow_repo_refresh=force_refresh)
398 force_shadow_repo_refresh=force_refresh)
399
399
400 c.pr_merge_errors = _merge_check.error_details
400 c.pr_merge_errors = _merge_check.error_details
401 c.pr_merge_possible = not _merge_check.failed
401 c.pr_merge_possible = not _merge_check.failed
402 c.pr_merge_message = _merge_check.merge_msg
402 c.pr_merge_message = _merge_check.merge_msg
403 c.pr_merge_source_commit = _merge_check.source_commit
403 c.pr_merge_source_commit = _merge_check.source_commit
404 c.pr_merge_target_commit = _merge_check.target_commit
404 c.pr_merge_target_commit = _merge_check.target_commit
405
405
406 c.pr_merge_info = MergeCheck.get_merge_conditions(
406 c.pr_merge_info = MergeCheck.get_merge_conditions(
407 pull_request_latest, translator=self.request.translate)
407 pull_request_latest, translator=self.request.translate)
408
408
409 c.pull_request_review_status = _merge_check.review_status
409 c.pull_request_review_status = _merge_check.review_status
410 if merge_checks:
410 if merge_checks:
411 self.request.override_renderer = \
411 self.request.override_renderer = \
412 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
412 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
413 return self._get_template_context(c)
413 return self._get_template_context(c)
414
414
415 comments_model = CommentsModel()
415 comments_model = CommentsModel()
416
416
417 # reviewers and statuses
417 # reviewers and statuses
418 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
418 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
419 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
419 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
420
420
421 # GENERAL COMMENTS with versions #
421 # GENERAL COMMENTS with versions #
422 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
422 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
423 q = q.order_by(ChangesetComment.comment_id.asc())
423 q = q.order_by(ChangesetComment.comment_id.asc())
424 general_comments = q
424 general_comments = q
425
425
426 # pick comments we want to render at current version
426 # pick comments we want to render at current version
427 c.comment_versions = comments_model.aggregate_comments(
427 c.comment_versions = comments_model.aggregate_comments(
428 general_comments, versions, c.at_version_num)
428 general_comments, versions, c.at_version_num)
429 c.comments = c.comment_versions[c.at_version_num]['until']
429 c.comments = c.comment_versions[c.at_version_num]['until']
430
430
431 # INLINE COMMENTS with versions #
431 # INLINE COMMENTS with versions #
432 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
432 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
433 q = q.order_by(ChangesetComment.comment_id.asc())
433 q = q.order_by(ChangesetComment.comment_id.asc())
434 inline_comments = q
434 inline_comments = q
435
435
436 c.inline_versions = comments_model.aggregate_comments(
436 c.inline_versions = comments_model.aggregate_comments(
437 inline_comments, versions, c.at_version_num, inline=True)
437 inline_comments, versions, c.at_version_num, inline=True)
438
438
439 # TODOs
439 # TODOs
440 c.unresolved_comments = CommentsModel() \
440 c.unresolved_comments = CommentsModel() \
441 .get_pull_request_unresolved_todos(pull_request)
441 .get_pull_request_unresolved_todos(pull_request)
442 c.resolved_comments = CommentsModel() \
442 c.resolved_comments = CommentsModel() \
443 .get_pull_request_resolved_todos(pull_request)
443 .get_pull_request_resolved_todos(pull_request)
444
444
445 # inject latest version
445 # inject latest version
446 latest_ver = PullRequest.get_pr_display_object(
446 latest_ver = PullRequest.get_pr_display_object(
447 pull_request_latest, pull_request_latest)
447 pull_request_latest, pull_request_latest)
448
448
449 c.versions = versions + [latest_ver]
449 c.versions = versions + [latest_ver]
450
450
451 # if we use version, then do not show later comments
451 # if we use version, then do not show later comments
452 # than current version
452 # than current version
453 display_inline_comments = collections.defaultdict(
453 display_inline_comments = collections.defaultdict(
454 lambda: collections.defaultdict(list))
454 lambda: collections.defaultdict(list))
455 for co in inline_comments:
455 for co in inline_comments:
456 if c.at_version_num:
456 if c.at_version_num:
457 # pick comments that are at least UPTO given version, so we
457 # pick comments that are at least UPTO given version, so we
458 # don't render comments for higher version
458 # don't render comments for higher version
459 should_render = co.pull_request_version_id and \
459 should_render = co.pull_request_version_id and \
460 co.pull_request_version_id <= c.at_version_num
460 co.pull_request_version_id <= c.at_version_num
461 else:
461 else:
462 # showing all, for 'latest'
462 # showing all, for 'latest'
463 should_render = True
463 should_render = True
464
464
465 if should_render:
465 if should_render:
466 display_inline_comments[co.f_path][co.line_no].append(co)
466 display_inline_comments[co.f_path][co.line_no].append(co)
467
467
468 # load diff data into template context, if we use compare mode then
468 # load diff data into template context, if we use compare mode then
469 # diff is calculated based on changes between versions of PR
469 # diff is calculated based on changes between versions of PR
470
470
471 source_repo = pull_request_at_ver.source_repo
471 source_repo = pull_request_at_ver.source_repo
472 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
472 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
473
473
474 target_repo = pull_request_at_ver.target_repo
474 target_repo = pull_request_at_ver.target_repo
475 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
475 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
476
476
477 if compare:
477 if compare:
478 # in compare switch the diff base to latest commit from prev version
478 # in compare switch the diff base to latest commit from prev version
479 target_ref_id = prev_pull_request_display_obj.revisions[0]
479 target_ref_id = prev_pull_request_display_obj.revisions[0]
480
480
481 # despite opening commits for bookmarks/branches/tags, we always
481 # despite opening commits for bookmarks/branches/tags, we always
482 # convert this to rev to prevent changes after bookmark or branch change
482 # convert this to rev to prevent changes after bookmark or branch change
483 c.source_ref_type = 'rev'
483 c.source_ref_type = 'rev'
484 c.source_ref = source_ref_id
484 c.source_ref = source_ref_id
485
485
486 c.target_ref_type = 'rev'
486 c.target_ref_type = 'rev'
487 c.target_ref = target_ref_id
487 c.target_ref = target_ref_id
488
488
489 c.source_repo = source_repo
489 c.source_repo = source_repo
490 c.target_repo = target_repo
490 c.target_repo = target_repo
491
491
492 c.commit_ranges = []
492 c.commit_ranges = []
493 source_commit = EmptyCommit()
493 source_commit = EmptyCommit()
494 target_commit = EmptyCommit()
494 target_commit = EmptyCommit()
495 c.missing_requirements = False
495 c.missing_requirements = False
496
496
497 source_scm = source_repo.scm_instance()
497 source_scm = source_repo.scm_instance()
498 target_scm = target_repo.scm_instance()
498 target_scm = target_repo.scm_instance()
499
499
500 shadow_scm = None
500 shadow_scm = None
501 try:
501 try:
502 shadow_scm = pull_request_latest.get_shadow_repo()
502 shadow_scm = pull_request_latest.get_shadow_repo()
503 except Exception:
503 except Exception:
504 log.debug('Failed to get shadow repo', exc_info=True)
504 log.debug('Failed to get shadow repo', exc_info=True)
505 # try first the existing source_repo, and then shadow
505 # try first the existing source_repo, and then shadow
506 # repo if we can obtain one
506 # repo if we can obtain one
507 commits_source_repo = source_scm
507 commits_source_repo = source_scm
508 if shadow_scm:
508 if shadow_scm:
509 commits_source_repo = shadow_scm
509 commits_source_repo = shadow_scm
510
510
511 c.commits_source_repo = commits_source_repo
511 c.commits_source_repo = commits_source_repo
512 c.ancestor = None # set it to None, to hide it from PR view
512 c.ancestor = None # set it to None, to hide it from PR view
513
513
514 # empty version means latest, so we keep this to prevent
514 # empty version means latest, so we keep this to prevent
515 # double caching
515 # double caching
516 version_normalized = version or 'latest'
516 version_normalized = version or 'latest'
517 from_version_normalized = from_version or 'latest'
517 from_version_normalized = from_version or 'latest'
518
518
519 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
519 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
520 cache_file_path = diff_cache_exist(
520 cache_file_path = diff_cache_exist(
521 cache_path, 'pull_request', pull_request_id, version_normalized,
521 cache_path, 'pull_request', pull_request_id, version_normalized,
522 from_version_normalized, source_ref_id, target_ref_id,
522 from_version_normalized, source_ref_id, target_ref_id,
523 hide_whitespace_changes, diff_context, c.fulldiff)
523 hide_whitespace_changes, diff_context, c.fulldiff)
524
524
525 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
525 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
526 force_recache = self.get_recache_flag()
526 force_recache = self.get_recache_flag()
527
527
528 cached_diff = None
528 cached_diff = None
529 if caching_enabled:
529 if caching_enabled:
530 cached_diff = load_cached_diff(cache_file_path)
530 cached_diff = load_cached_diff(cache_file_path)
531
531
532 has_proper_commit_cache = (
532 has_proper_commit_cache = (
533 cached_diff and cached_diff.get('commits')
533 cached_diff and cached_diff.get('commits')
534 and len(cached_diff.get('commits', [])) == 5
534 and len(cached_diff.get('commits', [])) == 5
535 and cached_diff.get('commits')[0]
535 and cached_diff.get('commits')[0]
536 and cached_diff.get('commits')[3])
536 and cached_diff.get('commits')[3])
537
537
538 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
538 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
539 diff_commit_cache = \
539 diff_commit_cache = \
540 (ancestor_commit, commit_cache, missing_requirements,
540 (ancestor_commit, commit_cache, missing_requirements,
541 source_commit, target_commit) = cached_diff['commits']
541 source_commit, target_commit) = cached_diff['commits']
542 else:
542 else:
543 # NOTE(marcink): we reach potentially unreachable errors when a PR has
543 # NOTE(marcink): we reach potentially unreachable errors when a PR has
544 # merge errors resulting in potentially hidden commits in the shadow repo.
544 # merge errors resulting in potentially hidden commits in the shadow repo.
545 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
545 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
546 and _merge_check.merge_response
546 and _merge_check.merge_response
547 maybe_unreachable = maybe_unreachable \
547 maybe_unreachable = maybe_unreachable \
548 and _merge_check.merge_response.metadata.get('unresolved_files')
548 and _merge_check.merge_response.metadata.get('unresolved_files')
549 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
549 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
550 diff_commit_cache = \
550 diff_commit_cache = \
551 (ancestor_commit, commit_cache, missing_requirements,
551 (ancestor_commit, commit_cache, missing_requirements,
552 source_commit, target_commit) = self.get_commits(
552 source_commit, target_commit) = self.get_commits(
553 commits_source_repo,
553 commits_source_repo,
554 pull_request_at_ver,
554 pull_request_at_ver,
555 source_commit,
555 source_commit,
556 source_ref_id,
556 source_ref_id,
557 source_scm,
557 source_scm,
558 target_commit,
558 target_commit,
559 target_ref_id,
559 target_ref_id,
560 target_scm, maybe_unreachable=maybe_unreachable)
560 target_scm, maybe_unreachable=maybe_unreachable)
561
561
562 # register our commit range
562 # register our commit range
563 for comm in commit_cache.values():
563 for comm in commit_cache.values():
564 c.commit_ranges.append(comm)
564 c.commit_ranges.append(comm)
565
565
566 c.missing_requirements = missing_requirements
566 c.missing_requirements = missing_requirements
567 c.ancestor_commit = ancestor_commit
567 c.ancestor_commit = ancestor_commit
568 c.statuses = source_repo.statuses(
568 c.statuses = source_repo.statuses(
569 [x.raw_id for x in c.commit_ranges])
569 [x.raw_id for x in c.commit_ranges])
570
570
571 # auto collapse if we have more than limit
571 # auto collapse if we have more than limit
572 collapse_limit = diffs.DiffProcessor._collapse_commits_over
572 collapse_limit = diffs.DiffProcessor._collapse_commits_over
573 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
573 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
574 c.compare_mode = compare
574 c.compare_mode = compare
575
575
576 # diff_limit is the old behavior, will cut off the whole diff
576 # diff_limit is the old behavior, will cut off the whole diff
577 # if the limit is applied otherwise will just hide the
577 # if the limit is applied otherwise will just hide the
578 # big files from the front-end
578 # big files from the front-end
579 diff_limit = c.visual.cut_off_limit_diff
579 diff_limit = c.visual.cut_off_limit_diff
580 file_limit = c.visual.cut_off_limit_file
580 file_limit = c.visual.cut_off_limit_file
581
581
582 c.missing_commits = False
582 c.missing_commits = False
583 if (c.missing_requirements
583 if (c.missing_requirements
584 or isinstance(source_commit, EmptyCommit)
584 or isinstance(source_commit, EmptyCommit)
585 or source_commit == target_commit):
585 or source_commit == target_commit):
586
586
587 c.missing_commits = True
587 c.missing_commits = True
588 else:
588 else:
589 c.inline_comments = display_inline_comments
589 c.inline_comments = display_inline_comments
590
590
591 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
591 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
592 if not force_recache and has_proper_diff_cache:
592 if not force_recache and has_proper_diff_cache:
593 c.diffset = cached_diff['diff']
593 c.diffset = cached_diff['diff']
594 (ancestor_commit, commit_cache, missing_requirements,
594 (ancestor_commit, commit_cache, missing_requirements,
595 source_commit, target_commit) = cached_diff['commits']
595 source_commit, target_commit) = cached_diff['commits']
596 else:
596 else:
597 c.diffset = self._get_diffset(
597 c.diffset = self._get_diffset(
598 c.source_repo.repo_name, commits_source_repo,
598 c.source_repo.repo_name, commits_source_repo,
599 source_ref_id, target_ref_id,
599 source_ref_id, target_ref_id,
600 target_commit, source_commit,
600 target_commit, source_commit,
601 diff_limit, file_limit, c.fulldiff,
601 diff_limit, file_limit, c.fulldiff,
602 hide_whitespace_changes, diff_context)
602 hide_whitespace_changes, diff_context)
603
603
604 # save cached diff
604 # save cached diff
605 if caching_enabled:
605 if caching_enabled:
606 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
606 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
607
607
608 c.limited_diff = c.diffset.limited_diff
608 c.limited_diff = c.diffset.limited_diff
609
609
610 # calculate removed files that are bound to comments
610 # calculate removed files that are bound to comments
611 comment_deleted_files = [
611 comment_deleted_files = [
612 fname for fname in display_inline_comments
612 fname for fname in display_inline_comments
613 if fname not in c.diffset.file_stats]
613 if fname not in c.diffset.file_stats]
614
614
615 c.deleted_files_comments = collections.defaultdict(dict)
615 c.deleted_files_comments = collections.defaultdict(dict)
616 for fname, per_line_comments in display_inline_comments.items():
616 for fname, per_line_comments in display_inline_comments.items():
617 if fname in comment_deleted_files:
617 if fname in comment_deleted_files:
618 c.deleted_files_comments[fname]['stats'] = 0
618 c.deleted_files_comments[fname]['stats'] = 0
619 c.deleted_files_comments[fname]['comments'] = list()
619 c.deleted_files_comments[fname]['comments'] = list()
620 for lno, comments in per_line_comments.items():
620 for lno, comments in per_line_comments.items():
621 c.deleted_files_comments[fname]['comments'].extend(comments)
621 c.deleted_files_comments[fname]['comments'].extend(comments)
622
622
623 # maybe calculate the range diff
623 # maybe calculate the range diff
624 if c.range_diff_on:
624 if c.range_diff_on:
625 # TODO(marcink): set whitespace/context
625 # TODO(marcink): set whitespace/context
626 context_lcl = 3
626 context_lcl = 3
627 ign_whitespace_lcl = False
627 ign_whitespace_lcl = False
628
628
629 for commit in c.commit_ranges:
629 for commit in c.commit_ranges:
630 commit2 = commit
630 commit2 = commit
631 commit1 = commit.first_parent
631 commit1 = commit.first_parent
632
632
633 range_diff_cache_file_path = diff_cache_exist(
633 range_diff_cache_file_path = diff_cache_exist(
634 cache_path, 'diff', commit.raw_id,
634 cache_path, 'diff', commit.raw_id,
635 ign_whitespace_lcl, context_lcl, c.fulldiff)
635 ign_whitespace_lcl, context_lcl, c.fulldiff)
636
636
637 cached_diff = None
637 cached_diff = None
638 if caching_enabled:
638 if caching_enabled:
639 cached_diff = load_cached_diff(range_diff_cache_file_path)
639 cached_diff = load_cached_diff(range_diff_cache_file_path)
640
640
641 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
641 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
642 if not force_recache and has_proper_diff_cache:
642 if not force_recache and has_proper_diff_cache:
643 diffset = cached_diff['diff']
643 diffset = cached_diff['diff']
644 else:
644 else:
645 diffset = self._get_range_diffset(
645 diffset = self._get_range_diffset(
646 commits_source_repo, source_repo,
646 commits_source_repo, source_repo,
647 commit1, commit2, diff_limit, file_limit,
647 commit1, commit2, diff_limit, file_limit,
648 c.fulldiff, ign_whitespace_lcl, context_lcl
648 c.fulldiff, ign_whitespace_lcl, context_lcl
649 )
649 )
650
650
651 # save cached diff
651 # save cached diff
652 if caching_enabled:
652 if caching_enabled:
653 cache_diff(range_diff_cache_file_path, diffset, None)
653 cache_diff(range_diff_cache_file_path, diffset, None)
654
654
655 c.changes[commit.raw_id] = diffset
655 c.changes[commit.raw_id] = diffset
656
656
657 # this is a hack to properly display links, when creating PR, the
657 # this is a hack to properly display links, when creating PR, the
658 # compare view and others uses different notation, and
658 # compare view and others uses different notation, and
659 # compare_commits.mako renders links based on the target_repo.
659 # compare_commits.mako renders links based on the target_repo.
660 # We need to swap that here to generate it properly on the html side
660 # We need to swap that here to generate it properly on the html side
661 c.target_repo = c.source_repo
661 c.target_repo = c.source_repo
662
662
663 c.commit_statuses = ChangesetStatus.STATUSES
663 c.commit_statuses = ChangesetStatus.STATUSES
664
664
665 c.show_version_changes = not pr_closed
665 c.show_version_changes = not pr_closed
666 if c.show_version_changes:
666 if c.show_version_changes:
667 cur_obj = pull_request_at_ver
667 cur_obj = pull_request_at_ver
668 prev_obj = prev_pull_request_at_ver
668 prev_obj = prev_pull_request_at_ver
669
669
670 old_commit_ids = prev_obj.revisions
670 old_commit_ids = prev_obj.revisions
671 new_commit_ids = cur_obj.revisions
671 new_commit_ids = cur_obj.revisions
672 commit_changes = PullRequestModel()._calculate_commit_id_changes(
672 commit_changes = PullRequestModel()._calculate_commit_id_changes(
673 old_commit_ids, new_commit_ids)
673 old_commit_ids, new_commit_ids)
674 c.commit_changes_summary = commit_changes
674 c.commit_changes_summary = commit_changes
675
675
676 # calculate the diff for commits between versions
676 # calculate the diff for commits between versions
677 c.commit_changes = []
677 c.commit_changes = []
678 mark = lambda cs, fw: list(
678 mark = lambda cs, fw: list(
679 h.itertools.izip_longest([], cs, fillvalue=fw))
679 h.itertools.izip_longest([], cs, fillvalue=fw))
680 for c_type, raw_id in mark(commit_changes.added, 'a') \
680 for c_type, raw_id in mark(commit_changes.added, 'a') \
681 + mark(commit_changes.removed, 'r') \
681 + mark(commit_changes.removed, 'r') \
682 + mark(commit_changes.common, 'c'):
682 + mark(commit_changes.common, 'c'):
683
683
684 if raw_id in commit_cache:
684 if raw_id in commit_cache:
685 commit = commit_cache[raw_id]
685 commit = commit_cache[raw_id]
686 else:
686 else:
687 try:
687 try:
688 commit = commits_source_repo.get_commit(raw_id)
688 commit = commits_source_repo.get_commit(raw_id)
689 except CommitDoesNotExistError:
689 except CommitDoesNotExistError:
690 # in case we fail extracting still use "dummy" commit
690 # in case we fail extracting still use "dummy" commit
691 # for display in commit diff
691 # for display in commit diff
692 commit = h.AttributeDict(
692 commit = h.AttributeDict(
693 {'raw_id': raw_id,
693 {'raw_id': raw_id,
694 'message': 'EMPTY or MISSING COMMIT'})
694 'message': 'EMPTY or MISSING COMMIT'})
695 c.commit_changes.append([c_type, commit])
695 c.commit_changes.append([c_type, commit])
696
696
697 # current user review statuses for each version
697 # current user review statuses for each version
698 c.review_versions = {}
698 c.review_versions = {}
699 if self._rhodecode_user.user_id in allowed_reviewers:
699 if self._rhodecode_user.user_id in allowed_reviewers:
700 for co in general_comments:
700 for co in general_comments:
701 if co.author.user_id == self._rhodecode_user.user_id:
701 if co.author.user_id == self._rhodecode_user.user_id:
702 status = co.status_change
702 status = co.status_change
703 if status:
703 if status:
704 _ver_pr = status[0].comment.pull_request_version_id
704 _ver_pr = status[0].comment.pull_request_version_id
705 c.review_versions[_ver_pr] = status[0]
705 c.review_versions[_ver_pr] = status[0]
706
706
707 return self._get_template_context(c)
707 return self._get_template_context(c)
708
708
709 def get_commits(
709 def get_commits(
710 self, commits_source_repo, pull_request_at_ver, source_commit,
710 self, commits_source_repo, pull_request_at_ver, source_commit,
711 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
711 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
712 maybe_unreachable=False):
712 maybe_unreachable=False):
713
713
714 commit_cache = collections.OrderedDict()
714 commit_cache = collections.OrderedDict()
715 missing_requirements = False
715 missing_requirements = False
716
716
717 try:
717 try:
718 pre_load = ["author", "date", "message", "branch", "parents"]
718 pre_load = ["author", "date", "message", "branch", "parents"]
719
719
720 pull_request_commits = pull_request_at_ver.revisions
720 pull_request_commits = pull_request_at_ver.revisions
721 log.debug('Loading %s commits from %s',
721 log.debug('Loading %s commits from %s',
722 len(pull_request_commits), commits_source_repo)
722 len(pull_request_commits), commits_source_repo)
723
723
724 for rev in pull_request_commits:
724 for rev in pull_request_commits:
725 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
725 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
726 maybe_unreachable=maybe_unreachable)
726 maybe_unreachable=maybe_unreachable)
727 commit_cache[comm.raw_id] = comm
727 commit_cache[comm.raw_id] = comm
728
728
729 # Order here matters, we first need to get target, and then
729 # Order here matters, we first need to get target, and then
730 # the source
730 # the source
731 target_commit = commits_source_repo.get_commit(
731 target_commit = commits_source_repo.get_commit(
732 commit_id=safe_str(target_ref_id))
732 commit_id=safe_str(target_ref_id))
733
733
734 source_commit = commits_source_repo.get_commit(
734 source_commit = commits_source_repo.get_commit(
735 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
735 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
736 except CommitDoesNotExistError:
736 except CommitDoesNotExistError:
737 log.warning('Failed to get commit from `{}` repo'.format(
737 log.warning('Failed to get commit from `{}` repo'.format(
738 commits_source_repo), exc_info=True)
738 commits_source_repo), exc_info=True)
739 except RepositoryRequirementError:
739 except RepositoryRequirementError:
740 log.warning('Failed to get all required data from repo', exc_info=True)
740 log.warning('Failed to get all required data from repo', exc_info=True)
741 missing_requirements = True
741 missing_requirements = True
742 ancestor_commit = None
742 ancestor_commit = None
743 try:
743 try:
744 ancestor_id = source_scm.get_common_ancestor(
744 ancestor_id = source_scm.get_common_ancestor(
745 source_commit.raw_id, target_commit.raw_id, target_scm)
745 source_commit.raw_id, target_commit.raw_id, target_scm)
746 ancestor_commit = source_scm.get_commit(ancestor_id)
746 ancestor_commit = source_scm.get_commit(ancestor_id)
747 except Exception:
747 except Exception:
748 ancestor_commit = None
748 ancestor_commit = None
749 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
749 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
750
750
751 def assure_not_empty_repo(self):
751 def assure_not_empty_repo(self):
752 _ = self.request.translate
752 _ = self.request.translate
753
753
754 try:
754 try:
755 self.db_repo.scm_instance().get_commit()
755 self.db_repo.scm_instance().get_commit()
756 except EmptyRepositoryError:
756 except EmptyRepositoryError:
757 h.flash(h.literal(_('There are no commits yet')),
757 h.flash(h.literal(_('There are no commits yet')),
758 category='warning')
758 category='warning')
759 raise HTTPFound(
759 raise HTTPFound(
760 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
760 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
761
761
762 @LoginRequired()
762 @LoginRequired()
763 @NotAnonymous()
763 @NotAnonymous()
764 @HasRepoPermissionAnyDecorator(
764 @HasRepoPermissionAnyDecorator(
765 'repository.read', 'repository.write', 'repository.admin')
765 'repository.read', 'repository.write', 'repository.admin')
766 @view_config(
766 @view_config(
767 route_name='pullrequest_new', request_method='GET',
767 route_name='pullrequest_new', request_method='GET',
768 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
768 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
769 def pull_request_new(self):
769 def pull_request_new(self):
770 _ = self.request.translate
770 _ = self.request.translate
771 c = self.load_default_context()
771 c = self.load_default_context()
772
772
773 self.assure_not_empty_repo()
773 self.assure_not_empty_repo()
774 source_repo = self.db_repo
774 source_repo = self.db_repo
775
775
776 commit_id = self.request.GET.get('commit')
776 commit_id = self.request.GET.get('commit')
777 branch_ref = self.request.GET.get('branch')
777 branch_ref = self.request.GET.get('branch')
778 bookmark_ref = self.request.GET.get('bookmark')
778 bookmark_ref = self.request.GET.get('bookmark')
779
779
780 try:
780 try:
781 source_repo_data = PullRequestModel().generate_repo_data(
781 source_repo_data = PullRequestModel().generate_repo_data(
782 source_repo, commit_id=commit_id,
782 source_repo, commit_id=commit_id,
783 branch=branch_ref, bookmark=bookmark_ref,
783 branch=branch_ref, bookmark=bookmark_ref,
784 translator=self.request.translate)
784 translator=self.request.translate)
785 except CommitDoesNotExistError as e:
785 except CommitDoesNotExistError as e:
786 log.exception(e)
786 log.exception(e)
787 h.flash(_('Commit does not exist'), 'error')
787 h.flash(_('Commit does not exist'), 'error')
788 raise HTTPFound(
788 raise HTTPFound(
789 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
789 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
790
790
791 default_target_repo = source_repo
791 default_target_repo = source_repo
792
792
793 if source_repo.parent and c.has_origin_repo_read_perm:
793 if source_repo.parent and c.has_origin_repo_read_perm:
794 parent_vcs_obj = source_repo.parent.scm_instance()
794 parent_vcs_obj = source_repo.parent.scm_instance()
795 if parent_vcs_obj and not parent_vcs_obj.is_empty():
795 if parent_vcs_obj and not parent_vcs_obj.is_empty():
796 # change default if we have a parent repo
796 # change default if we have a parent repo
797 default_target_repo = source_repo.parent
797 default_target_repo = source_repo.parent
798
798
799 target_repo_data = PullRequestModel().generate_repo_data(
799 target_repo_data = PullRequestModel().generate_repo_data(
800 default_target_repo, translator=self.request.translate)
800 default_target_repo, translator=self.request.translate)
801
801
802 selected_source_ref = source_repo_data['refs']['selected_ref']
802 selected_source_ref = source_repo_data['refs']['selected_ref']
803 title_source_ref = ''
803 title_source_ref = ''
804 if selected_source_ref:
804 if selected_source_ref:
805 title_source_ref = selected_source_ref.split(':', 2)[1]
805 title_source_ref = selected_source_ref.split(':', 2)[1]
806 c.default_title = PullRequestModel().generate_pullrequest_title(
806 c.default_title = PullRequestModel().generate_pullrequest_title(
807 source=source_repo.repo_name,
807 source=source_repo.repo_name,
808 source_ref=title_source_ref,
808 source_ref=title_source_ref,
809 target=default_target_repo.repo_name
809 target=default_target_repo.repo_name
810 )
810 )
811
811
812 c.default_repo_data = {
812 c.default_repo_data = {
813 'source_repo_name': source_repo.repo_name,
813 'source_repo_name': source_repo.repo_name,
814 'source_refs_json': json.dumps(source_repo_data),
814 'source_refs_json': json.dumps(source_repo_data),
815 'target_repo_name': default_target_repo.repo_name,
815 'target_repo_name': default_target_repo.repo_name,
816 'target_refs_json': json.dumps(target_repo_data),
816 'target_refs_json': json.dumps(target_repo_data),
817 }
817 }
818 c.default_source_ref = selected_source_ref
818 c.default_source_ref = selected_source_ref
819
819
820 return self._get_template_context(c)
820 return self._get_template_context(c)
821
821
822 @LoginRequired()
822 @LoginRequired()
823 @NotAnonymous()
823 @NotAnonymous()
824 @HasRepoPermissionAnyDecorator(
824 @HasRepoPermissionAnyDecorator(
825 'repository.read', 'repository.write', 'repository.admin')
825 'repository.read', 'repository.write', 'repository.admin')
826 @view_config(
826 @view_config(
827 route_name='pullrequest_repo_refs', request_method='GET',
827 route_name='pullrequest_repo_refs', request_method='GET',
828 renderer='json_ext', xhr=True)
828 renderer='json_ext', xhr=True)
829 def pull_request_repo_refs(self):
829 def pull_request_repo_refs(self):
830 self.load_default_context()
830 self.load_default_context()
831 target_repo_name = self.request.matchdict['target_repo_name']
831 target_repo_name = self.request.matchdict['target_repo_name']
832 repo = Repository.get_by_repo_name(target_repo_name)
832 repo = Repository.get_by_repo_name(target_repo_name)
833 if not repo:
833 if not repo:
834 raise HTTPNotFound()
834 raise HTTPNotFound()
835
835
836 target_perm = HasRepoPermissionAny(
836 target_perm = HasRepoPermissionAny(
837 'repository.read', 'repository.write', 'repository.admin')(
837 'repository.read', 'repository.write', 'repository.admin')(
838 target_repo_name)
838 target_repo_name)
839 if not target_perm:
839 if not target_perm:
840 raise HTTPNotFound()
840 raise HTTPNotFound()
841
841
842 return PullRequestModel().generate_repo_data(
842 return PullRequestModel().generate_repo_data(
843 repo, translator=self.request.translate)
843 repo, translator=self.request.translate)
844
844
845 @LoginRequired()
845 @LoginRequired()
846 @NotAnonymous()
846 @NotAnonymous()
847 @HasRepoPermissionAnyDecorator(
847 @HasRepoPermissionAnyDecorator(
848 'repository.read', 'repository.write', 'repository.admin')
848 'repository.read', 'repository.write', 'repository.admin')
849 @view_config(
849 @view_config(
850 route_name='pullrequest_repo_targets', request_method='GET',
850 route_name='pullrequest_repo_targets', request_method='GET',
851 renderer='json_ext', xhr=True)
851 renderer='json_ext', xhr=True)
852 def pullrequest_repo_targets(self):
852 def pullrequest_repo_targets(self):
853 _ = self.request.translate
853 _ = self.request.translate
854 filter_query = self.request.GET.get('query')
854 filter_query = self.request.GET.get('query')
855
855
856 # get the parents
856 # get the parents
857 parent_target_repos = []
857 parent_target_repos = []
858 if self.db_repo.parent:
858 if self.db_repo.parent:
859 parents_query = Repository.query() \
859 parents_query = Repository.query() \
860 .order_by(func.length(Repository.repo_name)) \
860 .order_by(func.length(Repository.repo_name)) \
861 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
861 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
862
862
863 if filter_query:
863 if filter_query:
864 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
864 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
865 parents_query = parents_query.filter(
865 parents_query = parents_query.filter(
866 Repository.repo_name.ilike(ilike_expression))
866 Repository.repo_name.ilike(ilike_expression))
867 parents = parents_query.limit(20).all()
867 parents = parents_query.limit(20).all()
868
868
869 for parent in parents:
869 for parent in parents:
870 parent_vcs_obj = parent.scm_instance()
870 parent_vcs_obj = parent.scm_instance()
871 if parent_vcs_obj and not parent_vcs_obj.is_empty():
871 if parent_vcs_obj and not parent_vcs_obj.is_empty():
872 parent_target_repos.append(parent)
872 parent_target_repos.append(parent)
873
873
874 # get other forks, and repo itself
874 # get other forks, and repo itself
875 query = Repository.query() \
875 query = Repository.query() \
876 .order_by(func.length(Repository.repo_name)) \
876 .order_by(func.length(Repository.repo_name)) \
877 .filter(
877 .filter(
878 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
878 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
879 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
879 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
880 ) \
880 ) \
881 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
881 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
882
882
883 if filter_query:
883 if filter_query:
884 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
884 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
885 query = query.filter(Repository.repo_name.ilike(ilike_expression))
885 query = query.filter(Repository.repo_name.ilike(ilike_expression))
886
886
887 limit = max(20 - len(parent_target_repos), 5) # not less then 5
887 limit = max(20 - len(parent_target_repos), 5) # not less then 5
888 target_repos = query.limit(limit).all()
888 target_repos = query.limit(limit).all()
889
889
890 all_target_repos = target_repos + parent_target_repos
890 all_target_repos = target_repos + parent_target_repos
891
891
892 repos = []
892 repos = []
893 # This checks permissions to the repositories
893 # This checks permissions to the repositories
894 for obj in ScmModel().get_repos(all_target_repos):
894 for obj in ScmModel().get_repos(all_target_repos):
895 repos.append({
895 repos.append({
896 'id': obj['name'],
896 'id': obj['name'],
897 'text': obj['name'],
897 'text': obj['name'],
898 'type': 'repo',
898 'type': 'repo',
899 'repo_id': obj['dbrepo']['repo_id'],
899 'repo_id': obj['dbrepo']['repo_id'],
900 'repo_type': obj['dbrepo']['repo_type'],
900 'repo_type': obj['dbrepo']['repo_type'],
901 'private': obj['dbrepo']['private'],
901 'private': obj['dbrepo']['private'],
902
902
903 })
903 })
904
904
905 data = {
905 data = {
906 'more': False,
906 'more': False,
907 'results': [{
907 'results': [{
908 'text': _('Repositories'),
908 'text': _('Repositories'),
909 'children': repos
909 'children': repos
910 }] if repos else []
910 }] if repos else []
911 }
911 }
912 return data
912 return data
913
913
914 @LoginRequired()
914 @LoginRequired()
915 @NotAnonymous()
915 @NotAnonymous()
916 @HasRepoPermissionAnyDecorator(
916 @HasRepoPermissionAnyDecorator(
917 'repository.read', 'repository.write', 'repository.admin')
917 'repository.read', 'repository.write', 'repository.admin')
918 @CSRFRequired()
918 @CSRFRequired()
919 @view_config(
919 @view_config(
920 route_name='pullrequest_create', request_method='POST',
920 route_name='pullrequest_create', request_method='POST',
921 renderer=None)
921 renderer=None)
922 def pull_request_create(self):
922 def pull_request_create(self):
923 _ = self.request.translate
923 _ = self.request.translate
924 self.assure_not_empty_repo()
924 self.assure_not_empty_repo()
925 self.load_default_context()
925 self.load_default_context()
926
926
927 controls = peppercorn.parse(self.request.POST.items())
927 controls = peppercorn.parse(self.request.POST.items())
928
928
929 try:
929 try:
930 form = PullRequestForm(
930 form = PullRequestForm(
931 self.request.translate, self.db_repo.repo_id)()
931 self.request.translate, self.db_repo.repo_id)()
932 _form = form.to_python(controls)
932 _form = form.to_python(controls)
933 except formencode.Invalid as errors:
933 except formencode.Invalid as errors:
934 if errors.error_dict.get('revisions'):
934 if errors.error_dict.get('revisions'):
935 msg = 'Revisions: %s' % errors.error_dict['revisions']
935 msg = 'Revisions: %s' % errors.error_dict['revisions']
936 elif errors.error_dict.get('pullrequest_title'):
936 elif errors.error_dict.get('pullrequest_title'):
937 msg = errors.error_dict.get('pullrequest_title')
937 msg = errors.error_dict.get('pullrequest_title')
938 else:
938 else:
939 msg = _('Error creating pull request: {}').format(errors)
939 msg = _('Error creating pull request: {}').format(errors)
940 log.exception(msg)
940 log.exception(msg)
941 h.flash(msg, 'error')
941 h.flash(msg, 'error')
942
942
943 # would rather just go back to form ...
943 # would rather just go back to form ...
944 raise HTTPFound(
944 raise HTTPFound(
945 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
945 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
946
946
947 source_repo = _form['source_repo']
947 source_repo = _form['source_repo']
948 source_ref = _form['source_ref']
948 source_ref = _form['source_ref']
949 target_repo = _form['target_repo']
949 target_repo = _form['target_repo']
950 target_ref = _form['target_ref']
950 target_ref = _form['target_ref']
951 commit_ids = _form['revisions'][::-1]
951 commit_ids = _form['revisions'][::-1]
952
952
953 # find the ancestor for this pr
953 # find the ancestor for this pr
954 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
954 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
955 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
955 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
956
956
957 if not (source_db_repo or target_db_repo):
957 if not (source_db_repo or target_db_repo):
958 h.flash(_('source_repo or target repo not found'), category='error')
958 h.flash(_('source_repo or target repo not found'), category='error')
959 raise HTTPFound(
959 raise HTTPFound(
960 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
960 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
961
961
962 # re-check permissions again here
962 # re-check permissions again here
963 # source_repo we must have read permissions
963 # source_repo we must have read permissions
964
964
965 source_perm = HasRepoPermissionAny(
965 source_perm = HasRepoPermissionAny(
966 'repository.read', 'repository.write', 'repository.admin')(
966 'repository.read', 'repository.write', 'repository.admin')(
967 source_db_repo.repo_name)
967 source_db_repo.repo_name)
968 if not source_perm:
968 if not source_perm:
969 msg = _('Not Enough permissions to source repo `{}`.'.format(
969 msg = _('Not Enough permissions to source repo `{}`.'.format(
970 source_db_repo.repo_name))
970 source_db_repo.repo_name))
971 h.flash(msg, category='error')
971 h.flash(msg, category='error')
972 # copy the args back to redirect
972 # copy the args back to redirect
973 org_query = self.request.GET.mixed()
973 org_query = self.request.GET.mixed()
974 raise HTTPFound(
974 raise HTTPFound(
975 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
975 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
976 _query=org_query))
976 _query=org_query))
977
977
978 # target repo we must have read permissions, and also later on
978 # target repo we must have read permissions, and also later on
979 # we want to check branch permissions here
979 # we want to check branch permissions here
980 target_perm = HasRepoPermissionAny(
980 target_perm = HasRepoPermissionAny(
981 'repository.read', 'repository.write', 'repository.admin')(
981 'repository.read', 'repository.write', 'repository.admin')(
982 target_db_repo.repo_name)
982 target_db_repo.repo_name)
983 if not target_perm:
983 if not target_perm:
984 msg = _('Not Enough permissions to target repo `{}`.'.format(
984 msg = _('Not Enough permissions to target repo `{}`.'.format(
985 target_db_repo.repo_name))
985 target_db_repo.repo_name))
986 h.flash(msg, category='error')
986 h.flash(msg, category='error')
987 # copy the args back to redirect
987 # copy the args back to redirect
988 org_query = self.request.GET.mixed()
988 org_query = self.request.GET.mixed()
989 raise HTTPFound(
989 raise HTTPFound(
990 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
990 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
991 _query=org_query))
991 _query=org_query))
992
992
993 source_scm = source_db_repo.scm_instance()
993 source_scm = source_db_repo.scm_instance()
994 target_scm = target_db_repo.scm_instance()
994 target_scm = target_db_repo.scm_instance()
995
995
996 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
996 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
997 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
997 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
998
998
999 ancestor = source_scm.get_common_ancestor(
999 ancestor = source_scm.get_common_ancestor(
1000 source_commit.raw_id, target_commit.raw_id, target_scm)
1000 source_commit.raw_id, target_commit.raw_id, target_scm)
1001
1001
1002 # recalculate target ref based on ancestor
1002 # recalculate target ref based on ancestor
1003 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1003 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1004 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1004 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1005
1005
1006 get_default_reviewers_data, validate_default_reviewers = \
1006 get_default_reviewers_data, validate_default_reviewers = \
1007 PullRequestModel().get_reviewer_functions()
1007 PullRequestModel().get_reviewer_functions()
1008
1008
1009 # recalculate reviewers logic, to make sure we can validate this
1009 # recalculate reviewers logic, to make sure we can validate this
1010 reviewer_rules = get_default_reviewers_data(
1010 reviewer_rules = get_default_reviewers_data(
1011 self._rhodecode_db_user, source_db_repo,
1011 self._rhodecode_db_user, source_db_repo,
1012 source_commit, target_db_repo, target_commit)
1012 source_commit, target_db_repo, target_commit)
1013
1013
1014 given_reviewers = _form['review_members']
1014 given_reviewers = _form['review_members']
1015 reviewers = validate_default_reviewers(
1015 reviewers = validate_default_reviewers(
1016 given_reviewers, reviewer_rules)
1016 given_reviewers, reviewer_rules)
1017
1017
1018 pullrequest_title = _form['pullrequest_title']
1018 pullrequest_title = _form['pullrequest_title']
1019 title_source_ref = source_ref.split(':', 2)[1]
1019 title_source_ref = source_ref.split(':', 2)[1]
1020 if not pullrequest_title:
1020 if not pullrequest_title:
1021 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1021 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1022 source=source_repo,
1022 source=source_repo,
1023 source_ref=title_source_ref,
1023 source_ref=title_source_ref,
1024 target=target_repo
1024 target=target_repo
1025 )
1025 )
1026
1026
1027 description = _form['pullrequest_desc']
1027 description = _form['pullrequest_desc']
1028 description_renderer = _form['description_renderer']
1028 description_renderer = _form['description_renderer']
1029
1029
1030 try:
1030 try:
1031 pull_request = PullRequestModel().create(
1031 pull_request = PullRequestModel().create(
1032 created_by=self._rhodecode_user.user_id,
1032 created_by=self._rhodecode_user.user_id,
1033 source_repo=source_repo,
1033 source_repo=source_repo,
1034 source_ref=source_ref,
1034 source_ref=source_ref,
1035 target_repo=target_repo,
1035 target_repo=target_repo,
1036 target_ref=target_ref,
1036 target_ref=target_ref,
1037 revisions=commit_ids,
1037 revisions=commit_ids,
1038 reviewers=reviewers,
1038 reviewers=reviewers,
1039 title=pullrequest_title,
1039 title=pullrequest_title,
1040 description=description,
1040 description=description,
1041 description_renderer=description_renderer,
1041 description_renderer=description_renderer,
1042 reviewer_data=reviewer_rules,
1042 reviewer_data=reviewer_rules,
1043 auth_user=self._rhodecode_user
1043 auth_user=self._rhodecode_user
1044 )
1044 )
1045 Session().commit()
1045 Session().commit()
1046
1046
1047 h.flash(_('Successfully opened new pull request'),
1047 h.flash(_('Successfully opened new pull request'),
1048 category='success')
1048 category='success')
1049 except Exception:
1049 except Exception:
1050 msg = _('Error occurred during creation of this pull request.')
1050 msg = _('Error occurred during creation of this pull request.')
1051 log.exception(msg)
1051 log.exception(msg)
1052 h.flash(msg, category='error')
1052 h.flash(msg, category='error')
1053
1053
1054 # copy the args back to redirect
1054 # copy the args back to redirect
1055 org_query = self.request.GET.mixed()
1055 org_query = self.request.GET.mixed()
1056 raise HTTPFound(
1056 raise HTTPFound(
1057 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1057 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1058 _query=org_query))
1058 _query=org_query))
1059
1059
1060 raise HTTPFound(
1060 raise HTTPFound(
1061 h.route_path('pullrequest_show', repo_name=target_repo,
1061 h.route_path('pullrequest_show', repo_name=target_repo,
1062 pull_request_id=pull_request.pull_request_id))
1062 pull_request_id=pull_request.pull_request_id))
1063
1063
1064 @LoginRequired()
1064 @LoginRequired()
1065 @NotAnonymous()
1065 @NotAnonymous()
1066 @HasRepoPermissionAnyDecorator(
1066 @HasRepoPermissionAnyDecorator(
1067 'repository.read', 'repository.write', 'repository.admin')
1067 'repository.read', 'repository.write', 'repository.admin')
1068 @CSRFRequired()
1068 @CSRFRequired()
1069 @view_config(
1069 @view_config(
1070 route_name='pullrequest_update', request_method='POST',
1070 route_name='pullrequest_update', request_method='POST',
1071 renderer='json_ext')
1071 renderer='json_ext')
1072 def pull_request_update(self):
1072 def pull_request_update(self):
1073 pull_request = PullRequest.get_or_404(
1073 pull_request = PullRequest.get_or_404(
1074 self.request.matchdict['pull_request_id'])
1074 self.request.matchdict['pull_request_id'])
1075 _ = self.request.translate
1075 _ = self.request.translate
1076
1076
1077 self.load_default_context()
1077 self.load_default_context()
1078 redirect_url = None
1078 redirect_url = None
1079
1079
1080 if pull_request.is_closed():
1080 if pull_request.is_closed():
1081 log.debug('update: forbidden because pull request is closed')
1081 log.debug('update: forbidden because pull request is closed')
1082 msg = _(u'Cannot update closed pull requests.')
1082 msg = _(u'Cannot update closed pull requests.')
1083 h.flash(msg, category='error')
1083 h.flash(msg, category='error')
1084 return {'response': True,
1084 return {'response': True,
1085 'redirect_url': redirect_url}
1085 'redirect_url': redirect_url}
1086
1086
1087 is_state_changing = pull_request.is_state_changing()
1087 is_state_changing = pull_request.is_state_changing()
1088
1088
1089 # only owner or admin can update it
1089 # only owner or admin can update it
1090 allowed_to_update = PullRequestModel().check_user_update(
1090 allowed_to_update = PullRequestModel().check_user_update(
1091 pull_request, self._rhodecode_user)
1091 pull_request, self._rhodecode_user)
1092 if allowed_to_update:
1092 if allowed_to_update:
1093 controls = peppercorn.parse(self.request.POST.items())
1093 controls = peppercorn.parse(self.request.POST.items())
1094 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1094 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1095
1095
1096 if 'review_members' in controls:
1096 if 'review_members' in controls:
1097 self._update_reviewers(
1097 self._update_reviewers(
1098 pull_request, controls['review_members'],
1098 pull_request, controls['review_members'],
1099 pull_request.reviewer_data)
1099 pull_request.reviewer_data)
1100 elif str2bool(self.request.POST.get('update_commits', 'false')):
1100 elif str2bool(self.request.POST.get('update_commits', 'false')):
1101 if is_state_changing:
1101 if is_state_changing:
1102 log.debug('commits update: forbidden because pull request is in state %s',
1102 log.debug('commits update: forbidden because pull request is in state %s',
1103 pull_request.pull_request_state)
1103 pull_request.pull_request_state)
1104 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1104 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1105 u'Current state is: `{}`').format(
1105 u'Current state is: `{}`').format(
1106 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1106 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1107 h.flash(msg, category='error')
1107 h.flash(msg, category='error')
1108 return {'response': True,
1108 return {'response': True,
1109 'redirect_url': redirect_url}
1109 'redirect_url': redirect_url}
1110
1110
1111 self._update_commits(pull_request)
1111 self._update_commits(pull_request)
1112 if force_refresh:
1112 if force_refresh:
1113 redirect_url = h.route_path(
1113 redirect_url = h.route_path(
1114 'pullrequest_show', repo_name=self.db_repo_name,
1114 'pullrequest_show', repo_name=self.db_repo_name,
1115 pull_request_id=pull_request.pull_request_id,
1115 pull_request_id=pull_request.pull_request_id,
1116 _query={"force_refresh": 1})
1116 _query={"force_refresh": 1})
1117 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1117 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1118 self._edit_pull_request(pull_request)
1118 self._edit_pull_request(pull_request)
1119 else:
1119 else:
1120 raise HTTPBadRequest()
1120 raise HTTPBadRequest()
1121
1121
1122 return {'response': True,
1122 return {'response': True,
1123 'redirect_url': redirect_url}
1123 'redirect_url': redirect_url}
1124 raise HTTPForbidden()
1124 raise HTTPForbidden()
1125
1125
1126 def _edit_pull_request(self, pull_request):
1126 def _edit_pull_request(self, pull_request):
1127 _ = self.request.translate
1127 _ = self.request.translate
1128
1128
1129 try:
1129 try:
1130 PullRequestModel().edit(
1130 PullRequestModel().edit(
1131 pull_request,
1131 pull_request,
1132 self.request.POST.get('title'),
1132 self.request.POST.get('title'),
1133 self.request.POST.get('description'),
1133 self.request.POST.get('description'),
1134 self.request.POST.get('description_renderer'),
1134 self.request.POST.get('description_renderer'),
1135 self._rhodecode_user)
1135 self._rhodecode_user)
1136 except ValueError:
1136 except ValueError:
1137 msg = _(u'Cannot update closed pull requests.')
1137 msg = _(u'Cannot update closed pull requests.')
1138 h.flash(msg, category='error')
1138 h.flash(msg, category='error')
1139 return
1139 return
1140 else:
1140 else:
1141 Session().commit()
1141 Session().commit()
1142
1142
1143 msg = _(u'Pull request title & description updated.')
1143 msg = _(u'Pull request title & description updated.')
1144 h.flash(msg, category='success')
1144 h.flash(msg, category='success')
1145 return
1145 return
1146
1146
1147 def _update_commits(self, pull_request):
1147 def _update_commits(self, pull_request):
1148 _ = self.request.translate
1148 _ = self.request.translate
1149
1149
1150 with pull_request.set_state(PullRequest.STATE_UPDATING):
1150 with pull_request.set_state(PullRequest.STATE_UPDATING):
1151 resp = PullRequestModel().update_commits(
1151 resp = PullRequestModel().update_commits(
1152 pull_request, self._rhodecode_db_user)
1152 pull_request, self._rhodecode_db_user)
1153
1153
1154 if resp.executed:
1154 if resp.executed:
1155
1155
1156 if resp.target_changed and resp.source_changed:
1156 if resp.target_changed and resp.source_changed:
1157 changed = 'target and source repositories'
1157 changed = 'target and source repositories'
1158 elif resp.target_changed and not resp.source_changed:
1158 elif resp.target_changed and not resp.source_changed:
1159 changed = 'target repository'
1159 changed = 'target repository'
1160 elif not resp.target_changed and resp.source_changed:
1160 elif not resp.target_changed and resp.source_changed:
1161 changed = 'source repository'
1161 changed = 'source repository'
1162 else:
1162 else:
1163 changed = 'nothing'
1163 changed = 'nothing'
1164
1164
1165 msg = _(u'Pull request updated to "{source_commit_id}" with '
1165 msg = _(u'Pull request updated to "{source_commit_id}" with '
1166 u'{count_added} added, {count_removed} removed commits. '
1166 u'{count_added} added, {count_removed} removed commits. '
1167 u'Source of changes: {change_source}')
1167 u'Source of changes: {change_source}')
1168 msg = msg.format(
1168 msg = msg.format(
1169 source_commit_id=pull_request.source_ref_parts.commit_id,
1169 source_commit_id=pull_request.source_ref_parts.commit_id,
1170 count_added=len(resp.changes.added),
1170 count_added=len(resp.changes.added),
1171 count_removed=len(resp.changes.removed),
1171 count_removed=len(resp.changes.removed),
1172 change_source=changed)
1172 change_source=changed)
1173 h.flash(msg, category='success')
1173 h.flash(msg, category='success')
1174
1174
1175 channel = '/repo${}$/pr/{}'.format(
1175 channel = '/repo${}$/pr/{}'.format(
1176 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1176 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1177 message = msg + (
1177 message = msg + (
1178 ' - <a onclick="window.location.reload()">'
1178 ' - <a onclick="window.location.reload()">'
1179 '<strong>{}</strong></a>'.format(_('Reload page')))
1179 '<strong>{}</strong></a>'.format(_('Reload page')))
1180 channelstream.post_message(
1180 channelstream.post_message(
1181 channel, message, self._rhodecode_user.username,
1181 channel, message, self._rhodecode_user.username,
1182 registry=self.request.registry)
1182 registry=self.request.registry)
1183 else:
1183 else:
1184 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1184 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1185 warning_reasons = [
1185 warning_reasons = [
1186 UpdateFailureReason.NO_CHANGE,
1186 UpdateFailureReason.NO_CHANGE,
1187 UpdateFailureReason.WRONG_REF_TYPE,
1187 UpdateFailureReason.WRONG_REF_TYPE,
1188 ]
1188 ]
1189 category = 'warning' if resp.reason in warning_reasons else 'error'
1189 category = 'warning' if resp.reason in warning_reasons else 'error'
1190 h.flash(msg, category=category)
1190 h.flash(msg, category=category)
1191
1191
1192 @LoginRequired()
1192 @LoginRequired()
1193 @NotAnonymous()
1193 @NotAnonymous()
1194 @HasRepoPermissionAnyDecorator(
1194 @HasRepoPermissionAnyDecorator(
1195 'repository.read', 'repository.write', 'repository.admin')
1195 'repository.read', 'repository.write', 'repository.admin')
1196 @CSRFRequired()
1196 @CSRFRequired()
1197 @view_config(
1197 @view_config(
1198 route_name='pullrequest_merge', request_method='POST',
1198 route_name='pullrequest_merge', request_method='POST',
1199 renderer='json_ext')
1199 renderer='json_ext')
1200 def pull_request_merge(self):
1200 def pull_request_merge(self):
1201 """
1201 """
1202 Merge will perform a server-side merge of the specified
1202 Merge will perform a server-side merge of the specified
1203 pull request, if the pull request is approved and mergeable.
1203 pull request, if the pull request is approved and mergeable.
1204 After successful merging, the pull request is automatically
1204 After successful merging, the pull request is automatically
1205 closed, with a relevant comment.
1205 closed, with a relevant comment.
1206 """
1206 """
1207 pull_request = PullRequest.get_or_404(
1207 pull_request = PullRequest.get_or_404(
1208 self.request.matchdict['pull_request_id'])
1208 self.request.matchdict['pull_request_id'])
1209 _ = self.request.translate
1209 _ = self.request.translate
1210
1210
1211 if pull_request.is_state_changing():
1211 if pull_request.is_state_changing():
1212 log.debug('show: forbidden because pull request is in state %s',
1212 log.debug('show: forbidden because pull request is in state %s',
1213 pull_request.pull_request_state)
1213 pull_request.pull_request_state)
1214 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1214 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1215 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1215 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1216 pull_request.pull_request_state)
1216 pull_request.pull_request_state)
1217 h.flash(msg, category='error')
1217 h.flash(msg, category='error')
1218 raise HTTPFound(
1218 raise HTTPFound(
1219 h.route_path('pullrequest_show',
1219 h.route_path('pullrequest_show',
1220 repo_name=pull_request.target_repo.repo_name,
1220 repo_name=pull_request.target_repo.repo_name,
1221 pull_request_id=pull_request.pull_request_id))
1221 pull_request_id=pull_request.pull_request_id))
1222
1222
1223 self.load_default_context()
1223 self.load_default_context()
1224
1224
1225 with pull_request.set_state(PullRequest.STATE_UPDATING):
1225 with pull_request.set_state(PullRequest.STATE_UPDATING):
1226 check = MergeCheck.validate(
1226 check = MergeCheck.validate(
1227 pull_request, auth_user=self._rhodecode_user,
1227 pull_request, auth_user=self._rhodecode_user,
1228 translator=self.request.translate)
1228 translator=self.request.translate)
1229 merge_possible = not check.failed
1229 merge_possible = not check.failed
1230
1230
1231 for err_type, error_msg in check.errors:
1231 for err_type, error_msg in check.errors:
1232 h.flash(error_msg, category=err_type)
1232 h.flash(error_msg, category=err_type)
1233
1233
1234 if merge_possible:
1234 if merge_possible:
1235 log.debug("Pre-conditions checked, trying to merge.")
1235 log.debug("Pre-conditions checked, trying to merge.")
1236 extras = vcs_operation_context(
1236 extras = vcs_operation_context(
1237 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1237 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1238 username=self._rhodecode_db_user.username, action='push',
1238 username=self._rhodecode_db_user.username, action='push',
1239 scm=pull_request.target_repo.repo_type)
1239 scm=pull_request.target_repo.repo_type)
1240 with pull_request.set_state(PullRequest.STATE_UPDATING):
1240 with pull_request.set_state(PullRequest.STATE_UPDATING):
1241 self._merge_pull_request(
1241 self._merge_pull_request(
1242 pull_request, self._rhodecode_db_user, extras)
1242 pull_request, self._rhodecode_db_user, extras)
1243 else:
1243 else:
1244 log.debug("Pre-conditions failed, NOT merging.")
1244 log.debug("Pre-conditions failed, NOT merging.")
1245
1245
1246 raise HTTPFound(
1246 raise HTTPFound(
1247 h.route_path('pullrequest_show',
1247 h.route_path('pullrequest_show',
1248 repo_name=pull_request.target_repo.repo_name,
1248 repo_name=pull_request.target_repo.repo_name,
1249 pull_request_id=pull_request.pull_request_id))
1249 pull_request_id=pull_request.pull_request_id))
1250
1250
1251 def _merge_pull_request(self, pull_request, user, extras):
1251 def _merge_pull_request(self, pull_request, user, extras):
1252 _ = self.request.translate
1252 _ = self.request.translate
1253 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1253 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1254
1254
1255 if merge_resp.executed:
1255 if merge_resp.executed:
1256 log.debug("The merge was successful, closing the pull request.")
1256 log.debug("The merge was successful, closing the pull request.")
1257 PullRequestModel().close_pull_request(
1257 PullRequestModel().close_pull_request(
1258 pull_request.pull_request_id, user)
1258 pull_request.pull_request_id, user)
1259 Session().commit()
1259 Session().commit()
1260 msg = _('Pull request was successfully merged and closed.')
1260 msg = _('Pull request was successfully merged and closed.')
1261 h.flash(msg, category='success')
1261 h.flash(msg, category='success')
1262 else:
1262 else:
1263 log.debug(
1263 log.debug(
1264 "The merge was not successful. Merge response: %s", merge_resp)
1264 "The merge was not successful. Merge response: %s", merge_resp)
1265 msg = merge_resp.merge_status_message
1265 msg = merge_resp.merge_status_message
1266 h.flash(msg, category='error')
1266 h.flash(msg, category='error')
1267
1267
1268 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1268 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1269 _ = self.request.translate
1269 _ = self.request.translate
1270
1270
1271 get_default_reviewers_data, validate_default_reviewers = \
1271 get_default_reviewers_data, validate_default_reviewers = \
1272 PullRequestModel().get_reviewer_functions()
1272 PullRequestModel().get_reviewer_functions()
1273
1273
1274 try:
1274 try:
1275 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1275 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1276 except ValueError as e:
1276 except ValueError as e:
1277 log.error('Reviewers Validation: {}'.format(e))
1277 log.error('Reviewers Validation: {}'.format(e))
1278 h.flash(e, category='error')
1278 h.flash(e, category='error')
1279 return
1279 return
1280
1280
1281 old_calculated_status = pull_request.calculated_review_status()
1281 old_calculated_status = pull_request.calculated_review_status()
1282 PullRequestModel().update_reviewers(
1282 PullRequestModel().update_reviewers(
1283 pull_request, reviewers, self._rhodecode_user)
1283 pull_request, reviewers, self._rhodecode_user)
1284 h.flash(_('Pull request reviewers updated.'), category='success')
1284 h.flash(_('Pull request reviewers updated.'), category='success')
1285 Session().commit()
1285 Session().commit()
1286
1286
1287 # trigger status changed if change in reviewers changes the status
1287 # trigger status changed if change in reviewers changes the status
1288 calculated_status = pull_request.calculated_review_status()
1288 calculated_status = pull_request.calculated_review_status()
1289 if old_calculated_status != calculated_status:
1289 if old_calculated_status != calculated_status:
1290 PullRequestModel().trigger_pull_request_hook(
1290 PullRequestModel().trigger_pull_request_hook(
1291 pull_request, self._rhodecode_user, 'review_status_change',
1291 pull_request, self._rhodecode_user, 'review_status_change',
1292 data={'status': calculated_status})
1292 data={'status': calculated_status})
1293
1293
1294 @LoginRequired()
1294 @LoginRequired()
1295 @NotAnonymous()
1295 @NotAnonymous()
1296 @HasRepoPermissionAnyDecorator(
1296 @HasRepoPermissionAnyDecorator(
1297 'repository.read', 'repository.write', 'repository.admin')
1297 'repository.read', 'repository.write', 'repository.admin')
1298 @CSRFRequired()
1298 @CSRFRequired()
1299 @view_config(
1299 @view_config(
1300 route_name='pullrequest_delete', request_method='POST',
1300 route_name='pullrequest_delete', request_method='POST',
1301 renderer='json_ext')
1301 renderer='json_ext')
1302 def pull_request_delete(self):
1302 def pull_request_delete(self):
1303 _ = self.request.translate
1303 _ = self.request.translate
1304
1304
1305 pull_request = PullRequest.get_or_404(
1305 pull_request = PullRequest.get_or_404(
1306 self.request.matchdict['pull_request_id'])
1306 self.request.matchdict['pull_request_id'])
1307 self.load_default_context()
1307 self.load_default_context()
1308
1308
1309 pr_closed = pull_request.is_closed()
1309 pr_closed = pull_request.is_closed()
1310 allowed_to_delete = PullRequestModel().check_user_delete(
1310 allowed_to_delete = PullRequestModel().check_user_delete(
1311 pull_request, self._rhodecode_user) and not pr_closed
1311 pull_request, self._rhodecode_user) and not pr_closed
1312
1312
1313 # only owner can delete it !
1313 # only owner can delete it !
1314 if allowed_to_delete:
1314 if allowed_to_delete:
1315 PullRequestModel().delete(pull_request, self._rhodecode_user)
1315 PullRequestModel().delete(pull_request, self._rhodecode_user)
1316 Session().commit()
1316 Session().commit()
1317 h.flash(_('Successfully deleted pull request'),
1317 h.flash(_('Successfully deleted pull request'),
1318 category='success')
1318 category='success')
1319 raise HTTPFound(h.route_path('pullrequest_show_all',
1319 raise HTTPFound(h.route_path('pullrequest_show_all',
1320 repo_name=self.db_repo_name))
1320 repo_name=self.db_repo_name))
1321
1321
1322 log.warning('user %s tried to delete pull request without access',
1322 log.warning('user %s tried to delete pull request without access',
1323 self._rhodecode_user)
1323 self._rhodecode_user)
1324 raise HTTPNotFound()
1324 raise HTTPNotFound()
1325
1325
1326 @LoginRequired()
1326 @LoginRequired()
1327 @NotAnonymous()
1327 @NotAnonymous()
1328 @HasRepoPermissionAnyDecorator(
1328 @HasRepoPermissionAnyDecorator(
1329 'repository.read', 'repository.write', 'repository.admin')
1329 'repository.read', 'repository.write', 'repository.admin')
1330 @CSRFRequired()
1330 @CSRFRequired()
1331 @view_config(
1331 @view_config(
1332 route_name='pullrequest_comment_create', request_method='POST',
1332 route_name='pullrequest_comment_create', request_method='POST',
1333 renderer='json_ext')
1333 renderer='json_ext')
1334 def pull_request_comment_create(self):
1334 def pull_request_comment_create(self):
1335 _ = self.request.translate
1335 _ = self.request.translate
1336
1336
1337 pull_request = PullRequest.get_or_404(
1337 pull_request = PullRequest.get_or_404(
1338 self.request.matchdict['pull_request_id'])
1338 self.request.matchdict['pull_request_id'])
1339 pull_request_id = pull_request.pull_request_id
1339 pull_request_id = pull_request.pull_request_id
1340
1340
1341 if pull_request.is_closed():
1341 if pull_request.is_closed():
1342 log.debug('comment: forbidden because pull request is closed')
1342 log.debug('comment: forbidden because pull request is closed')
1343 raise HTTPForbidden()
1343 raise HTTPForbidden()
1344
1344
1345 allowed_to_comment = PullRequestModel().check_user_comment(
1345 allowed_to_comment = PullRequestModel().check_user_comment(
1346 pull_request, self._rhodecode_user)
1346 pull_request, self._rhodecode_user)
1347 if not allowed_to_comment:
1347 if not allowed_to_comment:
1348 log.debug(
1348 log.debug(
1349 'comment: forbidden because pull request is from forbidden repo')
1349 'comment: forbidden because pull request is from forbidden repo')
1350 raise HTTPForbidden()
1350 raise HTTPForbidden()
1351
1351
1352 c = self.load_default_context()
1352 c = self.load_default_context()
1353
1353
1354 status = self.request.POST.get('changeset_status', None)
1354 status = self.request.POST.get('changeset_status', None)
1355 text = self.request.POST.get('text')
1355 text = self.request.POST.get('text')
1356 comment_type = self.request.POST.get('comment_type')
1356 comment_type = self.request.POST.get('comment_type')
1357 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1357 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1358 close_pull_request = self.request.POST.get('close_pull_request')
1358 close_pull_request = self.request.POST.get('close_pull_request')
1359
1359
1360 # the logic here should work like following, if we submit close
1360 # the logic here should work like following, if we submit close
1361 # pr comment, use `close_pull_request_with_comment` function
1361 # pr comment, use `close_pull_request_with_comment` function
1362 # else handle regular comment logic
1362 # else handle regular comment logic
1363
1363
1364 if close_pull_request:
1364 if close_pull_request:
1365 # only owner or admin or person with write permissions
1365 # only owner or admin or person with write permissions
1366 allowed_to_close = PullRequestModel().check_user_update(
1366 allowed_to_close = PullRequestModel().check_user_update(
1367 pull_request, self._rhodecode_user)
1367 pull_request, self._rhodecode_user)
1368 if not allowed_to_close:
1368 if not allowed_to_close:
1369 log.debug('comment: forbidden because not allowed to close '
1369 log.debug('comment: forbidden because not allowed to close '
1370 'pull request %s', pull_request_id)
1370 'pull request %s', pull_request_id)
1371 raise HTTPForbidden()
1371 raise HTTPForbidden()
1372
1372
1373 # This also triggers `review_status_change`
1373 # This also triggers `review_status_change`
1374 comment, status = PullRequestModel().close_pull_request_with_comment(
1374 comment, status = PullRequestModel().close_pull_request_with_comment(
1375 pull_request, self._rhodecode_user, self.db_repo, message=text,
1375 pull_request, self._rhodecode_user, self.db_repo, message=text,
1376 auth_user=self._rhodecode_user)
1376 auth_user=self._rhodecode_user)
1377 Session().flush()
1377 Session().flush()
1378
1378
1379 PullRequestModel().trigger_pull_request_hook(
1379 PullRequestModel().trigger_pull_request_hook(
1380 pull_request, self._rhodecode_user, 'comment',
1380 pull_request, self._rhodecode_user, 'comment',
1381 data={'comment': comment})
1381 data={'comment': comment})
1382
1382
1383 else:
1383 else:
1384 # regular comment case, could be inline, or one with status.
1384 # regular comment case, could be inline, or one with status.
1385 # for that one we check also permissions
1385 # for that one we check also permissions
1386
1386
1387 allowed_to_change_status = PullRequestModel().check_user_change_status(
1387 allowed_to_change_status = PullRequestModel().check_user_change_status(
1388 pull_request, self._rhodecode_user)
1388 pull_request, self._rhodecode_user)
1389
1389
1390 if status and allowed_to_change_status:
1390 if status and allowed_to_change_status:
1391 message = (_('Status change %(transition_icon)s %(status)s')
1391 message = (_('Status change %(transition_icon)s %(status)s')
1392 % {'transition_icon': '>',
1392 % {'transition_icon': '>',
1393 'status': ChangesetStatus.get_status_lbl(status)})
1393 'status': ChangesetStatus.get_status_lbl(status)})
1394 text = text or message
1394 text = text or message
1395
1395
1396 comment = CommentsModel().create(
1396 comment = CommentsModel().create(
1397 text=text,
1397 text=text,
1398 repo=self.db_repo.repo_id,
1398 repo=self.db_repo.repo_id,
1399 user=self._rhodecode_user.user_id,
1399 user=self._rhodecode_user.user_id,
1400 pull_request=pull_request,
1400 pull_request=pull_request,
1401 f_path=self.request.POST.get('f_path'),
1401 f_path=self.request.POST.get('f_path'),
1402 line_no=self.request.POST.get('line'),
1402 line_no=self.request.POST.get('line'),
1403 status_change=(ChangesetStatus.get_status_lbl(status)
1403 status_change=(ChangesetStatus.get_status_lbl(status)
1404 if status and allowed_to_change_status else None),
1404 if status and allowed_to_change_status else None),
1405 status_change_type=(status
1405 status_change_type=(status
1406 if status and allowed_to_change_status else None),
1406 if status and allowed_to_change_status else None),
1407 comment_type=comment_type,
1407 comment_type=comment_type,
1408 resolves_comment_id=resolves_comment_id,
1408 resolves_comment_id=resolves_comment_id,
1409 auth_user=self._rhodecode_user
1409 auth_user=self._rhodecode_user
1410 )
1410 )
1411
1411
1412 if allowed_to_change_status:
1412 if allowed_to_change_status:
1413 # calculate old status before we change it
1413 # calculate old status before we change it
1414 old_calculated_status = pull_request.calculated_review_status()
1414 old_calculated_status = pull_request.calculated_review_status()
1415
1415
1416 # get status if set !
1416 # get status if set !
1417 if status:
1417 if status:
1418 ChangesetStatusModel().set_status(
1418 ChangesetStatusModel().set_status(
1419 self.db_repo.repo_id,
1419 self.db_repo.repo_id,
1420 status,
1420 status,
1421 self._rhodecode_user.user_id,
1421 self._rhodecode_user.user_id,
1422 comment,
1422 comment,
1423 pull_request=pull_request
1423 pull_request=pull_request
1424 )
1424 )
1425
1425
1426 Session().flush()
1426 Session().flush()
1427 # this is somehow required to get access to some relationship
1427 # this is somehow required to get access to some relationship
1428 # loaded on comment
1428 # loaded on comment
1429 Session().refresh(comment)
1429 Session().refresh(comment)
1430
1430
1431 PullRequestModel().trigger_pull_request_hook(
1431 PullRequestModel().trigger_pull_request_hook(
1432 pull_request, self._rhodecode_user, 'comment',
1432 pull_request, self._rhodecode_user, 'comment',
1433 data={'comment': comment})
1433 data={'comment': comment})
1434
1434
1435 # we now calculate the status of pull request, and based on that
1435 # we now calculate the status of pull request, and based on that
1436 # calculation we set the commits status
1436 # calculation we set the commits status
1437 calculated_status = pull_request.calculated_review_status()
1437 calculated_status = pull_request.calculated_review_status()
1438 if old_calculated_status != calculated_status:
1438 if old_calculated_status != calculated_status:
1439 PullRequestModel().trigger_pull_request_hook(
1439 PullRequestModel().trigger_pull_request_hook(
1440 pull_request, self._rhodecode_user, 'review_status_change',
1440 pull_request, self._rhodecode_user, 'review_status_change',
1441 data={'status': calculated_status})
1441 data={'status': calculated_status})
1442
1442
1443 Session().commit()
1443 Session().commit()
1444
1444
1445 data = {
1445 data = {
1446 'target_id': h.safeid(h.safe_unicode(
1446 'target_id': h.safeid(h.safe_unicode(
1447 self.request.POST.get('f_path'))),
1447 self.request.POST.get('f_path'))),
1448 }
1448 }
1449 if comment:
1449 if comment:
1450 c.co = comment
1450 c.co = comment
1451 rendered_comment = render(
1451 rendered_comment = render(
1452 'rhodecode:templates/changeset/changeset_comment_block.mako',
1452 'rhodecode:templates/changeset/changeset_comment_block.mako',
1453 self._get_template_context(c), self.request)
1453 self._get_template_context(c), self.request)
1454
1454
1455 data.update(comment.get_dict())
1455 data.update(comment.get_dict())
1456 data.update({'rendered_text': rendered_comment})
1456 data.update({'rendered_text': rendered_comment})
1457
1457
1458 return data
1458 return data
1459
1459
1460 @LoginRequired()
1460 @LoginRequired()
1461 @NotAnonymous()
1461 @NotAnonymous()
1462 @HasRepoPermissionAnyDecorator(
1462 @HasRepoPermissionAnyDecorator(
1463 'repository.read', 'repository.write', 'repository.admin')
1463 'repository.read', 'repository.write', 'repository.admin')
1464 @CSRFRequired()
1464 @CSRFRequired()
1465 @view_config(
1465 @view_config(
1466 route_name='pullrequest_comment_delete', request_method='POST',
1466 route_name='pullrequest_comment_delete', request_method='POST',
1467 renderer='json_ext')
1467 renderer='json_ext')
1468 def pull_request_comment_delete(self):
1468 def pull_request_comment_delete(self):
1469 pull_request = PullRequest.get_or_404(
1469 pull_request = PullRequest.get_or_404(
1470 self.request.matchdict['pull_request_id'])
1470 self.request.matchdict['pull_request_id'])
1471
1471
1472 comment = ChangesetComment.get_or_404(
1472 comment = ChangesetComment.get_or_404(
1473 self.request.matchdict['comment_id'])
1473 self.request.matchdict['comment_id'])
1474 comment_id = comment.comment_id
1474 comment_id = comment.comment_id
1475
1475
1476 if comment.immutable:
1477 # don't allow deleting comments that are immutable
1478 raise HTTPForbidden()
1479
1476 if pull_request.is_closed():
1480 if pull_request.is_closed():
1477 log.debug('comment: forbidden because pull request is closed')
1481 log.debug('comment: forbidden because pull request is closed')
1478 raise HTTPForbidden()
1482 raise HTTPForbidden()
1479
1483
1480 if not comment:
1484 if not comment:
1481 log.debug('Comment with id:%s not found, skipping', comment_id)
1485 log.debug('Comment with id:%s not found, skipping', comment_id)
1482 # comment already deleted in another call probably
1486 # comment already deleted in another call probably
1483 return True
1487 return True
1484
1488
1485 if comment.pull_request.is_closed():
1489 if comment.pull_request.is_closed():
1486 # don't allow deleting comments on closed pull request
1490 # don't allow deleting comments on closed pull request
1487 raise HTTPForbidden()
1491 raise HTTPForbidden()
1488
1492
1489 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1493 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1490 super_admin = h.HasPermissionAny('hg.admin')()
1494 super_admin = h.HasPermissionAny('hg.admin')()
1491 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1495 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1492 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1496 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1493 comment_repo_admin = is_repo_admin and is_repo_comment
1497 comment_repo_admin = is_repo_admin and is_repo_comment
1494
1498
1495 if super_admin or comment_owner or comment_repo_admin:
1499 if super_admin or comment_owner or comment_repo_admin:
1496 old_calculated_status = comment.pull_request.calculated_review_status()
1500 old_calculated_status = comment.pull_request.calculated_review_status()
1497 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1501 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1498 Session().commit()
1502 Session().commit()
1499 calculated_status = comment.pull_request.calculated_review_status()
1503 calculated_status = comment.pull_request.calculated_review_status()
1500 if old_calculated_status != calculated_status:
1504 if old_calculated_status != calculated_status:
1501 PullRequestModel().trigger_pull_request_hook(
1505 PullRequestModel().trigger_pull_request_hook(
1502 comment.pull_request, self._rhodecode_user, 'review_status_change',
1506 comment.pull_request, self._rhodecode_user, 'review_status_change',
1503 data={'status': calculated_status})
1507 data={'status': calculated_status})
1504 return True
1508 return True
1505 else:
1509 else:
1506 log.warning('No permissions for user %s to delete comment_id: %s',
1510 log.warning('No permissions for user %s to delete comment_id: %s',
1507 self._rhodecode_db_user, comment_id)
1511 self._rhodecode_db_user, comment_id)
1508 raise HTTPNotFound()
1512 raise HTTPNotFound()
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,426 +1,426 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
8 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 <% latest_ver = len(getattr(c, 'versions', [])) %>
10 <% latest_ver = len(getattr(c, 'versions', [])) %>
11 % if inline:
11 % if inline:
12 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
13 % else:
13 % else:
14 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
15 % endif
15 % endif
16
16
17 <div class="comment
17 <div class="comment
18 ${'comment-inline' if inline else 'comment-general'}
18 ${'comment-inline' if inline else 'comment-general'}
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
20 id="comment-${comment.comment_id}"
20 id="comment-${comment.comment_id}"
21 line="${comment.line_no}"
21 line="${comment.line_no}"
22 data-comment-id="${comment.comment_id}"
22 data-comment-id="${comment.comment_id}"
23 data-comment-type="${comment.comment_type}"
23 data-comment-type="${comment.comment_type}"
24 data-comment-line-no="${comment.line_no}"
24 data-comment-line-no="${comment.line_no}"
25 data-comment-inline=${h.json.dumps(inline)}
25 data-comment-inline=${h.json.dumps(inline)}
26 style="${'display: none;' if outdated_at_ver else ''}">
26 style="${'display: none;' if outdated_at_ver else ''}">
27
27
28 <div class="meta">
28 <div class="meta">
29 <div class="comment-type-label">
29 <div class="comment-type-label">
30 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
30 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
31 % if comment.comment_type == 'todo':
31 % if comment.comment_type == 'todo':
32 % if comment.resolved:
32 % if comment.resolved:
33 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
33 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
34 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
34 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
35 </div>
35 </div>
36 % else:
36 % else:
37 <div class="resolved tooltip" style="display: none">
37 <div class="resolved tooltip" style="display: none">
38 <span>${comment.comment_type}</span>
38 <span>${comment.comment_type}</span>
39 </div>
39 </div>
40 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
40 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
41 ${comment.comment_type}
41 ${comment.comment_type}
42 </div>
42 </div>
43 % endif
43 % endif
44 % else:
44 % else:
45 % if comment.resolved_comment:
45 % if comment.resolved_comment:
46 fix
46 fix
47 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
47 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
48 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
48 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
49 </a>
49 </a>
50 % else:
50 % else:
51 ${comment.comment_type or 'note'}
51 ${comment.comment_type or 'note'}
52 % endif
52 % endif
53 % endif
53 % endif
54 </div>
54 </div>
55 </div>
55 </div>
56
56
57 <div class="author ${'author-inline' if inline else 'author-general'}">
57 <div class="author ${'author-inline' if inline else 'author-general'}">
58 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
58 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
59 </div>
59 </div>
60 <div class="date">
60 <div class="date">
61 ${h.age_component(comment.modified_at, time_is_local=True)}
61 ${h.age_component(comment.modified_at, time_is_local=True)}
62 </div>
62 </div>
63 % if inline:
63 % if inline:
64 <span></span>
64 <span></span>
65 % else:
65 % else:
66 <div class="status-change">
66 <div class="status-change">
67 % if comment.pull_request:
67 % if comment.pull_request:
68 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
68 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
69 % if comment.status_change:
69 % if comment.status_change:
70 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}:
70 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}:
71 % else:
71 % else:
72 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}
72 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}
73 % endif
73 % endif
74 </a>
74 </a>
75 % else:
75 % else:
76 % if comment.status_change:
76 % if comment.status_change:
77 ${_('Status change on commit')}:
77 ${_('Status change on commit')}:
78 % endif
78 % endif
79 % endif
79 % endif
80 </div>
80 </div>
81 % endif
81 % endif
82
82
83 % if comment.status_change:
83 % if comment.status_change:
84 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
84 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
85 <div title="${_('Commit status')}" class="changeset-status-lbl">
85 <div title="${_('Commit status')}" class="changeset-status-lbl">
86 ${comment.status_change[0].status_lbl}
86 ${comment.status_change[0].status_lbl}
87 </div>
87 </div>
88 % endif
88 % endif
89
89
90 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
90 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
91
91
92 <div class="comment-links-block">
92 <div class="comment-links-block">
93 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
93 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
94 <span class="tag authortag tooltip" title="${_('Pull request author')}">
94 <span class="tag authortag tooltip" title="${_('Pull request author')}">
95 ${_('author')}
95 ${_('author')}
96 </span>
96 </span>
97 |
97 |
98 % endif
98 % endif
99 % if inline:
99 % if inline:
100 <div class="pr-version-inline">
100 <div class="pr-version-inline">
101 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
101 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
102 % if outdated_at_ver:
102 % if outdated_at_ver:
103 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
103 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
104 outdated ${'v{}'.format(pr_index_ver)} |
104 outdated ${'v{}'.format(pr_index_ver)} |
105 </code>
105 </code>
106 % elif pr_index_ver:
106 % elif pr_index_ver:
107 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
107 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
108 ${'v{}'.format(pr_index_ver)} |
108 ${'v{}'.format(pr_index_ver)} |
109 </code>
109 </code>
110 % endif
110 % endif
111 </a>
111 </a>
112 </div>
112 </div>
113 % else:
113 % else:
114 % if comment.pull_request_version_id and pr_index_ver:
114 % if comment.pull_request_version_id and pr_index_ver:
115 |
115 |
116 <div class="pr-version">
116 <div class="pr-version">
117 % if comment.outdated:
117 % if comment.outdated:
118 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
118 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
119 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
119 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
120 </a>
120 </a>
121 % else:
121 % else:
122 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
122 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
123 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
123 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
124 <code class="pr-version-num">
124 <code class="pr-version-num">
125 ${'v{}'.format(pr_index_ver)}
125 ${'v{}'.format(pr_index_ver)}
126 </code>
126 </code>
127 </a>
127 </a>
128 </div>
128 </div>
129 % endif
129 % endif
130 </div>
130 </div>
131 % endif
131 % endif
132 % endif
132 % endif
133
133
134 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
134 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
135 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
135 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
136 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
136 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
137 ## permissions to delete
137 ## permissions to delete
138 %if c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
138 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
139 ## TODO: dan: add edit comment here
139 ## TODO: dan: add edit comment here
140 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
140 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
141 %else:
141 %else:
142 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
142 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
143 %endif
143 %endif
144 %else:
144 %else:
145 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
145 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
146 %endif
146 %endif
147
147
148 % if outdated_at_ver:
148 % if outdated_at_ver:
149 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
149 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
150 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
150 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
151 % else:
151 % else:
152 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
152 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
153 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
153 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
154 % endif
154 % endif
155
155
156 </div>
156 </div>
157 </div>
157 </div>
158 <div class="text">
158 <div class="text">
159 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
159 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
160 </div>
160 </div>
161
161
162 </div>
162 </div>
163 </%def>
163 </%def>
164
164
165 ## generate main comments
165 ## generate main comments
166 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
166 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
167 <%
167 <%
168 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
168 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
169 %>
169 %>
170
170
171 <div class="general-comments" id="comments">
171 <div class="general-comments" id="comments">
172 %for comment in comments:
172 %for comment in comments:
173 <div id="comment-tr-${comment.comment_id}">
173 <div id="comment-tr-${comment.comment_id}">
174 ## only render comments that are not from pull request, or from
174 ## only render comments that are not from pull request, or from
175 ## pull request and a status change
175 ## pull request and a status change
176 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
176 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
177 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
177 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
178 %endif
178 %endif
179 </div>
179 </div>
180 %endfor
180 %endfor
181 ## to anchor ajax comments
181 ## to anchor ajax comments
182 <div id="injected_page_comments"></div>
182 <div id="injected_page_comments"></div>
183 </div>
183 </div>
184 </%def>
184 </%def>
185
185
186
186
187 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
187 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
188
188
189 <div class="comments">
189 <div class="comments">
190 <%
190 <%
191 if is_pull_request:
191 if is_pull_request:
192 placeholder = _('Leave a comment on this Pull Request.')
192 placeholder = _('Leave a comment on this Pull Request.')
193 elif is_compare:
193 elif is_compare:
194 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
194 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
195 else:
195 else:
196 placeholder = _('Leave a comment on this Commit.')
196 placeholder = _('Leave a comment on this Commit.')
197 %>
197 %>
198
198
199 % if c.rhodecode_user.username != h.DEFAULT_USER:
199 % if c.rhodecode_user.username != h.DEFAULT_USER:
200 <div class="js-template" id="cb-comment-general-form-template">
200 <div class="js-template" id="cb-comment-general-form-template">
201 ## template generated for injection
201 ## template generated for injection
202 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
203 </div>
203 </div>
204
204
205 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
205 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
206 ## inject form here
206 ## inject form here
207 </div>
207 </div>
208 <script type="text/javascript">
208 <script type="text/javascript">
209 var lineNo = 'general';
209 var lineNo = 'general';
210 var resolvesCommentId = null;
210 var resolvesCommentId = null;
211 var generalCommentForm = Rhodecode.comments.createGeneralComment(
211 var generalCommentForm = Rhodecode.comments.createGeneralComment(
212 lineNo, "${placeholder}", resolvesCommentId);
212 lineNo, "${placeholder}", resolvesCommentId);
213
213
214 // set custom success callback on rangeCommit
214 // set custom success callback on rangeCommit
215 % if is_compare:
215 % if is_compare:
216 generalCommentForm.setHandleFormSubmit(function(o) {
216 generalCommentForm.setHandleFormSubmit(function(o) {
217 var self = generalCommentForm;
217 var self = generalCommentForm;
218
218
219 var text = self.cm.getValue();
219 var text = self.cm.getValue();
220 var status = self.getCommentStatus();
220 var status = self.getCommentStatus();
221 var commentType = self.getCommentType();
221 var commentType = self.getCommentType();
222
222
223 if (text === "" && !status) {
223 if (text === "" && !status) {
224 return;
224 return;
225 }
225 }
226
226
227 // we can pick which commits we want to make the comment by
227 // we can pick which commits we want to make the comment by
228 // selecting them via click on preview pane, this will alter the hidden inputs
228 // selecting them via click on preview pane, this will alter the hidden inputs
229 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
229 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
230
230
231 var commitIds = [];
231 var commitIds = [];
232 $('#changeset_compare_view_content .compare_select').each(function(el) {
232 $('#changeset_compare_view_content .compare_select').each(function(el) {
233 var commitId = this.id.replace('row-', '');
233 var commitId = this.id.replace('row-', '');
234 if ($(this).hasClass('hl') || !cherryPicked) {
234 if ($(this).hasClass('hl') || !cherryPicked) {
235 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
235 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
236 commitIds.push(commitId);
236 commitIds.push(commitId);
237 } else {
237 } else {
238 $("input[data-commit-id='{0}']".format(commitId)).val('')
238 $("input[data-commit-id='{0}']".format(commitId)).val('')
239 }
239 }
240 });
240 });
241
241
242 self.setActionButtonsDisabled(true);
242 self.setActionButtonsDisabled(true);
243 self.cm.setOption("readOnly", true);
243 self.cm.setOption("readOnly", true);
244 var postData = {
244 var postData = {
245 'text': text,
245 'text': text,
246 'changeset_status': status,
246 'changeset_status': status,
247 'comment_type': commentType,
247 'comment_type': commentType,
248 'commit_ids': commitIds,
248 'commit_ids': commitIds,
249 'csrf_token': CSRF_TOKEN
249 'csrf_token': CSRF_TOKEN
250 };
250 };
251
251
252 var submitSuccessCallback = function(o) {
252 var submitSuccessCallback = function(o) {
253 location.reload(true);
253 location.reload(true);
254 };
254 };
255 var submitFailCallback = function(){
255 var submitFailCallback = function(){
256 self.resetCommentFormState(text)
256 self.resetCommentFormState(text)
257 };
257 };
258 self.submitAjaxPOST(
258 self.submitAjaxPOST(
259 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
259 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
260 });
260 });
261 % endif
261 % endif
262
262
263 </script>
263 </script>
264 % else:
264 % else:
265 ## form state when not logged in
265 ## form state when not logged in
266 <div class="comment-form ac">
266 <div class="comment-form ac">
267
267
268 <div class="comment-area">
268 <div class="comment-area">
269 <div class="comment-area-header">
269 <div class="comment-area-header">
270 <ul class="nav-links clearfix">
270 <ul class="nav-links clearfix">
271 <li class="active">
271 <li class="active">
272 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
272 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
273 </li>
273 </li>
274 <li class="">
274 <li class="">
275 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
275 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
276 </li>
276 </li>
277 </ul>
277 </ul>
278 </div>
278 </div>
279
279
280 <div class="comment-area-write" style="display: block;">
280 <div class="comment-area-write" style="display: block;">
281 <div id="edit-container">
281 <div id="edit-container">
282 <div style="padding: 40px 0">
282 <div style="padding: 40px 0">
283 ${_('You need to be logged in to leave comments.')}
283 ${_('You need to be logged in to leave comments.')}
284 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
284 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
285 </div>
285 </div>
286 </div>
286 </div>
287 <div id="preview-container" class="clearfix" style="display: none;">
287 <div id="preview-container" class="clearfix" style="display: none;">
288 <div id="preview-box" class="preview-box"></div>
288 <div id="preview-box" class="preview-box"></div>
289 </div>
289 </div>
290 </div>
290 </div>
291
291
292 <div class="comment-area-footer">
292 <div class="comment-area-footer">
293 <div class="toolbar">
293 <div class="toolbar">
294 <div class="toolbar-text">
294 <div class="toolbar-text">
295 </div>
295 </div>
296 </div>
296 </div>
297 </div>
297 </div>
298 </div>
298 </div>
299
299
300 <div class="comment-footer">
300 <div class="comment-footer">
301 </div>
301 </div>
302
302
303 </div>
303 </div>
304 % endif
304 % endif
305
305
306 <script type="text/javascript">
306 <script type="text/javascript">
307 bindToggleButtons();
307 bindToggleButtons();
308 </script>
308 </script>
309 </div>
309 </div>
310 </%def>
310 </%def>
311
311
312
312
313 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
313 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
314
314
315 ## comment injected based on assumption that user is logged in
315 ## comment injected based on assumption that user is logged in
316 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
316 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
317
317
318 <div class="comment-area">
318 <div class="comment-area">
319 <div class="comment-area-header">
319 <div class="comment-area-header">
320 <div class="pull-left">
320 <div class="pull-left">
321 <ul class="nav-links clearfix">
321 <ul class="nav-links clearfix">
322 <li class="active">
322 <li class="active">
323 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
323 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
324 </li>
324 </li>
325 <li class="">
325 <li class="">
326 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
326 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
327 </li>
327 </li>
328 </ul>
328 </ul>
329 </div>
329 </div>
330 <div class="pull-right">
330 <div class="pull-right">
331 <span class="comment-area-text">${_('Mark as')}:</span>
331 <span class="comment-area-text">${_('Mark as')}:</span>
332 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
332 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
333 % for val in c.visual.comment_types:
333 % for val in c.visual.comment_types:
334 <option value="${val}">${val.upper()}</option>
334 <option value="${val}">${val.upper()}</option>
335 % endfor
335 % endfor
336 </select>
336 </select>
337 </div>
337 </div>
338 </div>
338 </div>
339
339
340 <div class="comment-area-write" style="display: block;">
340 <div class="comment-area-write" style="display: block;">
341 <div id="edit-container_${lineno_id}">
341 <div id="edit-container_${lineno_id}">
342 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
342 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
343 </div>
343 </div>
344 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
344 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
345 <div id="preview-box_${lineno_id}" class="preview-box"></div>
345 <div id="preview-box_${lineno_id}" class="preview-box"></div>
346 </div>
346 </div>
347 </div>
347 </div>
348
348
349 <div class="comment-area-footer comment-attachment-uploader">
349 <div class="comment-area-footer comment-attachment-uploader">
350 <div class="toolbar">
350 <div class="toolbar">
351
351
352 <div class="comment-attachment-text">
352 <div class="comment-attachment-text">
353 <div class="dropzone-text">
353 <div class="dropzone-text">
354 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
354 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
355 </div>
355 </div>
356 <div class="dropzone-upload" style="display:none">
356 <div class="dropzone-upload" style="display:none">
357 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
357 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
358 </div>
358 </div>
359 </div>
359 </div>
360
360
361 ## comments dropzone template, empty on purpose
361 ## comments dropzone template, empty on purpose
362 <div style="display: none" class="comment-attachment-uploader-template">
362 <div style="display: none" class="comment-attachment-uploader-template">
363 <div class="dz-file-preview" style="margin: 0">
363 <div class="dz-file-preview" style="margin: 0">
364 <div class="dz-error-message"></div>
364 <div class="dz-error-message"></div>
365 </div>
365 </div>
366 </div>
366 </div>
367
367
368 </div>
368 </div>
369 </div>
369 </div>
370 </div>
370 </div>
371
371
372 <div class="comment-footer">
372 <div class="comment-footer">
373
373
374 ## inject extra inputs into the form
374 ## inject extra inputs into the form
375 % if form_extras and isinstance(form_extras, (list, tuple)):
375 % if form_extras and isinstance(form_extras, (list, tuple)):
376 <div id="comment_form_extras">
376 <div id="comment_form_extras">
377 % for form_ex_el in form_extras:
377 % for form_ex_el in form_extras:
378 ${form_ex_el|n}
378 ${form_ex_el|n}
379 % endfor
379 % endfor
380 </div>
380 </div>
381 % endif
381 % endif
382
382
383 <div class="action-buttons">
383 <div class="action-buttons">
384 % if form_type != 'inline':
384 % if form_type != 'inline':
385 <div class="action-buttons-extra"></div>
385 <div class="action-buttons-extra"></div>
386 % endif
386 % endif
387
387
388 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
388 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
389
389
390 ## inline for has a file, and line-number together with cancel hide button.
390 ## inline for has a file, and line-number together with cancel hide button.
391 % if form_type == 'inline':
391 % if form_type == 'inline':
392 <input type="hidden" name="f_path" value="{0}">
392 <input type="hidden" name="f_path" value="{0}">
393 <input type="hidden" name="line" value="${lineno_id}">
393 <input type="hidden" name="line" value="${lineno_id}">
394 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
394 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
395 ${_('Cancel')}
395 ${_('Cancel')}
396 </button>
396 </button>
397 % endif
397 % endif
398 </div>
398 </div>
399
399
400 % if review_statuses:
400 % if review_statuses:
401 <div class="status_box">
401 <div class="status_box">
402 <select id="change_status_${lineno_id}" name="changeset_status">
402 <select id="change_status_${lineno_id}" name="changeset_status">
403 <option></option> ## Placeholder
403 <option></option> ## Placeholder
404 % for status, lbl in review_statuses:
404 % for status, lbl in review_statuses:
405 <option value="${status}" data-status="${status}">${lbl}</option>
405 <option value="${status}" data-status="${status}">${lbl}</option>
406 %if is_pull_request and change_status and status in ('approved', 'rejected'):
406 %if is_pull_request and change_status and status in ('approved', 'rejected'):
407 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
407 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
408 %endif
408 %endif
409 % endfor
409 % endfor
410 </select>
410 </select>
411 </div>
411 </div>
412 % endif
412 % endif
413
413
414 <div class="toolbar-text">
414 <div class="toolbar-text">
415 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
415 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
416 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
416 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
417 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
417 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
418 ${_('and')}
418 ${_('and')}
419 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
419 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
420 ${_('actions supported.')}
420 ${_('actions supported.')}
421 </div>
421 </div>
422 </div>
422 </div>
423
423
424 </form>
424 </form>
425
425
426 </%def> No newline at end of file
426 </%def>
General Comments 0
You need to be logged in to leave comments. Login now