##// END OF EJS Templates
git: use force fetch and update for target ref. This solves a case...
marcink -
r2784:e8c62649 default
parent child Browse files
Show More
@@ -1,1140 +1,1203 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.tests import (
33 from rhodecode.tests import (
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
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 @pytest.mark.usefixtures('app', 'autologin_user')
63 @pytest.mark.usefixtures('app', 'autologin_user')
64 @pytest.mark.backends("git", "hg")
64 @pytest.mark.backends("git", "hg")
65 class TestPullrequestsView(object):
65 class TestPullrequestsView(object):
66
66
67 def test_index(self, backend):
67 def test_index(self, backend):
68 self.app.get(route_path(
68 self.app.get(route_path(
69 'pullrequest_new',
69 'pullrequest_new',
70 repo_name=backend.repo_name))
70 repo_name=backend.repo_name))
71
71
72 def test_option_menu_create_pull_request_exists(self, backend):
72 def test_option_menu_create_pull_request_exists(self, backend):
73 repo_name = backend.repo_name
73 repo_name = backend.repo_name
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75
75
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 'pullrequest_new', repo_name=repo_name)
77 'pullrequest_new', repo_name=repo_name)
78 response.mustcontain(create_pr_link)
78 response.mustcontain(create_pr_link)
79
79
80 def test_create_pr_form_with_raw_commit_id(self, backend):
80 def test_create_pr_form_with_raw_commit_id(self, backend):
81 repo = backend.repo
81 repo = backend.repo
82
82
83 self.app.get(
83 self.app.get(
84 route_path('pullrequest_new',
84 route_path('pullrequest_new',
85 repo_name=repo.repo_name,
85 repo_name=repo.repo_name,
86 commit=repo.get_commit().raw_id),
86 commit=repo.get_commit().raw_id),
87 status=200)
87 status=200)
88
88
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 def test_show(self, pr_util, pr_merge_enabled):
90 def test_show(self, pr_util, pr_merge_enabled):
91 pull_request = pr_util.create_pull_request(
91 pull_request = pr_util.create_pull_request(
92 mergeable=pr_merge_enabled, enable_notifications=False)
92 mergeable=pr_merge_enabled, enable_notifications=False)
93
93
94 response = self.app.get(route_path(
94 response = self.app.get(route_path(
95 'pullrequest_show',
95 'pullrequest_show',
96 repo_name=pull_request.target_repo.scm_instance().name,
96 repo_name=pull_request.target_repo.scm_instance().name,
97 pull_request_id=pull_request.pull_request_id))
97 pull_request_id=pull_request.pull_request_id))
98
98
99 for commit_id in pull_request.revisions:
99 for commit_id in pull_request.revisions:
100 response.mustcontain(commit_id)
100 response.mustcontain(commit_id)
101
101
102 assert pull_request.target_ref_parts.type in response
102 assert pull_request.target_ref_parts.type in response
103 assert pull_request.target_ref_parts.name in response
103 assert pull_request.target_ref_parts.name in response
104 target_clone_url = pull_request.target_repo.clone_url()
104 target_clone_url = pull_request.target_repo.clone_url()
105 assert target_clone_url in response
105 assert target_clone_url in response
106
106
107 assert 'class="pull-request-merge"' in response
107 assert 'class="pull-request-merge"' in response
108 assert (
108 assert (
109 'Server-side pull request merging is disabled.'
109 'Server-side pull request merging is disabled.'
110 in response) != pr_merge_enabled
110 in response) != pr_merge_enabled
111
111
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
113 # Logout
113 # Logout
114 response = self.app.post(
114 response = self.app.post(
115 h.route_path('logout'),
115 h.route_path('logout'),
116 params={'csrf_token': csrf_token})
116 params={'csrf_token': csrf_token})
117 # Login as regular user
117 # Login as regular user
118 response = self.app.post(h.route_path('login'),
118 response = self.app.post(h.route_path('login'),
119 {'username': TEST_USER_REGULAR_LOGIN,
119 {'username': TEST_USER_REGULAR_LOGIN,
120 'password': 'test12'})
120 'password': 'test12'})
121
121
122 pull_request = pr_util.create_pull_request(
122 pull_request = pr_util.create_pull_request(
123 author=TEST_USER_REGULAR_LOGIN)
123 author=TEST_USER_REGULAR_LOGIN)
124
124
125 response = self.app.get(route_path(
125 response = self.app.get(route_path(
126 'pullrequest_show',
126 'pullrequest_show',
127 repo_name=pull_request.target_repo.scm_instance().name,
127 repo_name=pull_request.target_repo.scm_instance().name,
128 pull_request_id=pull_request.pull_request_id))
128 pull_request_id=pull_request.pull_request_id))
129
129
130 response.mustcontain('Server-side pull request merging is disabled.')
130 response.mustcontain('Server-side pull request merging is disabled.')
131
131
132 assert_response = response.assert_response()
132 assert_response = response.assert_response()
133 # for regular user without a merge permissions, we don't see it
133 # for regular user without a merge permissions, we don't see it
134 assert_response.no_element_exists('#close-pull-request-action')
134 assert_response.no_element_exists('#close-pull-request-action')
135
135
136 user_util.grant_user_permission_to_repo(
136 user_util.grant_user_permission_to_repo(
137 pull_request.target_repo,
137 pull_request.target_repo,
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
139 'repository.write')
139 'repository.write')
140 response = self.app.get(route_path(
140 response = self.app.get(route_path(
141 'pullrequest_show',
141 'pullrequest_show',
142 repo_name=pull_request.target_repo.scm_instance().name,
142 repo_name=pull_request.target_repo.scm_instance().name,
143 pull_request_id=pull_request.pull_request_id))
143 pull_request_id=pull_request.pull_request_id))
144
144
145 response.mustcontain('Server-side pull request merging is disabled.')
145 response.mustcontain('Server-side pull request merging is disabled.')
146
146
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 # now regular user has a merge permissions, we have CLOSE button
148 # now regular user has a merge permissions, we have CLOSE button
149 assert_response.one_element_exists('#close-pull-request-action')
149 assert_response.one_element_exists('#close-pull-request-action')
150
150
151 def test_show_invalid_commit_id(self, pr_util):
151 def test_show_invalid_commit_id(self, pr_util):
152 # Simulating invalid revisions which will cause a lookup error
152 # Simulating invalid revisions which will cause a lookup error
153 pull_request = pr_util.create_pull_request()
153 pull_request = pr_util.create_pull_request()
154 pull_request.revisions = ['invalid']
154 pull_request.revisions = ['invalid']
155 Session().add(pull_request)
155 Session().add(pull_request)
156 Session().commit()
156 Session().commit()
157
157
158 response = self.app.get(route_path(
158 response = self.app.get(route_path(
159 'pullrequest_show',
159 'pullrequest_show',
160 repo_name=pull_request.target_repo.scm_instance().name,
160 repo_name=pull_request.target_repo.scm_instance().name,
161 pull_request_id=pull_request.pull_request_id))
161 pull_request_id=pull_request.pull_request_id))
162
162
163 for commit_id in pull_request.revisions:
163 for commit_id in pull_request.revisions:
164 response.mustcontain(commit_id)
164 response.mustcontain(commit_id)
165
165
166 def test_show_invalid_source_reference(self, pr_util):
166 def test_show_invalid_source_reference(self, pr_util):
167 pull_request = pr_util.create_pull_request()
167 pull_request = pr_util.create_pull_request()
168 pull_request.source_ref = 'branch:b:invalid'
168 pull_request.source_ref = 'branch:b:invalid'
169 Session().add(pull_request)
169 Session().add(pull_request)
170 Session().commit()
170 Session().commit()
171
171
172 self.app.get(route_path(
172 self.app.get(route_path(
173 'pullrequest_show',
173 'pullrequest_show',
174 repo_name=pull_request.target_repo.scm_instance().name,
174 repo_name=pull_request.target_repo.scm_instance().name,
175 pull_request_id=pull_request.pull_request_id))
175 pull_request_id=pull_request.pull_request_id))
176
176
177 def test_edit_title_description(self, pr_util, csrf_token):
177 def test_edit_title_description(self, pr_util, csrf_token):
178 pull_request = pr_util.create_pull_request()
178 pull_request = pr_util.create_pull_request()
179 pull_request_id = pull_request.pull_request_id
179 pull_request_id = pull_request.pull_request_id
180
180
181 response = self.app.post(
181 response = self.app.post(
182 route_path('pullrequest_update',
182 route_path('pullrequest_update',
183 repo_name=pull_request.target_repo.repo_name,
183 repo_name=pull_request.target_repo.repo_name,
184 pull_request_id=pull_request_id),
184 pull_request_id=pull_request_id),
185 params={
185 params={
186 'edit_pull_request': 'true',
186 'edit_pull_request': 'true',
187 'title': 'New title',
187 'title': 'New title',
188 'description': 'New description',
188 'description': 'New description',
189 'csrf_token': csrf_token})
189 'csrf_token': csrf_token})
190
190
191 assert_session_flash(
191 assert_session_flash(
192 response, u'Pull request title & description updated.',
192 response, u'Pull request title & description updated.',
193 category='success')
193 category='success')
194
194
195 pull_request = PullRequest.get(pull_request_id)
195 pull_request = PullRequest.get(pull_request_id)
196 assert pull_request.title == 'New title'
196 assert pull_request.title == 'New title'
197 assert pull_request.description == 'New description'
197 assert pull_request.description == 'New description'
198
198
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
200 pull_request = pr_util.create_pull_request()
200 pull_request = pr_util.create_pull_request()
201 pull_request_id = pull_request.pull_request_id
201 pull_request_id = pull_request.pull_request_id
202 repo_name = pull_request.target_repo.repo_name
202 repo_name = pull_request.target_repo.repo_name
203 pr_util.close()
203 pr_util.close()
204
204
205 response = self.app.post(
205 response = self.app.post(
206 route_path('pullrequest_update',
206 route_path('pullrequest_update',
207 repo_name=repo_name, pull_request_id=pull_request_id),
207 repo_name=repo_name, pull_request_id=pull_request_id),
208 params={
208 params={
209 'edit_pull_request': 'true',
209 'edit_pull_request': 'true',
210 'title': 'New title',
210 'title': 'New title',
211 'description': 'New description',
211 'description': 'New description',
212 'csrf_token': csrf_token}, status=200)
212 'csrf_token': csrf_token}, status=200)
213 assert_session_flash(
213 assert_session_flash(
214 response, u'Cannot update closed pull requests.',
214 response, u'Cannot update closed pull requests.',
215 category='error')
215 category='error')
216
216
217 def test_update_invalid_source_reference(self, pr_util, csrf_token):
217 def test_update_invalid_source_reference(self, pr_util, csrf_token):
218 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
218 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
219
219
220 pull_request = pr_util.create_pull_request()
220 pull_request = pr_util.create_pull_request()
221 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
221 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
222 Session().add(pull_request)
222 Session().add(pull_request)
223 Session().commit()
223 Session().commit()
224
224
225 pull_request_id = pull_request.pull_request_id
225 pull_request_id = pull_request.pull_request_id
226
226
227 response = self.app.post(
227 response = self.app.post(
228 route_path('pullrequest_update',
228 route_path('pullrequest_update',
229 repo_name=pull_request.target_repo.repo_name,
229 repo_name=pull_request.target_repo.repo_name,
230 pull_request_id=pull_request_id),
230 pull_request_id=pull_request_id),
231 params={'update_commits': 'true',
231 params={'update_commits': 'true',
232 'csrf_token': csrf_token})
232 'csrf_token': csrf_token})
233
233
234 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
234 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
235 UpdateFailureReason.MISSING_SOURCE_REF])
235 UpdateFailureReason.MISSING_SOURCE_REF])
236 assert_session_flash(response, expected_msg, category='error')
236 assert_session_flash(response, expected_msg, category='error')
237
237
238 def test_missing_target_reference(self, pr_util, csrf_token):
238 def test_missing_target_reference(self, pr_util, csrf_token):
239 from rhodecode.lib.vcs.backends.base import MergeFailureReason
239 from rhodecode.lib.vcs.backends.base import MergeFailureReason
240 pull_request = pr_util.create_pull_request(
240 pull_request = pr_util.create_pull_request(
241 approved=True, mergeable=True)
241 approved=True, mergeable=True)
242 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
242 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
243 Session().add(pull_request)
243 Session().add(pull_request)
244 Session().commit()
244 Session().commit()
245
245
246 pull_request_id = pull_request.pull_request_id
246 pull_request_id = pull_request.pull_request_id
247 pull_request_url = route_path(
247 pull_request_url = route_path(
248 'pullrequest_show',
248 'pullrequest_show',
249 repo_name=pull_request.target_repo.repo_name,
249 repo_name=pull_request.target_repo.repo_name,
250 pull_request_id=pull_request_id)
250 pull_request_id=pull_request_id)
251
251
252 response = self.app.get(pull_request_url)
252 response = self.app.get(pull_request_url)
253
253
254 assertr = AssertResponse(response)
254 assertr = AssertResponse(response)
255 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
255 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
256 MergeFailureReason.MISSING_TARGET_REF]
256 MergeFailureReason.MISSING_TARGET_REF]
257 assertr.element_contains(
257 assertr.element_contains(
258 'span[data-role="merge-message"]', str(expected_msg))
258 'span[data-role="merge-message"]', str(expected_msg))
259
259
260 def test_comment_and_close_pull_request_custom_message_approved(
260 def test_comment_and_close_pull_request_custom_message_approved(
261 self, pr_util, csrf_token, xhr_header):
261 self, pr_util, csrf_token, xhr_header):
262
262
263 pull_request = pr_util.create_pull_request(approved=True)
263 pull_request = pr_util.create_pull_request(approved=True)
264 pull_request_id = pull_request.pull_request_id
264 pull_request_id = pull_request.pull_request_id
265 author = pull_request.user_id
265 author = pull_request.user_id
266 repo = pull_request.target_repo.repo_id
266 repo = pull_request.target_repo.repo_id
267
267
268 self.app.post(
268 self.app.post(
269 route_path('pullrequest_comment_create',
269 route_path('pullrequest_comment_create',
270 repo_name=pull_request.target_repo.scm_instance().name,
270 repo_name=pull_request.target_repo.scm_instance().name,
271 pull_request_id=pull_request_id),
271 pull_request_id=pull_request_id),
272 params={
272 params={
273 'close_pull_request': '1',
273 'close_pull_request': '1',
274 'text': 'Closing a PR',
274 'text': 'Closing a PR',
275 'csrf_token': csrf_token},
275 'csrf_token': csrf_token},
276 extra_environ=xhr_header,)
276 extra_environ=xhr_header,)
277
277
278 journal = UserLog.query()\
278 journal = UserLog.query()\
279 .filter(UserLog.user_id == author)\
279 .filter(UserLog.user_id == author)\
280 .filter(UserLog.repository_id == repo) \
280 .filter(UserLog.repository_id == repo) \
281 .order_by('user_log_id') \
281 .order_by('user_log_id') \
282 .all()
282 .all()
283 assert journal[-1].action == 'repo.pull_request.close'
283 assert journal[-1].action == 'repo.pull_request.close'
284
284
285 pull_request = PullRequest.get(pull_request_id)
285 pull_request = PullRequest.get(pull_request_id)
286 assert pull_request.is_closed()
286 assert pull_request.is_closed()
287
287
288 status = ChangesetStatusModel().get_status(
288 status = ChangesetStatusModel().get_status(
289 pull_request.source_repo, pull_request=pull_request)
289 pull_request.source_repo, pull_request=pull_request)
290 assert status == ChangesetStatus.STATUS_APPROVED
290 assert status == ChangesetStatus.STATUS_APPROVED
291 comments = ChangesetComment().query() \
291 comments = ChangesetComment().query() \
292 .filter(ChangesetComment.pull_request == pull_request) \
292 .filter(ChangesetComment.pull_request == pull_request) \
293 .order_by(ChangesetComment.comment_id.asc())\
293 .order_by(ChangesetComment.comment_id.asc())\
294 .all()
294 .all()
295 assert comments[-1].text == 'Closing a PR'
295 assert comments[-1].text == 'Closing a PR'
296
296
297 def test_comment_force_close_pull_request_rejected(
297 def test_comment_force_close_pull_request_rejected(
298 self, pr_util, csrf_token, xhr_header):
298 self, pr_util, csrf_token, xhr_header):
299 pull_request = pr_util.create_pull_request()
299 pull_request = pr_util.create_pull_request()
300 pull_request_id = pull_request.pull_request_id
300 pull_request_id = pull_request.pull_request_id
301 PullRequestModel().update_reviewers(
301 PullRequestModel().update_reviewers(
302 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
302 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
303 pull_request.author)
303 pull_request.author)
304 author = pull_request.user_id
304 author = pull_request.user_id
305 repo = pull_request.target_repo.repo_id
305 repo = pull_request.target_repo.repo_id
306
306
307 self.app.post(
307 self.app.post(
308 route_path('pullrequest_comment_create',
308 route_path('pullrequest_comment_create',
309 repo_name=pull_request.target_repo.scm_instance().name,
309 repo_name=pull_request.target_repo.scm_instance().name,
310 pull_request_id=pull_request_id),
310 pull_request_id=pull_request_id),
311 params={
311 params={
312 'close_pull_request': '1',
312 'close_pull_request': '1',
313 'csrf_token': csrf_token},
313 'csrf_token': csrf_token},
314 extra_environ=xhr_header)
314 extra_environ=xhr_header)
315
315
316 pull_request = PullRequest.get(pull_request_id)
316 pull_request = PullRequest.get(pull_request_id)
317
317
318 journal = UserLog.query()\
318 journal = UserLog.query()\
319 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
319 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
320 .order_by('user_log_id') \
320 .order_by('user_log_id') \
321 .all()
321 .all()
322 assert journal[-1].action == 'repo.pull_request.close'
322 assert journal[-1].action == 'repo.pull_request.close'
323
323
324 # check only the latest status, not the review status
324 # check only the latest status, not the review status
325 status = ChangesetStatusModel().get_status(
325 status = ChangesetStatusModel().get_status(
326 pull_request.source_repo, pull_request=pull_request)
326 pull_request.source_repo, pull_request=pull_request)
327 assert status == ChangesetStatus.STATUS_REJECTED
327 assert status == ChangesetStatus.STATUS_REJECTED
328
328
329 def test_comment_and_close_pull_request(
329 def test_comment_and_close_pull_request(
330 self, pr_util, csrf_token, xhr_header):
330 self, pr_util, csrf_token, xhr_header):
331 pull_request = pr_util.create_pull_request()
331 pull_request = pr_util.create_pull_request()
332 pull_request_id = pull_request.pull_request_id
332 pull_request_id = pull_request.pull_request_id
333
333
334 response = self.app.post(
334 response = self.app.post(
335 route_path('pullrequest_comment_create',
335 route_path('pullrequest_comment_create',
336 repo_name=pull_request.target_repo.scm_instance().name,
336 repo_name=pull_request.target_repo.scm_instance().name,
337 pull_request_id=pull_request.pull_request_id),
337 pull_request_id=pull_request.pull_request_id),
338 params={
338 params={
339 'close_pull_request': 'true',
339 'close_pull_request': 'true',
340 'csrf_token': csrf_token},
340 'csrf_token': csrf_token},
341 extra_environ=xhr_header)
341 extra_environ=xhr_header)
342
342
343 assert response.json
343 assert response.json
344
344
345 pull_request = PullRequest.get(pull_request_id)
345 pull_request = PullRequest.get(pull_request_id)
346 assert pull_request.is_closed()
346 assert pull_request.is_closed()
347
347
348 # check only the latest status, not the review status
348 # check only the latest status, not the review status
349 status = ChangesetStatusModel().get_status(
349 status = ChangesetStatusModel().get_status(
350 pull_request.source_repo, pull_request=pull_request)
350 pull_request.source_repo, pull_request=pull_request)
351 assert status == ChangesetStatus.STATUS_REJECTED
351 assert status == ChangesetStatus.STATUS_REJECTED
352
352
353 def test_create_pull_request(self, backend, csrf_token):
353 def test_create_pull_request(self, backend, csrf_token):
354 commits = [
354 commits = [
355 {'message': 'ancestor'},
355 {'message': 'ancestor'},
356 {'message': 'change'},
356 {'message': 'change'},
357 {'message': 'change2'},
357 {'message': 'change2'},
358 ]
358 ]
359 commit_ids = backend.create_master_repo(commits)
359 commit_ids = backend.create_master_repo(commits)
360 target = backend.create_repo(heads=['ancestor'])
360 target = backend.create_repo(heads=['ancestor'])
361 source = backend.create_repo(heads=['change2'])
361 source = backend.create_repo(heads=['change2'])
362
362
363 response = self.app.post(
363 response = self.app.post(
364 route_path('pullrequest_create', repo_name=source.repo_name),
364 route_path('pullrequest_create', repo_name=source.repo_name),
365 [
365 [
366 ('source_repo', source.repo_name),
366 ('source_repo', source.repo_name),
367 ('source_ref', 'branch:default:' + commit_ids['change2']),
367 ('source_ref', 'branch:default:' + commit_ids['change2']),
368 ('target_repo', target.repo_name),
368 ('target_repo', target.repo_name),
369 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
369 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
370 ('common_ancestor', commit_ids['ancestor']),
370 ('common_ancestor', commit_ids['ancestor']),
371 ('pullrequest_desc', 'Description'),
371 ('pullrequest_desc', 'Description'),
372 ('pullrequest_title', 'Title'),
372 ('pullrequest_title', 'Title'),
373 ('__start__', 'review_members:sequence'),
373 ('__start__', 'review_members:sequence'),
374 ('__start__', 'reviewer:mapping'),
374 ('__start__', 'reviewer:mapping'),
375 ('user_id', '1'),
375 ('user_id', '1'),
376 ('__start__', 'reasons:sequence'),
376 ('__start__', 'reasons:sequence'),
377 ('reason', 'Some reason'),
377 ('reason', 'Some reason'),
378 ('__end__', 'reasons:sequence'),
378 ('__end__', 'reasons:sequence'),
379 ('__start__', 'rules:sequence'),
379 ('__start__', 'rules:sequence'),
380 ('__end__', 'rules:sequence'),
380 ('__end__', 'rules:sequence'),
381 ('mandatory', 'False'),
381 ('mandatory', 'False'),
382 ('__end__', 'reviewer:mapping'),
382 ('__end__', 'reviewer:mapping'),
383 ('__end__', 'review_members:sequence'),
383 ('__end__', 'review_members:sequence'),
384 ('__start__', 'revisions:sequence'),
384 ('__start__', 'revisions:sequence'),
385 ('revisions', commit_ids['change']),
385 ('revisions', commit_ids['change']),
386 ('revisions', commit_ids['change2']),
386 ('revisions', commit_ids['change2']),
387 ('__end__', 'revisions:sequence'),
387 ('__end__', 'revisions:sequence'),
388 ('user', ''),
388 ('user', ''),
389 ('csrf_token', csrf_token),
389 ('csrf_token', csrf_token),
390 ],
390 ],
391 status=302)
391 status=302)
392
392
393 location = response.headers['Location']
393 location = response.headers['Location']
394 pull_request_id = location.rsplit('/', 1)[1]
394 pull_request_id = location.rsplit('/', 1)[1]
395 assert pull_request_id != 'new'
395 assert pull_request_id != 'new'
396 pull_request = PullRequest.get(int(pull_request_id))
396 pull_request = PullRequest.get(int(pull_request_id))
397
397
398 # check that we have now both revisions
398 # check that we have now both revisions
399 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
399 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
400 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
400 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
401 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
401 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
402 assert pull_request.target_ref == expected_target_ref
402 assert pull_request.target_ref == expected_target_ref
403
403
404 def test_reviewer_notifications(self, backend, csrf_token):
404 def test_reviewer_notifications(self, backend, csrf_token):
405 # We have to use the app.post for this test so it will create the
405 # We have to use the app.post for this test so it will create the
406 # notifications properly with the new PR
406 # notifications properly with the new PR
407 commits = [
407 commits = [
408 {'message': 'ancestor',
408 {'message': 'ancestor',
409 'added': [FileNode('file_A', content='content_of_ancestor')]},
409 'added': [FileNode('file_A', content='content_of_ancestor')]},
410 {'message': 'change',
410 {'message': 'change',
411 'added': [FileNode('file_a', content='content_of_change')]},
411 'added': [FileNode('file_a', content='content_of_change')]},
412 {'message': 'change-child'},
412 {'message': 'change-child'},
413 {'message': 'ancestor-child', 'parents': ['ancestor'],
413 {'message': 'ancestor-child', 'parents': ['ancestor'],
414 'added': [
414 'added': [
415 FileNode('file_B', content='content_of_ancestor_child')]},
415 FileNode('file_B', content='content_of_ancestor_child')]},
416 {'message': 'ancestor-child-2'},
416 {'message': 'ancestor-child-2'},
417 ]
417 ]
418 commit_ids = backend.create_master_repo(commits)
418 commit_ids = backend.create_master_repo(commits)
419 target = backend.create_repo(heads=['ancestor-child'])
419 target = backend.create_repo(heads=['ancestor-child'])
420 source = backend.create_repo(heads=['change'])
420 source = backend.create_repo(heads=['change'])
421
421
422 response = self.app.post(
422 response = self.app.post(
423 route_path('pullrequest_create', repo_name=source.repo_name),
423 route_path('pullrequest_create', repo_name=source.repo_name),
424 [
424 [
425 ('source_repo', source.repo_name),
425 ('source_repo', source.repo_name),
426 ('source_ref', 'branch:default:' + commit_ids['change']),
426 ('source_ref', 'branch:default:' + commit_ids['change']),
427 ('target_repo', target.repo_name),
427 ('target_repo', target.repo_name),
428 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
428 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
429 ('common_ancestor', commit_ids['ancestor']),
429 ('common_ancestor', commit_ids['ancestor']),
430 ('pullrequest_desc', 'Description'),
430 ('pullrequest_desc', 'Description'),
431 ('pullrequest_title', 'Title'),
431 ('pullrequest_title', 'Title'),
432 ('__start__', 'review_members:sequence'),
432 ('__start__', 'review_members:sequence'),
433 ('__start__', 'reviewer:mapping'),
433 ('__start__', 'reviewer:mapping'),
434 ('user_id', '2'),
434 ('user_id', '2'),
435 ('__start__', 'reasons:sequence'),
435 ('__start__', 'reasons:sequence'),
436 ('reason', 'Some reason'),
436 ('reason', 'Some reason'),
437 ('__end__', 'reasons:sequence'),
437 ('__end__', 'reasons:sequence'),
438 ('__start__', 'rules:sequence'),
438 ('__start__', 'rules:sequence'),
439 ('__end__', 'rules:sequence'),
439 ('__end__', 'rules:sequence'),
440 ('mandatory', 'False'),
440 ('mandatory', 'False'),
441 ('__end__', 'reviewer:mapping'),
441 ('__end__', 'reviewer:mapping'),
442 ('__end__', 'review_members:sequence'),
442 ('__end__', 'review_members:sequence'),
443 ('__start__', 'revisions:sequence'),
443 ('__start__', 'revisions:sequence'),
444 ('revisions', commit_ids['change']),
444 ('revisions', commit_ids['change']),
445 ('__end__', 'revisions:sequence'),
445 ('__end__', 'revisions:sequence'),
446 ('user', ''),
446 ('user', ''),
447 ('csrf_token', csrf_token),
447 ('csrf_token', csrf_token),
448 ],
448 ],
449 status=302)
449 status=302)
450
450
451 location = response.headers['Location']
451 location = response.headers['Location']
452
452
453 pull_request_id = location.rsplit('/', 1)[1]
453 pull_request_id = location.rsplit('/', 1)[1]
454 assert pull_request_id != 'new'
454 assert pull_request_id != 'new'
455 pull_request = PullRequest.get(int(pull_request_id))
455 pull_request = PullRequest.get(int(pull_request_id))
456
456
457 # Check that a notification was made
457 # Check that a notification was made
458 notifications = Notification.query()\
458 notifications = Notification.query()\
459 .filter(Notification.created_by == pull_request.author.user_id,
459 .filter(Notification.created_by == pull_request.author.user_id,
460 Notification.type_ == Notification.TYPE_PULL_REQUEST,
460 Notification.type_ == Notification.TYPE_PULL_REQUEST,
461 Notification.subject.contains(
461 Notification.subject.contains(
462 "wants you to review pull request #%s" % pull_request_id))
462 "wants you to review pull request #%s" % pull_request_id))
463 assert len(notifications.all()) == 1
463 assert len(notifications.all()) == 1
464
464
465 # Change reviewers and check that a notification was made
465 # Change reviewers and check that a notification was made
466 PullRequestModel().update_reviewers(
466 PullRequestModel().update_reviewers(
467 pull_request.pull_request_id, [(1, [], False, [])],
467 pull_request.pull_request_id, [(1, [], False, [])],
468 pull_request.author)
468 pull_request.author)
469 assert len(notifications.all()) == 2
469 assert len(notifications.all()) == 2
470
470
471 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
471 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
472 csrf_token):
472 csrf_token):
473 commits = [
473 commits = [
474 {'message': 'ancestor',
474 {'message': 'ancestor',
475 'added': [FileNode('file_A', content='content_of_ancestor')]},
475 'added': [FileNode('file_A', content='content_of_ancestor')]},
476 {'message': 'change',
476 {'message': 'change',
477 'added': [FileNode('file_a', content='content_of_change')]},
477 'added': [FileNode('file_a', content='content_of_change')]},
478 {'message': 'change-child'},
478 {'message': 'change-child'},
479 {'message': 'ancestor-child', 'parents': ['ancestor'],
479 {'message': 'ancestor-child', 'parents': ['ancestor'],
480 'added': [
480 'added': [
481 FileNode('file_B', content='content_of_ancestor_child')]},
481 FileNode('file_B', content='content_of_ancestor_child')]},
482 {'message': 'ancestor-child-2'},
482 {'message': 'ancestor-child-2'},
483 ]
483 ]
484 commit_ids = backend.create_master_repo(commits)
484 commit_ids = backend.create_master_repo(commits)
485 target = backend.create_repo(heads=['ancestor-child'])
485 target = backend.create_repo(heads=['ancestor-child'])
486 source = backend.create_repo(heads=['change'])
486 source = backend.create_repo(heads=['change'])
487
487
488 response = self.app.post(
488 response = self.app.post(
489 route_path('pullrequest_create', repo_name=source.repo_name),
489 route_path('pullrequest_create', repo_name=source.repo_name),
490 [
490 [
491 ('source_repo', source.repo_name),
491 ('source_repo', source.repo_name),
492 ('source_ref', 'branch:default:' + commit_ids['change']),
492 ('source_ref', 'branch:default:' + commit_ids['change']),
493 ('target_repo', target.repo_name),
493 ('target_repo', target.repo_name),
494 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
494 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
495 ('common_ancestor', commit_ids['ancestor']),
495 ('common_ancestor', commit_ids['ancestor']),
496 ('pullrequest_desc', 'Description'),
496 ('pullrequest_desc', 'Description'),
497 ('pullrequest_title', 'Title'),
497 ('pullrequest_title', 'Title'),
498 ('__start__', 'review_members:sequence'),
498 ('__start__', 'review_members:sequence'),
499 ('__start__', 'reviewer:mapping'),
499 ('__start__', 'reviewer:mapping'),
500 ('user_id', '1'),
500 ('user_id', '1'),
501 ('__start__', 'reasons:sequence'),
501 ('__start__', 'reasons:sequence'),
502 ('reason', 'Some reason'),
502 ('reason', 'Some reason'),
503 ('__end__', 'reasons:sequence'),
503 ('__end__', 'reasons:sequence'),
504 ('__start__', 'rules:sequence'),
504 ('__start__', 'rules:sequence'),
505 ('__end__', 'rules:sequence'),
505 ('__end__', 'rules:sequence'),
506 ('mandatory', 'False'),
506 ('mandatory', 'False'),
507 ('__end__', 'reviewer:mapping'),
507 ('__end__', 'reviewer:mapping'),
508 ('__end__', 'review_members:sequence'),
508 ('__end__', 'review_members:sequence'),
509 ('__start__', 'revisions:sequence'),
509 ('__start__', 'revisions:sequence'),
510 ('revisions', commit_ids['change']),
510 ('revisions', commit_ids['change']),
511 ('__end__', 'revisions:sequence'),
511 ('__end__', 'revisions:sequence'),
512 ('user', ''),
512 ('user', ''),
513 ('csrf_token', csrf_token),
513 ('csrf_token', csrf_token),
514 ],
514 ],
515 status=302)
515 status=302)
516
516
517 location = response.headers['Location']
517 location = response.headers['Location']
518
518
519 pull_request_id = location.rsplit('/', 1)[1]
519 pull_request_id = location.rsplit('/', 1)[1]
520 assert pull_request_id != 'new'
520 assert pull_request_id != 'new'
521 pull_request = PullRequest.get(int(pull_request_id))
521 pull_request = PullRequest.get(int(pull_request_id))
522
522
523 # target_ref has to point to the ancestor's commit_id in order to
523 # target_ref has to point to the ancestor's commit_id in order to
524 # show the correct diff
524 # show the correct diff
525 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
525 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
526 assert pull_request.target_ref == expected_target_ref
526 assert pull_request.target_ref == expected_target_ref
527
527
528 # Check generated diff contents
528 # Check generated diff contents
529 response = response.follow()
529 response = response.follow()
530 assert 'content_of_ancestor' not in response.body
530 assert 'content_of_ancestor' not in response.body
531 assert 'content_of_ancestor-child' not in response.body
531 assert 'content_of_ancestor-child' not in response.body
532 assert 'content_of_change' in response.body
532 assert 'content_of_change' in response.body
533
533
534 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
534 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
535 # Clear any previous calls to rcextensions
535 # Clear any previous calls to rcextensions
536 rhodecode.EXTENSIONS.calls.clear()
536 rhodecode.EXTENSIONS.calls.clear()
537
537
538 pull_request = pr_util.create_pull_request(
538 pull_request = pr_util.create_pull_request(
539 approved=True, mergeable=True)
539 approved=True, mergeable=True)
540 pull_request_id = pull_request.pull_request_id
540 pull_request_id = pull_request.pull_request_id
541 repo_name = pull_request.target_repo.scm_instance().name,
541 repo_name = pull_request.target_repo.scm_instance().name,
542
542
543 response = self.app.post(
543 response = self.app.post(
544 route_path('pullrequest_merge',
544 route_path('pullrequest_merge',
545 repo_name=str(repo_name[0]),
545 repo_name=str(repo_name[0]),
546 pull_request_id=pull_request_id),
546 pull_request_id=pull_request_id),
547 params={'csrf_token': csrf_token}).follow()
547 params={'csrf_token': csrf_token}).follow()
548
548
549 pull_request = PullRequest.get(pull_request_id)
549 pull_request = PullRequest.get(pull_request_id)
550
550
551 assert response.status_int == 200
551 assert response.status_int == 200
552 assert pull_request.is_closed()
552 assert pull_request.is_closed()
553 assert_pull_request_status(
553 assert_pull_request_status(
554 pull_request, ChangesetStatus.STATUS_APPROVED)
554 pull_request, ChangesetStatus.STATUS_APPROVED)
555
555
556 # Check the relevant log entries were added
556 # Check the relevant log entries were added
557 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
557 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
558 actions = [log.action for log in user_logs]
558 actions = [log.action for log in user_logs]
559 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
559 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
560 expected_actions = [
560 expected_actions = [
561 u'repo.pull_request.close',
561 u'repo.pull_request.close',
562 u'repo.pull_request.merge',
562 u'repo.pull_request.merge',
563 u'repo.pull_request.comment.create'
563 u'repo.pull_request.comment.create'
564 ]
564 ]
565 assert actions == expected_actions
565 assert actions == expected_actions
566
566
567 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
567 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
568 actions = [log for log in user_logs]
568 actions = [log for log in user_logs]
569 assert actions[-1].action == 'user.push'
569 assert actions[-1].action == 'user.push'
570 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
570 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
571
571
572 # Check post_push rcextension was really executed
572 # Check post_push rcextension was really executed
573 push_calls = rhodecode.EXTENSIONS.calls['post_push']
573 push_calls = rhodecode.EXTENSIONS.calls['post_push']
574 assert len(push_calls) == 1
574 assert len(push_calls) == 1
575 unused_last_call_args, last_call_kwargs = push_calls[0]
575 unused_last_call_args, last_call_kwargs = push_calls[0]
576 assert last_call_kwargs['action'] == 'push'
576 assert last_call_kwargs['action'] == 'push'
577 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
577 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
578
578
579 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
579 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
580 pull_request = pr_util.create_pull_request(mergeable=False)
580 pull_request = pr_util.create_pull_request(mergeable=False)
581 pull_request_id = pull_request.pull_request_id
581 pull_request_id = pull_request.pull_request_id
582 pull_request = PullRequest.get(pull_request_id)
582 pull_request = PullRequest.get(pull_request_id)
583
583
584 response = self.app.post(
584 response = self.app.post(
585 route_path('pullrequest_merge',
585 route_path('pullrequest_merge',
586 repo_name=pull_request.target_repo.scm_instance().name,
586 repo_name=pull_request.target_repo.scm_instance().name,
587 pull_request_id=pull_request.pull_request_id),
587 pull_request_id=pull_request.pull_request_id),
588 params={'csrf_token': csrf_token}).follow()
588 params={'csrf_token': csrf_token}).follow()
589
589
590 assert response.status_int == 200
590 assert response.status_int == 200
591 response.mustcontain(
591 response.mustcontain(
592 'Merge is not currently possible because of below failed checks.')
592 'Merge is not currently possible because of below failed checks.')
593 response.mustcontain('Server-side pull request merging is disabled.')
593 response.mustcontain('Server-side pull request merging is disabled.')
594
594
595 @pytest.mark.skip_backends('svn')
595 @pytest.mark.skip_backends('svn')
596 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
596 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
597 pull_request = pr_util.create_pull_request(mergeable=True)
597 pull_request = pr_util.create_pull_request(mergeable=True)
598 pull_request_id = pull_request.pull_request_id
598 pull_request_id = pull_request.pull_request_id
599 repo_name = pull_request.target_repo.scm_instance().name
599 repo_name = pull_request.target_repo.scm_instance().name
600
600
601 response = self.app.post(
601 response = self.app.post(
602 route_path('pullrequest_merge',
602 route_path('pullrequest_merge',
603 repo_name=repo_name,
603 repo_name=repo_name,
604 pull_request_id=pull_request_id),
604 pull_request_id=pull_request_id),
605 params={'csrf_token': csrf_token}).follow()
605 params={'csrf_token': csrf_token}).follow()
606
606
607 assert response.status_int == 200
607 assert response.status_int == 200
608
608
609 response.mustcontain(
609 response.mustcontain(
610 'Merge is not currently possible because of below failed checks.')
610 'Merge is not currently possible because of below failed checks.')
611 response.mustcontain('Pull request reviewer approval is pending.')
611 response.mustcontain('Pull request reviewer approval is pending.')
612
612
613 def test_merge_pull_request_renders_failure_reason(
613 def test_merge_pull_request_renders_failure_reason(
614 self, user_regular, csrf_token, pr_util):
614 self, user_regular, csrf_token, pr_util):
615 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
615 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
616 pull_request_id = pull_request.pull_request_id
616 pull_request_id = pull_request.pull_request_id
617 repo_name = pull_request.target_repo.scm_instance().name
617 repo_name = pull_request.target_repo.scm_instance().name
618
618
619 model_patcher = mock.patch.multiple(
619 model_patcher = mock.patch.multiple(
620 PullRequestModel,
620 PullRequestModel,
621 merge=mock.Mock(return_value=MergeResponse(
621 merge=mock.Mock(return_value=MergeResponse(
622 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
622 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
623 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
623 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
624
624
625 with model_patcher:
625 with model_patcher:
626 response = self.app.post(
626 response = self.app.post(
627 route_path('pullrequest_merge',
627 route_path('pullrequest_merge',
628 repo_name=repo_name,
628 repo_name=repo_name,
629 pull_request_id=pull_request_id),
629 pull_request_id=pull_request_id),
630 params={'csrf_token': csrf_token}, status=302)
630 params={'csrf_token': csrf_token}, status=302)
631
631
632 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
632 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
633 MergeFailureReason.PUSH_FAILED])
633 MergeFailureReason.PUSH_FAILED])
634
634
635 def test_update_source_revision(self, backend, csrf_token):
635 def test_update_source_revision(self, backend, csrf_token):
636 commits = [
636 commits = [
637 {'message': 'ancestor'},
637 {'message': 'ancestor'},
638 {'message': 'change'},
638 {'message': 'change'},
639 {'message': 'change-2'},
639 {'message': 'change-2'},
640 ]
640 ]
641 commit_ids = backend.create_master_repo(commits)
641 commit_ids = backend.create_master_repo(commits)
642 target = backend.create_repo(heads=['ancestor'])
642 target = backend.create_repo(heads=['ancestor'])
643 source = backend.create_repo(heads=['change'])
643 source = backend.create_repo(heads=['change'])
644
644
645 # create pr from a in source to A in target
645 # create pr from a in source to A in target
646 pull_request = PullRequest()
646 pull_request = PullRequest()
647 pull_request.source_repo = source
647 pull_request.source_repo = source
648 # TODO: johbo: Make sure that we write the source ref this way!
648 # TODO: johbo: Make sure that we write the source ref this way!
649 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
649 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
650 branch=backend.default_branch_name, commit_id=commit_ids['change'])
650 branch=backend.default_branch_name, commit_id=commit_ids['change'])
651 pull_request.target_repo = target
651 pull_request.target_repo = target
652
652
653 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
653 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
654 branch=backend.default_branch_name,
654 branch=backend.default_branch_name,
655 commit_id=commit_ids['ancestor'])
655 commit_id=commit_ids['ancestor'])
656 pull_request.revisions = [commit_ids['change']]
656 pull_request.revisions = [commit_ids['change']]
657 pull_request.title = u"Test"
657 pull_request.title = u"Test"
658 pull_request.description = u"Description"
658 pull_request.description = u"Description"
659 pull_request.author = UserModel().get_by_username(
659 pull_request.author = UserModel().get_by_username(
660 TEST_USER_ADMIN_LOGIN)
660 TEST_USER_ADMIN_LOGIN)
661 Session().add(pull_request)
661 Session().add(pull_request)
662 Session().commit()
662 Session().commit()
663 pull_request_id = pull_request.pull_request_id
663 pull_request_id = pull_request.pull_request_id
664
664
665 # source has ancestor - change - change-2
665 # source has ancestor - change - change-2
666 backend.pull_heads(source, heads=['change-2'])
666 backend.pull_heads(source, heads=['change-2'])
667
667
668 # update PR
668 # update PR
669 self.app.post(
669 self.app.post(
670 route_path('pullrequest_update',
670 route_path('pullrequest_update',
671 repo_name=target.repo_name,
671 repo_name=target.repo_name,
672 pull_request_id=pull_request_id),
672 pull_request_id=pull_request_id),
673 params={'update_commits': 'true',
673 params={'update_commits': 'true',
674 'csrf_token': csrf_token})
674 'csrf_token': csrf_token})
675
675
676 # check that we have now both revisions
676 # check that we have now both revisions
677 pull_request = PullRequest.get(pull_request_id)
677 pull_request = PullRequest.get(pull_request_id)
678 assert pull_request.revisions == [
678 assert pull_request.revisions == [
679 commit_ids['change-2'], commit_ids['change']]
679 commit_ids['change-2'], commit_ids['change']]
680
680
681 # TODO: johbo: this should be a test on its own
681 # TODO: johbo: this should be a test on its own
682 response = self.app.get(route_path(
682 response = self.app.get(route_path(
683 'pullrequest_new',
683 'pullrequest_new',
684 repo_name=target.repo_name))
684 repo_name=target.repo_name))
685 assert response.status_int == 200
685 assert response.status_int == 200
686 assert 'Pull request updated to' in response.body
686 assert 'Pull request updated to' in response.body
687 assert 'with 1 added, 0 removed commits.' in response.body
687 assert 'with 1 added, 0 removed commits.' in response.body
688
688
689 def test_update_target_revision(self, backend, csrf_token):
689 def test_update_target_revision(self, backend, csrf_token):
690 commits = [
690 commits = [
691 {'message': 'ancestor'},
691 {'message': 'ancestor'},
692 {'message': 'change'},
692 {'message': 'change'},
693 {'message': 'ancestor-new', 'parents': ['ancestor']},
693 {'message': 'ancestor-new', 'parents': ['ancestor']},
694 {'message': 'change-rebased'},
694 {'message': 'change-rebased'},
695 ]
695 ]
696 commit_ids = backend.create_master_repo(commits)
696 commit_ids = backend.create_master_repo(commits)
697 target = backend.create_repo(heads=['ancestor'])
697 target = backend.create_repo(heads=['ancestor'])
698 source = backend.create_repo(heads=['change'])
698 source = backend.create_repo(heads=['change'])
699
699
700 # create pr from a in source to A in target
700 # create pr from a in source to A in target
701 pull_request = PullRequest()
701 pull_request = PullRequest()
702 pull_request.source_repo = source
702 pull_request.source_repo = source
703 # TODO: johbo: Make sure that we write the source ref this way!
703 # TODO: johbo: Make sure that we write the source ref this way!
704 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
704 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
705 branch=backend.default_branch_name, commit_id=commit_ids['change'])
705 branch=backend.default_branch_name, commit_id=commit_ids['change'])
706 pull_request.target_repo = target
706 pull_request.target_repo = target
707 # TODO: johbo: Target ref should be branch based, since tip can jump
707 # TODO: johbo: Target ref should be branch based, since tip can jump
708 # from branch to branch
708 # from branch to branch
709 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
709 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
710 branch=backend.default_branch_name,
710 branch=backend.default_branch_name,
711 commit_id=commit_ids['ancestor'])
711 commit_id=commit_ids['ancestor'])
712 pull_request.revisions = [commit_ids['change']]
712 pull_request.revisions = [commit_ids['change']]
713 pull_request.title = u"Test"
713 pull_request.title = u"Test"
714 pull_request.description = u"Description"
714 pull_request.description = u"Description"
715 pull_request.author = UserModel().get_by_username(
715 pull_request.author = UserModel().get_by_username(
716 TEST_USER_ADMIN_LOGIN)
716 TEST_USER_ADMIN_LOGIN)
717 Session().add(pull_request)
717 Session().add(pull_request)
718 Session().commit()
718 Session().commit()
719 pull_request_id = pull_request.pull_request_id
719 pull_request_id = pull_request.pull_request_id
720
720
721 # target has ancestor - ancestor-new
721 # target has ancestor - ancestor-new
722 # source has ancestor - ancestor-new - change-rebased
722 # source has ancestor - ancestor-new - change-rebased
723 backend.pull_heads(target, heads=['ancestor-new'])
723 backend.pull_heads(target, heads=['ancestor-new'])
724 backend.pull_heads(source, heads=['change-rebased'])
724 backend.pull_heads(source, heads=['change-rebased'])
725
725
726 # update PR
726 # update PR
727 self.app.post(
727 self.app.post(
728 route_path('pullrequest_update',
728 route_path('pullrequest_update',
729 repo_name=target.repo_name,
729 repo_name=target.repo_name,
730 pull_request_id=pull_request_id),
730 pull_request_id=pull_request_id),
731 params={'update_commits': 'true',
731 params={'update_commits': 'true',
732 'csrf_token': csrf_token},
732 'csrf_token': csrf_token},
733 status=200)
733 status=200)
734
734
735 # check that we have now both revisions
735 # check that we have now both revisions
736 pull_request = PullRequest.get(pull_request_id)
736 pull_request = PullRequest.get(pull_request_id)
737 assert pull_request.revisions == [commit_ids['change-rebased']]
737 assert pull_request.revisions == [commit_ids['change-rebased']]
738 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
738 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
739 branch=backend.default_branch_name,
739 branch=backend.default_branch_name,
740 commit_id=commit_ids['ancestor-new'])
740 commit_id=commit_ids['ancestor-new'])
741
741
742 # TODO: johbo: This should be a test on its own
742 # TODO: johbo: This should be a test on its own
743 response = self.app.get(route_path(
743 response = self.app.get(route_path(
744 'pullrequest_new',
744 'pullrequest_new',
745 repo_name=target.repo_name))
745 repo_name=target.repo_name))
746 assert response.status_int == 200
746 assert response.status_int == 200
747 assert 'Pull request updated to' in response.body
747 assert 'Pull request updated to' in response.body
748 assert 'with 1 added, 1 removed commits.' in response.body
748 assert 'with 1 added, 1 removed commits.' in response.body
749
749
750 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
751 backend = backend_git
752 commits = [
753 {'message': 'master-commit-1'},
754 {'message': 'master-commit-2-change-1'},
755 {'message': 'master-commit-3-change-2'},
756
757 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
758 {'message': 'feat-commit-2'},
759 ]
760 commit_ids = backend.create_master_repo(commits)
761 target = backend.create_repo(heads=['master-commit-3-change-2'])
762 source = backend.create_repo(heads=['feat-commit-2'])
763
764 # create pr from a in source to A in target
765 pull_request = PullRequest()
766 pull_request.source_repo = source
767 # TODO: johbo: Make sure that we write the source ref this way!
768 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
769 branch=backend.default_branch_name,
770 commit_id=commit_ids['master-commit-3-change-2'])
771
772 pull_request.target_repo = target
773 # TODO: johbo: Target ref should be branch based, since tip can jump
774 # from branch to branch
775 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
776 branch=backend.default_branch_name,
777 commit_id=commit_ids['feat-commit-2'])
778
779 pull_request.revisions = [
780 commit_ids['feat-commit-1'],
781 commit_ids['feat-commit-2']
782 ]
783 pull_request.title = u"Test"
784 pull_request.description = u"Description"
785 pull_request.author = UserModel().get_by_username(
786 TEST_USER_ADMIN_LOGIN)
787 Session().add(pull_request)
788 Session().commit()
789 pull_request_id = pull_request.pull_request_id
790
791 # PR is created, now we simulate a force-push into target,
792 # that drops a 2 last commits
793 vcsrepo = target.scm_instance()
794 vcsrepo.config.clear_section('hooks')
795 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
796
797 # update PR
798 self.app.post(
799 route_path('pullrequest_update',
800 repo_name=target.repo_name,
801 pull_request_id=pull_request_id),
802 params={'update_commits': 'true',
803 'csrf_token': csrf_token},
804 status=200)
805
806 response = self.app.get(route_path(
807 'pullrequest_new',
808 repo_name=target.repo_name))
809 assert response.status_int == 200
810 response.mustcontain('Pull request updated to')
811 response.mustcontain('with 0 added, 0 removed commits.')
812
750 def test_update_of_ancestor_reference(self, backend, csrf_token):
813 def test_update_of_ancestor_reference(self, backend, csrf_token):
751 commits = [
814 commits = [
752 {'message': 'ancestor'},
815 {'message': 'ancestor'},
753 {'message': 'change'},
816 {'message': 'change'},
754 {'message': 'change-2'},
817 {'message': 'change-2'},
755 {'message': 'ancestor-new', 'parents': ['ancestor']},
818 {'message': 'ancestor-new', 'parents': ['ancestor']},
756 {'message': 'change-rebased'},
819 {'message': 'change-rebased'},
757 ]
820 ]
758 commit_ids = backend.create_master_repo(commits)
821 commit_ids = backend.create_master_repo(commits)
759 target = backend.create_repo(heads=['ancestor'])
822 target = backend.create_repo(heads=['ancestor'])
760 source = backend.create_repo(heads=['change'])
823 source = backend.create_repo(heads=['change'])
761
824
762 # create pr from a in source to A in target
825 # create pr from a in source to A in target
763 pull_request = PullRequest()
826 pull_request = PullRequest()
764 pull_request.source_repo = source
827 pull_request.source_repo = source
765 # TODO: johbo: Make sure that we write the source ref this way!
828 # TODO: johbo: Make sure that we write the source ref this way!
766 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
829 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
767 branch=backend.default_branch_name,
830 branch=backend.default_branch_name,
768 commit_id=commit_ids['change'])
831 commit_id=commit_ids['change'])
769 pull_request.target_repo = target
832 pull_request.target_repo = target
770 # TODO: johbo: Target ref should be branch based, since tip can jump
833 # TODO: johbo: Target ref should be branch based, since tip can jump
771 # from branch to branch
834 # from branch to branch
772 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
835 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
773 branch=backend.default_branch_name,
836 branch=backend.default_branch_name,
774 commit_id=commit_ids['ancestor'])
837 commit_id=commit_ids['ancestor'])
775 pull_request.revisions = [commit_ids['change']]
838 pull_request.revisions = [commit_ids['change']]
776 pull_request.title = u"Test"
839 pull_request.title = u"Test"
777 pull_request.description = u"Description"
840 pull_request.description = u"Description"
778 pull_request.author = UserModel().get_by_username(
841 pull_request.author = UserModel().get_by_username(
779 TEST_USER_ADMIN_LOGIN)
842 TEST_USER_ADMIN_LOGIN)
780 Session().add(pull_request)
843 Session().add(pull_request)
781 Session().commit()
844 Session().commit()
782 pull_request_id = pull_request.pull_request_id
845 pull_request_id = pull_request.pull_request_id
783
846
784 # target has ancestor - ancestor-new
847 # target has ancestor - ancestor-new
785 # source has ancestor - ancestor-new - change-rebased
848 # source has ancestor - ancestor-new - change-rebased
786 backend.pull_heads(target, heads=['ancestor-new'])
849 backend.pull_heads(target, heads=['ancestor-new'])
787 backend.pull_heads(source, heads=['change-rebased'])
850 backend.pull_heads(source, heads=['change-rebased'])
788
851
789 # update PR
852 # update PR
790 self.app.post(
853 self.app.post(
791 route_path('pullrequest_update',
854 route_path('pullrequest_update',
792 repo_name=target.repo_name,
855 repo_name=target.repo_name,
793 pull_request_id=pull_request_id),
856 pull_request_id=pull_request_id),
794 params={'update_commits': 'true',
857 params={'update_commits': 'true',
795 'csrf_token': csrf_token},
858 'csrf_token': csrf_token},
796 status=200)
859 status=200)
797
860
798 # Expect the target reference to be updated correctly
861 # Expect the target reference to be updated correctly
799 pull_request = PullRequest.get(pull_request_id)
862 pull_request = PullRequest.get(pull_request_id)
800 assert pull_request.revisions == [commit_ids['change-rebased']]
863 assert pull_request.revisions == [commit_ids['change-rebased']]
801 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
864 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
802 branch=backend.default_branch_name,
865 branch=backend.default_branch_name,
803 commit_id=commit_ids['ancestor-new'])
866 commit_id=commit_ids['ancestor-new'])
804 assert pull_request.target_ref == expected_target_ref
867 assert pull_request.target_ref == expected_target_ref
805
868
806 def test_remove_pull_request_branch(self, backend_git, csrf_token):
869 def test_remove_pull_request_branch(self, backend_git, csrf_token):
807 branch_name = 'development'
870 branch_name = 'development'
808 commits = [
871 commits = [
809 {'message': 'initial-commit'},
872 {'message': 'initial-commit'},
810 {'message': 'old-feature'},
873 {'message': 'old-feature'},
811 {'message': 'new-feature', 'branch': branch_name},
874 {'message': 'new-feature', 'branch': branch_name},
812 ]
875 ]
813 repo = backend_git.create_repo(commits)
876 repo = backend_git.create_repo(commits)
814 commit_ids = backend_git.commit_ids
877 commit_ids = backend_git.commit_ids
815
878
816 pull_request = PullRequest()
879 pull_request = PullRequest()
817 pull_request.source_repo = repo
880 pull_request.source_repo = repo
818 pull_request.target_repo = repo
881 pull_request.target_repo = repo
819 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
882 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
820 branch=branch_name, commit_id=commit_ids['new-feature'])
883 branch=branch_name, commit_id=commit_ids['new-feature'])
821 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
884 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
822 branch=backend_git.default_branch_name,
885 branch=backend_git.default_branch_name,
823 commit_id=commit_ids['old-feature'])
886 commit_id=commit_ids['old-feature'])
824 pull_request.revisions = [commit_ids['new-feature']]
887 pull_request.revisions = [commit_ids['new-feature']]
825 pull_request.title = u"Test"
888 pull_request.title = u"Test"
826 pull_request.description = u"Description"
889 pull_request.description = u"Description"
827 pull_request.author = UserModel().get_by_username(
890 pull_request.author = UserModel().get_by_username(
828 TEST_USER_ADMIN_LOGIN)
891 TEST_USER_ADMIN_LOGIN)
829 Session().add(pull_request)
892 Session().add(pull_request)
830 Session().commit()
893 Session().commit()
831
894
832 vcs = repo.scm_instance()
895 vcs = repo.scm_instance()
833 vcs.remove_ref('refs/heads/{}'.format(branch_name))
896 vcs.remove_ref('refs/heads/{}'.format(branch_name))
834
897
835 response = self.app.get(route_path(
898 response = self.app.get(route_path(
836 'pullrequest_show',
899 'pullrequest_show',
837 repo_name=repo.repo_name,
900 repo_name=repo.repo_name,
838 pull_request_id=pull_request.pull_request_id))
901 pull_request_id=pull_request.pull_request_id))
839
902
840 assert response.status_int == 200
903 assert response.status_int == 200
841 assert_response = AssertResponse(response)
904 assert_response = AssertResponse(response)
842 assert_response.element_contains(
905 assert_response.element_contains(
843 '#changeset_compare_view_content .alert strong',
906 '#changeset_compare_view_content .alert strong',
844 'Missing commits')
907 'Missing commits')
845 assert_response.element_contains(
908 assert_response.element_contains(
846 '#changeset_compare_view_content .alert',
909 '#changeset_compare_view_content .alert',
847 'This pull request cannot be displayed, because one or more'
910 'This pull request cannot be displayed, because one or more'
848 ' commits no longer exist in the source repository.')
911 ' commits no longer exist in the source repository.')
849
912
850 def test_strip_commits_from_pull_request(
913 def test_strip_commits_from_pull_request(
851 self, backend, pr_util, csrf_token):
914 self, backend, pr_util, csrf_token):
852 commits = [
915 commits = [
853 {'message': 'initial-commit'},
916 {'message': 'initial-commit'},
854 {'message': 'old-feature'},
917 {'message': 'old-feature'},
855 {'message': 'new-feature', 'parents': ['initial-commit']},
918 {'message': 'new-feature', 'parents': ['initial-commit']},
856 ]
919 ]
857 pull_request = pr_util.create_pull_request(
920 pull_request = pr_util.create_pull_request(
858 commits, target_head='initial-commit', source_head='new-feature',
921 commits, target_head='initial-commit', source_head='new-feature',
859 revisions=['new-feature'])
922 revisions=['new-feature'])
860
923
861 vcs = pr_util.source_repository.scm_instance()
924 vcs = pr_util.source_repository.scm_instance()
862 if backend.alias == 'git':
925 if backend.alias == 'git':
863 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
926 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
864 else:
927 else:
865 vcs.strip(pr_util.commit_ids['new-feature'])
928 vcs.strip(pr_util.commit_ids['new-feature'])
866
929
867 response = self.app.get(route_path(
930 response = self.app.get(route_path(
868 'pullrequest_show',
931 'pullrequest_show',
869 repo_name=pr_util.target_repository.repo_name,
932 repo_name=pr_util.target_repository.repo_name,
870 pull_request_id=pull_request.pull_request_id))
933 pull_request_id=pull_request.pull_request_id))
871
934
872 assert response.status_int == 200
935 assert response.status_int == 200
873 assert_response = AssertResponse(response)
936 assert_response = AssertResponse(response)
874 assert_response.element_contains(
937 assert_response.element_contains(
875 '#changeset_compare_view_content .alert strong',
938 '#changeset_compare_view_content .alert strong',
876 'Missing commits')
939 'Missing commits')
877 assert_response.element_contains(
940 assert_response.element_contains(
878 '#changeset_compare_view_content .alert',
941 '#changeset_compare_view_content .alert',
879 'This pull request cannot be displayed, because one or more'
942 'This pull request cannot be displayed, because one or more'
880 ' commits no longer exist in the source repository.')
943 ' commits no longer exist in the source repository.')
881 assert_response.element_contains(
944 assert_response.element_contains(
882 '#update_commits',
945 '#update_commits',
883 'Update commits')
946 'Update commits')
884
947
885 def test_strip_commits_and_update(
948 def test_strip_commits_and_update(
886 self, backend, pr_util, csrf_token):
949 self, backend, pr_util, csrf_token):
887 commits = [
950 commits = [
888 {'message': 'initial-commit'},
951 {'message': 'initial-commit'},
889 {'message': 'old-feature'},
952 {'message': 'old-feature'},
890 {'message': 'new-feature', 'parents': ['old-feature']},
953 {'message': 'new-feature', 'parents': ['old-feature']},
891 ]
954 ]
892 pull_request = pr_util.create_pull_request(
955 pull_request = pr_util.create_pull_request(
893 commits, target_head='old-feature', source_head='new-feature',
956 commits, target_head='old-feature', source_head='new-feature',
894 revisions=['new-feature'], mergeable=True)
957 revisions=['new-feature'], mergeable=True)
895
958
896 vcs = pr_util.source_repository.scm_instance()
959 vcs = pr_util.source_repository.scm_instance()
897 if backend.alias == 'git':
960 if backend.alias == 'git':
898 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
961 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
899 else:
962 else:
900 vcs.strip(pr_util.commit_ids['new-feature'])
963 vcs.strip(pr_util.commit_ids['new-feature'])
901
964
902 response = self.app.post(
965 response = self.app.post(
903 route_path('pullrequest_update',
966 route_path('pullrequest_update',
904 repo_name=pull_request.target_repo.repo_name,
967 repo_name=pull_request.target_repo.repo_name,
905 pull_request_id=pull_request.pull_request_id),
968 pull_request_id=pull_request.pull_request_id),
906 params={'update_commits': 'true',
969 params={'update_commits': 'true',
907 'csrf_token': csrf_token})
970 'csrf_token': csrf_token})
908
971
909 assert response.status_int == 200
972 assert response.status_int == 200
910 assert response.body == 'true'
973 assert response.body == 'true'
911
974
912 # Make sure that after update, it won't raise 500 errors
975 # Make sure that after update, it won't raise 500 errors
913 response = self.app.get(route_path(
976 response = self.app.get(route_path(
914 'pullrequest_show',
977 'pullrequest_show',
915 repo_name=pr_util.target_repository.repo_name,
978 repo_name=pr_util.target_repository.repo_name,
916 pull_request_id=pull_request.pull_request_id))
979 pull_request_id=pull_request.pull_request_id))
917
980
918 assert response.status_int == 200
981 assert response.status_int == 200
919 assert_response = AssertResponse(response)
982 assert_response = AssertResponse(response)
920 assert_response.element_contains(
983 assert_response.element_contains(
921 '#changeset_compare_view_content .alert strong',
984 '#changeset_compare_view_content .alert strong',
922 'Missing commits')
985 'Missing commits')
923
986
924 def test_branch_is_a_link(self, pr_util):
987 def test_branch_is_a_link(self, pr_util):
925 pull_request = pr_util.create_pull_request()
988 pull_request = pr_util.create_pull_request()
926 pull_request.source_ref = 'branch:origin:1234567890abcdef'
989 pull_request.source_ref = 'branch:origin:1234567890abcdef'
927 pull_request.target_ref = 'branch:target:abcdef1234567890'
990 pull_request.target_ref = 'branch:target:abcdef1234567890'
928 Session().add(pull_request)
991 Session().add(pull_request)
929 Session().commit()
992 Session().commit()
930
993
931 response = self.app.get(route_path(
994 response = self.app.get(route_path(
932 'pullrequest_show',
995 'pullrequest_show',
933 repo_name=pull_request.target_repo.scm_instance().name,
996 repo_name=pull_request.target_repo.scm_instance().name,
934 pull_request_id=pull_request.pull_request_id))
997 pull_request_id=pull_request.pull_request_id))
935 assert response.status_int == 200
998 assert response.status_int == 200
936 assert_response = AssertResponse(response)
999 assert_response = AssertResponse(response)
937
1000
938 origin = assert_response.get_element('.pr-origininfo .tag')
1001 origin = assert_response.get_element('.pr-origininfo .tag')
939 origin_children = origin.getchildren()
1002 origin_children = origin.getchildren()
940 assert len(origin_children) == 1
1003 assert len(origin_children) == 1
941 target = assert_response.get_element('.pr-targetinfo .tag')
1004 target = assert_response.get_element('.pr-targetinfo .tag')
942 target_children = target.getchildren()
1005 target_children = target.getchildren()
943 assert len(target_children) == 1
1006 assert len(target_children) == 1
944
1007
945 expected_origin_link = route_path(
1008 expected_origin_link = route_path(
946 'repo_changelog',
1009 'repo_changelog',
947 repo_name=pull_request.source_repo.scm_instance().name,
1010 repo_name=pull_request.source_repo.scm_instance().name,
948 params=dict(branch='origin'))
1011 params=dict(branch='origin'))
949 expected_target_link = route_path(
1012 expected_target_link = route_path(
950 'repo_changelog',
1013 'repo_changelog',
951 repo_name=pull_request.target_repo.scm_instance().name,
1014 repo_name=pull_request.target_repo.scm_instance().name,
952 params=dict(branch='target'))
1015 params=dict(branch='target'))
953 assert origin_children[0].attrib['href'] == expected_origin_link
1016 assert origin_children[0].attrib['href'] == expected_origin_link
954 assert origin_children[0].text == 'branch: origin'
1017 assert origin_children[0].text == 'branch: origin'
955 assert target_children[0].attrib['href'] == expected_target_link
1018 assert target_children[0].attrib['href'] == expected_target_link
956 assert target_children[0].text == 'branch: target'
1019 assert target_children[0].text == 'branch: target'
957
1020
958 def test_bookmark_is_not_a_link(self, pr_util):
1021 def test_bookmark_is_not_a_link(self, pr_util):
959 pull_request = pr_util.create_pull_request()
1022 pull_request = pr_util.create_pull_request()
960 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1023 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
961 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1024 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
962 Session().add(pull_request)
1025 Session().add(pull_request)
963 Session().commit()
1026 Session().commit()
964
1027
965 response = self.app.get(route_path(
1028 response = self.app.get(route_path(
966 'pullrequest_show',
1029 'pullrequest_show',
967 repo_name=pull_request.target_repo.scm_instance().name,
1030 repo_name=pull_request.target_repo.scm_instance().name,
968 pull_request_id=pull_request.pull_request_id))
1031 pull_request_id=pull_request.pull_request_id))
969 assert response.status_int == 200
1032 assert response.status_int == 200
970 assert_response = AssertResponse(response)
1033 assert_response = AssertResponse(response)
971
1034
972 origin = assert_response.get_element('.pr-origininfo .tag')
1035 origin = assert_response.get_element('.pr-origininfo .tag')
973 assert origin.text.strip() == 'bookmark: origin'
1036 assert origin.text.strip() == 'bookmark: origin'
974 assert origin.getchildren() == []
1037 assert origin.getchildren() == []
975
1038
976 target = assert_response.get_element('.pr-targetinfo .tag')
1039 target = assert_response.get_element('.pr-targetinfo .tag')
977 assert target.text.strip() == 'bookmark: target'
1040 assert target.text.strip() == 'bookmark: target'
978 assert target.getchildren() == []
1041 assert target.getchildren() == []
979
1042
980 def test_tag_is_not_a_link(self, pr_util):
1043 def test_tag_is_not_a_link(self, pr_util):
981 pull_request = pr_util.create_pull_request()
1044 pull_request = pr_util.create_pull_request()
982 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1045 pull_request.source_ref = 'tag:origin:1234567890abcdef'
983 pull_request.target_ref = 'tag:target:abcdef1234567890'
1046 pull_request.target_ref = 'tag:target:abcdef1234567890'
984 Session().add(pull_request)
1047 Session().add(pull_request)
985 Session().commit()
1048 Session().commit()
986
1049
987 response = self.app.get(route_path(
1050 response = self.app.get(route_path(
988 'pullrequest_show',
1051 'pullrequest_show',
989 repo_name=pull_request.target_repo.scm_instance().name,
1052 repo_name=pull_request.target_repo.scm_instance().name,
990 pull_request_id=pull_request.pull_request_id))
1053 pull_request_id=pull_request.pull_request_id))
991 assert response.status_int == 200
1054 assert response.status_int == 200
992 assert_response = AssertResponse(response)
1055 assert_response = AssertResponse(response)
993
1056
994 origin = assert_response.get_element('.pr-origininfo .tag')
1057 origin = assert_response.get_element('.pr-origininfo .tag')
995 assert origin.text.strip() == 'tag: origin'
1058 assert origin.text.strip() == 'tag: origin'
996 assert origin.getchildren() == []
1059 assert origin.getchildren() == []
997
1060
998 target = assert_response.get_element('.pr-targetinfo .tag')
1061 target = assert_response.get_element('.pr-targetinfo .tag')
999 assert target.text.strip() == 'tag: target'
1062 assert target.text.strip() == 'tag: target'
1000 assert target.getchildren() == []
1063 assert target.getchildren() == []
1001
1064
1002 @pytest.mark.parametrize('mergeable', [True, False])
1065 @pytest.mark.parametrize('mergeable', [True, False])
1003 def test_shadow_repository_link(
1066 def test_shadow_repository_link(
1004 self, mergeable, pr_util, http_host_only_stub):
1067 self, mergeable, pr_util, http_host_only_stub):
1005 """
1068 """
1006 Check that the pull request summary page displays a link to the shadow
1069 Check that the pull request summary page displays a link to the shadow
1007 repository if the pull request is mergeable. If it is not mergeable
1070 repository if the pull request is mergeable. If it is not mergeable
1008 the link should not be displayed.
1071 the link should not be displayed.
1009 """
1072 """
1010 pull_request = pr_util.create_pull_request(
1073 pull_request = pr_util.create_pull_request(
1011 mergeable=mergeable, enable_notifications=False)
1074 mergeable=mergeable, enable_notifications=False)
1012 target_repo = pull_request.target_repo.scm_instance()
1075 target_repo = pull_request.target_repo.scm_instance()
1013 pr_id = pull_request.pull_request_id
1076 pr_id = pull_request.pull_request_id
1014 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1077 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1015 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1078 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1016
1079
1017 response = self.app.get(route_path(
1080 response = self.app.get(route_path(
1018 'pullrequest_show',
1081 'pullrequest_show',
1019 repo_name=target_repo.name,
1082 repo_name=target_repo.name,
1020 pull_request_id=pr_id))
1083 pull_request_id=pr_id))
1021
1084
1022 assertr = AssertResponse(response)
1085 assertr = AssertResponse(response)
1023 if mergeable:
1086 if mergeable:
1024 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1087 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1025 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1088 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1026 else:
1089 else:
1027 assertr.no_element_exists('.pr-mergeinfo')
1090 assertr.no_element_exists('.pr-mergeinfo')
1028
1091
1029
1092
1030 @pytest.mark.usefixtures('app')
1093 @pytest.mark.usefixtures('app')
1031 @pytest.mark.backends("git", "hg")
1094 @pytest.mark.backends("git", "hg")
1032 class TestPullrequestsControllerDelete(object):
1095 class TestPullrequestsControllerDelete(object):
1033 def test_pull_request_delete_button_permissions_admin(
1096 def test_pull_request_delete_button_permissions_admin(
1034 self, autologin_user, user_admin, pr_util):
1097 self, autologin_user, user_admin, pr_util):
1035 pull_request = pr_util.create_pull_request(
1098 pull_request = pr_util.create_pull_request(
1036 author=user_admin.username, enable_notifications=False)
1099 author=user_admin.username, enable_notifications=False)
1037
1100
1038 response = self.app.get(route_path(
1101 response = self.app.get(route_path(
1039 'pullrequest_show',
1102 'pullrequest_show',
1040 repo_name=pull_request.target_repo.scm_instance().name,
1103 repo_name=pull_request.target_repo.scm_instance().name,
1041 pull_request_id=pull_request.pull_request_id))
1104 pull_request_id=pull_request.pull_request_id))
1042
1105
1043 response.mustcontain('id="delete_pullrequest"')
1106 response.mustcontain('id="delete_pullrequest"')
1044 response.mustcontain('Confirm to delete this pull request')
1107 response.mustcontain('Confirm to delete this pull request')
1045
1108
1046 def test_pull_request_delete_button_permissions_owner(
1109 def test_pull_request_delete_button_permissions_owner(
1047 self, autologin_regular_user, user_regular, pr_util):
1110 self, autologin_regular_user, user_regular, pr_util):
1048 pull_request = pr_util.create_pull_request(
1111 pull_request = pr_util.create_pull_request(
1049 author=user_regular.username, enable_notifications=False)
1112 author=user_regular.username, enable_notifications=False)
1050
1113
1051 response = self.app.get(route_path(
1114 response = self.app.get(route_path(
1052 'pullrequest_show',
1115 'pullrequest_show',
1053 repo_name=pull_request.target_repo.scm_instance().name,
1116 repo_name=pull_request.target_repo.scm_instance().name,
1054 pull_request_id=pull_request.pull_request_id))
1117 pull_request_id=pull_request.pull_request_id))
1055
1118
1056 response.mustcontain('id="delete_pullrequest"')
1119 response.mustcontain('id="delete_pullrequest"')
1057 response.mustcontain('Confirm to delete this pull request')
1120 response.mustcontain('Confirm to delete this pull request')
1058
1121
1059 def test_pull_request_delete_button_permissions_forbidden(
1122 def test_pull_request_delete_button_permissions_forbidden(
1060 self, autologin_regular_user, user_regular, user_admin, pr_util):
1123 self, autologin_regular_user, user_regular, user_admin, pr_util):
1061 pull_request = pr_util.create_pull_request(
1124 pull_request = pr_util.create_pull_request(
1062 author=user_admin.username, enable_notifications=False)
1125 author=user_admin.username, enable_notifications=False)
1063
1126
1064 response = self.app.get(route_path(
1127 response = self.app.get(route_path(
1065 'pullrequest_show',
1128 'pullrequest_show',
1066 repo_name=pull_request.target_repo.scm_instance().name,
1129 repo_name=pull_request.target_repo.scm_instance().name,
1067 pull_request_id=pull_request.pull_request_id))
1130 pull_request_id=pull_request.pull_request_id))
1068 response.mustcontain(no=['id="delete_pullrequest"'])
1131 response.mustcontain(no=['id="delete_pullrequest"'])
1069 response.mustcontain(no=['Confirm to delete this pull request'])
1132 response.mustcontain(no=['Confirm to delete this pull request'])
1070
1133
1071 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1134 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1072 self, autologin_regular_user, user_regular, user_admin, pr_util,
1135 self, autologin_regular_user, user_regular, user_admin, pr_util,
1073 user_util):
1136 user_util):
1074
1137
1075 pull_request = pr_util.create_pull_request(
1138 pull_request = pr_util.create_pull_request(
1076 author=user_admin.username, enable_notifications=False)
1139 author=user_admin.username, enable_notifications=False)
1077
1140
1078 user_util.grant_user_permission_to_repo(
1141 user_util.grant_user_permission_to_repo(
1079 pull_request.target_repo, user_regular,
1142 pull_request.target_repo, user_regular,
1080 'repository.write')
1143 'repository.write')
1081
1144
1082 response = self.app.get(route_path(
1145 response = self.app.get(route_path(
1083 'pullrequest_show',
1146 'pullrequest_show',
1084 repo_name=pull_request.target_repo.scm_instance().name,
1147 repo_name=pull_request.target_repo.scm_instance().name,
1085 pull_request_id=pull_request.pull_request_id))
1148 pull_request_id=pull_request.pull_request_id))
1086
1149
1087 response.mustcontain('id="open_edit_pullrequest"')
1150 response.mustcontain('id="open_edit_pullrequest"')
1088 response.mustcontain('id="delete_pullrequest"')
1151 response.mustcontain('id="delete_pullrequest"')
1089 response.mustcontain(no=['Confirm to delete this pull request'])
1152 response.mustcontain(no=['Confirm to delete this pull request'])
1090
1153
1091 def test_delete_comment_returns_404_if_comment_does_not_exist(
1154 def test_delete_comment_returns_404_if_comment_does_not_exist(
1092 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1155 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1093
1156
1094 pull_request = pr_util.create_pull_request(
1157 pull_request = pr_util.create_pull_request(
1095 author=user_admin.username, enable_notifications=False)
1158 author=user_admin.username, enable_notifications=False)
1096
1159
1097 self.app.post(
1160 self.app.post(
1098 route_path(
1161 route_path(
1099 'pullrequest_comment_delete',
1162 'pullrequest_comment_delete',
1100 repo_name=pull_request.target_repo.scm_instance().name,
1163 repo_name=pull_request.target_repo.scm_instance().name,
1101 pull_request_id=pull_request.pull_request_id,
1164 pull_request_id=pull_request.pull_request_id,
1102 comment_id=1024404),
1165 comment_id=1024404),
1103 extra_environ=xhr_header,
1166 extra_environ=xhr_header,
1104 params={'csrf_token': csrf_token},
1167 params={'csrf_token': csrf_token},
1105 status=404
1168 status=404
1106 )
1169 )
1107
1170
1108 def test_delete_comment(
1171 def test_delete_comment(
1109 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1172 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1110
1173
1111 pull_request = pr_util.create_pull_request(
1174 pull_request = pr_util.create_pull_request(
1112 author=user_admin.username, enable_notifications=False)
1175 author=user_admin.username, enable_notifications=False)
1113 comment = pr_util.create_comment()
1176 comment = pr_util.create_comment()
1114 comment_id = comment.comment_id
1177 comment_id = comment.comment_id
1115
1178
1116 response = self.app.post(
1179 response = self.app.post(
1117 route_path(
1180 route_path(
1118 'pullrequest_comment_delete',
1181 'pullrequest_comment_delete',
1119 repo_name=pull_request.target_repo.scm_instance().name,
1182 repo_name=pull_request.target_repo.scm_instance().name,
1120 pull_request_id=pull_request.pull_request_id,
1183 pull_request_id=pull_request.pull_request_id,
1121 comment_id=comment_id),
1184 comment_id=comment_id),
1122 extra_environ=xhr_header,
1185 extra_environ=xhr_header,
1123 params={'csrf_token': csrf_token},
1186 params={'csrf_token': csrf_token},
1124 status=200
1187 status=200
1125 )
1188 )
1126 assert response.body == 'true'
1189 assert response.body == 'true'
1127
1190
1128
1191
1129 def assert_pull_request_status(pull_request, expected_status):
1192 def assert_pull_request_status(pull_request, expected_status):
1130 status = ChangesetStatusModel().calculated_review_status(
1193 status = ChangesetStatusModel().calculated_review_status(
1131 pull_request=pull_request)
1194 pull_request=pull_request)
1132 assert status == expected_status
1195 assert status == expected_status
1133
1196
1134
1197
1135 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1198 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1136 @pytest.mark.usefixtures("autologin_user")
1199 @pytest.mark.usefixtures("autologin_user")
1137 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1200 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1138 response = app.get(
1201 response = app.get(
1139 route_path(route, repo_name=backend_svn.repo_name), status=404)
1202 route_path(route, repo_name=backend_svn.repo_name), status=404)
1140
1203
@@ -1,985 +1,1006 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 GIT repository module
22 GIT repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import os
26 import os
27 import re
27 import re
28
28
29 from zope.cachedescriptors.property import Lazy as LazyProperty
29 from zope.cachedescriptors.property import Lazy as LazyProperty
30
30
31 from rhodecode.lib.compat import OrderedDict
31 from rhodecode.lib.compat import OrderedDict
32 from rhodecode.lib.datelib import (
32 from rhodecode.lib.datelib import (
33 utcdate_fromtimestamp, makedate, date_astimestamp)
33 utcdate_fromtimestamp, makedate, date_astimestamp)
34 from rhodecode.lib.utils import safe_unicode, safe_str
34 from rhodecode.lib.utils import safe_unicode, safe_str
35 from rhodecode.lib.vcs import connection, path as vcspath
35 from rhodecode.lib.vcs import connection, path as vcspath
36 from rhodecode.lib.vcs.backends.base import (
36 from rhodecode.lib.vcs.backends.base import (
37 BaseRepository, CollectionGenerator, Config, MergeResponse,
37 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 MergeFailureReason, Reference)
38 MergeFailureReason, Reference)
39 from rhodecode.lib.vcs.backends.git.commit import GitCommit
39 from rhodecode.lib.vcs.backends.git.commit import GitCommit
40 from rhodecode.lib.vcs.backends.git.diff import GitDiff
40 from rhodecode.lib.vcs.backends.git.diff import GitDiff
41 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
41 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
42 from rhodecode.lib.vcs.exceptions import (
42 from rhodecode.lib.vcs.exceptions import (
43 CommitDoesNotExistError, EmptyRepositoryError,
43 CommitDoesNotExistError, EmptyRepositoryError,
44 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
44 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
45
45
46
46
47 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
47 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 class GitRepository(BaseRepository):
52 class GitRepository(BaseRepository):
53 """
53 """
54 Git repository backend.
54 Git repository backend.
55 """
55 """
56 DEFAULT_BRANCH_NAME = 'master'
56 DEFAULT_BRANCH_NAME = 'master'
57
57
58 contact = BaseRepository.DEFAULT_CONTACT
58 contact = BaseRepository.DEFAULT_CONTACT
59
59
60 def __init__(self, repo_path, config=None, create=False, src_url=None,
60 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 update_after_clone=False, with_wire=None, bare=False):
61 update_after_clone=False, with_wire=None, bare=False):
62
62
63 self.path = safe_str(os.path.abspath(repo_path))
63 self.path = safe_str(os.path.abspath(repo_path))
64 self.config = config if config else self.get_default_config()
64 self.config = config if config else self.get_default_config()
65 self._remote = connection.Git(
65 self._remote = connection.Git(
66 self.path, self.config, with_wire=with_wire)
66 self.path, self.config, with_wire=with_wire)
67
67
68 self._init_repo(create, src_url, update_after_clone, bare)
68 self._init_repo(create, src_url, update_after_clone, bare)
69
69
70 # caches
70 # caches
71 self._commit_ids = {}
71 self._commit_ids = {}
72
72
73 @LazyProperty
73 @LazyProperty
74 def bare(self):
74 def bare(self):
75 return self._remote.bare()
75 return self._remote.bare()
76
76
77 @LazyProperty
77 @LazyProperty
78 def head(self):
78 def head(self):
79 return self._remote.head()
79 return self._remote.head()
80
80
81 @LazyProperty
81 @LazyProperty
82 def commit_ids(self):
82 def commit_ids(self):
83 """
83 """
84 Returns list of commit ids, in ascending order. Being lazy
84 Returns list of commit ids, in ascending order. Being lazy
85 attribute allows external tools to inject commit ids from cache.
85 attribute allows external tools to inject commit ids from cache.
86 """
86 """
87 commit_ids = self._get_all_commit_ids()
87 commit_ids = self._get_all_commit_ids()
88 self._rebuild_cache(commit_ids)
88 self._rebuild_cache(commit_ids)
89 return commit_ids
89 return commit_ids
90
90
91 def _rebuild_cache(self, commit_ids):
91 def _rebuild_cache(self, commit_ids):
92 self._commit_ids = dict((commit_id, index)
92 self._commit_ids = dict((commit_id, index)
93 for index, commit_id in enumerate(commit_ids))
93 for index, commit_id in enumerate(commit_ids))
94
94
95 def run_git_command(self, cmd, **opts):
95 def run_git_command(self, cmd, **opts):
96 """
96 """
97 Runs given ``cmd`` as git command and returns tuple
97 Runs given ``cmd`` as git command and returns tuple
98 (stdout, stderr).
98 (stdout, stderr).
99
99
100 :param cmd: git command to be executed
100 :param cmd: git command to be executed
101 :param opts: env options to pass into Subprocess command
101 :param opts: env options to pass into Subprocess command
102 """
102 """
103 if not isinstance(cmd, list):
103 if not isinstance(cmd, list):
104 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
104 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
105
105
106 skip_stderr_log = opts.pop('skip_stderr_log', False)
106 skip_stderr_log = opts.pop('skip_stderr_log', False)
107 out, err = self._remote.run_git_command(cmd, **opts)
107 out, err = self._remote.run_git_command(cmd, **opts)
108 if err and not skip_stderr_log:
108 if err and not skip_stderr_log:
109 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
109 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
110 return out, err
110 return out, err
111
111
112 @staticmethod
112 @staticmethod
113 def check_url(url, config):
113 def check_url(url, config):
114 """
114 """
115 Function will check given url and try to verify if it's a valid
115 Function will check given url and try to verify if it's a valid
116 link. Sometimes it may happened that git will issue basic
116 link. Sometimes it may happened that git will issue basic
117 auth request that can cause whole API to hang when used from python
117 auth request that can cause whole API to hang when used from python
118 or other external calls.
118 or other external calls.
119
119
120 On failures it'll raise urllib2.HTTPError, exception is also thrown
120 On failures it'll raise urllib2.HTTPError, exception is also thrown
121 when the return code is non 200
121 when the return code is non 200
122 """
122 """
123 # check first if it's not an url
123 # check first if it's not an url
124 if os.path.isdir(url) or url.startswith('file:'):
124 if os.path.isdir(url) or url.startswith('file:'):
125 return True
125 return True
126
126
127 if '+' in url.split('://', 1)[0]:
127 if '+' in url.split('://', 1)[0]:
128 url = url.split('+', 1)[1]
128 url = url.split('+', 1)[1]
129
129
130 # Request the _remote to verify the url
130 # Request the _remote to verify the url
131 return connection.Git.check_url(url, config.serialize())
131 return connection.Git.check_url(url, config.serialize())
132
132
133 @staticmethod
133 @staticmethod
134 def is_valid_repository(path):
134 def is_valid_repository(path):
135 if os.path.isdir(os.path.join(path, '.git')):
135 if os.path.isdir(os.path.join(path, '.git')):
136 return True
136 return True
137 # check case of bare repository
137 # check case of bare repository
138 try:
138 try:
139 GitRepository(path)
139 GitRepository(path)
140 return True
140 return True
141 except VCSError:
141 except VCSError:
142 pass
142 pass
143 return False
143 return False
144
144
145 def _init_repo(self, create, src_url=None, update_after_clone=False,
145 def _init_repo(self, create, src_url=None, update_after_clone=False,
146 bare=False):
146 bare=False):
147 if create and os.path.exists(self.path):
147 if create and os.path.exists(self.path):
148 raise RepositoryError(
148 raise RepositoryError(
149 "Cannot create repository at %s, location already exist"
149 "Cannot create repository at %s, location already exist"
150 % self.path)
150 % self.path)
151
151
152 try:
152 try:
153 if create and src_url:
153 if create and src_url:
154 GitRepository.check_url(src_url, self.config)
154 GitRepository.check_url(src_url, self.config)
155 self.clone(src_url, update_after_clone, bare)
155 self.clone(src_url, update_after_clone, bare)
156 elif create:
156 elif create:
157 os.makedirs(self.path, mode=0755)
157 os.makedirs(self.path, mode=0755)
158
158
159 if bare:
159 if bare:
160 self._remote.init_bare()
160 self._remote.init_bare()
161 else:
161 else:
162 self._remote.init()
162 self._remote.init()
163 else:
163 else:
164 if not self._remote.assert_correct_path():
164 if not self._remote.assert_correct_path():
165 raise RepositoryError(
165 raise RepositoryError(
166 'Path "%s" does not contain a Git repository' %
166 'Path "%s" does not contain a Git repository' %
167 (self.path,))
167 (self.path,))
168
168
169 # TODO: johbo: check if we have to translate the OSError here
169 # TODO: johbo: check if we have to translate the OSError here
170 except OSError as err:
170 except OSError as err:
171 raise RepositoryError(err)
171 raise RepositoryError(err)
172
172
173 def _get_all_commit_ids(self, filters=None):
173 def _get_all_commit_ids(self, filters=None):
174 # we must check if this repo is not empty, since later command
174 # we must check if this repo is not empty, since later command
175 # fails if it is. And it's cheaper to ask than throw the subprocess
175 # fails if it is. And it's cheaper to ask than throw the subprocess
176 # errors
176 # errors
177 try:
177 try:
178 self._remote.head()
178 self._remote.head()
179 except KeyError:
179 except KeyError:
180 return []
180 return []
181
181
182 rev_filter = ['--branches', '--tags']
182 rev_filter = ['--branches', '--tags']
183 extra_filter = []
183 extra_filter = []
184
184
185 if filters:
185 if filters:
186 if filters.get('since'):
186 if filters.get('since'):
187 extra_filter.append('--since=%s' % (filters['since']))
187 extra_filter.append('--since=%s' % (filters['since']))
188 if filters.get('until'):
188 if filters.get('until'):
189 extra_filter.append('--until=%s' % (filters['until']))
189 extra_filter.append('--until=%s' % (filters['until']))
190 if filters.get('branch_name'):
190 if filters.get('branch_name'):
191 rev_filter = ['--tags']
191 rev_filter = ['--tags']
192 extra_filter.append(filters['branch_name'])
192 extra_filter.append(filters['branch_name'])
193 rev_filter.extend(extra_filter)
193 rev_filter.extend(extra_filter)
194
194
195 # if filters.get('start') or filters.get('end'):
195 # if filters.get('start') or filters.get('end'):
196 # # skip is offset, max-count is limit
196 # # skip is offset, max-count is limit
197 # if filters.get('start'):
197 # if filters.get('start'):
198 # extra_filter += ' --skip=%s' % filters['start']
198 # extra_filter += ' --skip=%s' % filters['start']
199 # if filters.get('end'):
199 # if filters.get('end'):
200 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
200 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
201
201
202 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
202 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
203 try:
203 try:
204 output, __ = self.run_git_command(cmd)
204 output, __ = self.run_git_command(cmd)
205 except RepositoryError:
205 except RepositoryError:
206 # Can be raised for empty repositories
206 # Can be raised for empty repositories
207 return []
207 return []
208 return output.splitlines()
208 return output.splitlines()
209
209
210 def _get_commit_id(self, commit_id_or_idx):
210 def _get_commit_id(self, commit_id_or_idx):
211 def is_null(value):
211 def is_null(value):
212 return len(value) == commit_id_or_idx.count('0')
212 return len(value) == commit_id_or_idx.count('0')
213
213
214 if self.is_empty():
214 if self.is_empty():
215 raise EmptyRepositoryError("There are no commits yet")
215 raise EmptyRepositoryError("There are no commits yet")
216
216
217 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
217 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
218 return self.commit_ids[-1]
218 return self.commit_ids[-1]
219
219
220 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
220 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
221 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
221 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
222 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
222 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
223 try:
223 try:
224 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
224 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
225 except Exception:
225 except Exception:
226 msg = "Commit %s does not exist for %s" % (
226 msg = "Commit %s does not exist for %s" % (
227 commit_id_or_idx, self)
227 commit_id_or_idx, self)
228 raise CommitDoesNotExistError(msg)
228 raise CommitDoesNotExistError(msg)
229
229
230 elif is_bstr:
230 elif is_bstr:
231 # check full path ref, eg. refs/heads/master
231 # check full path ref, eg. refs/heads/master
232 ref_id = self._refs.get(commit_id_or_idx)
232 ref_id = self._refs.get(commit_id_or_idx)
233 if ref_id:
233 if ref_id:
234 return ref_id
234 return ref_id
235
235
236 # check branch name
236 # check branch name
237 branch_ids = self.branches.values()
237 branch_ids = self.branches.values()
238 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
238 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
239 if ref_id:
239 if ref_id:
240 return ref_id
240 return ref_id
241
241
242 # check tag name
242 # check tag name
243 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
243 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
244 if ref_id:
244 if ref_id:
245 return ref_id
245 return ref_id
246
246
247 if (not SHA_PATTERN.match(commit_id_or_idx) or
247 if (not SHA_PATTERN.match(commit_id_or_idx) or
248 commit_id_or_idx not in self.commit_ids):
248 commit_id_or_idx not in self.commit_ids):
249 msg = "Commit %s does not exist for %s" % (
249 msg = "Commit %s does not exist for %s" % (
250 commit_id_or_idx, self)
250 commit_id_or_idx, self)
251 raise CommitDoesNotExistError(msg)
251 raise CommitDoesNotExistError(msg)
252
252
253 # Ensure we return full id
253 # Ensure we return full id
254 if not SHA_PATTERN.match(str(commit_id_or_idx)):
254 if not SHA_PATTERN.match(str(commit_id_or_idx)):
255 raise CommitDoesNotExistError(
255 raise CommitDoesNotExistError(
256 "Given commit id %s not recognized" % commit_id_or_idx)
256 "Given commit id %s not recognized" % commit_id_or_idx)
257 return commit_id_or_idx
257 return commit_id_or_idx
258
258
259 def get_hook_location(self):
259 def get_hook_location(self):
260 """
260 """
261 returns absolute path to location where hooks are stored
261 returns absolute path to location where hooks are stored
262 """
262 """
263 loc = os.path.join(self.path, 'hooks')
263 loc = os.path.join(self.path, 'hooks')
264 if not self.bare:
264 if not self.bare:
265 loc = os.path.join(self.path, '.git', 'hooks')
265 loc = os.path.join(self.path, '.git', 'hooks')
266 return loc
266 return loc
267
267
268 @LazyProperty
268 @LazyProperty
269 def last_change(self):
269 def last_change(self):
270 """
270 """
271 Returns last change made on this repository as
271 Returns last change made on this repository as
272 `datetime.datetime` object.
272 `datetime.datetime` object.
273 """
273 """
274 try:
274 try:
275 return self.get_commit().date
275 return self.get_commit().date
276 except RepositoryError:
276 except RepositoryError:
277 tzoffset = makedate()[1]
277 tzoffset = makedate()[1]
278 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
278 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
279
279
280 def _get_fs_mtime(self):
280 def _get_fs_mtime(self):
281 idx_loc = '' if self.bare else '.git'
281 idx_loc = '' if self.bare else '.git'
282 # fallback to filesystem
282 # fallback to filesystem
283 in_path = os.path.join(self.path, idx_loc, "index")
283 in_path = os.path.join(self.path, idx_loc, "index")
284 he_path = os.path.join(self.path, idx_loc, "HEAD")
284 he_path = os.path.join(self.path, idx_loc, "HEAD")
285 if os.path.exists(in_path):
285 if os.path.exists(in_path):
286 return os.stat(in_path).st_mtime
286 return os.stat(in_path).st_mtime
287 else:
287 else:
288 return os.stat(he_path).st_mtime
288 return os.stat(he_path).st_mtime
289
289
290 @LazyProperty
290 @LazyProperty
291 def description(self):
291 def description(self):
292 description = self._remote.get_description()
292 description = self._remote.get_description()
293 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
293 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
294
294
295 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
295 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
296 if self.is_empty():
296 if self.is_empty():
297 return OrderedDict()
297 return OrderedDict()
298
298
299 result = []
299 result = []
300 for ref, sha in self._refs.iteritems():
300 for ref, sha in self._refs.iteritems():
301 if ref.startswith(prefix):
301 if ref.startswith(prefix):
302 ref_name = ref
302 ref_name = ref
303 if strip_prefix:
303 if strip_prefix:
304 ref_name = ref[len(prefix):]
304 ref_name = ref[len(prefix):]
305 result.append((safe_unicode(ref_name), sha))
305 result.append((safe_unicode(ref_name), sha))
306
306
307 def get_name(entry):
307 def get_name(entry):
308 return entry[0]
308 return entry[0]
309
309
310 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
310 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
311
311
312 def _get_branches(self):
312 def _get_branches(self):
313 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
313 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
314
314
315 @LazyProperty
315 @LazyProperty
316 def branches(self):
316 def branches(self):
317 return self._get_branches()
317 return self._get_branches()
318
318
319 @LazyProperty
319 @LazyProperty
320 def branches_closed(self):
320 def branches_closed(self):
321 return {}
321 return {}
322
322
323 @LazyProperty
323 @LazyProperty
324 def bookmarks(self):
324 def bookmarks(self):
325 return {}
325 return {}
326
326
327 @LazyProperty
327 @LazyProperty
328 def branches_all(self):
328 def branches_all(self):
329 all_branches = {}
329 all_branches = {}
330 all_branches.update(self.branches)
330 all_branches.update(self.branches)
331 all_branches.update(self.branches_closed)
331 all_branches.update(self.branches_closed)
332 return all_branches
332 return all_branches
333
333
334 @LazyProperty
334 @LazyProperty
335 def tags(self):
335 def tags(self):
336 return self._get_tags()
336 return self._get_tags()
337
337
338 def _get_tags(self):
338 def _get_tags(self):
339 return self._get_refs_entries(
339 return self._get_refs_entries(
340 prefix='refs/tags/', strip_prefix=True, reverse=True)
340 prefix='refs/tags/', strip_prefix=True, reverse=True)
341
341
342 def tag(self, name, user, commit_id=None, message=None, date=None,
342 def tag(self, name, user, commit_id=None, message=None, date=None,
343 **kwargs):
343 **kwargs):
344 # TODO: fix this method to apply annotated tags correct with message
344 # TODO: fix this method to apply annotated tags correct with message
345 """
345 """
346 Creates and returns a tag for the given ``commit_id``.
346 Creates and returns a tag for the given ``commit_id``.
347
347
348 :param name: name for new tag
348 :param name: name for new tag
349 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
349 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
350 :param commit_id: commit id for which new tag would be created
350 :param commit_id: commit id for which new tag would be created
351 :param message: message of the tag's commit
351 :param message: message of the tag's commit
352 :param date: date of tag's commit
352 :param date: date of tag's commit
353
353
354 :raises TagAlreadyExistError: if tag with same name already exists
354 :raises TagAlreadyExistError: if tag with same name already exists
355 """
355 """
356 if name in self.tags:
356 if name in self.tags:
357 raise TagAlreadyExistError("Tag %s already exists" % name)
357 raise TagAlreadyExistError("Tag %s already exists" % name)
358 commit = self.get_commit(commit_id=commit_id)
358 commit = self.get_commit(commit_id=commit_id)
359 message = message or "Added tag %s for commit %s" % (
359 message = message or "Added tag %s for commit %s" % (
360 name, commit.raw_id)
360 name, commit.raw_id)
361 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
361 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
362
362
363 self._refs = self._get_refs()
363 self._refs = self._get_refs()
364 self.tags = self._get_tags()
364 self.tags = self._get_tags()
365 return commit
365 return commit
366
366
367 def remove_tag(self, name, user, message=None, date=None):
367 def remove_tag(self, name, user, message=None, date=None):
368 """
368 """
369 Removes tag with the given ``name``.
369 Removes tag with the given ``name``.
370
370
371 :param name: name of the tag to be removed
371 :param name: name of the tag to be removed
372 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
372 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
373 :param message: message of the tag's removal commit
373 :param message: message of the tag's removal commit
374 :param date: date of tag's removal commit
374 :param date: date of tag's removal commit
375
375
376 :raises TagDoesNotExistError: if tag with given name does not exists
376 :raises TagDoesNotExistError: if tag with given name does not exists
377 """
377 """
378 if name not in self.tags:
378 if name not in self.tags:
379 raise TagDoesNotExistError("Tag %s does not exist" % name)
379 raise TagDoesNotExistError("Tag %s does not exist" % name)
380 tagpath = vcspath.join(
380 tagpath = vcspath.join(
381 self._remote.get_refs_path(), 'refs', 'tags', name)
381 self._remote.get_refs_path(), 'refs', 'tags', name)
382 try:
382 try:
383 os.remove(tagpath)
383 os.remove(tagpath)
384 self._refs = self._get_refs()
384 self._refs = self._get_refs()
385 self.tags = self._get_tags()
385 self.tags = self._get_tags()
386 except OSError as e:
386 except OSError as e:
387 raise RepositoryError(e.strerror)
387 raise RepositoryError(e.strerror)
388
388
389 def _get_refs(self):
389 def _get_refs(self):
390 return self._remote.get_refs()
390 return self._remote.get_refs()
391
391
392 @LazyProperty
392 @LazyProperty
393 def _refs(self):
393 def _refs(self):
394 return self._get_refs()
394 return self._get_refs()
395
395
396 @property
396 @property
397 def _ref_tree(self):
397 def _ref_tree(self):
398 node = tree = {}
398 node = tree = {}
399 for ref, sha in self._refs.iteritems():
399 for ref, sha in self._refs.iteritems():
400 path = ref.split('/')
400 path = ref.split('/')
401 for bit in path[:-1]:
401 for bit in path[:-1]:
402 node = node.setdefault(bit, {})
402 node = node.setdefault(bit, {})
403 node[path[-1]] = sha
403 node[path[-1]] = sha
404 node = tree
404 node = tree
405 return tree
405 return tree
406
406
407 def get_remote_ref(self, ref_name):
407 def get_remote_ref(self, ref_name):
408 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
408 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
409 try:
409 try:
410 return self._refs[ref_key]
410 return self._refs[ref_key]
411 except Exception:
411 except Exception:
412 return
412 return
413
413
414 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
414 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
415 """
415 """
416 Returns `GitCommit` object representing commit from git repository
416 Returns `GitCommit` object representing commit from git repository
417 at the given `commit_id` or head (most recent commit) if None given.
417 at the given `commit_id` or head (most recent commit) if None given.
418 """
418 """
419 if commit_id is not None:
419 if commit_id is not None:
420 self._validate_commit_id(commit_id)
420 self._validate_commit_id(commit_id)
421 elif commit_idx is not None:
421 elif commit_idx is not None:
422 self._validate_commit_idx(commit_idx)
422 self._validate_commit_idx(commit_idx)
423 commit_id = commit_idx
423 commit_id = commit_idx
424 commit_id = self._get_commit_id(commit_id)
424 commit_id = self._get_commit_id(commit_id)
425 try:
425 try:
426 # Need to call remote to translate id for tagging scenario
426 # Need to call remote to translate id for tagging scenario
427 commit_id = self._remote.get_object(commit_id)["commit_id"]
427 commit_id = self._remote.get_object(commit_id)["commit_id"]
428 idx = self._commit_ids[commit_id]
428 idx = self._commit_ids[commit_id]
429 except KeyError:
429 except KeyError:
430 raise RepositoryError("Cannot get object with id %s" % commit_id)
430 raise RepositoryError("Cannot get object with id %s" % commit_id)
431
431
432 return GitCommit(self, commit_id, idx, pre_load=pre_load)
432 return GitCommit(self, commit_id, idx, pre_load=pre_load)
433
433
434 def get_commits(
434 def get_commits(
435 self, start_id=None, end_id=None, start_date=None, end_date=None,
435 self, start_id=None, end_id=None, start_date=None, end_date=None,
436 branch_name=None, show_hidden=False, pre_load=None):
436 branch_name=None, show_hidden=False, pre_load=None):
437 """
437 """
438 Returns generator of `GitCommit` objects from start to end (both
438 Returns generator of `GitCommit` objects from start to end (both
439 are inclusive), in ascending date order.
439 are inclusive), in ascending date order.
440
440
441 :param start_id: None, str(commit_id)
441 :param start_id: None, str(commit_id)
442 :param end_id: None, str(commit_id)
442 :param end_id: None, str(commit_id)
443 :param start_date: if specified, commits with commit date less than
443 :param start_date: if specified, commits with commit date less than
444 ``start_date`` would be filtered out from returned set
444 ``start_date`` would be filtered out from returned set
445 :param end_date: if specified, commits with commit date greater than
445 :param end_date: if specified, commits with commit date greater than
446 ``end_date`` would be filtered out from returned set
446 ``end_date`` would be filtered out from returned set
447 :param branch_name: if specified, commits not reachable from given
447 :param branch_name: if specified, commits not reachable from given
448 branch would be filtered out from returned set
448 branch would be filtered out from returned set
449 :param show_hidden: Show hidden commits such as obsolete or hidden from
449 :param show_hidden: Show hidden commits such as obsolete or hidden from
450 Mercurial evolve
450 Mercurial evolve
451 :raise BranchDoesNotExistError: If given `branch_name` does not
451 :raise BranchDoesNotExistError: If given `branch_name` does not
452 exist.
452 exist.
453 :raise CommitDoesNotExistError: If commits for given `start` or
453 :raise CommitDoesNotExistError: If commits for given `start` or
454 `end` could not be found.
454 `end` could not be found.
455
455
456 """
456 """
457 if self.is_empty():
457 if self.is_empty():
458 raise EmptyRepositoryError("There are no commits yet")
458 raise EmptyRepositoryError("There are no commits yet")
459 self._validate_branch_name(branch_name)
459 self._validate_branch_name(branch_name)
460
460
461 if start_id is not None:
461 if start_id is not None:
462 self._validate_commit_id(start_id)
462 self._validate_commit_id(start_id)
463 if end_id is not None:
463 if end_id is not None:
464 self._validate_commit_id(end_id)
464 self._validate_commit_id(end_id)
465
465
466 start_raw_id = self._get_commit_id(start_id)
466 start_raw_id = self._get_commit_id(start_id)
467 start_pos = self._commit_ids[start_raw_id] if start_id else None
467 start_pos = self._commit_ids[start_raw_id] if start_id else None
468 end_raw_id = self._get_commit_id(end_id)
468 end_raw_id = self._get_commit_id(end_id)
469 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
469 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
470
470
471 if None not in [start_id, end_id] and start_pos > end_pos:
471 if None not in [start_id, end_id] and start_pos > end_pos:
472 raise RepositoryError(
472 raise RepositoryError(
473 "Start commit '%s' cannot be after end commit '%s'" %
473 "Start commit '%s' cannot be after end commit '%s'" %
474 (start_id, end_id))
474 (start_id, end_id))
475
475
476 if end_pos is not None:
476 if end_pos is not None:
477 end_pos += 1
477 end_pos += 1
478
478
479 filter_ = []
479 filter_ = []
480 if branch_name:
480 if branch_name:
481 filter_.append({'branch_name': branch_name})
481 filter_.append({'branch_name': branch_name})
482 if start_date and not end_date:
482 if start_date and not end_date:
483 filter_.append({'since': start_date})
483 filter_.append({'since': start_date})
484 if end_date and not start_date:
484 if end_date and not start_date:
485 filter_.append({'until': end_date})
485 filter_.append({'until': end_date})
486 if start_date and end_date:
486 if start_date and end_date:
487 filter_.append({'since': start_date})
487 filter_.append({'since': start_date})
488 filter_.append({'until': end_date})
488 filter_.append({'until': end_date})
489
489
490 # if start_pos or end_pos:
490 # if start_pos or end_pos:
491 # filter_.append({'start': start_pos})
491 # filter_.append({'start': start_pos})
492 # filter_.append({'end': end_pos})
492 # filter_.append({'end': end_pos})
493
493
494 if filter_:
494 if filter_:
495 revfilters = {
495 revfilters = {
496 'branch_name': branch_name,
496 'branch_name': branch_name,
497 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
497 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
498 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
498 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
499 'start': start_pos,
499 'start': start_pos,
500 'end': end_pos,
500 'end': end_pos,
501 }
501 }
502 commit_ids = self._get_all_commit_ids(filters=revfilters)
502 commit_ids = self._get_all_commit_ids(filters=revfilters)
503
503
504 # pure python stuff, it's slow due to walker walking whole repo
504 # pure python stuff, it's slow due to walker walking whole repo
505 # def get_revs(walker):
505 # def get_revs(walker):
506 # for walker_entry in walker:
506 # for walker_entry in walker:
507 # yield walker_entry.commit.id
507 # yield walker_entry.commit.id
508 # revfilters = {}
508 # revfilters = {}
509 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
509 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
510 else:
510 else:
511 commit_ids = self.commit_ids
511 commit_ids = self.commit_ids
512
512
513 if start_pos or end_pos:
513 if start_pos or end_pos:
514 commit_ids = commit_ids[start_pos: end_pos]
514 commit_ids = commit_ids[start_pos: end_pos]
515
515
516 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
516 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
517
517
518 def get_diff(
518 def get_diff(
519 self, commit1, commit2, path='', ignore_whitespace=False,
519 self, commit1, commit2, path='', ignore_whitespace=False,
520 context=3, path1=None):
520 context=3, path1=None):
521 """
521 """
522 Returns (git like) *diff*, as plain text. Shows changes introduced by
522 Returns (git like) *diff*, as plain text. Shows changes introduced by
523 ``commit2`` since ``commit1``.
523 ``commit2`` since ``commit1``.
524
524
525 :param commit1: Entry point from which diff is shown. Can be
525 :param commit1: Entry point from which diff is shown. Can be
526 ``self.EMPTY_COMMIT`` - in this case, patch showing all
526 ``self.EMPTY_COMMIT`` - in this case, patch showing all
527 the changes since empty state of the repository until ``commit2``
527 the changes since empty state of the repository until ``commit2``
528 :param commit2: Until which commits changes should be shown.
528 :param commit2: Until which commits changes should be shown.
529 :param ignore_whitespace: If set to ``True``, would not show whitespace
529 :param ignore_whitespace: If set to ``True``, would not show whitespace
530 changes. Defaults to ``False``.
530 changes. Defaults to ``False``.
531 :param context: How many lines before/after changed lines should be
531 :param context: How many lines before/after changed lines should be
532 shown. Defaults to ``3``.
532 shown. Defaults to ``3``.
533 """
533 """
534 self._validate_diff_commits(commit1, commit2)
534 self._validate_diff_commits(commit1, commit2)
535 if path1 is not None and path1 != path:
535 if path1 is not None and path1 != path:
536 raise ValueError("Diff of two different paths not supported.")
536 raise ValueError("Diff of two different paths not supported.")
537
537
538 flags = [
538 flags = [
539 '-U%s' % context, '--full-index', '--binary', '-p',
539 '-U%s' % context, '--full-index', '--binary', '-p',
540 '-M', '--abbrev=40']
540 '-M', '--abbrev=40']
541 if ignore_whitespace:
541 if ignore_whitespace:
542 flags.append('-w')
542 flags.append('-w')
543
543
544 if commit1 == self.EMPTY_COMMIT:
544 if commit1 == self.EMPTY_COMMIT:
545 cmd = ['show'] + flags + [commit2.raw_id]
545 cmd = ['show'] + flags + [commit2.raw_id]
546 else:
546 else:
547 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
547 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
548
548
549 if path:
549 if path:
550 cmd.extend(['--', path])
550 cmd.extend(['--', path])
551
551
552 stdout, __ = self.run_git_command(cmd)
552 stdout, __ = self.run_git_command(cmd)
553 # If we used 'show' command, strip first few lines (until actual diff
553 # If we used 'show' command, strip first few lines (until actual diff
554 # starts)
554 # starts)
555 if commit1 == self.EMPTY_COMMIT:
555 if commit1 == self.EMPTY_COMMIT:
556 lines = stdout.splitlines()
556 lines = stdout.splitlines()
557 x = 0
557 x = 0
558 for line in lines:
558 for line in lines:
559 if line.startswith('diff'):
559 if line.startswith('diff'):
560 break
560 break
561 x += 1
561 x += 1
562 # Append new line just like 'diff' command do
562 # Append new line just like 'diff' command do
563 stdout = '\n'.join(lines[x:]) + '\n'
563 stdout = '\n'.join(lines[x:]) + '\n'
564 return GitDiff(stdout)
564 return GitDiff(stdout)
565
565
566 def strip(self, commit_id, branch_name):
566 def strip(self, commit_id, branch_name):
567 commit = self.get_commit(commit_id=commit_id)
567 commit = self.get_commit(commit_id=commit_id)
568 if commit.merge:
568 if commit.merge:
569 raise Exception('Cannot reset to merge commit')
569 raise Exception('Cannot reset to merge commit')
570
570
571 # parent is going to be the new head now
571 # parent is going to be the new head now
572 commit = commit.parents[0]
572 commit = commit.parents[0]
573 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
573 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
574
574
575 self.commit_ids = self._get_all_commit_ids()
575 self.commit_ids = self._get_all_commit_ids()
576 self._rebuild_cache(self.commit_ids)
576 self._rebuild_cache(self.commit_ids)
577
577
578 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
578 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
579 if commit_id1 == commit_id2:
579 if commit_id1 == commit_id2:
580 return commit_id1
580 return commit_id1
581
581
582 if self != repo2:
582 if self != repo2:
583 commits = self._remote.get_missing_revs(
583 commits = self._remote.get_missing_revs(
584 commit_id1, commit_id2, repo2.path)
584 commit_id1, commit_id2, repo2.path)
585 if commits:
585 if commits:
586 commit = repo2.get_commit(commits[-1])
586 commit = repo2.get_commit(commits[-1])
587 if commit.parents:
587 if commit.parents:
588 ancestor_id = commit.parents[0].raw_id
588 ancestor_id = commit.parents[0].raw_id
589 else:
589 else:
590 ancestor_id = None
590 ancestor_id = None
591 else:
591 else:
592 # no commits from other repo, ancestor_id is the commit_id2
592 # no commits from other repo, ancestor_id is the commit_id2
593 ancestor_id = commit_id2
593 ancestor_id = commit_id2
594 else:
594 else:
595 output, __ = self.run_git_command(
595 output, __ = self.run_git_command(
596 ['merge-base', commit_id1, commit_id2])
596 ['merge-base', commit_id1, commit_id2])
597 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
597 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
598
598
599 return ancestor_id
599 return ancestor_id
600
600
601 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
601 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
602 repo1 = self
602 repo1 = self
603 ancestor_id = None
603 ancestor_id = None
604
604
605 if commit_id1 == commit_id2:
605 if commit_id1 == commit_id2:
606 commits = []
606 commits = []
607 elif repo1 != repo2:
607 elif repo1 != repo2:
608 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
608 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
609 repo2.path)
609 repo2.path)
610 commits = [
610 commits = [
611 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
611 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
612 for commit_id in reversed(missing_ids)]
612 for commit_id in reversed(missing_ids)]
613 else:
613 else:
614 output, __ = repo1.run_git_command(
614 output, __ = repo1.run_git_command(
615 ['log', '--reverse', '--pretty=format: %H', '-s',
615 ['log', '--reverse', '--pretty=format: %H', '-s',
616 '%s..%s' % (commit_id1, commit_id2)])
616 '%s..%s' % (commit_id1, commit_id2)])
617 commits = [
617 commits = [
618 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
618 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
619 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
619 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
620
620
621 return commits
621 return commits
622
622
623 @LazyProperty
623 @LazyProperty
624 def in_memory_commit(self):
624 def in_memory_commit(self):
625 """
625 """
626 Returns ``GitInMemoryCommit`` object for this repository.
626 Returns ``GitInMemoryCommit`` object for this repository.
627 """
627 """
628 return GitInMemoryCommit(self)
628 return GitInMemoryCommit(self)
629
629
630 def clone(self, url, update_after_clone=True, bare=False):
630 def clone(self, url, update_after_clone=True, bare=False):
631 """
631 """
632 Tries to clone commits from external location.
632 Tries to clone commits from external location.
633
633
634 :param update_after_clone: If set to ``False``, git won't checkout
634 :param update_after_clone: If set to ``False``, git won't checkout
635 working directory
635 working directory
636 :param bare: If set to ``True``, repository would be cloned into
636 :param bare: If set to ``True``, repository would be cloned into
637 *bare* git repository (no working directory at all).
637 *bare* git repository (no working directory at all).
638 """
638 """
639 # init_bare and init expect empty dir created to proceed
639 # init_bare and init expect empty dir created to proceed
640 if not os.path.exists(self.path):
640 if not os.path.exists(self.path):
641 os.mkdir(self.path)
641 os.mkdir(self.path)
642
642
643 if bare:
643 if bare:
644 self._remote.init_bare()
644 self._remote.init_bare()
645 else:
645 else:
646 self._remote.init()
646 self._remote.init()
647
647
648 deferred = '^{}'
648 deferred = '^{}'
649 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
649 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
650
650
651 return self._remote.clone(
651 return self._remote.clone(
652 url, deferred, valid_refs, update_after_clone)
652 url, deferred, valid_refs, update_after_clone)
653
653
654 def pull(self, url, commit_ids=None):
654 def pull(self, url, commit_ids=None):
655 """
655 """
656 Tries to pull changes from external location. We use fetch here since
656 Tries to pull changes from external location. We use fetch here since
657 pull in get does merges and we want to be compatible with hg backend so
657 pull in get does merges and we want to be compatible with hg backend so
658 pull == fetch in this case
658 pull == fetch in this case
659 """
659 """
660 self.fetch(url, commit_ids=commit_ids)
660 self.fetch(url, commit_ids=commit_ids)
661
661
662 def fetch(self, url, commit_ids=None):
662 def fetch(self, url, commit_ids=None):
663 """
663 """
664 Tries to fetch changes from external location.
664 Tries to fetch changes from external location.
665 """
665 """
666 refs = None
666 refs = None
667
667
668 if commit_ids is not None:
668 if commit_ids is not None:
669 remote_refs = self._remote.get_remote_refs(url)
669 remote_refs = self._remote.get_remote_refs(url)
670 refs = [
670 refs = [
671 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
671 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
672 self._remote.fetch(url, refs=refs)
672 self._remote.fetch(url, refs=refs)
673
673
674 def push(self, url):
674 def push(self, url):
675 refs = None
675 refs = None
676 self._remote.sync_push(url, refs=refs)
676 self._remote.sync_push(url, refs=refs)
677
677
678 def set_refs(self, ref_name, commit_id):
678 def set_refs(self, ref_name, commit_id):
679 self._remote.set_refs(ref_name, commit_id)
679 self._remote.set_refs(ref_name, commit_id)
680
680
681 def remove_ref(self, ref_name):
681 def remove_ref(self, ref_name):
682 self._remote.remove_ref(ref_name)
682 self._remote.remove_ref(ref_name)
683
683
684 def _update_server_info(self):
684 def _update_server_info(self):
685 """
685 """
686 runs gits update-server-info command in this repo instance
686 runs gits update-server-info command in this repo instance
687 """
687 """
688 self._remote.update_server_info()
688 self._remote.update_server_info()
689
689
690 def _current_branch(self):
690 def _current_branch(self):
691 """
691 """
692 Return the name of the current branch.
692 Return the name of the current branch.
693
693
694 It only works for non bare repositories (i.e. repositories with a
694 It only works for non bare repositories (i.e. repositories with a
695 working copy)
695 working copy)
696 """
696 """
697 if self.bare:
697 if self.bare:
698 raise RepositoryError('Bare git repos do not have active branches')
698 raise RepositoryError('Bare git repos do not have active branches')
699
699
700 if self.is_empty():
700 if self.is_empty():
701 return None
701 return None
702
702
703 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
703 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
704 return stdout.strip()
704 return stdout.strip()
705
705
706 def _checkout(self, branch_name, create=False, force=False):
706 def _checkout(self, branch_name, create=False, force=False):
707 """
707 """
708 Checkout a branch in the working directory.
708 Checkout a branch in the working directory.
709
709
710 It tries to create the branch if create is True, failing if the branch
710 It tries to create the branch if create is True, failing if the branch
711 already exists.
711 already exists.
712
712
713 It only works for non bare repositories (i.e. repositories with a
713 It only works for non bare repositories (i.e. repositories with a
714 working copy)
714 working copy)
715 """
715 """
716 if self.bare:
716 if self.bare:
717 raise RepositoryError('Cannot checkout branches in a bare git repo')
717 raise RepositoryError('Cannot checkout branches in a bare git repo')
718
718
719 cmd = ['checkout']
719 cmd = ['checkout']
720 if force:
720 if force:
721 cmd.append('-f')
721 cmd.append('-f')
722 if create:
722 if create:
723 cmd.append('-b')
723 cmd.append('-b')
724 cmd.append(branch_name)
724 cmd.append(branch_name)
725 self.run_git_command(cmd, fail_on_stderr=False)
725 self.run_git_command(cmd, fail_on_stderr=False)
726
726
727 def _identify(self):
727 def _identify(self):
728 """
728 """
729 Return the current state of the working directory.
729 Return the current state of the working directory.
730 """
730 """
731 if self.bare:
731 if self.bare:
732 raise RepositoryError('Bare git repos do not have active branches')
732 raise RepositoryError('Bare git repos do not have active branches')
733
733
734 if self.is_empty():
734 if self.is_empty():
735 return None
735 return None
736
736
737 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
737 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
738 return stdout.strip()
738 return stdout.strip()
739
739
740 def _local_clone(self, clone_path, branch_name, source_branch=None):
740 def _local_clone(self, clone_path, branch_name, source_branch=None):
741 """
741 """
742 Create a local clone of the current repo.
742 Create a local clone of the current repo.
743 """
743 """
744 # N.B.(skreft): the --branch option is required as otherwise the shallow
744 # N.B.(skreft): the --branch option is required as otherwise the shallow
745 # clone will only fetch the active branch.
745 # clone will only fetch the active branch.
746 cmd = ['clone', '--branch', branch_name,
746 cmd = ['clone', '--branch', branch_name,
747 self.path, os.path.abspath(clone_path)]
747 self.path, os.path.abspath(clone_path)]
748
748
749 self.run_git_command(cmd, fail_on_stderr=False)
749 self.run_git_command(cmd, fail_on_stderr=False)
750
750
751 # if we get the different source branch, make sure we also fetch it for
751 # if we get the different source branch, make sure we also fetch it for
752 # merge conditions
752 # merge conditions
753 if source_branch and source_branch != branch_name:
753 if source_branch and source_branch != branch_name:
754 # check if the ref exists.
754 # check if the ref exists.
755 shadow_repo = GitRepository(os.path.abspath(clone_path))
755 shadow_repo = GitRepository(os.path.abspath(clone_path))
756 if shadow_repo.get_remote_ref(source_branch):
756 if shadow_repo.get_remote_ref(source_branch):
757 cmd = ['fetch', self.path, source_branch]
757 cmd = ['fetch', self.path, source_branch]
758 self.run_git_command(cmd, fail_on_stderr=False)
758 self.run_git_command(cmd, fail_on_stderr=False)
759
759
760 def _local_fetch(self, repository_path, branch_name):
760 def _local_fetch(self, repository_path, branch_name, use_origin=False):
761 """
761 """
762 Fetch a branch from a local repository.
762 Fetch a branch from a local repository.
763 """
763 """
764 repository_path = os.path.abspath(repository_path)
764 repository_path = os.path.abspath(repository_path)
765 if repository_path == self.path:
765 if repository_path == self.path:
766 raise ValueError('Cannot fetch from the same repository')
766 raise ValueError('Cannot fetch from the same repository')
767
767
768 cmd = ['fetch', '--no-tags', repository_path, branch_name]
768 if use_origin:
769 branch_name = '+{branch}:refs/heads/{branch}'.format(
770 branch=branch_name)
771
772 cmd = ['fetch', '--no-tags', '--update-head-ok',
773 repository_path, branch_name]
774 self.run_git_command(cmd, fail_on_stderr=False)
775
776 def _local_reset(self, branch_name):
777 branch_name = '{}'.format(branch_name)
778 cmd = ['reset', '--hard', branch_name]
769 self.run_git_command(cmd, fail_on_stderr=False)
779 self.run_git_command(cmd, fail_on_stderr=False)
770
780
771 def _last_fetch_heads(self):
781 def _last_fetch_heads(self):
772 """
782 """
773 Return the last fetched heads that need merging.
783 Return the last fetched heads that need merging.
774
784
775 The algorithm is defined at
785 The algorithm is defined at
776 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
786 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
777 """
787 """
778 if not self.bare:
788 if not self.bare:
779 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
789 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
780 else:
790 else:
781 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
791 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
782
792
783 heads = []
793 heads = []
784 with open(fetch_heads_path) as f:
794 with open(fetch_heads_path) as f:
785 for line in f:
795 for line in f:
786 if ' not-for-merge ' in line:
796 if ' not-for-merge ' in line:
787 continue
797 continue
788 line = re.sub('\t.*', '', line, flags=re.DOTALL)
798 line = re.sub('\t.*', '', line, flags=re.DOTALL)
789 heads.append(line)
799 heads.append(line)
790
800
791 return heads
801 return heads
792
802
793 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
803 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
794 return GitRepository(shadow_repository_path)
804 return GitRepository(shadow_repository_path)
795
805
796 def _local_pull(self, repository_path, branch_name, ff_only=True):
806 def _local_pull(self, repository_path, branch_name, ff_only=True):
797 """
807 """
798 Pull a branch from a local repository.
808 Pull a branch from a local repository.
799 """
809 """
800 if self.bare:
810 if self.bare:
801 raise RepositoryError('Cannot pull into a bare git repository')
811 raise RepositoryError('Cannot pull into a bare git repository')
802 # N.B.(skreft): The --ff-only option is to make sure this is a
812 # N.B.(skreft): The --ff-only option is to make sure this is a
803 # fast-forward (i.e., we are only pulling new changes and there are no
813 # fast-forward (i.e., we are only pulling new changes and there are no
804 # conflicts with our current branch)
814 # conflicts with our current branch)
805 # Additionally, that option needs to go before --no-tags, otherwise git
815 # Additionally, that option needs to go before --no-tags, otherwise git
806 # pull complains about it being an unknown flag.
816 # pull complains about it being an unknown flag.
807 cmd = ['pull']
817 cmd = ['pull']
808 if ff_only:
818 if ff_only:
809 cmd.append('--ff-only')
819 cmd.append('--ff-only')
810 cmd.extend(['--no-tags', repository_path, branch_name])
820 cmd.extend(['--no-tags', repository_path, branch_name])
811 self.run_git_command(cmd, fail_on_stderr=False)
821 self.run_git_command(cmd, fail_on_stderr=False)
812
822
813 def _local_merge(self, merge_message, user_name, user_email, heads):
823 def _local_merge(self, merge_message, user_name, user_email, heads):
814 """
824 """
815 Merge the given head into the checked out branch.
825 Merge the given head into the checked out branch.
816
826
817 It will force a merge commit.
827 It will force a merge commit.
818
828
819 Currently it raises an error if the repo is empty, as it is not possible
829 Currently it raises an error if the repo is empty, as it is not possible
820 to create a merge commit in an empty repo.
830 to create a merge commit in an empty repo.
821
831
822 :param merge_message: The message to use for the merge commit.
832 :param merge_message: The message to use for the merge commit.
823 :param heads: the heads to merge.
833 :param heads: the heads to merge.
824 """
834 """
825 if self.bare:
835 if self.bare:
826 raise RepositoryError('Cannot merge into a bare git repository')
836 raise RepositoryError('Cannot merge into a bare git repository')
827
837
828 if not heads:
838 if not heads:
829 return
839 return
830
840
831 if self.is_empty():
841 if self.is_empty():
832 # TODO(skreft): do somehting more robust in this case.
842 # TODO(skreft): do somehting more robust in this case.
833 raise RepositoryError(
843 raise RepositoryError(
834 'Do not know how to merge into empty repositories yet')
844 'Do not know how to merge into empty repositories yet')
835
845
836 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
846 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
837 # commit message. We also specify the user who is doing the merge.
847 # commit message. We also specify the user who is doing the merge.
838 cmd = ['-c', 'user.name="%s"' % safe_str(user_name),
848 cmd = ['-c', 'user.name="%s"' % safe_str(user_name),
839 '-c', 'user.email=%s' % safe_str(user_email),
849 '-c', 'user.email=%s' % safe_str(user_email),
840 'merge', '--no-ff', '-m', safe_str(merge_message)]
850 'merge', '--no-ff', '-m', safe_str(merge_message)]
841 cmd.extend(heads)
851 cmd.extend(heads)
842 try:
852 try:
843 self.run_git_command(cmd, fail_on_stderr=False)
853 output = self.run_git_command(cmd, fail_on_stderr=False)
844 except RepositoryError:
854 except RepositoryError:
845 # Cleanup any merge leftovers
855 # Cleanup any merge leftovers
846 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
856 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
847 raise
857 raise
848
858
849 def _local_push(
859 def _local_push(
850 self, source_branch, repository_path, target_branch,
860 self, source_branch, repository_path, target_branch,
851 enable_hooks=False, rc_scm_data=None):
861 enable_hooks=False, rc_scm_data=None):
852 """
862 """
853 Push the source_branch to the given repository and target_branch.
863 Push the source_branch to the given repository and target_branch.
854
864
855 Currently it if the target_branch is not master and the target repo is
865 Currently it if the target_branch is not master and the target repo is
856 empty, the push will work, but then GitRepository won't be able to find
866 empty, the push will work, but then GitRepository won't be able to find
857 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
867 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
858 pointing to master, which does not exist).
868 pointing to master, which does not exist).
859
869
860 It does not run the hooks in the target repo.
870 It does not run the hooks in the target repo.
861 """
871 """
862 # TODO(skreft): deal with the case in which the target repo is empty,
872 # TODO(skreft): deal with the case in which the target repo is empty,
863 # and the target_branch is not master.
873 # and the target_branch is not master.
864 target_repo = GitRepository(repository_path)
874 target_repo = GitRepository(repository_path)
865 if (not target_repo.bare and
875 if (not target_repo.bare and
866 target_repo._current_branch() == target_branch):
876 target_repo._current_branch() == target_branch):
867 # Git prevents pushing to the checked out branch, so simulate it by
877 # Git prevents pushing to the checked out branch, so simulate it by
868 # pulling into the target repository.
878 # pulling into the target repository.
869 target_repo._local_pull(self.path, source_branch)
879 target_repo._local_pull(self.path, source_branch)
870 else:
880 else:
871 cmd = ['push', os.path.abspath(repository_path),
881 cmd = ['push', os.path.abspath(repository_path),
872 '%s:%s' % (source_branch, target_branch)]
882 '%s:%s' % (source_branch, target_branch)]
873 gitenv = {}
883 gitenv = {}
874 if rc_scm_data:
884 if rc_scm_data:
875 gitenv.update({'RC_SCM_DATA': rc_scm_data})
885 gitenv.update({'RC_SCM_DATA': rc_scm_data})
876
886
877 if not enable_hooks:
887 if not enable_hooks:
878 gitenv['RC_SKIP_HOOKS'] = '1'
888 gitenv['RC_SKIP_HOOKS'] = '1'
879 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
889 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
880
890
881 def _get_new_pr_branch(self, source_branch, target_branch):
891 def _get_new_pr_branch(self, source_branch, target_branch):
882 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
892 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
883 pr_branches = []
893 pr_branches = []
884 for branch in self.branches:
894 for branch in self.branches:
885 if branch.startswith(prefix):
895 if branch.startswith(prefix):
886 pr_branches.append(int(branch[len(prefix):]))
896 pr_branches.append(int(branch[len(prefix):]))
887
897
888 if not pr_branches:
898 if not pr_branches:
889 branch_id = 0
899 branch_id = 0
890 else:
900 else:
891 branch_id = max(pr_branches) + 1
901 branch_id = max(pr_branches) + 1
892
902
893 return '%s%d' % (prefix, branch_id)
903 return '%s%d' % (prefix, branch_id)
894
904
895 def _merge_repo(self, shadow_repository_path, target_ref,
905 def _merge_repo(self, shadow_repository_path, target_ref,
896 source_repo, source_ref, merge_message,
906 source_repo, source_ref, merge_message,
897 merger_name, merger_email, dry_run=False,
907 merger_name, merger_email, dry_run=False,
898 use_rebase=False, close_branch=False):
908 use_rebase=False, close_branch=False):
899 if target_ref.commit_id != self.branches[target_ref.name]:
909 if target_ref.commit_id != self.branches[target_ref.name]:
910 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
911 target_ref.commit_id, self.branches[target_ref.name])
900 return MergeResponse(
912 return MergeResponse(
901 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
913 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
902
914
903 shadow_repo = GitRepository(shadow_repository_path)
915 shadow_repo = GitRepository(shadow_repository_path)
904 # checkout source, if it's different. Otherwise we could not
916 # checkout source, if it's different. Otherwise we could not
905 # fetch proper commits for merge testing
917 # fetch proper commits for merge testing
906 if source_ref.name != target_ref.name:
918 if source_ref.name != target_ref.name:
907 if shadow_repo.get_remote_ref(source_ref.name):
919 if shadow_repo.get_remote_ref(source_ref.name):
908 shadow_repo._checkout(source_ref.name, force=True)
920 shadow_repo._checkout(source_ref.name, force=True)
909
921
910 # checkout target
922 # checkout target, and fetch changes
911 shadow_repo._checkout(target_ref.name, force=True)
923 shadow_repo._checkout(target_ref.name, force=True)
912 shadow_repo._local_pull(self.path, target_ref.name)
924
925 # fetch/reset pull the target, in case it is changed
926 # this handles even force changes
927 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
928 shadow_repo._local_reset(target_ref.name)
913
929
914 # Need to reload repo to invalidate the cache, or otherwise we cannot
930 # Need to reload repo to invalidate the cache, or otherwise we cannot
915 # retrieve the last target commit.
931 # retrieve the last target commit.
916 shadow_repo = GitRepository(shadow_repository_path)
932 shadow_repo = GitRepository(shadow_repository_path)
917 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
933 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
934 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
935 target_ref, target_ref.commit_id,
936 shadow_repo.branches[target_ref.name])
918 return MergeResponse(
937 return MergeResponse(
919 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
938 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
920
939
940 # calculate new branch
921 pr_branch = shadow_repo._get_new_pr_branch(
941 pr_branch = shadow_repo._get_new_pr_branch(
922 source_ref.name, target_ref.name)
942 source_ref.name, target_ref.name)
923 log.debug('using pull-request merge branch: `%s`', pr_branch)
943 log.debug('using pull-request merge branch: `%s`', pr_branch)
944 # checkout to temp branch, and fetch changes
924 shadow_repo._checkout(pr_branch, create=True)
945 shadow_repo._checkout(pr_branch, create=True)
925 try:
946 try:
926 shadow_repo._local_fetch(source_repo.path, source_ref.name)
947 shadow_repo._local_fetch(source_repo.path, source_ref.name)
927 except RepositoryError:
948 except RepositoryError:
928 log.exception('Failure when doing local fetch on git shadow repo')
949 log.exception('Failure when doing local fetch on git shadow repo')
929 return MergeResponse(
950 return MergeResponse(
930 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
951 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
931
952
932 merge_ref = None
953 merge_ref = None
933 merge_failure_reason = MergeFailureReason.NONE
954 merge_failure_reason = MergeFailureReason.NONE
934 try:
955 try:
935 shadow_repo._local_merge(merge_message, merger_name, merger_email,
956 shadow_repo._local_merge(merge_message, merger_name, merger_email,
936 [source_ref.commit_id])
957 [source_ref.commit_id])
937 merge_possible = True
958 merge_possible = True
938
959
939 # Need to reload repo to invalidate the cache, or otherwise we
960 # Need to reload repo to invalidate the cache, or otherwise we
940 # cannot retrieve the merge commit.
961 # cannot retrieve the merge commit.
941 shadow_repo = GitRepository(shadow_repository_path)
962 shadow_repo = GitRepository(shadow_repository_path)
942 merge_commit_id = shadow_repo.branches[pr_branch]
963 merge_commit_id = shadow_repo.branches[pr_branch]
943
964
944 # Set a reference pointing to the merge commit. This reference may
965 # Set a reference pointing to the merge commit. This reference may
945 # be used to easily identify the last successful merge commit in
966 # be used to easily identify the last successful merge commit in
946 # the shadow repository.
967 # the shadow repository.
947 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
968 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
948 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
969 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
949 except RepositoryError:
970 except RepositoryError:
950 log.exception('Failure when doing local merge on git shadow repo')
971 log.exception('Failure when doing local merge on git shadow repo')
951 merge_possible = False
972 merge_possible = False
952 merge_failure_reason = MergeFailureReason.MERGE_FAILED
973 merge_failure_reason = MergeFailureReason.MERGE_FAILED
953
974
954 if merge_possible and not dry_run:
975 if merge_possible and not dry_run:
955 try:
976 try:
956 shadow_repo._local_push(
977 shadow_repo._local_push(
957 pr_branch, self.path, target_ref.name, enable_hooks=True,
978 pr_branch, self.path, target_ref.name, enable_hooks=True,
958 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
979 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
959 merge_succeeded = True
980 merge_succeeded = True
960 except RepositoryError:
981 except RepositoryError:
961 log.exception(
982 log.exception(
962 'Failure when doing local push on git shadow repo')
983 'Failure when doing local push on git shadow repo')
963 merge_succeeded = False
984 merge_succeeded = False
964 merge_failure_reason = MergeFailureReason.PUSH_FAILED
985 merge_failure_reason = MergeFailureReason.PUSH_FAILED
965 else:
986 else:
966 merge_succeeded = False
987 merge_succeeded = False
967
988
968 return MergeResponse(
989 return MergeResponse(
969 merge_possible, merge_succeeded, merge_ref,
990 merge_possible, merge_succeeded, merge_ref,
970 merge_failure_reason)
991 merge_failure_reason)
971
992
972 def _get_shadow_repository_path(self, workspace_id):
993 def _get_shadow_repository_path(self, workspace_id):
973 # The name of the shadow repository must start with '.', so it is
994 # The name of the shadow repository must start with '.', so it is
974 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
995 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
975 return os.path.join(
996 return os.path.join(
976 os.path.dirname(self.path),
997 os.path.dirname(self.path),
977 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
998 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
978
999
979 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
1000 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
980 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
1001 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
981 if not os.path.exists(shadow_repository_path):
1002 if not os.path.exists(shadow_repository_path):
982 self._local_clone(
1003 self._local_clone(
983 shadow_repository_path, target_ref.name, source_ref.name)
1004 shadow_repository_path, target_ref.name, source_ref.name)
984
1005
985 return shadow_repository_path
1006 return shadow_repository_path
@@ -1,855 +1,856 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 <span id="pr-title">
12 <span id="pr-title">
13 ${c.pull_request.title}
13 ${c.pull_request.title}
14 %if c.pull_request.is_closed():
14 %if c.pull_request.is_closed():
15 (${_('Closed')})
15 (${_('Closed')})
16 %endif
16 %endif
17 </span>
17 </span>
18 <div id="pr-title-edit" class="input" style="display: none;">
18 <div id="pr-title-edit" class="input" style="display: none;">
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 </div>
20 </div>
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='showpullrequest')}
28 ${self.repo_menu(active='showpullrequest')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32
32
33 <script type="text/javascript">
33 <script type="text/javascript">
34 // TODO: marcink switch this to pyroutes
34 // TODO: marcink switch this to pyroutes
35 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
35 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 </script>
37 </script>
38 <div class="box">
38 <div class="box">
39
39
40 <div class="title">
40 <div class="title">
41 ${self.repo_page_title(c.rhodecode_db_repo)}
41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 </div>
42 </div>
43
43
44 ${self.breadcrumbs()}
44 ${self.breadcrumbs()}
45
45
46 <div class="box pr-summary">
46 <div class="box pr-summary">
47
47
48 <div class="summary-details block-left">
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
50 <div class="pr-details-title">
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 %if c.allowed_to_update:
52 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
54 % if c.allowed_to_delete:
55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
58 ${h.end_form()}
59 % else:
59 % else:
60 ${_('Delete')}
60 ${_('Delete')}
61 % endif
61 % endif
62 </div>
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
65 %endif
66 </div>
66 </div>
67
67
68 <div id="summary" class="fields pr-details-content">
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
69 <div class="field">
70 <div class="label-summary">
70 <div class="label-summary">
71 <label>${_('Source')}:</label>
71 <label>${_('Source')}:</label>
72 </div>
72 </div>
73 <div class="input">
73 <div class="input">
74 <div class="pr-origininfo">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
75 ## branch link is only valid if it is a branch
76 <span class="tag">
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 %else:
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
81 %endif
82 </span>
82 </span>
83 <span class="clone-url">
83 <span class="clone-url">
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 </span>
85 </span>
86 <br/>
86 <br/>
87 % if c.ancestor_commit:
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
88 ${_('Common ancestor')}:
89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
90 % endif
91 </div>
91 </div>
92 %if h.is_hg(c.pull_request.source_repo):
92 %if h.is_hg(c.pull_request.source_repo):
93 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
93 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
94 %elif h.is_git(c.pull_request.source_repo):
94 %elif h.is_git(c.pull_request.source_repo):
95 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
95 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
96 %endif
96 %endif
97
97
98 <div class="">
98 <div class="">
99 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
99 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
100 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
100 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
101 </div>
101 </div>
102
102
103 </div>
103 </div>
104 </div>
104 </div>
105 <div class="field">
105 <div class="field">
106 <div class="label-summary">
106 <div class="label-summary">
107 <label>${_('Target')}:</label>
107 <label>${_('Target')}:</label>
108 </div>
108 </div>
109 <div class="input">
109 <div class="input">
110 <div class="pr-targetinfo">
110 <div class="pr-targetinfo">
111 ## branch link is only valid if it is a branch
111 ## branch link is only valid if it is a branch
112 <span class="tag">
112 <span class="tag">
113 %if c.pull_request.target_ref_parts.type == 'branch':
113 %if c.pull_request.target_ref_parts.type == 'branch':
114 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
114 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
115 %else:
115 %else:
116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
117 %endif
117 %endif
118 </span>
118 </span>
119 <span class="clone-url">
119 <span class="clone-url">
120 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
120 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
121 </span>
121 </span>
122 </div>
122 </div>
123 </div>
123 </div>
124 </div>
124 </div>
125
125
126 ## Link to the shadow repository.
126 ## Link to the shadow repository.
127 <div class="field">
127 <div class="field">
128 <div class="label-summary">
128 <div class="label-summary">
129 <label>${_('Merge')}:</label>
129 <label>${_('Merge')}:</label>
130 </div>
130 </div>
131 <div class="input">
131 <div class="input">
132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
133 %if h.is_hg(c.pull_request.target_repo):
133 %if h.is_hg(c.pull_request.target_repo):
134 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
134 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
135 %elif h.is_git(c.pull_request.target_repo):
135 %elif h.is_git(c.pull_request.target_repo):
136 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
136 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
137 %endif
137 %endif
138 <div class="">
138 <div class="">
139 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
139 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
140 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
140 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
141 </div>
141 </div>
142 % else:
142 % else:
143 <div class="">
143 <div class="">
144 ${_('Shadow repository data not available')}.
144 ${_('Shadow repository data not available')}.
145 </div>
145 </div>
146 % endif
146 % endif
147 </div>
147 </div>
148 </div>
148 </div>
149
149
150 <div class="field">
150 <div class="field">
151 <div class="label-summary">
151 <div class="label-summary">
152 <label>${_('Review')}:</label>
152 <label>${_('Review')}:</label>
153 </div>
153 </div>
154 <div class="input">
154 <div class="input">
155 %if c.pull_request_review_status:
155 %if c.pull_request_review_status:
156 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
156 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
157 <span class="changeset-status-lbl tooltip">
157 <span class="changeset-status-lbl tooltip">
158 %if c.pull_request.is_closed():
158 %if c.pull_request.is_closed():
159 ${_('Closed')},
159 ${_('Closed')},
160 %endif
160 %endif
161 ${h.commit_status_lbl(c.pull_request_review_status)}
161 ${h.commit_status_lbl(c.pull_request_review_status)}
162 </span>
162 </span>
163 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
163 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
164 %endif
164 %endif
165 </div>
165 </div>
166 </div>
166 </div>
167 <div class="field">
167 <div class="field">
168 <div class="pr-description-label label-summary">
168 <div class="pr-description-label label-summary">
169 <label>${_('Description')}:</label>
169 <label>${_('Description')}:</label>
170 </div>
170 </div>
171 <div id="pr-desc" class="input">
171 <div id="pr-desc" class="input">
172 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
172 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
173 </div>
173 </div>
174 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
174 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
175 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
175 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
176 </div>
176 </div>
177 </div>
177 </div>
178
178
179 <div class="field">
179 <div class="field">
180 <div class="label-summary">
180 <div class="label-summary">
181 <label>${_('Versions')}:</label>
181 <label>${_('Versions')}:</label>
182 </div>
182 </div>
183
183
184 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
184 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
185 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
185 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
186
186
187 <div class="pr-versions">
187 <div class="pr-versions">
188 % if c.show_version_changes:
188 % if c.show_version_changes:
189 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
189 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
190 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
190 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
191 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
191 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
192 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
192 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
193 data-toggle-off="${_('Hide all versions of this pull request')}">
193 data-toggle-off="${_('Hide all versions of this pull request')}">
194 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
194 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
195 </a>
195 </a>
196 <table>
196 <table>
197 ## SHOW ALL VERSIONS OF PR
197 ## SHOW ALL VERSIONS OF PR
198 <% ver_pr = None %>
198 <% ver_pr = None %>
199
199
200 % for data in reversed(list(enumerate(c.versions, 1))):
200 % for data in reversed(list(enumerate(c.versions, 1))):
201 <% ver_pos = data[0] %>
201 <% ver_pos = data[0] %>
202 <% ver = data[1] %>
202 <% ver = data[1] %>
203 <% ver_pr = ver.pull_request_version_id %>
203 <% ver_pr = ver.pull_request_version_id %>
204 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
204 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
205
205
206 <tr class="version-pr" style="display: ${display_row}">
206 <tr class="version-pr" style="display: ${display_row}">
207 <td>
207 <td>
208 <code>
208 <code>
209 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
209 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
210 </code>
210 </code>
211 </td>
211 </td>
212 <td>
212 <td>
213 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
213 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
214 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
214 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
215 </td>
215 </td>
216 <td>
216 <td>
217 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
217 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
218 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
218 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
219 </div>
219 </div>
220 </td>
220 </td>
221 <td>
221 <td>
222 % if c.at_version_num != ver_pr:
222 % if c.at_version_num != ver_pr:
223 <i class="icon-comment"></i>
223 <i class="icon-comment"></i>
224 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
224 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
225 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
225 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
226 </code>
226 </code>
227 % endif
227 % endif
228 </td>
228 </td>
229 <td>
229 <td>
230 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
230 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
231 </td>
231 </td>
232 <td>
232 <td>
233 ${h.age_component(ver.updated_on, time_is_local=True)}
233 ${h.age_component(ver.updated_on, time_is_local=True)}
234 </td>
234 </td>
235 </tr>
235 </tr>
236 % endfor
236 % endfor
237
237
238 <tr>
238 <tr>
239 <td colspan="6">
239 <td colspan="6">
240 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
240 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
241 data-label-text-locked="${_('select versions to show changes')}"
241 data-label-text-locked="${_('select versions to show changes')}"
242 data-label-text-diff="${_('show changes between versions')}"
242 data-label-text-diff="${_('show changes between versions')}"
243 data-label-text-show="${_('show pull request for this version')}"
243 data-label-text-show="${_('show pull request for this version')}"
244 >
244 >
245 ${_('select versions to show changes')}
245 ${_('select versions to show changes')}
246 </button>
246 </button>
247 </td>
247 </td>
248 </tr>
248 </tr>
249
249
250 ## show comment/inline comments summary
250 ## show comment/inline comments summary
251 <%def name="comments_summary()">
251 <%def name="comments_summary()">
252 <tr>
252 <tr>
253 <td colspan="6" class="comments-summary-td">
253 <td colspan="6" class="comments-summary-td">
254
254
255 % if c.at_version:
255 % if c.at_version:
256 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
256 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
257 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
257 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
258 ${_('Comments at this version')}:
258 ${_('Comments at this version')}:
259 % else:
259 % else:
260 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
260 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
261 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
261 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
262 ${_('Comments for this pull request')}:
262 ${_('Comments for this pull request')}:
263 % endif
263 % endif
264
264
265
265
266 %if general_comm_count_ver:
266 %if general_comm_count_ver:
267 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
267 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
268 %else:
268 %else:
269 ${_("%d General ") % general_comm_count_ver}
269 ${_("%d General ") % general_comm_count_ver}
270 %endif
270 %endif
271
271
272 %if inline_comm_count_ver:
272 %if inline_comm_count_ver:
273 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
273 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
274 %else:
274 %else:
275 , ${_("%d Inline") % inline_comm_count_ver}
275 , ${_("%d Inline") % inline_comm_count_ver}
276 %endif
276 %endif
277
277
278 %if outdated_comm_count_ver:
278 %if outdated_comm_count_ver:
279 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
279 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
280 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
280 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
281 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
281 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
282 %else:
282 %else:
283 , ${_("%d Outdated") % outdated_comm_count_ver}
283 , ${_("%d Outdated") % outdated_comm_count_ver}
284 %endif
284 %endif
285 </td>
285 </td>
286 </tr>
286 </tr>
287 </%def>
287 </%def>
288 ${comments_summary()}
288 ${comments_summary()}
289 </table>
289 </table>
290 % else:
290 % else:
291 <div class="input">
291 <div class="input">
292 ${_('Pull request versions not available')}.
292 ${_('Pull request versions not available')}.
293 </div>
293 </div>
294 <div>
294 <div>
295 <table>
295 <table>
296 ${comments_summary()}
296 ${comments_summary()}
297 </table>
297 </table>
298 </div>
298 </div>
299 % endif
299 % endif
300 </div>
300 </div>
301 </div>
301 </div>
302
302
303 <div id="pr-save" class="field" style="display: none;">
303 <div id="pr-save" class="field" style="display: none;">
304 <div class="label-summary"></div>
304 <div class="label-summary"></div>
305 <div class="input">
305 <div class="input">
306 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
306 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
307 </div>
307 </div>
308 </div>
308 </div>
309 </div>
309 </div>
310 </div>
310 </div>
311 <div>
311 <div>
312 ## AUTHOR
312 ## AUTHOR
313 <div class="reviewers-title block-right">
313 <div class="reviewers-title block-right">
314 <div class="pr-details-title">
314 <div class="pr-details-title">
315 ${_('Author of this pull request')}
315 ${_('Author of this pull request')}
316 </div>
316 </div>
317 </div>
317 </div>
318 <div class="block-right pr-details-content reviewers">
318 <div class="block-right pr-details-content reviewers">
319 <ul class="group_members">
319 <ul class="group_members">
320 <li>
320 <li>
321 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
321 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
322 </li>
322 </li>
323 </ul>
323 </ul>
324 </div>
324 </div>
325
325
326 ## REVIEW RULES
326 ## REVIEW RULES
327 <div id="review_rules" style="display: none" class="reviewers-title block-right">
327 <div id="review_rules" style="display: none" class="reviewers-title block-right">
328 <div class="pr-details-title">
328 <div class="pr-details-title">
329 ${_('Reviewer rules')}
329 ${_('Reviewer rules')}
330 %if c.allowed_to_update:
330 %if c.allowed_to_update:
331 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
331 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
332 %endif
332 %endif
333 </div>
333 </div>
334 <div class="pr-reviewer-rules">
334 <div class="pr-reviewer-rules">
335 ## review rules will be appended here, by default reviewers logic
335 ## review rules will be appended here, by default reviewers logic
336 </div>
336 </div>
337 <input id="review_data" type="hidden" name="review_data" value="">
337 <input id="review_data" type="hidden" name="review_data" value="">
338 </div>
338 </div>
339
339
340 ## REVIEWERS
340 ## REVIEWERS
341 <div class="reviewers-title block-right">
341 <div class="reviewers-title block-right">
342 <div class="pr-details-title">
342 <div class="pr-details-title">
343 ${_('Pull request reviewers')}
343 ${_('Pull request reviewers')}
344 %if c.allowed_to_update:
344 %if c.allowed_to_update:
345 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
345 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
346 %endif
346 %endif
347 </div>
347 </div>
348 </div>
348 </div>
349 <div id="reviewers" class="block-right pr-details-content reviewers">
349 <div id="reviewers" class="block-right pr-details-content reviewers">
350
350
351 ## members redering block
351 ## members redering block
352 <input type="hidden" name="__start__" value="review_members:sequence">
352 <input type="hidden" name="__start__" value="review_members:sequence">
353 <ul id="review_members" class="group_members">
353 <ul id="review_members" class="group_members">
354
354
355 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
355 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
356 <script>
356 <script>
357 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
357 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
358 var status = "${(status[0][1].status if status else 'not_reviewed')}";
358 var status = "${(status[0][1].status if status else 'not_reviewed')}";
359 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
359 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
360 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
360 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
361
361
362 var entry = renderTemplate('reviewMemberEntry', {
362 var entry = renderTemplate('reviewMemberEntry', {
363 'member': member,
363 'member': member,
364 'mandatory': member.mandatory,
364 'mandatory': member.mandatory,
365 'reasons': member.reasons,
365 'reasons': member.reasons,
366 'allowed_to_update': allowed_to_update,
366 'allowed_to_update': allowed_to_update,
367 'review_status': status,
367 'review_status': status,
368 'review_status_label': status_lbl,
368 'review_status_label': status_lbl,
369 'user_group': member.user_group,
369 'user_group': member.user_group,
370 'create': false
370 'create': false
371 });
371 });
372 $('#review_members').append(entry)
372 $('#review_members').append(entry)
373 </script>
373 </script>
374
374
375 % endfor
375 % endfor
376
376
377 </ul>
377 </ul>
378 <input type="hidden" name="__end__" value="review_members:sequence">
378 <input type="hidden" name="__end__" value="review_members:sequence">
379 ## end members redering block
379 ## end members redering block
380
380
381 %if not c.pull_request.is_closed():
381 %if not c.pull_request.is_closed():
382 <div id="add_reviewer" class="ac" style="display: none;">
382 <div id="add_reviewer" class="ac" style="display: none;">
383 %if c.allowed_to_update:
383 %if c.allowed_to_update:
384 % if not c.forbid_adding_reviewers:
384 % if not c.forbid_adding_reviewers:
385 <div id="add_reviewer_input" class="reviewer_ac">
385 <div id="add_reviewer_input" class="reviewer_ac">
386 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
386 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
387 <div id="reviewers_container"></div>
387 <div id="reviewers_container"></div>
388 </div>
388 </div>
389 % endif
389 % endif
390 <div class="pull-right">
390 <div class="pull-right">
391 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
391 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
392 </div>
392 </div>
393 %endif
393 %endif
394 </div>
394 </div>
395 %endif
395 %endif
396 </div>
396 </div>
397 </div>
397 </div>
398 </div>
398 </div>
399 <div class="box">
399 <div class="box">
400 ##DIFF
400 ##DIFF
401 <div class="table" >
401 <div class="table" >
402 <div id="changeset_compare_view_content">
402 <div id="changeset_compare_view_content">
403 ##CS
403 ##CS
404 % if c.missing_requirements:
404 % if c.missing_requirements:
405 <div class="box">
405 <div class="box">
406 <div class="alert alert-warning">
406 <div class="alert alert-warning">
407 <div>
407 <div>
408 <strong>${_('Missing requirements:')}</strong>
408 <strong>${_('Missing requirements:')}</strong>
409 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
409 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
410 </div>
410 </div>
411 </div>
411 </div>
412 </div>
412 </div>
413 % elif c.missing_commits:
413 % elif c.missing_commits:
414 <div class="box">
414 <div class="box">
415 <div class="alert alert-warning">
415 <div class="alert alert-warning">
416 <div>
416 <div>
417 <strong>${_('Missing commits')}:</strong>
417 <strong>${_('Missing commits')}:</strong>
418 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
418 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
419 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
419 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
420 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
420 </div>
421 </div>
421 </div>
422 </div>
422 </div>
423 </div>
423 % endif
424 % endif
424
425
425 <div class="compare_view_commits_title">
426 <div class="compare_view_commits_title">
426 % if not c.compare_mode:
427 % if not c.compare_mode:
427
428
428 % if c.at_version_pos:
429 % if c.at_version_pos:
429 <h4>
430 <h4>
430 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
431 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
431 </h4>
432 </h4>
432 % endif
433 % endif
433
434
434 <div class="pull-left">
435 <div class="pull-left">
435 <div class="btn-group">
436 <div class="btn-group">
436 <a
437 <a
437 class="btn"
438 class="btn"
438 href="#"
439 href="#"
439 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
440 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
440 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
441 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
441 </a>
442 </a>
442 <a
443 <a
443 class="btn"
444 class="btn"
444 href="#"
445 href="#"
445 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
446 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
446 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
447 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
447 </a>
448 </a>
448 </div>
449 </div>
449 </div>
450 </div>
450
451
451 <div class="pull-right">
452 <div class="pull-right">
452 % if c.allowed_to_update and not c.pull_request.is_closed():
453 % if c.allowed_to_update and not c.pull_request.is_closed():
453 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
454 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
454 % else:
455 % else:
455 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
456 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
456 % endif
457 % endif
457
458
458 </div>
459 </div>
459 % endif
460 % endif
460 </div>
461 </div>
461
462
462 % if not c.missing_commits:
463 % if not c.missing_commits:
463 % if c.compare_mode:
464 % if c.compare_mode:
464 % if c.at_version:
465 % if c.at_version:
465 <h4>
466 <h4>
466 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
467 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
467 </h4>
468 </h4>
468
469
469 <div class="subtitle-compare">
470 <div class="subtitle-compare">
470 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
471 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
471 </div>
472 </div>
472
473
473 <div class="container">
474 <div class="container">
474 <table class="rctable compare_view_commits">
475 <table class="rctable compare_view_commits">
475 <tr>
476 <tr>
476 <th></th>
477 <th></th>
477 <th>${_('Time')}</th>
478 <th>${_('Time')}</th>
478 <th>${_('Author')}</th>
479 <th>${_('Author')}</th>
479 <th>${_('Commit')}</th>
480 <th>${_('Commit')}</th>
480 <th></th>
481 <th></th>
481 <th>${_('Description')}</th>
482 <th>${_('Description')}</th>
482 </tr>
483 </tr>
483
484
484 % for c_type, commit in c.commit_changes:
485 % for c_type, commit in c.commit_changes:
485 % if c_type in ['a', 'r']:
486 % if c_type in ['a', 'r']:
486 <%
487 <%
487 if c_type == 'a':
488 if c_type == 'a':
488 cc_title = _('Commit added in displayed changes')
489 cc_title = _('Commit added in displayed changes')
489 elif c_type == 'r':
490 elif c_type == 'r':
490 cc_title = _('Commit removed in displayed changes')
491 cc_title = _('Commit removed in displayed changes')
491 else:
492 else:
492 cc_title = ''
493 cc_title = ''
493 %>
494 %>
494 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
495 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
495 <td>
496 <td>
496 <div class="commit-change-indicator color-${c_type}-border">
497 <div class="commit-change-indicator color-${c_type}-border">
497 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
498 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
498 ${c_type.upper()}
499 ${c_type.upper()}
499 </div>
500 </div>
500 </div>
501 </div>
501 </td>
502 </td>
502 <td class="td-time">
503 <td class="td-time">
503 ${h.age_component(commit.date)}
504 ${h.age_component(commit.date)}
504 </td>
505 </td>
505 <td class="td-user">
506 <td class="td-user">
506 ${base.gravatar_with_user(commit.author, 16)}
507 ${base.gravatar_with_user(commit.author, 16)}
507 </td>
508 </td>
508 <td class="td-hash">
509 <td class="td-hash">
509 <code>
510 <code>
510 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
511 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
511 r${commit.revision}:${h.short_id(commit.raw_id)}
512 r${commit.revision}:${h.short_id(commit.raw_id)}
512 </a>
513 </a>
513 ${h.hidden('revisions', commit.raw_id)}
514 ${h.hidden('revisions', commit.raw_id)}
514 </code>
515 </code>
515 </td>
516 </td>
516 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
517 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
517 <div class="show_more_col">
518 <div class="show_more_col">
518 <i class="show_more"></i>
519 <i class="show_more"></i>
519 </div>
520 </div>
520 </td>
521 </td>
521 <td class="mid td-description">
522 <td class="mid td-description">
522 <div class="log-container truncate-wrap">
523 <div class="log-container truncate-wrap">
523 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
524 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
524 ${h.urlify_commit_message(commit.message, c.repo_name)}
525 ${h.urlify_commit_message(commit.message, c.repo_name)}
525 </div>
526 </div>
526 </div>
527 </div>
527 </td>
528 </td>
528 </tr>
529 </tr>
529 % endif
530 % endif
530 % endfor
531 % endfor
531 </table>
532 </table>
532 </div>
533 </div>
533
534
534 <script>
535 <script>
535 $('.expand_commit').on('click',function(e){
536 $('.expand_commit').on('click',function(e){
536 var target_expand = $(this);
537 var target_expand = $(this);
537 var cid = target_expand.data('commitId');
538 var cid = target_expand.data('commitId');
538
539
539 if (target_expand.hasClass('open')){
540 if (target_expand.hasClass('open')){
540 $('#c-'+cid).css({
541 $('#c-'+cid).css({
541 'height': '1.5em',
542 'height': '1.5em',
542 'white-space': 'nowrap',
543 'white-space': 'nowrap',
543 'text-overflow': 'ellipsis',
544 'text-overflow': 'ellipsis',
544 'overflow':'hidden'
545 'overflow':'hidden'
545 });
546 });
546 target_expand.removeClass('open');
547 target_expand.removeClass('open');
547 }
548 }
548 else {
549 else {
549 $('#c-'+cid).css({
550 $('#c-'+cid).css({
550 'height': 'auto',
551 'height': 'auto',
551 'white-space': 'pre-line',
552 'white-space': 'pre-line',
552 'text-overflow': 'initial',
553 'text-overflow': 'initial',
553 'overflow':'visible'
554 'overflow':'visible'
554 });
555 });
555 target_expand.addClass('open');
556 target_expand.addClass('open');
556 }
557 }
557 });
558 });
558 </script>
559 </script>
559
560
560 % endif
561 % endif
561
562
562 % else:
563 % else:
563 <%include file="/compare/compare_commits.mako" />
564 <%include file="/compare/compare_commits.mako" />
564 % endif
565 % endif
565
566
566 <div class="cs_files">
567 <div class="cs_files">
567 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
568 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
568 ${cbdiffs.render_diffset_menu()}
569 ${cbdiffs.render_diffset_menu()}
569 ${cbdiffs.render_diffset(
570 ${cbdiffs.render_diffset(
570 c.diffset, use_comments=True,
571 c.diffset, use_comments=True,
571 collapse_when_files_over=30,
572 collapse_when_files_over=30,
572 disable_new_comments=not c.allowed_to_comment,
573 disable_new_comments=not c.allowed_to_comment,
573 deleted_files_comments=c.deleted_files_comments,
574 deleted_files_comments=c.deleted_files_comments,
574 inline_comments=c.inline_comments)}
575 inline_comments=c.inline_comments)}
575 </div>
576 </div>
576 % else:
577 % else:
577 ## skipping commits we need to clear the view for missing commits
578 ## skipping commits we need to clear the view for missing commits
578 <div style="clear:both;"></div>
579 <div style="clear:both;"></div>
579 % endif
580 % endif
580
581
581 </div>
582 </div>
582 </div>
583 </div>
583
584
584 ## template for inline comment form
585 ## template for inline comment form
585 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
586 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
586
587
587 ## render general comments
588 ## render general comments
588
589
589 <div id="comment-tr-show">
590 <div id="comment-tr-show">
590 <div class="comment">
591 <div class="comment">
591 % if general_outdated_comm_count_ver:
592 % if general_outdated_comm_count_ver:
592 <div class="meta">
593 <div class="meta">
593 % if general_outdated_comm_count_ver == 1:
594 % if general_outdated_comm_count_ver == 1:
594 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
595 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
595 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
596 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
596 % else:
597 % else:
597 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
598 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
598 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
599 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
599 % endif
600 % endif
600 </div>
601 </div>
601 % endif
602 % endif
602 </div>
603 </div>
603 </div>
604 </div>
604
605
605 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
606 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
606
607
607 % if not c.pull_request.is_closed():
608 % if not c.pull_request.is_closed():
608 ## merge status, and merge action
609 ## merge status, and merge action
609 <div class="pull-request-merge">
610 <div class="pull-request-merge">
610 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
611 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
611 </div>
612 </div>
612
613
613 ## main comment form and it status
614 ## main comment form and it status
614 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
615 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
615 pull_request_id=c.pull_request.pull_request_id),
616 pull_request_id=c.pull_request.pull_request_id),
616 c.pull_request_review_status,
617 c.pull_request_review_status,
617 is_pull_request=True, change_status=c.allowed_to_change_status)}
618 is_pull_request=True, change_status=c.allowed_to_change_status)}
618 %endif
619 %endif
619
620
620 <script type="text/javascript">
621 <script type="text/javascript">
621 if (location.hash) {
622 if (location.hash) {
622 var result = splitDelimitedHash(location.hash);
623 var result = splitDelimitedHash(location.hash);
623 var line = $('html').find(result.loc);
624 var line = $('html').find(result.loc);
624 // show hidden comments if we use location.hash
625 // show hidden comments if we use location.hash
625 if (line.hasClass('comment-general')) {
626 if (line.hasClass('comment-general')) {
626 $(line).show();
627 $(line).show();
627 } else if (line.hasClass('comment-inline')) {
628 } else if (line.hasClass('comment-inline')) {
628 $(line).show();
629 $(line).show();
629 var $cb = $(line).closest('.cb');
630 var $cb = $(line).closest('.cb');
630 $cb.removeClass('cb-collapsed')
631 $cb.removeClass('cb-collapsed')
631 }
632 }
632 if (line.length > 0){
633 if (line.length > 0){
633 offsetScroll(line, 70);
634 offsetScroll(line, 70);
634 }
635 }
635 }
636 }
636
637
637 versionController = new VersionController();
638 versionController = new VersionController();
638 versionController.init();
639 versionController.init();
639
640
640 reviewersController = new ReviewersController();
641 reviewersController = new ReviewersController();
641
642
642 $(function(){
643 $(function(){
643
644
644 // custom code mirror
645 // custom code mirror
645 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
646 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
646
647
647 var PRDetails = {
648 var PRDetails = {
648 editButton: $('#open_edit_pullrequest'),
649 editButton: $('#open_edit_pullrequest'),
649 closeButton: $('#close_edit_pullrequest'),
650 closeButton: $('#close_edit_pullrequest'),
650 deleteButton: $('#delete_pullrequest'),
651 deleteButton: $('#delete_pullrequest'),
651 viewFields: $('#pr-desc, #pr-title'),
652 viewFields: $('#pr-desc, #pr-title'),
652 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
653 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
653
654
654 init: function() {
655 init: function() {
655 var that = this;
656 var that = this;
656 this.editButton.on('click', function(e) { that.edit(); });
657 this.editButton.on('click', function(e) { that.edit(); });
657 this.closeButton.on('click', function(e) { that.view(); });
658 this.closeButton.on('click', function(e) { that.view(); });
658 },
659 },
659
660
660 edit: function(event) {
661 edit: function(event) {
661 this.viewFields.hide();
662 this.viewFields.hide();
662 this.editButton.hide();
663 this.editButton.hide();
663 this.deleteButton.hide();
664 this.deleteButton.hide();
664 this.closeButton.show();
665 this.closeButton.show();
665 this.editFields.show();
666 this.editFields.show();
666 codeMirrorInstance.refresh();
667 codeMirrorInstance.refresh();
667 },
668 },
668
669
669 view: function(event) {
670 view: function(event) {
670 this.editButton.show();
671 this.editButton.show();
671 this.deleteButton.show();
672 this.deleteButton.show();
672 this.editFields.hide();
673 this.editFields.hide();
673 this.closeButton.hide();
674 this.closeButton.hide();
674 this.viewFields.show();
675 this.viewFields.show();
675 }
676 }
676 };
677 };
677
678
678 var ReviewersPanel = {
679 var ReviewersPanel = {
679 editButton: $('#open_edit_reviewers'),
680 editButton: $('#open_edit_reviewers'),
680 closeButton: $('#close_edit_reviewers'),
681 closeButton: $('#close_edit_reviewers'),
681 addButton: $('#add_reviewer'),
682 addButton: $('#add_reviewer'),
682 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
683 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
683
684
684 init: function() {
685 init: function() {
685 var self = this;
686 var self = this;
686 this.editButton.on('click', function(e) { self.edit(); });
687 this.editButton.on('click', function(e) { self.edit(); });
687 this.closeButton.on('click', function(e) { self.close(); });
688 this.closeButton.on('click', function(e) { self.close(); });
688 },
689 },
689
690
690 edit: function(event) {
691 edit: function(event) {
691 this.editButton.hide();
692 this.editButton.hide();
692 this.closeButton.show();
693 this.closeButton.show();
693 this.addButton.show();
694 this.addButton.show();
694 this.removeButtons.css('visibility', 'visible');
695 this.removeButtons.css('visibility', 'visible');
695 // review rules
696 // review rules
696 reviewersController.loadReviewRules(
697 reviewersController.loadReviewRules(
697 ${c.pull_request.reviewer_data_json | n});
698 ${c.pull_request.reviewer_data_json | n});
698 },
699 },
699
700
700 close: function(event) {
701 close: function(event) {
701 this.editButton.show();
702 this.editButton.show();
702 this.closeButton.hide();
703 this.closeButton.hide();
703 this.addButton.hide();
704 this.addButton.hide();
704 this.removeButtons.css('visibility', 'hidden');
705 this.removeButtons.css('visibility', 'hidden');
705 // hide review rules
706 // hide review rules
706 reviewersController.hideReviewRules()
707 reviewersController.hideReviewRules()
707 }
708 }
708 };
709 };
709
710
710 PRDetails.init();
711 PRDetails.init();
711 ReviewersPanel.init();
712 ReviewersPanel.init();
712
713
713 showOutdated = function(self){
714 showOutdated = function(self){
714 $('.comment-inline.comment-outdated').show();
715 $('.comment-inline.comment-outdated').show();
715 $('.filediff-outdated').show();
716 $('.filediff-outdated').show();
716 $('.showOutdatedComments').hide();
717 $('.showOutdatedComments').hide();
717 $('.hideOutdatedComments').show();
718 $('.hideOutdatedComments').show();
718 };
719 };
719
720
720 hideOutdated = function(self){
721 hideOutdated = function(self){
721 $('.comment-inline.comment-outdated').hide();
722 $('.comment-inline.comment-outdated').hide();
722 $('.filediff-outdated').hide();
723 $('.filediff-outdated').hide();
723 $('.hideOutdatedComments').hide();
724 $('.hideOutdatedComments').hide();
724 $('.showOutdatedComments').show();
725 $('.showOutdatedComments').show();
725 };
726 };
726
727
727 refreshMergeChecks = function(){
728 refreshMergeChecks = function(){
728 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
729 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
729 $('.pull-request-merge').css('opacity', 0.3);
730 $('.pull-request-merge').css('opacity', 0.3);
730 $('.action-buttons-extra').css('opacity', 0.3);
731 $('.action-buttons-extra').css('opacity', 0.3);
731
732
732 $('.pull-request-merge').load(
733 $('.pull-request-merge').load(
733 loadUrl, function() {
734 loadUrl, function() {
734 $('.pull-request-merge').css('opacity', 1);
735 $('.pull-request-merge').css('opacity', 1);
735
736
736 $('.action-buttons-extra').css('opacity', 1);
737 $('.action-buttons-extra').css('opacity', 1);
737 injectCloseAction();
738 injectCloseAction();
738 }
739 }
739 );
740 );
740 };
741 };
741
742
742 injectCloseAction = function() {
743 injectCloseAction = function() {
743 var closeAction = $('#close-pull-request-action').html();
744 var closeAction = $('#close-pull-request-action').html();
744 var $actionButtons = $('.action-buttons-extra');
745 var $actionButtons = $('.action-buttons-extra');
745 // clear the action before
746 // clear the action before
746 $actionButtons.html("");
747 $actionButtons.html("");
747 $actionButtons.html(closeAction);
748 $actionButtons.html(closeAction);
748 };
749 };
749
750
750 closePullRequest = function (status) {
751 closePullRequest = function (status) {
751 // inject closing flag
752 // inject closing flag
752 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
753 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
753 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
754 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
754 $(generalCommentForm.submitForm).submit();
755 $(generalCommentForm.submitForm).submit();
755 };
756 };
756
757
757 $('#show-outdated-comments').on('click', function(e){
758 $('#show-outdated-comments').on('click', function(e){
758 var button = $(this);
759 var button = $(this);
759 var outdated = $('.comment-outdated');
760 var outdated = $('.comment-outdated');
760
761
761 if (button.html() === "(Show)") {
762 if (button.html() === "(Show)") {
762 button.html("(Hide)");
763 button.html("(Hide)");
763 outdated.show();
764 outdated.show();
764 } else {
765 } else {
765 button.html("(Show)");
766 button.html("(Show)");
766 outdated.hide();
767 outdated.hide();
767 }
768 }
768 });
769 });
769
770
770 $('.show-inline-comments').on('change', function(e){
771 $('.show-inline-comments').on('change', function(e){
771 var show = 'none';
772 var show = 'none';
772 var target = e.currentTarget;
773 var target = e.currentTarget;
773 if(target.checked){
774 if(target.checked){
774 show = ''
775 show = ''
775 }
776 }
776 var boxid = $(target).attr('id_for');
777 var boxid = $(target).attr('id_for');
777 var comments = $('#{0} .inline-comments'.format(boxid));
778 var comments = $('#{0} .inline-comments'.format(boxid));
778 var fn_display = function(idx){
779 var fn_display = function(idx){
779 $(this).css('display', show);
780 $(this).css('display', show);
780 };
781 };
781 $(comments).each(fn_display);
782 $(comments).each(fn_display);
782 var btns = $('#{0} .inline-comments-button'.format(boxid));
783 var btns = $('#{0} .inline-comments-button'.format(boxid));
783 $(btns).each(fn_display);
784 $(btns).each(fn_display);
784 });
785 });
785
786
786 $('#merge_pull_request_form').submit(function() {
787 $('#merge_pull_request_form').submit(function() {
787 if (!$('#merge_pull_request').attr('disabled')) {
788 if (!$('#merge_pull_request').attr('disabled')) {
788 $('#merge_pull_request').attr('disabled', 'disabled');
789 $('#merge_pull_request').attr('disabled', 'disabled');
789 }
790 }
790 return true;
791 return true;
791 });
792 });
792
793
793 $('#edit_pull_request').on('click', function(e){
794 $('#edit_pull_request').on('click', function(e){
794 var title = $('#pr-title-input').val();
795 var title = $('#pr-title-input').val();
795 var description = codeMirrorInstance.getValue();
796 var description = codeMirrorInstance.getValue();
796 editPullRequest(
797 editPullRequest(
797 "${c.repo_name}", "${c.pull_request.pull_request_id}",
798 "${c.repo_name}", "${c.pull_request.pull_request_id}",
798 title, description);
799 title, description);
799 });
800 });
800
801
801 $('#update_pull_request').on('click', function(e){
802 $('#update_pull_request').on('click', function(e){
802 $(this).attr('disabled', 'disabled');
803 $(this).attr('disabled', 'disabled');
803 $(this).addClass('disabled');
804 $(this).addClass('disabled');
804 $(this).html(_gettext('Saving...'));
805 $(this).html(_gettext('Saving...'));
805 reviewersController.updateReviewers(
806 reviewersController.updateReviewers(
806 "${c.repo_name}", "${c.pull_request.pull_request_id}");
807 "${c.repo_name}", "${c.pull_request.pull_request_id}");
807 });
808 });
808
809
809 $('#update_commits').on('click', function(e){
810 $('#update_commits').on('click', function(e){
810 var isDisabled = !$(e.currentTarget).attr('disabled');
811 var isDisabled = !$(e.currentTarget).attr('disabled');
811 $(e.currentTarget).attr('disabled', 'disabled');
812 $(e.currentTarget).attr('disabled', 'disabled');
812 $(e.currentTarget).addClass('disabled');
813 $(e.currentTarget).addClass('disabled');
813 $(e.currentTarget).removeClass('btn-primary');
814 $(e.currentTarget).removeClass('btn-primary');
814 $(e.currentTarget).text(_gettext('Updating...'));
815 $(e.currentTarget).text(_gettext('Updating...'));
815 if(isDisabled){
816 if(isDisabled){
816 updateCommits(
817 updateCommits(
817 "${c.repo_name}", "${c.pull_request.pull_request_id}");
818 "${c.repo_name}", "${c.pull_request.pull_request_id}");
818 }
819 }
819 });
820 });
820 // fixing issue with caches on firefox
821 // fixing issue with caches on firefox
821 $('#update_commits').removeAttr("disabled");
822 $('#update_commits').removeAttr("disabled");
822
823
823 $('.show-inline-comments').on('click', function(e){
824 $('.show-inline-comments').on('click', function(e){
824 var boxid = $(this).attr('data-comment-id');
825 var boxid = $(this).attr('data-comment-id');
825 var button = $(this);
826 var button = $(this);
826
827
827 if(button.hasClass("comments-visible")) {
828 if(button.hasClass("comments-visible")) {
828 $('#{0} .inline-comments'.format(boxid)).each(function(index){
829 $('#{0} .inline-comments'.format(boxid)).each(function(index){
829 $(this).hide();
830 $(this).hide();
830 });
831 });
831 button.removeClass("comments-visible");
832 button.removeClass("comments-visible");
832 } else {
833 } else {
833 $('#{0} .inline-comments'.format(boxid)).each(function(index){
834 $('#{0} .inline-comments'.format(boxid)).each(function(index){
834 $(this).show();
835 $(this).show();
835 });
836 });
836 button.addClass("comments-visible");
837 button.addClass("comments-visible");
837 }
838 }
838 });
839 });
839
840
840 // register submit callback on commentForm form to track TODOs
841 // register submit callback on commentForm form to track TODOs
841 window.commentFormGlobalSubmitSuccessCallback = function(){
842 window.commentFormGlobalSubmitSuccessCallback = function(){
842 refreshMergeChecks();
843 refreshMergeChecks();
843 };
844 };
844 // initial injection
845 // initial injection
845 injectCloseAction();
846 injectCloseAction();
846
847
847 ReviewerAutoComplete('#user');
848 ReviewerAutoComplete('#user');
848
849
849 })
850 })
850 </script>
851 </script>
851
852
852 </div>
853 </div>
853 </div>
854 </div>
854
855
855 </%def>
856 </%def>
General Comments 0
You need to be logged in to leave comments. Login now