##// END OF EJS Templates
pull-requests: added awaiting my review filter for users pull-requests....
super-admin -
r4690:2e951f8d stable
parent child Browse files
Show More

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

@@ -1,204 +1,208 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 # -*- coding: utf-8 -*-
20 # -*- coding: utf-8 -*-
21
21
22 # Copyright (C) 2016-2020 RhodeCode GmbH
22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 #
23 #
24 # This program is free software: you can redistribute it and/or modify
24 # This program is free software: you can redistribute it and/or modify
25 # it under the terms of the GNU Affero General Public License, version 3
25 # it under the terms of the GNU Affero General Public License, version 3
26 # (only), as published by the Free Software Foundation.
26 # (only), as published by the Free Software Foundation.
27 #
27 #
28 # This program is distributed in the hope that it will be useful,
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
31 # GNU General Public License for more details.
32 #
32 #
33 # You should have received a copy of the GNU Affero General Public License
33 # You should have received a copy of the GNU Affero General Public License
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 #
35 #
36 # This program is dual-licensed. If you wish to learn more about the
36 # This program is dual-licensed. If you wish to learn more about the
37 # RhodeCode Enterprise Edition, including its added features, Support services,
37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39
39
40 import pytest
40 import pytest
41
41
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.tests import TestController, assert_session_flash
43 from rhodecode.tests import TestController, assert_session_flash
44 from rhodecode.lib import helpers as h
44 from rhodecode.lib import helpers as h
45
45
46
46
47 def route_path(name, params=None, **kwargs):
47 def route_path(name, params=None, **kwargs):
48 import urllib
48 import urllib
49 from rhodecode.apps._base import ADMIN_PREFIX
49 from rhodecode.apps._base import ADMIN_PREFIX
50
50
51 base_url = {
51 base_url = {
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 }[name].format(**kwargs)
56 }[name].format(**kwargs)
57
57
58 if params:
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 return base_url
60 return base_url
61
61
62
62
63 class TestMyAccountEdit(TestController):
63 class TestMyAccountEdit(TestController):
64
64
65 def test_my_account_edit(self):
65 def test_my_account_edit(self):
66 self.log_user()
66 self.log_user()
67 response = self.app.get(route_path('my_account_edit'))
67 response = self.app.get(route_path('my_account_edit'))
68
68
69 response.mustcontain('value="test_admin')
69 response.mustcontain('value="test_admin')
70
70
71 @pytest.mark.backends("git", "hg")
71 @pytest.mark.backends("git", "hg")
72 def test_my_account_my_pullrequests(self, pr_util):
72 def test_my_account_my_pullrequests(self, pr_util):
73 self.log_user()
73 self.log_user()
74 response = self.app.get(route_path('my_account_pullrequests'))
74 response = self.app.get(route_path('my_account_pullrequests'))
75 response.mustcontain('There are currently no open pull '
75 response.mustcontain('There are currently no open pull '
76 'requests requiring your participation.')
76 'requests requiring your participation.')
77
77
78 @pytest.mark.backends("git", "hg")
78 @pytest.mark.backends("git", "hg")
79 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header):
79 @pytest.mark.parametrize('params, expected_title', [
80 ({'closed': 1}, 'Closed'),
81 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 ])
83 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
80 self.log_user()
84 self.log_user()
81 response = self.app.get(route_path('my_account_pullrequests_data'),
85 response = self.app.get(route_path('my_account_pullrequests_data'),
82 extra_environ=xhr_header)
86 extra_environ=xhr_header)
83 assert response.json == {
87 assert response.json == {
84 u'data': [], u'draw': None,
88 u'data': [], u'draw': None,
85 u'recordsFiltered': 0, u'recordsTotal': 0}
89 u'recordsFiltered': 0, u'recordsTotal': 0}
86
90
87 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
88 expected = {
92 expected = {
89 'author_raw': 'RhodeCode Admin',
93 'author_raw': 'RhodeCode Admin',
90 'name_raw': pr.pull_request_id
94 'name_raw': pr.pull_request_id
91 }
95 }
92 response = self.app.get(route_path('my_account_pullrequests_data'),
96 response = self.app.get(route_path('my_account_pullrequests_data'),
93 extra_environ=xhr_header)
97 extra_environ=xhr_header)
94 assert response.json['recordsTotal'] == 1
98 assert response.json['recordsTotal'] == 1
95 assert response.json['data'][0]['author_raw'] == expected['author_raw']
99 assert response.json['data'][0]['author_raw'] == expected['author_raw']
96
100
97 assert response.json['data'][0]['author_raw'] == expected['author_raw']
101 assert response.json['data'][0]['author_raw'] == expected['author_raw']
98 assert response.json['data'][0]['name_raw'] == expected['name_raw']
102 assert response.json['data'][0]['name_raw'] == expected['name_raw']
99
103
100 @pytest.mark.parametrize(
104 @pytest.mark.parametrize(
101 "name, attrs", [
105 "name, attrs", [
102 ('firstname', {'firstname': 'new_username'}),
106 ('firstname', {'firstname': 'new_username'}),
103 ('lastname', {'lastname': 'new_username'}),
107 ('lastname', {'lastname': 'new_username'}),
104 ('admin', {'admin': True}),
108 ('admin', {'admin': True}),
105 ('admin', {'admin': False}),
109 ('admin', {'admin': False}),
106 ('extern_type', {'extern_type': 'ldap'}),
110 ('extern_type', {'extern_type': 'ldap'}),
107 ('extern_type', {'extern_type': None}),
111 ('extern_type', {'extern_type': None}),
108 # ('extern_name', {'extern_name': 'test'}),
112 # ('extern_name', {'extern_name': 'test'}),
109 # ('extern_name', {'extern_name': None}),
113 # ('extern_name', {'extern_name': None}),
110 ('active', {'active': False}),
114 ('active', {'active': False}),
111 ('active', {'active': True}),
115 ('active', {'active': True}),
112 ('email', {'email': u'some@email.com'}),
116 ('email', {'email': u'some@email.com'}),
113 ])
117 ])
114 def test_my_account_update(self, name, attrs, user_util):
118 def test_my_account_update(self, name, attrs, user_util):
115 usr = user_util.create_user(password='qweqwe')
119 usr = user_util.create_user(password='qweqwe')
116 params = usr.get_api_data() # current user data
120 params = usr.get_api_data() # current user data
117 user_id = usr.user_id
121 user_id = usr.user_id
118 self.log_user(
122 self.log_user(
119 username=usr.username, password='qweqwe')
123 username=usr.username, password='qweqwe')
120
124
121 params.update({'password_confirmation': ''})
125 params.update({'password_confirmation': ''})
122 params.update({'new_password': ''})
126 params.update({'new_password': ''})
123 params.update({'extern_type': u'rhodecode'})
127 params.update({'extern_type': u'rhodecode'})
124 params.update({'extern_name': u'rhodecode'})
128 params.update({'extern_name': u'rhodecode'})
125 params.update({'csrf_token': self.csrf_token})
129 params.update({'csrf_token': self.csrf_token})
126
130
127 params.update(attrs)
131 params.update(attrs)
128 # my account page cannot set language param yet, only for admins
132 # my account page cannot set language param yet, only for admins
129 del params['language']
133 del params['language']
130 if name == 'email':
134 if name == 'email':
131 uem = user_util.create_additional_user_email(usr, attrs['email'])
135 uem = user_util.create_additional_user_email(usr, attrs['email'])
132 email_before = User.get(user_id).email
136 email_before = User.get(user_id).email
133
137
134 response = self.app.post(route_path('my_account_update'), params)
138 response = self.app.post(route_path('my_account_update'), params)
135
139
136 assert_session_flash(
140 assert_session_flash(
137 response, 'Your account was updated successfully')
141 response, 'Your account was updated successfully')
138
142
139 del params['csrf_token']
143 del params['csrf_token']
140
144
141 updated_user = User.get(user_id)
145 updated_user = User.get(user_id)
142 updated_params = updated_user.get_api_data()
146 updated_params = updated_user.get_api_data()
143 updated_params.update({'password_confirmation': ''})
147 updated_params.update({'password_confirmation': ''})
144 updated_params.update({'new_password': ''})
148 updated_params.update({'new_password': ''})
145
149
146 params['last_login'] = updated_params['last_login']
150 params['last_login'] = updated_params['last_login']
147 params['last_activity'] = updated_params['last_activity']
151 params['last_activity'] = updated_params['last_activity']
148 # my account page cannot set language param yet, only for admins
152 # my account page cannot set language param yet, only for admins
149 # but we get this info from API anyway
153 # but we get this info from API anyway
150 params['language'] = updated_params['language']
154 params['language'] = updated_params['language']
151
155
152 if name == 'email':
156 if name == 'email':
153 params['emails'] = [attrs['email'], email_before]
157 params['emails'] = [attrs['email'], email_before]
154 if name == 'extern_type':
158 if name == 'extern_type':
155 # cannot update this via form, expected value is original one
159 # cannot update this via form, expected value is original one
156 params['extern_type'] = "rhodecode"
160 params['extern_type'] = "rhodecode"
157 if name == 'extern_name':
161 if name == 'extern_name':
158 # cannot update this via form, expected value is original one
162 # cannot update this via form, expected value is original one
159 params['extern_name'] = str(user_id)
163 params['extern_name'] = str(user_id)
160 if name == 'active':
164 if name == 'active':
161 # my account cannot deactivate account
165 # my account cannot deactivate account
162 params['active'] = True
166 params['active'] = True
163 if name == 'admin':
167 if name == 'admin':
164 # my account cannot make you an admin !
168 # my account cannot make you an admin !
165 params['admin'] = False
169 params['admin'] = False
166
170
167 assert params == updated_params
171 assert params == updated_params
168
172
169 def test_my_account_update_err_email_not_exists_in_emails(self):
173 def test_my_account_update_err_email_not_exists_in_emails(self):
170 self.log_user()
174 self.log_user()
171
175
172 new_email = 'test_regular@mail.com' # not in emails
176 new_email = 'test_regular@mail.com' # not in emails
173 params = {
177 params = {
174 'username': 'test_admin',
178 'username': 'test_admin',
175 'new_password': 'test12',
179 'new_password': 'test12',
176 'password_confirmation': 'test122',
180 'password_confirmation': 'test122',
177 'firstname': 'NewName',
181 'firstname': 'NewName',
178 'lastname': 'NewLastname',
182 'lastname': 'NewLastname',
179 'email': new_email,
183 'email': new_email,
180 'csrf_token': self.csrf_token,
184 'csrf_token': self.csrf_token,
181 }
185 }
182
186
183 response = self.app.post(route_path('my_account_update'),
187 response = self.app.post(route_path('my_account_update'),
184 params=params)
188 params=params)
185
189
186 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
190 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
187
191
188 def test_my_account_update_bad_email_address(self):
192 def test_my_account_update_bad_email_address(self):
189 self.log_user('test_regular2', 'test12')
193 self.log_user('test_regular2', 'test12')
190
194
191 new_email = 'newmail.pl'
195 new_email = 'newmail.pl'
192 params = {
196 params = {
193 'username': 'test_admin',
197 'username': 'test_admin',
194 'new_password': 'test12',
198 'new_password': 'test12',
195 'password_confirmation': 'test122',
199 'password_confirmation': 'test122',
196 'firstname': 'NewName',
200 'firstname': 'NewName',
197 'lastname': 'NewLastname',
201 'lastname': 'NewLastname',
198 'email': new_email,
202 'email': new_email,
199 'csrf_token': self.csrf_token,
203 'csrf_token': self.csrf_token,
200 }
204 }
201 response = self.app.post(route_path('my_account_update'),
205 response = self.app.post(route_path('my_account_update'),
202 params=params)
206 params=params)
203
207
204 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
208 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,752 +1,783 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 logging
21 import logging
22 import datetime
22 import datetime
23 import string
23 import string
24
24
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import peppercorn
27 import peppercorn
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29
29
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode import forms
31 from rhodecode import forms
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import audit_logger
33 from rhodecode.lib import audit_logger
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, NotAnonymous, CSRFRequired,
36 LoginRequired, NotAnonymous, CSRFRequired,
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 from rhodecode.lib.channelstream import (
38 from rhodecode.lib.channelstream import (
39 channelstream_request, ChannelstreamException)
39 channelstream_request, ChannelstreamException)
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 IntegrityError, or_, in_filter_generator,
44 IntegrityError, or_, in_filter_generator,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup)
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.user_group import UserGroupModel
50 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.validation_schema.schemas import user_schema
51 from rhodecode.model.validation_schema.schemas import user_schema
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class MyAccountView(BaseAppView, DataGridAppView):
56 class MyAccountView(BaseAppView, DataGridAppView):
57 ALLOW_SCOPED_TOKENS = False
57 ALLOW_SCOPED_TOKENS = False
58 """
58 """
59 This view has alternative version inside EE, if modified please take a look
59 This view has alternative version inside EE, if modified please take a look
60 in there as well.
60 in there as well.
61 """
61 """
62
62
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context()
64 c = self._get_local_tmpl_context()
65 c.user = c.auth_user.get_instance()
65 c.user = c.auth_user.get_instance()
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 return c
67 return c
68
68
69 @LoginRequired()
69 @LoginRequired()
70 @NotAnonymous()
70 @NotAnonymous()
71 def my_account_profile(self):
71 def my_account_profile(self):
72 c = self.load_default_context()
72 c = self.load_default_context()
73 c.active = 'profile'
73 c.active = 'profile'
74 c.extern_type = c.user.extern_type
74 c.extern_type = c.user.extern_type
75 return self._get_template_context(c)
75 return self._get_template_context(c)
76
76
77 @LoginRequired()
77 @LoginRequired()
78 @NotAnonymous()
78 @NotAnonymous()
79 def my_account_edit(self):
79 def my_account_edit(self):
80 c = self.load_default_context()
80 c = self.load_default_context()
81 c.active = 'profile_edit'
81 c.active = 'profile_edit'
82 c.extern_type = c.user.extern_type
82 c.extern_type = c.user.extern_type
83 c.extern_name = c.user.extern_name
83 c.extern_name = c.user.extern_name
84
84
85 schema = user_schema.UserProfileSchema().bind(
85 schema = user_schema.UserProfileSchema().bind(
86 username=c.user.username, user_emails=c.user.emails)
86 username=c.user.username, user_emails=c.user.emails)
87 appstruct = {
87 appstruct = {
88 'username': c.user.username,
88 'username': c.user.username,
89 'email': c.user.email,
89 'email': c.user.email,
90 'firstname': c.user.firstname,
90 'firstname': c.user.firstname,
91 'lastname': c.user.lastname,
91 'lastname': c.user.lastname,
92 'description': c.user.description,
92 'description': c.user.description,
93 }
93 }
94 c.form = forms.RcForm(
94 c.form = forms.RcForm(
95 schema, appstruct=appstruct,
95 schema, appstruct=appstruct,
96 action=h.route_path('my_account_update'),
96 action=h.route_path('my_account_update'),
97 buttons=(forms.buttons.save, forms.buttons.reset))
97 buttons=(forms.buttons.save, forms.buttons.reset))
98
98
99 return self._get_template_context(c)
99 return self._get_template_context(c)
100
100
101 @LoginRequired()
101 @LoginRequired()
102 @NotAnonymous()
102 @NotAnonymous()
103 @CSRFRequired()
103 @CSRFRequired()
104 def my_account_update(self):
104 def my_account_update(self):
105 _ = self.request.translate
105 _ = self.request.translate
106 c = self.load_default_context()
106 c = self.load_default_context()
107 c.active = 'profile_edit'
107 c.active = 'profile_edit'
108 c.perm_user = c.auth_user
108 c.perm_user = c.auth_user
109 c.extern_type = c.user.extern_type
109 c.extern_type = c.user.extern_type
110 c.extern_name = c.user.extern_name
110 c.extern_name = c.user.extern_name
111
111
112 schema = user_schema.UserProfileSchema().bind(
112 schema = user_schema.UserProfileSchema().bind(
113 username=c.user.username, user_emails=c.user.emails)
113 username=c.user.username, user_emails=c.user.emails)
114 form = forms.RcForm(
114 form = forms.RcForm(
115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116
116
117 controls = self.request.POST.items()
117 controls = self.request.POST.items()
118 try:
118 try:
119 valid_data = form.validate(controls)
119 valid_data = form.validate(controls)
120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 'new_password', 'password_confirmation']
121 'new_password', 'password_confirmation']
122 if c.extern_type != "rhodecode":
122 if c.extern_type != "rhodecode":
123 # forbid updating username for external accounts
123 # forbid updating username for external accounts
124 skip_attrs.append('username')
124 skip_attrs.append('username')
125 old_email = c.user.email
125 old_email = c.user.email
126 UserModel().update_user(
126 UserModel().update_user(
127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 **valid_data)
128 **valid_data)
129 if old_email != valid_data['email']:
129 if old_email != valid_data['email']:
130 old = UserEmailMap.query() \
130 old = UserEmailMap.query() \
131 .filter(UserEmailMap.user == c.user)\
131 .filter(UserEmailMap.user == c.user)\
132 .filter(UserEmailMap.email == valid_data['email'])\
132 .filter(UserEmailMap.email == valid_data['email'])\
133 .first()
133 .first()
134 old.email = old_email
134 old.email = old_email
135 h.flash(_('Your account was updated successfully'), category='success')
135 h.flash(_('Your account was updated successfully'), category='success')
136 Session().commit()
136 Session().commit()
137 except forms.ValidationFailure as e:
137 except forms.ValidationFailure as e:
138 c.form = e
138 c.form = e
139 return self._get_template_context(c)
139 return self._get_template_context(c)
140 except Exception:
140 except Exception:
141 log.exception("Exception updating user")
141 log.exception("Exception updating user")
142 h.flash(_('Error occurred during update of user'),
142 h.flash(_('Error occurred during update of user'),
143 category='error')
143 category='error')
144 raise HTTPFound(h.route_path('my_account_profile'))
144 raise HTTPFound(h.route_path('my_account_profile'))
145
145
146 @LoginRequired()
146 @LoginRequired()
147 @NotAnonymous()
147 @NotAnonymous()
148 def my_account_password(self):
148 def my_account_password(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150 c.active = 'password'
150 c.active = 'password'
151 c.extern_type = c.user.extern_type
151 c.extern_type = c.user.extern_type
152
152
153 schema = user_schema.ChangePasswordSchema().bind(
153 schema = user_schema.ChangePasswordSchema().bind(
154 username=c.user.username)
154 username=c.user.username)
155
155
156 form = forms.Form(
156 form = forms.Form(
157 schema,
157 schema,
158 action=h.route_path('my_account_password_update'),
158 action=h.route_path('my_account_password_update'),
159 buttons=(forms.buttons.save, forms.buttons.reset))
159 buttons=(forms.buttons.save, forms.buttons.reset))
160
160
161 c.form = form
161 c.form = form
162 return self._get_template_context(c)
162 return self._get_template_context(c)
163
163
164 @LoginRequired()
164 @LoginRequired()
165 @NotAnonymous()
165 @NotAnonymous()
166 @CSRFRequired()
166 @CSRFRequired()
167 def my_account_password_update(self):
167 def my_account_password_update(self):
168 _ = self.request.translate
168 _ = self.request.translate
169 c = self.load_default_context()
169 c = self.load_default_context()
170 c.active = 'password'
170 c.active = 'password'
171 c.extern_type = c.user.extern_type
171 c.extern_type = c.user.extern_type
172
172
173 schema = user_schema.ChangePasswordSchema().bind(
173 schema = user_schema.ChangePasswordSchema().bind(
174 username=c.user.username)
174 username=c.user.username)
175
175
176 form = forms.Form(
176 form = forms.Form(
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178
178
179 if c.extern_type != 'rhodecode':
179 if c.extern_type != 'rhodecode':
180 raise HTTPFound(self.request.route_path('my_account_password'))
180 raise HTTPFound(self.request.route_path('my_account_password'))
181
181
182 controls = self.request.POST.items()
182 controls = self.request.POST.items()
183 try:
183 try:
184 valid_data = form.validate(controls)
184 valid_data = form.validate(controls)
185 UserModel().update_user(c.user.user_id, **valid_data)
185 UserModel().update_user(c.user.user_id, **valid_data)
186 c.user.update_userdata(force_password_change=False)
186 c.user.update_userdata(force_password_change=False)
187 Session().commit()
187 Session().commit()
188 except forms.ValidationFailure as e:
188 except forms.ValidationFailure as e:
189 c.form = e
189 c.form = e
190 return self._get_template_context(c)
190 return self._get_template_context(c)
191
191
192 except Exception:
192 except Exception:
193 log.exception("Exception updating password")
193 log.exception("Exception updating password")
194 h.flash(_('Error occurred during update of user password'),
194 h.flash(_('Error occurred during update of user password'),
195 category='error')
195 category='error')
196 else:
196 else:
197 instance = c.auth_user.get_instance()
197 instance = c.auth_user.get_instance()
198 self.session.setdefault('rhodecode_user', {}).update(
198 self.session.setdefault('rhodecode_user', {}).update(
199 {'password': md5(instance.password)})
199 {'password': md5(instance.password)})
200 self.session.save()
200 self.session.save()
201 h.flash(_("Successfully updated password"), category='success')
201 h.flash(_("Successfully updated password"), category='success')
202
202
203 raise HTTPFound(self.request.route_path('my_account_password'))
203 raise HTTPFound(self.request.route_path('my_account_password'))
204
204
205 @LoginRequired()
205 @LoginRequired()
206 @NotAnonymous()
206 @NotAnonymous()
207 def my_account_auth_tokens(self):
207 def my_account_auth_tokens(self):
208 _ = self.request.translate
208 _ = self.request.translate
209
209
210 c = self.load_default_context()
210 c = self.load_default_context()
211 c.active = 'auth_tokens'
211 c.active = 'auth_tokens'
212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 c.role_values = [
213 c.role_values = [
214 (x, AuthTokenModel.cls._get_role_name(x))
214 (x, AuthTokenModel.cls._get_role_name(x))
215 for x in AuthTokenModel.cls.ROLES]
215 for x in AuthTokenModel.cls.ROLES]
216 c.role_options = [(c.role_values, _("Role"))]
216 c.role_options = [(c.role_values, _("Role"))]
217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 c.user.user_id, show_expired=True)
218 c.user.user_id, show_expired=True)
219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 return self._get_template_context(c)
220 return self._get_template_context(c)
221
221
222 @LoginRequired()
222 @LoginRequired()
223 @NotAnonymous()
223 @NotAnonymous()
224 @CSRFRequired()
224 @CSRFRequired()
225 def my_account_auth_tokens_view(self):
225 def my_account_auth_tokens_view(self):
226 _ = self.request.translate
226 _ = self.request.translate
227 c = self.load_default_context()
227 c = self.load_default_context()
228
228
229 auth_token_id = self.request.POST.get('auth_token_id')
229 auth_token_id = self.request.POST.get('auth_token_id')
230
230
231 if auth_token_id:
231 if auth_token_id:
232 token = UserApiKeys.get_or_404(auth_token_id)
232 token = UserApiKeys.get_or_404(auth_token_id)
233 if token.user.user_id != c.user.user_id:
233 if token.user.user_id != c.user.user_id:
234 raise HTTPNotFound()
234 raise HTTPNotFound()
235
235
236 return {
236 return {
237 'auth_token': token.api_key
237 'auth_token': token.api_key
238 }
238 }
239
239
240 def maybe_attach_token_scope(self, token):
240 def maybe_attach_token_scope(self, token):
241 # implemented in EE edition
241 # implemented in EE edition
242 pass
242 pass
243
243
244 @LoginRequired()
244 @LoginRequired()
245 @NotAnonymous()
245 @NotAnonymous()
246 @CSRFRequired()
246 @CSRFRequired()
247 def my_account_auth_tokens_add(self):
247 def my_account_auth_tokens_add(self):
248 _ = self.request.translate
248 _ = self.request.translate
249 c = self.load_default_context()
249 c = self.load_default_context()
250
250
251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 description = self.request.POST.get('description')
252 description = self.request.POST.get('description')
253 role = self.request.POST.get('role')
253 role = self.request.POST.get('role')
254
254
255 token = UserModel().add_auth_token(
255 token = UserModel().add_auth_token(
256 user=c.user.user_id,
256 user=c.user.user_id,
257 lifetime_minutes=lifetime, role=role, description=description,
257 lifetime_minutes=lifetime, role=role, description=description,
258 scope_callback=self.maybe_attach_token_scope)
258 scope_callback=self.maybe_attach_token_scope)
259 token_data = token.get_api_data()
259 token_data = token.get_api_data()
260
260
261 audit_logger.store_web(
261 audit_logger.store_web(
262 'user.edit.token.add', action_data={
262 'user.edit.token.add', action_data={
263 'data': {'token': token_data, 'user': 'self'}},
263 'data': {'token': token_data, 'user': 'self'}},
264 user=self._rhodecode_user, )
264 user=self._rhodecode_user, )
265 Session().commit()
265 Session().commit()
266
266
267 h.flash(_("Auth token successfully created"), category='success')
267 h.flash(_("Auth token successfully created"), category='success')
268 return HTTPFound(h.route_path('my_account_auth_tokens'))
268 return HTTPFound(h.route_path('my_account_auth_tokens'))
269
269
270 @LoginRequired()
270 @LoginRequired()
271 @NotAnonymous()
271 @NotAnonymous()
272 @CSRFRequired()
272 @CSRFRequired()
273 def my_account_auth_tokens_delete(self):
273 def my_account_auth_tokens_delete(self):
274 _ = self.request.translate
274 _ = self.request.translate
275 c = self.load_default_context()
275 c = self.load_default_context()
276
276
277 del_auth_token = self.request.POST.get('del_auth_token')
277 del_auth_token = self.request.POST.get('del_auth_token')
278
278
279 if del_auth_token:
279 if del_auth_token:
280 token = UserApiKeys.get_or_404(del_auth_token)
280 token = UserApiKeys.get_or_404(del_auth_token)
281 token_data = token.get_api_data()
281 token_data = token.get_api_data()
282
282
283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 audit_logger.store_web(
284 audit_logger.store_web(
285 'user.edit.token.delete', action_data={
285 'user.edit.token.delete', action_data={
286 'data': {'token': token_data, 'user': 'self'}},
286 'data': {'token': token_data, 'user': 'self'}},
287 user=self._rhodecode_user,)
287 user=self._rhodecode_user,)
288 Session().commit()
288 Session().commit()
289 h.flash(_("Auth token successfully deleted"), category='success')
289 h.flash(_("Auth token successfully deleted"), category='success')
290
290
291 return HTTPFound(h.route_path('my_account_auth_tokens'))
291 return HTTPFound(h.route_path('my_account_auth_tokens'))
292
292
293 @LoginRequired()
293 @LoginRequired()
294 @NotAnonymous()
294 @NotAnonymous()
295 def my_account_emails(self):
295 def my_account_emails(self):
296 _ = self.request.translate
296 _ = self.request.translate
297
297
298 c = self.load_default_context()
298 c = self.load_default_context()
299 c.active = 'emails'
299 c.active = 'emails'
300
300
301 c.user_email_map = UserEmailMap.query()\
301 c.user_email_map = UserEmailMap.query()\
302 .filter(UserEmailMap.user == c.user).all()
302 .filter(UserEmailMap.user == c.user).all()
303
303
304 schema = user_schema.AddEmailSchema().bind(
304 schema = user_schema.AddEmailSchema().bind(
305 username=c.user.username, user_emails=c.user.emails)
305 username=c.user.username, user_emails=c.user.emails)
306
306
307 form = forms.RcForm(schema,
307 form = forms.RcForm(schema,
308 action=h.route_path('my_account_emails_add'),
308 action=h.route_path('my_account_emails_add'),
309 buttons=(forms.buttons.save, forms.buttons.reset))
309 buttons=(forms.buttons.save, forms.buttons.reset))
310
310
311 c.form = form
311 c.form = form
312 return self._get_template_context(c)
312 return self._get_template_context(c)
313
313
314 @LoginRequired()
314 @LoginRequired()
315 @NotAnonymous()
315 @NotAnonymous()
316 @CSRFRequired()
316 @CSRFRequired()
317 def my_account_emails_add(self):
317 def my_account_emails_add(self):
318 _ = self.request.translate
318 _ = self.request.translate
319 c = self.load_default_context()
319 c = self.load_default_context()
320 c.active = 'emails'
320 c.active = 'emails'
321
321
322 schema = user_schema.AddEmailSchema().bind(
322 schema = user_schema.AddEmailSchema().bind(
323 username=c.user.username, user_emails=c.user.emails)
323 username=c.user.username, user_emails=c.user.emails)
324
324
325 form = forms.RcForm(
325 form = forms.RcForm(
326 schema, action=h.route_path('my_account_emails_add'),
326 schema, action=h.route_path('my_account_emails_add'),
327 buttons=(forms.buttons.save, forms.buttons.reset))
327 buttons=(forms.buttons.save, forms.buttons.reset))
328
328
329 controls = self.request.POST.items()
329 controls = self.request.POST.items()
330 try:
330 try:
331 valid_data = form.validate(controls)
331 valid_data = form.validate(controls)
332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 audit_logger.store_web(
333 audit_logger.store_web(
334 'user.edit.email.add', action_data={
334 'user.edit.email.add', action_data={
335 'data': {'email': valid_data['email'], 'user': 'self'}},
335 'data': {'email': valid_data['email'], 'user': 'self'}},
336 user=self._rhodecode_user,)
336 user=self._rhodecode_user,)
337 Session().commit()
337 Session().commit()
338 except formencode.Invalid as error:
338 except formencode.Invalid as error:
339 h.flash(h.escape(error.error_dict['email']), category='error')
339 h.flash(h.escape(error.error_dict['email']), category='error')
340 except forms.ValidationFailure as e:
340 except forms.ValidationFailure as e:
341 c.user_email_map = UserEmailMap.query() \
341 c.user_email_map = UserEmailMap.query() \
342 .filter(UserEmailMap.user == c.user).all()
342 .filter(UserEmailMap.user == c.user).all()
343 c.form = e
343 c.form = e
344 return self._get_template_context(c)
344 return self._get_template_context(c)
345 except Exception:
345 except Exception:
346 log.exception("Exception adding email")
346 log.exception("Exception adding email")
347 h.flash(_('Error occurred during adding email'),
347 h.flash(_('Error occurred during adding email'),
348 category='error')
348 category='error')
349 else:
349 else:
350 h.flash(_("Successfully added email"), category='success')
350 h.flash(_("Successfully added email"), category='success')
351
351
352 raise HTTPFound(self.request.route_path('my_account_emails'))
352 raise HTTPFound(self.request.route_path('my_account_emails'))
353
353
354 @LoginRequired()
354 @LoginRequired()
355 @NotAnonymous()
355 @NotAnonymous()
356 @CSRFRequired()
356 @CSRFRequired()
357 def my_account_emails_delete(self):
357 def my_account_emails_delete(self):
358 _ = self.request.translate
358 _ = self.request.translate
359 c = self.load_default_context()
359 c = self.load_default_context()
360
360
361 del_email_id = self.request.POST.get('del_email_id')
361 del_email_id = self.request.POST.get('del_email_id')
362 if del_email_id:
362 if del_email_id:
363 email = UserEmailMap.get_or_404(del_email_id).email
363 email = UserEmailMap.get_or_404(del_email_id).email
364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 audit_logger.store_web(
365 audit_logger.store_web(
366 'user.edit.email.delete', action_data={
366 'user.edit.email.delete', action_data={
367 'data': {'email': email, 'user': 'self'}},
367 'data': {'email': email, 'user': 'self'}},
368 user=self._rhodecode_user,)
368 user=self._rhodecode_user,)
369 Session().commit()
369 Session().commit()
370 h.flash(_("Email successfully deleted"),
370 h.flash(_("Email successfully deleted"),
371 category='success')
371 category='success')
372 return HTTPFound(h.route_path('my_account_emails'))
372 return HTTPFound(h.route_path('my_account_emails'))
373
373
374 @LoginRequired()
374 @LoginRequired()
375 @NotAnonymous()
375 @NotAnonymous()
376 @CSRFRequired()
376 @CSRFRequired()
377 def my_account_notifications_test_channelstream(self):
377 def my_account_notifications_test_channelstream(self):
378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 self._rhodecode_user.username, datetime.datetime.now())
379 self._rhodecode_user.username, datetime.datetime.now())
380 payload = {
380 payload = {
381 # 'channel': 'broadcast',
381 # 'channel': 'broadcast',
382 'type': 'message',
382 'type': 'message',
383 'timestamp': datetime.datetime.utcnow(),
383 'timestamp': datetime.datetime.utcnow(),
384 'user': 'system',
384 'user': 'system',
385 'pm_users': [self._rhodecode_user.username],
385 'pm_users': [self._rhodecode_user.username],
386 'message': {
386 'message': {
387 'message': message,
387 'message': message,
388 'level': 'info',
388 'level': 'info',
389 'topic': '/notifications'
389 'topic': '/notifications'
390 }
390 }
391 }
391 }
392
392
393 registry = self.request.registry
393 registry = self.request.registry
394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 channelstream_config = rhodecode_plugins.get('channelstream', {})
395 channelstream_config = rhodecode_plugins.get('channelstream', {})
396
396
397 try:
397 try:
398 channelstream_request(channelstream_config, [payload], '/message')
398 channelstream_request(channelstream_config, [payload], '/message')
399 except ChannelstreamException as e:
399 except ChannelstreamException as e:
400 log.exception('Failed to send channelstream data')
400 log.exception('Failed to send channelstream data')
401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
402 return {"response": 'Channelstream data sent. '
402 return {"response": 'Channelstream data sent. '
403 'You should see a new live message now.'}
403 'You should see a new live message now.'}
404
404
405 def _load_my_repos_data(self, watched=False):
405 def _load_my_repos_data(self, watched=False):
406
406
407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408
408
409 if watched:
409 if watched:
410 # repos user watch
410 # repos user watch
411 repo_list = Session().query(
411 repo_list = Session().query(
412 Repository
412 Repository
413 ) \
413 ) \
414 .join(
414 .join(
415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 ) \
416 ) \
417 .filter(
417 .filter(
418 UserFollowing.user_id == self._rhodecode_user.user_id
418 UserFollowing.user_id == self._rhodecode_user.user_id
419 ) \
419 ) \
420 .filter(or_(
420 .filter(or_(
421 # generate multiple IN to fix limitation problems
421 # generate multiple IN to fix limitation problems
422 *in_filter_generator(Repository.repo_id, allowed_ids))
422 *in_filter_generator(Repository.repo_id, allowed_ids))
423 ) \
423 ) \
424 .order_by(Repository.repo_name) \
424 .order_by(Repository.repo_name) \
425 .all()
425 .all()
426
426
427 else:
427 else:
428 # repos user is owner of
428 # repos user is owner of
429 repo_list = Session().query(
429 repo_list = Session().query(
430 Repository
430 Repository
431 ) \
431 ) \
432 .filter(
432 .filter(
433 Repository.user_id == self._rhodecode_user.user_id
433 Repository.user_id == self._rhodecode_user.user_id
434 ) \
434 ) \
435 .filter(or_(
435 .filter(or_(
436 # generate multiple IN to fix limitation problems
436 # generate multiple IN to fix limitation problems
437 *in_filter_generator(Repository.repo_id, allowed_ids))
437 *in_filter_generator(Repository.repo_id, allowed_ids))
438 ) \
438 ) \
439 .order_by(Repository.repo_name) \
439 .order_by(Repository.repo_name) \
440 .all()
440 .all()
441
441
442 _render = self.request.get_partial_renderer(
442 _render = self.request.get_partial_renderer(
443 'rhodecode:templates/data_table/_dt_elements.mako')
443 'rhodecode:templates/data_table/_dt_elements.mako')
444
444
445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 short_name=False, admin=False)
447 short_name=False, admin=False)
448
448
449 repos_data = []
449 repos_data = []
450 for repo in repo_list:
450 for repo in repo_list:
451 row = {
451 row = {
452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 repo.private, repo.archived, repo.fork),
453 repo.private, repo.archived, repo.fork),
454 "name_raw": repo.repo_name.lower(),
454 "name_raw": repo.repo_name.lower(),
455 }
455 }
456
456
457 repos_data.append(row)
457 repos_data.append(row)
458
458
459 # json used to render the grid
459 # json used to render the grid
460 return json.dumps(repos_data)
460 return json.dumps(repos_data)
461
461
462 @LoginRequired()
462 @LoginRequired()
463 @NotAnonymous()
463 @NotAnonymous()
464 def my_account_repos(self):
464 def my_account_repos(self):
465 c = self.load_default_context()
465 c = self.load_default_context()
466 c.active = 'repos'
466 c.active = 'repos'
467
467
468 # json used to render the grid
468 # json used to render the grid
469 c.data = self._load_my_repos_data()
469 c.data = self._load_my_repos_data()
470 return self._get_template_context(c)
470 return self._get_template_context(c)
471
471
472 @LoginRequired()
472 @LoginRequired()
473 @NotAnonymous()
473 @NotAnonymous()
474 def my_account_watched(self):
474 def my_account_watched(self):
475 c = self.load_default_context()
475 c = self.load_default_context()
476 c.active = 'watched'
476 c.active = 'watched'
477
477
478 # json used to render the grid
478 # json used to render the grid
479 c.data = self._load_my_repos_data(watched=True)
479 c.data = self._load_my_repos_data(watched=True)
480 return self._get_template_context(c)
480 return self._get_template_context(c)
481
481
482 @LoginRequired()
482 @LoginRequired()
483 @NotAnonymous()
483 @NotAnonymous()
484 def my_account_bookmarks(self):
484 def my_account_bookmarks(self):
485 c = self.load_default_context()
485 c = self.load_default_context()
486 c.active = 'bookmarks'
486 c.active = 'bookmarks'
487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 self._rhodecode_db_user.user_id, cache=False)
488 self._rhodecode_db_user.user_id, cache=False)
489 return self._get_template_context(c)
489 return self._get_template_context(c)
490
490
491 def _process_bookmark_entry(self, entry, user_id):
491 def _process_bookmark_entry(self, entry, user_id):
492 position = safe_int(entry.get('position'))
492 position = safe_int(entry.get('position'))
493 cur_position = safe_int(entry.get('cur_position'))
493 cur_position = safe_int(entry.get('cur_position'))
494 if position is None:
494 if position is None:
495 return
495 return
496
496
497 # check if this is an existing entry
497 # check if this is an existing entry
498 is_new = False
498 is_new = False
499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500
500
501 if db_entry and str2bool(entry.get('remove')):
501 if db_entry and str2bool(entry.get('remove')):
502 log.debug('Marked bookmark %s for deletion', db_entry)
502 log.debug('Marked bookmark %s for deletion', db_entry)
503 Session().delete(db_entry)
503 Session().delete(db_entry)
504 return
504 return
505
505
506 if not db_entry:
506 if not db_entry:
507 # new
507 # new
508 db_entry = UserBookmark()
508 db_entry = UserBookmark()
509 is_new = True
509 is_new = True
510
510
511 should_save = False
511 should_save = False
512 default_redirect_url = ''
512 default_redirect_url = ''
513
513
514 # save repo
514 # save repo
515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 repo = Repository.get(entry['bookmark_repo'])
516 repo = Repository.get(entry['bookmark_repo'])
517 perm_check = HasRepoPermissionAny(
517 perm_check = HasRepoPermissionAny(
518 'repository.read', 'repository.write', 'repository.admin')
518 'repository.read', 'repository.write', 'repository.admin')
519 if repo and perm_check(repo_name=repo.repo_name):
519 if repo and perm_check(repo_name=repo.repo_name):
520 db_entry.repository = repo
520 db_entry.repository = repo
521 should_save = True
521 should_save = True
522 default_redirect_url = '${repo_url}'
522 default_redirect_url = '${repo_url}'
523 # save repo group
523 # save repo group
524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 perm_check = HasRepoGroupPermissionAny(
526 perm_check = HasRepoGroupPermissionAny(
527 'group.read', 'group.write', 'group.admin')
527 'group.read', 'group.write', 'group.admin')
528
528
529 if repo_group and perm_check(group_name=repo_group.group_name):
529 if repo_group and perm_check(group_name=repo_group.group_name):
530 db_entry.repository_group = repo_group
530 db_entry.repository_group = repo_group
531 should_save = True
531 should_save = True
532 default_redirect_url = '${repo_group_url}'
532 default_redirect_url = '${repo_group_url}'
533 # save generic info
533 # save generic info
534 elif entry.get('title') and entry.get('redirect_url'):
534 elif entry.get('title') and entry.get('redirect_url'):
535 should_save = True
535 should_save = True
536
536
537 if should_save:
537 if should_save:
538 # mark user and position
538 # mark user and position
539 db_entry.user_id = user_id
539 db_entry.user_id = user_id
540 db_entry.position = position
540 db_entry.position = position
541 db_entry.title = entry.get('title')
541 db_entry.title = entry.get('title')
542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544
544
545 Session().add(db_entry)
545 Session().add(db_entry)
546
546
547 @LoginRequired()
547 @LoginRequired()
548 @NotAnonymous()
548 @NotAnonymous()
549 @CSRFRequired()
549 @CSRFRequired()
550 def my_account_bookmarks_update(self):
550 def my_account_bookmarks_update(self):
551 _ = self.request.translate
551 _ = self.request.translate
552 c = self.load_default_context()
552 c = self.load_default_context()
553 c.active = 'bookmarks'
553 c.active = 'bookmarks'
554
554
555 controls = peppercorn.parse(self.request.POST.items())
555 controls = peppercorn.parse(self.request.POST.items())
556 user_id = c.user.user_id
556 user_id = c.user.user_id
557
557
558 # validate positions
558 # validate positions
559 positions = {}
559 positions = {}
560 for entry in controls.get('bookmarks', []):
560 for entry in controls.get('bookmarks', []):
561 position = safe_int(entry['position'])
561 position = safe_int(entry['position'])
562 if position is None:
562 if position is None:
563 continue
563 continue
564
564
565 if position in positions:
565 if position in positions:
566 h.flash(_("Position {} is defined twice. "
566 h.flash(_("Position {} is defined twice. "
567 "Please correct this error.").format(position), category='error')
567 "Please correct this error.").format(position), category='error')
568 return HTTPFound(h.route_path('my_account_bookmarks'))
568 return HTTPFound(h.route_path('my_account_bookmarks'))
569
569
570 entry['position'] = position
570 entry['position'] = position
571 entry['cur_position'] = safe_int(entry.get('cur_position'))
571 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 positions[position] = entry
572 positions[position] = entry
573
573
574 try:
574 try:
575 for entry in positions.values():
575 for entry in positions.values():
576 self._process_bookmark_entry(entry, user_id)
576 self._process_bookmark_entry(entry, user_id)
577
577
578 Session().commit()
578 Session().commit()
579 h.flash(_("Update Bookmarks"), category='success')
579 h.flash(_("Update Bookmarks"), category='success')
580 except IntegrityError:
580 except IntegrityError:
581 h.flash(_("Failed to update bookmarks. "
581 h.flash(_("Failed to update bookmarks. "
582 "Make sure an unique position is used."), category='error')
582 "Make sure an unique position is used."), category='error')
583
583
584 return HTTPFound(h.route_path('my_account_bookmarks'))
584 return HTTPFound(h.route_path('my_account_bookmarks'))
585
585
586 @LoginRequired()
586 @LoginRequired()
587 @NotAnonymous()
587 @NotAnonymous()
588 def my_account_goto_bookmark(self):
588 def my_account_goto_bookmark(self):
589
589
590 bookmark_id = self.request.matchdict['bookmark_id']
590 bookmark_id = self.request.matchdict['bookmark_id']
591 user_bookmark = UserBookmark().query()\
591 user_bookmark = UserBookmark().query()\
592 .filter(UserBookmark.user_id == self.request.user.user_id) \
592 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 .filter(UserBookmark.position == bookmark_id).scalar()
593 .filter(UserBookmark.position == bookmark_id).scalar()
594
594
595 redirect_url = h.route_path('my_account_bookmarks')
595 redirect_url = h.route_path('my_account_bookmarks')
596 if not user_bookmark:
596 if not user_bookmark:
597 raise HTTPFound(redirect_url)
597 raise HTTPFound(redirect_url)
598
598
599 # repository set
599 # repository set
600 if user_bookmark.repository:
600 if user_bookmark.repository:
601 repo_name = user_bookmark.repository.repo_name
601 repo_name = user_bookmark.repository.repo_name
602 base_redirect_url = h.route_path(
602 base_redirect_url = h.route_path(
603 'repo_summary', repo_name=repo_name)
603 'repo_summary', repo_name=repo_name)
604 if user_bookmark.redirect_url and \
604 if user_bookmark.redirect_url and \
605 '${repo_url}' in user_bookmark.redirect_url:
605 '${repo_url}' in user_bookmark.redirect_url:
606 redirect_url = string.Template(user_bookmark.redirect_url)\
606 redirect_url = string.Template(user_bookmark.redirect_url)\
607 .safe_substitute({'repo_url': base_redirect_url})
607 .safe_substitute({'repo_url': base_redirect_url})
608 else:
608 else:
609 redirect_url = base_redirect_url
609 redirect_url = base_redirect_url
610 # repository group set
610 # repository group set
611 elif user_bookmark.repository_group:
611 elif user_bookmark.repository_group:
612 repo_group_name = user_bookmark.repository_group.group_name
612 repo_group_name = user_bookmark.repository_group.group_name
613 base_redirect_url = h.route_path(
613 base_redirect_url = h.route_path(
614 'repo_group_home', repo_group_name=repo_group_name)
614 'repo_group_home', repo_group_name=repo_group_name)
615 if user_bookmark.redirect_url and \
615 if user_bookmark.redirect_url and \
616 '${repo_group_url}' in user_bookmark.redirect_url:
616 '${repo_group_url}' in user_bookmark.redirect_url:
617 redirect_url = string.Template(user_bookmark.redirect_url)\
617 redirect_url = string.Template(user_bookmark.redirect_url)\
618 .safe_substitute({'repo_group_url': base_redirect_url})
618 .safe_substitute({'repo_group_url': base_redirect_url})
619 else:
619 else:
620 redirect_url = base_redirect_url
620 redirect_url = base_redirect_url
621 # custom URL set
621 # custom URL set
622 elif user_bookmark.redirect_url:
622 elif user_bookmark.redirect_url:
623 server_url = h.route_url('home').rstrip('/')
623 server_url = h.route_url('home').rstrip('/')
624 redirect_url = string.Template(user_bookmark.redirect_url) \
624 redirect_url = string.Template(user_bookmark.redirect_url) \
625 .safe_substitute({'server_url': server_url})
625 .safe_substitute({'server_url': server_url})
626
626
627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 raise HTTPFound(redirect_url)
628 raise HTTPFound(redirect_url)
629
629
630 @LoginRequired()
630 @LoginRequired()
631 @NotAnonymous()
631 @NotAnonymous()
632 def my_account_perms(self):
632 def my_account_perms(self):
633 c = self.load_default_context()
633 c = self.load_default_context()
634 c.active = 'perms'
634 c.active = 'perms'
635
635
636 c.perm_user = c.auth_user
636 c.perm_user = c.auth_user
637 return self._get_template_context(c)
637 return self._get_template_context(c)
638
638
639 @LoginRequired()
639 @LoginRequired()
640 @NotAnonymous()
640 @NotAnonymous()
641 def my_notifications(self):
641 def my_notifications(self):
642 c = self.load_default_context()
642 c = self.load_default_context()
643 c.active = 'notifications'
643 c.active = 'notifications'
644
644
645 return self._get_template_context(c)
645 return self._get_template_context(c)
646
646
647 @LoginRequired()
647 @LoginRequired()
648 @NotAnonymous()
648 @NotAnonymous()
649 @CSRFRequired()
649 @CSRFRequired()
650 def my_notifications_toggle_visibility(self):
650 def my_notifications_toggle_visibility(self):
651 user = self._rhodecode_db_user
651 user = self._rhodecode_db_user
652 new_status = not user.user_data.get('notification_status', True)
652 new_status = not user.user_data.get('notification_status', True)
653 user.update_userdata(notification_status=new_status)
653 user.update_userdata(notification_status=new_status)
654 Session().commit()
654 Session().commit()
655 return user.user_data['notification_status']
655 return user.user_data['notification_status']
656
656
657 def _get_pull_requests_list(self, statuses):
657 def _get_pull_requests_list(self, statuses, filter_type=None):
658 draw, start, limit = self._extract_chunk(self.request)
658 draw, start, limit = self._extract_chunk(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
660
660
661 _render = self.request.get_partial_renderer(
661 _render = self.request.get_partial_renderer(
662 'rhodecode:templates/data_table/_dt_elements.mako')
662 'rhodecode:templates/data_table/_dt_elements.mako')
663
663
664 if filter_type == 'awaiting_my_review':
665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 user_id=self._rhodecode_user.user_id,
667 statuses=statuses, query=search_q,
668 offset=start, length=limit, order_by=order_by,
669 order_dir=order_dir)
670
671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 else:
664 pull_requests = PullRequestModel().get_im_participating_in(
674 pull_requests = PullRequestModel().get_im_participating_in(
665 user_id=self._rhodecode_user.user_id,
675 user_id=self._rhodecode_user.user_id,
666 statuses=statuses, query=search_q,
676 statuses=statuses, query=search_q,
667 offset=start, length=limit, order_by=order_by,
677 offset=start, length=limit, order_by=order_by,
668 order_dir=order_dir)
678 order_dir=order_dir)
669
679
670 pull_requests_total_count = PullRequestModel().count_im_participating_in(
680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
671 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
672
682
673 data = []
683 data = []
674 comments_model = CommentsModel()
684 comments_model = CommentsModel()
675 for pr in pull_requests:
685 for pr in pull_requests:
676 repo_id = pr.target_repo_id
686 repo_id = pr.target_repo_id
677 comments_count = comments_model.get_all_comments(
687 comments_count = comments_model.get_all_comments(
678 repo_id, pull_request=pr, include_drafts=False, count_only=True)
688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
679 owned = pr.user_id == self._rhodecode_user.user_id
689 owned = pr.user_id == self._rhodecode_user.user_id
680
690
691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 if review_statuses and review_statuses[4]:
694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 my_review_status = statuses[0][1].status
696
681 data.append({
697 data.append({
682 'target_repo': _render('pullrequest_target_repo',
698 'target_repo': _render('pullrequest_target_repo',
683 pr.target_repo.repo_name),
699 pr.target_repo.repo_name),
684 'name': _render('pullrequest_name',
700 'name': _render('pullrequest_name',
685 pr.pull_request_id, pr.pull_request_state,
701 pr.pull_request_id, pr.pull_request_state,
686 pr.work_in_progress, pr.target_repo.repo_name,
702 pr.work_in_progress, pr.target_repo.repo_name,
687 short=True),
703 short=True),
688 'name_raw': pr.pull_request_id,
704 'name_raw': pr.pull_request_id,
689 'status': _render('pullrequest_status',
705 'status': _render('pullrequest_status',
690 pr.calculated_review_status()),
706 pr.calculated_review_status()),
707 'my_status': _render('pullrequest_status',
708 my_review_status),
691 'title': _render('pullrequest_title', pr.title, pr.description),
709 'title': _render('pullrequest_title', pr.title, pr.description),
692 'description': h.escape(pr.description),
710 'description': h.escape(pr.description),
693 'updated_on': _render('pullrequest_updated_on',
711 'updated_on': _render('pullrequest_updated_on',
694 h.datetime_to_time(pr.updated_on),
712 h.datetime_to_time(pr.updated_on),
695 pr.versions_count),
713 pr.versions_count),
696 'updated_on_raw': h.datetime_to_time(pr.updated_on),
714 'updated_on_raw': h.datetime_to_time(pr.updated_on),
697 'created_on': _render('pullrequest_updated_on',
715 'created_on': _render('pullrequest_updated_on',
698 h.datetime_to_time(pr.created_on)),
716 h.datetime_to_time(pr.created_on)),
699 'created_on_raw': h.datetime_to_time(pr.created_on),
717 'created_on_raw': h.datetime_to_time(pr.created_on),
700 'state': pr.pull_request_state,
718 'state': pr.pull_request_state,
701 'author': _render('pullrequest_author',
719 'author': _render('pullrequest_author',
702 pr.author.full_contact, ),
720 pr.author.full_contact, ),
703 'author_raw': pr.author.full_name,
721 'author_raw': pr.author.full_name,
704 'comments': _render('pullrequest_comments', comments_count),
722 'comments': _render('pullrequest_comments', comments_count),
705 'comments_raw': comments_count,
723 'comments_raw': comments_count,
706 'closed': pr.is_closed(),
724 'closed': pr.is_closed(),
707 'owned': owned
725 'owned': owned
708 })
726 })
709
727
710 # json used to render the grid
728 # json used to render the grid
711 data = ({
729 data = ({
712 'draw': draw,
730 'draw': draw,
713 'data': data,
731 'data': data,
714 'recordsTotal': pull_requests_total_count,
732 'recordsTotal': pull_requests_total_count,
715 'recordsFiltered': pull_requests_total_count,
733 'recordsFiltered': pull_requests_total_count,
716 })
734 })
717 return data
735 return data
718
736
719 @LoginRequired()
737 @LoginRequired()
720 @NotAnonymous()
738 @NotAnonymous()
721 def my_account_pullrequests(self):
739 def my_account_pullrequests(self):
722 c = self.load_default_context()
740 c = self.load_default_context()
723 c.active = 'pullrequests'
741 c.active = 'pullrequests'
724 req_get = self.request.GET
742 req_get = self.request.GET
725
743
726 c.closed = str2bool(req_get.get('pr_show_closed'))
744 c.closed = str2bool(req_get.get('closed'))
745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746
747 c.selected_filter = 'all'
748 if c.closed:
749 c.selected_filter = 'all_closed'
750 if c.awaiting_my_review:
751 c.selected_filter = 'awaiting_my_review'
727
752
728 return self._get_template_context(c)
753 return self._get_template_context(c)
729
754
730 @LoginRequired()
755 @LoginRequired()
731 @NotAnonymous()
756 @NotAnonymous()
732 def my_account_pullrequests_data(self):
757 def my_account_pullrequests_data(self):
733 self.load_default_context()
758 self.load_default_context()
734 req_get = self.request.GET
759 req_get = self.request.GET
760
761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
735 closed = str2bool(req_get.get('closed'))
762 closed = str2bool(req_get.get('closed'))
736
763
737 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
738 if closed:
765 if closed:
739 statuses += [PullRequest.STATUS_CLOSED]
766 statuses += [PullRequest.STATUS_CLOSED]
740
767
741 data = self._get_pull_requests_list(statuses=statuses)
768 filter_type = \
769 'awaiting_my_review' if awaiting_my_review \
770 else None
771
772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
742 return data
773 return data
743
774
744 @LoginRequired()
775 @LoginRequired()
745 @NotAnonymous()
776 @NotAnonymous()
746 def my_account_user_group_membership(self):
777 def my_account_user_group_membership(self):
747 c = self.load_default_context()
778 c = self.load_default_context()
748 c.active = 'user_group_membership'
779 c.active = 'user_group_membership'
749 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
780 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
750 for group in self._rhodecode_db_user.group_member]
781 for group in self._rhodecode_db_user.group_member]
751 c.user_groups = json.dumps(groups)
782 c.user_groups = json.dumps(groups)
752 return self._get_template_context(c)
783 return self._get_template_context(c)
@@ -1,84 +1,84 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 from rhodecode.model.db import Repository
22 from rhodecode.model.db import Repository
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib
26 import urllib
27
27
28 base_url = {
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
31 }[name].format(**kwargs)
32
32
33 if params:
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 return base_url
35 return base_url
36
36
37
37
38 @pytest.mark.backends("git", "hg")
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
39 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
40 class TestPullRequestList(object):
41
41
42 @pytest.mark.parametrize('params, expected_title', [
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed'),
43 ({'source': 0, 'closed': 1}, 'Closed'),
44 ({'source': 0, 'my': 1}, 'Opened by me'),
44 ({'source': 0, 'my': 1}, 'Created by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 ({'source': 1}, 'From this repo'),
47 ({'source': 1}, 'From this repo'),
48 ])
48 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
50 pull_request = pr_util.create_pull_request()
51
51
52 response = self.app.get(
52 response = self.app.get(
53 route_path('pullrequest_show_all',
53 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
54 repo_name=pull_request.target_repo.repo_name,
55 params=params))
55 params=params))
56
56
57 assert_response = response.assert_response()
57 assert_response = response.assert_response()
58
58
59 element = assert_response.get_element('.title .active')
59 element = assert_response.get_element('.title .active')
60 element_text = element.text_content()
60 element_text = element.text_content()
61 assert expected_title == element_text
61 assert expected_title == element_text
62
62
63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 pull_request = pr_util.create_pull_request()
64 pull_request = pr_util.create_pull_request()
65 response = self.app.get(
65 response = self.app.get(
66 route_path('pullrequest_show_all_data',
66 route_path('pullrequest_show_all_data',
67 repo_name=pull_request.target_repo.repo_name),
67 repo_name=pull_request.target_repo.repo_name),
68 extra_environ=xhr_header)
68 extra_environ=xhr_header)
69
69
70 assert response.json['recordsTotal'] == 1
70 assert response.json['recordsTotal'] == 1
71 assert response.json['data'][0]['description'] == 'Description'
71 assert response.json['data'][0]['description'] == 'Description'
72
72
73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 xss_description = "<script>alert('Hi!')</script>"
74 xss_description = "<script>alert('Hi!')</script>"
75 pull_request = pr_util.create_pull_request(description=xss_description)
75 pull_request = pr_util.create_pull_request(description=xss_description)
76
76
77 response = self.app.get(
77 response = self.app.get(
78 route_path('pullrequest_show_all_data',
78 route_path('pullrequest_show_all_data',
79 repo_name=pull_request.target_repo.repo_name),
79 repo_name=pull_request.target_repo.repo_name),
80 extra_environ=xhr_header)
80 extra_environ=xhr_header)
81
81
82 assert response.json['recordsTotal'] == 1
82 assert response.json['recordsTotal'] == 1
83 assert response.json['data'][0]['description'] == \
83 assert response.json['data'][0]['description'] == \
84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,1861 +1,1868 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, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29
29
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.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name,
83 statuses=statuses, offset=start, length=limit,
83 search_q=search_q, statuses=statuses,
84 order_by=order_by, order_dir=order_dir)
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name,
87 opened_by=opened_by)
87 search_q=search_q, statuses=statuses)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, self._rhodecode_user.user_id,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 repo_name, self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
95 search_q=search_q, statuses=statuses)
97 else:
96 else:
98 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
101 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
102 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
103 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
104 opened_by=opened_by)
103 opened_by=opened_by)
105
104
106 data = []
105 data = []
107 comments_model = CommentsModel()
106 comments_model = CommentsModel()
108 for pr in pull_requests:
107 for pr in pull_requests:
109 comments_count = comments_model.get_all_comments(
108 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr,
109 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
110 include_drafts=False, count_only=True)
112
111
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
117
113 data.append({
118 data.append({
114 'name': _render('pullrequest_name',
119 'name': _render('pullrequest_name',
115 pr.pull_request_id, pr.pull_request_state,
120 pr.pull_request_id, pr.pull_request_state,
116 pr.work_in_progress, pr.target_repo.repo_name,
121 pr.work_in_progress, pr.target_repo.repo_name,
117 short=True),
122 short=True),
118 'name_raw': pr.pull_request_id,
123 'name_raw': pr.pull_request_id,
119 'status': _render('pullrequest_status',
124 'status': _render('pullrequest_status',
120 pr.calculated_review_status()),
125 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
127 my_review_status),
121 'title': _render('pullrequest_title', pr.title, pr.description),
128 'title': _render('pullrequest_title', pr.title, pr.description),
122 'description': h.escape(pr.description),
129 'description': h.escape(pr.description),
123 'updated_on': _render('pullrequest_updated_on',
130 'updated_on': _render('pullrequest_updated_on',
124 h.datetime_to_time(pr.updated_on),
131 h.datetime_to_time(pr.updated_on),
125 pr.versions_count),
132 pr.versions_count),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 'created_on': _render('pullrequest_updated_on',
134 'created_on': _render('pullrequest_updated_on',
128 h.datetime_to_time(pr.created_on)),
135 h.datetime_to_time(pr.created_on)),
129 'created_on_raw': h.datetime_to_time(pr.created_on),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
130 'state': pr.pull_request_state,
137 'state': pr.pull_request_state,
131 'author': _render('pullrequest_author',
138 'author': _render('pullrequest_author',
132 pr.author.full_contact, ),
139 pr.author.full_contact, ),
133 'author_raw': pr.author.full_name,
140 'author_raw': pr.author.full_name,
134 'comments': _render('pullrequest_comments', comments_count),
141 'comments': _render('pullrequest_comments', comments_count),
135 'comments_raw': comments_count,
142 'comments_raw': comments_count,
136 'closed': pr.is_closed(),
143 'closed': pr.is_closed(),
137 })
144 })
138
145
139 data = ({
146 data = ({
140 'draw': draw,
147 'draw': draw,
141 'data': data,
148 'data': data,
142 'recordsTotal': pull_requests_total_count,
149 'recordsTotal': pull_requests_total_count,
143 'recordsFiltered': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
144 })
151 })
145 return data
152 return data
146
153
147 @LoginRequired()
154 @LoginRequired()
148 @HasRepoPermissionAnyDecorator(
155 @HasRepoPermissionAnyDecorator(
149 'repository.read', 'repository.write', 'repository.admin')
156 'repository.read', 'repository.write', 'repository.admin')
150 def pull_request_list(self):
157 def pull_request_list(self):
151 c = self.load_default_context()
158 c = self.load_default_context()
152
159
153 req_get = self.request.GET
160 req_get = self.request.GET
154 c.source = str2bool(req_get.get('source'))
161 c.source = str2bool(req_get.get('source'))
155 c.closed = str2bool(req_get.get('closed'))
162 c.closed = str2bool(req_get.get('closed'))
156 c.my = str2bool(req_get.get('my'))
163 c.my = str2bool(req_get.get('my'))
157 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159
166
160 c.active = 'open'
167 c.active = 'open'
161 if c.my:
168 if c.my:
162 c.active = 'my'
169 c.active = 'my'
163 if c.closed:
170 if c.closed:
164 c.active = 'closed'
171 c.active = 'closed'
165 if c.awaiting_review and not c.source:
172 if c.awaiting_review and not c.source:
166 c.active = 'awaiting'
173 c.active = 'awaiting'
167 if c.source and not c.awaiting_review:
174 if c.source and not c.awaiting_review:
168 c.active = 'source'
175 c.active = 'source'
169 if c.awaiting_my_review:
176 if c.awaiting_my_review:
170 c.active = 'awaiting_my'
177 c.active = 'awaiting_my'
171
178
172 return self._get_template_context(c)
179 return self._get_template_context(c)
173
180
174 @LoginRequired()
181 @LoginRequired()
175 @HasRepoPermissionAnyDecorator(
182 @HasRepoPermissionAnyDecorator(
176 'repository.read', 'repository.write', 'repository.admin')
183 'repository.read', 'repository.write', 'repository.admin')
177 def pull_request_list_data(self):
184 def pull_request_list_data(self):
178 self.load_default_context()
185 self.load_default_context()
179
186
180 # additional filters
187 # additional filters
181 req_get = self.request.GET
188 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
189 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
190 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
191 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
194
188 filter_type = 'awaiting_review' if awaiting_review \
195 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
196 else 'awaiting_my_review' if awaiting_my_review \
190 else None
197 else None
191
198
192 opened_by = None
199 opened_by = None
193 if my:
200 if my:
194 opened_by = [self._rhodecode_user.user_id]
201 opened_by = [self._rhodecode_user.user_id]
195
202
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
204 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
199
206
200 data = self._get_pull_requests_list(
207 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
208 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
210
204 return data
211 return data
205
212
206 def _is_diff_cache_enabled(self, target_repo):
213 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
214 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
215 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
216 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
217 return caching_enabled
211
218
212 def _get_diffset(self, source_repo_name, source_repo,
219 def _get_diffset(self, source_repo_name, source_repo,
213 ancestor_commit,
220 ancestor_commit,
214 source_ref_id, target_ref_id,
221 source_ref_id, target_ref_id,
215 target_commit, source_commit, diff_limit, file_limit,
222 target_commit, source_commit, diff_limit, file_limit,
216 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217
224
218 target_commit_final = target_commit
225 target_commit_final = target_commit
219 source_commit_final = source_commit
226 source_commit_final = source_commit
220
227
221 if use_ancestor:
228 if use_ancestor:
222 # we might want to not use it for versions
229 # we might want to not use it for versions
223 target_ref_id = ancestor_commit.raw_id
230 target_ref_id = ancestor_commit.raw_id
224 target_commit_final = ancestor_commit
231 target_commit_final = ancestor_commit
225
232
226 vcs_diff = PullRequestModel().get_diff(
233 vcs_diff = PullRequestModel().get_diff(
227 source_repo, source_ref_id, target_ref_id,
234 source_repo, source_ref_id, target_ref_id,
228 hide_whitespace_changes, diff_context)
235 hide_whitespace_changes, diff_context)
229
236
230 diff_processor = diffs.DiffProcessor(
237 diff_processor = diffs.DiffProcessor(
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 file_limit=file_limit, show_full_diff=fulldiff)
239 file_limit=file_limit, show_full_diff=fulldiff)
233
240
234 _parsed = diff_processor.prepare()
241 _parsed = diff_processor.prepare()
235
242
236 diffset = codeblocks.DiffSet(
243 diffset = codeblocks.DiffSet(
237 repo_name=self.db_repo_name,
244 repo_name=self.db_repo_name,
238 source_repo_name=source_repo_name,
245 source_repo_name=source_repo_name,
239 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
241 )
248 )
242 diffset = self.path_filter.render_patchset_filtered(
249 diffset = self.path_filter.render_patchset_filtered(
243 diffset, _parsed, target_ref_id, source_ref_id)
250 diffset, _parsed, target_ref_id, source_ref_id)
244
251
245 return diffset
252 return diffset
246
253
247 def _get_range_diffset(self, source_scm, source_repo,
254 def _get_range_diffset(self, source_scm, source_repo,
248 commit1, commit2, diff_limit, file_limit,
255 commit1, commit2, diff_limit, file_limit,
249 fulldiff, hide_whitespace_changes, diff_context):
256 fulldiff, hide_whitespace_changes, diff_context):
250 vcs_diff = source_scm.get_diff(
257 vcs_diff = source_scm.get_diff(
251 commit1, commit2,
258 commit1, commit2,
252 ignore_whitespace=hide_whitespace_changes,
259 ignore_whitespace=hide_whitespace_changes,
253 context=diff_context)
260 context=diff_context)
254
261
255 diff_processor = diffs.DiffProcessor(
262 diff_processor = diffs.DiffProcessor(
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 file_limit=file_limit, show_full_diff=fulldiff)
264 file_limit=file_limit, show_full_diff=fulldiff)
258
265
259 _parsed = diff_processor.prepare()
266 _parsed = diff_processor.prepare()
260
267
261 diffset = codeblocks.DiffSet(
268 diffset = codeblocks.DiffSet(
262 repo_name=source_repo.repo_name,
269 repo_name=source_repo.repo_name,
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
265
272
266 diffset = self.path_filter.render_patchset_filtered(
273 diffset = self.path_filter.render_patchset_filtered(
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268
275
269 return diffset
276 return diffset
270
277
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 comments_model = CommentsModel()
279 comments_model = CommentsModel()
273
280
274 # GENERAL COMMENTS with versions #
281 # GENERAL COMMENTS with versions #
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
277 if not include_drafts:
284 if not include_drafts:
278 q = q.filter(ChangesetComment.draft == false())
285 q = q.filter(ChangesetComment.draft == false())
279 general_comments = q
286 general_comments = q
280
287
281 # pick comments we want to render at current version
288 # pick comments we want to render at current version
282 c.comment_versions = comments_model.aggregate_comments(
289 c.comment_versions = comments_model.aggregate_comments(
283 general_comments, versions, c.at_version_num)
290 general_comments, versions, c.at_version_num)
284
291
285 # INLINE COMMENTS with versions #
292 # INLINE COMMENTS with versions #
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 q = q.order_by(ChangesetComment.comment_id.asc())
294 q = q.order_by(ChangesetComment.comment_id.asc())
288 if not include_drafts:
295 if not include_drafts:
289 q = q.filter(ChangesetComment.draft == false())
296 q = q.filter(ChangesetComment.draft == false())
290 inline_comments = q
297 inline_comments = q
291
298
292 c.inline_versions = comments_model.aggregate_comments(
299 c.inline_versions = comments_model.aggregate_comments(
293 inline_comments, versions, c.at_version_num, inline=True)
300 inline_comments, versions, c.at_version_num, inline=True)
294
301
295 # Comments inline+general
302 # Comments inline+general
296 if c.at_version:
303 if c.at_version:
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
299 else:
306 else:
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
302
309
303 return general_comments, inline_comments
310 return general_comments, inline_comments
304
311
305 @LoginRequired()
312 @LoginRequired()
306 @HasRepoPermissionAnyDecorator(
313 @HasRepoPermissionAnyDecorator(
307 'repository.read', 'repository.write', 'repository.admin')
314 'repository.read', 'repository.write', 'repository.admin')
308 def pull_request_show(self):
315 def pull_request_show(self):
309 _ = self.request.translate
316 _ = self.request.translate
310 c = self.load_default_context()
317 c = self.load_default_context()
311
318
312 pull_request = PullRequest.get_or_404(
319 pull_request = PullRequest.get_or_404(
313 self.request.matchdict['pull_request_id'])
320 self.request.matchdict['pull_request_id'])
314 pull_request_id = pull_request.pull_request_id
321 pull_request_id = pull_request.pull_request_id
315
322
316 c.state_progressing = pull_request.is_state_changing()
323 c.state_progressing = pull_request.is_state_changing()
317 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
318
325
319 _new_state = {
326 _new_state = {
320 'created': PullRequest.STATE_CREATED,
327 'created': PullRequest.STATE_CREATED,
321 }.get(self.request.GET.get('force_state'))
328 }.get(self.request.GET.get('force_state'))
322
329
323 if c.is_super_admin and _new_state:
330 if c.is_super_admin and _new_state:
324 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
325 h.flash(
332 h.flash(
326 _('Pull Request state was force changed to `{}`').format(_new_state),
333 _('Pull Request state was force changed to `{}`').format(_new_state),
327 category='success')
334 category='success')
328 Session().commit()
335 Session().commit()
329
336
330 raise HTTPFound(h.route_path(
337 raise HTTPFound(h.route_path(
331 'pullrequest_show', repo_name=self.db_repo_name,
338 'pullrequest_show', repo_name=self.db_repo_name,
332 pull_request_id=pull_request_id))
339 pull_request_id=pull_request_id))
333
340
334 version = self.request.GET.get('version')
341 version = self.request.GET.get('version')
335 from_version = self.request.GET.get('from_version') or version
342 from_version = self.request.GET.get('from_version') or version
336 merge_checks = self.request.GET.get('merge_checks')
343 merge_checks = self.request.GET.get('merge_checks')
337 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
338 force_refresh = str2bool(self.request.GET.get('force_refresh'))
345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
339 c.range_diff_on = self.request.GET.get('range-diff') == "1"
346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
340
347
341 # fetch global flags of ignore ws or context lines
348 # fetch global flags of ignore ws or context lines
342 diff_context = diffs.get_diff_context(self.request)
349 diff_context = diffs.get_diff_context(self.request)
343 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
344
351
345 (pull_request_latest,
352 (pull_request_latest,
346 pull_request_at_ver,
353 pull_request_at_ver,
347 pull_request_display_obj,
354 pull_request_display_obj,
348 at_version) = PullRequestModel().get_pr_version(
355 at_version) = PullRequestModel().get_pr_version(
349 pull_request_id, version=version)
356 pull_request_id, version=version)
350
357
351 pr_closed = pull_request_latest.is_closed()
358 pr_closed = pull_request_latest.is_closed()
352
359
353 if pr_closed and (version or from_version):
360 if pr_closed and (version or from_version):
354 # not allow to browse versions for closed PR
361 # not allow to browse versions for closed PR
355 raise HTTPFound(h.route_path(
362 raise HTTPFound(h.route_path(
356 'pullrequest_show', repo_name=self.db_repo_name,
363 'pullrequest_show', repo_name=self.db_repo_name,
357 pull_request_id=pull_request_id))
364 pull_request_id=pull_request_id))
358
365
359 versions = pull_request_display_obj.versions()
366 versions = pull_request_display_obj.versions()
360
367
361 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
362
369
363 # used to store per-commit range diffs
370 # used to store per-commit range diffs
364 c.changes = collections.OrderedDict()
371 c.changes = collections.OrderedDict()
365
372
366 c.at_version = at_version
373 c.at_version = at_version
367 c.at_version_num = (at_version
374 c.at_version_num = (at_version
368 if at_version and at_version != PullRequest.LATEST_VER
375 if at_version and at_version != PullRequest.LATEST_VER
369 else None)
376 else None)
370
377
371 c.at_version_index = ChangesetComment.get_index_from_version(
378 c.at_version_index = ChangesetComment.get_index_from_version(
372 c.at_version_num, versions)
379 c.at_version_num, versions)
373
380
374 (prev_pull_request_latest,
381 (prev_pull_request_latest,
375 prev_pull_request_at_ver,
382 prev_pull_request_at_ver,
376 prev_pull_request_display_obj,
383 prev_pull_request_display_obj,
377 prev_at_version) = PullRequestModel().get_pr_version(
384 prev_at_version) = PullRequestModel().get_pr_version(
378 pull_request_id, version=from_version)
385 pull_request_id, version=from_version)
379
386
380 c.from_version = prev_at_version
387 c.from_version = prev_at_version
381 c.from_version_num = (prev_at_version
388 c.from_version_num = (prev_at_version
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 else None)
390 else None)
384 c.from_version_index = ChangesetComment.get_index_from_version(
391 c.from_version_index = ChangesetComment.get_index_from_version(
385 c.from_version_num, versions)
392 c.from_version_num, versions)
386
393
387 # define if we're in COMPARE mode or VIEW at version mode
394 # define if we're in COMPARE mode or VIEW at version mode
388 compare = at_version != prev_at_version
395 compare = at_version != prev_at_version
389
396
390 # pull_requests repo_name we opened it against
397 # pull_requests repo_name we opened it against
391 # ie. target_repo must match
398 # ie. target_repo must match
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 log.warning('Mismatch between the current repo: %s, and target %s',
400 log.warning('Mismatch between the current repo: %s, and target %s',
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 raise HTTPNotFound()
402 raise HTTPNotFound()
396
403
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398
405
399 c.pull_request = pull_request_display_obj
406 c.pull_request = pull_request_display_obj
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 c.pull_request_latest = pull_request_latest
408 c.pull_request_latest = pull_request_latest
402
409
403 # inject latest version
410 # inject latest version
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 c.versions = versions + [latest_ver]
412 c.versions = versions + [latest_ver]
406
413
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 c.allowed_to_change_status = False
415 c.allowed_to_change_status = False
409 c.allowed_to_update = False
416 c.allowed_to_update = False
410 c.allowed_to_merge = False
417 c.allowed_to_merge = False
411 c.allowed_to_delete = False
418 c.allowed_to_delete = False
412 c.allowed_to_comment = False
419 c.allowed_to_comment = False
413 c.allowed_to_close = False
420 c.allowed_to_close = False
414 else:
421 else:
415 can_change_status = PullRequestModel().check_user_change_status(
422 can_change_status = PullRequestModel().check_user_change_status(
416 pull_request_at_ver, self._rhodecode_user)
423 pull_request_at_ver, self._rhodecode_user)
417 c.allowed_to_change_status = can_change_status and not pr_closed
424 c.allowed_to_change_status = can_change_status and not pr_closed
418
425
419 c.allowed_to_update = PullRequestModel().check_user_update(
426 c.allowed_to_update = PullRequestModel().check_user_update(
420 pull_request_latest, self._rhodecode_user) and not pr_closed
427 pull_request_latest, self._rhodecode_user) and not pr_closed
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
428 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 pull_request_latest, self._rhodecode_user) and not pr_closed
429 pull_request_latest, self._rhodecode_user) and not pr_closed
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
430 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 pull_request_latest, self._rhodecode_user) and not pr_closed
431 pull_request_latest, self._rhodecode_user) and not pr_closed
425 c.allowed_to_comment = not pr_closed
432 c.allowed_to_comment = not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427
434
428 c.forbid_adding_reviewers = False
435 c.forbid_adding_reviewers = False
429
436
430 if pull_request_latest.reviewer_data and \
437 if pull_request_latest.reviewer_data and \
431 'rules' in pull_request_latest.reviewer_data:
438 'rules' in pull_request_latest.reviewer_data:
432 rules = pull_request_latest.reviewer_data['rules'] or {}
439 rules = pull_request_latest.reviewer_data['rules'] or {}
433 try:
440 try:
434 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
435 except Exception:
442 except Exception:
436 pass
443 pass
437
444
438 # check merge capabilities
445 # check merge capabilities
439 _merge_check = MergeCheck.validate(
446 _merge_check = MergeCheck.validate(
440 pull_request_latest, auth_user=self._rhodecode_user,
447 pull_request_latest, auth_user=self._rhodecode_user,
441 translator=self.request.translate,
448 translator=self.request.translate,
442 force_shadow_repo_refresh=force_refresh)
449 force_shadow_repo_refresh=force_refresh)
443
450
444 c.pr_merge_errors = _merge_check.error_details
451 c.pr_merge_errors = _merge_check.error_details
445 c.pr_merge_possible = not _merge_check.failed
452 c.pr_merge_possible = not _merge_check.failed
446 c.pr_merge_message = _merge_check.merge_msg
453 c.pr_merge_message = _merge_check.merge_msg
447 c.pr_merge_source_commit = _merge_check.source_commit
454 c.pr_merge_source_commit = _merge_check.source_commit
448 c.pr_merge_target_commit = _merge_check.target_commit
455 c.pr_merge_target_commit = _merge_check.target_commit
449
456
450 c.pr_merge_info = MergeCheck.get_merge_conditions(
457 c.pr_merge_info = MergeCheck.get_merge_conditions(
451 pull_request_latest, translator=self.request.translate)
458 pull_request_latest, translator=self.request.translate)
452
459
453 c.pull_request_review_status = _merge_check.review_status
460 c.pull_request_review_status = _merge_check.review_status
454 if merge_checks:
461 if merge_checks:
455 self.request.override_renderer = \
462 self.request.override_renderer = \
456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
457 return self._get_template_context(c)
464 return self._get_template_context(c)
458
465
459 c.reviewers_count = pull_request.reviewers_count
466 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
467 c.observers_count = pull_request.observers_count
461
468
462 # reviewers and statuses
469 # reviewers and statuses
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
470 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
466
473
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
468 member_reviewer = h.reviewer_as_json(
475 member_reviewer = h.reviewer_as_json(
469 member, reasons=reasons, mandatory=mandatory,
476 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
477 role=review_obj.role,
471 user_group=review_obj.rule_user_group_data()
478 user_group=review_obj.rule_user_group_data()
472 )
479 )
473
480
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
475 member_reviewer['review_status'] = current_review_status
482 member_reviewer['review_status'] = current_review_status
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
484 member_reviewer['allowed_to_update'] = c.allowed_to_update
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
479
486
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
487 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
481
488
482 for observer_obj, member in pull_request_at_ver.observers():
489 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
490 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
491 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
492 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
493 user_group=observer_obj.rule_user_group_data()
487 )
494 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
495 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
497
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
498 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
499
493 general_comments, inline_comments = \
500 general_comments, inline_comments = \
494 self.register_comments_vars(c, pull_request_latest, versions)
501 self.register_comments_vars(c, pull_request_latest, versions)
495
502
496 # TODOs
503 # TODOs
497 c.unresolved_comments = CommentsModel() \
504 c.unresolved_comments = CommentsModel() \
498 .get_pull_request_unresolved_todos(pull_request_latest)
505 .get_pull_request_unresolved_todos(pull_request_latest)
499 c.resolved_comments = CommentsModel() \
506 c.resolved_comments = CommentsModel() \
500 .get_pull_request_resolved_todos(pull_request_latest)
507 .get_pull_request_resolved_todos(pull_request_latest)
501
508
502 # Drafts
509 # Drafts
503 c.draft_comments = CommentsModel().get_pull_request_drafts(
510 c.draft_comments = CommentsModel().get_pull_request_drafts(
504 self._rhodecode_db_user.user_id,
511 self._rhodecode_db_user.user_id,
505 pull_request_latest)
512 pull_request_latest)
506
513
507 # if we use version, then do not show later comments
514 # if we use version, then do not show later comments
508 # than current version
515 # than current version
509 display_inline_comments = collections.defaultdict(
516 display_inline_comments = collections.defaultdict(
510 lambda: collections.defaultdict(list))
517 lambda: collections.defaultdict(list))
511 for co in inline_comments:
518 for co in inline_comments:
512 if c.at_version_num:
519 if c.at_version_num:
513 # pick comments that are at least UPTO given version, so we
520 # pick comments that are at least UPTO given version, so we
514 # don't render comments for higher version
521 # don't render comments for higher version
515 should_render = co.pull_request_version_id and \
522 should_render = co.pull_request_version_id and \
516 co.pull_request_version_id <= c.at_version_num
523 co.pull_request_version_id <= c.at_version_num
517 else:
524 else:
518 # showing all, for 'latest'
525 # showing all, for 'latest'
519 should_render = True
526 should_render = True
520
527
521 if should_render:
528 if should_render:
522 display_inline_comments[co.f_path][co.line_no].append(co)
529 display_inline_comments[co.f_path][co.line_no].append(co)
523
530
524 # load diff data into template context, if we use compare mode then
531 # load diff data into template context, if we use compare mode then
525 # diff is calculated based on changes between versions of PR
532 # diff is calculated based on changes between versions of PR
526
533
527 source_repo = pull_request_at_ver.source_repo
534 source_repo = pull_request_at_ver.source_repo
528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
529
536
530 target_repo = pull_request_at_ver.target_repo
537 target_repo = pull_request_at_ver.target_repo
531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
532
539
533 if compare:
540 if compare:
534 # in compare switch the diff base to latest commit from prev version
541 # in compare switch the diff base to latest commit from prev version
535 target_ref_id = prev_pull_request_display_obj.revisions[0]
542 target_ref_id = prev_pull_request_display_obj.revisions[0]
536
543
537 # despite opening commits for bookmarks/branches/tags, we always
544 # despite opening commits for bookmarks/branches/tags, we always
538 # convert this to rev to prevent changes after bookmark or branch change
545 # convert this to rev to prevent changes after bookmark or branch change
539 c.source_ref_type = 'rev'
546 c.source_ref_type = 'rev'
540 c.source_ref = source_ref_id
547 c.source_ref = source_ref_id
541
548
542 c.target_ref_type = 'rev'
549 c.target_ref_type = 'rev'
543 c.target_ref = target_ref_id
550 c.target_ref = target_ref_id
544
551
545 c.source_repo = source_repo
552 c.source_repo = source_repo
546 c.target_repo = target_repo
553 c.target_repo = target_repo
547
554
548 c.commit_ranges = []
555 c.commit_ranges = []
549 source_commit = EmptyCommit()
556 source_commit = EmptyCommit()
550 target_commit = EmptyCommit()
557 target_commit = EmptyCommit()
551 c.missing_requirements = False
558 c.missing_requirements = False
552
559
553 source_scm = source_repo.scm_instance()
560 source_scm = source_repo.scm_instance()
554 target_scm = target_repo.scm_instance()
561 target_scm = target_repo.scm_instance()
555
562
556 shadow_scm = None
563 shadow_scm = None
557 try:
564 try:
558 shadow_scm = pull_request_latest.get_shadow_repo()
565 shadow_scm = pull_request_latest.get_shadow_repo()
559 except Exception:
566 except Exception:
560 log.debug('Failed to get shadow repo', exc_info=True)
567 log.debug('Failed to get shadow repo', exc_info=True)
561 # try first the existing source_repo, and then shadow
568 # try first the existing source_repo, and then shadow
562 # repo if we can obtain one
569 # repo if we can obtain one
563 commits_source_repo = source_scm
570 commits_source_repo = source_scm
564 if shadow_scm:
571 if shadow_scm:
565 commits_source_repo = shadow_scm
572 commits_source_repo = shadow_scm
566
573
567 c.commits_source_repo = commits_source_repo
574 c.commits_source_repo = commits_source_repo
568 c.ancestor = None # set it to None, to hide it from PR view
575 c.ancestor = None # set it to None, to hide it from PR view
569
576
570 # empty version means latest, so we keep this to prevent
577 # empty version means latest, so we keep this to prevent
571 # double caching
578 # double caching
572 version_normalized = version or PullRequest.LATEST_VER
579 version_normalized = version or PullRequest.LATEST_VER
573 from_version_normalized = from_version or PullRequest.LATEST_VER
580 from_version_normalized = from_version or PullRequest.LATEST_VER
574
581
575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
576 cache_file_path = diff_cache_exist(
583 cache_file_path = diff_cache_exist(
577 cache_path, 'pull_request', pull_request_id, version_normalized,
584 cache_path, 'pull_request', pull_request_id, version_normalized,
578 from_version_normalized, source_ref_id, target_ref_id,
585 from_version_normalized, source_ref_id, target_ref_id,
579 hide_whitespace_changes, diff_context, c.fulldiff)
586 hide_whitespace_changes, diff_context, c.fulldiff)
580
587
581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
582 force_recache = self.get_recache_flag()
589 force_recache = self.get_recache_flag()
583
590
584 cached_diff = None
591 cached_diff = None
585 if caching_enabled:
592 if caching_enabled:
586 cached_diff = load_cached_diff(cache_file_path)
593 cached_diff = load_cached_diff(cache_file_path)
587
594
588 has_proper_commit_cache = (
595 has_proper_commit_cache = (
589 cached_diff and cached_diff.get('commits')
596 cached_diff and cached_diff.get('commits')
590 and len(cached_diff.get('commits', [])) == 5
597 and len(cached_diff.get('commits', [])) == 5
591 and cached_diff.get('commits')[0]
598 and cached_diff.get('commits')[0]
592 and cached_diff.get('commits')[3])
599 and cached_diff.get('commits')[3])
593
600
594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
595 diff_commit_cache = \
602 diff_commit_cache = \
596 (ancestor_commit, commit_cache, missing_requirements,
603 (ancestor_commit, commit_cache, missing_requirements,
597 source_commit, target_commit) = cached_diff['commits']
604 source_commit, target_commit) = cached_diff['commits']
598 else:
605 else:
599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
600 # merge errors resulting in potentially hidden commits in the shadow repo.
607 # merge errors resulting in potentially hidden commits in the shadow repo.
601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
602 and _merge_check.merge_response
609 and _merge_check.merge_response
603 maybe_unreachable = maybe_unreachable \
610 maybe_unreachable = maybe_unreachable \
604 and _merge_check.merge_response.metadata.get('unresolved_files')
611 and _merge_check.merge_response.metadata.get('unresolved_files')
605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
606 diff_commit_cache = \
613 diff_commit_cache = \
607 (ancestor_commit, commit_cache, missing_requirements,
614 (ancestor_commit, commit_cache, missing_requirements,
608 source_commit, target_commit) = self.get_commits(
615 source_commit, target_commit) = self.get_commits(
609 commits_source_repo,
616 commits_source_repo,
610 pull_request_at_ver,
617 pull_request_at_ver,
611 source_commit,
618 source_commit,
612 source_ref_id,
619 source_ref_id,
613 source_scm,
620 source_scm,
614 target_commit,
621 target_commit,
615 target_ref_id,
622 target_ref_id,
616 target_scm,
623 target_scm,
617 maybe_unreachable=maybe_unreachable)
624 maybe_unreachable=maybe_unreachable)
618
625
619 # register our commit range
626 # register our commit range
620 for comm in commit_cache.values():
627 for comm in commit_cache.values():
621 c.commit_ranges.append(comm)
628 c.commit_ranges.append(comm)
622
629
623 c.missing_requirements = missing_requirements
630 c.missing_requirements = missing_requirements
624 c.ancestor_commit = ancestor_commit
631 c.ancestor_commit = ancestor_commit
625 c.statuses = source_repo.statuses(
632 c.statuses = source_repo.statuses(
626 [x.raw_id for x in c.commit_ranges])
633 [x.raw_id for x in c.commit_ranges])
627
634
628 # auto collapse if we have more than limit
635 # auto collapse if we have more than limit
629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
631 c.compare_mode = compare
638 c.compare_mode = compare
632
639
633 # diff_limit is the old behavior, will cut off the whole diff
640 # diff_limit is the old behavior, will cut off the whole diff
634 # if the limit is applied otherwise will just hide the
641 # if the limit is applied otherwise will just hide the
635 # big files from the front-end
642 # big files from the front-end
636 diff_limit = c.visual.cut_off_limit_diff
643 diff_limit = c.visual.cut_off_limit_diff
637 file_limit = c.visual.cut_off_limit_file
644 file_limit = c.visual.cut_off_limit_file
638
645
639 c.missing_commits = False
646 c.missing_commits = False
640 if (c.missing_requirements
647 if (c.missing_requirements
641 or isinstance(source_commit, EmptyCommit)
648 or isinstance(source_commit, EmptyCommit)
642 or source_commit == target_commit):
649 or source_commit == target_commit):
643
650
644 c.missing_commits = True
651 c.missing_commits = True
645 else:
652 else:
646 c.inline_comments = display_inline_comments
653 c.inline_comments = display_inline_comments
647
654
648 use_ancestor = True
655 use_ancestor = True
649 if from_version_normalized != version_normalized:
656 if from_version_normalized != version_normalized:
650 use_ancestor = False
657 use_ancestor = False
651
658
652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
653 if not force_recache and has_proper_diff_cache:
660 if not force_recache and has_proper_diff_cache:
654 c.diffset = cached_diff['diff']
661 c.diffset = cached_diff['diff']
655 else:
662 else:
656 try:
663 try:
657 c.diffset = self._get_diffset(
664 c.diffset = self._get_diffset(
658 c.source_repo.repo_name, commits_source_repo,
665 c.source_repo.repo_name, commits_source_repo,
659 c.ancestor_commit,
666 c.ancestor_commit,
660 source_ref_id, target_ref_id,
667 source_ref_id, target_ref_id,
661 target_commit, source_commit,
668 target_commit, source_commit,
662 diff_limit, file_limit, c.fulldiff,
669 diff_limit, file_limit, c.fulldiff,
663 hide_whitespace_changes, diff_context,
670 hide_whitespace_changes, diff_context,
664 use_ancestor=use_ancestor
671 use_ancestor=use_ancestor
665 )
672 )
666
673
667 # save cached diff
674 # save cached diff
668 if caching_enabled:
675 if caching_enabled:
669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
670 except CommitDoesNotExistError:
677 except CommitDoesNotExistError:
671 log.exception('Failed to generate diffset')
678 log.exception('Failed to generate diffset')
672 c.missing_commits = True
679 c.missing_commits = True
673
680
674 if not c.missing_commits:
681 if not c.missing_commits:
675
682
676 c.limited_diff = c.diffset.limited_diff
683 c.limited_diff = c.diffset.limited_diff
677
684
678 # calculate removed files that are bound to comments
685 # calculate removed files that are bound to comments
679 comment_deleted_files = [
686 comment_deleted_files = [
680 fname for fname in display_inline_comments
687 fname for fname in display_inline_comments
681 if fname not in c.diffset.file_stats]
688 if fname not in c.diffset.file_stats]
682
689
683 c.deleted_files_comments = collections.defaultdict(dict)
690 c.deleted_files_comments = collections.defaultdict(dict)
684 for fname, per_line_comments in display_inline_comments.items():
691 for fname, per_line_comments in display_inline_comments.items():
685 if fname in comment_deleted_files:
692 if fname in comment_deleted_files:
686 c.deleted_files_comments[fname]['stats'] = 0
693 c.deleted_files_comments[fname]['stats'] = 0
687 c.deleted_files_comments[fname]['comments'] = list()
694 c.deleted_files_comments[fname]['comments'] = list()
688 for lno, comments in per_line_comments.items():
695 for lno, comments in per_line_comments.items():
689 c.deleted_files_comments[fname]['comments'].extend(comments)
696 c.deleted_files_comments[fname]['comments'].extend(comments)
690
697
691 # maybe calculate the range diff
698 # maybe calculate the range diff
692 if c.range_diff_on:
699 if c.range_diff_on:
693 # TODO(marcink): set whitespace/context
700 # TODO(marcink): set whitespace/context
694 context_lcl = 3
701 context_lcl = 3
695 ign_whitespace_lcl = False
702 ign_whitespace_lcl = False
696
703
697 for commit in c.commit_ranges:
704 for commit in c.commit_ranges:
698 commit2 = commit
705 commit2 = commit
699 commit1 = commit.first_parent
706 commit1 = commit.first_parent
700
707
701 range_diff_cache_file_path = diff_cache_exist(
708 range_diff_cache_file_path = diff_cache_exist(
702 cache_path, 'diff', commit.raw_id,
709 cache_path, 'diff', commit.raw_id,
703 ign_whitespace_lcl, context_lcl, c.fulldiff)
710 ign_whitespace_lcl, context_lcl, c.fulldiff)
704
711
705 cached_diff = None
712 cached_diff = None
706 if caching_enabled:
713 if caching_enabled:
707 cached_diff = load_cached_diff(range_diff_cache_file_path)
714 cached_diff = load_cached_diff(range_diff_cache_file_path)
708
715
709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
710 if not force_recache and has_proper_diff_cache:
717 if not force_recache and has_proper_diff_cache:
711 diffset = cached_diff['diff']
718 diffset = cached_diff['diff']
712 else:
719 else:
713 diffset = self._get_range_diffset(
720 diffset = self._get_range_diffset(
714 commits_source_repo, source_repo,
721 commits_source_repo, source_repo,
715 commit1, commit2, diff_limit, file_limit,
722 commit1, commit2, diff_limit, file_limit,
716 c.fulldiff, ign_whitespace_lcl, context_lcl
723 c.fulldiff, ign_whitespace_lcl, context_lcl
717 )
724 )
718
725
719 # save cached diff
726 # save cached diff
720 if caching_enabled:
727 if caching_enabled:
721 cache_diff(range_diff_cache_file_path, diffset, None)
728 cache_diff(range_diff_cache_file_path, diffset, None)
722
729
723 c.changes[commit.raw_id] = diffset
730 c.changes[commit.raw_id] = diffset
724
731
725 # this is a hack to properly display links, when creating PR, the
732 # this is a hack to properly display links, when creating PR, the
726 # compare view and others uses different notation, and
733 # compare view and others uses different notation, and
727 # compare_commits.mako renders links based on the target_repo.
734 # compare_commits.mako renders links based on the target_repo.
728 # We need to swap that here to generate it properly on the html side
735 # We need to swap that here to generate it properly on the html side
729 c.target_repo = c.source_repo
736 c.target_repo = c.source_repo
730
737
731 c.commit_statuses = ChangesetStatus.STATUSES
738 c.commit_statuses = ChangesetStatus.STATUSES
732
739
733 c.show_version_changes = not pr_closed
740 c.show_version_changes = not pr_closed
734 if c.show_version_changes:
741 if c.show_version_changes:
735 cur_obj = pull_request_at_ver
742 cur_obj = pull_request_at_ver
736 prev_obj = prev_pull_request_at_ver
743 prev_obj = prev_pull_request_at_ver
737
744
738 old_commit_ids = prev_obj.revisions
745 old_commit_ids = prev_obj.revisions
739 new_commit_ids = cur_obj.revisions
746 new_commit_ids = cur_obj.revisions
740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
741 old_commit_ids, new_commit_ids)
748 old_commit_ids, new_commit_ids)
742 c.commit_changes_summary = commit_changes
749 c.commit_changes_summary = commit_changes
743
750
744 # calculate the diff for commits between versions
751 # calculate the diff for commits between versions
745 c.commit_changes = []
752 c.commit_changes = []
746
753
747 def mark(cs, fw):
754 def mark(cs, fw):
748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
755 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
749
756
750 for c_type, raw_id in mark(commit_changes.added, 'a') \
757 for c_type, raw_id in mark(commit_changes.added, 'a') \
751 + mark(commit_changes.removed, 'r') \
758 + mark(commit_changes.removed, 'r') \
752 + mark(commit_changes.common, 'c'):
759 + mark(commit_changes.common, 'c'):
753
760
754 if raw_id in commit_cache:
761 if raw_id in commit_cache:
755 commit = commit_cache[raw_id]
762 commit = commit_cache[raw_id]
756 else:
763 else:
757 try:
764 try:
758 commit = commits_source_repo.get_commit(raw_id)
765 commit = commits_source_repo.get_commit(raw_id)
759 except CommitDoesNotExistError:
766 except CommitDoesNotExistError:
760 # in case we fail extracting still use "dummy" commit
767 # in case we fail extracting still use "dummy" commit
761 # for display in commit diff
768 # for display in commit diff
762 commit = h.AttributeDict(
769 commit = h.AttributeDict(
763 {'raw_id': raw_id,
770 {'raw_id': raw_id,
764 'message': 'EMPTY or MISSING COMMIT'})
771 'message': 'EMPTY or MISSING COMMIT'})
765 c.commit_changes.append([c_type, commit])
772 c.commit_changes.append([c_type, commit])
766
773
767 # current user review statuses for each version
774 # current user review statuses for each version
768 c.review_versions = {}
775 c.review_versions = {}
769 is_reviewer = PullRequestModel().is_user_reviewer(
776 is_reviewer = PullRequestModel().is_user_reviewer(
770 pull_request, self._rhodecode_user)
777 pull_request, self._rhodecode_user)
771 if is_reviewer:
778 if is_reviewer:
772 for co in general_comments:
779 for co in general_comments:
773 if co.author.user_id == self._rhodecode_user.user_id:
780 if co.author.user_id == self._rhodecode_user.user_id:
774 status = co.status_change
781 status = co.status_change
775 if status:
782 if status:
776 _ver_pr = status[0].comment.pull_request_version_id
783 _ver_pr = status[0].comment.pull_request_version_id
777 c.review_versions[_ver_pr] = status[0]
784 c.review_versions[_ver_pr] = status[0]
778
785
779 return self._get_template_context(c)
786 return self._get_template_context(c)
780
787
781 def get_commits(
788 def get_commits(
782 self, commits_source_repo, pull_request_at_ver, source_commit,
789 self, commits_source_repo, pull_request_at_ver, source_commit,
783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
784 maybe_unreachable=False):
791 maybe_unreachable=False):
785
792
786 commit_cache = collections.OrderedDict()
793 commit_cache = collections.OrderedDict()
787 missing_requirements = False
794 missing_requirements = False
788
795
789 try:
796 try:
790 pre_load = ["author", "date", "message", "branch", "parents"]
797 pre_load = ["author", "date", "message", "branch", "parents"]
791
798
792 pull_request_commits = pull_request_at_ver.revisions
799 pull_request_commits = pull_request_at_ver.revisions
793 log.debug('Loading %s commits from %s',
800 log.debug('Loading %s commits from %s',
794 len(pull_request_commits), commits_source_repo)
801 len(pull_request_commits), commits_source_repo)
795
802
796 for rev in pull_request_commits:
803 for rev in pull_request_commits:
797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
798 maybe_unreachable=maybe_unreachable)
805 maybe_unreachable=maybe_unreachable)
799 commit_cache[comm.raw_id] = comm
806 commit_cache[comm.raw_id] = comm
800
807
801 # Order here matters, we first need to get target, and then
808 # Order here matters, we first need to get target, and then
802 # the source
809 # the source
803 target_commit = commits_source_repo.get_commit(
810 target_commit = commits_source_repo.get_commit(
804 commit_id=safe_str(target_ref_id))
811 commit_id=safe_str(target_ref_id))
805
812
806 source_commit = commits_source_repo.get_commit(
813 source_commit = commits_source_repo.get_commit(
807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
808 except CommitDoesNotExistError:
815 except CommitDoesNotExistError:
809 log.warning('Failed to get commit from `{}` repo'.format(
816 log.warning('Failed to get commit from `{}` repo'.format(
810 commits_source_repo), exc_info=True)
817 commits_source_repo), exc_info=True)
811 except RepositoryRequirementError:
818 except RepositoryRequirementError:
812 log.warning('Failed to get all required data from repo', exc_info=True)
819 log.warning('Failed to get all required data from repo', exc_info=True)
813 missing_requirements = True
820 missing_requirements = True
814
821
815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
816
823
817 try:
824 try:
818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
819 except Exception:
826 except Exception:
820 ancestor_commit = None
827 ancestor_commit = None
821
828
822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
823
830
824 def assure_not_empty_repo(self):
831 def assure_not_empty_repo(self):
825 _ = self.request.translate
832 _ = self.request.translate
826
833
827 try:
834 try:
828 self.db_repo.scm_instance().get_commit()
835 self.db_repo.scm_instance().get_commit()
829 except EmptyRepositoryError:
836 except EmptyRepositoryError:
830 h.flash(h.literal(_('There are no commits yet')),
837 h.flash(h.literal(_('There are no commits yet')),
831 category='warning')
838 category='warning')
832 raise HTTPFound(
839 raise HTTPFound(
833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
834
841
835 @LoginRequired()
842 @LoginRequired()
836 @NotAnonymous()
843 @NotAnonymous()
837 @HasRepoPermissionAnyDecorator(
844 @HasRepoPermissionAnyDecorator(
838 'repository.read', 'repository.write', 'repository.admin')
845 'repository.read', 'repository.write', 'repository.admin')
839 def pull_request_new(self):
846 def pull_request_new(self):
840 _ = self.request.translate
847 _ = self.request.translate
841 c = self.load_default_context()
848 c = self.load_default_context()
842
849
843 self.assure_not_empty_repo()
850 self.assure_not_empty_repo()
844 source_repo = self.db_repo
851 source_repo = self.db_repo
845
852
846 commit_id = self.request.GET.get('commit')
853 commit_id = self.request.GET.get('commit')
847 branch_ref = self.request.GET.get('branch')
854 branch_ref = self.request.GET.get('branch')
848 bookmark_ref = self.request.GET.get('bookmark')
855 bookmark_ref = self.request.GET.get('bookmark')
849
856
850 try:
857 try:
851 source_repo_data = PullRequestModel().generate_repo_data(
858 source_repo_data = PullRequestModel().generate_repo_data(
852 source_repo, commit_id=commit_id,
859 source_repo, commit_id=commit_id,
853 branch=branch_ref, bookmark=bookmark_ref,
860 branch=branch_ref, bookmark=bookmark_ref,
854 translator=self.request.translate)
861 translator=self.request.translate)
855 except CommitDoesNotExistError as e:
862 except CommitDoesNotExistError as e:
856 log.exception(e)
863 log.exception(e)
857 h.flash(_('Commit does not exist'), 'error')
864 h.flash(_('Commit does not exist'), 'error')
858 raise HTTPFound(
865 raise HTTPFound(
859 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
860
867
861 default_target_repo = source_repo
868 default_target_repo = source_repo
862
869
863 if source_repo.parent and c.has_origin_repo_read_perm:
870 if source_repo.parent and c.has_origin_repo_read_perm:
864 parent_vcs_obj = source_repo.parent.scm_instance()
871 parent_vcs_obj = source_repo.parent.scm_instance()
865 if parent_vcs_obj and not parent_vcs_obj.is_empty():
872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
866 # change default if we have a parent repo
873 # change default if we have a parent repo
867 default_target_repo = source_repo.parent
874 default_target_repo = source_repo.parent
868
875
869 target_repo_data = PullRequestModel().generate_repo_data(
876 target_repo_data = PullRequestModel().generate_repo_data(
870 default_target_repo, translator=self.request.translate)
877 default_target_repo, translator=self.request.translate)
871
878
872 selected_source_ref = source_repo_data['refs']['selected_ref']
879 selected_source_ref = source_repo_data['refs']['selected_ref']
873 title_source_ref = ''
880 title_source_ref = ''
874 if selected_source_ref:
881 if selected_source_ref:
875 title_source_ref = selected_source_ref.split(':', 2)[1]
882 title_source_ref = selected_source_ref.split(':', 2)[1]
876 c.default_title = PullRequestModel().generate_pullrequest_title(
883 c.default_title = PullRequestModel().generate_pullrequest_title(
877 source=source_repo.repo_name,
884 source=source_repo.repo_name,
878 source_ref=title_source_ref,
885 source_ref=title_source_ref,
879 target=default_target_repo.repo_name
886 target=default_target_repo.repo_name
880 )
887 )
881
888
882 c.default_repo_data = {
889 c.default_repo_data = {
883 'source_repo_name': source_repo.repo_name,
890 'source_repo_name': source_repo.repo_name,
884 'source_refs_json': json.dumps(source_repo_data),
891 'source_refs_json': json.dumps(source_repo_data),
885 'target_repo_name': default_target_repo.repo_name,
892 'target_repo_name': default_target_repo.repo_name,
886 'target_refs_json': json.dumps(target_repo_data),
893 'target_refs_json': json.dumps(target_repo_data),
887 }
894 }
888 c.default_source_ref = selected_source_ref
895 c.default_source_ref = selected_source_ref
889
896
890 return self._get_template_context(c)
897 return self._get_template_context(c)
891
898
892 @LoginRequired()
899 @LoginRequired()
893 @NotAnonymous()
900 @NotAnonymous()
894 @HasRepoPermissionAnyDecorator(
901 @HasRepoPermissionAnyDecorator(
895 'repository.read', 'repository.write', 'repository.admin')
902 'repository.read', 'repository.write', 'repository.admin')
896 def pull_request_repo_refs(self):
903 def pull_request_repo_refs(self):
897 self.load_default_context()
904 self.load_default_context()
898 target_repo_name = self.request.matchdict['target_repo_name']
905 target_repo_name = self.request.matchdict['target_repo_name']
899 repo = Repository.get_by_repo_name(target_repo_name)
906 repo = Repository.get_by_repo_name(target_repo_name)
900 if not repo:
907 if not repo:
901 raise HTTPNotFound()
908 raise HTTPNotFound()
902
909
903 target_perm = HasRepoPermissionAny(
910 target_perm = HasRepoPermissionAny(
904 'repository.read', 'repository.write', 'repository.admin')(
911 'repository.read', 'repository.write', 'repository.admin')(
905 target_repo_name)
912 target_repo_name)
906 if not target_perm:
913 if not target_perm:
907 raise HTTPNotFound()
914 raise HTTPNotFound()
908
915
909 return PullRequestModel().generate_repo_data(
916 return PullRequestModel().generate_repo_data(
910 repo, translator=self.request.translate)
917 repo, translator=self.request.translate)
911
918
912 @LoginRequired()
919 @LoginRequired()
913 @NotAnonymous()
920 @NotAnonymous()
914 @HasRepoPermissionAnyDecorator(
921 @HasRepoPermissionAnyDecorator(
915 'repository.read', 'repository.write', 'repository.admin')
922 'repository.read', 'repository.write', 'repository.admin')
916 def pullrequest_repo_targets(self):
923 def pullrequest_repo_targets(self):
917 _ = self.request.translate
924 _ = self.request.translate
918 filter_query = self.request.GET.get('query')
925 filter_query = self.request.GET.get('query')
919
926
920 # get the parents
927 # get the parents
921 parent_target_repos = []
928 parent_target_repos = []
922 if self.db_repo.parent:
929 if self.db_repo.parent:
923 parents_query = Repository.query() \
930 parents_query = Repository.query() \
924 .order_by(func.length(Repository.repo_name)) \
931 .order_by(func.length(Repository.repo_name)) \
925 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
926
933
927 if filter_query:
934 if filter_query:
928 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
935 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
929 parents_query = parents_query.filter(
936 parents_query = parents_query.filter(
930 Repository.repo_name.ilike(ilike_expression))
937 Repository.repo_name.ilike(ilike_expression))
931 parents = parents_query.limit(20).all()
938 parents = parents_query.limit(20).all()
932
939
933 for parent in parents:
940 for parent in parents:
934 parent_vcs_obj = parent.scm_instance()
941 parent_vcs_obj = parent.scm_instance()
935 if parent_vcs_obj and not parent_vcs_obj.is_empty():
942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
936 parent_target_repos.append(parent)
943 parent_target_repos.append(parent)
937
944
938 # get other forks, and repo itself
945 # get other forks, and repo itself
939 query = Repository.query() \
946 query = Repository.query() \
940 .order_by(func.length(Repository.repo_name)) \
947 .order_by(func.length(Repository.repo_name)) \
941 .filter(
948 .filter(
942 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
943 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
944 ) \
951 ) \
945 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
946
953
947 if filter_query:
954 if filter_query:
948 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
955 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
949 query = query.filter(Repository.repo_name.ilike(ilike_expression))
956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
950
957
951 limit = max(20 - len(parent_target_repos), 5) # not less then 5
958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
952 target_repos = query.limit(limit).all()
959 target_repos = query.limit(limit).all()
953
960
954 all_target_repos = target_repos + parent_target_repos
961 all_target_repos = target_repos + parent_target_repos
955
962
956 repos = []
963 repos = []
957 # This checks permissions to the repositories
964 # This checks permissions to the repositories
958 for obj in ScmModel().get_repos(all_target_repos):
965 for obj in ScmModel().get_repos(all_target_repos):
959 repos.append({
966 repos.append({
960 'id': obj['name'],
967 'id': obj['name'],
961 'text': obj['name'],
968 'text': obj['name'],
962 'type': 'repo',
969 'type': 'repo',
963 'repo_id': obj['dbrepo']['repo_id'],
970 'repo_id': obj['dbrepo']['repo_id'],
964 'repo_type': obj['dbrepo']['repo_type'],
971 'repo_type': obj['dbrepo']['repo_type'],
965 'private': obj['dbrepo']['private'],
972 'private': obj['dbrepo']['private'],
966
973
967 })
974 })
968
975
969 data = {
976 data = {
970 'more': False,
977 'more': False,
971 'results': [{
978 'results': [{
972 'text': _('Repositories'),
979 'text': _('Repositories'),
973 'children': repos
980 'children': repos
974 }] if repos else []
981 }] if repos else []
975 }
982 }
976 return data
983 return data
977
984
978 @classmethod
985 @classmethod
979 def get_comment_ids(cls, post_data):
986 def get_comment_ids(cls, post_data):
980 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
981
988
982 @LoginRequired()
989 @LoginRequired()
983 @NotAnonymous()
990 @NotAnonymous()
984 @HasRepoPermissionAnyDecorator(
991 @HasRepoPermissionAnyDecorator(
985 'repository.read', 'repository.write', 'repository.admin')
992 'repository.read', 'repository.write', 'repository.admin')
986 def pullrequest_comments(self):
993 def pullrequest_comments(self):
987 self.load_default_context()
994 self.load_default_context()
988
995
989 pull_request = PullRequest.get_or_404(
996 pull_request = PullRequest.get_or_404(
990 self.request.matchdict['pull_request_id'])
997 self.request.matchdict['pull_request_id'])
991 pull_request_id = pull_request.pull_request_id
998 pull_request_id = pull_request.pull_request_id
992 version = self.request.GET.get('version')
999 version = self.request.GET.get('version')
993
1000
994 _render = self.request.get_partial_renderer(
1001 _render = self.request.get_partial_renderer(
995 'rhodecode:templates/base/sidebar.mako')
1002 'rhodecode:templates/base/sidebar.mako')
996 c = _render.get_call_context()
1003 c = _render.get_call_context()
997
1004
998 (pull_request_latest,
1005 (pull_request_latest,
999 pull_request_at_ver,
1006 pull_request_at_ver,
1000 pull_request_display_obj,
1007 pull_request_display_obj,
1001 at_version) = PullRequestModel().get_pr_version(
1008 at_version) = PullRequestModel().get_pr_version(
1002 pull_request_id, version=version)
1009 pull_request_id, version=version)
1003 versions = pull_request_display_obj.versions()
1010 versions = pull_request_display_obj.versions()
1004 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1005 c.versions = versions + [latest_ver]
1012 c.versions = versions + [latest_ver]
1006
1013
1007 c.at_version = at_version
1014 c.at_version = at_version
1008 c.at_version_num = (at_version
1015 c.at_version_num = (at_version
1009 if at_version and at_version != PullRequest.LATEST_VER
1016 if at_version and at_version != PullRequest.LATEST_VER
1010 else None)
1017 else None)
1011
1018
1012 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1013 all_comments = c.inline_comments_flat + c.comments
1020 all_comments = c.inline_comments_flat + c.comments
1014
1021
1015 existing_ids = self.get_comment_ids(self.request.POST)
1022 existing_ids = self.get_comment_ids(self.request.POST)
1016 return _render('comments_table', all_comments, len(all_comments),
1023 return _render('comments_table', all_comments, len(all_comments),
1017 existing_ids=existing_ids)
1024 existing_ids=existing_ids)
1018
1025
1019 @LoginRequired()
1026 @LoginRequired()
1020 @NotAnonymous()
1027 @NotAnonymous()
1021 @HasRepoPermissionAnyDecorator(
1028 @HasRepoPermissionAnyDecorator(
1022 'repository.read', 'repository.write', 'repository.admin')
1029 'repository.read', 'repository.write', 'repository.admin')
1023 def pullrequest_todos(self):
1030 def pullrequest_todos(self):
1024 self.load_default_context()
1031 self.load_default_context()
1025
1032
1026 pull_request = PullRequest.get_or_404(
1033 pull_request = PullRequest.get_or_404(
1027 self.request.matchdict['pull_request_id'])
1034 self.request.matchdict['pull_request_id'])
1028 pull_request_id = pull_request.pull_request_id
1035 pull_request_id = pull_request.pull_request_id
1029 version = self.request.GET.get('version')
1036 version = self.request.GET.get('version')
1030
1037
1031 _render = self.request.get_partial_renderer(
1038 _render = self.request.get_partial_renderer(
1032 'rhodecode:templates/base/sidebar.mako')
1039 'rhodecode:templates/base/sidebar.mako')
1033 c = _render.get_call_context()
1040 c = _render.get_call_context()
1034 (pull_request_latest,
1041 (pull_request_latest,
1035 pull_request_at_ver,
1042 pull_request_at_ver,
1036 pull_request_display_obj,
1043 pull_request_display_obj,
1037 at_version) = PullRequestModel().get_pr_version(
1044 at_version) = PullRequestModel().get_pr_version(
1038 pull_request_id, version=version)
1045 pull_request_id, version=version)
1039 versions = pull_request_display_obj.versions()
1046 versions = pull_request_display_obj.versions()
1040 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1041 c.versions = versions + [latest_ver]
1048 c.versions = versions + [latest_ver]
1042
1049
1043 c.at_version = at_version
1050 c.at_version = at_version
1044 c.at_version_num = (at_version
1051 c.at_version_num = (at_version
1045 if at_version and at_version != PullRequest.LATEST_VER
1052 if at_version and at_version != PullRequest.LATEST_VER
1046 else None)
1053 else None)
1047
1054
1048 c.unresolved_comments = CommentsModel() \
1055 c.unresolved_comments = CommentsModel() \
1049 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1050 c.resolved_comments = CommentsModel() \
1057 c.resolved_comments = CommentsModel() \
1051 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1052
1059
1053 all_comments = c.unresolved_comments + c.resolved_comments
1060 all_comments = c.unresolved_comments + c.resolved_comments
1054 existing_ids = self.get_comment_ids(self.request.POST)
1061 existing_ids = self.get_comment_ids(self.request.POST)
1055 return _render('comments_table', all_comments, len(c.unresolved_comments),
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1056 todo_comments=True, existing_ids=existing_ids)
1063 todo_comments=True, existing_ids=existing_ids)
1057
1064
1058 @LoginRequired()
1065 @LoginRequired()
1059 @NotAnonymous()
1066 @NotAnonymous()
1060 @HasRepoPermissionAnyDecorator(
1067 @HasRepoPermissionAnyDecorator(
1061 'repository.read', 'repository.write', 'repository.admin')
1068 'repository.read', 'repository.write', 'repository.admin')
1062 def pullrequest_drafts(self):
1069 def pullrequest_drafts(self):
1063 self.load_default_context()
1070 self.load_default_context()
1064
1071
1065 pull_request = PullRequest.get_or_404(
1072 pull_request = PullRequest.get_or_404(
1066 self.request.matchdict['pull_request_id'])
1073 self.request.matchdict['pull_request_id'])
1067 pull_request_id = pull_request.pull_request_id
1074 pull_request_id = pull_request.pull_request_id
1068 version = self.request.GET.get('version')
1075 version = self.request.GET.get('version')
1069
1076
1070 _render = self.request.get_partial_renderer(
1077 _render = self.request.get_partial_renderer(
1071 'rhodecode:templates/base/sidebar.mako')
1078 'rhodecode:templates/base/sidebar.mako')
1072 c = _render.get_call_context()
1079 c = _render.get_call_context()
1073
1080
1074 (pull_request_latest,
1081 (pull_request_latest,
1075 pull_request_at_ver,
1082 pull_request_at_ver,
1076 pull_request_display_obj,
1083 pull_request_display_obj,
1077 at_version) = PullRequestModel().get_pr_version(
1084 at_version) = PullRequestModel().get_pr_version(
1078 pull_request_id, version=version)
1085 pull_request_id, version=version)
1079 versions = pull_request_display_obj.versions()
1086 versions = pull_request_display_obj.versions()
1080 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1081 c.versions = versions + [latest_ver]
1088 c.versions = versions + [latest_ver]
1082
1089
1083 c.at_version = at_version
1090 c.at_version = at_version
1084 c.at_version_num = (at_version
1091 c.at_version_num = (at_version
1085 if at_version and at_version != PullRequest.LATEST_VER
1092 if at_version and at_version != PullRequest.LATEST_VER
1086 else None)
1093 else None)
1087
1094
1088 c.draft_comments = CommentsModel() \
1095 c.draft_comments = CommentsModel() \
1089 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1090
1097
1091 all_comments = c.draft_comments
1098 all_comments = c.draft_comments
1092
1099
1093 existing_ids = self.get_comment_ids(self.request.POST)
1100 existing_ids = self.get_comment_ids(self.request.POST)
1094 return _render('comments_table', all_comments, len(all_comments),
1101 return _render('comments_table', all_comments, len(all_comments),
1095 existing_ids=existing_ids, draft_comments=True)
1102 existing_ids=existing_ids, draft_comments=True)
1096
1103
1097 @LoginRequired()
1104 @LoginRequired()
1098 @NotAnonymous()
1105 @NotAnonymous()
1099 @HasRepoPermissionAnyDecorator(
1106 @HasRepoPermissionAnyDecorator(
1100 'repository.read', 'repository.write', 'repository.admin')
1107 'repository.read', 'repository.write', 'repository.admin')
1101 @CSRFRequired()
1108 @CSRFRequired()
1102 def pull_request_create(self):
1109 def pull_request_create(self):
1103 _ = self.request.translate
1110 _ = self.request.translate
1104 self.assure_not_empty_repo()
1111 self.assure_not_empty_repo()
1105 self.load_default_context()
1112 self.load_default_context()
1106
1113
1107 controls = peppercorn.parse(self.request.POST.items())
1114 controls = peppercorn.parse(self.request.POST.items())
1108
1115
1109 try:
1116 try:
1110 form = PullRequestForm(
1117 form = PullRequestForm(
1111 self.request.translate, self.db_repo.repo_id)()
1118 self.request.translate, self.db_repo.repo_id)()
1112 _form = form.to_python(controls)
1119 _form = form.to_python(controls)
1113 except formencode.Invalid as errors:
1120 except formencode.Invalid as errors:
1114 if errors.error_dict.get('revisions'):
1121 if errors.error_dict.get('revisions'):
1115 msg = 'Revisions: %s' % errors.error_dict['revisions']
1122 msg = 'Revisions: %s' % errors.error_dict['revisions']
1116 elif errors.error_dict.get('pullrequest_title'):
1123 elif errors.error_dict.get('pullrequest_title'):
1117 msg = errors.error_dict.get('pullrequest_title')
1124 msg = errors.error_dict.get('pullrequest_title')
1118 else:
1125 else:
1119 msg = _('Error creating pull request: {}').format(errors)
1126 msg = _('Error creating pull request: {}').format(errors)
1120 log.exception(msg)
1127 log.exception(msg)
1121 h.flash(msg, 'error')
1128 h.flash(msg, 'error')
1122
1129
1123 # would rather just go back to form ...
1130 # would rather just go back to form ...
1124 raise HTTPFound(
1131 raise HTTPFound(
1125 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1126
1133
1127 source_repo = _form['source_repo']
1134 source_repo = _form['source_repo']
1128 source_ref = _form['source_ref']
1135 source_ref = _form['source_ref']
1129 target_repo = _form['target_repo']
1136 target_repo = _form['target_repo']
1130 target_ref = _form['target_ref']
1137 target_ref = _form['target_ref']
1131 commit_ids = _form['revisions'][::-1]
1138 commit_ids = _form['revisions'][::-1]
1132 common_ancestor_id = _form['common_ancestor']
1139 common_ancestor_id = _form['common_ancestor']
1133
1140
1134 # find the ancestor for this pr
1141 # find the ancestor for this pr
1135 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1136 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1137
1144
1138 if not (source_db_repo or target_db_repo):
1145 if not (source_db_repo or target_db_repo):
1139 h.flash(_('source_repo or target repo not found'), category='error')
1146 h.flash(_('source_repo or target repo not found'), category='error')
1140 raise HTTPFound(
1147 raise HTTPFound(
1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1142
1149
1143 # re-check permissions again here
1150 # re-check permissions again here
1144 # source_repo we must have read permissions
1151 # source_repo we must have read permissions
1145
1152
1146 source_perm = HasRepoPermissionAny(
1153 source_perm = HasRepoPermissionAny(
1147 'repository.read', 'repository.write', 'repository.admin')(
1154 'repository.read', 'repository.write', 'repository.admin')(
1148 source_db_repo.repo_name)
1155 source_db_repo.repo_name)
1149 if not source_perm:
1156 if not source_perm:
1150 msg = _('Not Enough permissions to source repo `{}`.'.format(
1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1151 source_db_repo.repo_name))
1158 source_db_repo.repo_name))
1152 h.flash(msg, category='error')
1159 h.flash(msg, category='error')
1153 # copy the args back to redirect
1160 # copy the args back to redirect
1154 org_query = self.request.GET.mixed()
1161 org_query = self.request.GET.mixed()
1155 raise HTTPFound(
1162 raise HTTPFound(
1156 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1157 _query=org_query))
1164 _query=org_query))
1158
1165
1159 # target repo we must have read permissions, and also later on
1166 # target repo we must have read permissions, and also later on
1160 # we want to check branch permissions here
1167 # we want to check branch permissions here
1161 target_perm = HasRepoPermissionAny(
1168 target_perm = HasRepoPermissionAny(
1162 'repository.read', 'repository.write', 'repository.admin')(
1169 'repository.read', 'repository.write', 'repository.admin')(
1163 target_db_repo.repo_name)
1170 target_db_repo.repo_name)
1164 if not target_perm:
1171 if not target_perm:
1165 msg = _('Not Enough permissions to target repo `{}`.'.format(
1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1166 target_db_repo.repo_name))
1173 target_db_repo.repo_name))
1167 h.flash(msg, category='error')
1174 h.flash(msg, category='error')
1168 # copy the args back to redirect
1175 # copy the args back to redirect
1169 org_query = self.request.GET.mixed()
1176 org_query = self.request.GET.mixed()
1170 raise HTTPFound(
1177 raise HTTPFound(
1171 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1172 _query=org_query))
1179 _query=org_query))
1173
1180
1174 source_scm = source_db_repo.scm_instance()
1181 source_scm = source_db_repo.scm_instance()
1175 target_scm = target_db_repo.scm_instance()
1182 target_scm = target_db_repo.scm_instance()
1176
1183
1177 source_ref_obj = unicode_to_reference(source_ref)
1184 source_ref_obj = unicode_to_reference(source_ref)
1178 target_ref_obj = unicode_to_reference(target_ref)
1185 target_ref_obj = unicode_to_reference(target_ref)
1179
1186
1180 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1181 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1182
1189
1183 ancestor = source_scm.get_common_ancestor(
1190 ancestor = source_scm.get_common_ancestor(
1184 source_commit.raw_id, target_commit.raw_id, target_scm)
1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1185
1192
1186 # recalculate target ref based on ancestor
1193 # recalculate target ref based on ancestor
1187 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1188
1195
1189 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1190 PullRequestModel().get_reviewer_functions()
1197 PullRequestModel().get_reviewer_functions()
1191
1198
1192 # recalculate reviewers logic, to make sure we can validate this
1199 # recalculate reviewers logic, to make sure we can validate this
1193 reviewer_rules = get_default_reviewers_data(
1200 reviewer_rules = get_default_reviewers_data(
1194 self._rhodecode_db_user,
1201 self._rhodecode_db_user,
1195 source_db_repo,
1202 source_db_repo,
1196 source_ref_obj,
1203 source_ref_obj,
1197 target_db_repo,
1204 target_db_repo,
1198 target_ref_obj,
1205 target_ref_obj,
1199 include_diff_info=False)
1206 include_diff_info=False)
1200
1207
1201 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1202 observers = validate_observers(_form['observer_members'], reviewer_rules)
1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1203
1210
1204 pullrequest_title = _form['pullrequest_title']
1211 pullrequest_title = _form['pullrequest_title']
1205 title_source_ref = source_ref_obj.name
1212 title_source_ref = source_ref_obj.name
1206 if not pullrequest_title:
1213 if not pullrequest_title:
1207 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1208 source=source_repo,
1215 source=source_repo,
1209 source_ref=title_source_ref,
1216 source_ref=title_source_ref,
1210 target=target_repo
1217 target=target_repo
1211 )
1218 )
1212
1219
1213 description = _form['pullrequest_desc']
1220 description = _form['pullrequest_desc']
1214 description_renderer = _form['description_renderer']
1221 description_renderer = _form['description_renderer']
1215
1222
1216 try:
1223 try:
1217 pull_request = PullRequestModel().create(
1224 pull_request = PullRequestModel().create(
1218 created_by=self._rhodecode_user.user_id,
1225 created_by=self._rhodecode_user.user_id,
1219 source_repo=source_repo,
1226 source_repo=source_repo,
1220 source_ref=source_ref,
1227 source_ref=source_ref,
1221 target_repo=target_repo,
1228 target_repo=target_repo,
1222 target_ref=target_ref,
1229 target_ref=target_ref,
1223 revisions=commit_ids,
1230 revisions=commit_ids,
1224 common_ancestor_id=common_ancestor_id,
1231 common_ancestor_id=common_ancestor_id,
1225 reviewers=reviewers,
1232 reviewers=reviewers,
1226 observers=observers,
1233 observers=observers,
1227 title=pullrequest_title,
1234 title=pullrequest_title,
1228 description=description,
1235 description=description,
1229 description_renderer=description_renderer,
1236 description_renderer=description_renderer,
1230 reviewer_data=reviewer_rules,
1237 reviewer_data=reviewer_rules,
1231 auth_user=self._rhodecode_user
1238 auth_user=self._rhodecode_user
1232 )
1239 )
1233 Session().commit()
1240 Session().commit()
1234
1241
1235 h.flash(_('Successfully opened new pull request'),
1242 h.flash(_('Successfully opened new pull request'),
1236 category='success')
1243 category='success')
1237 except Exception:
1244 except Exception:
1238 msg = _('Error occurred during creation of this pull request.')
1245 msg = _('Error occurred during creation of this pull request.')
1239 log.exception(msg)
1246 log.exception(msg)
1240 h.flash(msg, category='error')
1247 h.flash(msg, category='error')
1241
1248
1242 # copy the args back to redirect
1249 # copy the args back to redirect
1243 org_query = self.request.GET.mixed()
1250 org_query = self.request.GET.mixed()
1244 raise HTTPFound(
1251 raise HTTPFound(
1245 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1246 _query=org_query))
1253 _query=org_query))
1247
1254
1248 raise HTTPFound(
1255 raise HTTPFound(
1249 h.route_path('pullrequest_show', repo_name=target_repo,
1256 h.route_path('pullrequest_show', repo_name=target_repo,
1250 pull_request_id=pull_request.pull_request_id))
1257 pull_request_id=pull_request.pull_request_id))
1251
1258
1252 @LoginRequired()
1259 @LoginRequired()
1253 @NotAnonymous()
1260 @NotAnonymous()
1254 @HasRepoPermissionAnyDecorator(
1261 @HasRepoPermissionAnyDecorator(
1255 'repository.read', 'repository.write', 'repository.admin')
1262 'repository.read', 'repository.write', 'repository.admin')
1256 @CSRFRequired()
1263 @CSRFRequired()
1257 def pull_request_update(self):
1264 def pull_request_update(self):
1258 pull_request = PullRequest.get_or_404(
1265 pull_request = PullRequest.get_or_404(
1259 self.request.matchdict['pull_request_id'])
1266 self.request.matchdict['pull_request_id'])
1260 _ = self.request.translate
1267 _ = self.request.translate
1261
1268
1262 c = self.load_default_context()
1269 c = self.load_default_context()
1263 redirect_url = None
1270 redirect_url = None
1264
1271
1265 if pull_request.is_closed():
1272 if pull_request.is_closed():
1266 log.debug('update: forbidden because pull request is closed')
1273 log.debug('update: forbidden because pull request is closed')
1267 msg = _(u'Cannot update closed pull requests.')
1274 msg = _(u'Cannot update closed pull requests.')
1268 h.flash(msg, category='error')
1275 h.flash(msg, category='error')
1269 return {'response': True,
1276 return {'response': True,
1270 'redirect_url': redirect_url}
1277 'redirect_url': redirect_url}
1271
1278
1272 is_state_changing = pull_request.is_state_changing()
1279 is_state_changing = pull_request.is_state_changing()
1273 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1274
1281
1275 # only owner or admin can update it
1282 # only owner or admin can update it
1276 allowed_to_update = PullRequestModel().check_user_update(
1283 allowed_to_update = PullRequestModel().check_user_update(
1277 pull_request, self._rhodecode_user)
1284 pull_request, self._rhodecode_user)
1278
1285
1279 if allowed_to_update:
1286 if allowed_to_update:
1280 controls = peppercorn.parse(self.request.POST.items())
1287 controls = peppercorn.parse(self.request.POST.items())
1281 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1282
1289
1283 if 'review_members' in controls:
1290 if 'review_members' in controls:
1284 self._update_reviewers(
1291 self._update_reviewers(
1285 c,
1292 c,
1286 pull_request, controls['review_members'],
1293 pull_request, controls['review_members'],
1287 pull_request.reviewer_data,
1294 pull_request.reviewer_data,
1288 PullRequestReviewers.ROLE_REVIEWER)
1295 PullRequestReviewers.ROLE_REVIEWER)
1289 elif 'observer_members' in controls:
1296 elif 'observer_members' in controls:
1290 self._update_reviewers(
1297 self._update_reviewers(
1291 c,
1298 c,
1292 pull_request, controls['observer_members'],
1299 pull_request, controls['observer_members'],
1293 pull_request.reviewer_data,
1300 pull_request.reviewer_data,
1294 PullRequestReviewers.ROLE_OBSERVER)
1301 PullRequestReviewers.ROLE_OBSERVER)
1295 elif str2bool(self.request.POST.get('update_commits', 'false')):
1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1296 if is_state_changing:
1303 if is_state_changing:
1297 log.debug('commits update: forbidden because pull request is in state %s',
1304 log.debug('commits update: forbidden because pull request is in state %s',
1298 pull_request.pull_request_state)
1305 pull_request.pull_request_state)
1299 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1306 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1300 u'Current state is: `{}`').format(
1307 u'Current state is: `{}`').format(
1301 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1308 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1302 h.flash(msg, category='error')
1309 h.flash(msg, category='error')
1303 return {'response': True,
1310 return {'response': True,
1304 'redirect_url': redirect_url}
1311 'redirect_url': redirect_url}
1305
1312
1306 self._update_commits(c, pull_request)
1313 self._update_commits(c, pull_request)
1307 if force_refresh:
1314 if force_refresh:
1308 redirect_url = h.route_path(
1315 redirect_url = h.route_path(
1309 'pullrequest_show', repo_name=self.db_repo_name,
1316 'pullrequest_show', repo_name=self.db_repo_name,
1310 pull_request_id=pull_request.pull_request_id,
1317 pull_request_id=pull_request.pull_request_id,
1311 _query={"force_refresh": 1})
1318 _query={"force_refresh": 1})
1312 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1319 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1313 self._edit_pull_request(pull_request)
1320 self._edit_pull_request(pull_request)
1314 else:
1321 else:
1315 log.error('Unhandled update data.')
1322 log.error('Unhandled update data.')
1316 raise HTTPBadRequest()
1323 raise HTTPBadRequest()
1317
1324
1318 return {'response': True,
1325 return {'response': True,
1319 'redirect_url': redirect_url}
1326 'redirect_url': redirect_url}
1320 raise HTTPForbidden()
1327 raise HTTPForbidden()
1321
1328
1322 def _edit_pull_request(self, pull_request):
1329 def _edit_pull_request(self, pull_request):
1323 """
1330 """
1324 Edit title and description
1331 Edit title and description
1325 """
1332 """
1326 _ = self.request.translate
1333 _ = self.request.translate
1327
1334
1328 try:
1335 try:
1329 PullRequestModel().edit(
1336 PullRequestModel().edit(
1330 pull_request,
1337 pull_request,
1331 self.request.POST.get('title'),
1338 self.request.POST.get('title'),
1332 self.request.POST.get('description'),
1339 self.request.POST.get('description'),
1333 self.request.POST.get('description_renderer'),
1340 self.request.POST.get('description_renderer'),
1334 self._rhodecode_user)
1341 self._rhodecode_user)
1335 except ValueError:
1342 except ValueError:
1336 msg = _(u'Cannot update closed pull requests.')
1343 msg = _(u'Cannot update closed pull requests.')
1337 h.flash(msg, category='error')
1344 h.flash(msg, category='error')
1338 return
1345 return
1339 else:
1346 else:
1340 Session().commit()
1347 Session().commit()
1341
1348
1342 msg = _(u'Pull request title & description updated.')
1349 msg = _(u'Pull request title & description updated.')
1343 h.flash(msg, category='success')
1350 h.flash(msg, category='success')
1344 return
1351 return
1345
1352
1346 def _update_commits(self, c, pull_request):
1353 def _update_commits(self, c, pull_request):
1347 _ = self.request.translate
1354 _ = self.request.translate
1348
1355
1349 with pull_request.set_state(PullRequest.STATE_UPDATING):
1356 with pull_request.set_state(PullRequest.STATE_UPDATING):
1350 resp = PullRequestModel().update_commits(
1357 resp = PullRequestModel().update_commits(
1351 pull_request, self._rhodecode_db_user)
1358 pull_request, self._rhodecode_db_user)
1352
1359
1353 if resp.executed:
1360 if resp.executed:
1354
1361
1355 if resp.target_changed and resp.source_changed:
1362 if resp.target_changed and resp.source_changed:
1356 changed = 'target and source repositories'
1363 changed = 'target and source repositories'
1357 elif resp.target_changed and not resp.source_changed:
1364 elif resp.target_changed and not resp.source_changed:
1358 changed = 'target repository'
1365 changed = 'target repository'
1359 elif not resp.target_changed and resp.source_changed:
1366 elif not resp.target_changed and resp.source_changed:
1360 changed = 'source repository'
1367 changed = 'source repository'
1361 else:
1368 else:
1362 changed = 'nothing'
1369 changed = 'nothing'
1363
1370
1364 msg = _(u'Pull request updated to "{source_commit_id}" with '
1371 msg = _(u'Pull request updated to "{source_commit_id}" with '
1365 u'{count_added} added, {count_removed} removed commits. '
1372 u'{count_added} added, {count_removed} removed commits. '
1366 u'Source of changes: {change_source}.')
1373 u'Source of changes: {change_source}.')
1367 msg = msg.format(
1374 msg = msg.format(
1368 source_commit_id=pull_request.source_ref_parts.commit_id,
1375 source_commit_id=pull_request.source_ref_parts.commit_id,
1369 count_added=len(resp.changes.added),
1376 count_added=len(resp.changes.added),
1370 count_removed=len(resp.changes.removed),
1377 count_removed=len(resp.changes.removed),
1371 change_source=changed)
1378 change_source=changed)
1372 h.flash(msg, category='success')
1379 h.flash(msg, category='success')
1373 channelstream.pr_update_channelstream_push(
1380 channelstream.pr_update_channelstream_push(
1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1381 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1375 else:
1382 else:
1376 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1377 warning_reasons = [
1384 warning_reasons = [
1378 UpdateFailureReason.NO_CHANGE,
1385 UpdateFailureReason.NO_CHANGE,
1379 UpdateFailureReason.WRONG_REF_TYPE,
1386 UpdateFailureReason.WRONG_REF_TYPE,
1380 ]
1387 ]
1381 category = 'warning' if resp.reason in warning_reasons else 'error'
1388 category = 'warning' if resp.reason in warning_reasons else 'error'
1382 h.flash(msg, category=category)
1389 h.flash(msg, category=category)
1383
1390
1384 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1391 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1385 _ = self.request.translate
1392 _ = self.request.translate
1386
1393
1387 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1394 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1388 PullRequestModel().get_reviewer_functions()
1395 PullRequestModel().get_reviewer_functions()
1389
1396
1390 if role == PullRequestReviewers.ROLE_REVIEWER:
1397 if role == PullRequestReviewers.ROLE_REVIEWER:
1391 try:
1398 try:
1392 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1399 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1393 except ValueError as e:
1400 except ValueError as e:
1394 log.error('Reviewers Validation: {}'.format(e))
1401 log.error('Reviewers Validation: {}'.format(e))
1395 h.flash(e, category='error')
1402 h.flash(e, category='error')
1396 return
1403 return
1397
1404
1398 old_calculated_status = pull_request.calculated_review_status()
1405 old_calculated_status = pull_request.calculated_review_status()
1399 PullRequestModel().update_reviewers(
1406 PullRequestModel().update_reviewers(
1400 pull_request, reviewers, self._rhodecode_db_user)
1407 pull_request, reviewers, self._rhodecode_db_user)
1401
1408
1402 Session().commit()
1409 Session().commit()
1403
1410
1404 msg = _('Pull request reviewers updated.')
1411 msg = _('Pull request reviewers updated.')
1405 h.flash(msg, category='success')
1412 h.flash(msg, category='success')
1406 channelstream.pr_update_channelstream_push(
1413 channelstream.pr_update_channelstream_push(
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1414 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1408
1415
1409 # trigger status changed if change in reviewers changes the status
1416 # trigger status changed if change in reviewers changes the status
1410 calculated_status = pull_request.calculated_review_status()
1417 calculated_status = pull_request.calculated_review_status()
1411 if old_calculated_status != calculated_status:
1418 if old_calculated_status != calculated_status:
1412 PullRequestModel().trigger_pull_request_hook(
1419 PullRequestModel().trigger_pull_request_hook(
1413 pull_request, self._rhodecode_user, 'review_status_change',
1420 pull_request, self._rhodecode_user, 'review_status_change',
1414 data={'status': calculated_status})
1421 data={'status': calculated_status})
1415
1422
1416 elif role == PullRequestReviewers.ROLE_OBSERVER:
1423 elif role == PullRequestReviewers.ROLE_OBSERVER:
1417 try:
1424 try:
1418 observers = validate_observers(review_members, reviewer_rules)
1425 observers = validate_observers(review_members, reviewer_rules)
1419 except ValueError as e:
1426 except ValueError as e:
1420 log.error('Observers Validation: {}'.format(e))
1427 log.error('Observers Validation: {}'.format(e))
1421 h.flash(e, category='error')
1428 h.flash(e, category='error')
1422 return
1429 return
1423
1430
1424 PullRequestModel().update_observers(
1431 PullRequestModel().update_observers(
1425 pull_request, observers, self._rhodecode_db_user)
1432 pull_request, observers, self._rhodecode_db_user)
1426
1433
1427 Session().commit()
1434 Session().commit()
1428 msg = _('Pull request observers updated.')
1435 msg = _('Pull request observers updated.')
1429 h.flash(msg, category='success')
1436 h.flash(msg, category='success')
1430 channelstream.pr_update_channelstream_push(
1437 channelstream.pr_update_channelstream_push(
1431 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1438 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1432
1439
1433 @LoginRequired()
1440 @LoginRequired()
1434 @NotAnonymous()
1441 @NotAnonymous()
1435 @HasRepoPermissionAnyDecorator(
1442 @HasRepoPermissionAnyDecorator(
1436 'repository.read', 'repository.write', 'repository.admin')
1443 'repository.read', 'repository.write', 'repository.admin')
1437 @CSRFRequired()
1444 @CSRFRequired()
1438 def pull_request_merge(self):
1445 def pull_request_merge(self):
1439 """
1446 """
1440 Merge will perform a server-side merge of the specified
1447 Merge will perform a server-side merge of the specified
1441 pull request, if the pull request is approved and mergeable.
1448 pull request, if the pull request is approved and mergeable.
1442 After successful merging, the pull request is automatically
1449 After successful merging, the pull request is automatically
1443 closed, with a relevant comment.
1450 closed, with a relevant comment.
1444 """
1451 """
1445 pull_request = PullRequest.get_or_404(
1452 pull_request = PullRequest.get_or_404(
1446 self.request.matchdict['pull_request_id'])
1453 self.request.matchdict['pull_request_id'])
1447 _ = self.request.translate
1454 _ = self.request.translate
1448
1455
1449 if pull_request.is_state_changing():
1456 if pull_request.is_state_changing():
1450 log.debug('show: forbidden because pull request is in state %s',
1457 log.debug('show: forbidden because pull request is in state %s',
1451 pull_request.pull_request_state)
1458 pull_request.pull_request_state)
1452 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1459 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1453 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1460 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1454 pull_request.pull_request_state)
1461 pull_request.pull_request_state)
1455 h.flash(msg, category='error')
1462 h.flash(msg, category='error')
1456 raise HTTPFound(
1463 raise HTTPFound(
1457 h.route_path('pullrequest_show',
1464 h.route_path('pullrequest_show',
1458 repo_name=pull_request.target_repo.repo_name,
1465 repo_name=pull_request.target_repo.repo_name,
1459 pull_request_id=pull_request.pull_request_id))
1466 pull_request_id=pull_request.pull_request_id))
1460
1467
1461 self.load_default_context()
1468 self.load_default_context()
1462
1469
1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1470 with pull_request.set_state(PullRequest.STATE_UPDATING):
1464 check = MergeCheck.validate(
1471 check = MergeCheck.validate(
1465 pull_request, auth_user=self._rhodecode_user,
1472 pull_request, auth_user=self._rhodecode_user,
1466 translator=self.request.translate)
1473 translator=self.request.translate)
1467 merge_possible = not check.failed
1474 merge_possible = not check.failed
1468
1475
1469 for err_type, error_msg in check.errors:
1476 for err_type, error_msg in check.errors:
1470 h.flash(error_msg, category=err_type)
1477 h.flash(error_msg, category=err_type)
1471
1478
1472 if merge_possible:
1479 if merge_possible:
1473 log.debug("Pre-conditions checked, trying to merge.")
1480 log.debug("Pre-conditions checked, trying to merge.")
1474 extras = vcs_operation_context(
1481 extras = vcs_operation_context(
1475 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1482 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1476 username=self._rhodecode_db_user.username, action='push',
1483 username=self._rhodecode_db_user.username, action='push',
1477 scm=pull_request.target_repo.repo_type)
1484 scm=pull_request.target_repo.repo_type)
1478 with pull_request.set_state(PullRequest.STATE_UPDATING):
1485 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 self._merge_pull_request(
1486 self._merge_pull_request(
1480 pull_request, self._rhodecode_db_user, extras)
1487 pull_request, self._rhodecode_db_user, extras)
1481 else:
1488 else:
1482 log.debug("Pre-conditions failed, NOT merging.")
1489 log.debug("Pre-conditions failed, NOT merging.")
1483
1490
1484 raise HTTPFound(
1491 raise HTTPFound(
1485 h.route_path('pullrequest_show',
1492 h.route_path('pullrequest_show',
1486 repo_name=pull_request.target_repo.repo_name,
1493 repo_name=pull_request.target_repo.repo_name,
1487 pull_request_id=pull_request.pull_request_id))
1494 pull_request_id=pull_request.pull_request_id))
1488
1495
1489 def _merge_pull_request(self, pull_request, user, extras):
1496 def _merge_pull_request(self, pull_request, user, extras):
1490 _ = self.request.translate
1497 _ = self.request.translate
1491 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1498 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1492
1499
1493 if merge_resp.executed:
1500 if merge_resp.executed:
1494 log.debug("The merge was successful, closing the pull request.")
1501 log.debug("The merge was successful, closing the pull request.")
1495 PullRequestModel().close_pull_request(
1502 PullRequestModel().close_pull_request(
1496 pull_request.pull_request_id, user)
1503 pull_request.pull_request_id, user)
1497 Session().commit()
1504 Session().commit()
1498 msg = _('Pull request was successfully merged and closed.')
1505 msg = _('Pull request was successfully merged and closed.')
1499 h.flash(msg, category='success')
1506 h.flash(msg, category='success')
1500 else:
1507 else:
1501 log.debug(
1508 log.debug(
1502 "The merge was not successful. Merge response: %s", merge_resp)
1509 "The merge was not successful. Merge response: %s", merge_resp)
1503 msg = merge_resp.merge_status_message
1510 msg = merge_resp.merge_status_message
1504 h.flash(msg, category='error')
1511 h.flash(msg, category='error')
1505
1512
1506 @LoginRequired()
1513 @LoginRequired()
1507 @NotAnonymous()
1514 @NotAnonymous()
1508 @HasRepoPermissionAnyDecorator(
1515 @HasRepoPermissionAnyDecorator(
1509 'repository.read', 'repository.write', 'repository.admin')
1516 'repository.read', 'repository.write', 'repository.admin')
1510 @CSRFRequired()
1517 @CSRFRequired()
1511 def pull_request_delete(self):
1518 def pull_request_delete(self):
1512 _ = self.request.translate
1519 _ = self.request.translate
1513
1520
1514 pull_request = PullRequest.get_or_404(
1521 pull_request = PullRequest.get_or_404(
1515 self.request.matchdict['pull_request_id'])
1522 self.request.matchdict['pull_request_id'])
1516 self.load_default_context()
1523 self.load_default_context()
1517
1524
1518 pr_closed = pull_request.is_closed()
1525 pr_closed = pull_request.is_closed()
1519 allowed_to_delete = PullRequestModel().check_user_delete(
1526 allowed_to_delete = PullRequestModel().check_user_delete(
1520 pull_request, self._rhodecode_user) and not pr_closed
1527 pull_request, self._rhodecode_user) and not pr_closed
1521
1528
1522 # only owner can delete it !
1529 # only owner can delete it !
1523 if allowed_to_delete:
1530 if allowed_to_delete:
1524 PullRequestModel().delete(pull_request, self._rhodecode_user)
1531 PullRequestModel().delete(pull_request, self._rhodecode_user)
1525 Session().commit()
1532 Session().commit()
1526 h.flash(_('Successfully deleted pull request'),
1533 h.flash(_('Successfully deleted pull request'),
1527 category='success')
1534 category='success')
1528 raise HTTPFound(h.route_path('pullrequest_show_all',
1535 raise HTTPFound(h.route_path('pullrequest_show_all',
1529 repo_name=self.db_repo_name))
1536 repo_name=self.db_repo_name))
1530
1537
1531 log.warning('user %s tried to delete pull request without access',
1538 log.warning('user %s tried to delete pull request without access',
1532 self._rhodecode_user)
1539 self._rhodecode_user)
1533 raise HTTPNotFound()
1540 raise HTTPNotFound()
1534
1541
1535 def _pull_request_comments_create(self, pull_request, comments):
1542 def _pull_request_comments_create(self, pull_request, comments):
1536 _ = self.request.translate
1543 _ = self.request.translate
1537 data = {}
1544 data = {}
1538 if not comments:
1545 if not comments:
1539 return
1546 return
1540 pull_request_id = pull_request.pull_request_id
1547 pull_request_id = pull_request.pull_request_id
1541
1548
1542 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1549 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1543
1550
1544 for entry in comments:
1551 for entry in comments:
1545 c = self.load_default_context()
1552 c = self.load_default_context()
1546 comment_type = entry['comment_type']
1553 comment_type = entry['comment_type']
1547 text = entry['text']
1554 text = entry['text']
1548 status = entry['status']
1555 status = entry['status']
1549 is_draft = str2bool(entry['is_draft'])
1556 is_draft = str2bool(entry['is_draft'])
1550 resolves_comment_id = entry['resolves_comment_id']
1557 resolves_comment_id = entry['resolves_comment_id']
1551 close_pull_request = entry['close_pull_request']
1558 close_pull_request = entry['close_pull_request']
1552 f_path = entry['f_path']
1559 f_path = entry['f_path']
1553 line_no = entry['line']
1560 line_no = entry['line']
1554 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1561 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1555
1562
1556 # the logic here should work like following, if we submit close
1563 # the logic here should work like following, if we submit close
1557 # pr comment, use `close_pull_request_with_comment` function
1564 # pr comment, use `close_pull_request_with_comment` function
1558 # else handle regular comment logic
1565 # else handle regular comment logic
1559
1566
1560 if close_pull_request:
1567 if close_pull_request:
1561 # only owner or admin or person with write permissions
1568 # only owner or admin or person with write permissions
1562 allowed_to_close = PullRequestModel().check_user_update(
1569 allowed_to_close = PullRequestModel().check_user_update(
1563 pull_request, self._rhodecode_user)
1570 pull_request, self._rhodecode_user)
1564 if not allowed_to_close:
1571 if not allowed_to_close:
1565 log.debug('comment: forbidden because not allowed to close '
1572 log.debug('comment: forbidden because not allowed to close '
1566 'pull request %s', pull_request_id)
1573 'pull request %s', pull_request_id)
1567 raise HTTPForbidden()
1574 raise HTTPForbidden()
1568
1575
1569 # This also triggers `review_status_change`
1576 # This also triggers `review_status_change`
1570 comment, status = PullRequestModel().close_pull_request_with_comment(
1577 comment, status = PullRequestModel().close_pull_request_with_comment(
1571 pull_request, self._rhodecode_user, self.db_repo, message=text,
1578 pull_request, self._rhodecode_user, self.db_repo, message=text,
1572 auth_user=self._rhodecode_user)
1579 auth_user=self._rhodecode_user)
1573 Session().flush()
1580 Session().flush()
1574 is_inline = comment.is_inline
1581 is_inline = comment.is_inline
1575
1582
1576 PullRequestModel().trigger_pull_request_hook(
1583 PullRequestModel().trigger_pull_request_hook(
1577 pull_request, self._rhodecode_user, 'comment',
1584 pull_request, self._rhodecode_user, 'comment',
1578 data={'comment': comment})
1585 data={'comment': comment})
1579
1586
1580 else:
1587 else:
1581 # regular comment case, could be inline, or one with status.
1588 # regular comment case, could be inline, or one with status.
1582 # for that one we check also permissions
1589 # for that one we check also permissions
1583 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1590 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1584 allowed_to_change_status = PullRequestModel().check_user_change_status(
1591 allowed_to_change_status = PullRequestModel().check_user_change_status(
1585 pull_request, self._rhodecode_user) and not is_draft
1592 pull_request, self._rhodecode_user) and not is_draft
1586
1593
1587 if status and allowed_to_change_status:
1594 if status and allowed_to_change_status:
1588 message = (_('Status change %(transition_icon)s %(status)s')
1595 message = (_('Status change %(transition_icon)s %(status)s')
1589 % {'transition_icon': '>',
1596 % {'transition_icon': '>',
1590 'status': ChangesetStatus.get_status_lbl(status)})
1597 'status': ChangesetStatus.get_status_lbl(status)})
1591 text = text or message
1598 text = text or message
1592
1599
1593 comment = CommentsModel().create(
1600 comment = CommentsModel().create(
1594 text=text,
1601 text=text,
1595 repo=self.db_repo.repo_id,
1602 repo=self.db_repo.repo_id,
1596 user=self._rhodecode_user.user_id,
1603 user=self._rhodecode_user.user_id,
1597 pull_request=pull_request,
1604 pull_request=pull_request,
1598 f_path=f_path,
1605 f_path=f_path,
1599 line_no=line_no,
1606 line_no=line_no,
1600 status_change=(ChangesetStatus.get_status_lbl(status)
1607 status_change=(ChangesetStatus.get_status_lbl(status)
1601 if status and allowed_to_change_status else None),
1608 if status and allowed_to_change_status else None),
1602 status_change_type=(status
1609 status_change_type=(status
1603 if status and allowed_to_change_status else None),
1610 if status and allowed_to_change_status else None),
1604 comment_type=comment_type,
1611 comment_type=comment_type,
1605 is_draft=is_draft,
1612 is_draft=is_draft,
1606 resolves_comment_id=resolves_comment_id,
1613 resolves_comment_id=resolves_comment_id,
1607 auth_user=self._rhodecode_user,
1614 auth_user=self._rhodecode_user,
1608 send_email=not is_draft, # skip notification for draft comments
1615 send_email=not is_draft, # skip notification for draft comments
1609 )
1616 )
1610 is_inline = comment.is_inline
1617 is_inline = comment.is_inline
1611
1618
1612 if allowed_to_change_status:
1619 if allowed_to_change_status:
1613 # calculate old status before we change it
1620 # calculate old status before we change it
1614 old_calculated_status = pull_request.calculated_review_status()
1621 old_calculated_status = pull_request.calculated_review_status()
1615
1622
1616 # get status if set !
1623 # get status if set !
1617 if status:
1624 if status:
1618 ChangesetStatusModel().set_status(
1625 ChangesetStatusModel().set_status(
1619 self.db_repo.repo_id,
1626 self.db_repo.repo_id,
1620 status,
1627 status,
1621 self._rhodecode_user.user_id,
1628 self._rhodecode_user.user_id,
1622 comment,
1629 comment,
1623 pull_request=pull_request
1630 pull_request=pull_request
1624 )
1631 )
1625
1632
1626 Session().flush()
1633 Session().flush()
1627 # this is somehow required to get access to some relationship
1634 # this is somehow required to get access to some relationship
1628 # loaded on comment
1635 # loaded on comment
1629 Session().refresh(comment)
1636 Session().refresh(comment)
1630
1637
1631 # skip notifications for drafts
1638 # skip notifications for drafts
1632 if not is_draft:
1639 if not is_draft:
1633 PullRequestModel().trigger_pull_request_hook(
1640 PullRequestModel().trigger_pull_request_hook(
1634 pull_request, self._rhodecode_user, 'comment',
1641 pull_request, self._rhodecode_user, 'comment',
1635 data={'comment': comment})
1642 data={'comment': comment})
1636
1643
1637 # we now calculate the status of pull request, and based on that
1644 # we now calculate the status of pull request, and based on that
1638 # calculation we set the commits status
1645 # calculation we set the commits status
1639 calculated_status = pull_request.calculated_review_status()
1646 calculated_status = pull_request.calculated_review_status()
1640 if old_calculated_status != calculated_status:
1647 if old_calculated_status != calculated_status:
1641 PullRequestModel().trigger_pull_request_hook(
1648 PullRequestModel().trigger_pull_request_hook(
1642 pull_request, self._rhodecode_user, 'review_status_change',
1649 pull_request, self._rhodecode_user, 'review_status_change',
1643 data={'status': calculated_status})
1650 data={'status': calculated_status})
1644
1651
1645 comment_id = comment.comment_id
1652 comment_id = comment.comment_id
1646 data[comment_id] = {
1653 data[comment_id] = {
1647 'target_id': target_elem_id
1654 'target_id': target_elem_id
1648 }
1655 }
1649 Session().flush()
1656 Session().flush()
1650
1657
1651 c.co = comment
1658 c.co = comment
1652 c.at_version_num = None
1659 c.at_version_num = None
1653 c.is_new = True
1660 c.is_new = True
1654 rendered_comment = render(
1661 rendered_comment = render(
1655 'rhodecode:templates/changeset/changeset_comment_block.mako',
1662 'rhodecode:templates/changeset/changeset_comment_block.mako',
1656 self._get_template_context(c), self.request)
1663 self._get_template_context(c), self.request)
1657
1664
1658 data[comment_id].update(comment.get_dict())
1665 data[comment_id].update(comment.get_dict())
1659 data[comment_id].update({'rendered_text': rendered_comment})
1666 data[comment_id].update({'rendered_text': rendered_comment})
1660
1667
1661 Session().commit()
1668 Session().commit()
1662
1669
1663 # skip channelstream for draft comments
1670 # skip channelstream for draft comments
1664 if not all_drafts:
1671 if not all_drafts:
1665 comment_broadcast_channel = channelstream.comment_channel(
1672 comment_broadcast_channel = channelstream.comment_channel(
1666 self.db_repo_name, pull_request_obj=pull_request)
1673 self.db_repo_name, pull_request_obj=pull_request)
1667
1674
1668 comment_data = data
1675 comment_data = data
1669 posted_comment_type = 'inline' if is_inline else 'general'
1676 posted_comment_type = 'inline' if is_inline else 'general'
1670 if len(data) == 1:
1677 if len(data) == 1:
1671 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1678 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1672 else:
1679 else:
1673 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1680 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1674
1681
1675 channelstream.comment_channelstream_push(
1682 channelstream.comment_channelstream_push(
1676 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1683 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1677 comment_data=comment_data)
1684 comment_data=comment_data)
1678
1685
1679 return data
1686 return data
1680
1687
1681 @LoginRequired()
1688 @LoginRequired()
1682 @NotAnonymous()
1689 @NotAnonymous()
1683 @HasRepoPermissionAnyDecorator(
1690 @HasRepoPermissionAnyDecorator(
1684 'repository.read', 'repository.write', 'repository.admin')
1691 'repository.read', 'repository.write', 'repository.admin')
1685 @CSRFRequired()
1692 @CSRFRequired()
1686 def pull_request_comment_create(self):
1693 def pull_request_comment_create(self):
1687 _ = self.request.translate
1694 _ = self.request.translate
1688
1695
1689 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1696 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1690
1697
1691 if pull_request.is_closed():
1698 if pull_request.is_closed():
1692 log.debug('comment: forbidden because pull request is closed')
1699 log.debug('comment: forbidden because pull request is closed')
1693 raise HTTPForbidden()
1700 raise HTTPForbidden()
1694
1701
1695 allowed_to_comment = PullRequestModel().check_user_comment(
1702 allowed_to_comment = PullRequestModel().check_user_comment(
1696 pull_request, self._rhodecode_user)
1703 pull_request, self._rhodecode_user)
1697 if not allowed_to_comment:
1704 if not allowed_to_comment:
1698 log.debug('comment: forbidden because pull request is from forbidden repo')
1705 log.debug('comment: forbidden because pull request is from forbidden repo')
1699 raise HTTPForbidden()
1706 raise HTTPForbidden()
1700
1707
1701 comment_data = {
1708 comment_data = {
1702 'comment_type': self.request.POST.get('comment_type'),
1709 'comment_type': self.request.POST.get('comment_type'),
1703 'text': self.request.POST.get('text'),
1710 'text': self.request.POST.get('text'),
1704 'status': self.request.POST.get('changeset_status', None),
1711 'status': self.request.POST.get('changeset_status', None),
1705 'is_draft': self.request.POST.get('draft'),
1712 'is_draft': self.request.POST.get('draft'),
1706 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1713 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1707 'close_pull_request': self.request.POST.get('close_pull_request'),
1714 'close_pull_request': self.request.POST.get('close_pull_request'),
1708 'f_path': self.request.POST.get('f_path'),
1715 'f_path': self.request.POST.get('f_path'),
1709 'line': self.request.POST.get('line'),
1716 'line': self.request.POST.get('line'),
1710 }
1717 }
1711 data = self._pull_request_comments_create(pull_request, [comment_data])
1718 data = self._pull_request_comments_create(pull_request, [comment_data])
1712
1719
1713 return data
1720 return data
1714
1721
1715 @LoginRequired()
1722 @LoginRequired()
1716 @NotAnonymous()
1723 @NotAnonymous()
1717 @HasRepoPermissionAnyDecorator(
1724 @HasRepoPermissionAnyDecorator(
1718 'repository.read', 'repository.write', 'repository.admin')
1725 'repository.read', 'repository.write', 'repository.admin')
1719 @CSRFRequired()
1726 @CSRFRequired()
1720 def pull_request_comment_delete(self):
1727 def pull_request_comment_delete(self):
1721 pull_request = PullRequest.get_or_404(
1728 pull_request = PullRequest.get_or_404(
1722 self.request.matchdict['pull_request_id'])
1729 self.request.matchdict['pull_request_id'])
1723
1730
1724 comment = ChangesetComment.get_or_404(
1731 comment = ChangesetComment.get_or_404(
1725 self.request.matchdict['comment_id'])
1732 self.request.matchdict['comment_id'])
1726 comment_id = comment.comment_id
1733 comment_id = comment.comment_id
1727
1734
1728 if comment.immutable:
1735 if comment.immutable:
1729 # don't allow deleting comments that are immutable
1736 # don't allow deleting comments that are immutable
1730 raise HTTPForbidden()
1737 raise HTTPForbidden()
1731
1738
1732 if pull_request.is_closed():
1739 if pull_request.is_closed():
1733 log.debug('comment: forbidden because pull request is closed')
1740 log.debug('comment: forbidden because pull request is closed')
1734 raise HTTPForbidden()
1741 raise HTTPForbidden()
1735
1742
1736 if not comment:
1743 if not comment:
1737 log.debug('Comment with id:%s not found, skipping', comment_id)
1744 log.debug('Comment with id:%s not found, skipping', comment_id)
1738 # comment already deleted in another call probably
1745 # comment already deleted in another call probably
1739 return True
1746 return True
1740
1747
1741 if comment.pull_request.is_closed():
1748 if comment.pull_request.is_closed():
1742 # don't allow deleting comments on closed pull request
1749 # don't allow deleting comments on closed pull request
1743 raise HTTPForbidden()
1750 raise HTTPForbidden()
1744
1751
1745 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1752 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1746 super_admin = h.HasPermissionAny('hg.admin')()
1753 super_admin = h.HasPermissionAny('hg.admin')()
1747 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1754 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1748 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1755 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1749 comment_repo_admin = is_repo_admin and is_repo_comment
1756 comment_repo_admin = is_repo_admin and is_repo_comment
1750
1757
1751 if comment.draft and not comment_owner:
1758 if comment.draft and not comment_owner:
1752 # We never allow to delete draft comments for other than owners
1759 # We never allow to delete draft comments for other than owners
1753 raise HTTPNotFound()
1760 raise HTTPNotFound()
1754
1761
1755 if super_admin or comment_owner or comment_repo_admin:
1762 if super_admin or comment_owner or comment_repo_admin:
1756 old_calculated_status = comment.pull_request.calculated_review_status()
1763 old_calculated_status = comment.pull_request.calculated_review_status()
1757 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1764 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1758 Session().commit()
1765 Session().commit()
1759 calculated_status = comment.pull_request.calculated_review_status()
1766 calculated_status = comment.pull_request.calculated_review_status()
1760 if old_calculated_status != calculated_status:
1767 if old_calculated_status != calculated_status:
1761 PullRequestModel().trigger_pull_request_hook(
1768 PullRequestModel().trigger_pull_request_hook(
1762 comment.pull_request, self._rhodecode_user, 'review_status_change',
1769 comment.pull_request, self._rhodecode_user, 'review_status_change',
1763 data={'status': calculated_status})
1770 data={'status': calculated_status})
1764 return True
1771 return True
1765 else:
1772 else:
1766 log.warning('No permissions for user %s to delete comment_id: %s',
1773 log.warning('No permissions for user %s to delete comment_id: %s',
1767 self._rhodecode_db_user, comment_id)
1774 self._rhodecode_db_user, comment_id)
1768 raise HTTPNotFound()
1775 raise HTTPNotFound()
1769
1776
1770 @LoginRequired()
1777 @LoginRequired()
1771 @NotAnonymous()
1778 @NotAnonymous()
1772 @HasRepoPermissionAnyDecorator(
1779 @HasRepoPermissionAnyDecorator(
1773 'repository.read', 'repository.write', 'repository.admin')
1780 'repository.read', 'repository.write', 'repository.admin')
1774 @CSRFRequired()
1781 @CSRFRequired()
1775 def pull_request_comment_edit(self):
1782 def pull_request_comment_edit(self):
1776 self.load_default_context()
1783 self.load_default_context()
1777
1784
1778 pull_request = PullRequest.get_or_404(
1785 pull_request = PullRequest.get_or_404(
1779 self.request.matchdict['pull_request_id']
1786 self.request.matchdict['pull_request_id']
1780 )
1787 )
1781 comment = ChangesetComment.get_or_404(
1788 comment = ChangesetComment.get_or_404(
1782 self.request.matchdict['comment_id']
1789 self.request.matchdict['comment_id']
1783 )
1790 )
1784 comment_id = comment.comment_id
1791 comment_id = comment.comment_id
1785
1792
1786 if comment.immutable:
1793 if comment.immutable:
1787 # don't allow deleting comments that are immutable
1794 # don't allow deleting comments that are immutable
1788 raise HTTPForbidden()
1795 raise HTTPForbidden()
1789
1796
1790 if pull_request.is_closed():
1797 if pull_request.is_closed():
1791 log.debug('comment: forbidden because pull request is closed')
1798 log.debug('comment: forbidden because pull request is closed')
1792 raise HTTPForbidden()
1799 raise HTTPForbidden()
1793
1800
1794 if comment.pull_request.is_closed():
1801 if comment.pull_request.is_closed():
1795 # don't allow deleting comments on closed pull request
1802 # don't allow deleting comments on closed pull request
1796 raise HTTPForbidden()
1803 raise HTTPForbidden()
1797
1804
1798 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1805 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1799 super_admin = h.HasPermissionAny('hg.admin')()
1806 super_admin = h.HasPermissionAny('hg.admin')()
1800 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1807 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1801 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1808 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1802 comment_repo_admin = is_repo_admin and is_repo_comment
1809 comment_repo_admin = is_repo_admin and is_repo_comment
1803
1810
1804 if super_admin or comment_owner or comment_repo_admin:
1811 if super_admin or comment_owner or comment_repo_admin:
1805 text = self.request.POST.get('text')
1812 text = self.request.POST.get('text')
1806 version = self.request.POST.get('version')
1813 version = self.request.POST.get('version')
1807 if text == comment.text:
1814 if text == comment.text:
1808 log.warning(
1815 log.warning(
1809 'Comment(PR): '
1816 'Comment(PR): '
1810 'Trying to create new version '
1817 'Trying to create new version '
1811 'with the same comment body {}'.format(
1818 'with the same comment body {}'.format(
1812 comment_id,
1819 comment_id,
1813 )
1820 )
1814 )
1821 )
1815 raise HTTPNotFound()
1822 raise HTTPNotFound()
1816
1823
1817 if version.isdigit():
1824 if version.isdigit():
1818 version = int(version)
1825 version = int(version)
1819 else:
1826 else:
1820 log.warning(
1827 log.warning(
1821 'Comment(PR): Wrong version type {} {} '
1828 'Comment(PR): Wrong version type {} {} '
1822 'for comment {}'.format(
1829 'for comment {}'.format(
1823 version,
1830 version,
1824 type(version),
1831 type(version),
1825 comment_id,
1832 comment_id,
1826 )
1833 )
1827 )
1834 )
1828 raise HTTPNotFound()
1835 raise HTTPNotFound()
1829
1836
1830 try:
1837 try:
1831 comment_history = CommentsModel().edit(
1838 comment_history = CommentsModel().edit(
1832 comment_id=comment_id,
1839 comment_id=comment_id,
1833 text=text,
1840 text=text,
1834 auth_user=self._rhodecode_user,
1841 auth_user=self._rhodecode_user,
1835 version=version,
1842 version=version,
1836 )
1843 )
1837 except CommentVersionMismatch:
1844 except CommentVersionMismatch:
1838 raise HTTPConflict()
1845 raise HTTPConflict()
1839
1846
1840 if not comment_history:
1847 if not comment_history:
1841 raise HTTPNotFound()
1848 raise HTTPNotFound()
1842
1849
1843 Session().commit()
1850 Session().commit()
1844 if not comment.draft:
1851 if not comment.draft:
1845 PullRequestModel().trigger_pull_request_hook(
1852 PullRequestModel().trigger_pull_request_hook(
1846 pull_request, self._rhodecode_user, 'comment_edit',
1853 pull_request, self._rhodecode_user, 'comment_edit',
1847 data={'comment': comment})
1854 data={'comment': comment})
1848
1855
1849 return {
1856 return {
1850 'comment_history_id': comment_history.comment_history_id,
1857 'comment_history_id': comment_history.comment_history_id,
1851 'comment_id': comment.comment_id,
1858 'comment_id': comment.comment_id,
1852 'comment_version': comment_history.version,
1859 'comment_version': comment_history.version,
1853 'comment_author_username': comment_history.author.username,
1860 'comment_author_username': comment_history.author.username,
1854 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1861 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1855 'comment_created_on': h.age_component(comment_history.created_on,
1862 'comment_created_on': h.age_component(comment_history.created_on,
1856 time_is_local=True),
1863 time_is_local=True),
1857 }
1864 }
1858 else:
1865 else:
1859 log.warning('No permissions for user %s to edit comment_id: %s',
1866 log.warning('No permissions for user %s to edit comment_id: %s',
1860 self._rhodecode_db_user, comment_id)
1867 self._rhodecode_db_user, comment_id)
1861 raise HTTPNotFound()
1868 raise HTTPNotFound()
@@ -1,396 +1,403 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 itertools
22 import itertools
23 import logging
23 import logging
24 import collections
24 import collections
25
25
26 from rhodecode.model import BaseModel
26 from rhodecode.model import BaseModel
27 from rhodecode.model.db import (
27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 from rhodecode.lib.markup_renderer import (
30 from rhodecode.lib.markup_renderer import (
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class ChangesetStatusModel(BaseModel):
36 class ChangesetStatusModel(BaseModel):
37
37
38 cls = ChangesetStatus
38 cls = ChangesetStatus
39
39
40 def __get_changeset_status(self, changeset_status):
40 def __get_changeset_status(self, changeset_status):
41 return self._get_instance(ChangesetStatus, changeset_status)
41 return self._get_instance(ChangesetStatus, changeset_status)
42
42
43 def __get_pull_request(self, pull_request):
43 def __get_pull_request(self, pull_request):
44 return self._get_instance(PullRequest, pull_request)
44 return self._get_instance(PullRequest, pull_request)
45
45
46 def _get_status_query(self, repo, revision, pull_request,
46 def _get_status_query(self, repo, revision, pull_request,
47 with_revisions=False):
47 with_revisions=False):
48 repo = self._get_repo(repo)
48 repo = self._get_repo(repo)
49
49
50 q = ChangesetStatus.query()\
50 q = ChangesetStatus.query()\
51 .filter(ChangesetStatus.repo == repo)
51 .filter(ChangesetStatus.repo == repo)
52 if not with_revisions:
52 if not with_revisions:
53 q = q.filter(ChangesetStatus.version == 0)
53 q = q.filter(ChangesetStatus.version == 0)
54
54
55 if revision:
55 if revision:
56 q = q.filter(ChangesetStatus.revision == revision)
56 q = q.filter(ChangesetStatus.revision == revision)
57 elif pull_request:
57 elif pull_request:
58 pull_request = self.__get_pull_request(pull_request)
58 pull_request = self.__get_pull_request(pull_request)
59 # TODO: johbo: Think about the impact of this join, there must
59 # TODO: johbo: Think about the impact of this join, there must
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
61 # to the pull request. Might be that we want to do the same for
61 # to the pull request. Might be that we want to do the same for
62 # the pull_request_version_id.
62 # the pull_request_version_id.
63 q = q.join(ChangesetComment).filter(
63 q = q.join(ChangesetComment).filter(
64 ChangesetStatus.pull_request == pull_request,
64 ChangesetStatus.pull_request == pull_request,
65 ChangesetComment.pull_request_version_id == None)
65 ChangesetComment.pull_request_version_id == None)
66 else:
66 else:
67 raise Exception('Please specify revision or pull_request')
67 raise Exception('Please specify revision or pull_request')
68 q = q.order_by(ChangesetStatus.version.asc())
68 q = q.order_by(ChangesetStatus.version.asc())
69 return q
69 return q
70
70
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 trim_votes=True):
72 trim_votes=True):
73 """
73 """
74 Calculate status based on given group members, and voting rule
74 Calculate status based on given group members, and voting rule
75
75
76
76
77 group1 - 4 members, 3 required for approval
77 group1 - 4 members, 3 required for approval
78 user1 - approved
78 user1 - approved
79 user2 - reject
79 user2 - reject
80 user3 - approved
80 user3 - approved
81 user4 - rejected
81 user4 - rejected
82
82
83 final_state: rejected, reasons not at least 3 votes
83 final_state: rejected, reasons not at least 3 votes
84
84
85
85
86 group1 - 4 members, 2 required for approval
86 group1 - 4 members, 2 required for approval
87 user1 - approved
87 user1 - approved
88 user2 - reject
88 user2 - reject
89 user3 - approved
89 user3 - approved
90 user4 - rejected
90 user4 - rejected
91
91
92 final_state: approved, reasons got at least 2 approvals
92 final_state: approved, reasons got at least 2 approvals
93
93
94 group1 - 4 members, ALL required for approval
94 group1 - 4 members, ALL required for approval
95 user1 - approved
95 user1 - approved
96 user2 - reject
96 user2 - reject
97 user3 - approved
97 user3 - approved
98 user4 - rejected
98 user4 - rejected
99
99
100 final_state: rejected, reasons not all approvals
100 final_state: rejected, reasons not all approvals
101
101
102
102
103 group1 - 4 members, ALL required for approval
103 group1 - 4 members, ALL required for approval
104 user1 - approved
104 user1 - approved
105 user2 - approved
105 user2 - approved
106 user3 - approved
106 user3 - approved
107 user4 - approved
107 user4 - approved
108
108
109 final_state: approved, reason all approvals received
109 final_state: approved, reason all approvals received
110
110
111 group1 - 4 members, 5 required for approval
111 group1 - 4 members, 5 required for approval
112 (approval should be shorted to number of actual members)
112 (approval should be shorted to number of actual members)
113
113
114 user1 - approved
114 user1 - approved
115 user2 - approved
115 user2 - approved
116 user3 - approved
116 user3 - approved
117 user4 - approved
117 user4 - approved
118
118
119 final_state: approved, reason all approvals received
119 final_state: approved, reason all approvals received
120
120
121 """
121 """
122 group_vote_data = {}
122 group_vote_data = {}
123 got_rule = False
123 got_rule = False
124 members = collections.OrderedDict()
124 members = collections.OrderedDict()
125 for review_obj, user, reasons, mandatory, statuses \
125 for review_obj, user, reasons, mandatory, statuses \
126 in group_statuses_by_reviewers:
126 in group_statuses_by_reviewers:
127
127
128 if not got_rule:
128 if not got_rule:
129 group_vote_data = review_obj.rule_user_group_data()
129 group_vote_data = review_obj.rule_user_group_data()
130 got_rule = bool(group_vote_data)
130 got_rule = bool(group_vote_data)
131
131
132 members[user.user_id] = statuses
132 members[user.user_id] = statuses
133
133
134 if not group_vote_data:
134 if not group_vote_data:
135 return []
135 return []
136
136
137 required_votes = group_vote_data['vote_rule']
137 required_votes = group_vote_data['vote_rule']
138 if required_votes == -1:
138 if required_votes == -1:
139 # -1 means all required, so we replace it with how many people
139 # -1 means all required, so we replace it with how many people
140 # are in the members
140 # are in the members
141 required_votes = len(members)
141 required_votes = len(members)
142
142
143 if trim_votes and required_votes > len(members):
143 if trim_votes and required_votes > len(members):
144 # we require more votes than we have members in the group
144 # we require more votes than we have members in the group
145 # in this case we trim the required votes to the number of members
145 # in this case we trim the required votes to the number of members
146 required_votes = len(members)
146 required_votes = len(members)
147
147
148 approvals = sum([
148 approvals = sum([
149 1 for statuses in members.values()
149 1 for statuses in members.values()
150 if statuses and
150 if statuses and
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152
152
153 calculated_votes = []
153 calculated_votes = []
154 # we have all votes from users, now check if we have enough votes
154 # we have all votes from users, now check if we have enough votes
155 # to fill other
155 # to fill other
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 if approvals >= required_votes:
157 if approvals >= required_votes:
158 fill_in = ChangesetStatus.STATUS_APPROVED
158 fill_in = ChangesetStatus.STATUS_APPROVED
159
159
160 for member, statuses in members.items():
160 for member, statuses in members.items():
161 if statuses:
161 if statuses:
162 ver, latest = statuses[0]
162 ver, latest = statuses[0]
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 calculated_votes.append(fill_in)
164 calculated_votes.append(fill_in)
165 else:
165 else:
166 calculated_votes.append(latest.status)
166 calculated_votes.append(latest.status)
167 else:
167 else:
168 calculated_votes.append(fill_in)
168 calculated_votes.append(fill_in)
169
169
170 return calculated_votes
170 return calculated_votes
171
171
172 def calculate_status(self, statuses_by_reviewers):
172 def calculate_status(self, statuses_by_reviewers):
173 """
173 """
174 Given the approval statuses from reviewers, calculates final approval
174 Given the approval statuses from reviewers, calculates final approval
175 status. There can only be 3 results, all approved, all rejected. If
175 status. There can only be 3 results, all approved, all rejected. If
176 there is no consensus the PR is under review.
176 there is no consensus the PR is under review.
177
177
178 :param statuses_by_reviewers:
178 :param statuses_by_reviewers:
179 """
179 """
180
180
181 def group_rule(element):
181 def group_rule(element):
182 review_obj = element[0]
182 review_obj = element[0]
183 rule_data = review_obj.rule_user_group_data()
183 rule_data = review_obj.rule_user_group_data()
184 if rule_data and rule_data['id']:
184 if rule_data and rule_data['id']:
185 return rule_data['id']
185 return rule_data['id']
186
186
187 voting_groups = itertools.groupby(
187 voting_groups = itertools.groupby(
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189
189
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191
191
192 reviewers_number = len(statuses_by_reviewers)
192 reviewers_number = len(statuses_by_reviewers)
193 votes = collections.defaultdict(int)
193 votes = collections.defaultdict(int)
194 for group, group_statuses_by_reviewers in voting_by_groups:
194 for group, group_statuses_by_reviewers in voting_by_groups:
195 if group:
195 if group:
196 # calculate how the "group" voted
196 # calculate how the "group" voted
197 for vote_status in self.calculate_group_vote(
197 for vote_status in self.calculate_group_vote(
198 group, group_statuses_by_reviewers):
198 group, group_statuses_by_reviewers):
199 votes[vote_status] += 1
199 votes[vote_status] += 1
200 else:
200 else:
201
201
202 for review_obj, user, reasons, mandatory, statuses \
202 for review_obj, user, reasons, mandatory, statuses \
203 in group_statuses_by_reviewers:
203 in group_statuses_by_reviewers:
204 # individual vote
204 # individual vote
205 if statuses:
205 if statuses:
206 ver, latest = statuses[0]
206 ver, latest = statuses[0]
207 votes[latest.status] += 1
207 votes[latest.status] += 1
208
208
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
211
211
212 # TODO(marcink): with group voting, how does rejected work,
212 # TODO(marcink): with group voting, how does rejected work,
213 # do we ever get rejected state ?
213 # do we ever get rejected state ?
214
214
215 if approved_votes_count and (approved_votes_count == reviewers_number):
215 if approved_votes_count and (approved_votes_count == reviewers_number):
216 return ChangesetStatus.STATUS_APPROVED
216 return ChangesetStatus.STATUS_APPROVED
217
217
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
219 return ChangesetStatus.STATUS_REJECTED
219 return ChangesetStatus.STATUS_REJECTED
220
220
221 return ChangesetStatus.STATUS_UNDER_REVIEW
221 return ChangesetStatus.STATUS_UNDER_REVIEW
222
222
223 def get_statuses(self, repo, revision=None, pull_request=None,
223 def get_statuses(self, repo, revision=None, pull_request=None,
224 with_revisions=False):
224 with_revisions=False):
225 q = self._get_status_query(repo, revision, pull_request,
225 q = self._get_status_query(repo, revision, pull_request,
226 with_revisions)
226 with_revisions)
227 return q.all()
227 return q.all()
228
228
229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
230 """
230 """
231 Returns latest status of changeset for given revision or for given
231 Returns latest status of changeset for given revision or for given
232 pull request. Statuses are versioned inside a table itself and
232 pull request. Statuses are versioned inside a table itself and
233 version == 0 is always the current one
233 version == 0 is always the current one
234
234
235 :param repo:
235 :param repo:
236 :param revision: 40char hash or None
236 :param revision: 40char hash or None
237 :param pull_request: pull_request reference
237 :param pull_request: pull_request reference
238 :param as_str: return status as string not object
238 :param as_str: return status as string not object
239 """
239 """
240 q = self._get_status_query(repo, revision, pull_request)
240 q = self._get_status_query(repo, revision, pull_request)
241
241
242 # need to use first here since there can be multiple statuses
242 # need to use first here since there can be multiple statuses
243 # returned from pull_request
243 # returned from pull_request
244 status = q.first()
244 status = q.first()
245 if as_str:
245 if as_str:
246 status = status.status if status else status
246 status = status.status if status else status
247 st = status or ChangesetStatus.DEFAULT
247 st = status or ChangesetStatus.DEFAULT
248 return str(st)
248 return str(st)
249 return status
249 return status
250
250
251 def _render_auto_status_message(
251 def _render_auto_status_message(
252 self, status, commit_id=None, pull_request=None):
252 self, status, commit_id=None, pull_request=None):
253 """
253 """
254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
255 so it's always looking the same disregarding on which default
255 so it's always looking the same disregarding on which default
256 renderer system is using.
256 renderer system is using.
257
257
258 :param status: status text to change into
258 :param status: status text to change into
259 :param commit_id: the commit_id we change the status for
259 :param commit_id: the commit_id we change the status for
260 :param pull_request: the pull request we change the status for
260 :param pull_request: the pull request we change the status for
261 """
261 """
262
262
263 new_status = ChangesetStatus.get_status_lbl(status)
263 new_status = ChangesetStatus.get_status_lbl(status)
264
264
265 params = {
265 params = {
266 'new_status_label': new_status,
266 'new_status_label': new_status,
267 'pull_request': pull_request,
267 'pull_request': pull_request,
268 'commit_id': commit_id,
268 'commit_id': commit_id,
269 }
269 }
270 renderer = RstTemplateRenderer()
270 renderer = RstTemplateRenderer()
271 return renderer.render('auto_status_change.mako', **params)
271 return renderer.render('auto_status_change.mako', **params)
272
272
273 def set_status(self, repo, status, user, comment=None, revision=None,
273 def set_status(self, repo, status, user, comment=None, revision=None,
274 pull_request=None, dont_allow_on_closed_pull_request=False):
274 pull_request=None, dont_allow_on_closed_pull_request=False):
275 """
275 """
276 Creates new status for changeset or updates the old ones bumping their
276 Creates new status for changeset or updates the old ones bumping their
277 version, leaving the current status at
277 version, leaving the current status at
278
278
279 :param repo:
279 :param repo:
280 :param revision:
280 :param revision:
281 :param status:
281 :param status:
282 :param user:
282 :param user:
283 :param comment:
283 :param comment:
284 :param dont_allow_on_closed_pull_request: don't allow a status change
284 :param dont_allow_on_closed_pull_request: don't allow a status change
285 if last status was for pull request and it's closed. We shouldn't
285 if last status was for pull request and it's closed. We shouldn't
286 mess around this manually
286 mess around this manually
287 """
287 """
288 repo = self._get_repo(repo)
288 repo = self._get_repo(repo)
289
289
290 q = ChangesetStatus.query()
290 q = ChangesetStatus.query()
291
291
292 if revision:
292 if revision:
293 q = q.filter(ChangesetStatus.repo == repo)
293 q = q.filter(ChangesetStatus.repo == repo)
294 q = q.filter(ChangesetStatus.revision == revision)
294 q = q.filter(ChangesetStatus.revision == revision)
295 elif pull_request:
295 elif pull_request:
296 pull_request = self.__get_pull_request(pull_request)
296 pull_request = self.__get_pull_request(pull_request)
297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
299 cur_statuses = q.all()
299 cur_statuses = q.all()
300
300
301 # if statuses exists and last is associated with a closed pull request
301 # if statuses exists and last is associated with a closed pull request
302 # we need to check if we can allow this status change
302 # we need to check if we can allow this status change
303 if (dont_allow_on_closed_pull_request and cur_statuses
303 if (dont_allow_on_closed_pull_request and cur_statuses
304 and getattr(cur_statuses[0].pull_request, 'status', '')
304 and getattr(cur_statuses[0].pull_request, 'status', '')
305 == PullRequest.STATUS_CLOSED):
305 == PullRequest.STATUS_CLOSED):
306 raise StatusChangeOnClosedPullRequestError(
306 raise StatusChangeOnClosedPullRequestError(
307 'Changing status on closed pull request is not allowed'
307 'Changing status on closed pull request is not allowed'
308 )
308 )
309
309
310 # update all current statuses with older version
310 # update all current statuses with older version
311 if cur_statuses:
311 if cur_statuses:
312 for st in cur_statuses:
312 for st in cur_statuses:
313 st.version += 1
313 st.version += 1
314 Session().add(st)
314 Session().add(st)
315 Session().flush()
315 Session().flush()
316
316
317 def _create_status(user, repo, status, comment, revision, pull_request):
317 def _create_status(user, repo, status, comment, revision, pull_request):
318 new_status = ChangesetStatus()
318 new_status = ChangesetStatus()
319 new_status.author = self._get_user(user)
319 new_status.author = self._get_user(user)
320 new_status.repo = self._get_repo(repo)
320 new_status.repo = self._get_repo(repo)
321 new_status.status = status
321 new_status.status = status
322 new_status.comment = comment
322 new_status.comment = comment
323 new_status.revision = revision
323 new_status.revision = revision
324 new_status.pull_request = pull_request
324 new_status.pull_request = pull_request
325 return new_status
325 return new_status
326
326
327 if not comment:
327 if not comment:
328 from rhodecode.model.comment import CommentsModel
328 from rhodecode.model.comment import CommentsModel
329 comment = CommentsModel().create(
329 comment = CommentsModel().create(
330 text=self._render_auto_status_message(
330 text=self._render_auto_status_message(
331 status, commit_id=revision, pull_request=pull_request),
331 status, commit_id=revision, pull_request=pull_request),
332 repo=repo,
332 repo=repo,
333 user=user,
333 user=user,
334 pull_request=pull_request,
334 pull_request=pull_request,
335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
336 )
336 )
337
337
338 if revision:
338 if revision:
339 new_status = _create_status(
339 new_status = _create_status(
340 user=user, repo=repo, status=status, comment=comment,
340 user=user, repo=repo, status=status, comment=comment,
341 revision=revision, pull_request=pull_request)
341 revision=revision, pull_request=pull_request)
342 Session().add(new_status)
342 Session().add(new_status)
343 return new_status
343 return new_status
344 elif pull_request:
344 elif pull_request:
345 # pull request can have more than one revision associated to it
345 # pull request can have more than one revision associated to it
346 # we need to create new version for each one
346 # we need to create new version for each one
347 new_statuses = []
347 new_statuses = []
348 repo = pull_request.source_repo
348 repo = pull_request.source_repo
349 for rev in pull_request.revisions:
349 for rev in pull_request.revisions:
350 new_status = _create_status(
350 new_status = _create_status(
351 user=user, repo=repo, status=status, comment=comment,
351 user=user, repo=repo, status=status, comment=comment,
352 revision=rev, pull_request=pull_request)
352 revision=rev, pull_request=pull_request)
353 new_statuses.append(new_status)
353 new_statuses.append(new_status)
354 Session().add(new_status)
354 Session().add(new_status)
355 return new_statuses
355 return new_statuses
356
356
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data, user=None):
358
358
359 commit_statuses_map = collections.defaultdict(list)
359 commit_statuses_map = collections.defaultdict(list)
360 for st in commit_statuses:
360 for st in commit_statuses:
361 commit_statuses_map[st.author.username] += [st]
361 commit_statuses_map[st.author.username] += [st]
362
362
363 reviewers = []
363 reviewers = []
364
364
365 def version(commit_status):
365 def version(commit_status):
366 return commit_status.version
366 return commit_status.version
367
367
368 for obj in reviewers_data:
368 for obj in reviewers_data:
369 if not obj.user:
369 if not obj.user:
370 continue
370 continue
371 if user and obj.user.username != user.username:
372 # single user filter
373 continue
374
371 statuses = commit_statuses_map.get(obj.user.username, None)
375 statuses = commit_statuses_map.get(obj.user.username, None)
372 if statuses:
376 if statuses:
373 status_groups = itertools.groupby(
377 status_groups = itertools.groupby(
374 sorted(statuses, key=version), version)
378 sorted(statuses, key=version), version)
375 statuses = [(x, list(y)[0]) for x, y in status_groups]
379 statuses = [(x, list(y)[0]) for x, y in status_groups]
376
380
377 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
381 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
378
382
383 if user:
384 return reviewers[0] if reviewers else reviewers
385 else:
379 return reviewers
386 return reviewers
380
387
381 def reviewers_statuses(self, pull_request):
388 def reviewers_statuses(self, pull_request, user=None):
382 _commit_statuses = self.get_statuses(
389 _commit_statuses = self.get_statuses(
383 pull_request.source_repo,
390 pull_request.source_repo,
384 pull_request=pull_request,
391 pull_request=pull_request,
385 with_revisions=True)
392 with_revisions=True)
386 reviewers = pull_request.get_pull_request_reviewers(
393 reviewers = pull_request.get_pull_request_reviewers(
387 role=PullRequestReviewers.ROLE_REVIEWER)
394 role=PullRequestReviewers.ROLE_REVIEWER)
388 return self.aggregate_votes_by_user(_commit_statuses, reviewers)
395 return self.aggregate_votes_by_user(_commit_statuses, reviewers, user=user)
389
396
390 def calculated_review_status(self, pull_request):
397 def calculated_review_status(self, pull_request):
391 """
398 """
392 calculate pull request status based on reviewers, it should be a list
399 calculate pull request status based on reviewers, it should be a list
393 of two element lists.
400 of two element lists.
394 """
401 """
395 reviewers = self.reviewers_statuses(pull_request)
402 reviewers = self.reviewers_statuses(pull_request)
396 return self.calculate_status(reviewers)
403 return self.calculate_status(reviewers)
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,2249 +1,2372 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-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 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import (
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
48 get_current_rhodecode_user)
49 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
55 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 aliased, null, lazyload, and_, or_, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
63 EmailNotificationModel
63 EmailNotificationModel
64 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
65 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
66
66
67
67
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70
70
71 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
72 # request update.
72 # request update.
73 class UpdateResponse(object):
73 class UpdateResponse(object):
74
74
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
77
77
78 self.executed = executed
78 self.executed = executed
79 self.reason = reason
79 self.reason = reason
80 self.new = new
80 self.new = new
81 self.old = old
81 self.old = old
82 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
83 self.changes = commit_changes
83 self.changes = commit_changes
84 self.source_changed = source_changed
84 self.source_changed = source_changed
85 self.target_changed = target_changed
85 self.target_changed = target_changed
86
86
87
87
88 def get_diff_info(
88 def get_diff_info(
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 get_commit_authors=True):
90 get_commit_authors=True):
91 """
91 """
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 This is also used for default reviewers logic
93 This is also used for default reviewers logic
94 """
94 """
95
95
96 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
98
98
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 if not ancestor_id:
100 if not ancestor_id:
101 raise ValueError(
101 raise ValueError(
102 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
103 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
104
104
105 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
106 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
107 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
108 ancestor_id, source_ref)
108 ancestor_id, source_ref)
109
109
110 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
113 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
114 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
115
115
116 vcs_diff = \
116 vcs_diff = \
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
119
119
120 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
121 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
122 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
123
123
124 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
125
125
126 all_files = []
126 all_files = []
127 all_files_changes = []
127 all_files_changes = []
128 changed_lines = {}
128 changed_lines = {}
129 stats = [0, 0]
129 stats = [0, 0]
130 for f in _parsed:
130 for f in _parsed:
131 all_files.append(f['filename'])
131 all_files.append(f['filename'])
132 all_files_changes.append({
132 all_files_changes.append({
133 'filename': f['filename'],
133 'filename': f['filename'],
134 'stats': f['stats']
134 'stats': f['stats']
135 })
135 })
136 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
137 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
138
138
139 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
140 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
141 continue
141 continue
142 # first line is "context" information
142 # first line is "context" information
143 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
144 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
145 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
146 continue
146 continue
147 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
148
148
149 commit_authors = []
149 commit_authors = []
150 user_counts = {}
150 user_counts = {}
151 email_counts = {}
151 email_counts = {}
152 author_counts = {}
152 author_counts = {}
153 _commit_cache = {}
153 _commit_cache = {}
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 log.debug('Obtaining commit authors from set of commits')
157 log.debug('Obtaining commit authors from set of commits')
158 _compare_data = target_scm.compare(
158 _compare_data = target_scm.compare(
159 target_ref, source_ref, source_scm, merge=True,
159 target_ref, source_ref, source_scm, merge=True,
160 pre_load=["author", "date", "message"]
160 pre_load=["author", "date", "message"]
161 )
161 )
162
162
163 for commit in _compare_data:
163 for commit in _compare_data:
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 # at this function which is later called via JSON serialization
165 # at this function which is later called via JSON serialization
166 serialized_commit = dict(
166 serialized_commit = dict(
167 author=commit.author,
167 author=commit.author,
168 date=commit.date,
168 date=commit.date,
169 message=commit.message,
169 message=commit.message,
170 commit_id=commit.raw_id,
170 commit_id=commit.raw_id,
171 raw_id=commit.raw_id
171 raw_id=commit.raw_id
172 )
172 )
173 commits.append(serialized_commit)
173 commits.append(serialized_commit)
174 user = User.get_from_cs_author(serialized_commit['author'])
174 user = User.get_from_cs_author(serialized_commit['author'])
175 if user and user not in commit_authors:
175 if user and user not in commit_authors:
176 commit_authors.append(user)
176 commit_authors.append(user)
177
177
178 # lines
178 # lines
179 if get_authors:
179 if get_authors:
180 log.debug('Calculating authors of changed files')
180 log.debug('Calculating authors of changed files')
181 target_commit = source_repo.get_commit(ancestor_id)
181 target_commit = source_repo.get_commit(ancestor_id)
182
182
183 for fname, lines in changed_lines.items():
183 for fname, lines in changed_lines.items():
184
184
185 try:
185 try:
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
187 except Exception:
187 except Exception:
188 log.exception("Failed to load node with path %s", fname)
188 log.exception("Failed to load node with path %s", fname)
189 continue
189 continue
190
190
191 if not isinstance(node, FileNode):
191 if not isinstance(node, FileNode):
192 continue
192 continue
193
193
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
195 if node.is_binary:
195 if node.is_binary:
196 author = node.last_commit.author
196 author = node.last_commit.author
197 email = node.last_commit.author_email
197 email = node.last_commit.author_email
198
198
199 user = User.get_from_cs_author(author)
199 user = User.get_from_cs_author(author)
200 if user:
200 if user:
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
204
204
205 continue
205 continue
206
206
207 for annotation in node.annotate:
207 for annotation in node.annotate:
208 line_no, commit_id, get_commit_func, line_text = annotation
208 line_no, commit_id, get_commit_func, line_text = annotation
209 if line_no in lines:
209 if line_no in lines:
210 if commit_id not in _commit_cache:
210 if commit_id not in _commit_cache:
211 _commit_cache[commit_id] = get_commit_func()
211 _commit_cache[commit_id] = get_commit_func()
212 commit = _commit_cache[commit_id]
212 commit = _commit_cache[commit_id]
213 author = commit.author
213 author = commit.author
214 email = commit.author_email
214 email = commit.author_email
215 user = User.get_from_cs_author(author)
215 user = User.get_from_cs_author(author)
216 if user:
216 if user:
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
220
220
221 log.debug('Default reviewers processing finished')
221 log.debug('Default reviewers processing finished')
222
222
223 return {
223 return {
224 'commits': commits,
224 'commits': commits,
225 'files': all_files_changes,
225 'files': all_files_changes,
226 'stats': stats,
226 'stats': stats,
227 'ancestor': ancestor_id,
227 'ancestor': ancestor_id,
228 # original authors of modified files
228 # original authors of modified files
229 'original_authors': {
229 'original_authors': {
230 'users': user_counts,
230 'users': user_counts,
231 'authors': author_counts,
231 'authors': author_counts,
232 'emails': email_counts,
232 'emails': email_counts,
233 },
233 },
234 'commit_authors': commit_authors
234 'commit_authors': commit_authors
235 }
235 }
236
236
237
237
238 class PullRequestModel(BaseModel):
238 class PullRequestModel(BaseModel):
239
239
240 cls = PullRequest
240 cls = PullRequest
241
241
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
243
243
244 UPDATE_STATUS_MESSAGES = {
244 UPDATE_STATUS_MESSAGES = {
245 UpdateFailureReason.NONE: lazy_ugettext(
245 UpdateFailureReason.NONE: lazy_ugettext(
246 'Pull request update successful.'),
246 'Pull request update successful.'),
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
248 'Pull request update failed because of an unknown error.'),
248 'Pull request update failed because of an unknown error.'),
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
250 'No update needed because the source and target have not changed.'),
250 'No update needed because the source and target have not changed.'),
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
252 'Pull request cannot be updated because the reference type is '
252 'Pull request cannot be updated because the reference type is '
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
255 'This pull request cannot be updated because the target '
255 'This pull request cannot be updated because the target '
256 'reference is missing.'),
256 'reference is missing.'),
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
258 'This pull request cannot be updated because the source '
258 'This pull request cannot be updated because the source '
259 'reference is missing.'),
259 'reference is missing.'),
260 }
260 }
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
263
263
264 def __get_pull_request(self, pull_request):
264 def __get_pull_request(self, pull_request):
265 return self._get_instance((
265 return self._get_instance((
266 PullRequest, PullRequestVersion), pull_request)
266 PullRequest, PullRequestVersion), pull_request)
267
267
268 def _check_perms(self, perms, pull_request, user, api=False):
268 def _check_perms(self, perms, pull_request, user, api=False):
269 if not api:
269 if not api:
270 return h.HasRepoPermissionAny(*perms)(
270 return h.HasRepoPermissionAny(*perms)(
271 user=user, repo_name=pull_request.target_repo.repo_name)
271 user=user, repo_name=pull_request.target_repo.repo_name)
272 else:
272 else:
273 return h.HasRepoPermissionAnyApi(*perms)(
273 return h.HasRepoPermissionAnyApi(*perms)(
274 user=user, repo_name=pull_request.target_repo.repo_name)
274 user=user, repo_name=pull_request.target_repo.repo_name)
275
275
276 def check_user_read(self, pull_request, user, api=False):
276 def check_user_read(self, pull_request, user, api=False):
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
278 return self._check_perms(_perms, pull_request, user, api)
278 return self._check_perms(_perms, pull_request, user, api)
279
279
280 def check_user_merge(self, pull_request, user, api=False):
280 def check_user_merge(self, pull_request, user, api=False):
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
282 return self._check_perms(_perms, pull_request, user, api)
282 return self._check_perms(_perms, pull_request, user, api)
283
283
284 def check_user_update(self, pull_request, user, api=False):
284 def check_user_update(self, pull_request, user, api=False):
285 owner = user.user_id == pull_request.user_id
285 owner = user.user_id == pull_request.user_id
286 return self.check_user_merge(pull_request, user, api) or owner
286 return self.check_user_merge(pull_request, user, api) or owner
287
287
288 def check_user_delete(self, pull_request, user):
288 def check_user_delete(self, pull_request, user):
289 owner = user.user_id == pull_request.user_id
289 owner = user.user_id == pull_request.user_id
290 _perms = ('repository.admin',)
290 _perms = ('repository.admin',)
291 return self._check_perms(_perms, pull_request, user) or owner
291 return self._check_perms(_perms, pull_request, user) or owner
292
292
293 def is_user_reviewer(self, pull_request, user):
293 def is_user_reviewer(self, pull_request, user):
294 return user.user_id in [
294 return user.user_id in [
295 x.user_id for x in
295 x.user_id for x in
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
297 if x.user
297 if x.user
298 ]
298 ]
299
299
300 def check_user_change_status(self, pull_request, user, api=False):
300 def check_user_change_status(self, pull_request, user, api=False):
301 return self.check_user_update(pull_request, user, api) \
301 return self.check_user_update(pull_request, user, api) \
302 or self.is_user_reviewer(pull_request, user)
302 or self.is_user_reviewer(pull_request, user)
303
303
304 def check_user_comment(self, pull_request, user):
304 def check_user_comment(self, pull_request, user):
305 owner = user.user_id == pull_request.user_id
305 owner = user.user_id == pull_request.user_id
306 return self.check_user_read(pull_request, user) or owner
306 return self.check_user_read(pull_request, user) or owner
307
307
308 def get(self, pull_request):
308 def get(self, pull_request):
309 return self.__get_pull_request(pull_request)
309 return self.__get_pull_request(pull_request)
310
310
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
312 statuses=None, opened_by=None, order_by=None,
312 statuses=None, opened_by=None, order_by=None,
313 order_dir='desc', only_created=False):
313 order_dir='desc', only_created=False):
314 repo = None
314 repo = None
315 if repo_name:
315 if repo_name:
316 repo = self._get_repo(repo_name)
316 repo = self._get_repo(repo_name)
317
317
318 q = PullRequest.query()
318 q = PullRequest.query()
319
319
320 if search_q:
320 if search_q:
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
322 q = q.join(User)
322 q = q.join(User, User.user_id == PullRequest.user_id)
323 q = q.filter(or_(
323 q = q.filter(or_(
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
325 User.username.ilike(like_expression),
325 User.username.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
328 ))
328 ))
329
329
330 # source or target
330 # source or target
331 if repo and source:
331 if repo and source:
332 q = q.filter(PullRequest.source_repo == repo)
332 q = q.filter(PullRequest.source_repo == repo)
333 elif repo:
333 elif repo:
334 q = q.filter(PullRequest.target_repo == repo)
334 q = q.filter(PullRequest.target_repo == repo)
335
335
336 # closed,opened
336 # closed,opened
337 if statuses:
337 if statuses:
338 q = q.filter(PullRequest.status.in_(statuses))
338 q = q.filter(PullRequest.status.in_(statuses))
339
339
340 # opened by filter
340 # opened by filter
341 if opened_by:
341 if opened_by:
342 q = q.filter(PullRequest.user_id.in_(opened_by))
342 q = q.filter(PullRequest.user_id.in_(opened_by))
343
343
344 # only get those that are in "created" state
344 # only get those that are in "created" state
345 if only_created:
345 if only_created:
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
347
347
348 if order_by:
348 if order_by:
349 order_map = {
349 order_map = {
350 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
352 'title': PullRequest.title,
352 'title': PullRequest.title,
353 'updated_on_raw': PullRequest.updated_on,
353 'updated_on_raw': PullRequest.updated_on,
354 'target_repo': PullRequest.target_repo_id
354 'target_repo': PullRequest.target_repo_id
355 }
355 }
356 if order_dir == 'asc':
356 if order_dir == 'asc':
357 q = q.order_by(order_map[order_by].asc())
357 q = q.order_by(order_map[order_by].asc())
358 else:
358 else:
359 q = q.order_by(order_map[order_by].desc())
359 q = q.order_by(order_map[order_by].desc())
360
360
361 return q
361 return q
362
362
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
364 opened_by=None):
364 opened_by=None):
365 """
365 """
366 Count the number of pull requests for a specific repository.
366 Count the number of pull requests for a specific repository.
367
367
368 :param repo_name: target or source repo
368 :param repo_name: target or source repo
369 :param search_q: filter by text
369 :param search_q: filter by text
370 :param source: boolean flag to specify if repo_name refers to source
370 :param source: boolean flag to specify if repo_name refers to source
371 :param statuses: list of pull request statuses
371 :param statuses: list of pull request statuses
372 :param opened_by: author user of the pull request
372 :param opened_by: author user of the pull request
373 :returns: int number of pull requests
373 :returns: int number of pull requests
374 """
374 """
375 q = self._prepare_get_all_query(
375 q = self._prepare_get_all_query(
376 repo_name, search_q=search_q, source=source, statuses=statuses,
376 repo_name, search_q=search_q, source=source, statuses=statuses,
377 opened_by=opened_by)
377 opened_by=opened_by)
378
378
379 return q.count()
379 return q.count()
380
380
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
383 """
383 """
384 Get all pull requests for a specific repository.
384 Get all pull requests for a specific repository.
385
385
386 :param repo_name: target or source repo
386 :param repo_name: target or source repo
387 :param search_q: filter by text
387 :param search_q: filter by text
388 :param source: boolean flag to specify if repo_name refers to source
388 :param source: boolean flag to specify if repo_name refers to source
389 :param statuses: list of pull request statuses
389 :param statuses: list of pull request statuses
390 :param opened_by: author user of the pull request
390 :param opened_by: author user of the pull request
391 :param offset: pagination offset
391 :param offset: pagination offset
392 :param length: length of returned list
392 :param length: length of returned list
393 :param order_by: order of the returned list
393 :param order_by: order of the returned list
394 :param order_dir: 'asc' or 'desc' ordering direction
394 :param order_dir: 'asc' or 'desc' ordering direction
395 :returns: list of pull requests
395 :returns: list of pull requests
396 """
396 """
397 q = self._prepare_get_all_query(
397 q = self._prepare_get_all_query(
398 repo_name, search_q=search_q, source=source, statuses=statuses,
398 repo_name, search_q=search_q, source=source, statuses=statuses,
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
408 def count_awaiting_review(self, repo_name, search_q=None, statuses=None):
409 opened_by=None):
410 """
409 """
411 Count the number of pull requests for a specific repository that are
410 Count the number of pull requests for a specific repository that are
412 awaiting review.
411 awaiting review.
413
412
414 :param repo_name: target or source repo
413 :param repo_name: target or source repo
415 :param search_q: filter by text
414 :param search_q: filter by text
416 :param source: boolean flag to specify if repo_name refers to source
417 :param statuses: list of pull request statuses
415 :param statuses: list of pull request statuses
418 :param opened_by: author user of the pull request
419 :returns: int number of pull requests
416 :returns: int number of pull requests
420 """
417 """
421 pull_requests = self.get_awaiting_review(
418 pull_requests = self.get_awaiting_review(
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
419 repo_name, search_q=search_q, statuses=statuses)
423
420
424 return len(pull_requests)
421 return len(pull_requests)
425
422
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
423 def get_awaiting_review(self, repo_name, search_q=None, statuses=None,
427 opened_by=None, offset=0, length=None,
424 offset=0, length=None, order_by=None, order_dir='desc'):
428 order_by=None, order_dir='desc'):
429 """
425 """
430 Get all pull requests for a specific repository that are awaiting
426 Get all pull requests for a specific repository that are awaiting
431 review.
427 review.
432
428
433 :param repo_name: target or source repo
429 :param repo_name: target or source repo
434 :param search_q: filter by text
430 :param search_q: filter by text
435 :param source: boolean flag to specify if repo_name refers to source
436 :param statuses: list of pull request statuses
431 :param statuses: list of pull request statuses
437 :param opened_by: author user of the pull request
438 :param offset: pagination offset
432 :param offset: pagination offset
439 :param length: length of returned list
433 :param length: length of returned list
440 :param order_by: order of the returned list
434 :param order_by: order of the returned list
441 :param order_dir: 'asc' or 'desc' ordering direction
435 :param order_dir: 'asc' or 'desc' ordering direction
442 :returns: list of pull requests
436 :returns: list of pull requests
443 """
437 """
444 pull_requests = self.get_all(
438 pull_requests = self.get_all(
445 repo_name, search_q=search_q, source=source, statuses=statuses,
439 repo_name, search_q=search_q, statuses=statuses,
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
440 order_by=order_by, order_dir=order_dir)
447
441
448 _filtered_pull_requests = []
442 _filtered_pull_requests = []
449 for pr in pull_requests:
443 for pr in pull_requests:
450 status = pr.calculated_review_status()
444 status = pr.calculated_review_status()
451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
445 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
452 ChangesetStatus.STATUS_UNDER_REVIEW]:
446 ChangesetStatus.STATUS_UNDER_REVIEW]:
453 _filtered_pull_requests.append(pr)
447 _filtered_pull_requests.append(pr)
454 if length:
448 if length:
455 return _filtered_pull_requests[offset:offset+length]
449 return _filtered_pull_requests[offset:offset+length]
456 else:
450 else:
457 return _filtered_pull_requests
451 return _filtered_pull_requests
458
452
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
453 def _prepare_awaiting_my_review_review_query(
460 opened_by=None, user_id=None):
454 self, repo_name, user_id, search_q=None, statuses=None,
455 order_by=None, order_dir='desc'):
456
457 for_review_statuses = [
458 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
459 ]
460
461 pull_request_alias = aliased(PullRequest)
462 status_alias = aliased(ChangesetStatus)
463 reviewers_alias = aliased(PullRequestReviewers)
464 repo_alias = aliased(Repository)
465
466 last_ver_subq = Session()\
467 .query(func.min(ChangesetStatus.version)) \
468 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
469 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
470 .subquery()
471
472 q = Session().query(pull_request_alias) \
473 .options(lazyload(pull_request_alias.author)) \
474 .join(reviewers_alias,
475 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
476 .join(repo_alias,
477 repo_alias.repo_id == pull_request_alias.target_repo_id) \
478 .outerjoin(status_alias,
479 and_(status_alias.user_id == reviewers_alias.user_id,
480 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
481 .filter(or_(status_alias.version == null(),
482 status_alias.version == last_ver_subq)) \
483 .filter(reviewers_alias.user_id == user_id) \
484 .filter(repo_alias.repo_name == repo_name) \
485 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
486 .group_by(pull_request_alias)
487
488 # closed,opened
489 if statuses:
490 q = q.filter(pull_request_alias.status.in_(statuses))
491
492 if search_q:
493 like_expression = u'%{}%'.format(safe_unicode(search_q))
494 q = q.join(User, User.user_id == pull_request_alias.user_id)
495 q = q.filter(or_(
496 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
497 User.username.ilike(like_expression),
498 pull_request_alias.title.ilike(like_expression),
499 pull_request_alias.description.ilike(like_expression),
500 ))
501
502 if order_by:
503 order_map = {
504 'name_raw': pull_request_alias.pull_request_id,
505 'title': pull_request_alias.title,
506 'updated_on_raw': pull_request_alias.updated_on,
507 'target_repo': pull_request_alias.target_repo_id
508 }
509 if order_dir == 'asc':
510 q = q.order_by(order_map[order_by].asc())
511 else:
512 q = q.order_by(order_map[order_by].desc())
513
514 return q
515
516 def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None):
461 """
517 """
462 Count the number of pull requests for a specific repository that are
518 Count the number of pull requests for a specific repository that are
463 awaiting review from a specific user.
519 awaiting review from a specific user.
464
520
465 :param repo_name: target or source repo
521 :param repo_name: target or source repo
522 :param user_id: reviewer user of the pull request
466 :param search_q: filter by text
523 :param search_q: filter by text
467 :param source: boolean flag to specify if repo_name refers to source
468 :param statuses: list of pull request statuses
524 :param statuses: list of pull request statuses
469 :param opened_by: author user of the pull request
470 :param user_id: reviewer user of the pull request
471 :returns: int number of pull requests
525 :returns: int number of pull requests
472 """
526 """
473 pull_requests = self.get_awaiting_my_review(
527 q = self._prepare_awaiting_my_review_review_query(
474 repo_name, search_q=search_q, source=source, statuses=statuses,
528 repo_name, user_id, search_q=search_q, statuses=statuses)
475 opened_by=opened_by, user_id=user_id)
529 return q.count()
476
530
477 return len(pull_requests)
531 def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None,
478
532 offset=0, length=None, order_by=None, order_dir='desc'):
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
480 opened_by=None, user_id=None, offset=0,
481 length=None, order_by=None, order_dir='desc'):
482 """
533 """
483 Get all pull requests for a specific repository that are awaiting
534 Get all pull requests for a specific repository that are awaiting
484 review from a specific user.
535 review from a specific user.
485
536
486 :param repo_name: target or source repo
537 :param repo_name: target or source repo
538 :param user_id: reviewer user of the pull request
487 :param search_q: filter by text
539 :param search_q: filter by text
488 :param source: boolean flag to specify if repo_name refers to source
489 :param statuses: list of pull request statuses
540 :param statuses: list of pull request statuses
490 :param opened_by: author user of the pull request
491 :param user_id: reviewer user of the pull request
492 :param offset: pagination offset
541 :param offset: pagination offset
493 :param length: length of returned list
542 :param length: length of returned list
494 :param order_by: order of the returned list
543 :param order_by: order of the returned list
495 :param order_dir: 'asc' or 'desc' ordering direction
544 :param order_dir: 'asc' or 'desc' ordering direction
496 :returns: list of pull requests
545 :returns: list of pull requests
497 """
546 """
498 pull_requests = self.get_all(
499 repo_name, search_q=search_q, source=source, statuses=statuses,
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
501
547
502 _my = PullRequestModel().get_not_reviewed(user_id)
548 q = self._prepare_awaiting_my_review_review_query(
503 my_participation = []
549 repo_name, user_id, search_q=search_q, statuses=statuses,
504 for pr in pull_requests:
550 order_by=order_by, order_dir=order_dir)
505 if pr in _my:
551
506 my_participation.append(pr)
507 _filtered_pull_requests = my_participation
508 if length:
552 if length:
509 return _filtered_pull_requests[offset:offset+length]
553 pull_requests = q.limit(length).offset(offset).all()
510 else:
554 else:
511 return _filtered_pull_requests
555 pull_requests = q.all()
556
557 return pull_requests
512
558
513 def get_not_reviewed(self, user_id):
559 def _prepare_im_participating_query(self, user_id=None, statuses=None, query='',
514 return [
515 x.pull_request for x in PullRequestReviewers.query().filter(
516 PullRequestReviewers.user_id == user_id).all()
517 ]
518
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
520 order_by=None, order_dir='desc'):
560 order_by=None, order_dir='desc'):
561 """
562 return a query of pull-requests user is an creator, or he's added as a reviewer
563 """
521 q = PullRequest.query()
564 q = PullRequest.query()
522 if user_id:
565 if user_id:
523 reviewers_subquery = Session().query(
566 reviewers_subquery = Session().query(
524 PullRequestReviewers.pull_request_id).filter(
567 PullRequestReviewers.pull_request_id).filter(
525 PullRequestReviewers.user_id == user_id).subquery()
568 PullRequestReviewers.user_id == user_id).subquery()
526 user_filter = or_(
569 user_filter = or_(
527 PullRequest.user_id == user_id,
570 PullRequest.user_id == user_id,
528 PullRequest.pull_request_id.in_(reviewers_subquery)
571 PullRequest.pull_request_id.in_(reviewers_subquery)
529 )
572 )
530 q = PullRequest.query().filter(user_filter)
573 q = PullRequest.query().filter(user_filter)
531
574
532 # closed,opened
575 # closed,opened
533 if statuses:
576 if statuses:
534 q = q.filter(PullRequest.status.in_(statuses))
577 q = q.filter(PullRequest.status.in_(statuses))
535
578
536 if query:
579 if query:
537 like_expression = u'%{}%'.format(safe_unicode(query))
580 like_expression = u'%{}%'.format(safe_unicode(query))
538 q = q.join(User)
581 q = q.join(User, User.user_id == PullRequest.user_id)
539 q = q.filter(or_(
582 q = q.filter(or_(
540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
583 cast(PullRequest.pull_request_id, String).ilike(like_expression),
541 User.username.ilike(like_expression),
584 User.username.ilike(like_expression),
542 PullRequest.title.ilike(like_expression),
585 PullRequest.title.ilike(like_expression),
543 PullRequest.description.ilike(like_expression),
586 PullRequest.description.ilike(like_expression),
544 ))
587 ))
545 if order_by:
588 if order_by:
546 order_map = {
589 order_map = {
547 'name_raw': PullRequest.pull_request_id,
590 'name_raw': PullRequest.pull_request_id,
548 'title': PullRequest.title,
591 'title': PullRequest.title,
549 'updated_on_raw': PullRequest.updated_on,
592 'updated_on_raw': PullRequest.updated_on,
550 'target_repo': PullRequest.target_repo_id
593 'target_repo': PullRequest.target_repo_id
551 }
594 }
552 if order_dir == 'asc':
595 if order_dir == 'asc':
553 q = q.order_by(order_map[order_by].asc())
596 q = q.order_by(order_map[order_by].asc())
554 else:
597 else:
555 q = q.order_by(order_map[order_by].desc())
598 q = q.order_by(order_map[order_by].desc())
556
599
557 return q
600 return q
558
601
559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
602 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
603 q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query)
561 return q.count()
604 return q.count()
562
605
563 def get_im_participating_in(
606 def get_im_participating_in(
564 self, user_id=None, statuses=None, query='', offset=0,
607 self, user_id=None, statuses=None, query='', offset=0,
565 length=None, order_by=None, order_dir='desc'):
608 length=None, order_by=None, order_dir='desc'):
566 """
609 """
567 Get all Pull requests that i'm participating in, or i have opened
610 Get all Pull requests that i'm participating in as a reviewer, or i have opened
568 """
611 """
569
612
570 q = self._prepare_participating_query(
613 q = self._prepare_im_participating_query(
614 user_id, statuses=statuses, query=query, order_by=order_by,
615 order_dir=order_dir)
616
617 if length:
618 pull_requests = q.limit(length).offset(offset).all()
619 else:
620 pull_requests = q.all()
621
622 return pull_requests
623
624 def _prepare_participating_in_for_review_query(
625 self, user_id, statuses=None, query='', order_by=None, order_dir='desc'):
626
627 for_review_statuses = [
628 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
629 ]
630
631 pull_request_alias = aliased(PullRequest)
632 status_alias = aliased(ChangesetStatus)
633 reviewers_alias = aliased(PullRequestReviewers)
634
635 last_ver_subq = Session()\
636 .query(func.min(ChangesetStatus.version)) \
637 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
638 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
639 .subquery()
640
641 q = Session().query(pull_request_alias) \
642 .options(lazyload(pull_request_alias.author)) \
643 .join(reviewers_alias,
644 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
645 .outerjoin(status_alias,
646 and_(status_alias.user_id == reviewers_alias.user_id,
647 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
648 .filter(or_(status_alias.version == null(),
649 status_alias.version == last_ver_subq)) \
650 .filter(reviewers_alias.user_id == user_id) \
651 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
652 .group_by(pull_request_alias)
653
654 # closed,opened
655 if statuses:
656 q = q.filter(pull_request_alias.status.in_(statuses))
657
658 if query:
659 like_expression = u'%{}%'.format(safe_unicode(query))
660 q = q.join(User, User.user_id == pull_request_alias.user_id)
661 q = q.filter(or_(
662 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
663 User.username.ilike(like_expression),
664 pull_request_alias.title.ilike(like_expression),
665 pull_request_alias.description.ilike(like_expression),
666 ))
667
668 if order_by:
669 order_map = {
670 'name_raw': pull_request_alias.pull_request_id,
671 'title': pull_request_alias.title,
672 'updated_on_raw': pull_request_alias.updated_on,
673 'target_repo': pull_request_alias.target_repo_id
674 }
675 if order_dir == 'asc':
676 q = q.order_by(order_map[order_by].asc())
677 else:
678 q = q.order_by(order_map[order_by].desc())
679
680 return q
681
682 def count_im_participating_in_for_review(self, user_id, statuses=None, query=''):
683 q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query)
684 return q.count()
685
686 def get_im_participating_in_for_review(
687 self, user_id, statuses=None, query='', offset=0,
688 length=None, order_by=None, order_dir='desc'):
689 """
690 Get all Pull requests that needs user approval or rejection
691 """
692
693 q = self._prepare_participating_in_for_review_query(
571 user_id, statuses=statuses, query=query, order_by=order_by,
694 user_id, statuses=statuses, query=query, order_by=order_by,
572 order_dir=order_dir)
695 order_dir=order_dir)
573
696
574 if length:
697 if length:
575 pull_requests = q.limit(length).offset(offset).all()
698 pull_requests = q.limit(length).offset(offset).all()
576 else:
699 else:
577 pull_requests = q.all()
700 pull_requests = q.all()
578
701
579 return pull_requests
702 return pull_requests
580
703
581 def get_versions(self, pull_request):
704 def get_versions(self, pull_request):
582 """
705 """
583 returns version of pull request sorted by ID descending
706 returns version of pull request sorted by ID descending
584 """
707 """
585 return PullRequestVersion.query()\
708 return PullRequestVersion.query()\
586 .filter(PullRequestVersion.pull_request == pull_request)\
709 .filter(PullRequestVersion.pull_request == pull_request)\
587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
710 .order_by(PullRequestVersion.pull_request_version_id.asc())\
588 .all()
711 .all()
589
712
590 def get_pr_version(self, pull_request_id, version=None):
713 def get_pr_version(self, pull_request_id, version=None):
591 at_version = None
714 at_version = None
592
715
593 if version and version == 'latest':
716 if version and version == 'latest':
594 pull_request_ver = PullRequest.get(pull_request_id)
717 pull_request_ver = PullRequest.get(pull_request_id)
595 pull_request_obj = pull_request_ver
718 pull_request_obj = pull_request_ver
596 _org_pull_request_obj = pull_request_obj
719 _org_pull_request_obj = pull_request_obj
597 at_version = 'latest'
720 at_version = 'latest'
598 elif version:
721 elif version:
599 pull_request_ver = PullRequestVersion.get_or_404(version)
722 pull_request_ver = PullRequestVersion.get_or_404(version)
600 pull_request_obj = pull_request_ver
723 pull_request_obj = pull_request_ver
601 _org_pull_request_obj = pull_request_ver.pull_request
724 _org_pull_request_obj = pull_request_ver.pull_request
602 at_version = pull_request_ver.pull_request_version_id
725 at_version = pull_request_ver.pull_request_version_id
603 else:
726 else:
604 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
727 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
605 pull_request_id)
728 pull_request_id)
606
729
607 pull_request_display_obj = PullRequest.get_pr_display_object(
730 pull_request_display_obj = PullRequest.get_pr_display_object(
608 pull_request_obj, _org_pull_request_obj)
731 pull_request_obj, _org_pull_request_obj)
609
732
610 return _org_pull_request_obj, pull_request_obj, \
733 return _org_pull_request_obj, pull_request_obj, \
611 pull_request_display_obj, at_version
734 pull_request_display_obj, at_version
612
735
613 def pr_commits_versions(self, versions):
736 def pr_commits_versions(self, versions):
614 """
737 """
615 Maps the pull-request commits into all known PR versions. This way we can obtain
738 Maps the pull-request commits into all known PR versions. This way we can obtain
616 each pr version the commit was introduced in.
739 each pr version the commit was introduced in.
617 """
740 """
618 commit_versions = collections.defaultdict(list)
741 commit_versions = collections.defaultdict(list)
619 num_versions = [x.pull_request_version_id for x in versions]
742 num_versions = [x.pull_request_version_id for x in versions]
620 for ver in versions:
743 for ver in versions:
621 for commit_id in ver.revisions:
744 for commit_id in ver.revisions:
622 ver_idx = ChangesetComment.get_index_from_version(
745 ver_idx = ChangesetComment.get_index_from_version(
623 ver.pull_request_version_id, num_versions=num_versions)
746 ver.pull_request_version_id, num_versions=num_versions)
624 commit_versions[commit_id].append(ver_idx)
747 commit_versions[commit_id].append(ver_idx)
625 return commit_versions
748 return commit_versions
626
749
627 def create(self, created_by, source_repo, source_ref, target_repo,
750 def create(self, created_by, source_repo, source_ref, target_repo,
628 target_ref, revisions, reviewers, observers, title, description=None,
751 target_ref, revisions, reviewers, observers, title, description=None,
629 common_ancestor_id=None,
752 common_ancestor_id=None,
630 description_renderer=None,
753 description_renderer=None,
631 reviewer_data=None, translator=None, auth_user=None):
754 reviewer_data=None, translator=None, auth_user=None):
632 translator = translator or get_current_request().translate
755 translator = translator or get_current_request().translate
633
756
634 created_by_user = self._get_user(created_by)
757 created_by_user = self._get_user(created_by)
635 auth_user = auth_user or created_by_user.AuthUser()
758 auth_user = auth_user or created_by_user.AuthUser()
636 source_repo = self._get_repo(source_repo)
759 source_repo = self._get_repo(source_repo)
637 target_repo = self._get_repo(target_repo)
760 target_repo = self._get_repo(target_repo)
638
761
639 pull_request = PullRequest()
762 pull_request = PullRequest()
640 pull_request.source_repo = source_repo
763 pull_request.source_repo = source_repo
641 pull_request.source_ref = source_ref
764 pull_request.source_ref = source_ref
642 pull_request.target_repo = target_repo
765 pull_request.target_repo = target_repo
643 pull_request.target_ref = target_ref
766 pull_request.target_ref = target_ref
644 pull_request.revisions = revisions
767 pull_request.revisions = revisions
645 pull_request.title = title
768 pull_request.title = title
646 pull_request.description = description
769 pull_request.description = description
647 pull_request.description_renderer = description_renderer
770 pull_request.description_renderer = description_renderer
648 pull_request.author = created_by_user
771 pull_request.author = created_by_user
649 pull_request.reviewer_data = reviewer_data
772 pull_request.reviewer_data = reviewer_data
650 pull_request.pull_request_state = pull_request.STATE_CREATING
773 pull_request.pull_request_state = pull_request.STATE_CREATING
651 pull_request.common_ancestor_id = common_ancestor_id
774 pull_request.common_ancestor_id = common_ancestor_id
652
775
653 Session().add(pull_request)
776 Session().add(pull_request)
654 Session().flush()
777 Session().flush()
655
778
656 reviewer_ids = set()
779 reviewer_ids = set()
657 # members / reviewers
780 # members / reviewers
658 for reviewer_object in reviewers:
781 for reviewer_object in reviewers:
659 user_id, reasons, mandatory, role, rules = reviewer_object
782 user_id, reasons, mandatory, role, rules = reviewer_object
660 user = self._get_user(user_id)
783 user = self._get_user(user_id)
661
784
662 # skip duplicates
785 # skip duplicates
663 if user.user_id in reviewer_ids:
786 if user.user_id in reviewer_ids:
664 continue
787 continue
665
788
666 reviewer_ids.add(user.user_id)
789 reviewer_ids.add(user.user_id)
667
790
668 reviewer = PullRequestReviewers()
791 reviewer = PullRequestReviewers()
669 reviewer.user = user
792 reviewer.user = user
670 reviewer.pull_request = pull_request
793 reviewer.pull_request = pull_request
671 reviewer.reasons = reasons
794 reviewer.reasons = reasons
672 reviewer.mandatory = mandatory
795 reviewer.mandatory = mandatory
673 reviewer.role = role
796 reviewer.role = role
674
797
675 # NOTE(marcink): pick only first rule for now
798 # NOTE(marcink): pick only first rule for now
676 rule_id = list(rules)[0] if rules else None
799 rule_id = list(rules)[0] if rules else None
677 rule = RepoReviewRule.get(rule_id) if rule_id else None
800 rule = RepoReviewRule.get(rule_id) if rule_id else None
678 if rule:
801 if rule:
679 review_group = rule.user_group_vote_rule(user_id)
802 review_group = rule.user_group_vote_rule(user_id)
680 # we check if this particular reviewer is member of a voting group
803 # we check if this particular reviewer is member of a voting group
681 if review_group:
804 if review_group:
682 # NOTE(marcink):
805 # NOTE(marcink):
683 # can be that user is member of more but we pick the first same,
806 # can be that user is member of more but we pick the first same,
684 # same as default reviewers algo
807 # same as default reviewers algo
685 review_group = review_group[0]
808 review_group = review_group[0]
686
809
687 rule_data = {
810 rule_data = {
688 'rule_name':
811 'rule_name':
689 rule.review_rule_name,
812 rule.review_rule_name,
690 'rule_user_group_entry_id':
813 'rule_user_group_entry_id':
691 review_group.repo_review_rule_users_group_id,
814 review_group.repo_review_rule_users_group_id,
692 'rule_user_group_name':
815 'rule_user_group_name':
693 review_group.users_group.users_group_name,
816 review_group.users_group.users_group_name,
694 'rule_user_group_members':
817 'rule_user_group_members':
695 [x.user.username for x in review_group.users_group.members],
818 [x.user.username for x in review_group.users_group.members],
696 'rule_user_group_members_id':
819 'rule_user_group_members_id':
697 [x.user.user_id for x in review_group.users_group.members],
820 [x.user.user_id for x in review_group.users_group.members],
698 }
821 }
699 # e.g {'vote_rule': -1, 'mandatory': True}
822 # e.g {'vote_rule': -1, 'mandatory': True}
700 rule_data.update(review_group.rule_data())
823 rule_data.update(review_group.rule_data())
701
824
702 reviewer.rule_data = rule_data
825 reviewer.rule_data = rule_data
703
826
704 Session().add(reviewer)
827 Session().add(reviewer)
705 Session().flush()
828 Session().flush()
706
829
707 for observer_object in observers:
830 for observer_object in observers:
708 user_id, reasons, mandatory, role, rules = observer_object
831 user_id, reasons, mandatory, role, rules = observer_object
709 user = self._get_user(user_id)
832 user = self._get_user(user_id)
710
833
711 # skip duplicates from reviewers
834 # skip duplicates from reviewers
712 if user.user_id in reviewer_ids:
835 if user.user_id in reviewer_ids:
713 continue
836 continue
714
837
715 #reviewer_ids.add(user.user_id)
838 #reviewer_ids.add(user.user_id)
716
839
717 observer = PullRequestReviewers()
840 observer = PullRequestReviewers()
718 observer.user = user
841 observer.user = user
719 observer.pull_request = pull_request
842 observer.pull_request = pull_request
720 observer.reasons = reasons
843 observer.reasons = reasons
721 observer.mandatory = mandatory
844 observer.mandatory = mandatory
722 observer.role = role
845 observer.role = role
723
846
724 # NOTE(marcink): pick only first rule for now
847 # NOTE(marcink): pick only first rule for now
725 rule_id = list(rules)[0] if rules else None
848 rule_id = list(rules)[0] if rules else None
726 rule = RepoReviewRule.get(rule_id) if rule_id else None
849 rule = RepoReviewRule.get(rule_id) if rule_id else None
727 if rule:
850 if rule:
728 # TODO(marcink): do we need this for observers ??
851 # TODO(marcink): do we need this for observers ??
729 pass
852 pass
730
853
731 Session().add(observer)
854 Session().add(observer)
732 Session().flush()
855 Session().flush()
733
856
734 # Set approval status to "Under Review" for all commits which are
857 # Set approval status to "Under Review" for all commits which are
735 # part of this pull request.
858 # part of this pull request.
736 ChangesetStatusModel().set_status(
859 ChangesetStatusModel().set_status(
737 repo=target_repo,
860 repo=target_repo,
738 status=ChangesetStatus.STATUS_UNDER_REVIEW,
861 status=ChangesetStatus.STATUS_UNDER_REVIEW,
739 user=created_by_user,
862 user=created_by_user,
740 pull_request=pull_request
863 pull_request=pull_request
741 )
864 )
742 # we commit early at this point. This has to do with a fact
865 # we commit early at this point. This has to do with a fact
743 # that before queries do some row-locking. And because of that
866 # that before queries do some row-locking. And because of that
744 # we need to commit and finish transaction before below validate call
867 # we need to commit and finish transaction before below validate call
745 # that for large repos could be long resulting in long row locks
868 # that for large repos could be long resulting in long row locks
746 Session().commit()
869 Session().commit()
747
870
748 # prepare workspace, and run initial merge simulation. Set state during that
871 # prepare workspace, and run initial merge simulation. Set state during that
749 # operation
872 # operation
750 pull_request = PullRequest.get(pull_request.pull_request_id)
873 pull_request = PullRequest.get(pull_request.pull_request_id)
751
874
752 # set as merging, for merge simulation, and if finished to created so we mark
875 # set as merging, for merge simulation, and if finished to created so we mark
753 # simulation is working fine
876 # simulation is working fine
754 with pull_request.set_state(PullRequest.STATE_MERGING,
877 with pull_request.set_state(PullRequest.STATE_MERGING,
755 final_state=PullRequest.STATE_CREATED) as state_obj:
878 final_state=PullRequest.STATE_CREATED) as state_obj:
756 MergeCheck.validate(
879 MergeCheck.validate(
757 pull_request, auth_user=auth_user, translator=translator)
880 pull_request, auth_user=auth_user, translator=translator)
758
881
759 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
882 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
760 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
883 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
761
884
762 creation_data = pull_request.get_api_data(with_merge_state=False)
885 creation_data = pull_request.get_api_data(with_merge_state=False)
763 self._log_audit_action(
886 self._log_audit_action(
764 'repo.pull_request.create', {'data': creation_data},
887 'repo.pull_request.create', {'data': creation_data},
765 auth_user, pull_request)
888 auth_user, pull_request)
766
889
767 return pull_request
890 return pull_request
768
891
769 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
892 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
770 pull_request = self.__get_pull_request(pull_request)
893 pull_request = self.__get_pull_request(pull_request)
771 target_scm = pull_request.target_repo.scm_instance()
894 target_scm = pull_request.target_repo.scm_instance()
772 if action == 'create':
895 if action == 'create':
773 trigger_hook = hooks_utils.trigger_create_pull_request_hook
896 trigger_hook = hooks_utils.trigger_create_pull_request_hook
774 elif action == 'merge':
897 elif action == 'merge':
775 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
898 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
776 elif action == 'close':
899 elif action == 'close':
777 trigger_hook = hooks_utils.trigger_close_pull_request_hook
900 trigger_hook = hooks_utils.trigger_close_pull_request_hook
778 elif action == 'review_status_change':
901 elif action == 'review_status_change':
779 trigger_hook = hooks_utils.trigger_review_pull_request_hook
902 trigger_hook = hooks_utils.trigger_review_pull_request_hook
780 elif action == 'update':
903 elif action == 'update':
781 trigger_hook = hooks_utils.trigger_update_pull_request_hook
904 trigger_hook = hooks_utils.trigger_update_pull_request_hook
782 elif action == 'comment':
905 elif action == 'comment':
783 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
906 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
784 elif action == 'comment_edit':
907 elif action == 'comment_edit':
785 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
908 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
786 else:
909 else:
787 return
910 return
788
911
789 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
912 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
790 pull_request, action, trigger_hook)
913 pull_request, action, trigger_hook)
791 trigger_hook(
914 trigger_hook(
792 username=user.username,
915 username=user.username,
793 repo_name=pull_request.target_repo.repo_name,
916 repo_name=pull_request.target_repo.repo_name,
794 repo_type=target_scm.alias,
917 repo_type=target_scm.alias,
795 pull_request=pull_request,
918 pull_request=pull_request,
796 data=data)
919 data=data)
797
920
798 def _get_commit_ids(self, pull_request):
921 def _get_commit_ids(self, pull_request):
799 """
922 """
800 Return the commit ids of the merged pull request.
923 Return the commit ids of the merged pull request.
801
924
802 This method is not dealing correctly yet with the lack of autoupdates
925 This method is not dealing correctly yet with the lack of autoupdates
803 nor with the implicit target updates.
926 nor with the implicit target updates.
804 For example: if a commit in the source repo is already in the target it
927 For example: if a commit in the source repo is already in the target it
805 will be reported anyways.
928 will be reported anyways.
806 """
929 """
807 merge_rev = pull_request.merge_rev
930 merge_rev = pull_request.merge_rev
808 if merge_rev is None:
931 if merge_rev is None:
809 raise ValueError('This pull request was not merged yet')
932 raise ValueError('This pull request was not merged yet')
810
933
811 commit_ids = list(pull_request.revisions)
934 commit_ids = list(pull_request.revisions)
812 if merge_rev not in commit_ids:
935 if merge_rev not in commit_ids:
813 commit_ids.append(merge_rev)
936 commit_ids.append(merge_rev)
814
937
815 return commit_ids
938 return commit_ids
816
939
817 def merge_repo(self, pull_request, user, extras):
940 def merge_repo(self, pull_request, user, extras):
818 log.debug("Merging pull request %s", pull_request.pull_request_id)
941 log.debug("Merging pull request %s", pull_request.pull_request_id)
819 extras['user_agent'] = 'internal-merge'
942 extras['user_agent'] = 'internal-merge'
820 merge_state = self._merge_pull_request(pull_request, user, extras)
943 merge_state = self._merge_pull_request(pull_request, user, extras)
821 if merge_state.executed:
944 if merge_state.executed:
822 log.debug("Merge was successful, updating the pull request comments.")
945 log.debug("Merge was successful, updating the pull request comments.")
823 self._comment_and_close_pr(pull_request, user, merge_state)
946 self._comment_and_close_pr(pull_request, user, merge_state)
824
947
825 self._log_audit_action(
948 self._log_audit_action(
826 'repo.pull_request.merge',
949 'repo.pull_request.merge',
827 {'merge_state': merge_state.__dict__},
950 {'merge_state': merge_state.__dict__},
828 user, pull_request)
951 user, pull_request)
829
952
830 else:
953 else:
831 log.warn("Merge failed, not updating the pull request.")
954 log.warn("Merge failed, not updating the pull request.")
832 return merge_state
955 return merge_state
833
956
834 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
957 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
835 target_vcs = pull_request.target_repo.scm_instance()
958 target_vcs = pull_request.target_repo.scm_instance()
836 source_vcs = pull_request.source_repo.scm_instance()
959 source_vcs = pull_request.source_repo.scm_instance()
837
960
838 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
961 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
839 pr_id=pull_request.pull_request_id,
962 pr_id=pull_request.pull_request_id,
840 pr_title=pull_request.title,
963 pr_title=pull_request.title,
841 source_repo=source_vcs.name,
964 source_repo=source_vcs.name,
842 source_ref_name=pull_request.source_ref_parts.name,
965 source_ref_name=pull_request.source_ref_parts.name,
843 target_repo=target_vcs.name,
966 target_repo=target_vcs.name,
844 target_ref_name=pull_request.target_ref_parts.name,
967 target_ref_name=pull_request.target_ref_parts.name,
845 )
968 )
846
969
847 workspace_id = self._workspace_id(pull_request)
970 workspace_id = self._workspace_id(pull_request)
848 repo_id = pull_request.target_repo.repo_id
971 repo_id = pull_request.target_repo.repo_id
849 use_rebase = self._use_rebase_for_merging(pull_request)
972 use_rebase = self._use_rebase_for_merging(pull_request)
850 close_branch = self._close_branch_before_merging(pull_request)
973 close_branch = self._close_branch_before_merging(pull_request)
851 user_name = self._user_name_for_merging(pull_request, user)
974 user_name = self._user_name_for_merging(pull_request, user)
852
975
853 target_ref = self._refresh_reference(
976 target_ref = self._refresh_reference(
854 pull_request.target_ref_parts, target_vcs)
977 pull_request.target_ref_parts, target_vcs)
855
978
856 callback_daemon, extras = prepare_callback_daemon(
979 callback_daemon, extras = prepare_callback_daemon(
857 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
980 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
858 host=vcs_settings.HOOKS_HOST,
981 host=vcs_settings.HOOKS_HOST,
859 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
982 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
860
983
861 with callback_daemon:
984 with callback_daemon:
862 # TODO: johbo: Implement a clean way to run a config_override
985 # TODO: johbo: Implement a clean way to run a config_override
863 # for a single call.
986 # for a single call.
864 target_vcs.config.set(
987 target_vcs.config.set(
865 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
988 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
866
989
867 merge_state = target_vcs.merge(
990 merge_state = target_vcs.merge(
868 repo_id, workspace_id, target_ref, source_vcs,
991 repo_id, workspace_id, target_ref, source_vcs,
869 pull_request.source_ref_parts,
992 pull_request.source_ref_parts,
870 user_name=user_name, user_email=user.email,
993 user_name=user_name, user_email=user.email,
871 message=message, use_rebase=use_rebase,
994 message=message, use_rebase=use_rebase,
872 close_branch=close_branch)
995 close_branch=close_branch)
873 return merge_state
996 return merge_state
874
997
875 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
998 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
876 pull_request.merge_rev = merge_state.merge_ref.commit_id
999 pull_request.merge_rev = merge_state.merge_ref.commit_id
877 pull_request.updated_on = datetime.datetime.now()
1000 pull_request.updated_on = datetime.datetime.now()
878 close_msg = close_msg or 'Pull request merged and closed'
1001 close_msg = close_msg or 'Pull request merged and closed'
879
1002
880 CommentsModel().create(
1003 CommentsModel().create(
881 text=safe_unicode(close_msg),
1004 text=safe_unicode(close_msg),
882 repo=pull_request.target_repo.repo_id,
1005 repo=pull_request.target_repo.repo_id,
883 user=user.user_id,
1006 user=user.user_id,
884 pull_request=pull_request.pull_request_id,
1007 pull_request=pull_request.pull_request_id,
885 f_path=None,
1008 f_path=None,
886 line_no=None,
1009 line_no=None,
887 closing_pr=True
1010 closing_pr=True
888 )
1011 )
889
1012
890 Session().add(pull_request)
1013 Session().add(pull_request)
891 Session().flush()
1014 Session().flush()
892 # TODO: paris: replace invalidation with less radical solution
1015 # TODO: paris: replace invalidation with less radical solution
893 ScmModel().mark_for_invalidation(
1016 ScmModel().mark_for_invalidation(
894 pull_request.target_repo.repo_name)
1017 pull_request.target_repo.repo_name)
895 self.trigger_pull_request_hook(pull_request, user, 'merge')
1018 self.trigger_pull_request_hook(pull_request, user, 'merge')
896
1019
897 def has_valid_update_type(self, pull_request):
1020 def has_valid_update_type(self, pull_request):
898 source_ref_type = pull_request.source_ref_parts.type
1021 source_ref_type = pull_request.source_ref_parts.type
899 return source_ref_type in self.REF_TYPES
1022 return source_ref_type in self.REF_TYPES
900
1023
901 def get_flow_commits(self, pull_request):
1024 def get_flow_commits(self, pull_request):
902
1025
903 # source repo
1026 # source repo
904 source_ref_name = pull_request.source_ref_parts.name
1027 source_ref_name = pull_request.source_ref_parts.name
905 source_ref_type = pull_request.source_ref_parts.type
1028 source_ref_type = pull_request.source_ref_parts.type
906 source_ref_id = pull_request.source_ref_parts.commit_id
1029 source_ref_id = pull_request.source_ref_parts.commit_id
907 source_repo = pull_request.source_repo.scm_instance()
1030 source_repo = pull_request.source_repo.scm_instance()
908
1031
909 try:
1032 try:
910 if source_ref_type in self.REF_TYPES:
1033 if source_ref_type in self.REF_TYPES:
911 source_commit = source_repo.get_commit(
1034 source_commit = source_repo.get_commit(
912 source_ref_name, reference_obj=pull_request.source_ref_parts)
1035 source_ref_name, reference_obj=pull_request.source_ref_parts)
913 else:
1036 else:
914 source_commit = source_repo.get_commit(source_ref_id)
1037 source_commit = source_repo.get_commit(source_ref_id)
915 except CommitDoesNotExistError:
1038 except CommitDoesNotExistError:
916 raise SourceRefMissing()
1039 raise SourceRefMissing()
917
1040
918 # target repo
1041 # target repo
919 target_ref_name = pull_request.target_ref_parts.name
1042 target_ref_name = pull_request.target_ref_parts.name
920 target_ref_type = pull_request.target_ref_parts.type
1043 target_ref_type = pull_request.target_ref_parts.type
921 target_ref_id = pull_request.target_ref_parts.commit_id
1044 target_ref_id = pull_request.target_ref_parts.commit_id
922 target_repo = pull_request.target_repo.scm_instance()
1045 target_repo = pull_request.target_repo.scm_instance()
923
1046
924 try:
1047 try:
925 if target_ref_type in self.REF_TYPES:
1048 if target_ref_type in self.REF_TYPES:
926 target_commit = target_repo.get_commit(
1049 target_commit = target_repo.get_commit(
927 target_ref_name, reference_obj=pull_request.target_ref_parts)
1050 target_ref_name, reference_obj=pull_request.target_ref_parts)
928 else:
1051 else:
929 target_commit = target_repo.get_commit(target_ref_id)
1052 target_commit = target_repo.get_commit(target_ref_id)
930 except CommitDoesNotExistError:
1053 except CommitDoesNotExistError:
931 raise TargetRefMissing()
1054 raise TargetRefMissing()
932
1055
933 return source_commit, target_commit
1056 return source_commit, target_commit
934
1057
935 def update_commits(self, pull_request, updating_user):
1058 def update_commits(self, pull_request, updating_user):
936 """
1059 """
937 Get the updated list of commits for the pull request
1060 Get the updated list of commits for the pull request
938 and return the new pull request version and the list
1061 and return the new pull request version and the list
939 of commits processed by this update action
1062 of commits processed by this update action
940
1063
941 updating_user is the user_object who triggered the update
1064 updating_user is the user_object who triggered the update
942 """
1065 """
943 pull_request = self.__get_pull_request(pull_request)
1066 pull_request = self.__get_pull_request(pull_request)
944 source_ref_type = pull_request.source_ref_parts.type
1067 source_ref_type = pull_request.source_ref_parts.type
945 source_ref_name = pull_request.source_ref_parts.name
1068 source_ref_name = pull_request.source_ref_parts.name
946 source_ref_id = pull_request.source_ref_parts.commit_id
1069 source_ref_id = pull_request.source_ref_parts.commit_id
947
1070
948 target_ref_type = pull_request.target_ref_parts.type
1071 target_ref_type = pull_request.target_ref_parts.type
949 target_ref_name = pull_request.target_ref_parts.name
1072 target_ref_name = pull_request.target_ref_parts.name
950 target_ref_id = pull_request.target_ref_parts.commit_id
1073 target_ref_id = pull_request.target_ref_parts.commit_id
951
1074
952 if not self.has_valid_update_type(pull_request):
1075 if not self.has_valid_update_type(pull_request):
953 log.debug("Skipping update of pull request %s due to ref type: %s",
1076 log.debug("Skipping update of pull request %s due to ref type: %s",
954 pull_request, source_ref_type)
1077 pull_request, source_ref_type)
955 return UpdateResponse(
1078 return UpdateResponse(
956 executed=False,
1079 executed=False,
957 reason=UpdateFailureReason.WRONG_REF_TYPE,
1080 reason=UpdateFailureReason.WRONG_REF_TYPE,
958 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1081 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
959 source_changed=False, target_changed=False)
1082 source_changed=False, target_changed=False)
960
1083
961 try:
1084 try:
962 source_commit, target_commit = self.get_flow_commits(pull_request)
1085 source_commit, target_commit = self.get_flow_commits(pull_request)
963 except SourceRefMissing:
1086 except SourceRefMissing:
964 return UpdateResponse(
1087 return UpdateResponse(
965 executed=False,
1088 executed=False,
966 reason=UpdateFailureReason.MISSING_SOURCE_REF,
1089 reason=UpdateFailureReason.MISSING_SOURCE_REF,
967 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1090 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
968 source_changed=False, target_changed=False)
1091 source_changed=False, target_changed=False)
969 except TargetRefMissing:
1092 except TargetRefMissing:
970 return UpdateResponse(
1093 return UpdateResponse(
971 executed=False,
1094 executed=False,
972 reason=UpdateFailureReason.MISSING_TARGET_REF,
1095 reason=UpdateFailureReason.MISSING_TARGET_REF,
973 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1096 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
974 source_changed=False, target_changed=False)
1097 source_changed=False, target_changed=False)
975
1098
976 source_changed = source_ref_id != source_commit.raw_id
1099 source_changed = source_ref_id != source_commit.raw_id
977 target_changed = target_ref_id != target_commit.raw_id
1100 target_changed = target_ref_id != target_commit.raw_id
978
1101
979 if not (source_changed or target_changed):
1102 if not (source_changed or target_changed):
980 log.debug("Nothing changed in pull request %s", pull_request)
1103 log.debug("Nothing changed in pull request %s", pull_request)
981 return UpdateResponse(
1104 return UpdateResponse(
982 executed=False,
1105 executed=False,
983 reason=UpdateFailureReason.NO_CHANGE,
1106 reason=UpdateFailureReason.NO_CHANGE,
984 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1107 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
985 source_changed=target_changed, target_changed=source_changed)
1108 source_changed=target_changed, target_changed=source_changed)
986
1109
987 change_in_found = 'target repo' if target_changed else 'source repo'
1110 change_in_found = 'target repo' if target_changed else 'source repo'
988 log.debug('Updating pull request because of change in %s detected',
1111 log.debug('Updating pull request because of change in %s detected',
989 change_in_found)
1112 change_in_found)
990
1113
991 # Finally there is a need for an update, in case of source change
1114 # Finally there is a need for an update, in case of source change
992 # we create a new version, else just an update
1115 # we create a new version, else just an update
993 if source_changed:
1116 if source_changed:
994 pull_request_version = self._create_version_from_snapshot(pull_request)
1117 pull_request_version = self._create_version_from_snapshot(pull_request)
995 self._link_comments_to_version(pull_request_version)
1118 self._link_comments_to_version(pull_request_version)
996 else:
1119 else:
997 try:
1120 try:
998 ver = pull_request.versions[-1]
1121 ver = pull_request.versions[-1]
999 except IndexError:
1122 except IndexError:
1000 ver = None
1123 ver = None
1001
1124
1002 pull_request.pull_request_version_id = \
1125 pull_request.pull_request_version_id = \
1003 ver.pull_request_version_id if ver else None
1126 ver.pull_request_version_id if ver else None
1004 pull_request_version = pull_request
1127 pull_request_version = pull_request
1005
1128
1006 source_repo = pull_request.source_repo.scm_instance()
1129 source_repo = pull_request.source_repo.scm_instance()
1007 target_repo = pull_request.target_repo.scm_instance()
1130 target_repo = pull_request.target_repo.scm_instance()
1008
1131
1009 # re-compute commit ids
1132 # re-compute commit ids
1010 old_commit_ids = pull_request.revisions
1133 old_commit_ids = pull_request.revisions
1011 pre_load = ["author", "date", "message", "branch"]
1134 pre_load = ["author", "date", "message", "branch"]
1012 commit_ranges = target_repo.compare(
1135 commit_ranges = target_repo.compare(
1013 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1136 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1014 pre_load=pre_load)
1137 pre_load=pre_load)
1015
1138
1016 target_ref = target_commit.raw_id
1139 target_ref = target_commit.raw_id
1017 source_ref = source_commit.raw_id
1140 source_ref = source_commit.raw_id
1018 ancestor_commit_id = target_repo.get_common_ancestor(
1141 ancestor_commit_id = target_repo.get_common_ancestor(
1019 target_ref, source_ref, source_repo)
1142 target_ref, source_ref, source_repo)
1020
1143
1021 if not ancestor_commit_id:
1144 if not ancestor_commit_id:
1022 raise ValueError(
1145 raise ValueError(
1023 'cannot calculate diff info without a common ancestor. '
1146 'cannot calculate diff info without a common ancestor. '
1024 'Make sure both repositories are related, and have a common forking commit.')
1147 'Make sure both repositories are related, and have a common forking commit.')
1025
1148
1026 pull_request.common_ancestor_id = ancestor_commit_id
1149 pull_request.common_ancestor_id = ancestor_commit_id
1027
1150
1028 pull_request.source_ref = '%s:%s:%s' % (
1151 pull_request.source_ref = '%s:%s:%s' % (
1029 source_ref_type, source_ref_name, source_commit.raw_id)
1152 source_ref_type, source_ref_name, source_commit.raw_id)
1030 pull_request.target_ref = '%s:%s:%s' % (
1153 pull_request.target_ref = '%s:%s:%s' % (
1031 target_ref_type, target_ref_name, ancestor_commit_id)
1154 target_ref_type, target_ref_name, ancestor_commit_id)
1032
1155
1033 pull_request.revisions = [
1156 pull_request.revisions = [
1034 commit.raw_id for commit in reversed(commit_ranges)]
1157 commit.raw_id for commit in reversed(commit_ranges)]
1035 pull_request.updated_on = datetime.datetime.now()
1158 pull_request.updated_on = datetime.datetime.now()
1036 Session().add(pull_request)
1159 Session().add(pull_request)
1037 new_commit_ids = pull_request.revisions
1160 new_commit_ids = pull_request.revisions
1038
1161
1039 old_diff_data, new_diff_data = self._generate_update_diffs(
1162 old_diff_data, new_diff_data = self._generate_update_diffs(
1040 pull_request, pull_request_version)
1163 pull_request, pull_request_version)
1041
1164
1042 # calculate commit and file changes
1165 # calculate commit and file changes
1043 commit_changes = self._calculate_commit_id_changes(
1166 commit_changes = self._calculate_commit_id_changes(
1044 old_commit_ids, new_commit_ids)
1167 old_commit_ids, new_commit_ids)
1045 file_changes = self._calculate_file_changes(
1168 file_changes = self._calculate_file_changes(
1046 old_diff_data, new_diff_data)
1169 old_diff_data, new_diff_data)
1047
1170
1048 # set comments as outdated if DIFFS changed
1171 # set comments as outdated if DIFFS changed
1049 CommentsModel().outdate_comments(
1172 CommentsModel().outdate_comments(
1050 pull_request, old_diff_data=old_diff_data,
1173 pull_request, old_diff_data=old_diff_data,
1051 new_diff_data=new_diff_data)
1174 new_diff_data=new_diff_data)
1052
1175
1053 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1176 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1054 file_node_changes = (
1177 file_node_changes = (
1055 file_changes.added or file_changes.modified or file_changes.removed)
1178 file_changes.added or file_changes.modified or file_changes.removed)
1056 pr_has_changes = valid_commit_changes or file_node_changes
1179 pr_has_changes = valid_commit_changes or file_node_changes
1057
1180
1058 # Add an automatic comment to the pull request, in case
1181 # Add an automatic comment to the pull request, in case
1059 # anything has changed
1182 # anything has changed
1060 if pr_has_changes:
1183 if pr_has_changes:
1061 update_comment = CommentsModel().create(
1184 update_comment = CommentsModel().create(
1062 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1185 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1063 repo=pull_request.target_repo,
1186 repo=pull_request.target_repo,
1064 user=pull_request.author,
1187 user=pull_request.author,
1065 pull_request=pull_request,
1188 pull_request=pull_request,
1066 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1189 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1067
1190
1068 # Update status to "Under Review" for added commits
1191 # Update status to "Under Review" for added commits
1069 for commit_id in commit_changes.added:
1192 for commit_id in commit_changes.added:
1070 ChangesetStatusModel().set_status(
1193 ChangesetStatusModel().set_status(
1071 repo=pull_request.source_repo,
1194 repo=pull_request.source_repo,
1072 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1195 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1073 comment=update_comment,
1196 comment=update_comment,
1074 user=pull_request.author,
1197 user=pull_request.author,
1075 pull_request=pull_request,
1198 pull_request=pull_request,
1076 revision=commit_id)
1199 revision=commit_id)
1077
1200
1078 # send update email to users
1201 # send update email to users
1079 try:
1202 try:
1080 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1203 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1081 ancestor_commit_id=ancestor_commit_id,
1204 ancestor_commit_id=ancestor_commit_id,
1082 commit_changes=commit_changes,
1205 commit_changes=commit_changes,
1083 file_changes=file_changes)
1206 file_changes=file_changes)
1084 except Exception:
1207 except Exception:
1085 log.exception('Failed to send email notification to users')
1208 log.exception('Failed to send email notification to users')
1086
1209
1087 log.debug(
1210 log.debug(
1088 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1211 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1089 'removed_ids: %s', pull_request.pull_request_id,
1212 'removed_ids: %s', pull_request.pull_request_id,
1090 commit_changes.added, commit_changes.common, commit_changes.removed)
1213 commit_changes.added, commit_changes.common, commit_changes.removed)
1091 log.debug(
1214 log.debug(
1092 'Updated pull request with the following file changes: %s',
1215 'Updated pull request with the following file changes: %s',
1093 file_changes)
1216 file_changes)
1094
1217
1095 log.info(
1218 log.info(
1096 "Updated pull request %s from commit %s to commit %s, "
1219 "Updated pull request %s from commit %s to commit %s, "
1097 "stored new version %s of this pull request.",
1220 "stored new version %s of this pull request.",
1098 pull_request.pull_request_id, source_ref_id,
1221 pull_request.pull_request_id, source_ref_id,
1099 pull_request.source_ref_parts.commit_id,
1222 pull_request.source_ref_parts.commit_id,
1100 pull_request_version.pull_request_version_id)
1223 pull_request_version.pull_request_version_id)
1101 Session().commit()
1224 Session().commit()
1102 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1225 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1103
1226
1104 return UpdateResponse(
1227 return UpdateResponse(
1105 executed=True, reason=UpdateFailureReason.NONE,
1228 executed=True, reason=UpdateFailureReason.NONE,
1106 old=pull_request, new=pull_request_version,
1229 old=pull_request, new=pull_request_version,
1107 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1230 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1108 source_changed=source_changed, target_changed=target_changed)
1231 source_changed=source_changed, target_changed=target_changed)
1109
1232
1110 def _create_version_from_snapshot(self, pull_request):
1233 def _create_version_from_snapshot(self, pull_request):
1111 version = PullRequestVersion()
1234 version = PullRequestVersion()
1112 version.title = pull_request.title
1235 version.title = pull_request.title
1113 version.description = pull_request.description
1236 version.description = pull_request.description
1114 version.status = pull_request.status
1237 version.status = pull_request.status
1115 version.pull_request_state = pull_request.pull_request_state
1238 version.pull_request_state = pull_request.pull_request_state
1116 version.created_on = datetime.datetime.now()
1239 version.created_on = datetime.datetime.now()
1117 version.updated_on = pull_request.updated_on
1240 version.updated_on = pull_request.updated_on
1118 version.user_id = pull_request.user_id
1241 version.user_id = pull_request.user_id
1119 version.source_repo = pull_request.source_repo
1242 version.source_repo = pull_request.source_repo
1120 version.source_ref = pull_request.source_ref
1243 version.source_ref = pull_request.source_ref
1121 version.target_repo = pull_request.target_repo
1244 version.target_repo = pull_request.target_repo
1122 version.target_ref = pull_request.target_ref
1245 version.target_ref = pull_request.target_ref
1123
1246
1124 version._last_merge_source_rev = pull_request._last_merge_source_rev
1247 version._last_merge_source_rev = pull_request._last_merge_source_rev
1125 version._last_merge_target_rev = pull_request._last_merge_target_rev
1248 version._last_merge_target_rev = pull_request._last_merge_target_rev
1126 version.last_merge_status = pull_request.last_merge_status
1249 version.last_merge_status = pull_request.last_merge_status
1127 version.last_merge_metadata = pull_request.last_merge_metadata
1250 version.last_merge_metadata = pull_request.last_merge_metadata
1128 version.shadow_merge_ref = pull_request.shadow_merge_ref
1251 version.shadow_merge_ref = pull_request.shadow_merge_ref
1129 version.merge_rev = pull_request.merge_rev
1252 version.merge_rev = pull_request.merge_rev
1130 version.reviewer_data = pull_request.reviewer_data
1253 version.reviewer_data = pull_request.reviewer_data
1131
1254
1132 version.revisions = pull_request.revisions
1255 version.revisions = pull_request.revisions
1133 version.common_ancestor_id = pull_request.common_ancestor_id
1256 version.common_ancestor_id = pull_request.common_ancestor_id
1134 version.pull_request = pull_request
1257 version.pull_request = pull_request
1135 Session().add(version)
1258 Session().add(version)
1136 Session().flush()
1259 Session().flush()
1137
1260
1138 return version
1261 return version
1139
1262
1140 def _generate_update_diffs(self, pull_request, pull_request_version):
1263 def _generate_update_diffs(self, pull_request, pull_request_version):
1141
1264
1142 diff_context = (
1265 diff_context = (
1143 self.DIFF_CONTEXT +
1266 self.DIFF_CONTEXT +
1144 CommentsModel.needed_extra_diff_context())
1267 CommentsModel.needed_extra_diff_context())
1145 hide_whitespace_changes = False
1268 hide_whitespace_changes = False
1146 source_repo = pull_request_version.source_repo
1269 source_repo = pull_request_version.source_repo
1147 source_ref_id = pull_request_version.source_ref_parts.commit_id
1270 source_ref_id = pull_request_version.source_ref_parts.commit_id
1148 target_ref_id = pull_request_version.target_ref_parts.commit_id
1271 target_ref_id = pull_request_version.target_ref_parts.commit_id
1149 old_diff = self._get_diff_from_pr_or_version(
1272 old_diff = self._get_diff_from_pr_or_version(
1150 source_repo, source_ref_id, target_ref_id,
1273 source_repo, source_ref_id, target_ref_id,
1151 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1274 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1152
1275
1153 source_repo = pull_request.source_repo
1276 source_repo = pull_request.source_repo
1154 source_ref_id = pull_request.source_ref_parts.commit_id
1277 source_ref_id = pull_request.source_ref_parts.commit_id
1155 target_ref_id = pull_request.target_ref_parts.commit_id
1278 target_ref_id = pull_request.target_ref_parts.commit_id
1156
1279
1157 new_diff = self._get_diff_from_pr_or_version(
1280 new_diff = self._get_diff_from_pr_or_version(
1158 source_repo, source_ref_id, target_ref_id,
1281 source_repo, source_ref_id, target_ref_id,
1159 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1282 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1160
1283
1161 old_diff_data = diffs.DiffProcessor(old_diff)
1284 old_diff_data = diffs.DiffProcessor(old_diff)
1162 old_diff_data.prepare()
1285 old_diff_data.prepare()
1163 new_diff_data = diffs.DiffProcessor(new_diff)
1286 new_diff_data = diffs.DiffProcessor(new_diff)
1164 new_diff_data.prepare()
1287 new_diff_data.prepare()
1165
1288
1166 return old_diff_data, new_diff_data
1289 return old_diff_data, new_diff_data
1167
1290
1168 def _link_comments_to_version(self, pull_request_version):
1291 def _link_comments_to_version(self, pull_request_version):
1169 """
1292 """
1170 Link all unlinked comments of this pull request to the given version.
1293 Link all unlinked comments of this pull request to the given version.
1171
1294
1172 :param pull_request_version: The `PullRequestVersion` to which
1295 :param pull_request_version: The `PullRequestVersion` to which
1173 the comments shall be linked.
1296 the comments shall be linked.
1174
1297
1175 """
1298 """
1176 pull_request = pull_request_version.pull_request
1299 pull_request = pull_request_version.pull_request
1177 comments = ChangesetComment.query()\
1300 comments = ChangesetComment.query()\
1178 .filter(
1301 .filter(
1179 # TODO: johbo: Should we query for the repo at all here?
1302 # TODO: johbo: Should we query for the repo at all here?
1180 # Pending decision on how comments of PRs are to be related
1303 # Pending decision on how comments of PRs are to be related
1181 # to either the source repo, the target repo or no repo at all.
1304 # to either the source repo, the target repo or no repo at all.
1182 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1305 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1183 ChangesetComment.pull_request == pull_request,
1306 ChangesetComment.pull_request == pull_request,
1184 ChangesetComment.pull_request_version == None)\
1307 ChangesetComment.pull_request_version == None)\
1185 .order_by(ChangesetComment.comment_id.asc())
1308 .order_by(ChangesetComment.comment_id.asc())
1186
1309
1187 # TODO: johbo: Find out why this breaks if it is done in a bulk
1310 # TODO: johbo: Find out why this breaks if it is done in a bulk
1188 # operation.
1311 # operation.
1189 for comment in comments:
1312 for comment in comments:
1190 comment.pull_request_version_id = (
1313 comment.pull_request_version_id = (
1191 pull_request_version.pull_request_version_id)
1314 pull_request_version.pull_request_version_id)
1192 Session().add(comment)
1315 Session().add(comment)
1193
1316
1194 def _calculate_commit_id_changes(self, old_ids, new_ids):
1317 def _calculate_commit_id_changes(self, old_ids, new_ids):
1195 added = [x for x in new_ids if x not in old_ids]
1318 added = [x for x in new_ids if x not in old_ids]
1196 common = [x for x in new_ids if x in old_ids]
1319 common = [x for x in new_ids if x in old_ids]
1197 removed = [x for x in old_ids if x not in new_ids]
1320 removed = [x for x in old_ids if x not in new_ids]
1198 total = new_ids
1321 total = new_ids
1199 return ChangeTuple(added, common, removed, total)
1322 return ChangeTuple(added, common, removed, total)
1200
1323
1201 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1324 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1202
1325
1203 old_files = OrderedDict()
1326 old_files = OrderedDict()
1204 for diff_data in old_diff_data.parsed_diff:
1327 for diff_data in old_diff_data.parsed_diff:
1205 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1328 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1206
1329
1207 added_files = []
1330 added_files = []
1208 modified_files = []
1331 modified_files = []
1209 removed_files = []
1332 removed_files = []
1210 for diff_data in new_diff_data.parsed_diff:
1333 for diff_data in new_diff_data.parsed_diff:
1211 new_filename = diff_data['filename']
1334 new_filename = diff_data['filename']
1212 new_hash = md5_safe(diff_data['raw_diff'])
1335 new_hash = md5_safe(diff_data['raw_diff'])
1213
1336
1214 old_hash = old_files.get(new_filename)
1337 old_hash = old_files.get(new_filename)
1215 if not old_hash:
1338 if not old_hash:
1216 # file is not present in old diff, we have to figure out from parsed diff
1339 # file is not present in old diff, we have to figure out from parsed diff
1217 # operation ADD/REMOVE
1340 # operation ADD/REMOVE
1218 operations_dict = diff_data['stats']['ops']
1341 operations_dict = diff_data['stats']['ops']
1219 if diffs.DEL_FILENODE in operations_dict:
1342 if diffs.DEL_FILENODE in operations_dict:
1220 removed_files.append(new_filename)
1343 removed_files.append(new_filename)
1221 else:
1344 else:
1222 added_files.append(new_filename)
1345 added_files.append(new_filename)
1223 else:
1346 else:
1224 if new_hash != old_hash:
1347 if new_hash != old_hash:
1225 modified_files.append(new_filename)
1348 modified_files.append(new_filename)
1226 # now remove a file from old, since we have seen it already
1349 # now remove a file from old, since we have seen it already
1227 del old_files[new_filename]
1350 del old_files[new_filename]
1228
1351
1229 # removed files is when there are present in old, but not in NEW,
1352 # removed files is when there are present in old, but not in NEW,
1230 # since we remove old files that are present in new diff, left-overs
1353 # since we remove old files that are present in new diff, left-overs
1231 # if any should be the removed files
1354 # if any should be the removed files
1232 removed_files.extend(old_files.keys())
1355 removed_files.extend(old_files.keys())
1233
1356
1234 return FileChangeTuple(added_files, modified_files, removed_files)
1357 return FileChangeTuple(added_files, modified_files, removed_files)
1235
1358
1236 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1359 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1237 """
1360 """
1238 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1361 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1239 so it's always looking the same disregarding on which default
1362 so it's always looking the same disregarding on which default
1240 renderer system is using.
1363 renderer system is using.
1241
1364
1242 :param ancestor_commit_id: ancestor raw_id
1365 :param ancestor_commit_id: ancestor raw_id
1243 :param changes: changes named tuple
1366 :param changes: changes named tuple
1244 :param file_changes: file changes named tuple
1367 :param file_changes: file changes named tuple
1245
1368
1246 """
1369 """
1247 new_status = ChangesetStatus.get_status_lbl(
1370 new_status = ChangesetStatus.get_status_lbl(
1248 ChangesetStatus.STATUS_UNDER_REVIEW)
1371 ChangesetStatus.STATUS_UNDER_REVIEW)
1249
1372
1250 changed_files = (
1373 changed_files = (
1251 file_changes.added + file_changes.modified + file_changes.removed)
1374 file_changes.added + file_changes.modified + file_changes.removed)
1252
1375
1253 params = {
1376 params = {
1254 'under_review_label': new_status,
1377 'under_review_label': new_status,
1255 'added_commits': changes.added,
1378 'added_commits': changes.added,
1256 'removed_commits': changes.removed,
1379 'removed_commits': changes.removed,
1257 'changed_files': changed_files,
1380 'changed_files': changed_files,
1258 'added_files': file_changes.added,
1381 'added_files': file_changes.added,
1259 'modified_files': file_changes.modified,
1382 'modified_files': file_changes.modified,
1260 'removed_files': file_changes.removed,
1383 'removed_files': file_changes.removed,
1261 'ancestor_commit_id': ancestor_commit_id
1384 'ancestor_commit_id': ancestor_commit_id
1262 }
1385 }
1263 renderer = RstTemplateRenderer()
1386 renderer = RstTemplateRenderer()
1264 return renderer.render('pull_request_update.mako', **params)
1387 return renderer.render('pull_request_update.mako', **params)
1265
1388
1266 def edit(self, pull_request, title, description, description_renderer, user):
1389 def edit(self, pull_request, title, description, description_renderer, user):
1267 pull_request = self.__get_pull_request(pull_request)
1390 pull_request = self.__get_pull_request(pull_request)
1268 old_data = pull_request.get_api_data(with_merge_state=False)
1391 old_data = pull_request.get_api_data(with_merge_state=False)
1269 if pull_request.is_closed():
1392 if pull_request.is_closed():
1270 raise ValueError('This pull request is closed')
1393 raise ValueError('This pull request is closed')
1271 if title:
1394 if title:
1272 pull_request.title = title
1395 pull_request.title = title
1273 pull_request.description = description
1396 pull_request.description = description
1274 pull_request.updated_on = datetime.datetime.now()
1397 pull_request.updated_on = datetime.datetime.now()
1275 pull_request.description_renderer = description_renderer
1398 pull_request.description_renderer = description_renderer
1276 Session().add(pull_request)
1399 Session().add(pull_request)
1277 self._log_audit_action(
1400 self._log_audit_action(
1278 'repo.pull_request.edit', {'old_data': old_data},
1401 'repo.pull_request.edit', {'old_data': old_data},
1279 user, pull_request)
1402 user, pull_request)
1280
1403
1281 def update_reviewers(self, pull_request, reviewer_data, user):
1404 def update_reviewers(self, pull_request, reviewer_data, user):
1282 """
1405 """
1283 Update the reviewers in the pull request
1406 Update the reviewers in the pull request
1284
1407
1285 :param pull_request: the pr to update
1408 :param pull_request: the pr to update
1286 :param reviewer_data: list of tuples
1409 :param reviewer_data: list of tuples
1287 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1410 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1288 :param user: current use who triggers this action
1411 :param user: current use who triggers this action
1289 """
1412 """
1290
1413
1291 pull_request = self.__get_pull_request(pull_request)
1414 pull_request = self.__get_pull_request(pull_request)
1292 if pull_request.is_closed():
1415 if pull_request.is_closed():
1293 raise ValueError('This pull request is closed')
1416 raise ValueError('This pull request is closed')
1294
1417
1295 reviewers = {}
1418 reviewers = {}
1296 for user_id, reasons, mandatory, role, rules in reviewer_data:
1419 for user_id, reasons, mandatory, role, rules in reviewer_data:
1297 if isinstance(user_id, (int, compat.string_types)):
1420 if isinstance(user_id, (int, compat.string_types)):
1298 user_id = self._get_user(user_id).user_id
1421 user_id = self._get_user(user_id).user_id
1299 reviewers[user_id] = {
1422 reviewers[user_id] = {
1300 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1423 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1301
1424
1302 reviewers_ids = set(reviewers.keys())
1425 reviewers_ids = set(reviewers.keys())
1303 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1426 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1304 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1427 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1305
1428
1306 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1429 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1307
1430
1308 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1431 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1309 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1432 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1310
1433
1311 log.debug("Adding %s reviewers", ids_to_add)
1434 log.debug("Adding %s reviewers", ids_to_add)
1312 log.debug("Removing %s reviewers", ids_to_remove)
1435 log.debug("Removing %s reviewers", ids_to_remove)
1313 changed = False
1436 changed = False
1314 added_audit_reviewers = []
1437 added_audit_reviewers = []
1315 removed_audit_reviewers = []
1438 removed_audit_reviewers = []
1316
1439
1317 for uid in ids_to_add:
1440 for uid in ids_to_add:
1318 changed = True
1441 changed = True
1319 _usr = self._get_user(uid)
1442 _usr = self._get_user(uid)
1320 reviewer = PullRequestReviewers()
1443 reviewer = PullRequestReviewers()
1321 reviewer.user = _usr
1444 reviewer.user = _usr
1322 reviewer.pull_request = pull_request
1445 reviewer.pull_request = pull_request
1323 reviewer.reasons = reviewers[uid]['reasons']
1446 reviewer.reasons = reviewers[uid]['reasons']
1324 # NOTE(marcink): mandatory shouldn't be changed now
1447 # NOTE(marcink): mandatory shouldn't be changed now
1325 # reviewer.mandatory = reviewers[uid]['reasons']
1448 # reviewer.mandatory = reviewers[uid]['reasons']
1326 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1449 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1327 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1450 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1328 Session().add(reviewer)
1451 Session().add(reviewer)
1329 added_audit_reviewers.append(reviewer.get_dict())
1452 added_audit_reviewers.append(reviewer.get_dict())
1330
1453
1331 for uid in ids_to_remove:
1454 for uid in ids_to_remove:
1332 changed = True
1455 changed = True
1333 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1456 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1334 # This is an edge case that handles previous state of having the same reviewer twice.
1457 # This is an edge case that handles previous state of having the same reviewer twice.
1335 # this CAN happen due to the lack of DB checks
1458 # this CAN happen due to the lack of DB checks
1336 reviewers = PullRequestReviewers.query()\
1459 reviewers = PullRequestReviewers.query()\
1337 .filter(PullRequestReviewers.user_id == uid,
1460 .filter(PullRequestReviewers.user_id == uid,
1338 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1461 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1339 PullRequestReviewers.pull_request == pull_request)\
1462 PullRequestReviewers.pull_request == pull_request)\
1340 .all()
1463 .all()
1341
1464
1342 for obj in reviewers:
1465 for obj in reviewers:
1343 added_audit_reviewers.append(obj.get_dict())
1466 added_audit_reviewers.append(obj.get_dict())
1344 Session().delete(obj)
1467 Session().delete(obj)
1345
1468
1346 if changed:
1469 if changed:
1347 Session().expire_all()
1470 Session().expire_all()
1348 pull_request.updated_on = datetime.datetime.now()
1471 pull_request.updated_on = datetime.datetime.now()
1349 Session().add(pull_request)
1472 Session().add(pull_request)
1350
1473
1351 # finally store audit logs
1474 # finally store audit logs
1352 for user_data in added_audit_reviewers:
1475 for user_data in added_audit_reviewers:
1353 self._log_audit_action(
1476 self._log_audit_action(
1354 'repo.pull_request.reviewer.add', {'data': user_data},
1477 'repo.pull_request.reviewer.add', {'data': user_data},
1355 user, pull_request)
1478 user, pull_request)
1356 for user_data in removed_audit_reviewers:
1479 for user_data in removed_audit_reviewers:
1357 self._log_audit_action(
1480 self._log_audit_action(
1358 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1481 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1359 user, pull_request)
1482 user, pull_request)
1360
1483
1361 self.notify_reviewers(pull_request, ids_to_add, user)
1484 self.notify_reviewers(pull_request, ids_to_add, user)
1362 return ids_to_add, ids_to_remove
1485 return ids_to_add, ids_to_remove
1363
1486
1364 def update_observers(self, pull_request, observer_data, user):
1487 def update_observers(self, pull_request, observer_data, user):
1365 """
1488 """
1366 Update the observers in the pull request
1489 Update the observers in the pull request
1367
1490
1368 :param pull_request: the pr to update
1491 :param pull_request: the pr to update
1369 :param observer_data: list of tuples
1492 :param observer_data: list of tuples
1370 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1493 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1371 :param user: current use who triggers this action
1494 :param user: current use who triggers this action
1372 """
1495 """
1373 pull_request = self.__get_pull_request(pull_request)
1496 pull_request = self.__get_pull_request(pull_request)
1374 if pull_request.is_closed():
1497 if pull_request.is_closed():
1375 raise ValueError('This pull request is closed')
1498 raise ValueError('This pull request is closed')
1376
1499
1377 observers = {}
1500 observers = {}
1378 for user_id, reasons, mandatory, role, rules in observer_data:
1501 for user_id, reasons, mandatory, role, rules in observer_data:
1379 if isinstance(user_id, (int, compat.string_types)):
1502 if isinstance(user_id, (int, compat.string_types)):
1380 user_id = self._get_user(user_id).user_id
1503 user_id = self._get_user(user_id).user_id
1381 observers[user_id] = {
1504 observers[user_id] = {
1382 'reasons': reasons, 'observers': mandatory, 'role': role}
1505 'reasons': reasons, 'observers': mandatory, 'role': role}
1383
1506
1384 observers_ids = set(observers.keys())
1507 observers_ids = set(observers.keys())
1385 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1508 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1386 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1509 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1387
1510
1388 current_observers_ids = set([x.user.user_id for x in current_observers])
1511 current_observers_ids = set([x.user.user_id for x in current_observers])
1389
1512
1390 ids_to_add = observers_ids.difference(current_observers_ids)
1513 ids_to_add = observers_ids.difference(current_observers_ids)
1391 ids_to_remove = current_observers_ids.difference(observers_ids)
1514 ids_to_remove = current_observers_ids.difference(observers_ids)
1392
1515
1393 log.debug("Adding %s observer", ids_to_add)
1516 log.debug("Adding %s observer", ids_to_add)
1394 log.debug("Removing %s observer", ids_to_remove)
1517 log.debug("Removing %s observer", ids_to_remove)
1395 changed = False
1518 changed = False
1396 added_audit_observers = []
1519 added_audit_observers = []
1397 removed_audit_observers = []
1520 removed_audit_observers = []
1398
1521
1399 for uid in ids_to_add:
1522 for uid in ids_to_add:
1400 changed = True
1523 changed = True
1401 _usr = self._get_user(uid)
1524 _usr = self._get_user(uid)
1402 observer = PullRequestReviewers()
1525 observer = PullRequestReviewers()
1403 observer.user = _usr
1526 observer.user = _usr
1404 observer.pull_request = pull_request
1527 observer.pull_request = pull_request
1405 observer.reasons = observers[uid]['reasons']
1528 observer.reasons = observers[uid]['reasons']
1406 # NOTE(marcink): mandatory shouldn't be changed now
1529 # NOTE(marcink): mandatory shouldn't be changed now
1407 # observer.mandatory = observer[uid]['reasons']
1530 # observer.mandatory = observer[uid]['reasons']
1408
1531
1409 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1532 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1410 observer.role = PullRequestReviewers.ROLE_OBSERVER
1533 observer.role = PullRequestReviewers.ROLE_OBSERVER
1411 Session().add(observer)
1534 Session().add(observer)
1412 added_audit_observers.append(observer.get_dict())
1535 added_audit_observers.append(observer.get_dict())
1413
1536
1414 for uid in ids_to_remove:
1537 for uid in ids_to_remove:
1415 changed = True
1538 changed = True
1416 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1539 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1417 # This is an edge case that handles previous state of having the same reviewer twice.
1540 # This is an edge case that handles previous state of having the same reviewer twice.
1418 # this CAN happen due to the lack of DB checks
1541 # this CAN happen due to the lack of DB checks
1419 observers = PullRequestReviewers.query()\
1542 observers = PullRequestReviewers.query()\
1420 .filter(PullRequestReviewers.user_id == uid,
1543 .filter(PullRequestReviewers.user_id == uid,
1421 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1544 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1422 PullRequestReviewers.pull_request == pull_request)\
1545 PullRequestReviewers.pull_request == pull_request)\
1423 .all()
1546 .all()
1424
1547
1425 for obj in observers:
1548 for obj in observers:
1426 added_audit_observers.append(obj.get_dict())
1549 added_audit_observers.append(obj.get_dict())
1427 Session().delete(obj)
1550 Session().delete(obj)
1428
1551
1429 if changed:
1552 if changed:
1430 Session().expire_all()
1553 Session().expire_all()
1431 pull_request.updated_on = datetime.datetime.now()
1554 pull_request.updated_on = datetime.datetime.now()
1432 Session().add(pull_request)
1555 Session().add(pull_request)
1433
1556
1434 # finally store audit logs
1557 # finally store audit logs
1435 for user_data in added_audit_observers:
1558 for user_data in added_audit_observers:
1436 self._log_audit_action(
1559 self._log_audit_action(
1437 'repo.pull_request.observer.add', {'data': user_data},
1560 'repo.pull_request.observer.add', {'data': user_data},
1438 user, pull_request)
1561 user, pull_request)
1439 for user_data in removed_audit_observers:
1562 for user_data in removed_audit_observers:
1440 self._log_audit_action(
1563 self._log_audit_action(
1441 'repo.pull_request.observer.delete', {'old_data': user_data},
1564 'repo.pull_request.observer.delete', {'old_data': user_data},
1442 user, pull_request)
1565 user, pull_request)
1443
1566
1444 self.notify_observers(pull_request, ids_to_add, user)
1567 self.notify_observers(pull_request, ids_to_add, user)
1445 return ids_to_add, ids_to_remove
1568 return ids_to_add, ids_to_remove
1446
1569
1447 def get_url(self, pull_request, request=None, permalink=False):
1570 def get_url(self, pull_request, request=None, permalink=False):
1448 if not request:
1571 if not request:
1449 request = get_current_request()
1572 request = get_current_request()
1450
1573
1451 if permalink:
1574 if permalink:
1452 return request.route_url(
1575 return request.route_url(
1453 'pull_requests_global',
1576 'pull_requests_global',
1454 pull_request_id=pull_request.pull_request_id,)
1577 pull_request_id=pull_request.pull_request_id,)
1455 else:
1578 else:
1456 return request.route_url('pullrequest_show',
1579 return request.route_url('pullrequest_show',
1457 repo_name=safe_str(pull_request.target_repo.repo_name),
1580 repo_name=safe_str(pull_request.target_repo.repo_name),
1458 pull_request_id=pull_request.pull_request_id,)
1581 pull_request_id=pull_request.pull_request_id,)
1459
1582
1460 def get_shadow_clone_url(self, pull_request, request=None):
1583 def get_shadow_clone_url(self, pull_request, request=None):
1461 """
1584 """
1462 Returns qualified url pointing to the shadow repository. If this pull
1585 Returns qualified url pointing to the shadow repository. If this pull
1463 request is closed there is no shadow repository and ``None`` will be
1586 request is closed there is no shadow repository and ``None`` will be
1464 returned.
1587 returned.
1465 """
1588 """
1466 if pull_request.is_closed():
1589 if pull_request.is_closed():
1467 return None
1590 return None
1468 else:
1591 else:
1469 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1592 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1470 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1593 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1471
1594
1472 def _notify_reviewers(self, pull_request, user_ids, role, user):
1595 def _notify_reviewers(self, pull_request, user_ids, role, user):
1473 # notification to reviewers/observers
1596 # notification to reviewers/observers
1474 if not user_ids:
1597 if not user_ids:
1475 return
1598 return
1476
1599
1477 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1600 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1478
1601
1479 pull_request_obj = pull_request
1602 pull_request_obj = pull_request
1480 # get the current participants of this pull request
1603 # get the current participants of this pull request
1481 recipients = user_ids
1604 recipients = user_ids
1482 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1605 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1483
1606
1484 pr_source_repo = pull_request_obj.source_repo
1607 pr_source_repo = pull_request_obj.source_repo
1485 pr_target_repo = pull_request_obj.target_repo
1608 pr_target_repo = pull_request_obj.target_repo
1486
1609
1487 pr_url = h.route_url('pullrequest_show',
1610 pr_url = h.route_url('pullrequest_show',
1488 repo_name=pr_target_repo.repo_name,
1611 repo_name=pr_target_repo.repo_name,
1489 pull_request_id=pull_request_obj.pull_request_id,)
1612 pull_request_id=pull_request_obj.pull_request_id,)
1490
1613
1491 # set some variables for email notification
1614 # set some variables for email notification
1492 pr_target_repo_url = h.route_url(
1615 pr_target_repo_url = h.route_url(
1493 'repo_summary', repo_name=pr_target_repo.repo_name)
1616 'repo_summary', repo_name=pr_target_repo.repo_name)
1494
1617
1495 pr_source_repo_url = h.route_url(
1618 pr_source_repo_url = h.route_url(
1496 'repo_summary', repo_name=pr_source_repo.repo_name)
1619 'repo_summary', repo_name=pr_source_repo.repo_name)
1497
1620
1498 # pull request specifics
1621 # pull request specifics
1499 pull_request_commits = [
1622 pull_request_commits = [
1500 (x.raw_id, x.message)
1623 (x.raw_id, x.message)
1501 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1624 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1502
1625
1503 current_rhodecode_user = user
1626 current_rhodecode_user = user
1504 kwargs = {
1627 kwargs = {
1505 'user': current_rhodecode_user,
1628 'user': current_rhodecode_user,
1506 'pull_request_author': pull_request.author,
1629 'pull_request_author': pull_request.author,
1507 'pull_request': pull_request_obj,
1630 'pull_request': pull_request_obj,
1508 'pull_request_commits': pull_request_commits,
1631 'pull_request_commits': pull_request_commits,
1509
1632
1510 'pull_request_target_repo': pr_target_repo,
1633 'pull_request_target_repo': pr_target_repo,
1511 'pull_request_target_repo_url': pr_target_repo_url,
1634 'pull_request_target_repo_url': pr_target_repo_url,
1512
1635
1513 'pull_request_source_repo': pr_source_repo,
1636 'pull_request_source_repo': pr_source_repo,
1514 'pull_request_source_repo_url': pr_source_repo_url,
1637 'pull_request_source_repo_url': pr_source_repo_url,
1515
1638
1516 'pull_request_url': pr_url,
1639 'pull_request_url': pr_url,
1517 'thread_ids': [pr_url],
1640 'thread_ids': [pr_url],
1518 'user_role': role
1641 'user_role': role
1519 }
1642 }
1520
1643
1521 # create notification objects, and emails
1644 # create notification objects, and emails
1522 NotificationModel().create(
1645 NotificationModel().create(
1523 created_by=current_rhodecode_user,
1646 created_by=current_rhodecode_user,
1524 notification_subject='', # Filled in based on the notification_type
1647 notification_subject='', # Filled in based on the notification_type
1525 notification_body='', # Filled in based on the notification_type
1648 notification_body='', # Filled in based on the notification_type
1526 notification_type=notification_type,
1649 notification_type=notification_type,
1527 recipients=recipients,
1650 recipients=recipients,
1528 email_kwargs=kwargs,
1651 email_kwargs=kwargs,
1529 )
1652 )
1530
1653
1531 def notify_reviewers(self, pull_request, reviewers_ids, user):
1654 def notify_reviewers(self, pull_request, reviewers_ids, user):
1532 return self._notify_reviewers(pull_request, reviewers_ids,
1655 return self._notify_reviewers(pull_request, reviewers_ids,
1533 PullRequestReviewers.ROLE_REVIEWER, user)
1656 PullRequestReviewers.ROLE_REVIEWER, user)
1534
1657
1535 def notify_observers(self, pull_request, observers_ids, user):
1658 def notify_observers(self, pull_request, observers_ids, user):
1536 return self._notify_reviewers(pull_request, observers_ids,
1659 return self._notify_reviewers(pull_request, observers_ids,
1537 PullRequestReviewers.ROLE_OBSERVER, user)
1660 PullRequestReviewers.ROLE_OBSERVER, user)
1538
1661
1539 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1662 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1540 commit_changes, file_changes):
1663 commit_changes, file_changes):
1541
1664
1542 updating_user_id = updating_user.user_id
1665 updating_user_id = updating_user.user_id
1543 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1666 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1544 # NOTE(marcink): send notification to all other users except to
1667 # NOTE(marcink): send notification to all other users except to
1545 # person who updated the PR
1668 # person who updated the PR
1546 recipients = reviewers.difference(set([updating_user_id]))
1669 recipients = reviewers.difference(set([updating_user_id]))
1547
1670
1548 log.debug('Notify following recipients about pull-request update %s', recipients)
1671 log.debug('Notify following recipients about pull-request update %s', recipients)
1549
1672
1550 pull_request_obj = pull_request
1673 pull_request_obj = pull_request
1551
1674
1552 # send email about the update
1675 # send email about the update
1553 changed_files = (
1676 changed_files = (
1554 file_changes.added + file_changes.modified + file_changes.removed)
1677 file_changes.added + file_changes.modified + file_changes.removed)
1555
1678
1556 pr_source_repo = pull_request_obj.source_repo
1679 pr_source_repo = pull_request_obj.source_repo
1557 pr_target_repo = pull_request_obj.target_repo
1680 pr_target_repo = pull_request_obj.target_repo
1558
1681
1559 pr_url = h.route_url('pullrequest_show',
1682 pr_url = h.route_url('pullrequest_show',
1560 repo_name=pr_target_repo.repo_name,
1683 repo_name=pr_target_repo.repo_name,
1561 pull_request_id=pull_request_obj.pull_request_id,)
1684 pull_request_id=pull_request_obj.pull_request_id,)
1562
1685
1563 # set some variables for email notification
1686 # set some variables for email notification
1564 pr_target_repo_url = h.route_url(
1687 pr_target_repo_url = h.route_url(
1565 'repo_summary', repo_name=pr_target_repo.repo_name)
1688 'repo_summary', repo_name=pr_target_repo.repo_name)
1566
1689
1567 pr_source_repo_url = h.route_url(
1690 pr_source_repo_url = h.route_url(
1568 'repo_summary', repo_name=pr_source_repo.repo_name)
1691 'repo_summary', repo_name=pr_source_repo.repo_name)
1569
1692
1570 email_kwargs = {
1693 email_kwargs = {
1571 'date': datetime.datetime.now(),
1694 'date': datetime.datetime.now(),
1572 'updating_user': updating_user,
1695 'updating_user': updating_user,
1573
1696
1574 'pull_request': pull_request_obj,
1697 'pull_request': pull_request_obj,
1575
1698
1576 'pull_request_target_repo': pr_target_repo,
1699 'pull_request_target_repo': pr_target_repo,
1577 'pull_request_target_repo_url': pr_target_repo_url,
1700 'pull_request_target_repo_url': pr_target_repo_url,
1578
1701
1579 'pull_request_source_repo': pr_source_repo,
1702 'pull_request_source_repo': pr_source_repo,
1580 'pull_request_source_repo_url': pr_source_repo_url,
1703 'pull_request_source_repo_url': pr_source_repo_url,
1581
1704
1582 'pull_request_url': pr_url,
1705 'pull_request_url': pr_url,
1583
1706
1584 'ancestor_commit_id': ancestor_commit_id,
1707 'ancestor_commit_id': ancestor_commit_id,
1585 'added_commits': commit_changes.added,
1708 'added_commits': commit_changes.added,
1586 'removed_commits': commit_changes.removed,
1709 'removed_commits': commit_changes.removed,
1587 'changed_files': changed_files,
1710 'changed_files': changed_files,
1588 'added_files': file_changes.added,
1711 'added_files': file_changes.added,
1589 'modified_files': file_changes.modified,
1712 'modified_files': file_changes.modified,
1590 'removed_files': file_changes.removed,
1713 'removed_files': file_changes.removed,
1591 'thread_ids': [pr_url],
1714 'thread_ids': [pr_url],
1592 }
1715 }
1593
1716
1594 # create notification objects, and emails
1717 # create notification objects, and emails
1595 NotificationModel().create(
1718 NotificationModel().create(
1596 created_by=updating_user,
1719 created_by=updating_user,
1597 notification_subject='', # Filled in based on the notification_type
1720 notification_subject='', # Filled in based on the notification_type
1598 notification_body='', # Filled in based on the notification_type
1721 notification_body='', # Filled in based on the notification_type
1599 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1722 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1600 recipients=recipients,
1723 recipients=recipients,
1601 email_kwargs=email_kwargs,
1724 email_kwargs=email_kwargs,
1602 )
1725 )
1603
1726
1604 def delete(self, pull_request, user=None):
1727 def delete(self, pull_request, user=None):
1605 if not user:
1728 if not user:
1606 user = getattr(get_current_rhodecode_user(), 'username', None)
1729 user = getattr(get_current_rhodecode_user(), 'username', None)
1607
1730
1608 pull_request = self.__get_pull_request(pull_request)
1731 pull_request = self.__get_pull_request(pull_request)
1609 old_data = pull_request.get_api_data(with_merge_state=False)
1732 old_data = pull_request.get_api_data(with_merge_state=False)
1610 self._cleanup_merge_workspace(pull_request)
1733 self._cleanup_merge_workspace(pull_request)
1611 self._log_audit_action(
1734 self._log_audit_action(
1612 'repo.pull_request.delete', {'old_data': old_data},
1735 'repo.pull_request.delete', {'old_data': old_data},
1613 user, pull_request)
1736 user, pull_request)
1614 Session().delete(pull_request)
1737 Session().delete(pull_request)
1615
1738
1616 def close_pull_request(self, pull_request, user):
1739 def close_pull_request(self, pull_request, user):
1617 pull_request = self.__get_pull_request(pull_request)
1740 pull_request = self.__get_pull_request(pull_request)
1618 self._cleanup_merge_workspace(pull_request)
1741 self._cleanup_merge_workspace(pull_request)
1619 pull_request.status = PullRequest.STATUS_CLOSED
1742 pull_request.status = PullRequest.STATUS_CLOSED
1620 pull_request.updated_on = datetime.datetime.now()
1743 pull_request.updated_on = datetime.datetime.now()
1621 Session().add(pull_request)
1744 Session().add(pull_request)
1622 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1745 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1623
1746
1624 pr_data = pull_request.get_api_data(with_merge_state=False)
1747 pr_data = pull_request.get_api_data(with_merge_state=False)
1625 self._log_audit_action(
1748 self._log_audit_action(
1626 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1749 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1627
1750
1628 def close_pull_request_with_comment(
1751 def close_pull_request_with_comment(
1629 self, pull_request, user, repo, message=None, auth_user=None):
1752 self, pull_request, user, repo, message=None, auth_user=None):
1630
1753
1631 pull_request_review_status = pull_request.calculated_review_status()
1754 pull_request_review_status = pull_request.calculated_review_status()
1632
1755
1633 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1756 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1634 # approved only if we have voting consent
1757 # approved only if we have voting consent
1635 status = ChangesetStatus.STATUS_APPROVED
1758 status = ChangesetStatus.STATUS_APPROVED
1636 else:
1759 else:
1637 status = ChangesetStatus.STATUS_REJECTED
1760 status = ChangesetStatus.STATUS_REJECTED
1638 status_lbl = ChangesetStatus.get_status_lbl(status)
1761 status_lbl = ChangesetStatus.get_status_lbl(status)
1639
1762
1640 default_message = (
1763 default_message = (
1641 'Closing with status change {transition_icon} {status}.'
1764 'Closing with status change {transition_icon} {status}.'
1642 ).format(transition_icon='>', status=status_lbl)
1765 ).format(transition_icon='>', status=status_lbl)
1643 text = message or default_message
1766 text = message or default_message
1644
1767
1645 # create a comment, and link it to new status
1768 # create a comment, and link it to new status
1646 comment = CommentsModel().create(
1769 comment = CommentsModel().create(
1647 text=text,
1770 text=text,
1648 repo=repo.repo_id,
1771 repo=repo.repo_id,
1649 user=user.user_id,
1772 user=user.user_id,
1650 pull_request=pull_request.pull_request_id,
1773 pull_request=pull_request.pull_request_id,
1651 status_change=status_lbl,
1774 status_change=status_lbl,
1652 status_change_type=status,
1775 status_change_type=status,
1653 closing_pr=True,
1776 closing_pr=True,
1654 auth_user=auth_user,
1777 auth_user=auth_user,
1655 )
1778 )
1656
1779
1657 # calculate old status before we change it
1780 # calculate old status before we change it
1658 old_calculated_status = pull_request.calculated_review_status()
1781 old_calculated_status = pull_request.calculated_review_status()
1659 ChangesetStatusModel().set_status(
1782 ChangesetStatusModel().set_status(
1660 repo.repo_id,
1783 repo.repo_id,
1661 status,
1784 status,
1662 user.user_id,
1785 user.user_id,
1663 comment=comment,
1786 comment=comment,
1664 pull_request=pull_request.pull_request_id
1787 pull_request=pull_request.pull_request_id
1665 )
1788 )
1666
1789
1667 Session().flush()
1790 Session().flush()
1668
1791
1669 self.trigger_pull_request_hook(pull_request, user, 'comment',
1792 self.trigger_pull_request_hook(pull_request, user, 'comment',
1670 data={'comment': comment})
1793 data={'comment': comment})
1671
1794
1672 # we now calculate the status of pull request again, and based on that
1795 # we now calculate the status of pull request again, and based on that
1673 # calculation trigger status change. This might happen in cases
1796 # calculation trigger status change. This might happen in cases
1674 # that non-reviewer admin closes a pr, which means his vote doesn't
1797 # that non-reviewer admin closes a pr, which means his vote doesn't
1675 # change the status, while if he's a reviewer this might change it.
1798 # change the status, while if he's a reviewer this might change it.
1676 calculated_status = pull_request.calculated_review_status()
1799 calculated_status = pull_request.calculated_review_status()
1677 if old_calculated_status != calculated_status:
1800 if old_calculated_status != calculated_status:
1678 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1801 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1679 data={'status': calculated_status})
1802 data={'status': calculated_status})
1680
1803
1681 # finally close the PR
1804 # finally close the PR
1682 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1805 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1683
1806
1684 return comment, status
1807 return comment, status
1685
1808
1686 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1809 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1687 _ = translator or get_current_request().translate
1810 _ = translator or get_current_request().translate
1688
1811
1689 if not self._is_merge_enabled(pull_request):
1812 if not self._is_merge_enabled(pull_request):
1690 return None, False, _('Server-side pull request merging is disabled.')
1813 return None, False, _('Server-side pull request merging is disabled.')
1691
1814
1692 if pull_request.is_closed():
1815 if pull_request.is_closed():
1693 return None, False, _('This pull request is closed.')
1816 return None, False, _('This pull request is closed.')
1694
1817
1695 merge_possible, msg = self._check_repo_requirements(
1818 merge_possible, msg = self._check_repo_requirements(
1696 target=pull_request.target_repo, source=pull_request.source_repo,
1819 target=pull_request.target_repo, source=pull_request.source_repo,
1697 translator=_)
1820 translator=_)
1698 if not merge_possible:
1821 if not merge_possible:
1699 return None, merge_possible, msg
1822 return None, merge_possible, msg
1700
1823
1701 try:
1824 try:
1702 merge_response = self._try_merge(
1825 merge_response = self._try_merge(
1703 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1826 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1704 log.debug("Merge response: %s", merge_response)
1827 log.debug("Merge response: %s", merge_response)
1705 return merge_response, merge_response.possible, merge_response.merge_status_message
1828 return merge_response, merge_response.possible, merge_response.merge_status_message
1706 except NotImplementedError:
1829 except NotImplementedError:
1707 return None, False, _('Pull request merging is not supported.')
1830 return None, False, _('Pull request merging is not supported.')
1708
1831
1709 def _check_repo_requirements(self, target, source, translator):
1832 def _check_repo_requirements(self, target, source, translator):
1710 """
1833 """
1711 Check if `target` and `source` have compatible requirements.
1834 Check if `target` and `source` have compatible requirements.
1712
1835
1713 Currently this is just checking for largefiles.
1836 Currently this is just checking for largefiles.
1714 """
1837 """
1715 _ = translator
1838 _ = translator
1716 target_has_largefiles = self._has_largefiles(target)
1839 target_has_largefiles = self._has_largefiles(target)
1717 source_has_largefiles = self._has_largefiles(source)
1840 source_has_largefiles = self._has_largefiles(source)
1718 merge_possible = True
1841 merge_possible = True
1719 message = u''
1842 message = u''
1720
1843
1721 if target_has_largefiles != source_has_largefiles:
1844 if target_has_largefiles != source_has_largefiles:
1722 merge_possible = False
1845 merge_possible = False
1723 if source_has_largefiles:
1846 if source_has_largefiles:
1724 message = _(
1847 message = _(
1725 'Target repository large files support is disabled.')
1848 'Target repository large files support is disabled.')
1726 else:
1849 else:
1727 message = _(
1850 message = _(
1728 'Source repository large files support is disabled.')
1851 'Source repository large files support is disabled.')
1729
1852
1730 return merge_possible, message
1853 return merge_possible, message
1731
1854
1732 def _has_largefiles(self, repo):
1855 def _has_largefiles(self, repo):
1733 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1856 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1734 'extensions', 'largefiles')
1857 'extensions', 'largefiles')
1735 return largefiles_ui and largefiles_ui[0].active
1858 return largefiles_ui and largefiles_ui[0].active
1736
1859
1737 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1860 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1738 """
1861 """
1739 Try to merge the pull request and return the merge status.
1862 Try to merge the pull request and return the merge status.
1740 """
1863 """
1741 log.debug(
1864 log.debug(
1742 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1865 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1743 pull_request.pull_request_id, force_shadow_repo_refresh)
1866 pull_request.pull_request_id, force_shadow_repo_refresh)
1744 target_vcs = pull_request.target_repo.scm_instance()
1867 target_vcs = pull_request.target_repo.scm_instance()
1745 # Refresh the target reference.
1868 # Refresh the target reference.
1746 try:
1869 try:
1747 target_ref = self._refresh_reference(
1870 target_ref = self._refresh_reference(
1748 pull_request.target_ref_parts, target_vcs)
1871 pull_request.target_ref_parts, target_vcs)
1749 except CommitDoesNotExistError:
1872 except CommitDoesNotExistError:
1750 merge_state = MergeResponse(
1873 merge_state = MergeResponse(
1751 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1874 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1752 metadata={'target_ref': pull_request.target_ref_parts})
1875 metadata={'target_ref': pull_request.target_ref_parts})
1753 return merge_state
1876 return merge_state
1754
1877
1755 target_locked = pull_request.target_repo.locked
1878 target_locked = pull_request.target_repo.locked
1756 if target_locked and target_locked[0]:
1879 if target_locked and target_locked[0]:
1757 locked_by = 'user:{}'.format(target_locked[0])
1880 locked_by = 'user:{}'.format(target_locked[0])
1758 log.debug("The target repository is locked by %s.", locked_by)
1881 log.debug("The target repository is locked by %s.", locked_by)
1759 merge_state = MergeResponse(
1882 merge_state = MergeResponse(
1760 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1883 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1761 metadata={'locked_by': locked_by})
1884 metadata={'locked_by': locked_by})
1762 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1885 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1763 pull_request, target_ref):
1886 pull_request, target_ref):
1764 log.debug("Refreshing the merge status of the repository.")
1887 log.debug("Refreshing the merge status of the repository.")
1765 merge_state = self._refresh_merge_state(
1888 merge_state = self._refresh_merge_state(
1766 pull_request, target_vcs, target_ref)
1889 pull_request, target_vcs, target_ref)
1767 else:
1890 else:
1768 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1891 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1769 metadata = {
1892 metadata = {
1770 'unresolved_files': '',
1893 'unresolved_files': '',
1771 'target_ref': pull_request.target_ref_parts,
1894 'target_ref': pull_request.target_ref_parts,
1772 'source_ref': pull_request.source_ref_parts,
1895 'source_ref': pull_request.source_ref_parts,
1773 }
1896 }
1774 if pull_request.last_merge_metadata:
1897 if pull_request.last_merge_metadata:
1775 metadata.update(pull_request.last_merge_metadata_parsed)
1898 metadata.update(pull_request.last_merge_metadata_parsed)
1776
1899
1777 if not possible and target_ref.type == 'branch':
1900 if not possible and target_ref.type == 'branch':
1778 # NOTE(marcink): case for mercurial multiple heads on branch
1901 # NOTE(marcink): case for mercurial multiple heads on branch
1779 heads = target_vcs._heads(target_ref.name)
1902 heads = target_vcs._heads(target_ref.name)
1780 if len(heads) != 1:
1903 if len(heads) != 1:
1781 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1904 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1782 metadata.update({
1905 metadata.update({
1783 'heads': heads
1906 'heads': heads
1784 })
1907 })
1785
1908
1786 merge_state = MergeResponse(
1909 merge_state = MergeResponse(
1787 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1910 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1788
1911
1789 return merge_state
1912 return merge_state
1790
1913
1791 def _refresh_reference(self, reference, vcs_repository):
1914 def _refresh_reference(self, reference, vcs_repository):
1792 if reference.type in self.UPDATABLE_REF_TYPES:
1915 if reference.type in self.UPDATABLE_REF_TYPES:
1793 name_or_id = reference.name
1916 name_or_id = reference.name
1794 else:
1917 else:
1795 name_or_id = reference.commit_id
1918 name_or_id = reference.commit_id
1796
1919
1797 refreshed_commit = vcs_repository.get_commit(name_or_id)
1920 refreshed_commit = vcs_repository.get_commit(name_or_id)
1798 refreshed_reference = Reference(
1921 refreshed_reference = Reference(
1799 reference.type, reference.name, refreshed_commit.raw_id)
1922 reference.type, reference.name, refreshed_commit.raw_id)
1800 return refreshed_reference
1923 return refreshed_reference
1801
1924
1802 def _needs_merge_state_refresh(self, pull_request, target_reference):
1925 def _needs_merge_state_refresh(self, pull_request, target_reference):
1803 return not(
1926 return not(
1804 pull_request.revisions and
1927 pull_request.revisions and
1805 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1928 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1806 target_reference.commit_id == pull_request._last_merge_target_rev)
1929 target_reference.commit_id == pull_request._last_merge_target_rev)
1807
1930
1808 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1931 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1809 workspace_id = self._workspace_id(pull_request)
1932 workspace_id = self._workspace_id(pull_request)
1810 source_vcs = pull_request.source_repo.scm_instance()
1933 source_vcs = pull_request.source_repo.scm_instance()
1811 repo_id = pull_request.target_repo.repo_id
1934 repo_id = pull_request.target_repo.repo_id
1812 use_rebase = self._use_rebase_for_merging(pull_request)
1935 use_rebase = self._use_rebase_for_merging(pull_request)
1813 close_branch = self._close_branch_before_merging(pull_request)
1936 close_branch = self._close_branch_before_merging(pull_request)
1814 merge_state = target_vcs.merge(
1937 merge_state = target_vcs.merge(
1815 repo_id, workspace_id,
1938 repo_id, workspace_id,
1816 target_reference, source_vcs, pull_request.source_ref_parts,
1939 target_reference, source_vcs, pull_request.source_ref_parts,
1817 dry_run=True, use_rebase=use_rebase,
1940 dry_run=True, use_rebase=use_rebase,
1818 close_branch=close_branch)
1941 close_branch=close_branch)
1819
1942
1820 # Do not store the response if there was an unknown error.
1943 # Do not store the response if there was an unknown error.
1821 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1944 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1822 pull_request._last_merge_source_rev = \
1945 pull_request._last_merge_source_rev = \
1823 pull_request.source_ref_parts.commit_id
1946 pull_request.source_ref_parts.commit_id
1824 pull_request._last_merge_target_rev = target_reference.commit_id
1947 pull_request._last_merge_target_rev = target_reference.commit_id
1825 pull_request.last_merge_status = merge_state.failure_reason
1948 pull_request.last_merge_status = merge_state.failure_reason
1826 pull_request.last_merge_metadata = merge_state.metadata
1949 pull_request.last_merge_metadata = merge_state.metadata
1827
1950
1828 pull_request.shadow_merge_ref = merge_state.merge_ref
1951 pull_request.shadow_merge_ref = merge_state.merge_ref
1829 Session().add(pull_request)
1952 Session().add(pull_request)
1830 Session().commit()
1953 Session().commit()
1831
1954
1832 return merge_state
1955 return merge_state
1833
1956
1834 def _workspace_id(self, pull_request):
1957 def _workspace_id(self, pull_request):
1835 workspace_id = 'pr-%s' % pull_request.pull_request_id
1958 workspace_id = 'pr-%s' % pull_request.pull_request_id
1836 return workspace_id
1959 return workspace_id
1837
1960
1838 def generate_repo_data(self, repo, commit_id=None, branch=None,
1961 def generate_repo_data(self, repo, commit_id=None, branch=None,
1839 bookmark=None, translator=None):
1962 bookmark=None, translator=None):
1840 from rhodecode.model.repo import RepoModel
1963 from rhodecode.model.repo import RepoModel
1841
1964
1842 all_refs, selected_ref = \
1965 all_refs, selected_ref = \
1843 self._get_repo_pullrequest_sources(
1966 self._get_repo_pullrequest_sources(
1844 repo.scm_instance(), commit_id=commit_id,
1967 repo.scm_instance(), commit_id=commit_id,
1845 branch=branch, bookmark=bookmark, translator=translator)
1968 branch=branch, bookmark=bookmark, translator=translator)
1846
1969
1847 refs_select2 = []
1970 refs_select2 = []
1848 for element in all_refs:
1971 for element in all_refs:
1849 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1972 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1850 refs_select2.append({'text': element[1], 'children': children})
1973 refs_select2.append({'text': element[1], 'children': children})
1851
1974
1852 return {
1975 return {
1853 'user': {
1976 'user': {
1854 'user_id': repo.user.user_id,
1977 'user_id': repo.user.user_id,
1855 'username': repo.user.username,
1978 'username': repo.user.username,
1856 'firstname': repo.user.first_name,
1979 'firstname': repo.user.first_name,
1857 'lastname': repo.user.last_name,
1980 'lastname': repo.user.last_name,
1858 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1981 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1859 },
1982 },
1860 'name': repo.repo_name,
1983 'name': repo.repo_name,
1861 'link': RepoModel().get_url(repo),
1984 'link': RepoModel().get_url(repo),
1862 'description': h.chop_at_smart(repo.description_safe, '\n'),
1985 'description': h.chop_at_smart(repo.description_safe, '\n'),
1863 'refs': {
1986 'refs': {
1864 'all_refs': all_refs,
1987 'all_refs': all_refs,
1865 'selected_ref': selected_ref,
1988 'selected_ref': selected_ref,
1866 'select2_refs': refs_select2
1989 'select2_refs': refs_select2
1867 }
1990 }
1868 }
1991 }
1869
1992
1870 def generate_pullrequest_title(self, source, source_ref, target):
1993 def generate_pullrequest_title(self, source, source_ref, target):
1871 return u'{source}#{at_ref} to {target}'.format(
1994 return u'{source}#{at_ref} to {target}'.format(
1872 source=source,
1995 source=source,
1873 at_ref=source_ref,
1996 at_ref=source_ref,
1874 target=target,
1997 target=target,
1875 )
1998 )
1876
1999
1877 def _cleanup_merge_workspace(self, pull_request):
2000 def _cleanup_merge_workspace(self, pull_request):
1878 # Merging related cleanup
2001 # Merging related cleanup
1879 repo_id = pull_request.target_repo.repo_id
2002 repo_id = pull_request.target_repo.repo_id
1880 target_scm = pull_request.target_repo.scm_instance()
2003 target_scm = pull_request.target_repo.scm_instance()
1881 workspace_id = self._workspace_id(pull_request)
2004 workspace_id = self._workspace_id(pull_request)
1882
2005
1883 try:
2006 try:
1884 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
2007 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1885 except NotImplementedError:
2008 except NotImplementedError:
1886 pass
2009 pass
1887
2010
1888 def _get_repo_pullrequest_sources(
2011 def _get_repo_pullrequest_sources(
1889 self, repo, commit_id=None, branch=None, bookmark=None,
2012 self, repo, commit_id=None, branch=None, bookmark=None,
1890 translator=None):
2013 translator=None):
1891 """
2014 """
1892 Return a structure with repo's interesting commits, suitable for
2015 Return a structure with repo's interesting commits, suitable for
1893 the selectors in pullrequest controller
2016 the selectors in pullrequest controller
1894
2017
1895 :param commit_id: a commit that must be in the list somehow
2018 :param commit_id: a commit that must be in the list somehow
1896 and selected by default
2019 and selected by default
1897 :param branch: a branch that must be in the list and selected
2020 :param branch: a branch that must be in the list and selected
1898 by default - even if closed
2021 by default - even if closed
1899 :param bookmark: a bookmark that must be in the list and selected
2022 :param bookmark: a bookmark that must be in the list and selected
1900 """
2023 """
1901 _ = translator or get_current_request().translate
2024 _ = translator or get_current_request().translate
1902
2025
1903 commit_id = safe_str(commit_id) if commit_id else None
2026 commit_id = safe_str(commit_id) if commit_id else None
1904 branch = safe_unicode(branch) if branch else None
2027 branch = safe_unicode(branch) if branch else None
1905 bookmark = safe_unicode(bookmark) if bookmark else None
2028 bookmark = safe_unicode(bookmark) if bookmark else None
1906
2029
1907 selected = None
2030 selected = None
1908
2031
1909 # order matters: first source that has commit_id in it will be selected
2032 # order matters: first source that has commit_id in it will be selected
1910 sources = []
2033 sources = []
1911 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
2034 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1912 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
2035 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1913
2036
1914 if commit_id:
2037 if commit_id:
1915 ref_commit = (h.short_id(commit_id), commit_id)
2038 ref_commit = (h.short_id(commit_id), commit_id)
1916 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
2039 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1917
2040
1918 sources.append(
2041 sources.append(
1919 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
2042 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1920 )
2043 )
1921
2044
1922 groups = []
2045 groups = []
1923
2046
1924 for group_key, ref_list, group_name, match in sources:
2047 for group_key, ref_list, group_name, match in sources:
1925 group_refs = []
2048 group_refs = []
1926 for ref_name, ref_id in ref_list:
2049 for ref_name, ref_id in ref_list:
1927 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
2050 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1928 group_refs.append((ref_key, ref_name))
2051 group_refs.append((ref_key, ref_name))
1929
2052
1930 if not selected:
2053 if not selected:
1931 if set([commit_id, match]) & set([ref_id, ref_name]):
2054 if set([commit_id, match]) & set([ref_id, ref_name]):
1932 selected = ref_key
2055 selected = ref_key
1933
2056
1934 if group_refs:
2057 if group_refs:
1935 groups.append((group_refs, group_name))
2058 groups.append((group_refs, group_name))
1936
2059
1937 if not selected:
2060 if not selected:
1938 ref = commit_id or branch or bookmark
2061 ref = commit_id or branch or bookmark
1939 if ref:
2062 if ref:
1940 raise CommitDoesNotExistError(
2063 raise CommitDoesNotExistError(
1941 u'No commit refs could be found matching: {}'.format(ref))
2064 u'No commit refs could be found matching: {}'.format(ref))
1942 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
2065 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1943 selected = u'branch:{}:{}'.format(
2066 selected = u'branch:{}:{}'.format(
1944 safe_unicode(repo.DEFAULT_BRANCH_NAME),
2067 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1945 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
2068 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1946 )
2069 )
1947 elif repo.commit_ids:
2070 elif repo.commit_ids:
1948 # make the user select in this case
2071 # make the user select in this case
1949 selected = None
2072 selected = None
1950 else:
2073 else:
1951 raise EmptyRepositoryError()
2074 raise EmptyRepositoryError()
1952 return groups, selected
2075 return groups, selected
1953
2076
1954 def get_diff(self, source_repo, source_ref_id, target_ref_id,
2077 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1955 hide_whitespace_changes, diff_context):
2078 hide_whitespace_changes, diff_context):
1956
2079
1957 return self._get_diff_from_pr_or_version(
2080 return self._get_diff_from_pr_or_version(
1958 source_repo, source_ref_id, target_ref_id,
2081 source_repo, source_ref_id, target_ref_id,
1959 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
2082 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1960
2083
1961 def _get_diff_from_pr_or_version(
2084 def _get_diff_from_pr_or_version(
1962 self, source_repo, source_ref_id, target_ref_id,
2085 self, source_repo, source_ref_id, target_ref_id,
1963 hide_whitespace_changes, diff_context):
2086 hide_whitespace_changes, diff_context):
1964
2087
1965 target_commit = source_repo.get_commit(
2088 target_commit = source_repo.get_commit(
1966 commit_id=safe_str(target_ref_id))
2089 commit_id=safe_str(target_ref_id))
1967 source_commit = source_repo.get_commit(
2090 source_commit = source_repo.get_commit(
1968 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
2091 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1969 if isinstance(source_repo, Repository):
2092 if isinstance(source_repo, Repository):
1970 vcs_repo = source_repo.scm_instance()
2093 vcs_repo = source_repo.scm_instance()
1971 else:
2094 else:
1972 vcs_repo = source_repo
2095 vcs_repo = source_repo
1973
2096
1974 # TODO: johbo: In the context of an update, we cannot reach
2097 # TODO: johbo: In the context of an update, we cannot reach
1975 # the old commit anymore with our normal mechanisms. It needs
2098 # the old commit anymore with our normal mechanisms. It needs
1976 # some sort of special support in the vcs layer to avoid this
2099 # some sort of special support in the vcs layer to avoid this
1977 # workaround.
2100 # workaround.
1978 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
2101 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1979 vcs_repo.alias == 'git'):
2102 vcs_repo.alias == 'git'):
1980 source_commit.raw_id = safe_str(source_ref_id)
2103 source_commit.raw_id = safe_str(source_ref_id)
1981
2104
1982 log.debug('calculating diff between '
2105 log.debug('calculating diff between '
1983 'source_ref:%s and target_ref:%s for repo `%s`',
2106 'source_ref:%s and target_ref:%s for repo `%s`',
1984 target_ref_id, source_ref_id,
2107 target_ref_id, source_ref_id,
1985 safe_unicode(vcs_repo.path))
2108 safe_unicode(vcs_repo.path))
1986
2109
1987 vcs_diff = vcs_repo.get_diff(
2110 vcs_diff = vcs_repo.get_diff(
1988 commit1=target_commit, commit2=source_commit,
2111 commit1=target_commit, commit2=source_commit,
1989 ignore_whitespace=hide_whitespace_changes, context=diff_context)
2112 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1990 return vcs_diff
2113 return vcs_diff
1991
2114
1992 def _is_merge_enabled(self, pull_request):
2115 def _is_merge_enabled(self, pull_request):
1993 return self._get_general_setting(
2116 return self._get_general_setting(
1994 pull_request, 'rhodecode_pr_merge_enabled')
2117 pull_request, 'rhodecode_pr_merge_enabled')
1995
2118
1996 def _use_rebase_for_merging(self, pull_request):
2119 def _use_rebase_for_merging(self, pull_request):
1997 repo_type = pull_request.target_repo.repo_type
2120 repo_type = pull_request.target_repo.repo_type
1998 if repo_type == 'hg':
2121 if repo_type == 'hg':
1999 return self._get_general_setting(
2122 return self._get_general_setting(
2000 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2123 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2001 elif repo_type == 'git':
2124 elif repo_type == 'git':
2002 return self._get_general_setting(
2125 return self._get_general_setting(
2003 pull_request, 'rhodecode_git_use_rebase_for_merging')
2126 pull_request, 'rhodecode_git_use_rebase_for_merging')
2004
2127
2005 return False
2128 return False
2006
2129
2007 def _user_name_for_merging(self, pull_request, user):
2130 def _user_name_for_merging(self, pull_request, user):
2008 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2131 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2009 if env_user_name_attr and hasattr(user, env_user_name_attr):
2132 if env_user_name_attr and hasattr(user, env_user_name_attr):
2010 user_name_attr = env_user_name_attr
2133 user_name_attr = env_user_name_attr
2011 else:
2134 else:
2012 user_name_attr = 'short_contact'
2135 user_name_attr = 'short_contact'
2013
2136
2014 user_name = getattr(user, user_name_attr)
2137 user_name = getattr(user, user_name_attr)
2015 return user_name
2138 return user_name
2016
2139
2017 def _close_branch_before_merging(self, pull_request):
2140 def _close_branch_before_merging(self, pull_request):
2018 repo_type = pull_request.target_repo.repo_type
2141 repo_type = pull_request.target_repo.repo_type
2019 if repo_type == 'hg':
2142 if repo_type == 'hg':
2020 return self._get_general_setting(
2143 return self._get_general_setting(
2021 pull_request, 'rhodecode_hg_close_branch_before_merging')
2144 pull_request, 'rhodecode_hg_close_branch_before_merging')
2022 elif repo_type == 'git':
2145 elif repo_type == 'git':
2023 return self._get_general_setting(
2146 return self._get_general_setting(
2024 pull_request, 'rhodecode_git_close_branch_before_merging')
2147 pull_request, 'rhodecode_git_close_branch_before_merging')
2025
2148
2026 return False
2149 return False
2027
2150
2028 def _get_general_setting(self, pull_request, settings_key, default=False):
2151 def _get_general_setting(self, pull_request, settings_key, default=False):
2029 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2152 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2030 settings = settings_model.get_general_settings()
2153 settings = settings_model.get_general_settings()
2031 return settings.get(settings_key, default)
2154 return settings.get(settings_key, default)
2032
2155
2033 def _log_audit_action(self, action, action_data, user, pull_request):
2156 def _log_audit_action(self, action, action_data, user, pull_request):
2034 audit_logger.store(
2157 audit_logger.store(
2035 action=action,
2158 action=action,
2036 action_data=action_data,
2159 action_data=action_data,
2037 user=user,
2160 user=user,
2038 repo=pull_request.target_repo)
2161 repo=pull_request.target_repo)
2039
2162
2040 def get_reviewer_functions(self):
2163 def get_reviewer_functions(self):
2041 """
2164 """
2042 Fetches functions for validation and fetching default reviewers.
2165 Fetches functions for validation and fetching default reviewers.
2043 If available we use the EE package, else we fallback to CE
2166 If available we use the EE package, else we fallback to CE
2044 package functions
2167 package functions
2045 """
2168 """
2046 try:
2169 try:
2047 from rc_reviewers.utils import get_default_reviewers_data
2170 from rc_reviewers.utils import get_default_reviewers_data
2048 from rc_reviewers.utils import validate_default_reviewers
2171 from rc_reviewers.utils import validate_default_reviewers
2049 from rc_reviewers.utils import validate_observers
2172 from rc_reviewers.utils import validate_observers
2050 except ImportError:
2173 except ImportError:
2051 from rhodecode.apps.repository.utils import get_default_reviewers_data
2174 from rhodecode.apps.repository.utils import get_default_reviewers_data
2052 from rhodecode.apps.repository.utils import validate_default_reviewers
2175 from rhodecode.apps.repository.utils import validate_default_reviewers
2053 from rhodecode.apps.repository.utils import validate_observers
2176 from rhodecode.apps.repository.utils import validate_observers
2054
2177
2055 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2178 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2056
2179
2057
2180
2058 class MergeCheck(object):
2181 class MergeCheck(object):
2059 """
2182 """
2060 Perform Merge Checks and returns a check object which stores information
2183 Perform Merge Checks and returns a check object which stores information
2061 about merge errors, and merge conditions
2184 about merge errors, and merge conditions
2062 """
2185 """
2063 TODO_CHECK = 'todo'
2186 TODO_CHECK = 'todo'
2064 PERM_CHECK = 'perm'
2187 PERM_CHECK = 'perm'
2065 REVIEW_CHECK = 'review'
2188 REVIEW_CHECK = 'review'
2066 MERGE_CHECK = 'merge'
2189 MERGE_CHECK = 'merge'
2067 WIP_CHECK = 'wip'
2190 WIP_CHECK = 'wip'
2068
2191
2069 def __init__(self):
2192 def __init__(self):
2070 self.review_status = None
2193 self.review_status = None
2071 self.merge_possible = None
2194 self.merge_possible = None
2072 self.merge_msg = ''
2195 self.merge_msg = ''
2073 self.merge_response = None
2196 self.merge_response = None
2074 self.failed = None
2197 self.failed = None
2075 self.errors = []
2198 self.errors = []
2076 self.error_details = OrderedDict()
2199 self.error_details = OrderedDict()
2077 self.source_commit = AttributeDict()
2200 self.source_commit = AttributeDict()
2078 self.target_commit = AttributeDict()
2201 self.target_commit = AttributeDict()
2079 self.reviewers_count = 0
2202 self.reviewers_count = 0
2080 self.observers_count = 0
2203 self.observers_count = 0
2081
2204
2082 def __repr__(self):
2205 def __repr__(self):
2083 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2206 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2084 self.merge_possible, self.failed, self.errors)
2207 self.merge_possible, self.failed, self.errors)
2085
2208
2086 def push_error(self, error_type, message, error_key, details):
2209 def push_error(self, error_type, message, error_key, details):
2087 self.failed = True
2210 self.failed = True
2088 self.errors.append([error_type, message])
2211 self.errors.append([error_type, message])
2089 self.error_details[error_key] = dict(
2212 self.error_details[error_key] = dict(
2090 details=details,
2213 details=details,
2091 error_type=error_type,
2214 error_type=error_type,
2092 message=message
2215 message=message
2093 )
2216 )
2094
2217
2095 @classmethod
2218 @classmethod
2096 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2219 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2097 force_shadow_repo_refresh=False):
2220 force_shadow_repo_refresh=False):
2098 _ = translator
2221 _ = translator
2099 merge_check = cls()
2222 merge_check = cls()
2100
2223
2101 # title has WIP:
2224 # title has WIP:
2102 if pull_request.work_in_progress:
2225 if pull_request.work_in_progress:
2103 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2226 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2104
2227
2105 msg = _('WIP marker in title prevents from accidental merge.')
2228 msg = _('WIP marker in title prevents from accidental merge.')
2106 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2229 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2107 if fail_early:
2230 if fail_early:
2108 return merge_check
2231 return merge_check
2109
2232
2110 # permissions to merge
2233 # permissions to merge
2111 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2234 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2112 if not user_allowed_to_merge:
2235 if not user_allowed_to_merge:
2113 log.debug("MergeCheck: cannot merge, approval is pending.")
2236 log.debug("MergeCheck: cannot merge, approval is pending.")
2114
2237
2115 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2238 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2116 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2239 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2117 if fail_early:
2240 if fail_early:
2118 return merge_check
2241 return merge_check
2119
2242
2120 # permission to merge into the target branch
2243 # permission to merge into the target branch
2121 target_commit_id = pull_request.target_ref_parts.commit_id
2244 target_commit_id = pull_request.target_ref_parts.commit_id
2122 if pull_request.target_ref_parts.type == 'branch':
2245 if pull_request.target_ref_parts.type == 'branch':
2123 branch_name = pull_request.target_ref_parts.name
2246 branch_name = pull_request.target_ref_parts.name
2124 else:
2247 else:
2125 # for mercurial we can always figure out the branch from the commit
2248 # for mercurial we can always figure out the branch from the commit
2126 # in case of bookmark
2249 # in case of bookmark
2127 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2250 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2128 branch_name = target_commit.branch
2251 branch_name = target_commit.branch
2129
2252
2130 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2253 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2131 pull_request.target_repo.repo_name, branch_name)
2254 pull_request.target_repo.repo_name, branch_name)
2132 if branch_perm and branch_perm == 'branch.none':
2255 if branch_perm and branch_perm == 'branch.none':
2133 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2256 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2134 branch_name, rule)
2257 branch_name, rule)
2135 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2258 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2136 if fail_early:
2259 if fail_early:
2137 return merge_check
2260 return merge_check
2138
2261
2139 # review status, must be always present
2262 # review status, must be always present
2140 review_status = pull_request.calculated_review_status()
2263 review_status = pull_request.calculated_review_status()
2141 merge_check.review_status = review_status
2264 merge_check.review_status = review_status
2142 merge_check.reviewers_count = pull_request.reviewers_count
2265 merge_check.reviewers_count = pull_request.reviewers_count
2143 merge_check.observers_count = pull_request.observers_count
2266 merge_check.observers_count = pull_request.observers_count
2144
2267
2145 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2268 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2146 if not status_approved and merge_check.reviewers_count:
2269 if not status_approved and merge_check.reviewers_count:
2147 log.debug("MergeCheck: cannot merge, approval is pending.")
2270 log.debug("MergeCheck: cannot merge, approval is pending.")
2148 msg = _('Pull request reviewer approval is pending.')
2271 msg = _('Pull request reviewer approval is pending.')
2149
2272
2150 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2273 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2151
2274
2152 if fail_early:
2275 if fail_early:
2153 return merge_check
2276 return merge_check
2154
2277
2155 # left over TODOs
2278 # left over TODOs
2156 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2279 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2157 if todos:
2280 if todos:
2158 log.debug("MergeCheck: cannot merge, {} "
2281 log.debug("MergeCheck: cannot merge, {} "
2159 "unresolved TODOs left.".format(len(todos)))
2282 "unresolved TODOs left.".format(len(todos)))
2160
2283
2161 if len(todos) == 1:
2284 if len(todos) == 1:
2162 msg = _('Cannot merge, {} TODO still not resolved.').format(
2285 msg = _('Cannot merge, {} TODO still not resolved.').format(
2163 len(todos))
2286 len(todos))
2164 else:
2287 else:
2165 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2288 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2166 len(todos))
2289 len(todos))
2167
2290
2168 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2291 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2169
2292
2170 if fail_early:
2293 if fail_early:
2171 return merge_check
2294 return merge_check
2172
2295
2173 # merge possible, here is the filesystem simulation + shadow repo
2296 # merge possible, here is the filesystem simulation + shadow repo
2174 merge_response, merge_status, msg = PullRequestModel().merge_status(
2297 merge_response, merge_status, msg = PullRequestModel().merge_status(
2175 pull_request, translator=translator,
2298 pull_request, translator=translator,
2176 force_shadow_repo_refresh=force_shadow_repo_refresh)
2299 force_shadow_repo_refresh=force_shadow_repo_refresh)
2177
2300
2178 merge_check.merge_possible = merge_status
2301 merge_check.merge_possible = merge_status
2179 merge_check.merge_msg = msg
2302 merge_check.merge_msg = msg
2180 merge_check.merge_response = merge_response
2303 merge_check.merge_response = merge_response
2181
2304
2182 source_ref_id = pull_request.source_ref_parts.commit_id
2305 source_ref_id = pull_request.source_ref_parts.commit_id
2183 target_ref_id = pull_request.target_ref_parts.commit_id
2306 target_ref_id = pull_request.target_ref_parts.commit_id
2184
2307
2185 try:
2308 try:
2186 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2309 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2187 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2310 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2188 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2311 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2189 merge_check.source_commit.current_raw_id = source_commit.raw_id
2312 merge_check.source_commit.current_raw_id = source_commit.raw_id
2190 merge_check.source_commit.previous_raw_id = source_ref_id
2313 merge_check.source_commit.previous_raw_id = source_ref_id
2191
2314
2192 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2315 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2193 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2316 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2194 merge_check.target_commit.current_raw_id = target_commit.raw_id
2317 merge_check.target_commit.current_raw_id = target_commit.raw_id
2195 merge_check.target_commit.previous_raw_id = target_ref_id
2318 merge_check.target_commit.previous_raw_id = target_ref_id
2196 except (SourceRefMissing, TargetRefMissing):
2319 except (SourceRefMissing, TargetRefMissing):
2197 pass
2320 pass
2198
2321
2199 if not merge_status:
2322 if not merge_status:
2200 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2323 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2201 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2324 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2202
2325
2203 if fail_early:
2326 if fail_early:
2204 return merge_check
2327 return merge_check
2205
2328
2206 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2329 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2207 return merge_check
2330 return merge_check
2208
2331
2209 @classmethod
2332 @classmethod
2210 def get_merge_conditions(cls, pull_request, translator):
2333 def get_merge_conditions(cls, pull_request, translator):
2211 _ = translator
2334 _ = translator
2212 merge_details = {}
2335 merge_details = {}
2213
2336
2214 model = PullRequestModel()
2337 model = PullRequestModel()
2215 use_rebase = model._use_rebase_for_merging(pull_request)
2338 use_rebase = model._use_rebase_for_merging(pull_request)
2216
2339
2217 if use_rebase:
2340 if use_rebase:
2218 merge_details['merge_strategy'] = dict(
2341 merge_details['merge_strategy'] = dict(
2219 details={},
2342 details={},
2220 message=_('Merge strategy: rebase')
2343 message=_('Merge strategy: rebase')
2221 )
2344 )
2222 else:
2345 else:
2223 merge_details['merge_strategy'] = dict(
2346 merge_details['merge_strategy'] = dict(
2224 details={},
2347 details={},
2225 message=_('Merge strategy: explicit merge commit')
2348 message=_('Merge strategy: explicit merge commit')
2226 )
2349 )
2227
2350
2228 close_branch = model._close_branch_before_merging(pull_request)
2351 close_branch = model._close_branch_before_merging(pull_request)
2229 if close_branch:
2352 if close_branch:
2230 repo_type = pull_request.target_repo.repo_type
2353 repo_type = pull_request.target_repo.repo_type
2231 close_msg = ''
2354 close_msg = ''
2232 if repo_type == 'hg':
2355 if repo_type == 'hg':
2233 close_msg = _('Source branch will be closed before the merge.')
2356 close_msg = _('Source branch will be closed before the merge.')
2234 elif repo_type == 'git':
2357 elif repo_type == 'git':
2235 close_msg = _('Source branch will be deleted after the merge.')
2358 close_msg = _('Source branch will be deleted after the merge.')
2236
2359
2237 merge_details['close_branch'] = dict(
2360 merge_details['close_branch'] = dict(
2238 details={},
2361 details={},
2239 message=close_msg
2362 message=close_msg
2240 )
2363 )
2241
2364
2242 return merge_details
2365 return merge_details
2243
2366
2244
2367
2245 ChangeTuple = collections.namedtuple(
2368 ChangeTuple = collections.namedtuple(
2246 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2369 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2247
2370
2248 FileChangeTuple = collections.namedtuple(
2371 FileChangeTuple = collections.namedtuple(
2249 'FileChangeTuple', ['added', 'modified', 'removed'])
2372 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,630 +1,630 b''
1
1
2
2
3 //BUTTONS
3 //BUTTONS
4 button,
4 button,
5 .btn,
5 .btn,
6 input[type="button"] {
6 input[type="button"] {
7 -webkit-appearance: none;
7 -webkit-appearance: none;
8 display: inline-block;
8 display: inline-block;
9 margin: 0 @padding/3 0 0;
9 margin: 0 @padding/3 0 0;
10 padding: @button-padding;
10 padding: @button-padding;
11 text-align: center;
11 text-align: center;
12 font-size: @basefontsize;
12 font-size: @basefontsize;
13 line-height: 1em;
13 line-height: 1em;
14 font-family: @text-light;
14 font-family: @text-light;
15 text-decoration: none;
15 text-decoration: none;
16 text-shadow: none;
16 text-shadow: none;
17 color: @grey2;
17 color: @grey2;
18 background-color: white;
18 background-color: white;
19 background-image: none;
19 background-image: none;
20 border: none;
20 border: none;
21 .border ( @border-thickness-buttons, @grey5 );
21 .border ( @border-thickness-buttons, @grey5 );
22 .border-radius (@border-radius);
22 .border-radius (@border-radius);
23 cursor: pointer;
23 cursor: pointer;
24 white-space: nowrap;
24 white-space: nowrap;
25 -webkit-transition: background .3s,color .3s;
25 -webkit-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
29 box-shadow: @button-shadow;
29 box-shadow: @button-shadow;
30 -webkit-box-shadow: @button-shadow;
30 -webkit-box-shadow: @button-shadow;
31
31
32
32
33
33
34 a {
34 a {
35 display: block;
35 display: block;
36 margin: 0;
36 margin: 0;
37 padding: 0;
37 padding: 0;
38 color: inherit;
38 color: inherit;
39 text-decoration: none;
39 text-decoration: none;
40
40
41 &:hover {
41 &:hover {
42 text-decoration: none;
42 text-decoration: none;
43 }
43 }
44 }
44 }
45
45
46 &:focus,
46 &:focus,
47 &:active {
47 &:active {
48 outline:none;
48 outline:none;
49 }
49 }
50
50
51 &:hover {
51 &:hover {
52 color: @rcdarkblue;
52 color: @rcdarkblue;
53 background-color: @grey6;
53 background-color: @grey6;
54
54
55 }
55 }
56
56
57 &.btn-active {
57 &.btn-active {
58 color: @rcdarkblue;
58 color: @rcdarkblue;
59 background-color: @grey6;
59 background-color: @grey6;
60 }
60 }
61
61
62 .icon-remove {
62 .icon-remove {
63 display: none;
63 display: none;
64 }
64 }
65
65
66 //disabled buttons
66 //disabled buttons
67 //last; overrides any other styles
67 //last; overrides any other styles
68 &:disabled {
68 &:disabled {
69 opacity: .7;
69 opacity: .7;
70 cursor: auto;
70 cursor: auto;
71 background-color: white;
71 background-color: white;
72 color: @grey4;
72 color: @grey4;
73 text-shadow: none;
73 text-shadow: none;
74 }
74 }
75
75
76 &.no-margin {
76 &.no-margin {
77 margin: 0 0 0 0;
77 margin: 0 0 0 0;
78 }
78 }
79
79
80
80
81
81
82 }
82 }
83
83
84
84
85 .btn-default {
85 .btn-default {
86 border: @border-thickness solid @grey5;
86 border: @border-thickness solid @grey5;
87 background-image: none;
87 background-image: none;
88 color: @grey2;
88 color: @grey2;
89
89
90 a {
90 a {
91 color: @grey2;
91 color: @grey2;
92 }
92 }
93
93
94 &:hover,
94 &:hover,
95 &.active {
95 &.active {
96 color: @rcdarkblue;
96 color: @rcdarkblue;
97 background-color: @white;
97 background-color: @white;
98 .border ( @border-thickness, @grey4 );
98 .border ( @border-thickness, @grey4 );
99
99
100 a {
100 a {
101 color: @grey2;
101 color: @grey2;
102 }
102 }
103 }
103 }
104 &:disabled {
104 &:disabled {
105 .border ( @border-thickness-buttons, @grey5 );
105 .border ( @border-thickness-buttons, @grey5 );
106 background-color: transparent;
106 background-color: transparent;
107 }
107 }
108 &.btn-active {
108 &.btn-active {
109 color: @rcdarkblue;
109 color: @rcdarkblue;
110 background-color: @white;
110 background-color: @white;
111 .border ( @border-thickness, @rcdarkblue );
111 .border ( @border-thickness, @rcdarkblue );
112 }
112 }
113 }
113 }
114
114
115 .btn-primary,
115 .btn-primary,
116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
117 .btn-success {
117 .btn-success {
118 .border ( @border-thickness, @rcblue );
118 .border ( @border-thickness, @rcblue );
119 background-color: @rcblue;
119 background-color: @rcblue;
120 color: white;
120 color: white;
121
121
122 a {
122 a {
123 color: white;
123 color: white;
124 }
124 }
125
125
126 &:hover,
126 &:hover,
127 &.active {
127 &.active {
128 .border ( @border-thickness, @rcdarkblue );
128 .border ( @border-thickness, @rcdarkblue );
129 color: white;
129 color: white;
130 background-color: @rcdarkblue;
130 background-color: @rcdarkblue;
131
131
132 a {
132 a {
133 color: white;
133 color: white;
134 }
134 }
135 }
135 }
136 &:disabled {
136 &:disabled {
137 background-color: @rcblue;
137 background-color: @rcblue;
138 }
138 }
139 }
139 }
140
140
141 .btn-secondary {
141 .btn-secondary {
142 &:extend(.btn-default);
142 &:extend(.btn-default);
143
143
144 background-color: white;
144 background-color: white;
145
145
146 &:focus {
146 &:focus {
147 outline: 0;
147 outline: 0;
148 }
148 }
149
149
150 &:hover {
150 &:hover {
151 &:extend(.btn-default:hover);
151 &:extend(.btn-default:hover);
152 }
152 }
153
153
154 &.btn-link {
154 &.btn-link {
155 &:extend(.btn-link);
155 &:extend(.btn-link);
156 color: @rcblue;
156 color: @rcblue;
157 }
157 }
158
158
159 &:disabled {
159 &:disabled {
160 color: @rcblue;
160 color: @rcblue;
161 background-color: white;
161 background-color: white;
162 }
162 }
163 }
163 }
164
164
165 .btn-danger,
165 .btn-danger,
166 .revoke_perm,
166 .revoke_perm,
167 .btn-x,
167 .btn-x,
168 .form .action_button.btn-x {
168 .form .action_button.btn-x {
169 .border ( @border-thickness, @alert2 );
169 .border ( @border-thickness, @alert2 );
170 background-color: white;
170 background-color: white;
171 color: @alert2;
171 color: @alert2;
172
172
173 a {
173 a {
174 color: @alert2;
174 color: @alert2;
175 }
175 }
176
176
177 &:hover,
177 &:hover,
178 &.active {
178 &.active {
179 .border ( @border-thickness, @alert2 );
179 .border ( @border-thickness, @alert2 );
180 color: white;
180 color: white;
181 background-color: @alert2;
181 background-color: @alert2;
182
182
183 a {
183 a {
184 color: white;
184 color: white;
185 }
185 }
186 }
186 }
187
187
188 i {
188 i {
189 display:none;
189 display:none;
190 }
190 }
191
191
192 &:disabled {
192 &:disabled {
193 background-color: white;
193 background-color: white;
194 color: @alert2;
194 color: @alert2;
195 }
195 }
196 }
196 }
197
197
198 .btn-warning {
198 .btn-warning {
199 .border ( @border-thickness, @alert3 );
199 .border ( @border-thickness, @alert3 );
200 background-color: white;
200 background-color: white;
201 color: @alert3;
201 color: @alert3;
202
202
203 a {
203 a {
204 color: @alert3;
204 color: @alert3;
205 }
205 }
206
206
207 &:hover,
207 &:hover,
208 &.active {
208 &.active {
209 .border ( @border-thickness, @alert3 );
209 .border ( @border-thickness, @alert3 );
210 color: white;
210 color: white;
211 background-color: @alert3;
211 background-color: @alert3;
212
212
213 a {
213 a {
214 color: white;
214 color: white;
215 }
215 }
216 }
216 }
217
217
218 i {
218 i {
219 display:none;
219 display:none;
220 }
220 }
221
221
222 &:disabled {
222 &:disabled {
223 background-color: white;
223 background-color: white;
224 color: @alert3;
224 color: @alert3;
225 }
225 }
226 }
226 }
227
227
228
228
229 .btn-approved-status {
229 .btn-approved-status {
230 .border ( @border-thickness, @alert1 );
230 .border ( @border-thickness, @alert1 );
231 background-color: white;
231 background-color: white;
232 color: @alert1;
232 color: @alert1;
233
233
234 }
234 }
235
235
236 .btn-rejected-status {
236 .btn-rejected-status {
237 .border ( @border-thickness, @alert2 );
237 .border ( @border-thickness, @alert2 );
238 background-color: white;
238 background-color: white;
239 color: @alert2;
239 color: @alert2;
240 }
240 }
241
241
242 .btn-sm,
242 .btn-sm,
243 .btn-mini,
243 .btn-mini,
244 .field-sm .btn {
244 .field-sm .btn {
245 padding: @padding/3;
245 padding: @padding/3;
246 }
246 }
247
247
248 .btn-xs {
248 .btn-xs {
249 padding: @padding/4;
249 padding: @padding/4;
250 }
250 }
251
251
252 .btn-lg {
252 .btn-lg {
253 padding: @padding * 1.2;
253 padding: @padding * 1.2;
254 }
254 }
255
255
256 .btn-group {
256 .btn-group {
257 display: inline-block;
257 display: inline-block;
258 .btn {
258 .btn {
259 float: left;
259 float: left;
260 margin: 0 0 0 0;
260 margin: 0 0 0 0;
261 // first item
261 // first item
262 &:first-of-type:not(:last-of-type) {
262 &:first-of-type:not(:last-of-type) {
263 border-radius: @border-radius 0 0 @border-radius;
263 border-radius: @border-radius 0 0 @border-radius;
264
264
265 }
265 }
266 // 2nd, if only 2 elements are there
266 // 2nd, if only 2 elements are there
267 &:nth-of-type(2) {
267 &:nth-of-type(2) {
268 border-left-width: 0;
268 border-left-width: 0;
269 }
269 }
270 // middle elements
270 // middle elements
271 &:not(:first-of-type):not(:last-of-type) {
271 &:not(:first-of-type):not(:last-of-type) {
272 border-radius: 0;
272 border-radius: 0;
273 border-left-width: 0;
273 border-left-width: 0;
274 border-right-width: 0;
274 border-right-width: 0;
275 }
275 }
276 // last item
276 // last item
277 &:last-of-type:not(:first-of-type) {
277 &:last-of-type:not(:first-of-type) {
278 border-radius: 0 @border-radius @border-radius 0;
278 border-radius: 0 @border-radius @border-radius 0;
279 }
279 }
280
280
281 &:only-child {
281 &:only-child {
282 border-radius: @border-radius;
282 border-radius: @border-radius;
283 }
283 }
284 }
284 }
285
285
286 }
286 }
287
287
288
288
289 .btn-group-actions {
289 .btn-group-actions {
290 position: relative;
290 position: relative;
291 z-index: 50;
291 z-index: 50;
292
292
293 &:not(.open) .btn-action-switcher-container {
293 &:not(.open) .btn-action-switcher-container {
294 display: none;
294 display: none;
295 }
295 }
296
296
297 .btn-more-option {
297 .btn-more-option {
298 margin-left: -1px;
298 margin-left: -1px;
299 padding-left: 2px;
299 padding-left: 2px;
300 padding-right: 2px;
300 padding-right: 2px;
301 }
301 }
302 }
302 }
303
303
304
304
305 .btn-action-switcher-container {
305 .btn-action-switcher-container {
306 position: absolute;
306 position: absolute;
307 top: 100%;
307 top: 100%;
308
308
309 &.left-align {
309 &.left-align {
310 left: 0;
310 left: 0;
311 }
311 }
312 &.right-align {
312 &.right-align {
313 right: 0;
313 right: 0;
314 }
314 }
315
315
316 }
316 }
317
317
318 .btn-action-switcher {
318 .btn-action-switcher {
319 display: block;
319 display: block;
320 position: relative;
320 position: relative;
321 z-index: 300;
321 z-index: 300;
322 max-width: 600px;
322 max-width: 600px;
323 margin-top: 4px;
323 margin-top: 4px;
324 margin-bottom: 24px;
324 margin-bottom: 24px;
325 font-size: 14px;
325 font-size: 14px;
326 font-weight: 400;
326 font-weight: 400;
327 padding: 8px 0;
327 padding: 8px 0;
328 background-color: #fff;
328 background-color: #fff;
329 border: 1px solid @grey4;
329 border: 1px solid @grey4;
330 border-radius: 3px;
330 border-radius: 3px;
331 box-shadow: @dropdown-shadow;
331 box-shadow: @dropdown-shadow;
332 overflow: auto;
332 overflow: auto;
333
333
334 li {
334 li {
335 display: block;
335 display: block;
336 text-align: left;
336 text-align: left;
337 list-style: none;
337 list-style: none;
338 padding: 5px 10px;
338 padding: 5px 10px;
339 }
339 }
340
340
341 li .action-help-block {
341 li .action-help-block {
342 font-size: 10px;
342 font-size: 10px;
343 line-height: normal;
343 line-height: normal;
344 color: @grey4;
344 color: @grey4;
345 }
345 }
346
346
347 }
347 }
348
348
349 .btn-link {
349 .btn-link {
350 background: transparent;
350 background: transparent;
351 border: none;
351 border: none;
352 padding: 0;
352 padding: 0;
353 color: @rcblue;
353 color: @rcblue;
354
354
355 &:hover {
355 &:hover {
356 background: transparent;
356 background: transparent;
357 border: none;
357 border: none;
358 color: @rcdarkblue;
358 color: @rcdarkblue;
359 }
359 }
360
360
361 //disabled buttons
361 //disabled buttons
362 //last; overrides any other styles
362 //last; overrides any other styles
363 &:disabled {
363 &:disabled {
364 opacity: .7;
364 opacity: .7;
365 cursor: auto;
365 cursor: auto;
366 background-color: white;
366 background-color: white;
367 color: @grey4;
367 color: @grey4;
368 text-shadow: none;
368 text-shadow: none;
369 }
369 }
370
370
371 // TODO: johbo: Check if we can avoid this, indicates that the structure
371 // TODO: johbo: Check if we can avoid this, indicates that the structure
372 // is not yet good.
372 // is not yet good.
373 // lisa: The button CSS reflects the button HTML; both need a cleanup.
373 // lisa: The button CSS reflects the button HTML; both need a cleanup.
374 &.btn-danger {
374 &.btn-danger {
375 color: @alert2;
375 color: @alert2;
376
376
377 &:hover {
377 &:hover {
378 color: darken(@alert2, 30%);
378 color: darken(@alert2, 30%);
379 }
379 }
380
380
381 &:disabled {
381 &:disabled {
382 color: @alert2;
382 color: @alert2;
383 }
383 }
384 }
384 }
385 }
385 }
386
386
387 .btn-social {
387 .btn-social {
388 &:extend(.btn-default);
388 &:extend(.btn-default);
389 margin: 5px 5px 5px 0px;
389 margin: 5px 5px 5px 0px;
390 min-width: 160px;
390 min-width: 160px;
391 }
391 }
392
392
393 // TODO: johbo: check these exceptions
393 // TODO: johbo: check these exceptions
394
394
395 .links {
395 .links {
396
396
397 .btn + .btn {
397 .btn + .btn {
398 margin-top: @padding;
398 margin-top: @padding;
399 }
399 }
400 }
400 }
401
401
402
402
403 .action_button {
403 .action_button {
404 display:inline;
404 display:inline;
405 margin: 0;
405 margin: 0;
406 padding: 0 1em 0 0;
406 padding: 0 1em 0 0;
407 font-size: inherit;
407 font-size: inherit;
408 color: @rcblue;
408 color: @rcblue;
409 border: none;
409 border: none;
410 border-radius: 0;
410 border-radius: 0;
411 background-color: transparent;
411 background-color: transparent;
412
412
413 &.last-item {
413 &.last-item {
414 border: none;
414 border: none;
415 padding: 0 0 0 0;
415 padding: 0 0 0 0;
416 }
416 }
417
417
418 &:last-child {
418 &:last-child {
419 border: none;
419 border: none;
420 padding: 0 0 0 0;
420 padding: 0 0 0 0;
421 }
421 }
422
422
423 &:hover {
423 &:hover {
424 color: @rcdarkblue;
424 color: @rcdarkblue;
425 background-color: transparent;
425 background-color: transparent;
426 border: none;
426 border: none;
427 }
427 }
428 .noselect
428 .noselect
429 }
429 }
430
430
431 .grid_delete {
431 .grid_delete {
432 .action_button {
432 .action_button {
433 border: none;
433 border: none;
434 }
434 }
435 }
435 }
436
436
437 input[type="submit"].btn-draft {
437 input[type="submit"].btn-draft {
438 .border ( @border-thickness, @rcblue );
438 .border ( @border-thickness, @rcblue );
439 background-color: white;
439 background-color: white;
440 color: @rcblue;
440 color: @rcblue;
441
441
442 a {
442 a {
443 color: @rcblue;
443 color: @rcblue;
444 }
444 }
445
445
446 &:hover,
446 &:hover,
447 &.active {
447 &.active {
448 .border ( @border-thickness, @rcdarkblue );
448 .border ( @border-thickness, @rcdarkblue );
449 background-color: white;
449 background-color: white;
450 color: @rcdarkblue;
450 color: @rcdarkblue;
451
451
452 a {
452 a {
453 color: @rcdarkblue;
453 color: @rcdarkblue;
454 }
454 }
455 }
455 }
456
456
457 &:disabled {
457 &:disabled {
458 background-color: white;
458 background-color: white;
459 color: @rcblue;
459 color: @rcblue;
460 }
460 }
461 }
461 }
462
462
463 input[type="submit"].btn-warning {
463 input[type="submit"].btn-warning {
464 &:extend(.btn-warning);
464 &:extend(.btn-warning);
465
465
466 &:focus {
466 &:focus {
467 outline: 0;
467 outline: 0;
468 }
468 }
469
469
470 &:hover {
470 &:hover {
471 &:extend(.btn-warning:hover);
471 &:extend(.btn-warning:hover);
472 }
472 }
473
473
474 &.btn-link {
474 &.btn-link {
475 &:extend(.btn-link);
475 &:extend(.btn-link);
476 color: @alert3;
476 color: @alert3;
477
477
478 &:disabled {
478 &:disabled {
479 color: @alert3;
479 color: @alert3;
480 background-color: transparent;
480 background-color: transparent;
481 }
481 }
482 }
482 }
483
483
484 &:disabled {
484 &:disabled {
485 .border ( @border-thickness-buttons, @alert3 );
485 .border ( @border-thickness-buttons, @alert3 );
486 background-color: white;
486 background-color: white;
487 color: @alert3;
487 color: @alert3;
488 opacity: 0.5;
488 opacity: 0.5;
489 }
489 }
490 }
490 }
491
491
492
492
493
493
494 // TODO: johbo: Form button tweaks, check if we can use the classes instead
494 // TODO: johbo: Form button tweaks, check if we can use the classes instead
495 input[type="submit"] {
495 input[type="submit"] {
496 &:extend(.btn-primary);
496 &:extend(.btn-primary);
497
497
498 &:focus {
498 &:focus {
499 outline: 0;
499 outline: 0;
500 }
500 }
501
501
502 &:hover {
502 &:hover {
503 &:extend(.btn-primary:hover);
503 &:extend(.btn-primary:hover);
504 }
504 }
505
505
506 &.btn-link {
506 &.btn-link {
507 &:extend(.btn-link);
507 &:extend(.btn-link);
508 color: @rcblue;
508 color: @rcblue;
509
509
510 &:disabled {
510 &:disabled {
511 color: @rcblue;
511 color: @rcblue;
512 background-color: transparent;
512 background-color: transparent;
513 }
513 }
514 }
514 }
515
515
516 &:disabled {
516 &:disabled {
517 .border ( @border-thickness-buttons, @rcblue );
517 .border ( @border-thickness-buttons, @rcblue );
518 background-color: @rcblue;
518 background-color: @rcblue;
519 color: white;
519 color: white;
520 opacity: 0.5;
520 opacity: 0.5;
521 }
521 }
522 }
522 }
523
523
524 input[type="reset"] {
524 input[type="reset"] {
525 &:extend(.btn-default);
525 &:extend(.btn-default);
526
526
527 // TODO: johbo: Check if this tweak can be avoided.
527 // TODO: johbo: Check if this tweak can be avoided.
528 background: transparent;
528 background: transparent;
529
529
530 &:focus {
530 &:focus {
531 outline: 0;
531 outline: 0;
532 }
532 }
533
533
534 &:hover {
534 &:hover {
535 &:extend(.btn-default:hover);
535 &:extend(.btn-default:hover);
536 }
536 }
537
537
538 &.btn-link {
538 &.btn-link {
539 &:extend(.btn-link);
539 &:extend(.btn-link);
540 color: @rcblue;
540 color: @rcblue;
541
541
542 &:disabled {
542 &:disabled {
543 border: none;
543 border: none;
544 }
544 }
545 }
545 }
546
546
547 &:disabled {
547 &:disabled {
548 .border ( @border-thickness-buttons, @rcblue );
548 .border ( @border-thickness-buttons, @rcblue );
549 background-color: white;
549 background-color: white;
550 color: @rcblue;
550 color: @rcblue;
551 }
551 }
552 }
552 }
553
553
554 input[type="submit"],
554 input[type="submit"],
555 input[type="reset"] {
555 input[type="reset"] {
556 &.btn-danger {
556 &.btn-danger {
557 &:extend(.btn-danger);
557 &:extend(.btn-danger);
558
558
559 &:focus {
559 &:focus {
560 outline: 0;
560 outline: 0;
561 }
561 }
562
562
563 &:hover {
563 &:hover {
564 &:extend(.btn-danger:hover);
564 &:extend(.btn-danger:hover);
565 }
565 }
566
566
567 &.btn-link {
567 &.btn-link {
568 &:extend(.btn-link);
568 &:extend(.btn-link);
569 color: @alert2;
569 color: @alert2;
570
570
571 &:hover {
571 &:hover {
572 color: darken(@alert2,30%);
572 color: darken(@alert2,30%);
573 }
573 }
574 }
574 }
575
575
576 &:disabled {
576 &:disabled {
577 color: @alert2;
577 color: @alert2;
578 background-color: white;
578 background-color: white;
579 }
579 }
580 }
580 }
581 &.btn-danger-action {
581 &.btn-danger-action {
582 .border ( @border-thickness, @alert2 );
582 .border ( @border-thickness, @alert2 );
583 background-color: @alert2;
583 background-color: @alert2;
584 color: white;
584 color: white;
585
585
586 a {
586 a {
587 color: white;
587 color: white;
588 }
588 }
589
589
590 &:hover {
590 &:hover {
591 background-color: darken(@alert2,20%);
591 background-color: darken(@alert2,20%);
592 }
592 }
593
593
594 &.active {
594 &.active {
595 .border ( @border-thickness, @alert2 );
595 .border ( @border-thickness, @alert2 );
596 color: white;
596 color: white;
597 background-color: @alert2;
597 background-color: @alert2;
598
598
599 a {
599 a {
600 color: white;
600 color: white;
601 }
601 }
602 }
602 }
603
603
604 &:disabled {
604 &:disabled {
605 background-color: white;
605 background-color: white;
606 color: @alert2;
606 color: @alert2;
607 }
607 }
608 }
608 }
609 }
609 }
610
610
611
611
612 .button-links {
612 .button-links {
613 float: left;
613 float: left;
614 display: inline;
614 display: inline;
615 margin: 0;
615 margin: 0;
616 padding-left: 0;
616 padding-left: 0;
617 list-style: none;
617 list-style: none;
618 text-align: right;
618 text-align: right;
619
619
620 li {
620 li {
621
621 list-style: none;
622
622 text-align: right;
623 display: inline-block;
623 }
624 }
624
625
625 li.active {
626 a.active {
626 background-color: @grey6;
627 border: 2px solid @rcblue;
627 .border ( @border-thickness, @grey4 );
628 }
628 }
629
629
630 }
630 }
@@ -1,85 +1,85 b''
1 // See panels-bootstrap.less
1 // See panels-bootstrap.less
2 // These provide overrides for custom styling of Bootstrap panels
2 // These provide overrides for custom styling of Bootstrap panels
3
3
4 .panel {
4 .panel {
5 &:extend(.clearfix);
5 &:extend(.clearfix);
6
6
7 width: 100%;
7 width: 100%;
8 margin: 0 0 25px 0;
8 margin: 0 0 25px 0;
9 .border-radius(@border-radius);
9 .border-radius(@border-radius);
10 .box-shadow(none);
10 .box-shadow(none);
11
11
12 .permalink {
12 .permalink {
13 visibility: hidden;
13 visibility: hidden;
14 }
14 }
15
15
16 &:hover .permalink {
16 &:hover .permalink {
17 visibility: visible;
17 visibility: visible;
18 color: @rcblue;
18 color: @rcblue;
19 }
19 }
20
20
21 .panel-heading {
21 .panel-heading {
22 position: relative;
22 position: relative;
23 min-height: 1em;
23 min-height: 1em;
24 padding: @padding @panel-padding;
24 padding: @padding @panel-padding;
25 border-bottom: none;
25 border-bottom: none;
26
26
27 .panel-title,
27 .panel-title,
28 h3.panel-title {
28 h3.panel-title {
29 float: left;
29 float: left;
30 padding: 0 @padding 0 0;
30 padding: 0 @padding 0 0;
31 line-height: 1;
31 line-height: 1;
32 font-size: @panel-title;
32 font-size: @panel-title;
33 color: @grey1;
33 color: @grey1;
34 }
34 }
35
35
36 .panel-edit {
36 .panel-edit {
37 float: right;
37 float: right;
38 line-height: 1;
38 line-height: 1;
39 font-size: @panel-title;
39 font-size: @panel-title;
40 }
40 }
41 }
41 }
42
42
43 .panel-body {
43 .panel-body {
44 padding: @panel-padding;
44 padding: @panel-padding;
45
45
46 &.panel-body-min-height {
46 &.panel-body-min-height {
47 min-height: 150px
47 min-height: 200px
48 }
48 }
49 }
49 }
50
50
51 .panel-footer {
51 .panel-footer {
52 background-color: white;
52 background-color: white;
53 padding: .65em @panel-padding .5em;
53 padding: .65em @panel-padding .5em;
54 font-size: @panel-footer;
54 font-size: @panel-footer;
55 color: @text-muted;
55 color: @text-muted;
56 }
56 }
57
57
58 .q_filter_box {
58 .q_filter_box {
59 min-width: 40%;
59 min-width: 40%;
60 }
60 }
61
61
62 // special cases
62 // special cases
63 &.user-profile {
63 &.user-profile {
64 float: left;
64 float: left;
65
65
66 .panel-body {
66 .panel-body {
67 &:extend(.clearfix);
67 &:extend(.clearfix);
68 }
68 }
69 }
69 }
70 }
70 }
71
71
72 .main-content h3.panel-title {
72 .main-content h3.panel-title {
73 font-size: @panel-title;
73 font-size: @panel-title;
74 color: @grey1;
74 color: @grey1;
75 }
75 }
76
76
77 .panel-body-title-text {
77 .panel-body-title-text {
78 margin: 0 0 20px 0;
78 margin: 0 0 20px 0;
79 }
79 }
80
80
81 // play nice with the current form and field css
81 // play nice with the current form and field css
82 .field.panel-default,
82 .field.panel-default,
83 .form.panel-default {
83 .form.panel-default {
84 width: auto;
84 width: auto;
85 } No newline at end of file
85 }
@@ -1,151 +1,164 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-body">
4 <div class="panel-heading">
5 <div style="height: 35px">
5 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
6 <%
6 </div>
7 selected_filter = 'all'
8 if c.closed:
9 selected_filter = 'all_closed'
10 %>
11
7
8 <div class="panel-body panel-body-min-height">
9 <div class="title">
12 <ul class="button-links">
10 <ul class="button-links">
13 <li class="btn ${h.is_active('all', selected_filter)}"><a href="${h.route_path('my_account_pullrequests')}">${_('All')}</a></li>
11 <li><a class="btn ${h.is_active('all', c.selected_filter)}"
14 <li class="btn ${h.is_active('all_closed', selected_filter)}"><a href="${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}">${_('All + Closed')}</a></li>
12 href="${h.route_path('my_account_pullrequests', _query={})}">
13 ${_('Open')}
14 </a>
15 </li>
16 <li><a class="btn ${h.is_active('all_closed', c.selected_filter)}"
17 href="${h.route_path('my_account_pullrequests', _query={'closed':1})}">
18 ${_('All + Closed')}
19 </a>
20 </li>
21 <li><a class="btn ${h.is_active('awaiting_my_review', c.selected_filter)}"
22 href="${h.route_path('my_account_pullrequests', _query={'awaiting_my_review':1})}">
23
24 ${_('Awaiting my review')}
25 </a>
26 </li>
15 </ul>
27 </ul>
16
28
17 <div class="grid-quick-filter">
29 <div class="grid-quick-filter">
18 <ul class="grid-filter-box">
30 <ul class="grid-filter-box">
19 <li class="grid-filter-box-icon">
31 <li class="grid-filter-box-icon">
20 <i class="icon-search"></i>
32 <i class="icon-search"></i>
21 </li>
33 </li>
22 <li class="grid-filter-box-input">
34 <li class="grid-filter-box-input">
23 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
35 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter"
36 placeholder="${_('quick filter...')}" value=""/>
24 </li>
37 </li>
25 </ul>
38 </ul>
26 </div>
39 </div>
27 </div>
40 </div>
28 </div>
29 </div>
30
41
31 <div class="panel panel-default">
32 <div class="panel-heading">
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
34 </div>
35 <div class="panel-body panel-body-min-height">
36 <table id="pull_request_list_table" class="rctable table-bordered"></table>
42 <table id="pull_request_list_table" class="rctable table-bordered"></table>
37 </div>
43 </div>
38 </div>
44 </div>
39
45
40 <script type="text/javascript">
46 <script type="text/javascript">
41 $(document).ready(function () {
47 $(document).ready(function () {
42
48
43 var $pullRequestListTable = $('#pull_request_list_table');
49 var $pullRequestListTable = $('#pull_request_list_table');
44
50
45 // participating object list
51 // participating object list
46 $pullRequestListTable.DataTable({
52 $pullRequestListTable.DataTable({
47 processing: true,
53 processing: true,
48 serverSide: true,
54 serverSide: true,
49 stateSave: true,
55 stateSave: true,
50 stateDuration: -1,
56 stateDuration: -1,
51 ajax: {
57 ajax: {
52 "url": "${h.route_path('my_account_pullrequests_data')}",
58 "url": "${h.route_path('my_account_pullrequests_data')}",
53 "data": function (d) {
59 "data": function (d) {
54 d.closed = "${c.closed}";
60 d.closed = "${c.closed}";
61 d.awaiting_my_review = "${c.awaiting_my_review}";
55 },
62 },
56 "dataSrc": function (json) {
63 "dataSrc": function (json) {
57 return json.data;
64 return json.data;
58 }
65 }
59 },
66 },
60
67
61 dom: 'rtp',
68 dom: 'rtp',
62 pageLength: ${c.visual.dashboard_items},
69 pageLength: ${c.visual.dashboard_items},
63 order: [[1, "desc"]],
70 order: [[2, "desc"]],
64 columns: [
71 columns: [
65 {
72 {
66 data: {
73 data: {
67 "_": "status",
74 "_": "status",
68 "sort": "status"
75 "sort": "status"
69 }, title: "", className: "td-status", orderable: false
76 }, title: "PR", className: "td-status", orderable: false
77 },
78 {
79 data: {
80 "_": "my_status",
81 "sort": "status"
82 }, title: "You", className: "td-status", orderable: false
70 },
83 },
71 {
84 {
72 data: {
85 data: {
73 "_": "name",
86 "_": "name",
74 "sort": "name_raw"
87 "sort": "name_raw"
75 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
88 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
76 },
89 },
77 {
90 {
78 data: {
91 data: {
79 "_": "title",
92 "_": "title",
80 "sort": "title"
93 "sort": "title"
81 }, title: "${_('Title')}", className: "td-description"
94 }, title: "${_('Title')}", className: "td-description"
82 },
95 },
83 {
96 {
84 data: {
97 data: {
85 "_": "author",
98 "_": "author",
86 "sort": "author_raw"
99 "sort": "author_raw"
87 }, title: "${_('Author')}", className: "td-user", orderable: false
100 }, title: "${_('Author')}", className: "td-user", orderable: false
88 },
101 },
89 {
102 {
90 data: {
103 data: {
91 "_": "comments",
104 "_": "comments",
92 "sort": "comments_raw"
105 "sort": "comments_raw"
93 }, title: "", className: "td-comments", orderable: false
106 }, title: "", className: "td-comments", orderable: false
94 },
107 },
95 {
108 {
96 data: {
109 data: {
97 "_": "updated_on",
110 "_": "updated_on",
98 "sort": "updated_on_raw"
111 "sort": "updated_on_raw"
99 }, title: "${_('Last Update')}", className: "td-time"
112 }, title: "${_('Last Update')}", className: "td-time"
100 },
113 },
101 {
114 {
102 data: {
115 data: {
103 "_": "target_repo",
116 "_": "target_repo",
104 "sort": "target_repo"
117 "sort": "target_repo"
105 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
118 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
106 },
119 },
107 ],
120 ],
108 language: {
121 language: {
109 paginate: DEFAULT_GRID_PAGINATION,
122 paginate: DEFAULT_GRID_PAGINATION,
110 sProcessing: _gettext('loading...'),
123 sProcessing: _gettext('loading...'),
111 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
124 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
112 },
125 },
113 "drawCallback": function (settings, json) {
126 "drawCallback": function (settings, json) {
114 timeagoActivate();
127 timeagoActivate();
115 tooltipActivate();
128 tooltipActivate();
116 },
129 },
117 "createdRow": function (row, data, index) {
130 "createdRow": function (row, data, index) {
118 if (data['closed']) {
131 if (data['closed']) {
119 $(row).addClass('closed');
132 $(row).addClass('closed');
120 }
133 }
121 if (data['owned']) {
134 if (data['owned']) {
122 $(row).addClass('owned');
135 $(row).addClass('owned');
123 }
136 }
124 },
137 },
125 "stateSaveParams": function (settings, data) {
138 "stateSaveParams": function (settings, data) {
126 data.search.search = ""; // Don't save search
139 data.search.search = ""; // Don't save search
127 data.start = 0; // don't save pagination
140 data.start = 0; // don't save pagination
128 }
141 }
129 });
142 });
130 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
143 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
131 $pullRequestListTable.css('opacity', 1);
144 $pullRequestListTable.css('opacity', 1);
132 });
145 });
133
146
134 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
147 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
135 $pullRequestListTable.css('opacity', 0.3);
148 $pullRequestListTable.css('opacity', 0.3);
136 });
149 });
137
150
138
151
139 // filter
152 // filter
140 $('#q_filter').on('keyup',
153 $('#q_filter').on('keyup',
141 $.debounce(250, function () {
154 $.debounce(250, function () {
142 $pullRequestListTable.DataTable().search(
155 $pullRequestListTable.DataTable().search(
143 $('#q_filter').val()
156 $('#q_filter').val()
144 ).draw();
157 ).draw();
145 })
158 })
146 );
159 );
147
160
148 });
161 });
149
162
150
163
151 </script>
164 </script>
@@ -1,146 +1,177 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('{} Pull Requests').format(c.repo_name)}
4 ${_('{} Pull Requests').format(c.repo_name)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()"></%def>
10 <%def name="breadcrumbs_links()"></%def>
11
11
12 <%def name="menu_bar_nav()">
12 <%def name="menu_bar_nav()">
13 ${self.menu_items(active='repositories')}
13 ${self.menu_items(active='repositories')}
14 </%def>
14 </%def>
15
15
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='showpullrequest')}
18 ${self.repo_menu(active='showpullrequest')}
19 </%def>
19 </%def>
20
20
21
21
22 <%def name="main()">
22 <%def name="main()">
23
23
24 <div class="box">
24 <div class="box">
25 <div class="title">
25 <div class="title">
26
26 <ul class="button-links">
27 <ul class="button-links">
27 <li class="btn ${h.is_active('open', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 <li><a class="btn ${h.is_active('open', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'open':1})}">${_('Open')}</a></li>
28 <li class="btn ${h.is_active('my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
29 <li><a class="btn ${h.is_active('my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Created by me')}</a></li>
29 <li class="btn ${h.is_active('awaiting', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 <li><a class="btn ${h.is_active('awaiting', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 <li class="btn ${h.is_active('awaiting_my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 <li><a class="btn ${h.is_active('awaiting_my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 <li class="btn ${h.is_active('closed', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 <li><a class="btn ${h.is_active('closed', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 <li class="btn ${h.is_active('source', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 <li><a class="btn ${h.is_active('source', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 </ul>
34 </ul>
34
35
35 <ul class="links">
36 <ul class="links">
36 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 <li>
38 <li>
38 <span>
39 <span>
39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 ${_('Open new Pull Request')}
41 ${_('Open new Pull Request')}
41 </a>
42 </a>
42 </span>
43 </span>
43 </li>
44 </li>
44 % endif
45 % endif
45
46
46 <li>
47 <li>
47 <div class="grid-quick-filter">
48 <div class="grid-quick-filter">
48 <ul class="grid-filter-box">
49 <ul class="grid-filter-box">
49 <li class="grid-filter-box-icon">
50 <li class="grid-filter-box-icon">
50 <i class="icon-search"></i>
51 <i class="icon-search"></i>
51 </li>
52 </li>
52 <li class="grid-filter-box-input">
53 <li class="grid-filter-box-input">
53 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
54 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
54 </li>
55 </li>
55 </ul>
56 </ul>
56 </div>
57 </div>
57 </li>
58 </li>
58
59
59 </ul>
60 </ul>
60
61
61 </div>
62 </div>
62
63
63 <div class="main-content-full-width">
64 <div class="main-content-full-width">
64 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 </div>
66 </div>
66
67
67 </div>
68 </div>
68
69
69 <script type="text/javascript">
70 <script type="text/javascript">
70 $(document).ready(function() {
71 $(document).ready(function() {
71 var $pullRequestListTable = $('#pull_request_list_table');
72 var $pullRequestListTable = $('#pull_request_list_table');
72
73
73 // object list
74 // object list
74 $pullRequestListTable.DataTable({
75 $pullRequestListTable.DataTable({
75 processing: true,
76 processing: true,
76 serverSide: true,
77 serverSide: true,
77 stateSave: true,
78 stateSave: true,
78 stateDuration: -1,
79 stateDuration: -1,
79 ajax: {
80 ajax: {
80 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
81 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
81 "data": function (d) {
82 "data": function (d) {
82 d.source = "${c.source}";
83 d.source = "${c.source}";
83 d.closed = "${c.closed}";
84 d.closed = "${c.closed}";
84 d.my = "${c.my}";
85 d.my = "${c.my}";
85 d.awaiting_review = "${c.awaiting_review}";
86 d.awaiting_review = "${c.awaiting_review}";
86 d.awaiting_my_review = "${c.awaiting_my_review}";
87 d.awaiting_my_review = "${c.awaiting_my_review}";
87 }
88 }
88 },
89 },
89 dom: 'rtp',
90 dom: 'rtp',
90 pageLength: ${c.visual.dashboard_items},
91 pageLength: ${c.visual.dashboard_items},
91 order: [[ 1, "desc" ]],
92 order: [[ 2, "desc" ]],
92 columns: [
93 columns: [
93 { data: {"_": "status",
94 {
94 "sort": "status"}, title: "", className: "td-status", orderable: false},
95 data: {
95 { data: {"_": "name",
96 "_": "status",
96 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
97 "sort": "status"
97 { data: {"_": "title",
98 }, title: "PR", className: "td-status", orderable: false
98 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
99 },
99 { data: {"_": "author",
100 {
100 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
101 data: {
101 { data: {"_": "comments",
102 "_": "my_status",
102 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
103 "sort": "status"
103 { data: {"_": "updated_on",
104 }, title: "You", className: "td-status", orderable: false
104 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
105 },
106 {
107 data: {
108 "_": "name",
109 "sort": "name_raw"
110 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
111 },
112 {
113 data: {
114 "_": "title",
115 "sort": "title"
116 }, title: "${_('Title')}", className: "td-description"
117 },
118 {
119 data: {
120 "_": "author",
121 "sort": "author_raw"
122 }, title: "${_('Author')}", className: "td-user", orderable: false
123 },
124 {
125 data: {
126 "_": "comments",
127 "sort": "comments_raw"
128 }, title: "", className: "td-comments", orderable: false
129 },
130 {
131 data: {
132 "_": "updated_on",
133 "sort": "updated_on_raw"
134 }, title: "${_('Last Update')}", className: "td-time"
135 }
105 ],
136 ],
106 language: {
137 language: {
107 paginate: DEFAULT_GRID_PAGINATION,
138 paginate: DEFAULT_GRID_PAGINATION,
108 sProcessing: _gettext('loading...'),
139 sProcessing: _gettext('loading...'),
109 emptyTable: _gettext("No pull requests available yet.")
140 emptyTable: _gettext("No pull requests available yet.")
110 },
141 },
111 "drawCallback": function( settings, json ) {
142 "drawCallback": function( settings, json ) {
112 timeagoActivate();
143 timeagoActivate();
113 tooltipActivate();
144 tooltipActivate();
114 },
145 },
115 "createdRow": function ( row, data, index ) {
146 "createdRow": function ( row, data, index ) {
116 if (data['closed']) {
147 if (data['closed']) {
117 $(row).addClass('closed');
148 $(row).addClass('closed');
118 }
149 }
119 },
150 },
120 "stateSaveParams": function (settings, data) {
151 "stateSaveParams": function (settings, data) {
121 data.search.search = ""; // Don't save search
152 data.search.search = ""; // Don't save search
122 data.start = 0; // don't save pagination
153 data.start = 0; // don't save pagination
123 }
154 }
124 });
155 });
125
156
126 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
157 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
127 $pullRequestListTable.css('opacity', 1);
158 $pullRequestListTable.css('opacity', 1);
128 });
159 });
129
160
130 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
161 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
131 $pullRequestListTable.css('opacity', 0.3);
162 $pullRequestListTable.css('opacity', 0.3);
132 });
163 });
133
164
134 // filter
165 // filter
135 $('#q_filter').on('keyup',
166 $('#q_filter').on('keyup',
136 $.debounce(250, function() {
167 $.debounce(250, function() {
137 $pullRequestListTable.DataTable().search(
168 $pullRequestListTable.DataTable().search(
138 $('#q_filter').val()
169 $('#q_filter').val()
139 ).draw();
170 ).draw();
140 })
171 })
141 );
172 );
142
173
143 });
174 });
144
175
145 </script>
176 </script>
146 </%def>
177 </%def>
General Comments 1
Under Review
author

Auto status change to "Under Review"

You need to be logged in to leave comments. Login now