##// END OF EJS Templates
pull-requests: moved the delete logic into the show view....
marcink -
r1085:a6c56473 default
parent child Browse files
Show More
@@ -1,887 +1,889 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24
24
25 import peppercorn
25 import peppercorn
26 import formencode
26 import formencode
27 import logging
27 import logging
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from sqlalchemy.sql import func
34 from sqlalchemy.sql import func
35 from sqlalchemy.sql.expression import or_
35 from sqlalchemy.sql.expression import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import auth, diffs, helpers as h
38 from rhodecode.lib import auth, diffs, helpers as h
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.base import (
40 from rhodecode.lib.base import (
41 BaseRepoController, render, vcs_operation_context)
41 BaseRepoController, render, vcs_operation_context)
42 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 HasAcceptedRepoType, XHRRequired)
44 HasAcceptedRepoType, XHRRequired)
45 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.utils import jsonify
46 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
48 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
49 from rhodecode.lib.vcs.exceptions import (
49 from rhodecode.lib.vcs.exceptions import (
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
51 from rhodecode.lib.diffs import LimitedDiffContainer
51 from rhodecode.lib.diffs import LimitedDiffContainer
52 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.comment import ChangesetCommentsModel
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
55 Repository
55 Repository
56 from rhodecode.model.forms import PullRequestForm
56 from rhodecode.model.forms import PullRequestForm
57 from rhodecode.model.meta import Session
57 from rhodecode.model.meta import Session
58 from rhodecode.model.pull_request import PullRequestModel
58 from rhodecode.model.pull_request import PullRequestModel
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class PullrequestsController(BaseRepoController):
63 class PullrequestsController(BaseRepoController):
64 def __before__(self):
64 def __before__(self):
65 super(PullrequestsController, self).__before__()
65 super(PullrequestsController, self).__before__()
66
66
67 def _load_compare_data(self, pull_request, enable_comments=True):
67 def _load_compare_data(self, pull_request, enable_comments=True):
68 """
68 """
69 Load context data needed for generating compare diff
69 Load context data needed for generating compare diff
70
70
71 :param pull_request: object related to the request
71 :param pull_request: object related to the request
72 :param enable_comments: flag to determine if comments are included
72 :param enable_comments: flag to determine if comments are included
73 """
73 """
74 source_repo = pull_request.source_repo
74 source_repo = pull_request.source_repo
75 source_ref_id = pull_request.source_ref_parts.commit_id
75 source_ref_id = pull_request.source_ref_parts.commit_id
76
76
77 target_repo = pull_request.target_repo
77 target_repo = pull_request.target_repo
78 target_ref_id = pull_request.target_ref_parts.commit_id
78 target_ref_id = pull_request.target_ref_parts.commit_id
79
79
80 # despite opening commits for bookmarks/branches/tags, we always
80 # despite opening commits for bookmarks/branches/tags, we always
81 # convert this to rev to prevent changes after bookmark or branch change
81 # convert this to rev to prevent changes after bookmark or branch change
82 c.source_ref_type = 'rev'
82 c.source_ref_type = 'rev'
83 c.source_ref = source_ref_id
83 c.source_ref = source_ref_id
84
84
85 c.target_ref_type = 'rev'
85 c.target_ref_type = 'rev'
86 c.target_ref = target_ref_id
86 c.target_ref = target_ref_id
87
87
88 c.source_repo = source_repo
88 c.source_repo = source_repo
89 c.target_repo = target_repo
89 c.target_repo = target_repo
90
90
91 c.fulldiff = bool(request.GET.get('fulldiff'))
91 c.fulldiff = bool(request.GET.get('fulldiff'))
92
92
93 # diff_limit is the old behavior, will cut off the whole diff
93 # diff_limit is the old behavior, will cut off the whole diff
94 # if the limit is applied otherwise will just hide the
94 # if the limit is applied otherwise will just hide the
95 # big files from the front-end
95 # big files from the front-end
96 diff_limit = self.cut_off_limit_diff
96 diff_limit = self.cut_off_limit_diff
97 file_limit = self.cut_off_limit_file
97 file_limit = self.cut_off_limit_file
98
98
99 pre_load = ["author", "branch", "date", "message"]
99 pre_load = ["author", "branch", "date", "message"]
100
100
101 c.commit_ranges = []
101 c.commit_ranges = []
102 source_commit = EmptyCommit()
102 source_commit = EmptyCommit()
103 target_commit = EmptyCommit()
103 target_commit = EmptyCommit()
104 c.missing_requirements = False
104 c.missing_requirements = False
105 try:
105 try:
106 c.commit_ranges = [
106 c.commit_ranges = [
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
108 for rev in pull_request.revisions]
108 for rev in pull_request.revisions]
109
109
110 c.statuses = source_repo.statuses(
110 c.statuses = source_repo.statuses(
111 [x.raw_id for x in c.commit_ranges])
111 [x.raw_id for x in c.commit_ranges])
112
112
113 target_commit = source_repo.get_commit(
113 target_commit = source_repo.get_commit(
114 commit_id=safe_str(target_ref_id))
114 commit_id=safe_str(target_ref_id))
115 source_commit = source_repo.get_commit(
115 source_commit = source_repo.get_commit(
116 commit_id=safe_str(source_ref_id))
116 commit_id=safe_str(source_ref_id))
117 except RepositoryRequirementError:
117 except RepositoryRequirementError:
118 c.missing_requirements = True
118 c.missing_requirements = True
119
119
120 c.missing_commits = False
120 c.missing_commits = False
121 if (c.missing_requirements or
121 if (c.missing_requirements or
122 isinstance(source_commit, EmptyCommit) or
122 isinstance(source_commit, EmptyCommit) or
123 source_commit == target_commit):
123 source_commit == target_commit):
124 _parsed = []
124 _parsed = []
125 c.missing_commits = True
125 c.missing_commits = True
126 else:
126 else:
127 vcs_diff = PullRequestModel().get_diff(pull_request)
127 vcs_diff = PullRequestModel().get_diff(pull_request)
128 diff_processor = diffs.DiffProcessor(
128 diff_processor = diffs.DiffProcessor(
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
130 file_limit=file_limit, show_full_diff=c.fulldiff)
130 file_limit=file_limit, show_full_diff=c.fulldiff)
131 _parsed = diff_processor.prepare()
131 _parsed = diff_processor.prepare()
132
132
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
134
134
135 c.files = []
135 c.files = []
136 c.changes = {}
136 c.changes = {}
137 c.lines_added = 0
137 c.lines_added = 0
138 c.lines_deleted = 0
138 c.lines_deleted = 0
139 c.included_files = []
139 c.included_files = []
140 c.deleted_files = []
140 c.deleted_files = []
141
141
142 for f in _parsed:
142 for f in _parsed:
143 st = f['stats']
143 st = f['stats']
144 c.lines_added += st['added']
144 c.lines_added += st['added']
145 c.lines_deleted += st['deleted']
145 c.lines_deleted += st['deleted']
146
146
147 fid = h.FID('', f['filename'])
147 fid = h.FID('', f['filename'])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
149 c.included_files.append(f['filename'])
149 c.included_files.append(f['filename'])
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
151 parsed_lines=[f])
151 parsed_lines=[f])
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
153
153
154 def _extract_ordering(self, request):
154 def _extract_ordering(self, request):
155 column_index = safe_int(request.GET.get('order[0][column]'))
155 column_index = safe_int(request.GET.get('order[0][column]'))
156 order_dir = request.GET.get('order[0][dir]', 'desc')
156 order_dir = request.GET.get('order[0][dir]', 'desc')
157 order_by = request.GET.get(
157 order_by = request.GET.get(
158 'columns[%s][data][sort]' % column_index, 'name_raw')
158 'columns[%s][data][sort]' % column_index, 'name_raw')
159 return order_by, order_dir
159 return order_by, order_dir
160
160
161 @LoginRequired()
161 @LoginRequired()
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 'repository.admin')
163 'repository.admin')
164 @HasAcceptedRepoType('git', 'hg')
164 @HasAcceptedRepoType('git', 'hg')
165 def show_all(self, repo_name):
165 def show_all(self, repo_name):
166 # filter types
166 # filter types
167 c.active = 'open'
167 c.active = 'open'
168 c.source = str2bool(request.GET.get('source'))
168 c.source = str2bool(request.GET.get('source'))
169 c.closed = str2bool(request.GET.get('closed'))
169 c.closed = str2bool(request.GET.get('closed'))
170 c.my = str2bool(request.GET.get('my'))
170 c.my = str2bool(request.GET.get('my'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
173 c.repo_name = repo_name
173 c.repo_name = repo_name
174
174
175 opened_by = None
175 opened_by = None
176 if c.my:
176 if c.my:
177 c.active = 'my'
177 c.active = 'my'
178 opened_by = [c.rhodecode_user.user_id]
178 opened_by = [c.rhodecode_user.user_id]
179
179
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
181 if c.closed:
181 if c.closed:
182 c.active = 'closed'
182 c.active = 'closed'
183 statuses = [PullRequest.STATUS_CLOSED]
183 statuses = [PullRequest.STATUS_CLOSED]
184
184
185 if c.awaiting_review and not c.source:
185 if c.awaiting_review and not c.source:
186 c.active = 'awaiting'
186 c.active = 'awaiting'
187 if c.source and not c.awaiting_review:
187 if c.source and not c.awaiting_review:
188 c.active = 'source'
188 c.active = 'source'
189 if c.awaiting_my_review:
189 if c.awaiting_my_review:
190 c.active = 'awaiting_my'
190 c.active = 'awaiting_my'
191
191
192 data = self._get_pull_requests_list(
192 data = self._get_pull_requests_list(
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
194 if not request.is_xhr:
194 if not request.is_xhr:
195 c.data = json.dumps(data['data'])
195 c.data = json.dumps(data['data'])
196 c.records_total = data['recordsTotal']
196 c.records_total = data['recordsTotal']
197 return render('/pullrequests/pullrequests.html')
197 return render('/pullrequests/pullrequests.html')
198 else:
198 else:
199 return json.dumps(data)
199 return json.dumps(data)
200
200
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
202 # pagination
202 # pagination
203 start = safe_int(request.GET.get('start'), 0)
203 start = safe_int(request.GET.get('start'), 0)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
205 order_by, order_dir = self._extract_ordering(request)
205 order_by, order_dir = self._extract_ordering(request)
206
206
207 if c.awaiting_review:
207 if c.awaiting_review:
208 pull_requests = PullRequestModel().get_awaiting_review(
208 pull_requests = PullRequestModel().get_awaiting_review(
209 repo_name, source=c.source, opened_by=opened_by,
209 repo_name, source=c.source, opened_by=opened_by,
210 statuses=statuses, offset=start, length=length,
210 statuses=statuses, offset=start, length=length,
211 order_by=order_by, order_dir=order_dir)
211 order_by=order_by, order_dir=order_dir)
212 pull_requests_total_count = PullRequestModel(
212 pull_requests_total_count = PullRequestModel(
213 ).count_awaiting_review(
213 ).count_awaiting_review(
214 repo_name, source=c.source, statuses=statuses,
214 repo_name, source=c.source, statuses=statuses,
215 opened_by=opened_by)
215 opened_by=opened_by)
216 elif c.awaiting_my_review:
216 elif c.awaiting_my_review:
217 pull_requests = PullRequestModel().get_awaiting_my_review(
217 pull_requests = PullRequestModel().get_awaiting_my_review(
218 repo_name, source=c.source, opened_by=opened_by,
218 repo_name, source=c.source, opened_by=opened_by,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
220 offset=start, length=length, order_by=order_by,
220 offset=start, length=length, order_by=order_by,
221 order_dir=order_dir)
221 order_dir=order_dir)
222 pull_requests_total_count = PullRequestModel(
222 pull_requests_total_count = PullRequestModel(
223 ).count_awaiting_my_review(
223 ).count_awaiting_my_review(
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
225 statuses=statuses, opened_by=opened_by)
225 statuses=statuses, opened_by=opened_by)
226 else:
226 else:
227 pull_requests = PullRequestModel().get_all(
227 pull_requests = PullRequestModel().get_all(
228 repo_name, source=c.source, opened_by=opened_by,
228 repo_name, source=c.source, opened_by=opened_by,
229 statuses=statuses, offset=start, length=length,
229 statuses=statuses, offset=start, length=length,
230 order_by=order_by, order_dir=order_dir)
230 order_by=order_by, order_dir=order_dir)
231 pull_requests_total_count = PullRequestModel().count_all(
231 pull_requests_total_count = PullRequestModel().count_all(
232 repo_name, source=c.source, statuses=statuses,
232 repo_name, source=c.source, statuses=statuses,
233 opened_by=opened_by)
233 opened_by=opened_by)
234
234
235 from rhodecode.lib.utils import PartialRenderer
235 from rhodecode.lib.utils import PartialRenderer
236 _render = PartialRenderer('data_table/_dt_elements.html')
236 _render = PartialRenderer('data_table/_dt_elements.html')
237 data = []
237 data = []
238 for pr in pull_requests:
238 for pr in pull_requests:
239 comments = ChangesetCommentsModel().get_all_comments(
239 comments = ChangesetCommentsModel().get_all_comments(
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
241
241
242 data.append({
242 data.append({
243 'name': _render('pullrequest_name',
243 'name': _render('pullrequest_name',
244 pr.pull_request_id, pr.target_repo.repo_name),
244 pr.pull_request_id, pr.target_repo.repo_name),
245 'name_raw': pr.pull_request_id,
245 'name_raw': pr.pull_request_id,
246 'status': _render('pullrequest_status',
246 'status': _render('pullrequest_status',
247 pr.calculated_review_status()),
247 pr.calculated_review_status()),
248 'title': _render(
248 'title': _render(
249 'pullrequest_title', pr.title, pr.description),
249 'pullrequest_title', pr.title, pr.description),
250 'description': h.escape(pr.description),
250 'description': h.escape(pr.description),
251 'updated_on': _render('pullrequest_updated_on',
251 'updated_on': _render('pullrequest_updated_on',
252 h.datetime_to_time(pr.updated_on)),
252 h.datetime_to_time(pr.updated_on)),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
254 'created_on': _render('pullrequest_updated_on',
254 'created_on': _render('pullrequest_updated_on',
255 h.datetime_to_time(pr.created_on)),
255 h.datetime_to_time(pr.created_on)),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
257 'author': _render('pullrequest_author',
257 'author': _render('pullrequest_author',
258 pr.author.full_contact, ),
258 pr.author.full_contact, ),
259 'author_raw': pr.author.full_name,
259 'author_raw': pr.author.full_name,
260 'comments': _render('pullrequest_comments', len(comments)),
260 'comments': _render('pullrequest_comments', len(comments)),
261 'comments_raw': len(comments),
261 'comments_raw': len(comments),
262 'closed': pr.is_closed(),
262 'closed': pr.is_closed(),
263 })
263 })
264 # json used to render the grid
264 # json used to render the grid
265 data = ({
265 data = ({
266 'data': data,
266 'data': data,
267 'recordsTotal': pull_requests_total_count,
267 'recordsTotal': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
269 })
269 })
270 return data
270 return data
271
271
272 @LoginRequired()
272 @LoginRequired()
273 @NotAnonymous()
273 @NotAnonymous()
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
275 'repository.admin')
275 'repository.admin')
276 @HasAcceptedRepoType('git', 'hg')
276 @HasAcceptedRepoType('git', 'hg')
277 def index(self):
277 def index(self):
278 source_repo = c.rhodecode_db_repo
278 source_repo = c.rhodecode_db_repo
279
279
280 try:
280 try:
281 source_repo.scm_instance().get_commit()
281 source_repo.scm_instance().get_commit()
282 except EmptyRepositoryError:
282 except EmptyRepositoryError:
283 h.flash(h.literal(_('There are no commits yet')),
283 h.flash(h.literal(_('There are no commits yet')),
284 category='warning')
284 category='warning')
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
286
286
287 commit_id = request.GET.get('commit')
287 commit_id = request.GET.get('commit')
288 branch_ref = request.GET.get('branch')
288 branch_ref = request.GET.get('branch')
289 bookmark_ref = request.GET.get('bookmark')
289 bookmark_ref = request.GET.get('bookmark')
290
290
291 try:
291 try:
292 source_repo_data = PullRequestModel().generate_repo_data(
292 source_repo_data = PullRequestModel().generate_repo_data(
293 source_repo, commit_id=commit_id,
293 source_repo, commit_id=commit_id,
294 branch=branch_ref, bookmark=bookmark_ref)
294 branch=branch_ref, bookmark=bookmark_ref)
295 except CommitDoesNotExistError as e:
295 except CommitDoesNotExistError as e:
296 log.exception(e)
296 log.exception(e)
297 h.flash(_('Commit does not exist'), 'error')
297 h.flash(_('Commit does not exist'), 'error')
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
299
299
300 default_target_repo = source_repo
300 default_target_repo = source_repo
301
301
302 if source_repo.parent:
302 if source_repo.parent:
303 parent_vcs_obj = source_repo.parent.scm_instance()
303 parent_vcs_obj = source_repo.parent.scm_instance()
304 if parent_vcs_obj and not parent_vcs_obj.is_empty():
304 if parent_vcs_obj and not parent_vcs_obj.is_empty():
305 # change default if we have a parent repo
305 # change default if we have a parent repo
306 default_target_repo = source_repo.parent
306 default_target_repo = source_repo.parent
307
307
308 target_repo_data = PullRequestModel().generate_repo_data(
308 target_repo_data = PullRequestModel().generate_repo_data(
309 default_target_repo)
309 default_target_repo)
310
310
311 selected_source_ref = source_repo_data['refs']['selected_ref']
311 selected_source_ref = source_repo_data['refs']['selected_ref']
312
312
313 title_source_ref = selected_source_ref.split(':', 2)[1]
313 title_source_ref = selected_source_ref.split(':', 2)[1]
314 c.default_title = PullRequestModel().generate_pullrequest_title(
314 c.default_title = PullRequestModel().generate_pullrequest_title(
315 source=source_repo.repo_name,
315 source=source_repo.repo_name,
316 source_ref=title_source_ref,
316 source_ref=title_source_ref,
317 target=default_target_repo.repo_name
317 target=default_target_repo.repo_name
318 )
318 )
319
319
320 c.default_repo_data = {
320 c.default_repo_data = {
321 'source_repo_name': source_repo.repo_name,
321 'source_repo_name': source_repo.repo_name,
322 'source_refs_json': json.dumps(source_repo_data),
322 'source_refs_json': json.dumps(source_repo_data),
323 'target_repo_name': default_target_repo.repo_name,
323 'target_repo_name': default_target_repo.repo_name,
324 'target_refs_json': json.dumps(target_repo_data),
324 'target_refs_json': json.dumps(target_repo_data),
325 }
325 }
326 c.default_source_ref = selected_source_ref
326 c.default_source_ref = selected_source_ref
327
327
328 return render('/pullrequests/pullrequest.html')
328 return render('/pullrequests/pullrequest.html')
329
329
330 @LoginRequired()
330 @LoginRequired()
331 @NotAnonymous()
331 @NotAnonymous()
332 @XHRRequired()
332 @XHRRequired()
333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 'repository.admin')
334 'repository.admin')
335 @jsonify
335 @jsonify
336 def get_repo_refs(self, repo_name, target_repo_name):
336 def get_repo_refs(self, repo_name, target_repo_name):
337 repo = Repository.get_by_repo_name(target_repo_name)
337 repo = Repository.get_by_repo_name(target_repo_name)
338 if not repo:
338 if not repo:
339 raise HTTPNotFound
339 raise HTTPNotFound
340 return PullRequestModel().generate_repo_data(repo)
340 return PullRequestModel().generate_repo_data(repo)
341
341
342 @LoginRequired()
342 @LoginRequired()
343 @NotAnonymous()
343 @NotAnonymous()
344 @XHRRequired()
344 @XHRRequired()
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
346 'repository.admin')
346 'repository.admin')
347 @jsonify
347 @jsonify
348 def get_repo_destinations(self, repo_name):
348 def get_repo_destinations(self, repo_name):
349 repo = Repository.get_by_repo_name(repo_name)
349 repo = Repository.get_by_repo_name(repo_name)
350 if not repo:
350 if not repo:
351 raise HTTPNotFound
351 raise HTTPNotFound
352 filter_query = request.GET.get('query')
352 filter_query = request.GET.get('query')
353
353
354 query = Repository.query() \
354 query = Repository.query() \
355 .order_by(func.length(Repository.repo_name)) \
355 .order_by(func.length(Repository.repo_name)) \
356 .filter(or_(
356 .filter(or_(
357 Repository.repo_name == repo.repo_name,
357 Repository.repo_name == repo.repo_name,
358 Repository.fork_id == repo.repo_id))
358 Repository.fork_id == repo.repo_id))
359
359
360 if filter_query:
360 if filter_query:
361 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
361 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
362 query = query.filter(
362 query = query.filter(
363 Repository.repo_name.ilike(ilike_expression))
363 Repository.repo_name.ilike(ilike_expression))
364
364
365 add_parent = False
365 add_parent = False
366 if repo.parent:
366 if repo.parent:
367 if filter_query in repo.parent.repo_name:
367 if filter_query in repo.parent.repo_name:
368 parent_vcs_obj = repo.parent.scm_instance()
368 parent_vcs_obj = repo.parent.scm_instance()
369 if parent_vcs_obj and not parent_vcs_obj.is_empty():
369 if parent_vcs_obj and not parent_vcs_obj.is_empty():
370 add_parent = True
370 add_parent = True
371
371
372 limit = 20 - 1 if add_parent else 20
372 limit = 20 - 1 if add_parent else 20
373 all_repos = query.limit(limit).all()
373 all_repos = query.limit(limit).all()
374 if add_parent:
374 if add_parent:
375 all_repos += [repo.parent]
375 all_repos += [repo.parent]
376
376
377 repos = []
377 repos = []
378 for obj in self.scm_model.get_repos(all_repos):
378 for obj in self.scm_model.get_repos(all_repos):
379 repos.append({
379 repos.append({
380 'id': obj['name'],
380 'id': obj['name'],
381 'text': obj['name'],
381 'text': obj['name'],
382 'type': 'repo',
382 'type': 'repo',
383 'obj': obj['dbrepo']
383 'obj': obj['dbrepo']
384 })
384 })
385
385
386 data = {
386 data = {
387 'more': False,
387 'more': False,
388 'results': [{
388 'results': [{
389 'text': _('Repositories'),
389 'text': _('Repositories'),
390 'children': repos
390 'children': repos
391 }] if repos else []
391 }] if repos else []
392 }
392 }
393 return data
393 return data
394
394
395 @LoginRequired()
395 @LoginRequired()
396 @NotAnonymous()
396 @NotAnonymous()
397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 'repository.admin')
398 'repository.admin')
399 @HasAcceptedRepoType('git', 'hg')
399 @HasAcceptedRepoType('git', 'hg')
400 @auth.CSRFRequired()
400 @auth.CSRFRequired()
401 def create(self, repo_name):
401 def create(self, repo_name):
402 repo = Repository.get_by_repo_name(repo_name)
402 repo = Repository.get_by_repo_name(repo_name)
403 if not repo:
403 if not repo:
404 raise HTTPNotFound
404 raise HTTPNotFound
405
405
406 controls = peppercorn.parse(request.POST.items())
406 controls = peppercorn.parse(request.POST.items())
407
407
408 try:
408 try:
409 _form = PullRequestForm(repo.repo_id)().to_python(controls)
409 _form = PullRequestForm(repo.repo_id)().to_python(controls)
410 except formencode.Invalid as errors:
410 except formencode.Invalid as errors:
411 if errors.error_dict.get('revisions'):
411 if errors.error_dict.get('revisions'):
412 msg = 'Revisions: %s' % errors.error_dict['revisions']
412 msg = 'Revisions: %s' % errors.error_dict['revisions']
413 elif errors.error_dict.get('pullrequest_title'):
413 elif errors.error_dict.get('pullrequest_title'):
414 msg = _('Pull request requires a title with min. 3 chars')
414 msg = _('Pull request requires a title with min. 3 chars')
415 else:
415 else:
416 msg = _('Error creating pull request: {}').format(errors)
416 msg = _('Error creating pull request: {}').format(errors)
417 log.exception(msg)
417 log.exception(msg)
418 h.flash(msg, 'error')
418 h.flash(msg, 'error')
419
419
420 # would rather just go back to form ...
420 # would rather just go back to form ...
421 return redirect(url('pullrequest_home', repo_name=repo_name))
421 return redirect(url('pullrequest_home', repo_name=repo_name))
422
422
423 source_repo = _form['source_repo']
423 source_repo = _form['source_repo']
424 source_ref = _form['source_ref']
424 source_ref = _form['source_ref']
425 target_repo = _form['target_repo']
425 target_repo = _form['target_repo']
426 target_ref = _form['target_ref']
426 target_ref = _form['target_ref']
427 commit_ids = _form['revisions'][::-1]
427 commit_ids = _form['revisions'][::-1]
428 reviewers = [
428 reviewers = [
429 (r['user_id'], r['reasons']) for r in _form['review_members']]
429 (r['user_id'], r['reasons']) for r in _form['review_members']]
430
430
431 # find the ancestor for this pr
431 # find the ancestor for this pr
432 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
432 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
433 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
433 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
434
434
435 source_scm = source_db_repo.scm_instance()
435 source_scm = source_db_repo.scm_instance()
436 target_scm = target_db_repo.scm_instance()
436 target_scm = target_db_repo.scm_instance()
437
437
438 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
438 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
439 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
439 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
440
440
441 ancestor = source_scm.get_common_ancestor(
441 ancestor = source_scm.get_common_ancestor(
442 source_commit.raw_id, target_commit.raw_id, target_scm)
442 source_commit.raw_id, target_commit.raw_id, target_scm)
443
443
444 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
444 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
445 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
445 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
446
446
447 pullrequest_title = _form['pullrequest_title']
447 pullrequest_title = _form['pullrequest_title']
448 title_source_ref = source_ref.split(':', 2)[1]
448 title_source_ref = source_ref.split(':', 2)[1]
449 if not pullrequest_title:
449 if not pullrequest_title:
450 pullrequest_title = PullRequestModel().generate_pullrequest_title(
450 pullrequest_title = PullRequestModel().generate_pullrequest_title(
451 source=source_repo,
451 source=source_repo,
452 source_ref=title_source_ref,
452 source_ref=title_source_ref,
453 target=target_repo
453 target=target_repo
454 )
454 )
455
455
456 description = _form['pullrequest_desc']
456 description = _form['pullrequest_desc']
457 try:
457 try:
458 pull_request = PullRequestModel().create(
458 pull_request = PullRequestModel().create(
459 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
459 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
460 target_ref, commit_ids, reviewers, pullrequest_title,
460 target_ref, commit_ids, reviewers, pullrequest_title,
461 description
461 description
462 )
462 )
463 Session().commit()
463 Session().commit()
464 h.flash(_('Successfully opened new pull request'),
464 h.flash(_('Successfully opened new pull request'),
465 category='success')
465 category='success')
466 except Exception as e:
466 except Exception as e:
467 msg = _('Error occurred during sending pull request')
467 msg = _('Error occurred during sending pull request')
468 log.exception(msg)
468 log.exception(msg)
469 h.flash(msg, category='error')
469 h.flash(msg, category='error')
470 return redirect(url('pullrequest_home', repo_name=repo_name))
470 return redirect(url('pullrequest_home', repo_name=repo_name))
471
471
472 return redirect(url('pullrequest_show', repo_name=target_repo,
472 return redirect(url('pullrequest_show', repo_name=target_repo,
473 pull_request_id=pull_request.pull_request_id))
473 pull_request_id=pull_request.pull_request_id))
474
474
475 @LoginRequired()
475 @LoginRequired()
476 @NotAnonymous()
476 @NotAnonymous()
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
478 'repository.admin')
478 'repository.admin')
479 @auth.CSRFRequired()
479 @auth.CSRFRequired()
480 @jsonify
480 @jsonify
481 def update(self, repo_name, pull_request_id):
481 def update(self, repo_name, pull_request_id):
482 pull_request_id = safe_int(pull_request_id)
482 pull_request_id = safe_int(pull_request_id)
483 pull_request = PullRequest.get_or_404(pull_request_id)
483 pull_request = PullRequest.get_or_404(pull_request_id)
484 # only owner or admin can update it
484 # only owner or admin can update it
485 allowed_to_update = PullRequestModel().check_user_update(
485 allowed_to_update = PullRequestModel().check_user_update(
486 pull_request, c.rhodecode_user)
486 pull_request, c.rhodecode_user)
487 if allowed_to_update:
487 if allowed_to_update:
488 controls = peppercorn.parse(request.POST.items())
488 controls = peppercorn.parse(request.POST.items())
489
489
490 if 'review_members' in controls:
490 if 'review_members' in controls:
491 self._update_reviewers(
491 self._update_reviewers(
492 pull_request_id, controls['review_members'])
492 pull_request_id, controls['review_members'])
493 elif str2bool(request.POST.get('update_commits', 'false')):
493 elif str2bool(request.POST.get('update_commits', 'false')):
494 self._update_commits(pull_request)
494 self._update_commits(pull_request)
495 elif str2bool(request.POST.get('close_pull_request', 'false')):
495 elif str2bool(request.POST.get('close_pull_request', 'false')):
496 self._reject_close(pull_request)
496 self._reject_close(pull_request)
497 elif str2bool(request.POST.get('edit_pull_request', 'false')):
497 elif str2bool(request.POST.get('edit_pull_request', 'false')):
498 self._edit_pull_request(pull_request)
498 self._edit_pull_request(pull_request)
499 else:
499 else:
500 raise HTTPBadRequest()
500 raise HTTPBadRequest()
501 return True
501 return True
502 raise HTTPForbidden()
502 raise HTTPForbidden()
503
503
504 def _edit_pull_request(self, pull_request):
504 def _edit_pull_request(self, pull_request):
505 try:
505 try:
506 PullRequestModel().edit(
506 PullRequestModel().edit(
507 pull_request, request.POST.get('title'),
507 pull_request, request.POST.get('title'),
508 request.POST.get('description'))
508 request.POST.get('description'))
509 except ValueError:
509 except ValueError:
510 msg = _(u'Cannot update closed pull requests.')
510 msg = _(u'Cannot update closed pull requests.')
511 h.flash(msg, category='error')
511 h.flash(msg, category='error')
512 return
512 return
513 else:
513 else:
514 Session().commit()
514 Session().commit()
515
515
516 msg = _(u'Pull request title & description updated.')
516 msg = _(u'Pull request title & description updated.')
517 h.flash(msg, category='success')
517 h.flash(msg, category='success')
518 return
518 return
519
519
520 def _update_commits(self, pull_request):
520 def _update_commits(self, pull_request):
521 resp = PullRequestModel().update_commits(pull_request)
521 resp = PullRequestModel().update_commits(pull_request)
522
522
523 if resp.executed:
523 if resp.executed:
524 msg = _(
524 msg = _(
525 u'Pull request updated to "{source_commit_id}" with '
525 u'Pull request updated to "{source_commit_id}" with '
526 u'{count_added} added, {count_removed} removed commits.')
526 u'{count_added} added, {count_removed} removed commits.')
527 msg = msg.format(
527 msg = msg.format(
528 source_commit_id=pull_request.source_ref_parts.commit_id,
528 source_commit_id=pull_request.source_ref_parts.commit_id,
529 count_added=len(resp.changes.added),
529 count_added=len(resp.changes.added),
530 count_removed=len(resp.changes.removed))
530 count_removed=len(resp.changes.removed))
531 h.flash(msg, category='success')
531 h.flash(msg, category='success')
532
532
533 registry = get_current_registry()
533 registry = get_current_registry()
534 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
534 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
535 channelstream_config = rhodecode_plugins.get('channelstream', {})
535 channelstream_config = rhodecode_plugins.get('channelstream', {})
536 if channelstream_config.get('enabled'):
536 if channelstream_config.get('enabled'):
537 message = msg + (
537 message = msg + (
538 ' - <a onclick="window.location.reload()">'
538 ' - <a onclick="window.location.reload()">'
539 '<strong>{}</strong></a>'.format(_('Reload page')))
539 '<strong>{}</strong></a>'.format(_('Reload page')))
540 channel = '/repo${}$/pr/{}'.format(
540 channel = '/repo${}$/pr/{}'.format(
541 pull_request.target_repo.repo_name,
541 pull_request.target_repo.repo_name,
542 pull_request.pull_request_id
542 pull_request.pull_request_id
543 )
543 )
544 payload = {
544 payload = {
545 'type': 'message',
545 'type': 'message',
546 'user': 'system',
546 'user': 'system',
547 'exclude_users': [request.user.username],
547 'exclude_users': [request.user.username],
548 'channel': channel,
548 'channel': channel,
549 'message': {
549 'message': {
550 'message': message,
550 'message': message,
551 'level': 'success',
551 'level': 'success',
552 'topic': '/notifications'
552 'topic': '/notifications'
553 }
553 }
554 }
554 }
555 channelstream_request(
555 channelstream_request(
556 channelstream_config, [payload], '/message',
556 channelstream_config, [payload], '/message',
557 raise_exc=False)
557 raise_exc=False)
558 else:
558 else:
559 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
559 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
560 warning_reasons = [
560 warning_reasons = [
561 UpdateFailureReason.NO_CHANGE,
561 UpdateFailureReason.NO_CHANGE,
562 UpdateFailureReason.WRONG_REF_TPYE,
562 UpdateFailureReason.WRONG_REF_TPYE,
563 ]
563 ]
564 category = 'warning' if resp.reason in warning_reasons else 'error'
564 category = 'warning' if resp.reason in warning_reasons else 'error'
565 h.flash(msg, category=category)
565 h.flash(msg, category=category)
566
566
567 @auth.CSRFRequired()
567 @auth.CSRFRequired()
568 @LoginRequired()
568 @LoginRequired()
569 @NotAnonymous()
569 @NotAnonymous()
570 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
570 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
571 'repository.admin')
571 'repository.admin')
572 def merge(self, repo_name, pull_request_id):
572 def merge(self, repo_name, pull_request_id):
573 """
573 """
574 POST /{repo_name}/pull-request/{pull_request_id}
574 POST /{repo_name}/pull-request/{pull_request_id}
575
575
576 Merge will perform a server-side merge of the specified
576 Merge will perform a server-side merge of the specified
577 pull request, if the pull request is approved and mergeable.
577 pull request, if the pull request is approved and mergeable.
578 After succesfull merging, the pull request is automatically
578 After succesfull merging, the pull request is automatically
579 closed, with a relevant comment.
579 closed, with a relevant comment.
580 """
580 """
581 pull_request_id = safe_int(pull_request_id)
581 pull_request_id = safe_int(pull_request_id)
582 pull_request = PullRequest.get_or_404(pull_request_id)
582 pull_request = PullRequest.get_or_404(pull_request_id)
583 user = c.rhodecode_user
583 user = c.rhodecode_user
584
584
585 if self._meets_merge_pre_conditions(pull_request, user):
585 if self._meets_merge_pre_conditions(pull_request, user):
586 log.debug("Pre-conditions checked, trying to merge.")
586 log.debug("Pre-conditions checked, trying to merge.")
587 extras = vcs_operation_context(
587 extras = vcs_operation_context(
588 request.environ, repo_name=pull_request.target_repo.repo_name,
588 request.environ, repo_name=pull_request.target_repo.repo_name,
589 username=user.username, action='push',
589 username=user.username, action='push',
590 scm=pull_request.target_repo.repo_type)
590 scm=pull_request.target_repo.repo_type)
591 self._merge_pull_request(pull_request, user, extras)
591 self._merge_pull_request(pull_request, user, extras)
592
592
593 return redirect(url(
593 return redirect(url(
594 'pullrequest_show',
594 'pullrequest_show',
595 repo_name=pull_request.target_repo.repo_name,
595 repo_name=pull_request.target_repo.repo_name,
596 pull_request_id=pull_request.pull_request_id))
596 pull_request_id=pull_request.pull_request_id))
597
597
598 def _meets_merge_pre_conditions(self, pull_request, user):
598 def _meets_merge_pre_conditions(self, pull_request, user):
599 if not PullRequestModel().check_user_merge(pull_request, user):
599 if not PullRequestModel().check_user_merge(pull_request, user):
600 raise HTTPForbidden()
600 raise HTTPForbidden()
601
601
602 merge_status, msg = PullRequestModel().merge_status(pull_request)
602 merge_status, msg = PullRequestModel().merge_status(pull_request)
603 if not merge_status:
603 if not merge_status:
604 log.debug("Cannot merge, not mergeable.")
604 log.debug("Cannot merge, not mergeable.")
605 h.flash(msg, category='error')
605 h.flash(msg, category='error')
606 return False
606 return False
607
607
608 if (pull_request.calculated_review_status()
608 if (pull_request.calculated_review_status()
609 is not ChangesetStatus.STATUS_APPROVED):
609 is not ChangesetStatus.STATUS_APPROVED):
610 log.debug("Cannot merge, approval is pending.")
610 log.debug("Cannot merge, approval is pending.")
611 msg = _('Pull request reviewer approval is pending.')
611 msg = _('Pull request reviewer approval is pending.')
612 h.flash(msg, category='error')
612 h.flash(msg, category='error')
613 return False
613 return False
614 return True
614 return True
615
615
616 def _merge_pull_request(self, pull_request, user, extras):
616 def _merge_pull_request(self, pull_request, user, extras):
617 merge_resp = PullRequestModel().merge(
617 merge_resp = PullRequestModel().merge(
618 pull_request, user, extras=extras)
618 pull_request, user, extras=extras)
619
619
620 if merge_resp.executed:
620 if merge_resp.executed:
621 log.debug("The merge was successful, closing the pull request.")
621 log.debug("The merge was successful, closing the pull request.")
622 PullRequestModel().close_pull_request(
622 PullRequestModel().close_pull_request(
623 pull_request.pull_request_id, user)
623 pull_request.pull_request_id, user)
624 Session().commit()
624 Session().commit()
625 msg = _('Pull request was successfully merged and closed.')
625 msg = _('Pull request was successfully merged and closed.')
626 h.flash(msg, category='success')
626 h.flash(msg, category='success')
627 else:
627 else:
628 log.debug(
628 log.debug(
629 "The merge was not successful. Merge response: %s",
629 "The merge was not successful. Merge response: %s",
630 merge_resp)
630 merge_resp)
631 msg = PullRequestModel().merge_status_message(
631 msg = PullRequestModel().merge_status_message(
632 merge_resp.failure_reason)
632 merge_resp.failure_reason)
633 h.flash(msg, category='error')
633 h.flash(msg, category='error')
634
634
635 def _update_reviewers(self, pull_request_id, review_members):
635 def _update_reviewers(self, pull_request_id, review_members):
636 reviewers = [
636 reviewers = [
637 (int(r['user_id']), r['reasons']) for r in review_members]
637 (int(r['user_id']), r['reasons']) for r in review_members]
638 PullRequestModel().update_reviewers(pull_request_id, reviewers)
638 PullRequestModel().update_reviewers(pull_request_id, reviewers)
639 Session().commit()
639 Session().commit()
640
640
641 def _reject_close(self, pull_request):
641 def _reject_close(self, pull_request):
642 if pull_request.is_closed():
642 if pull_request.is_closed():
643 raise HTTPForbidden()
643 raise HTTPForbidden()
644
644
645 PullRequestModel().close_pull_request_with_comment(
645 PullRequestModel().close_pull_request_with_comment(
646 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
646 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
647 Session().commit()
647 Session().commit()
648
648
649 @LoginRequired()
649 @LoginRequired()
650 @NotAnonymous()
650 @NotAnonymous()
651 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
651 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
652 'repository.admin')
652 'repository.admin')
653 @auth.CSRFRequired()
653 @auth.CSRFRequired()
654 @jsonify
654 @jsonify
655 def delete(self, repo_name, pull_request_id):
655 def delete(self, repo_name, pull_request_id):
656 pull_request_id = safe_int(pull_request_id)
656 pull_request_id = safe_int(pull_request_id)
657 pull_request = PullRequest.get_or_404(pull_request_id)
657 pull_request = PullRequest.get_or_404(pull_request_id)
658 # only owner can delete it !
658 # only owner can delete it !
659 if pull_request.author.user_id == c.rhodecode_user.user_id:
659 if pull_request.author.user_id == c.rhodecode_user.user_id:
660 PullRequestModel().delete(pull_request)
660 PullRequestModel().delete(pull_request)
661 Session().commit()
661 Session().commit()
662 h.flash(_('Successfully deleted pull request'),
662 h.flash(_('Successfully deleted pull request'),
663 category='success')
663 category='success')
664 return redirect(url('my_account_pullrequests'))
664 return redirect(url('my_account_pullrequests'))
665 raise HTTPForbidden()
665 raise HTTPForbidden()
666
666
667 @LoginRequired()
667 @LoginRequired()
668 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
668 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
669 'repository.admin')
669 'repository.admin')
670 def show(self, repo_name, pull_request_id):
670 def show(self, repo_name, pull_request_id):
671 pull_request_id = safe_int(pull_request_id)
671 pull_request_id = safe_int(pull_request_id)
672 c.pull_request = PullRequest.get_or_404(pull_request_id)
672 c.pull_request = PullRequest.get_or_404(pull_request_id)
673
673
674 c.template_context['pull_request_data']['pull_request_id'] = \
674 c.template_context['pull_request_data']['pull_request_id'] = \
675 pull_request_id
675 pull_request_id
676
676
677 # pull_requests repo_name we opened it against
677 # pull_requests repo_name we opened it against
678 # ie. target_repo must match
678 # ie. target_repo must match
679 if repo_name != c.pull_request.target_repo.repo_name:
679 if repo_name != c.pull_request.target_repo.repo_name:
680 raise HTTPNotFound
680 raise HTTPNotFound
681
681
682 c.allowed_to_change_status = PullRequestModel(). \
682 c.allowed_to_change_status = PullRequestModel(). \
683 check_user_change_status(c.pull_request, c.rhodecode_user)
683 check_user_change_status(c.pull_request, c.rhodecode_user)
684 c.allowed_to_update = PullRequestModel().check_user_update(
684 c.allowed_to_update = PullRequestModel().check_user_update(
685 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
685 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
686 c.allowed_to_merge = PullRequestModel().check_user_merge(
686 c.allowed_to_merge = PullRequestModel().check_user_merge(
687 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
687 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
688 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
688 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
689 c.pull_request)
689 c.pull_request)
690 c.allowed_to_delete = PullRequestModel().check_user_delete(
691 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
690
692
691 cc_model = ChangesetCommentsModel()
693 cc_model = ChangesetCommentsModel()
692
694
693 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
695 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
694
696
695 c.pull_request_review_status = c.pull_request.calculated_review_status()
697 c.pull_request_review_status = c.pull_request.calculated_review_status()
696 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
698 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
697 c.pull_request)
699 c.pull_request)
698 c.approval_msg = None
700 c.approval_msg = None
699 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
701 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
700 c.approval_msg = _('Reviewer approval is pending.')
702 c.approval_msg = _('Reviewer approval is pending.')
701 c.pr_merge_status = False
703 c.pr_merge_status = False
702 # load compare data into template context
704 # load compare data into template context
703 enable_comments = not c.pull_request.is_closed()
705 enable_comments = not c.pull_request.is_closed()
704 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
706 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
705
707
706 # this is a hack to properly display links, when creating PR, the
708 # this is a hack to properly display links, when creating PR, the
707 # compare view and others uses different notation, and
709 # compare view and others uses different notation, and
708 # compare_commits.html renders links based on the target_repo.
710 # compare_commits.html renders links based on the target_repo.
709 # We need to swap that here to generate it properly on the html side
711 # We need to swap that here to generate it properly on the html side
710 c.target_repo = c.source_repo
712 c.target_repo = c.source_repo
711
713
712 # inline comments
714 # inline comments
713 c.inline_cnt = 0
715 c.inline_cnt = 0
714 c.inline_comments = cc_model.get_inline_comments(
716 c.inline_comments = cc_model.get_inline_comments(
715 c.rhodecode_db_repo.repo_id,
717 c.rhodecode_db_repo.repo_id,
716 pull_request=pull_request_id).items()
718 pull_request=pull_request_id).items()
717 # count inline comments
719 # count inline comments
718 for __, lines in c.inline_comments:
720 for __, lines in c.inline_comments:
719 for comments in lines.values():
721 for comments in lines.values():
720 c.inline_cnt += len(comments)
722 c.inline_cnt += len(comments)
721
723
722 # outdated comments
724 # outdated comments
723 c.outdated_cnt = 0
725 c.outdated_cnt = 0
724 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
726 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
725 c.outdated_comments = cc_model.get_outdated_comments(
727 c.outdated_comments = cc_model.get_outdated_comments(
726 c.rhodecode_db_repo.repo_id,
728 c.rhodecode_db_repo.repo_id,
727 pull_request=c.pull_request)
729 pull_request=c.pull_request)
728 # Count outdated comments and check for deleted files
730 # Count outdated comments and check for deleted files
729 for file_name, lines in c.outdated_comments.iteritems():
731 for file_name, lines in c.outdated_comments.iteritems():
730 for comments in lines.values():
732 for comments in lines.values():
731 c.outdated_cnt += len(comments)
733 c.outdated_cnt += len(comments)
732 if file_name not in c.included_files:
734 if file_name not in c.included_files:
733 c.deleted_files.append(file_name)
735 c.deleted_files.append(file_name)
734 else:
736 else:
735 c.outdated_comments = {}
737 c.outdated_comments = {}
736
738
737 # comments
739 # comments
738 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
740 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
739 pull_request=pull_request_id)
741 pull_request=pull_request_id)
740
742
741 if c.allowed_to_update:
743 if c.allowed_to_update:
742 force_close = ('forced_closed', _('Close Pull Request'))
744 force_close = ('forced_closed', _('Close Pull Request'))
743 statuses = ChangesetStatus.STATUSES + [force_close]
745 statuses = ChangesetStatus.STATUSES + [force_close]
744 else:
746 else:
745 statuses = ChangesetStatus.STATUSES
747 statuses = ChangesetStatus.STATUSES
746 c.commit_statuses = statuses
748 c.commit_statuses = statuses
747
749
748 c.ancestor = None # TODO: add ancestor here
750 c.ancestor = None # TODO: add ancestor here
749
751
750 return render('/pullrequests/pullrequest_show.html')
752 return render('/pullrequests/pullrequest_show.html')
751
753
752 @LoginRequired()
754 @LoginRequired()
753 @NotAnonymous()
755 @NotAnonymous()
754 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
756 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
755 'repository.admin')
757 'repository.admin')
756 @auth.CSRFRequired()
758 @auth.CSRFRequired()
757 @jsonify
759 @jsonify
758 def comment(self, repo_name, pull_request_id):
760 def comment(self, repo_name, pull_request_id):
759 pull_request_id = safe_int(pull_request_id)
761 pull_request_id = safe_int(pull_request_id)
760 pull_request = PullRequest.get_or_404(pull_request_id)
762 pull_request = PullRequest.get_or_404(pull_request_id)
761 if pull_request.is_closed():
763 if pull_request.is_closed():
762 raise HTTPForbidden()
764 raise HTTPForbidden()
763
765
764 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
766 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
765 # as a changeset status, still we want to send it in one value.
767 # as a changeset status, still we want to send it in one value.
766 status = request.POST.get('changeset_status', None)
768 status = request.POST.get('changeset_status', None)
767 text = request.POST.get('text')
769 text = request.POST.get('text')
768 if status and '_closed' in status:
770 if status and '_closed' in status:
769 close_pr = True
771 close_pr = True
770 status = status.replace('_closed', '')
772 status = status.replace('_closed', '')
771 else:
773 else:
772 close_pr = False
774 close_pr = False
773
775
774 forced = (status == 'forced')
776 forced = (status == 'forced')
775 if forced:
777 if forced:
776 status = 'rejected'
778 status = 'rejected'
777
779
778 allowed_to_change_status = PullRequestModel().check_user_change_status(
780 allowed_to_change_status = PullRequestModel().check_user_change_status(
779 pull_request, c.rhodecode_user)
781 pull_request, c.rhodecode_user)
780
782
781 if status and allowed_to_change_status:
783 if status and allowed_to_change_status:
782 message = (_('Status change %(transition_icon)s %(status)s')
784 message = (_('Status change %(transition_icon)s %(status)s')
783 % {'transition_icon': '>',
785 % {'transition_icon': '>',
784 'status': ChangesetStatus.get_status_lbl(status)})
786 'status': ChangesetStatus.get_status_lbl(status)})
785 if close_pr:
787 if close_pr:
786 message = _('Closing with') + ' ' + message
788 message = _('Closing with') + ' ' + message
787 text = text or message
789 text = text or message
788 comm = ChangesetCommentsModel().create(
790 comm = ChangesetCommentsModel().create(
789 text=text,
791 text=text,
790 repo=c.rhodecode_db_repo.repo_id,
792 repo=c.rhodecode_db_repo.repo_id,
791 user=c.rhodecode_user.user_id,
793 user=c.rhodecode_user.user_id,
792 pull_request=pull_request_id,
794 pull_request=pull_request_id,
793 f_path=request.POST.get('f_path'),
795 f_path=request.POST.get('f_path'),
794 line_no=request.POST.get('line'),
796 line_no=request.POST.get('line'),
795 status_change=(ChangesetStatus.get_status_lbl(status)
797 status_change=(ChangesetStatus.get_status_lbl(status)
796 if status and allowed_to_change_status else None),
798 if status and allowed_to_change_status else None),
797 status_change_type=(status
799 status_change_type=(status
798 if status and allowed_to_change_status else None),
800 if status and allowed_to_change_status else None),
799 closing_pr=close_pr
801 closing_pr=close_pr
800 )
802 )
801
803
802
804
803
805
804 if allowed_to_change_status:
806 if allowed_to_change_status:
805 old_calculated_status = pull_request.calculated_review_status()
807 old_calculated_status = pull_request.calculated_review_status()
806 # get status if set !
808 # get status if set !
807 if status:
809 if status:
808 ChangesetStatusModel().set_status(
810 ChangesetStatusModel().set_status(
809 c.rhodecode_db_repo.repo_id,
811 c.rhodecode_db_repo.repo_id,
810 status,
812 status,
811 c.rhodecode_user.user_id,
813 c.rhodecode_user.user_id,
812 comm,
814 comm,
813 pull_request=pull_request_id
815 pull_request=pull_request_id
814 )
816 )
815
817
816 Session().flush()
818 Session().flush()
817 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
819 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
818 # we now calculate the status of pull request, and based on that
820 # we now calculate the status of pull request, and based on that
819 # calculation we set the commits status
821 # calculation we set the commits status
820 calculated_status = pull_request.calculated_review_status()
822 calculated_status = pull_request.calculated_review_status()
821 if old_calculated_status != calculated_status:
823 if old_calculated_status != calculated_status:
822 PullRequestModel()._trigger_pull_request_hook(
824 PullRequestModel()._trigger_pull_request_hook(
823 pull_request, c.rhodecode_user, 'review_status_change')
825 pull_request, c.rhodecode_user, 'review_status_change')
824
826
825 calculated_status_lbl = ChangesetStatus.get_status_lbl(
827 calculated_status_lbl = ChangesetStatus.get_status_lbl(
826 calculated_status)
828 calculated_status)
827
829
828 if close_pr:
830 if close_pr:
829 status_completed = (
831 status_completed = (
830 calculated_status in [ChangesetStatus.STATUS_APPROVED,
832 calculated_status in [ChangesetStatus.STATUS_APPROVED,
831 ChangesetStatus.STATUS_REJECTED])
833 ChangesetStatus.STATUS_REJECTED])
832 if forced or status_completed:
834 if forced or status_completed:
833 PullRequestModel().close_pull_request(
835 PullRequestModel().close_pull_request(
834 pull_request_id, c.rhodecode_user)
836 pull_request_id, c.rhodecode_user)
835 else:
837 else:
836 h.flash(_('Closing pull request on other statuses than '
838 h.flash(_('Closing pull request on other statuses than '
837 'rejected or approved is forbidden. '
839 'rejected or approved is forbidden. '
838 'Calculated status from all reviewers '
840 'Calculated status from all reviewers '
839 'is currently: %s') % calculated_status_lbl,
841 'is currently: %s') % calculated_status_lbl,
840 category='warning')
842 category='warning')
841
843
842 Session().commit()
844 Session().commit()
843
845
844 if not request.is_xhr:
846 if not request.is_xhr:
845 return redirect(h.url('pullrequest_show', repo_name=repo_name,
847 return redirect(h.url('pullrequest_show', repo_name=repo_name,
846 pull_request_id=pull_request_id))
848 pull_request_id=pull_request_id))
847
849
848 data = {
850 data = {
849 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
851 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
850 }
852 }
851 if comm:
853 if comm:
852 c.co = comm
854 c.co = comm
853 data.update(comm.get_dict())
855 data.update(comm.get_dict())
854 data.update({'rendered_text':
856 data.update({'rendered_text':
855 render('changeset/changeset_comment_block.html')})
857 render('changeset/changeset_comment_block.html')})
856
858
857 return data
859 return data
858
860
859 @LoginRequired()
861 @LoginRequired()
860 @NotAnonymous()
862 @NotAnonymous()
861 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
863 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
862 'repository.admin')
864 'repository.admin')
863 @auth.CSRFRequired()
865 @auth.CSRFRequired()
864 @jsonify
866 @jsonify
865 def delete_comment(self, repo_name, comment_id):
867 def delete_comment(self, repo_name, comment_id):
866 return self._delete_comment(comment_id)
868 return self._delete_comment(comment_id)
867
869
868 def _delete_comment(self, comment_id):
870 def _delete_comment(self, comment_id):
869 comment_id = safe_int(comment_id)
871 comment_id = safe_int(comment_id)
870 co = ChangesetComment.get_or_404(comment_id)
872 co = ChangesetComment.get_or_404(comment_id)
871 if co.pull_request.is_closed():
873 if co.pull_request.is_closed():
872 # don't allow deleting comments on closed pull request
874 # don't allow deleting comments on closed pull request
873 raise HTTPForbidden()
875 raise HTTPForbidden()
874
876
875 is_owner = co.author.user_id == c.rhodecode_user.user_id
877 is_owner = co.author.user_id == c.rhodecode_user.user_id
876 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
878 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
877 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
879 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
878 old_calculated_status = co.pull_request.calculated_review_status()
880 old_calculated_status = co.pull_request.calculated_review_status()
879 ChangesetCommentsModel().delete(comment=co)
881 ChangesetCommentsModel().delete(comment=co)
880 Session().commit()
882 Session().commit()
881 calculated_status = co.pull_request.calculated_review_status()
883 calculated_status = co.pull_request.calculated_review_status()
882 if old_calculated_status != calculated_status:
884 if old_calculated_status != calculated_status:
883 PullRequestModel()._trigger_pull_request_hook(
885 PullRequestModel()._trigger_pull_request_hook(
884 co.pull_request, c.rhodecode_user, 'review_status_change')
886 co.pull_request, c.rhodecode_user, 'review_status_change')
885 return True
887 return True
886 else:
888 else:
887 raise HTTPForbidden()
889 raise HTTPForbidden()
@@ -1,1309 +1,1314 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 from sqlalchemy import or_
35
35
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 from rhodecode.lib.markup_renderer import (
39 from rhodecode.lib.markup_renderer import (
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils import action_logger
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 CommitDoesNotExistError, EmptyRepositoryError)
47 CommitDoesNotExistError, EmptyRepositoryError)
48 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.comment import ChangesetCommentsModel
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 PullRequestVersion, ChangesetComment)
53 PullRequestVersion, ChangesetComment)
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.notification import NotificationModel, \
55 from rhodecode.model.notification import NotificationModel, \
56 EmailNotificationModel
56 EmailNotificationModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.settings import VcsSettingsModel
58 from rhodecode.model.settings import VcsSettingsModel
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 # Data structure to hold the response data when updating commits during a pull
64 # Data structure to hold the response data when updating commits during a pull
65 # request update.
65 # request update.
66 UpdateResponse = namedtuple(
66 UpdateResponse = namedtuple(
67 'UpdateResponse', 'executed, reason, new, old, changes')
67 'UpdateResponse', 'executed, reason, new, old, changes')
68
68
69
69
70 class PullRequestModel(BaseModel):
70 class PullRequestModel(BaseModel):
71
71
72 cls = PullRequest
72 cls = PullRequest
73
73
74 DIFF_CONTEXT = 3
74 DIFF_CONTEXT = 3
75
75
76 MERGE_STATUS_MESSAGES = {
76 MERGE_STATUS_MESSAGES = {
77 MergeFailureReason.NONE: lazy_ugettext(
77 MergeFailureReason.NONE: lazy_ugettext(
78 'This pull request can be automatically merged.'),
78 'This pull request can be automatically merged.'),
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 'This pull request cannot be merged because of an unhandled'
80 'This pull request cannot be merged because of an unhandled'
81 ' exception.'),
81 ' exception.'),
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 'This pull request cannot be merged because of conflicts.'),
83 'This pull request cannot be merged because of conflicts.'),
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 'This pull request could not be merged because push to target'
85 'This pull request could not be merged because push to target'
86 ' failed.'),
86 ' failed.'),
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 'This pull request cannot be merged because the target is not a'
88 'This pull request cannot be merged because the target is not a'
89 ' head.'),
89 ' head.'),
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 'This pull request cannot be merged because the source contains'
91 'This pull request cannot be merged because the source contains'
92 ' more branches than the target.'),
92 ' more branches than the target.'),
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 'This pull request cannot be merged because the target has'
94 'This pull request cannot be merged because the target has'
95 ' multiple heads.'),
95 ' multiple heads.'),
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 'This pull request cannot be merged because the target repository'
97 'This pull request cannot be merged because the target repository'
98 ' is locked.'),
98 ' is locked.'),
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 'This pull request cannot be merged because the target or the '
100 'This pull request cannot be merged because the target or the '
101 'source reference is missing.'),
101 'source reference is missing.'),
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 'This pull request cannot be merged because the target '
103 'This pull request cannot be merged because the target '
104 'reference is missing.'),
104 'reference is missing.'),
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 'This pull request cannot be merged because the source '
106 'This pull request cannot be merged because the source '
107 'reference is missing.'),
107 'reference is missing.'),
108 }
108 }
109
109
110 UPDATE_STATUS_MESSAGES = {
110 UPDATE_STATUS_MESSAGES = {
111 UpdateFailureReason.NONE: lazy_ugettext(
111 UpdateFailureReason.NONE: lazy_ugettext(
112 'Pull request update successful.'),
112 'Pull request update successful.'),
113 UpdateFailureReason.UNKNOWN: lazy_ugettext(
113 UpdateFailureReason.UNKNOWN: lazy_ugettext(
114 'Pull request update failed because of an unknown error.'),
114 'Pull request update failed because of an unknown error.'),
115 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
115 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
116 'No update needed because the source reference is already '
116 'No update needed because the source reference is already '
117 'up to date.'),
117 'up to date.'),
118 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
118 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
119 'Pull request cannot be updated because the reference type is '
119 'Pull request cannot be updated because the reference type is '
120 'not supported for an update.'),
120 'not supported for an update.'),
121 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
121 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
122 'This pull request cannot be updated because the target '
122 'This pull request cannot be updated because the target '
123 'reference is missing.'),
123 'reference is missing.'),
124 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
124 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
125 'This pull request cannot be updated because the source '
125 'This pull request cannot be updated because the source '
126 'reference is missing.'),
126 'reference is missing.'),
127 }
127 }
128
128
129 def __get_pull_request(self, pull_request):
129 def __get_pull_request(self, pull_request):
130 return self._get_instance(PullRequest, pull_request)
130 return self._get_instance(PullRequest, pull_request)
131
131
132 def _check_perms(self, perms, pull_request, user, api=False):
132 def _check_perms(self, perms, pull_request, user, api=False):
133 if not api:
133 if not api:
134 return h.HasRepoPermissionAny(*perms)(
134 return h.HasRepoPermissionAny(*perms)(
135 user=user, repo_name=pull_request.target_repo.repo_name)
135 user=user, repo_name=pull_request.target_repo.repo_name)
136 else:
136 else:
137 return h.HasRepoPermissionAnyApi(*perms)(
137 return h.HasRepoPermissionAnyApi(*perms)(
138 user=user, repo_name=pull_request.target_repo.repo_name)
138 user=user, repo_name=pull_request.target_repo.repo_name)
139
139
140 def check_user_read(self, pull_request, user, api=False):
140 def check_user_read(self, pull_request, user, api=False):
141 _perms = ('repository.admin', 'repository.write', 'repository.read',)
141 _perms = ('repository.admin', 'repository.write', 'repository.read',)
142 return self._check_perms(_perms, pull_request, user, api)
142 return self._check_perms(_perms, pull_request, user, api)
143
143
144 def check_user_merge(self, pull_request, user, api=False):
144 def check_user_merge(self, pull_request, user, api=False):
145 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
145 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
146 return self._check_perms(_perms, pull_request, user, api)
146 return self._check_perms(_perms, pull_request, user, api)
147
147
148 def check_user_update(self, pull_request, user, api=False):
148 def check_user_update(self, pull_request, user, api=False):
149 owner = user.user_id == pull_request.user_id
149 owner = user.user_id == pull_request.user_id
150 return self.check_user_merge(pull_request, user, api) or owner
150 return self.check_user_merge(pull_request, user, api) or owner
151
151
152 def check_user_delete(self, pull_request, user):
153 owner = user.user_id == pull_request.user_id
154 _perms = ('repository.admin')
155 return self._check_perms(_perms, pull_request, user) or owner
156
152 def check_user_change_status(self, pull_request, user, api=False):
157 def check_user_change_status(self, pull_request, user, api=False):
153 reviewer = user.user_id in [x.user_id for x in
158 reviewer = user.user_id in [x.user_id for x in
154 pull_request.reviewers]
159 pull_request.reviewers]
155 return self.check_user_update(pull_request, user, api) or reviewer
160 return self.check_user_update(pull_request, user, api) or reviewer
156
161
157 def get(self, pull_request):
162 def get(self, pull_request):
158 return self.__get_pull_request(pull_request)
163 return self.__get_pull_request(pull_request)
159
164
160 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
165 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
161 opened_by=None, order_by=None,
166 opened_by=None, order_by=None,
162 order_dir='desc'):
167 order_dir='desc'):
163 repo = None
168 repo = None
164 if repo_name:
169 if repo_name:
165 repo = self._get_repo(repo_name)
170 repo = self._get_repo(repo_name)
166
171
167 q = PullRequest.query()
172 q = PullRequest.query()
168
173
169 # source or target
174 # source or target
170 if repo and source:
175 if repo and source:
171 q = q.filter(PullRequest.source_repo == repo)
176 q = q.filter(PullRequest.source_repo == repo)
172 elif repo:
177 elif repo:
173 q = q.filter(PullRequest.target_repo == repo)
178 q = q.filter(PullRequest.target_repo == repo)
174
179
175 # closed,opened
180 # closed,opened
176 if statuses:
181 if statuses:
177 q = q.filter(PullRequest.status.in_(statuses))
182 q = q.filter(PullRequest.status.in_(statuses))
178
183
179 # opened by filter
184 # opened by filter
180 if opened_by:
185 if opened_by:
181 q = q.filter(PullRequest.user_id.in_(opened_by))
186 q = q.filter(PullRequest.user_id.in_(opened_by))
182
187
183 if order_by:
188 if order_by:
184 order_map = {
189 order_map = {
185 'name_raw': PullRequest.pull_request_id,
190 'name_raw': PullRequest.pull_request_id,
186 'title': PullRequest.title,
191 'title': PullRequest.title,
187 'updated_on_raw': PullRequest.updated_on,
192 'updated_on_raw': PullRequest.updated_on,
188 'target_repo': PullRequest.target_repo_id
193 'target_repo': PullRequest.target_repo_id
189 }
194 }
190 if order_dir == 'asc':
195 if order_dir == 'asc':
191 q = q.order_by(order_map[order_by].asc())
196 q = q.order_by(order_map[order_by].asc())
192 else:
197 else:
193 q = q.order_by(order_map[order_by].desc())
198 q = q.order_by(order_map[order_by].desc())
194
199
195 return q
200 return q
196
201
197 def count_all(self, repo_name, source=False, statuses=None,
202 def count_all(self, repo_name, source=False, statuses=None,
198 opened_by=None):
203 opened_by=None):
199 """
204 """
200 Count the number of pull requests for a specific repository.
205 Count the number of pull requests for a specific repository.
201
206
202 :param repo_name: target or source repo
207 :param repo_name: target or source repo
203 :param source: boolean flag to specify if repo_name refers to source
208 :param source: boolean flag to specify if repo_name refers to source
204 :param statuses: list of pull request statuses
209 :param statuses: list of pull request statuses
205 :param opened_by: author user of the pull request
210 :param opened_by: author user of the pull request
206 :returns: int number of pull requests
211 :returns: int number of pull requests
207 """
212 """
208 q = self._prepare_get_all_query(
213 q = self._prepare_get_all_query(
209 repo_name, source=source, statuses=statuses, opened_by=opened_by)
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
210
215
211 return q.count()
216 return q.count()
212
217
213 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
218 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
214 offset=0, length=None, order_by=None, order_dir='desc'):
219 offset=0, length=None, order_by=None, order_dir='desc'):
215 """
220 """
216 Get all pull requests for a specific repository.
221 Get all pull requests for a specific repository.
217
222
218 :param repo_name: target or source repo
223 :param repo_name: target or source repo
219 :param source: boolean flag to specify if repo_name refers to source
224 :param source: boolean flag to specify if repo_name refers to source
220 :param statuses: list of pull request statuses
225 :param statuses: list of pull request statuses
221 :param opened_by: author user of the pull request
226 :param opened_by: author user of the pull request
222 :param offset: pagination offset
227 :param offset: pagination offset
223 :param length: length of returned list
228 :param length: length of returned list
224 :param order_by: order of the returned list
229 :param order_by: order of the returned list
225 :param order_dir: 'asc' or 'desc' ordering direction
230 :param order_dir: 'asc' or 'desc' ordering direction
226 :returns: list of pull requests
231 :returns: list of pull requests
227 """
232 """
228 q = self._prepare_get_all_query(
233 q = self._prepare_get_all_query(
229 repo_name, source=source, statuses=statuses, opened_by=opened_by,
234 repo_name, source=source, statuses=statuses, opened_by=opened_by,
230 order_by=order_by, order_dir=order_dir)
235 order_by=order_by, order_dir=order_dir)
231
236
232 if length:
237 if length:
233 pull_requests = q.limit(length).offset(offset).all()
238 pull_requests = q.limit(length).offset(offset).all()
234 else:
239 else:
235 pull_requests = q.all()
240 pull_requests = q.all()
236
241
237 return pull_requests
242 return pull_requests
238
243
239 def count_awaiting_review(self, repo_name, source=False, statuses=None,
244 def count_awaiting_review(self, repo_name, source=False, statuses=None,
240 opened_by=None):
245 opened_by=None):
241 """
246 """
242 Count the number of pull requests for a specific repository that are
247 Count the number of pull requests for a specific repository that are
243 awaiting review.
248 awaiting review.
244
249
245 :param repo_name: target or source repo
250 :param repo_name: target or source repo
246 :param source: boolean flag to specify if repo_name refers to source
251 :param source: boolean flag to specify if repo_name refers to source
247 :param statuses: list of pull request statuses
252 :param statuses: list of pull request statuses
248 :param opened_by: author user of the pull request
253 :param opened_by: author user of the pull request
249 :returns: int number of pull requests
254 :returns: int number of pull requests
250 """
255 """
251 pull_requests = self.get_awaiting_review(
256 pull_requests = self.get_awaiting_review(
252 repo_name, source=source, statuses=statuses, opened_by=opened_by)
257 repo_name, source=source, statuses=statuses, opened_by=opened_by)
253
258
254 return len(pull_requests)
259 return len(pull_requests)
255
260
256 def get_awaiting_review(self, repo_name, source=False, statuses=None,
261 def get_awaiting_review(self, repo_name, source=False, statuses=None,
257 opened_by=None, offset=0, length=None,
262 opened_by=None, offset=0, length=None,
258 order_by=None, order_dir='desc'):
263 order_by=None, order_dir='desc'):
259 """
264 """
260 Get all pull requests for a specific repository that are awaiting
265 Get all pull requests for a specific repository that are awaiting
261 review.
266 review.
262
267
263 :param repo_name: target or source repo
268 :param repo_name: target or source repo
264 :param source: boolean flag to specify if repo_name refers to source
269 :param source: boolean flag to specify if repo_name refers to source
265 :param statuses: list of pull request statuses
270 :param statuses: list of pull request statuses
266 :param opened_by: author user of the pull request
271 :param opened_by: author user of the pull request
267 :param offset: pagination offset
272 :param offset: pagination offset
268 :param length: length of returned list
273 :param length: length of returned list
269 :param order_by: order of the returned list
274 :param order_by: order of the returned list
270 :param order_dir: 'asc' or 'desc' ordering direction
275 :param order_dir: 'asc' or 'desc' ordering direction
271 :returns: list of pull requests
276 :returns: list of pull requests
272 """
277 """
273 pull_requests = self.get_all(
278 pull_requests = self.get_all(
274 repo_name, source=source, statuses=statuses, opened_by=opened_by,
279 repo_name, source=source, statuses=statuses, opened_by=opened_by,
275 order_by=order_by, order_dir=order_dir)
280 order_by=order_by, order_dir=order_dir)
276
281
277 _filtered_pull_requests = []
282 _filtered_pull_requests = []
278 for pr in pull_requests:
283 for pr in pull_requests:
279 status = pr.calculated_review_status()
284 status = pr.calculated_review_status()
280 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
285 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
281 ChangesetStatus.STATUS_UNDER_REVIEW]:
286 ChangesetStatus.STATUS_UNDER_REVIEW]:
282 _filtered_pull_requests.append(pr)
287 _filtered_pull_requests.append(pr)
283 if length:
288 if length:
284 return _filtered_pull_requests[offset:offset+length]
289 return _filtered_pull_requests[offset:offset+length]
285 else:
290 else:
286 return _filtered_pull_requests
291 return _filtered_pull_requests
287
292
288 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
293 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
289 opened_by=None, user_id=None):
294 opened_by=None, user_id=None):
290 """
295 """
291 Count the number of pull requests for a specific repository that are
296 Count the number of pull requests for a specific repository that are
292 awaiting review from a specific user.
297 awaiting review from a specific user.
293
298
294 :param repo_name: target or source repo
299 :param repo_name: target or source repo
295 :param source: boolean flag to specify if repo_name refers to source
300 :param source: boolean flag to specify if repo_name refers to source
296 :param statuses: list of pull request statuses
301 :param statuses: list of pull request statuses
297 :param opened_by: author user of the pull request
302 :param opened_by: author user of the pull request
298 :param user_id: reviewer user of the pull request
303 :param user_id: reviewer user of the pull request
299 :returns: int number of pull requests
304 :returns: int number of pull requests
300 """
305 """
301 pull_requests = self.get_awaiting_my_review(
306 pull_requests = self.get_awaiting_my_review(
302 repo_name, source=source, statuses=statuses, opened_by=opened_by,
307 repo_name, source=source, statuses=statuses, opened_by=opened_by,
303 user_id=user_id)
308 user_id=user_id)
304
309
305 return len(pull_requests)
310 return len(pull_requests)
306
311
307 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
312 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
308 opened_by=None, user_id=None, offset=0,
313 opened_by=None, user_id=None, offset=0,
309 length=None, order_by=None, order_dir='desc'):
314 length=None, order_by=None, order_dir='desc'):
310 """
315 """
311 Get all pull requests for a specific repository that are awaiting
316 Get all pull requests for a specific repository that are awaiting
312 review from a specific user.
317 review from a specific user.
313
318
314 :param repo_name: target or source repo
319 :param repo_name: target or source repo
315 :param source: boolean flag to specify if repo_name refers to source
320 :param source: boolean flag to specify if repo_name refers to source
316 :param statuses: list of pull request statuses
321 :param statuses: list of pull request statuses
317 :param opened_by: author user of the pull request
322 :param opened_by: author user of the pull request
318 :param user_id: reviewer user of the pull request
323 :param user_id: reviewer user of the pull request
319 :param offset: pagination offset
324 :param offset: pagination offset
320 :param length: length of returned list
325 :param length: length of returned list
321 :param order_by: order of the returned list
326 :param order_by: order of the returned list
322 :param order_dir: 'asc' or 'desc' ordering direction
327 :param order_dir: 'asc' or 'desc' ordering direction
323 :returns: list of pull requests
328 :returns: list of pull requests
324 """
329 """
325 pull_requests = self.get_all(
330 pull_requests = self.get_all(
326 repo_name, source=source, statuses=statuses, opened_by=opened_by,
331 repo_name, source=source, statuses=statuses, opened_by=opened_by,
327 order_by=order_by, order_dir=order_dir)
332 order_by=order_by, order_dir=order_dir)
328
333
329 _my = PullRequestModel().get_not_reviewed(user_id)
334 _my = PullRequestModel().get_not_reviewed(user_id)
330 my_participation = []
335 my_participation = []
331 for pr in pull_requests:
336 for pr in pull_requests:
332 if pr in _my:
337 if pr in _my:
333 my_participation.append(pr)
338 my_participation.append(pr)
334 _filtered_pull_requests = my_participation
339 _filtered_pull_requests = my_participation
335 if length:
340 if length:
336 return _filtered_pull_requests[offset:offset+length]
341 return _filtered_pull_requests[offset:offset+length]
337 else:
342 else:
338 return _filtered_pull_requests
343 return _filtered_pull_requests
339
344
340 def get_not_reviewed(self, user_id):
345 def get_not_reviewed(self, user_id):
341 return [
346 return [
342 x.pull_request for x in PullRequestReviewers.query().filter(
347 x.pull_request for x in PullRequestReviewers.query().filter(
343 PullRequestReviewers.user_id == user_id).all()
348 PullRequestReviewers.user_id == user_id).all()
344 ]
349 ]
345
350
346 def _prepare_participating_query(self, user_id=None, statuses=None,
351 def _prepare_participating_query(self, user_id=None, statuses=None,
347 order_by=None, order_dir='desc'):
352 order_by=None, order_dir='desc'):
348 q = PullRequest.query()
353 q = PullRequest.query()
349 if user_id:
354 if user_id:
350 reviewers_subquery = Session().query(
355 reviewers_subquery = Session().query(
351 PullRequestReviewers.pull_request_id).filter(
356 PullRequestReviewers.pull_request_id).filter(
352 PullRequestReviewers.user_id == user_id).subquery()
357 PullRequestReviewers.user_id == user_id).subquery()
353 user_filter= or_(
358 user_filter= or_(
354 PullRequest.user_id == user_id,
359 PullRequest.user_id == user_id,
355 PullRequest.pull_request_id.in_(reviewers_subquery)
360 PullRequest.pull_request_id.in_(reviewers_subquery)
356 )
361 )
357 q = PullRequest.query().filter(user_filter)
362 q = PullRequest.query().filter(user_filter)
358
363
359 # closed,opened
364 # closed,opened
360 if statuses:
365 if statuses:
361 q = q.filter(PullRequest.status.in_(statuses))
366 q = q.filter(PullRequest.status.in_(statuses))
362
367
363 if order_by:
368 if order_by:
364 order_map = {
369 order_map = {
365 'name_raw': PullRequest.pull_request_id,
370 'name_raw': PullRequest.pull_request_id,
366 'title': PullRequest.title,
371 'title': PullRequest.title,
367 'updated_on_raw': PullRequest.updated_on,
372 'updated_on_raw': PullRequest.updated_on,
368 'target_repo': PullRequest.target_repo_id
373 'target_repo': PullRequest.target_repo_id
369 }
374 }
370 if order_dir == 'asc':
375 if order_dir == 'asc':
371 q = q.order_by(order_map[order_by].asc())
376 q = q.order_by(order_map[order_by].asc())
372 else:
377 else:
373 q = q.order_by(order_map[order_by].desc())
378 q = q.order_by(order_map[order_by].desc())
374
379
375 return q
380 return q
376
381
377 def count_im_participating_in(self, user_id=None, statuses=None):
382 def count_im_participating_in(self, user_id=None, statuses=None):
378 q = self._prepare_participating_query(user_id, statuses=statuses)
383 q = self._prepare_participating_query(user_id, statuses=statuses)
379 return q.count()
384 return q.count()
380
385
381 def get_im_participating_in(
386 def get_im_participating_in(
382 self, user_id=None, statuses=None, offset=0,
387 self, user_id=None, statuses=None, offset=0,
383 length=None, order_by=None, order_dir='desc'):
388 length=None, order_by=None, order_dir='desc'):
384 """
389 """
385 Get all Pull requests that i'm participating in, or i have opened
390 Get all Pull requests that i'm participating in, or i have opened
386 """
391 """
387
392
388 q = self._prepare_participating_query(
393 q = self._prepare_participating_query(
389 user_id, statuses=statuses, order_by=order_by,
394 user_id, statuses=statuses, order_by=order_by,
390 order_dir=order_dir)
395 order_dir=order_dir)
391
396
392 if length:
397 if length:
393 pull_requests = q.limit(length).offset(offset).all()
398 pull_requests = q.limit(length).offset(offset).all()
394 else:
399 else:
395 pull_requests = q.all()
400 pull_requests = q.all()
396
401
397 return pull_requests
402 return pull_requests
398
403
399 def get_versions(self, pull_request):
404 def get_versions(self, pull_request):
400 """
405 """
401 returns version of pull request sorted by ID descending
406 returns version of pull request sorted by ID descending
402 """
407 """
403 return PullRequestVersion.query()\
408 return PullRequestVersion.query()\
404 .filter(PullRequestVersion.pull_request == pull_request)\
409 .filter(PullRequestVersion.pull_request == pull_request)\
405 .order_by(PullRequestVersion.pull_request_version_id.asc())\
410 .order_by(PullRequestVersion.pull_request_version_id.asc())\
406 .all()
411 .all()
407
412
408 def create(self, created_by, source_repo, source_ref, target_repo,
413 def create(self, created_by, source_repo, source_ref, target_repo,
409 target_ref, revisions, reviewers, title, description=None):
414 target_ref, revisions, reviewers, title, description=None):
410 created_by_user = self._get_user(created_by)
415 created_by_user = self._get_user(created_by)
411 source_repo = self._get_repo(source_repo)
416 source_repo = self._get_repo(source_repo)
412 target_repo = self._get_repo(target_repo)
417 target_repo = self._get_repo(target_repo)
413
418
414 pull_request = PullRequest()
419 pull_request = PullRequest()
415 pull_request.source_repo = source_repo
420 pull_request.source_repo = source_repo
416 pull_request.source_ref = source_ref
421 pull_request.source_ref = source_ref
417 pull_request.target_repo = target_repo
422 pull_request.target_repo = target_repo
418 pull_request.target_ref = target_ref
423 pull_request.target_ref = target_ref
419 pull_request.revisions = revisions
424 pull_request.revisions = revisions
420 pull_request.title = title
425 pull_request.title = title
421 pull_request.description = description
426 pull_request.description = description
422 pull_request.author = created_by_user
427 pull_request.author = created_by_user
423
428
424 Session().add(pull_request)
429 Session().add(pull_request)
425 Session().flush()
430 Session().flush()
426
431
427 reviewer_ids = set()
432 reviewer_ids = set()
428 # members / reviewers
433 # members / reviewers
429 for reviewer_object in reviewers:
434 for reviewer_object in reviewers:
430 if isinstance(reviewer_object, tuple):
435 if isinstance(reviewer_object, tuple):
431 user_id, reasons = reviewer_object
436 user_id, reasons = reviewer_object
432 else:
437 else:
433 user_id, reasons = reviewer_object, []
438 user_id, reasons = reviewer_object, []
434
439
435 user = self._get_user(user_id)
440 user = self._get_user(user_id)
436 reviewer_ids.add(user.user_id)
441 reviewer_ids.add(user.user_id)
437
442
438 reviewer = PullRequestReviewers(user, pull_request, reasons)
443 reviewer = PullRequestReviewers(user, pull_request, reasons)
439 Session().add(reviewer)
444 Session().add(reviewer)
440
445
441 # Set approval status to "Under Review" for all commits which are
446 # Set approval status to "Under Review" for all commits which are
442 # part of this pull request.
447 # part of this pull request.
443 ChangesetStatusModel().set_status(
448 ChangesetStatusModel().set_status(
444 repo=target_repo,
449 repo=target_repo,
445 status=ChangesetStatus.STATUS_UNDER_REVIEW,
450 status=ChangesetStatus.STATUS_UNDER_REVIEW,
446 user=created_by_user,
451 user=created_by_user,
447 pull_request=pull_request
452 pull_request=pull_request
448 )
453 )
449
454
450 self.notify_reviewers(pull_request, reviewer_ids)
455 self.notify_reviewers(pull_request, reviewer_ids)
451 self._trigger_pull_request_hook(
456 self._trigger_pull_request_hook(
452 pull_request, created_by_user, 'create')
457 pull_request, created_by_user, 'create')
453
458
454 return pull_request
459 return pull_request
455
460
456 def _trigger_pull_request_hook(self, pull_request, user, action):
461 def _trigger_pull_request_hook(self, pull_request, user, action):
457 pull_request = self.__get_pull_request(pull_request)
462 pull_request = self.__get_pull_request(pull_request)
458 target_scm = pull_request.target_repo.scm_instance()
463 target_scm = pull_request.target_repo.scm_instance()
459 if action == 'create':
464 if action == 'create':
460 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
465 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
461 elif action == 'merge':
466 elif action == 'merge':
462 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
467 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
463 elif action == 'close':
468 elif action == 'close':
464 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
469 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
465 elif action == 'review_status_change':
470 elif action == 'review_status_change':
466 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
471 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
467 elif action == 'update':
472 elif action == 'update':
468 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
473 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
469 else:
474 else:
470 return
475 return
471
476
472 trigger_hook(
477 trigger_hook(
473 username=user.username,
478 username=user.username,
474 repo_name=pull_request.target_repo.repo_name,
479 repo_name=pull_request.target_repo.repo_name,
475 repo_alias=target_scm.alias,
480 repo_alias=target_scm.alias,
476 pull_request=pull_request)
481 pull_request=pull_request)
477
482
478 def _get_commit_ids(self, pull_request):
483 def _get_commit_ids(self, pull_request):
479 """
484 """
480 Return the commit ids of the merged pull request.
485 Return the commit ids of the merged pull request.
481
486
482 This method is not dealing correctly yet with the lack of autoupdates
487 This method is not dealing correctly yet with the lack of autoupdates
483 nor with the implicit target updates.
488 nor with the implicit target updates.
484 For example: if a commit in the source repo is already in the target it
489 For example: if a commit in the source repo is already in the target it
485 will be reported anyways.
490 will be reported anyways.
486 """
491 """
487 merge_rev = pull_request.merge_rev
492 merge_rev = pull_request.merge_rev
488 if merge_rev is None:
493 if merge_rev is None:
489 raise ValueError('This pull request was not merged yet')
494 raise ValueError('This pull request was not merged yet')
490
495
491 commit_ids = list(pull_request.revisions)
496 commit_ids = list(pull_request.revisions)
492 if merge_rev not in commit_ids:
497 if merge_rev not in commit_ids:
493 commit_ids.append(merge_rev)
498 commit_ids.append(merge_rev)
494
499
495 return commit_ids
500 return commit_ids
496
501
497 def merge(self, pull_request, user, extras):
502 def merge(self, pull_request, user, extras):
498 log.debug("Merging pull request %s", pull_request.pull_request_id)
503 log.debug("Merging pull request %s", pull_request.pull_request_id)
499 merge_state = self._merge_pull_request(pull_request, user, extras)
504 merge_state = self._merge_pull_request(pull_request, user, extras)
500 if merge_state.executed:
505 if merge_state.executed:
501 log.debug(
506 log.debug(
502 "Merge was successful, updating the pull request comments.")
507 "Merge was successful, updating the pull request comments.")
503 self._comment_and_close_pr(pull_request, user, merge_state)
508 self._comment_and_close_pr(pull_request, user, merge_state)
504 self._log_action('user_merged_pull_request', user, pull_request)
509 self._log_action('user_merged_pull_request', user, pull_request)
505 else:
510 else:
506 log.warn("Merge failed, not updating the pull request.")
511 log.warn("Merge failed, not updating the pull request.")
507 return merge_state
512 return merge_state
508
513
509 def _merge_pull_request(self, pull_request, user, extras):
514 def _merge_pull_request(self, pull_request, user, extras):
510 target_vcs = pull_request.target_repo.scm_instance()
515 target_vcs = pull_request.target_repo.scm_instance()
511 source_vcs = pull_request.source_repo.scm_instance()
516 source_vcs = pull_request.source_repo.scm_instance()
512 target_ref = self._refresh_reference(
517 target_ref = self._refresh_reference(
513 pull_request.target_ref_parts, target_vcs)
518 pull_request.target_ref_parts, target_vcs)
514
519
515 message = _(
520 message = _(
516 'Merge pull request #%(pr_id)s from '
521 'Merge pull request #%(pr_id)s from '
517 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
522 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
518 'pr_id': pull_request.pull_request_id,
523 'pr_id': pull_request.pull_request_id,
519 'source_repo': source_vcs.name,
524 'source_repo': source_vcs.name,
520 'source_ref_name': pull_request.source_ref_parts.name,
525 'source_ref_name': pull_request.source_ref_parts.name,
521 'pr_title': pull_request.title
526 'pr_title': pull_request.title
522 }
527 }
523
528
524 workspace_id = self._workspace_id(pull_request)
529 workspace_id = self._workspace_id(pull_request)
525 use_rebase = self._use_rebase_for_merging(pull_request)
530 use_rebase = self._use_rebase_for_merging(pull_request)
526
531
527 callback_daemon, extras = prepare_callback_daemon(
532 callback_daemon, extras = prepare_callback_daemon(
528 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
533 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
529 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
534 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
530
535
531 with callback_daemon:
536 with callback_daemon:
532 # TODO: johbo: Implement a clean way to run a config_override
537 # TODO: johbo: Implement a clean way to run a config_override
533 # for a single call.
538 # for a single call.
534 target_vcs.config.set(
539 target_vcs.config.set(
535 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
540 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
536 merge_state = target_vcs.merge(
541 merge_state = target_vcs.merge(
537 target_ref, source_vcs, pull_request.source_ref_parts,
542 target_ref, source_vcs, pull_request.source_ref_parts,
538 workspace_id, user_name=user.username,
543 workspace_id, user_name=user.username,
539 user_email=user.email, message=message, use_rebase=use_rebase)
544 user_email=user.email, message=message, use_rebase=use_rebase)
540 return merge_state
545 return merge_state
541
546
542 def _comment_and_close_pr(self, pull_request, user, merge_state):
547 def _comment_and_close_pr(self, pull_request, user, merge_state):
543 pull_request.merge_rev = merge_state.merge_ref.commit_id
548 pull_request.merge_rev = merge_state.merge_ref.commit_id
544 pull_request.updated_on = datetime.datetime.now()
549 pull_request.updated_on = datetime.datetime.now()
545
550
546 ChangesetCommentsModel().create(
551 ChangesetCommentsModel().create(
547 text=unicode(_('Pull request merged and closed')),
552 text=unicode(_('Pull request merged and closed')),
548 repo=pull_request.target_repo.repo_id,
553 repo=pull_request.target_repo.repo_id,
549 user=user.user_id,
554 user=user.user_id,
550 pull_request=pull_request.pull_request_id,
555 pull_request=pull_request.pull_request_id,
551 f_path=None,
556 f_path=None,
552 line_no=None,
557 line_no=None,
553 closing_pr=True
558 closing_pr=True
554 )
559 )
555
560
556 Session().add(pull_request)
561 Session().add(pull_request)
557 Session().flush()
562 Session().flush()
558 # TODO: paris: replace invalidation with less radical solution
563 # TODO: paris: replace invalidation with less radical solution
559 ScmModel().mark_for_invalidation(
564 ScmModel().mark_for_invalidation(
560 pull_request.target_repo.repo_name)
565 pull_request.target_repo.repo_name)
561 self._trigger_pull_request_hook(pull_request, user, 'merge')
566 self._trigger_pull_request_hook(pull_request, user, 'merge')
562
567
563 def has_valid_update_type(self, pull_request):
568 def has_valid_update_type(self, pull_request):
564 source_ref_type = pull_request.source_ref_parts.type
569 source_ref_type = pull_request.source_ref_parts.type
565 return source_ref_type in ['book', 'branch', 'tag']
570 return source_ref_type in ['book', 'branch', 'tag']
566
571
567 def update_commits(self, pull_request):
572 def update_commits(self, pull_request):
568 """
573 """
569 Get the updated list of commits for the pull request
574 Get the updated list of commits for the pull request
570 and return the new pull request version and the list
575 and return the new pull request version and the list
571 of commits processed by this update action
576 of commits processed by this update action
572 """
577 """
573 pull_request = self.__get_pull_request(pull_request)
578 pull_request = self.__get_pull_request(pull_request)
574 source_ref_type = pull_request.source_ref_parts.type
579 source_ref_type = pull_request.source_ref_parts.type
575 source_ref_name = pull_request.source_ref_parts.name
580 source_ref_name = pull_request.source_ref_parts.name
576 source_ref_id = pull_request.source_ref_parts.commit_id
581 source_ref_id = pull_request.source_ref_parts.commit_id
577
582
578 if not self.has_valid_update_type(pull_request):
583 if not self.has_valid_update_type(pull_request):
579 log.debug(
584 log.debug(
580 "Skipping update of pull request %s due to ref type: %s",
585 "Skipping update of pull request %s due to ref type: %s",
581 pull_request, source_ref_type)
586 pull_request, source_ref_type)
582 return UpdateResponse(
587 return UpdateResponse(
583 executed=False,
588 executed=False,
584 reason=UpdateFailureReason.WRONG_REF_TPYE,
589 reason=UpdateFailureReason.WRONG_REF_TPYE,
585 old=pull_request, new=None, changes=None)
590 old=pull_request, new=None, changes=None)
586
591
587 source_repo = pull_request.source_repo.scm_instance()
592 source_repo = pull_request.source_repo.scm_instance()
588 try:
593 try:
589 source_commit = source_repo.get_commit(commit_id=source_ref_name)
594 source_commit = source_repo.get_commit(commit_id=source_ref_name)
590 except CommitDoesNotExistError:
595 except CommitDoesNotExistError:
591 return UpdateResponse(
596 return UpdateResponse(
592 executed=False,
597 executed=False,
593 reason=UpdateFailureReason.MISSING_SOURCE_REF,
598 reason=UpdateFailureReason.MISSING_SOURCE_REF,
594 old=pull_request, new=None, changes=None)
599 old=pull_request, new=None, changes=None)
595
600
596 if source_ref_id == source_commit.raw_id:
601 if source_ref_id == source_commit.raw_id:
597 log.debug("Nothing changed in pull request %s", pull_request)
602 log.debug("Nothing changed in pull request %s", pull_request)
598 return UpdateResponse(
603 return UpdateResponse(
599 executed=False,
604 executed=False,
600 reason=UpdateFailureReason.NO_CHANGE,
605 reason=UpdateFailureReason.NO_CHANGE,
601 old=pull_request, new=None, changes=None)
606 old=pull_request, new=None, changes=None)
602
607
603 # Finally there is a need for an update
608 # Finally there is a need for an update
604 pull_request_version = self._create_version_from_snapshot(pull_request)
609 pull_request_version = self._create_version_from_snapshot(pull_request)
605 self._link_comments_to_version(pull_request_version)
610 self._link_comments_to_version(pull_request_version)
606
611
607 target_ref_type = pull_request.target_ref_parts.type
612 target_ref_type = pull_request.target_ref_parts.type
608 target_ref_name = pull_request.target_ref_parts.name
613 target_ref_name = pull_request.target_ref_parts.name
609 target_ref_id = pull_request.target_ref_parts.commit_id
614 target_ref_id = pull_request.target_ref_parts.commit_id
610 target_repo = pull_request.target_repo.scm_instance()
615 target_repo = pull_request.target_repo.scm_instance()
611
616
612 try:
617 try:
613 if target_ref_type in ('tag', 'branch', 'book'):
618 if target_ref_type in ('tag', 'branch', 'book'):
614 target_commit = target_repo.get_commit(target_ref_name)
619 target_commit = target_repo.get_commit(target_ref_name)
615 else:
620 else:
616 target_commit = target_repo.get_commit(target_ref_id)
621 target_commit = target_repo.get_commit(target_ref_id)
617 except CommitDoesNotExistError:
622 except CommitDoesNotExistError:
618 return UpdateResponse(
623 return UpdateResponse(
619 executed=False,
624 executed=False,
620 reason=UpdateFailureReason.MISSING_TARGET_REF,
625 reason=UpdateFailureReason.MISSING_TARGET_REF,
621 old=pull_request, new=None, changes=None)
626 old=pull_request, new=None, changes=None)
622
627
623 # re-compute commit ids
628 # re-compute commit ids
624 old_commit_ids = set(pull_request.revisions)
629 old_commit_ids = set(pull_request.revisions)
625 pre_load = ["author", "branch", "date", "message"]
630 pre_load = ["author", "branch", "date", "message"]
626 commit_ranges = target_repo.compare(
631 commit_ranges = target_repo.compare(
627 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
632 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
628 pre_load=pre_load)
633 pre_load=pre_load)
629
634
630 ancestor = target_repo.get_common_ancestor(
635 ancestor = target_repo.get_common_ancestor(
631 target_commit.raw_id, source_commit.raw_id, source_repo)
636 target_commit.raw_id, source_commit.raw_id, source_repo)
632
637
633 pull_request.source_ref = '%s:%s:%s' % (
638 pull_request.source_ref = '%s:%s:%s' % (
634 source_ref_type, source_ref_name, source_commit.raw_id)
639 source_ref_type, source_ref_name, source_commit.raw_id)
635 pull_request.target_ref = '%s:%s:%s' % (
640 pull_request.target_ref = '%s:%s:%s' % (
636 target_ref_type, target_ref_name, ancestor)
641 target_ref_type, target_ref_name, ancestor)
637 pull_request.revisions = [
642 pull_request.revisions = [
638 commit.raw_id for commit in reversed(commit_ranges)]
643 commit.raw_id for commit in reversed(commit_ranges)]
639 pull_request.updated_on = datetime.datetime.now()
644 pull_request.updated_on = datetime.datetime.now()
640 Session().add(pull_request)
645 Session().add(pull_request)
641 new_commit_ids = set(pull_request.revisions)
646 new_commit_ids = set(pull_request.revisions)
642
647
643 changes = self._calculate_commit_id_changes(
648 changes = self._calculate_commit_id_changes(
644 old_commit_ids, new_commit_ids)
649 old_commit_ids, new_commit_ids)
645
650
646 old_diff_data, new_diff_data = self._generate_update_diffs(
651 old_diff_data, new_diff_data = self._generate_update_diffs(
647 pull_request, pull_request_version)
652 pull_request, pull_request_version)
648
653
649 ChangesetCommentsModel().outdate_comments(
654 ChangesetCommentsModel().outdate_comments(
650 pull_request, old_diff_data=old_diff_data,
655 pull_request, old_diff_data=old_diff_data,
651 new_diff_data=new_diff_data)
656 new_diff_data=new_diff_data)
652
657
653 file_changes = self._calculate_file_changes(
658 file_changes = self._calculate_file_changes(
654 old_diff_data, new_diff_data)
659 old_diff_data, new_diff_data)
655
660
656 # Add an automatic comment to the pull request
661 # Add an automatic comment to the pull request
657 update_comment = ChangesetCommentsModel().create(
662 update_comment = ChangesetCommentsModel().create(
658 text=self._render_update_message(changes, file_changes),
663 text=self._render_update_message(changes, file_changes),
659 repo=pull_request.target_repo,
664 repo=pull_request.target_repo,
660 user=pull_request.author,
665 user=pull_request.author,
661 pull_request=pull_request,
666 pull_request=pull_request,
662 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
667 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
663
668
664 # Update status to "Under Review" for added commits
669 # Update status to "Under Review" for added commits
665 for commit_id in changes.added:
670 for commit_id in changes.added:
666 ChangesetStatusModel().set_status(
671 ChangesetStatusModel().set_status(
667 repo=pull_request.source_repo,
672 repo=pull_request.source_repo,
668 status=ChangesetStatus.STATUS_UNDER_REVIEW,
673 status=ChangesetStatus.STATUS_UNDER_REVIEW,
669 comment=update_comment,
674 comment=update_comment,
670 user=pull_request.author,
675 user=pull_request.author,
671 pull_request=pull_request,
676 pull_request=pull_request,
672 revision=commit_id)
677 revision=commit_id)
673
678
674 log.debug(
679 log.debug(
675 'Updated pull request %s, added_ids: %s, common_ids: %s, '
680 'Updated pull request %s, added_ids: %s, common_ids: %s, '
676 'removed_ids: %s', pull_request.pull_request_id,
681 'removed_ids: %s', pull_request.pull_request_id,
677 changes.added, changes.common, changes.removed)
682 changes.added, changes.common, changes.removed)
678 log.debug('Updated pull request with the following file changes: %s',
683 log.debug('Updated pull request with the following file changes: %s',
679 file_changes)
684 file_changes)
680
685
681 log.info(
686 log.info(
682 "Updated pull request %s from commit %s to commit %s, "
687 "Updated pull request %s from commit %s to commit %s, "
683 "stored new version %s of this pull request.",
688 "stored new version %s of this pull request.",
684 pull_request.pull_request_id, source_ref_id,
689 pull_request.pull_request_id, source_ref_id,
685 pull_request.source_ref_parts.commit_id,
690 pull_request.source_ref_parts.commit_id,
686 pull_request_version.pull_request_version_id)
691 pull_request_version.pull_request_version_id)
687 Session().commit()
692 Session().commit()
688 self._trigger_pull_request_hook(pull_request, pull_request.author,
693 self._trigger_pull_request_hook(pull_request, pull_request.author,
689 'update')
694 'update')
690
695
691 return UpdateResponse(
696 return UpdateResponse(
692 executed=True, reason=UpdateFailureReason.NONE,
697 executed=True, reason=UpdateFailureReason.NONE,
693 old=pull_request, new=pull_request_version, changes=changes)
698 old=pull_request, new=pull_request_version, changes=changes)
694
699
695 def _create_version_from_snapshot(self, pull_request):
700 def _create_version_from_snapshot(self, pull_request):
696 version = PullRequestVersion()
701 version = PullRequestVersion()
697 version.title = pull_request.title
702 version.title = pull_request.title
698 version.description = pull_request.description
703 version.description = pull_request.description
699 version.status = pull_request.status
704 version.status = pull_request.status
700 version.created_on = pull_request.created_on
705 version.created_on = pull_request.created_on
701 version.updated_on = pull_request.updated_on
706 version.updated_on = pull_request.updated_on
702 version.user_id = pull_request.user_id
707 version.user_id = pull_request.user_id
703 version.source_repo = pull_request.source_repo
708 version.source_repo = pull_request.source_repo
704 version.source_ref = pull_request.source_ref
709 version.source_ref = pull_request.source_ref
705 version.target_repo = pull_request.target_repo
710 version.target_repo = pull_request.target_repo
706 version.target_ref = pull_request.target_ref
711 version.target_ref = pull_request.target_ref
707
712
708 version._last_merge_source_rev = pull_request._last_merge_source_rev
713 version._last_merge_source_rev = pull_request._last_merge_source_rev
709 version._last_merge_target_rev = pull_request._last_merge_target_rev
714 version._last_merge_target_rev = pull_request._last_merge_target_rev
710 version._last_merge_status = pull_request._last_merge_status
715 version._last_merge_status = pull_request._last_merge_status
711 version.shadow_merge_ref = pull_request.shadow_merge_ref
716 version.shadow_merge_ref = pull_request.shadow_merge_ref
712 version.merge_rev = pull_request.merge_rev
717 version.merge_rev = pull_request.merge_rev
713
718
714 version.revisions = pull_request.revisions
719 version.revisions = pull_request.revisions
715 version.pull_request = pull_request
720 version.pull_request = pull_request
716 Session().add(version)
721 Session().add(version)
717 Session().flush()
722 Session().flush()
718
723
719 return version
724 return version
720
725
721 def _generate_update_diffs(self, pull_request, pull_request_version):
726 def _generate_update_diffs(self, pull_request, pull_request_version):
722 diff_context = (
727 diff_context = (
723 self.DIFF_CONTEXT +
728 self.DIFF_CONTEXT +
724 ChangesetCommentsModel.needed_extra_diff_context())
729 ChangesetCommentsModel.needed_extra_diff_context())
725 old_diff = self._get_diff_from_pr_or_version(
730 old_diff = self._get_diff_from_pr_or_version(
726 pull_request_version, context=diff_context)
731 pull_request_version, context=diff_context)
727 new_diff = self._get_diff_from_pr_or_version(
732 new_diff = self._get_diff_from_pr_or_version(
728 pull_request, context=diff_context)
733 pull_request, context=diff_context)
729
734
730 old_diff_data = diffs.DiffProcessor(old_diff)
735 old_diff_data = diffs.DiffProcessor(old_diff)
731 old_diff_data.prepare()
736 old_diff_data.prepare()
732 new_diff_data = diffs.DiffProcessor(new_diff)
737 new_diff_data = diffs.DiffProcessor(new_diff)
733 new_diff_data.prepare()
738 new_diff_data.prepare()
734
739
735 return old_diff_data, new_diff_data
740 return old_diff_data, new_diff_data
736
741
737 def _link_comments_to_version(self, pull_request_version):
742 def _link_comments_to_version(self, pull_request_version):
738 """
743 """
739 Link all unlinked comments of this pull request to the given version.
744 Link all unlinked comments of this pull request to the given version.
740
745
741 :param pull_request_version: The `PullRequestVersion` to which
746 :param pull_request_version: The `PullRequestVersion` to which
742 the comments shall be linked.
747 the comments shall be linked.
743
748
744 """
749 """
745 pull_request = pull_request_version.pull_request
750 pull_request = pull_request_version.pull_request
746 comments = ChangesetComment.query().filter(
751 comments = ChangesetComment.query().filter(
747 # TODO: johbo: Should we query for the repo at all here?
752 # TODO: johbo: Should we query for the repo at all here?
748 # Pending decision on how comments of PRs are to be related
753 # Pending decision on how comments of PRs are to be related
749 # to either the source repo, the target repo or no repo at all.
754 # to either the source repo, the target repo or no repo at all.
750 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
755 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
751 ChangesetComment.pull_request == pull_request,
756 ChangesetComment.pull_request == pull_request,
752 ChangesetComment.pull_request_version == None)
757 ChangesetComment.pull_request_version == None)
753
758
754 # TODO: johbo: Find out why this breaks if it is done in a bulk
759 # TODO: johbo: Find out why this breaks if it is done in a bulk
755 # operation.
760 # operation.
756 for comment in comments:
761 for comment in comments:
757 comment.pull_request_version_id = (
762 comment.pull_request_version_id = (
758 pull_request_version.pull_request_version_id)
763 pull_request_version.pull_request_version_id)
759 Session().add(comment)
764 Session().add(comment)
760
765
761 def _calculate_commit_id_changes(self, old_ids, new_ids):
766 def _calculate_commit_id_changes(self, old_ids, new_ids):
762 added = new_ids.difference(old_ids)
767 added = new_ids.difference(old_ids)
763 common = old_ids.intersection(new_ids)
768 common = old_ids.intersection(new_ids)
764 removed = old_ids.difference(new_ids)
769 removed = old_ids.difference(new_ids)
765 return ChangeTuple(added, common, removed)
770 return ChangeTuple(added, common, removed)
766
771
767 def _calculate_file_changes(self, old_diff_data, new_diff_data):
772 def _calculate_file_changes(self, old_diff_data, new_diff_data):
768
773
769 old_files = OrderedDict()
774 old_files = OrderedDict()
770 for diff_data in old_diff_data.parsed_diff:
775 for diff_data in old_diff_data.parsed_diff:
771 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
776 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
772
777
773 added_files = []
778 added_files = []
774 modified_files = []
779 modified_files = []
775 removed_files = []
780 removed_files = []
776 for diff_data in new_diff_data.parsed_diff:
781 for diff_data in new_diff_data.parsed_diff:
777 new_filename = diff_data['filename']
782 new_filename = diff_data['filename']
778 new_hash = md5_safe(diff_data['raw_diff'])
783 new_hash = md5_safe(diff_data['raw_diff'])
779
784
780 old_hash = old_files.get(new_filename)
785 old_hash = old_files.get(new_filename)
781 if not old_hash:
786 if not old_hash:
782 # file is not present in old diff, means it's added
787 # file is not present in old diff, means it's added
783 added_files.append(new_filename)
788 added_files.append(new_filename)
784 else:
789 else:
785 if new_hash != old_hash:
790 if new_hash != old_hash:
786 modified_files.append(new_filename)
791 modified_files.append(new_filename)
787 # now remove a file from old, since we have seen it already
792 # now remove a file from old, since we have seen it already
788 del old_files[new_filename]
793 del old_files[new_filename]
789
794
790 # removed files is when there are present in old, but not in NEW,
795 # removed files is when there are present in old, but not in NEW,
791 # since we remove old files that are present in new diff, left-overs
796 # since we remove old files that are present in new diff, left-overs
792 # if any should be the removed files
797 # if any should be the removed files
793 removed_files.extend(old_files.keys())
798 removed_files.extend(old_files.keys())
794
799
795 return FileChangeTuple(added_files, modified_files, removed_files)
800 return FileChangeTuple(added_files, modified_files, removed_files)
796
801
797 def _render_update_message(self, changes, file_changes):
802 def _render_update_message(self, changes, file_changes):
798 """
803 """
799 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
804 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
800 so it's always looking the same disregarding on which default
805 so it's always looking the same disregarding on which default
801 renderer system is using.
806 renderer system is using.
802
807
803 :param changes: changes named tuple
808 :param changes: changes named tuple
804 :param file_changes: file changes named tuple
809 :param file_changes: file changes named tuple
805
810
806 """
811 """
807 new_status = ChangesetStatus.get_status_lbl(
812 new_status = ChangesetStatus.get_status_lbl(
808 ChangesetStatus.STATUS_UNDER_REVIEW)
813 ChangesetStatus.STATUS_UNDER_REVIEW)
809
814
810 changed_files = (
815 changed_files = (
811 file_changes.added + file_changes.modified + file_changes.removed)
816 file_changes.added + file_changes.modified + file_changes.removed)
812
817
813 params = {
818 params = {
814 'under_review_label': new_status,
819 'under_review_label': new_status,
815 'added_commits': changes.added,
820 'added_commits': changes.added,
816 'removed_commits': changes.removed,
821 'removed_commits': changes.removed,
817 'changed_files': changed_files,
822 'changed_files': changed_files,
818 'added_files': file_changes.added,
823 'added_files': file_changes.added,
819 'modified_files': file_changes.modified,
824 'modified_files': file_changes.modified,
820 'removed_files': file_changes.removed,
825 'removed_files': file_changes.removed,
821 }
826 }
822 renderer = RstTemplateRenderer()
827 renderer = RstTemplateRenderer()
823 return renderer.render('pull_request_update.mako', **params)
828 return renderer.render('pull_request_update.mako', **params)
824
829
825 def edit(self, pull_request, title, description):
830 def edit(self, pull_request, title, description):
826 pull_request = self.__get_pull_request(pull_request)
831 pull_request = self.__get_pull_request(pull_request)
827 if pull_request.is_closed():
832 if pull_request.is_closed():
828 raise ValueError('This pull request is closed')
833 raise ValueError('This pull request is closed')
829 if title:
834 if title:
830 pull_request.title = title
835 pull_request.title = title
831 pull_request.description = description
836 pull_request.description = description
832 pull_request.updated_on = datetime.datetime.now()
837 pull_request.updated_on = datetime.datetime.now()
833 Session().add(pull_request)
838 Session().add(pull_request)
834
839
835 def update_reviewers(self, pull_request, reviewer_data):
840 def update_reviewers(self, pull_request, reviewer_data):
836 """
841 """
837 Update the reviewers in the pull request
842 Update the reviewers in the pull request
838
843
839 :param pull_request: the pr to update
844 :param pull_request: the pr to update
840 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
845 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
841 """
846 """
842
847
843 reviewers_reasons = {}
848 reviewers_reasons = {}
844 for user_id, reasons in reviewer_data:
849 for user_id, reasons in reviewer_data:
845 if isinstance(user_id, (int, basestring)):
850 if isinstance(user_id, (int, basestring)):
846 user_id = self._get_user(user_id).user_id
851 user_id = self._get_user(user_id).user_id
847 reviewers_reasons[user_id] = reasons
852 reviewers_reasons[user_id] = reasons
848
853
849 reviewers_ids = set(reviewers_reasons.keys())
854 reviewers_ids = set(reviewers_reasons.keys())
850 pull_request = self.__get_pull_request(pull_request)
855 pull_request = self.__get_pull_request(pull_request)
851 current_reviewers = PullRequestReviewers.query()\
856 current_reviewers = PullRequestReviewers.query()\
852 .filter(PullRequestReviewers.pull_request ==
857 .filter(PullRequestReviewers.pull_request ==
853 pull_request).all()
858 pull_request).all()
854 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
859 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
855
860
856 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
861 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
857 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
862 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
858
863
859 log.debug("Adding %s reviewers", ids_to_add)
864 log.debug("Adding %s reviewers", ids_to_add)
860 log.debug("Removing %s reviewers", ids_to_remove)
865 log.debug("Removing %s reviewers", ids_to_remove)
861 changed = False
866 changed = False
862 for uid in ids_to_add:
867 for uid in ids_to_add:
863 changed = True
868 changed = True
864 _usr = self._get_user(uid)
869 _usr = self._get_user(uid)
865 reasons = reviewers_reasons[uid]
870 reasons = reviewers_reasons[uid]
866 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
871 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
867 Session().add(reviewer)
872 Session().add(reviewer)
868
873
869 self.notify_reviewers(pull_request, ids_to_add)
874 self.notify_reviewers(pull_request, ids_to_add)
870
875
871 for uid in ids_to_remove:
876 for uid in ids_to_remove:
872 changed = True
877 changed = True
873 reviewer = PullRequestReviewers.query()\
878 reviewer = PullRequestReviewers.query()\
874 .filter(PullRequestReviewers.user_id == uid,
879 .filter(PullRequestReviewers.user_id == uid,
875 PullRequestReviewers.pull_request == pull_request)\
880 PullRequestReviewers.pull_request == pull_request)\
876 .scalar()
881 .scalar()
877 if reviewer:
882 if reviewer:
878 Session().delete(reviewer)
883 Session().delete(reviewer)
879 if changed:
884 if changed:
880 pull_request.updated_on = datetime.datetime.now()
885 pull_request.updated_on = datetime.datetime.now()
881 Session().add(pull_request)
886 Session().add(pull_request)
882
887
883 return ids_to_add, ids_to_remove
888 return ids_to_add, ids_to_remove
884
889
885 def get_url(self, pull_request):
890 def get_url(self, pull_request):
886 return h.url('pullrequest_show',
891 return h.url('pullrequest_show',
887 repo_name=safe_str(pull_request.target_repo.repo_name),
892 repo_name=safe_str(pull_request.target_repo.repo_name),
888 pull_request_id=pull_request.pull_request_id,
893 pull_request_id=pull_request.pull_request_id,
889 qualified=True)
894 qualified=True)
890
895
891 def get_shadow_clone_url(self, pull_request):
896 def get_shadow_clone_url(self, pull_request):
892 """
897 """
893 Returns qualified url pointing to the shadow repository. If this pull
898 Returns qualified url pointing to the shadow repository. If this pull
894 request is closed there is no shadow repository and ``None`` will be
899 request is closed there is no shadow repository and ``None`` will be
895 returned.
900 returned.
896 """
901 """
897 if pull_request.is_closed():
902 if pull_request.is_closed():
898 return None
903 return None
899 else:
904 else:
900 pr_url = urllib.unquote(self.get_url(pull_request))
905 pr_url = urllib.unquote(self.get_url(pull_request))
901 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
906 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
902
907
903 def notify_reviewers(self, pull_request, reviewers_ids):
908 def notify_reviewers(self, pull_request, reviewers_ids):
904 # notification to reviewers
909 # notification to reviewers
905 if not reviewers_ids:
910 if not reviewers_ids:
906 return
911 return
907
912
908 pull_request_obj = pull_request
913 pull_request_obj = pull_request
909 # get the current participants of this pull request
914 # get the current participants of this pull request
910 recipients = reviewers_ids
915 recipients = reviewers_ids
911 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
916 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
912
917
913 pr_source_repo = pull_request_obj.source_repo
918 pr_source_repo = pull_request_obj.source_repo
914 pr_target_repo = pull_request_obj.target_repo
919 pr_target_repo = pull_request_obj.target_repo
915
920
916 pr_url = h.url(
921 pr_url = h.url(
917 'pullrequest_show',
922 'pullrequest_show',
918 repo_name=pr_target_repo.repo_name,
923 repo_name=pr_target_repo.repo_name,
919 pull_request_id=pull_request_obj.pull_request_id,
924 pull_request_id=pull_request_obj.pull_request_id,
920 qualified=True,)
925 qualified=True,)
921
926
922 # set some variables for email notification
927 # set some variables for email notification
923 pr_target_repo_url = h.url(
928 pr_target_repo_url = h.url(
924 'summary_home',
929 'summary_home',
925 repo_name=pr_target_repo.repo_name,
930 repo_name=pr_target_repo.repo_name,
926 qualified=True)
931 qualified=True)
927
932
928 pr_source_repo_url = h.url(
933 pr_source_repo_url = h.url(
929 'summary_home',
934 'summary_home',
930 repo_name=pr_source_repo.repo_name,
935 repo_name=pr_source_repo.repo_name,
931 qualified=True)
936 qualified=True)
932
937
933 # pull request specifics
938 # pull request specifics
934 pull_request_commits = [
939 pull_request_commits = [
935 (x.raw_id, x.message)
940 (x.raw_id, x.message)
936 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
941 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
937
942
938 kwargs = {
943 kwargs = {
939 'user': pull_request.author,
944 'user': pull_request.author,
940 'pull_request': pull_request_obj,
945 'pull_request': pull_request_obj,
941 'pull_request_commits': pull_request_commits,
946 'pull_request_commits': pull_request_commits,
942
947
943 'pull_request_target_repo': pr_target_repo,
948 'pull_request_target_repo': pr_target_repo,
944 'pull_request_target_repo_url': pr_target_repo_url,
949 'pull_request_target_repo_url': pr_target_repo_url,
945
950
946 'pull_request_source_repo': pr_source_repo,
951 'pull_request_source_repo': pr_source_repo,
947 'pull_request_source_repo_url': pr_source_repo_url,
952 'pull_request_source_repo_url': pr_source_repo_url,
948
953
949 'pull_request_url': pr_url,
954 'pull_request_url': pr_url,
950 }
955 }
951
956
952 # pre-generate the subject for notification itself
957 # pre-generate the subject for notification itself
953 (subject,
958 (subject,
954 _h, _e, # we don't care about those
959 _h, _e, # we don't care about those
955 body_plaintext) = EmailNotificationModel().render_email(
960 body_plaintext) = EmailNotificationModel().render_email(
956 notification_type, **kwargs)
961 notification_type, **kwargs)
957
962
958 # create notification objects, and emails
963 # create notification objects, and emails
959 NotificationModel().create(
964 NotificationModel().create(
960 created_by=pull_request.author,
965 created_by=pull_request.author,
961 notification_subject=subject,
966 notification_subject=subject,
962 notification_body=body_plaintext,
967 notification_body=body_plaintext,
963 notification_type=notification_type,
968 notification_type=notification_type,
964 recipients=recipients,
969 recipients=recipients,
965 email_kwargs=kwargs,
970 email_kwargs=kwargs,
966 )
971 )
967
972
968 def delete(self, pull_request):
973 def delete(self, pull_request):
969 pull_request = self.__get_pull_request(pull_request)
974 pull_request = self.__get_pull_request(pull_request)
970 self._cleanup_merge_workspace(pull_request)
975 self._cleanup_merge_workspace(pull_request)
971 Session().delete(pull_request)
976 Session().delete(pull_request)
972
977
973 def close_pull_request(self, pull_request, user):
978 def close_pull_request(self, pull_request, user):
974 pull_request = self.__get_pull_request(pull_request)
979 pull_request = self.__get_pull_request(pull_request)
975 self._cleanup_merge_workspace(pull_request)
980 self._cleanup_merge_workspace(pull_request)
976 pull_request.status = PullRequest.STATUS_CLOSED
981 pull_request.status = PullRequest.STATUS_CLOSED
977 pull_request.updated_on = datetime.datetime.now()
982 pull_request.updated_on = datetime.datetime.now()
978 Session().add(pull_request)
983 Session().add(pull_request)
979 self._trigger_pull_request_hook(
984 self._trigger_pull_request_hook(
980 pull_request, pull_request.author, 'close')
985 pull_request, pull_request.author, 'close')
981 self._log_action('user_closed_pull_request', user, pull_request)
986 self._log_action('user_closed_pull_request', user, pull_request)
982
987
983 def close_pull_request_with_comment(self, pull_request, user, repo,
988 def close_pull_request_with_comment(self, pull_request, user, repo,
984 message=None):
989 message=None):
985 status = ChangesetStatus.STATUS_REJECTED
990 status = ChangesetStatus.STATUS_REJECTED
986
991
987 if not message:
992 if not message:
988 message = (
993 message = (
989 _('Status change %(transition_icon)s %(status)s') % {
994 _('Status change %(transition_icon)s %(status)s') % {
990 'transition_icon': '>',
995 'transition_icon': '>',
991 'status': ChangesetStatus.get_status_lbl(status)})
996 'status': ChangesetStatus.get_status_lbl(status)})
992
997
993 internal_message = _('Closing with') + ' ' + message
998 internal_message = _('Closing with') + ' ' + message
994
999
995 comm = ChangesetCommentsModel().create(
1000 comm = ChangesetCommentsModel().create(
996 text=internal_message,
1001 text=internal_message,
997 repo=repo.repo_id,
1002 repo=repo.repo_id,
998 user=user.user_id,
1003 user=user.user_id,
999 pull_request=pull_request.pull_request_id,
1004 pull_request=pull_request.pull_request_id,
1000 f_path=None,
1005 f_path=None,
1001 line_no=None,
1006 line_no=None,
1002 status_change=ChangesetStatus.get_status_lbl(status),
1007 status_change=ChangesetStatus.get_status_lbl(status),
1003 status_change_type=status,
1008 status_change_type=status,
1004 closing_pr=True
1009 closing_pr=True
1005 )
1010 )
1006
1011
1007 ChangesetStatusModel().set_status(
1012 ChangesetStatusModel().set_status(
1008 repo.repo_id,
1013 repo.repo_id,
1009 status,
1014 status,
1010 user.user_id,
1015 user.user_id,
1011 comm,
1016 comm,
1012 pull_request=pull_request.pull_request_id
1017 pull_request=pull_request.pull_request_id
1013 )
1018 )
1014 Session().flush()
1019 Session().flush()
1015
1020
1016 PullRequestModel().close_pull_request(
1021 PullRequestModel().close_pull_request(
1017 pull_request.pull_request_id, user)
1022 pull_request.pull_request_id, user)
1018
1023
1019 def merge_status(self, pull_request):
1024 def merge_status(self, pull_request):
1020 if not self._is_merge_enabled(pull_request):
1025 if not self._is_merge_enabled(pull_request):
1021 return False, _('Server-side pull request merging is disabled.')
1026 return False, _('Server-side pull request merging is disabled.')
1022 if pull_request.is_closed():
1027 if pull_request.is_closed():
1023 return False, _('This pull request is closed.')
1028 return False, _('This pull request is closed.')
1024 merge_possible, msg = self._check_repo_requirements(
1029 merge_possible, msg = self._check_repo_requirements(
1025 target=pull_request.target_repo, source=pull_request.source_repo)
1030 target=pull_request.target_repo, source=pull_request.source_repo)
1026 if not merge_possible:
1031 if not merge_possible:
1027 return merge_possible, msg
1032 return merge_possible, msg
1028
1033
1029 try:
1034 try:
1030 resp = self._try_merge(pull_request)
1035 resp = self._try_merge(pull_request)
1031 log.debug("Merge response: %s", resp)
1036 log.debug("Merge response: %s", resp)
1032 status = resp.possible, self.merge_status_message(
1037 status = resp.possible, self.merge_status_message(
1033 resp.failure_reason)
1038 resp.failure_reason)
1034 except NotImplementedError:
1039 except NotImplementedError:
1035 status = False, _('Pull request merging is not supported.')
1040 status = False, _('Pull request merging is not supported.')
1036
1041
1037 return status
1042 return status
1038
1043
1039 def _check_repo_requirements(self, target, source):
1044 def _check_repo_requirements(self, target, source):
1040 """
1045 """
1041 Check if `target` and `source` have compatible requirements.
1046 Check if `target` and `source` have compatible requirements.
1042
1047
1043 Currently this is just checking for largefiles.
1048 Currently this is just checking for largefiles.
1044 """
1049 """
1045 target_has_largefiles = self._has_largefiles(target)
1050 target_has_largefiles = self._has_largefiles(target)
1046 source_has_largefiles = self._has_largefiles(source)
1051 source_has_largefiles = self._has_largefiles(source)
1047 merge_possible = True
1052 merge_possible = True
1048 message = u''
1053 message = u''
1049
1054
1050 if target_has_largefiles != source_has_largefiles:
1055 if target_has_largefiles != source_has_largefiles:
1051 merge_possible = False
1056 merge_possible = False
1052 if source_has_largefiles:
1057 if source_has_largefiles:
1053 message = _(
1058 message = _(
1054 'Target repository large files support is disabled.')
1059 'Target repository large files support is disabled.')
1055 else:
1060 else:
1056 message = _(
1061 message = _(
1057 'Source repository large files support is disabled.')
1062 'Source repository large files support is disabled.')
1058
1063
1059 return merge_possible, message
1064 return merge_possible, message
1060
1065
1061 def _has_largefiles(self, repo):
1066 def _has_largefiles(self, repo):
1062 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1067 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1063 'extensions', 'largefiles')
1068 'extensions', 'largefiles')
1064 return largefiles_ui and largefiles_ui[0].active
1069 return largefiles_ui and largefiles_ui[0].active
1065
1070
1066 def _try_merge(self, pull_request):
1071 def _try_merge(self, pull_request):
1067 """
1072 """
1068 Try to merge the pull request and return the merge status.
1073 Try to merge the pull request and return the merge status.
1069 """
1074 """
1070 log.debug(
1075 log.debug(
1071 "Trying out if the pull request %s can be merged.",
1076 "Trying out if the pull request %s can be merged.",
1072 pull_request.pull_request_id)
1077 pull_request.pull_request_id)
1073 target_vcs = pull_request.target_repo.scm_instance()
1078 target_vcs = pull_request.target_repo.scm_instance()
1074
1079
1075 # Refresh the target reference.
1080 # Refresh the target reference.
1076 try:
1081 try:
1077 target_ref = self._refresh_reference(
1082 target_ref = self._refresh_reference(
1078 pull_request.target_ref_parts, target_vcs)
1083 pull_request.target_ref_parts, target_vcs)
1079 except CommitDoesNotExistError:
1084 except CommitDoesNotExistError:
1080 merge_state = MergeResponse(
1085 merge_state = MergeResponse(
1081 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1086 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1082 return merge_state
1087 return merge_state
1083
1088
1084 target_locked = pull_request.target_repo.locked
1089 target_locked = pull_request.target_repo.locked
1085 if target_locked and target_locked[0]:
1090 if target_locked and target_locked[0]:
1086 log.debug("The target repository is locked.")
1091 log.debug("The target repository is locked.")
1087 merge_state = MergeResponse(
1092 merge_state = MergeResponse(
1088 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1093 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1089 elif self._needs_merge_state_refresh(pull_request, target_ref):
1094 elif self._needs_merge_state_refresh(pull_request, target_ref):
1090 log.debug("Refreshing the merge status of the repository.")
1095 log.debug("Refreshing the merge status of the repository.")
1091 merge_state = self._refresh_merge_state(
1096 merge_state = self._refresh_merge_state(
1092 pull_request, target_vcs, target_ref)
1097 pull_request, target_vcs, target_ref)
1093 else:
1098 else:
1094 possible = pull_request.\
1099 possible = pull_request.\
1095 _last_merge_status == MergeFailureReason.NONE
1100 _last_merge_status == MergeFailureReason.NONE
1096 merge_state = MergeResponse(
1101 merge_state = MergeResponse(
1097 possible, False, None, pull_request._last_merge_status)
1102 possible, False, None, pull_request._last_merge_status)
1098
1103
1099 return merge_state
1104 return merge_state
1100
1105
1101 def _refresh_reference(self, reference, vcs_repository):
1106 def _refresh_reference(self, reference, vcs_repository):
1102 if reference.type in ('branch', 'book'):
1107 if reference.type in ('branch', 'book'):
1103 name_or_id = reference.name
1108 name_or_id = reference.name
1104 else:
1109 else:
1105 name_or_id = reference.commit_id
1110 name_or_id = reference.commit_id
1106 refreshed_commit = vcs_repository.get_commit(name_or_id)
1111 refreshed_commit = vcs_repository.get_commit(name_or_id)
1107 refreshed_reference = Reference(
1112 refreshed_reference = Reference(
1108 reference.type, reference.name, refreshed_commit.raw_id)
1113 reference.type, reference.name, refreshed_commit.raw_id)
1109 return refreshed_reference
1114 return refreshed_reference
1110
1115
1111 def _needs_merge_state_refresh(self, pull_request, target_reference):
1116 def _needs_merge_state_refresh(self, pull_request, target_reference):
1112 return not(
1117 return not(
1113 pull_request.revisions and
1118 pull_request.revisions and
1114 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1119 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1115 target_reference.commit_id == pull_request._last_merge_target_rev)
1120 target_reference.commit_id == pull_request._last_merge_target_rev)
1116
1121
1117 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1122 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1118 workspace_id = self._workspace_id(pull_request)
1123 workspace_id = self._workspace_id(pull_request)
1119 source_vcs = pull_request.source_repo.scm_instance()
1124 source_vcs = pull_request.source_repo.scm_instance()
1120 use_rebase = self._use_rebase_for_merging(pull_request)
1125 use_rebase = self._use_rebase_for_merging(pull_request)
1121 merge_state = target_vcs.merge(
1126 merge_state = target_vcs.merge(
1122 target_reference, source_vcs, pull_request.source_ref_parts,
1127 target_reference, source_vcs, pull_request.source_ref_parts,
1123 workspace_id, dry_run=True, use_rebase=use_rebase)
1128 workspace_id, dry_run=True, use_rebase=use_rebase)
1124
1129
1125 # Do not store the response if there was an unknown error.
1130 # Do not store the response if there was an unknown error.
1126 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1131 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1127 pull_request._last_merge_source_rev = \
1132 pull_request._last_merge_source_rev = \
1128 pull_request.source_ref_parts.commit_id
1133 pull_request.source_ref_parts.commit_id
1129 pull_request._last_merge_target_rev = target_reference.commit_id
1134 pull_request._last_merge_target_rev = target_reference.commit_id
1130 pull_request._last_merge_status = merge_state.failure_reason
1135 pull_request._last_merge_status = merge_state.failure_reason
1131 pull_request.shadow_merge_ref = merge_state.merge_ref
1136 pull_request.shadow_merge_ref = merge_state.merge_ref
1132 Session().add(pull_request)
1137 Session().add(pull_request)
1133 Session().commit()
1138 Session().commit()
1134
1139
1135 return merge_state
1140 return merge_state
1136
1141
1137 def _workspace_id(self, pull_request):
1142 def _workspace_id(self, pull_request):
1138 workspace_id = 'pr-%s' % pull_request.pull_request_id
1143 workspace_id = 'pr-%s' % pull_request.pull_request_id
1139 return workspace_id
1144 return workspace_id
1140
1145
1141 def merge_status_message(self, status_code):
1146 def merge_status_message(self, status_code):
1142 """
1147 """
1143 Return a human friendly error message for the given merge status code.
1148 Return a human friendly error message for the given merge status code.
1144 """
1149 """
1145 return self.MERGE_STATUS_MESSAGES[status_code]
1150 return self.MERGE_STATUS_MESSAGES[status_code]
1146
1151
1147 def generate_repo_data(self, repo, commit_id=None, branch=None,
1152 def generate_repo_data(self, repo, commit_id=None, branch=None,
1148 bookmark=None):
1153 bookmark=None):
1149 all_refs, selected_ref = \
1154 all_refs, selected_ref = \
1150 self._get_repo_pullrequest_sources(
1155 self._get_repo_pullrequest_sources(
1151 repo.scm_instance(), commit_id=commit_id,
1156 repo.scm_instance(), commit_id=commit_id,
1152 branch=branch, bookmark=bookmark)
1157 branch=branch, bookmark=bookmark)
1153
1158
1154 refs_select2 = []
1159 refs_select2 = []
1155 for element in all_refs:
1160 for element in all_refs:
1156 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1161 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1157 refs_select2.append({'text': element[1], 'children': children})
1162 refs_select2.append({'text': element[1], 'children': children})
1158
1163
1159 return {
1164 return {
1160 'user': {
1165 'user': {
1161 'user_id': repo.user.user_id,
1166 'user_id': repo.user.user_id,
1162 'username': repo.user.username,
1167 'username': repo.user.username,
1163 'firstname': repo.user.firstname,
1168 'firstname': repo.user.firstname,
1164 'lastname': repo.user.lastname,
1169 'lastname': repo.user.lastname,
1165 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1170 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1166 },
1171 },
1167 'description': h.chop_at_smart(repo.description, '\n'),
1172 'description': h.chop_at_smart(repo.description, '\n'),
1168 'refs': {
1173 'refs': {
1169 'all_refs': all_refs,
1174 'all_refs': all_refs,
1170 'selected_ref': selected_ref,
1175 'selected_ref': selected_ref,
1171 'select2_refs': refs_select2
1176 'select2_refs': refs_select2
1172 }
1177 }
1173 }
1178 }
1174
1179
1175 def generate_pullrequest_title(self, source, source_ref, target):
1180 def generate_pullrequest_title(self, source, source_ref, target):
1176 return u'{source}#{at_ref} to {target}'.format(
1181 return u'{source}#{at_ref} to {target}'.format(
1177 source=source,
1182 source=source,
1178 at_ref=source_ref,
1183 at_ref=source_ref,
1179 target=target,
1184 target=target,
1180 )
1185 )
1181
1186
1182 def _cleanup_merge_workspace(self, pull_request):
1187 def _cleanup_merge_workspace(self, pull_request):
1183 # Merging related cleanup
1188 # Merging related cleanup
1184 target_scm = pull_request.target_repo.scm_instance()
1189 target_scm = pull_request.target_repo.scm_instance()
1185 workspace_id = 'pr-%s' % pull_request.pull_request_id
1190 workspace_id = 'pr-%s' % pull_request.pull_request_id
1186
1191
1187 try:
1192 try:
1188 target_scm.cleanup_merge_workspace(workspace_id)
1193 target_scm.cleanup_merge_workspace(workspace_id)
1189 except NotImplementedError:
1194 except NotImplementedError:
1190 pass
1195 pass
1191
1196
1192 def _get_repo_pullrequest_sources(
1197 def _get_repo_pullrequest_sources(
1193 self, repo, commit_id=None, branch=None, bookmark=None):
1198 self, repo, commit_id=None, branch=None, bookmark=None):
1194 """
1199 """
1195 Return a structure with repo's interesting commits, suitable for
1200 Return a structure with repo's interesting commits, suitable for
1196 the selectors in pullrequest controller
1201 the selectors in pullrequest controller
1197
1202
1198 :param commit_id: a commit that must be in the list somehow
1203 :param commit_id: a commit that must be in the list somehow
1199 and selected by default
1204 and selected by default
1200 :param branch: a branch that must be in the list and selected
1205 :param branch: a branch that must be in the list and selected
1201 by default - even if closed
1206 by default - even if closed
1202 :param bookmark: a bookmark that must be in the list and selected
1207 :param bookmark: a bookmark that must be in the list and selected
1203 """
1208 """
1204
1209
1205 commit_id = safe_str(commit_id) if commit_id else None
1210 commit_id = safe_str(commit_id) if commit_id else None
1206 branch = safe_str(branch) if branch else None
1211 branch = safe_str(branch) if branch else None
1207 bookmark = safe_str(bookmark) if bookmark else None
1212 bookmark = safe_str(bookmark) if bookmark else None
1208
1213
1209 selected = None
1214 selected = None
1210
1215
1211 # order matters: first source that has commit_id in it will be selected
1216 # order matters: first source that has commit_id in it will be selected
1212 sources = []
1217 sources = []
1213 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1218 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1214 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1219 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1215
1220
1216 if commit_id:
1221 if commit_id:
1217 ref_commit = (h.short_id(commit_id), commit_id)
1222 ref_commit = (h.short_id(commit_id), commit_id)
1218 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1223 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1219
1224
1220 sources.append(
1225 sources.append(
1221 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1226 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1222 )
1227 )
1223
1228
1224 groups = []
1229 groups = []
1225 for group_key, ref_list, group_name, match in sources:
1230 for group_key, ref_list, group_name, match in sources:
1226 group_refs = []
1231 group_refs = []
1227 for ref_name, ref_id in ref_list:
1232 for ref_name, ref_id in ref_list:
1228 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1233 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1229 group_refs.append((ref_key, ref_name))
1234 group_refs.append((ref_key, ref_name))
1230
1235
1231 if not selected:
1236 if not selected:
1232 if set([commit_id, match]) & set([ref_id, ref_name]):
1237 if set([commit_id, match]) & set([ref_id, ref_name]):
1233 selected = ref_key
1238 selected = ref_key
1234
1239
1235 if group_refs:
1240 if group_refs:
1236 groups.append((group_refs, group_name))
1241 groups.append((group_refs, group_name))
1237
1242
1238 if not selected:
1243 if not selected:
1239 ref = commit_id or branch or bookmark
1244 ref = commit_id or branch or bookmark
1240 if ref:
1245 if ref:
1241 raise CommitDoesNotExistError(
1246 raise CommitDoesNotExistError(
1242 'No commit refs could be found matching: %s' % ref)
1247 'No commit refs could be found matching: %s' % ref)
1243 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1248 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1244 selected = 'branch:%s:%s' % (
1249 selected = 'branch:%s:%s' % (
1245 repo.DEFAULT_BRANCH_NAME,
1250 repo.DEFAULT_BRANCH_NAME,
1246 repo.branches[repo.DEFAULT_BRANCH_NAME]
1251 repo.branches[repo.DEFAULT_BRANCH_NAME]
1247 )
1252 )
1248 elif repo.commit_ids:
1253 elif repo.commit_ids:
1249 rev = repo.commit_ids[0]
1254 rev = repo.commit_ids[0]
1250 selected = 'rev:%s:%s' % (rev, rev)
1255 selected = 'rev:%s:%s' % (rev, rev)
1251 else:
1256 else:
1252 raise EmptyRepositoryError()
1257 raise EmptyRepositoryError()
1253 return groups, selected
1258 return groups, selected
1254
1259
1255 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1260 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1256 pull_request = self.__get_pull_request(pull_request)
1261 pull_request = self.__get_pull_request(pull_request)
1257 return self._get_diff_from_pr_or_version(pull_request, context=context)
1262 return self._get_diff_from_pr_or_version(pull_request, context=context)
1258
1263
1259 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1264 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1260 source_repo = pr_or_version.source_repo
1265 source_repo = pr_or_version.source_repo
1261
1266
1262 # we swap org/other ref since we run a simple diff on one repo
1267 # we swap org/other ref since we run a simple diff on one repo
1263 target_ref_id = pr_or_version.target_ref_parts.commit_id
1268 target_ref_id = pr_or_version.target_ref_parts.commit_id
1264 source_ref_id = pr_or_version.source_ref_parts.commit_id
1269 source_ref_id = pr_or_version.source_ref_parts.commit_id
1265 target_commit = source_repo.get_commit(
1270 target_commit = source_repo.get_commit(
1266 commit_id=safe_str(target_ref_id))
1271 commit_id=safe_str(target_ref_id))
1267 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1272 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1268 vcs_repo = source_repo.scm_instance()
1273 vcs_repo = source_repo.scm_instance()
1269
1274
1270 # TODO: johbo: In the context of an update, we cannot reach
1275 # TODO: johbo: In the context of an update, we cannot reach
1271 # the old commit anymore with our normal mechanisms. It needs
1276 # the old commit anymore with our normal mechanisms. It needs
1272 # some sort of special support in the vcs layer to avoid this
1277 # some sort of special support in the vcs layer to avoid this
1273 # workaround.
1278 # workaround.
1274 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1279 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1275 vcs_repo.alias == 'git'):
1280 vcs_repo.alias == 'git'):
1276 source_commit.raw_id = safe_str(source_ref_id)
1281 source_commit.raw_id = safe_str(source_ref_id)
1277
1282
1278 log.debug('calculating diff between '
1283 log.debug('calculating diff between '
1279 'source_ref:%s and target_ref:%s for repo `%s`',
1284 'source_ref:%s and target_ref:%s for repo `%s`',
1280 target_ref_id, source_ref_id,
1285 target_ref_id, source_ref_id,
1281 safe_unicode(vcs_repo.path))
1286 safe_unicode(vcs_repo.path))
1282
1287
1283 vcs_diff = vcs_repo.get_diff(
1288 vcs_diff = vcs_repo.get_diff(
1284 commit1=target_commit, commit2=source_commit, context=context)
1289 commit1=target_commit, commit2=source_commit, context=context)
1285 return vcs_diff
1290 return vcs_diff
1286
1291
1287 def _is_merge_enabled(self, pull_request):
1292 def _is_merge_enabled(self, pull_request):
1288 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1293 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1289 settings = settings_model.get_general_settings()
1294 settings = settings_model.get_general_settings()
1290 return settings.get('rhodecode_pr_merge_enabled', False)
1295 return settings.get('rhodecode_pr_merge_enabled', False)
1291
1296
1292 def _use_rebase_for_merging(self, pull_request):
1297 def _use_rebase_for_merging(self, pull_request):
1293 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1298 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1294 settings = settings_model.get_general_settings()
1299 settings = settings_model.get_general_settings()
1295 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1300 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1296
1301
1297 def _log_action(self, action, user, pull_request):
1302 def _log_action(self, action, user, pull_request):
1298 action_logger(
1303 action_logger(
1299 user,
1304 user,
1300 '{action}:{pr_id}'.format(
1305 '{action}:{pr_id}'.format(
1301 action=action, pr_id=pull_request.pull_request_id),
1306 action=action, pr_id=pull_request.pull_request_id),
1302 pull_request.target_repo)
1307 pull_request.target_repo)
1303
1308
1304
1309
1305 ChangeTuple = namedtuple('ChangeTuple',
1310 ChangeTuple = namedtuple('ChangeTuple',
1306 ['added', 'common', 'removed'])
1311 ['added', 'common', 'removed'])
1307
1312
1308 FileChangeTuple = namedtuple('FileChangeTuple',
1313 FileChangeTuple = namedtuple('FileChangeTuple',
1309 ['added', 'modified', 'removed'])
1314 ['added', 'modified', 'removed'])
@@ -1,2179 +1,2184 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29
29
30 //--- BASE ------------------//
30 //--- BASE ------------------//
31 .noscript-error {
31 .noscript-error {
32 top: 0;
32 top: 0;
33 left: 0;
33 left: 0;
34 width: 100%;
34 width: 100%;
35 z-index: 101;
35 z-index: 101;
36 text-align: center;
36 text-align: center;
37 font-family: @text-semibold;
37 font-family: @text-semibold;
38 font-size: 120%;
38 font-size: 120%;
39 color: white;
39 color: white;
40 background-color: @alert2;
40 background-color: @alert2;
41 padding: 5px 0 5px 0;
41 padding: 5px 0 5px 0;
42 }
42 }
43
43
44 html {
44 html {
45 display: table;
45 display: table;
46 height: 100%;
46 height: 100%;
47 width: 100%;
47 width: 100%;
48 }
48 }
49
49
50 body {
50 body {
51 display: table-cell;
51 display: table-cell;
52 width: 100%;
52 width: 100%;
53 }
53 }
54
54
55 //--- LAYOUT ------------------//
55 //--- LAYOUT ------------------//
56
56
57 .hidden{
57 .hidden{
58 display: none !important;
58 display: none !important;
59 }
59 }
60
60
61 .box{
61 .box{
62 float: left;
62 float: left;
63 width: 100%;
63 width: 100%;
64 }
64 }
65
65
66 .browser-header {
66 .browser-header {
67 clear: both;
67 clear: both;
68 }
68 }
69 .main {
69 .main {
70 clear: both;
70 clear: both;
71 padding:0 0 @pagepadding;
71 padding:0 0 @pagepadding;
72 height: auto;
72 height: auto;
73
73
74 &:after { //clearfix
74 &:after { //clearfix
75 content:"";
75 content:"";
76 clear:both;
76 clear:both;
77 width:100%;
77 width:100%;
78 display:block;
78 display:block;
79 }
79 }
80 }
80 }
81
81
82 .action-link{
82 .action-link{
83 margin-left: @padding;
83 margin-left: @padding;
84 padding-left: @padding;
84 padding-left: @padding;
85 border-left: @border-thickness solid @border-default-color;
85 border-left: @border-thickness solid @border-default-color;
86 }
86 }
87
87
88 input + .action-link, .action-link.first{
88 input + .action-link, .action-link.first{
89 border-left: none;
89 border-left: none;
90 }
90 }
91
91
92 .action-link.last{
92 .action-link.last{
93 margin-right: @padding;
93 margin-right: @padding;
94 padding-right: @padding;
94 padding-right: @padding;
95 }
95 }
96
96
97 .action-link.active,
97 .action-link.active,
98 .action-link.active a{
98 .action-link.active a{
99 color: @grey4;
99 color: @grey4;
100 }
100 }
101
101
102 ul.simple-list{
102 ul.simple-list{
103 list-style: none;
103 list-style: none;
104 margin: 0;
104 margin: 0;
105 padding: 0;
105 padding: 0;
106 }
106 }
107
107
108 .main-content {
108 .main-content {
109 padding-bottom: @pagepadding;
109 padding-bottom: @pagepadding;
110 }
110 }
111
111
112 .wrapper {
112 .wrapper {
113 position: relative;
113 position: relative;
114 max-width: @wrapper-maxwidth;
114 max-width: @wrapper-maxwidth;
115 margin: 0 auto;
115 margin: 0 auto;
116 }
116 }
117
117
118 #content {
118 #content {
119 clear: both;
119 clear: both;
120 padding: 0 @contentpadding;
120 padding: 0 @contentpadding;
121 }
121 }
122
122
123 .advanced-settings-fields{
123 .advanced-settings-fields{
124 input{
124 input{
125 margin-left: @textmargin;
125 margin-left: @textmargin;
126 margin-right: @padding/2;
126 margin-right: @padding/2;
127 }
127 }
128 }
128 }
129
129
130 .cs_files_title {
130 .cs_files_title {
131 margin: @pagepadding 0 0;
131 margin: @pagepadding 0 0;
132 }
132 }
133
133
134 input.inline[type="file"] {
134 input.inline[type="file"] {
135 display: inline;
135 display: inline;
136 }
136 }
137
137
138 .error_page {
138 .error_page {
139 margin: 10% auto;
139 margin: 10% auto;
140
140
141 h1 {
141 h1 {
142 color: @grey2;
142 color: @grey2;
143 }
143 }
144
144
145 .alert {
145 .alert {
146 margin: @padding 0;
146 margin: @padding 0;
147 }
147 }
148
148
149 .error-branding {
149 .error-branding {
150 font-family: @text-semibold;
150 font-family: @text-semibold;
151 color: @grey4;
151 color: @grey4;
152 }
152 }
153
153
154 .error_message {
154 .error_message {
155 font-family: @text-regular;
155 font-family: @text-regular;
156 }
156 }
157
157
158 .sidebar {
158 .sidebar {
159 min-height: 275px;
159 min-height: 275px;
160 margin: 0;
160 margin: 0;
161 padding: 0 0 @sidebarpadding @sidebarpadding;
161 padding: 0 0 @sidebarpadding @sidebarpadding;
162 border: none;
162 border: none;
163 }
163 }
164
164
165 .main-content {
165 .main-content {
166 position: relative;
166 position: relative;
167 margin: 0 @sidebarpadding @sidebarpadding;
167 margin: 0 @sidebarpadding @sidebarpadding;
168 padding: 0 0 0 @sidebarpadding;
168 padding: 0 0 0 @sidebarpadding;
169 border-left: @border-thickness solid @grey5;
169 border-left: @border-thickness solid @grey5;
170
170
171 @media (max-width:767px) {
171 @media (max-width:767px) {
172 clear: both;
172 clear: both;
173 width: 100%;
173 width: 100%;
174 margin: 0;
174 margin: 0;
175 border: none;
175 border: none;
176 }
176 }
177 }
177 }
178
178
179 .inner-column {
179 .inner-column {
180 float: left;
180 float: left;
181 width: 29.75%;
181 width: 29.75%;
182 min-height: 150px;
182 min-height: 150px;
183 margin: @sidebarpadding 2% 0 0;
183 margin: @sidebarpadding 2% 0 0;
184 padding: 0 2% 0 0;
184 padding: 0 2% 0 0;
185 border-right: @border-thickness solid @grey5;
185 border-right: @border-thickness solid @grey5;
186
186
187 @media (max-width:767px) {
187 @media (max-width:767px) {
188 clear: both;
188 clear: both;
189 width: 100%;
189 width: 100%;
190 border: none;
190 border: none;
191 }
191 }
192
192
193 ul {
193 ul {
194 padding-left: 1.25em;
194 padding-left: 1.25em;
195 }
195 }
196
196
197 &:last-child {
197 &:last-child {
198 margin: @sidebarpadding 0 0;
198 margin: @sidebarpadding 0 0;
199 border: none;
199 border: none;
200 }
200 }
201
201
202 h4 {
202 h4 {
203 margin: 0 0 @padding;
203 margin: 0 0 @padding;
204 font-family: @text-semibold;
204 font-family: @text-semibold;
205 }
205 }
206 }
206 }
207 }
207 }
208 .error-page-logo {
208 .error-page-logo {
209 width: 130px;
209 width: 130px;
210 height: 160px;
210 height: 160px;
211 }
211 }
212
212
213 // HEADER
213 // HEADER
214 .header {
214 .header {
215
215
216 // TODO: johbo: Fix login pages, so that they work without a min-height
216 // TODO: johbo: Fix login pages, so that they work without a min-height
217 // for the header and then remove the min-height. I chose a smaller value
217 // for the header and then remove the min-height. I chose a smaller value
218 // intentionally here to avoid rendering issues in the main navigation.
218 // intentionally here to avoid rendering issues in the main navigation.
219 min-height: 49px;
219 min-height: 49px;
220
220
221 position: relative;
221 position: relative;
222 vertical-align: bottom;
222 vertical-align: bottom;
223 padding: 0 @header-padding;
223 padding: 0 @header-padding;
224 background-color: @grey2;
224 background-color: @grey2;
225 color: @grey5;
225 color: @grey5;
226
226
227 .title {
227 .title {
228 overflow: visible;
228 overflow: visible;
229 }
229 }
230
230
231 &:before,
231 &:before,
232 &:after {
232 &:after {
233 content: "";
233 content: "";
234 clear: both;
234 clear: both;
235 width: 100%;
235 width: 100%;
236 }
236 }
237
237
238 // TODO: johbo: Avoids breaking "Repositories" chooser
238 // TODO: johbo: Avoids breaking "Repositories" chooser
239 .select2-container .select2-choice .select2-arrow {
239 .select2-container .select2-choice .select2-arrow {
240 display: none;
240 display: none;
241 }
241 }
242 }
242 }
243
243
244 #header-inner {
244 #header-inner {
245 &.title {
245 &.title {
246 margin: 0;
246 margin: 0;
247 }
247 }
248 &:before,
248 &:before,
249 &:after {
249 &:after {
250 content: "";
250 content: "";
251 clear: both;
251 clear: both;
252 }
252 }
253 }
253 }
254
254
255 // Gists
255 // Gists
256 #files_data {
256 #files_data {
257 clear: both; //for firefox
257 clear: both; //for firefox
258 }
258 }
259 #gistid {
259 #gistid {
260 margin-right: @padding;
260 margin-right: @padding;
261 }
261 }
262
262
263 // Global Settings Editor
263 // Global Settings Editor
264 .textarea.editor {
264 .textarea.editor {
265 float: left;
265 float: left;
266 position: relative;
266 position: relative;
267 max-width: @texteditor-width;
267 max-width: @texteditor-width;
268
268
269 select {
269 select {
270 position: absolute;
270 position: absolute;
271 top:10px;
271 top:10px;
272 right:0;
272 right:0;
273 }
273 }
274
274
275 .CodeMirror {
275 .CodeMirror {
276 margin: 0;
276 margin: 0;
277 }
277 }
278
278
279 .help-block {
279 .help-block {
280 margin: 0 0 @padding;
280 margin: 0 0 @padding;
281 padding:.5em;
281 padding:.5em;
282 background-color: @grey6;
282 background-color: @grey6;
283 }
283 }
284 }
284 }
285
285
286 ul.auth_plugins {
286 ul.auth_plugins {
287 margin: @padding 0 @padding @legend-width;
287 margin: @padding 0 @padding @legend-width;
288 padding: 0;
288 padding: 0;
289
289
290 li {
290 li {
291 margin-bottom: @padding;
291 margin-bottom: @padding;
292 line-height: 1em;
292 line-height: 1em;
293 list-style-type: none;
293 list-style-type: none;
294
294
295 .auth_buttons .btn {
295 .auth_buttons .btn {
296 margin-right: @padding;
296 margin-right: @padding;
297 }
297 }
298
298
299 &:before { content: none; }
299 &:before { content: none; }
300 }
300 }
301 }
301 }
302
302
303
303
304 // My Account PR list
304 // My Account PR list
305
305
306 #show_closed {
306 #show_closed {
307 margin: 0 1em 0 0;
307 margin: 0 1em 0 0;
308 }
308 }
309
309
310 .pullrequestlist {
310 .pullrequestlist {
311 .closed {
311 .closed {
312 background-color: @grey6;
312 background-color: @grey6;
313 }
313 }
314 .td-status {
314 .td-status {
315 padding-left: .5em;
315 padding-left: .5em;
316 }
316 }
317 .log-container .truncate {
317 .log-container .truncate {
318 height: 2.75em;
318 height: 2.75em;
319 white-space: pre-line;
319 white-space: pre-line;
320 }
320 }
321 table.rctable .user {
321 table.rctable .user {
322 padding-left: 0;
322 padding-left: 0;
323 }
323 }
324 table.rctable {
324 table.rctable {
325 td.td-description,
325 td.td-description,
326 .rc-user {
326 .rc-user {
327 min-width: auto;
327 min-width: auto;
328 }
328 }
329 }
329 }
330 }
330 }
331
331
332 // Pull Requests
332 // Pull Requests
333
333
334 .pullrequests_section_head {
334 .pullrequests_section_head {
335 display: block;
335 display: block;
336 clear: both;
336 clear: both;
337 margin: @padding 0;
337 margin: @padding 0;
338 font-family: @text-bold;
338 font-family: @text-bold;
339 }
339 }
340
340
341 .pr-origininfo, .pr-targetinfo {
341 .pr-origininfo, .pr-targetinfo {
342 position: relative;
342 position: relative;
343
343
344 .tag {
344 .tag {
345 display: inline-block;
345 display: inline-block;
346 margin: 0 1em .5em 0;
346 margin: 0 1em .5em 0;
347 }
347 }
348
348
349 .clone-url {
349 .clone-url {
350 display: inline-block;
350 display: inline-block;
351 margin: 0 0 .5em 0;
351 margin: 0 0 .5em 0;
352 padding: 0;
352 padding: 0;
353 line-height: 1.2em;
353 line-height: 1.2em;
354 }
354 }
355 }
355 }
356
356
357 .pr-pullinfo {
357 .pr-pullinfo {
358 clear: both;
358 clear: both;
359 margin: .5em 0;
359 margin: .5em 0;
360 }
360 }
361
361
362 #pr-title-input {
362 #pr-title-input {
363 width: 72%;
363 width: 72%;
364 font-size: 1em;
364 font-size: 1em;
365 font-family: @text-bold;
365 font-family: @text-bold;
366 margin: 0;
366 margin: 0;
367 padding: 0 0 0 @padding/4;
367 padding: 0 0 0 @padding/4;
368 line-height: 1.7em;
368 line-height: 1.7em;
369 color: @text-color;
369 color: @text-color;
370 letter-spacing: .02em;
370 letter-spacing: .02em;
371 }
371 }
372
372
373 #pullrequest_title {
373 #pullrequest_title {
374 width: 100%;
374 width: 100%;
375 box-sizing: border-box;
375 box-sizing: border-box;
376 }
376 }
377
377
378 #pr_open_message {
378 #pr_open_message {
379 border: @border-thickness solid #fff;
379 border: @border-thickness solid #fff;
380 border-radius: @border-radius;
380 border-radius: @border-radius;
381 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
381 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
382 text-align: right;
382 text-align: right;
383 overflow: hidden;
383 overflow: hidden;
384 }
384 }
385
385
386 .pr-submit-button {
386 .pr-submit-button {
387 float: right;
387 float: right;
388 margin: 0 0 0 5px;
388 margin: 0 0 0 5px;
389 }
389 }
390
390
391 .pr-spacing-container {
391 .pr-spacing-container {
392 padding: 20px;
392 padding: 20px;
393 clear: both
393 clear: both
394 }
394 }
395
395
396 #pr-description-input {
396 #pr-description-input {
397 margin-bottom: 0;
397 margin-bottom: 0;
398 }
398 }
399
399
400 .pr-description-label {
400 .pr-description-label {
401 vertical-align: top;
401 vertical-align: top;
402 }
402 }
403
403
404 .perms_section_head {
404 .perms_section_head {
405 min-width: 625px;
405 min-width: 625px;
406
406
407 h2 {
407 h2 {
408 margin-bottom: 0;
408 margin-bottom: 0;
409 }
409 }
410
410
411 .label-checkbox {
411 .label-checkbox {
412 float: left;
412 float: left;
413 }
413 }
414
414
415 &.field {
415 &.field {
416 margin: @space 0 @padding;
416 margin: @space 0 @padding;
417 }
417 }
418
418
419 &:first-child.field {
419 &:first-child.field {
420 margin-top: 0;
420 margin-top: 0;
421
421
422 .label {
422 .label {
423 margin-top: 0;
423 margin-top: 0;
424 padding-top: 0;
424 padding-top: 0;
425 }
425 }
426
426
427 .radios {
427 .radios {
428 padding-top: 0;
428 padding-top: 0;
429 }
429 }
430 }
430 }
431
431
432 .radios {
432 .radios {
433 float: right;
433 float: right;
434 position: relative;
434 position: relative;
435 width: 405px;
435 width: 405px;
436 }
436 }
437 }
437 }
438
438
439 //--- MODULES ------------------//
439 //--- MODULES ------------------//
440
440
441
441
442 // Server Announcement
442 // Server Announcement
443 #server-announcement {
443 #server-announcement {
444 width: 95%;
444 width: 95%;
445 margin: @padding auto;
445 margin: @padding auto;
446 padding: @padding;
446 padding: @padding;
447 border-width: 2px;
447 border-width: 2px;
448 border-style: solid;
448 border-style: solid;
449 .border-radius(2px);
449 .border-radius(2px);
450 font-family: @text-bold;
450 font-family: @text-bold;
451
451
452 &.info { border-color: @alert4; background-color: @alert4-inner; }
452 &.info { border-color: @alert4; background-color: @alert4-inner; }
453 &.warning { border-color: @alert3; background-color: @alert3-inner; }
453 &.warning { border-color: @alert3; background-color: @alert3-inner; }
454 &.error { border-color: @alert2; background-color: @alert2-inner; }
454 &.error { border-color: @alert2; background-color: @alert2-inner; }
455 &.success { border-color: @alert1; background-color: @alert1-inner; }
455 &.success { border-color: @alert1; background-color: @alert1-inner; }
456 &.neutral { border-color: @grey3; background-color: @grey6; }
456 &.neutral { border-color: @grey3; background-color: @grey6; }
457 }
457 }
458
458
459 // Fixed Sidebar Column
459 // Fixed Sidebar Column
460 .sidebar-col-wrapper {
460 .sidebar-col-wrapper {
461 padding-left: @sidebar-all-width;
461 padding-left: @sidebar-all-width;
462
462
463 .sidebar {
463 .sidebar {
464 width: @sidebar-width;
464 width: @sidebar-width;
465 margin-left: -@sidebar-all-width;
465 margin-left: -@sidebar-all-width;
466 }
466 }
467 }
467 }
468
468
469 .sidebar-col-wrapper.scw-small {
469 .sidebar-col-wrapper.scw-small {
470 padding-left: @sidebar-small-all-width;
470 padding-left: @sidebar-small-all-width;
471
471
472 .sidebar {
472 .sidebar {
473 width: @sidebar-small-width;
473 width: @sidebar-small-width;
474 margin-left: -@sidebar-small-all-width;
474 margin-left: -@sidebar-small-all-width;
475 }
475 }
476 }
476 }
477
477
478
478
479 // FOOTER
479 // FOOTER
480 #footer {
480 #footer {
481 padding: 0;
481 padding: 0;
482 text-align: center;
482 text-align: center;
483 vertical-align: middle;
483 vertical-align: middle;
484 color: @grey2;
484 color: @grey2;
485 background-color: @grey6;
485 background-color: @grey6;
486
486
487 p {
487 p {
488 margin: 0;
488 margin: 0;
489 padding: 1em;
489 padding: 1em;
490 line-height: 1em;
490 line-height: 1em;
491 }
491 }
492
492
493 .server-instance { //server instance
493 .server-instance { //server instance
494 display: none;
494 display: none;
495 }
495 }
496
496
497 .title {
497 .title {
498 float: none;
498 float: none;
499 margin: 0 auto;
499 margin: 0 auto;
500 }
500 }
501 }
501 }
502
502
503 button.close {
503 button.close {
504 padding: 0;
504 padding: 0;
505 cursor: pointer;
505 cursor: pointer;
506 background: transparent;
506 background: transparent;
507 border: 0;
507 border: 0;
508 .box-shadow(none);
508 .box-shadow(none);
509 -webkit-appearance: none;
509 -webkit-appearance: none;
510 }
510 }
511
511
512 .close {
512 .close {
513 float: right;
513 float: right;
514 font-size: 21px;
514 font-size: 21px;
515 font-family: @text-bootstrap;
515 font-family: @text-bootstrap;
516 line-height: 1em;
516 line-height: 1em;
517 font-weight: bold;
517 font-weight: bold;
518 color: @grey2;
518 color: @grey2;
519
519
520 &:hover,
520 &:hover,
521 &:focus {
521 &:focus {
522 color: @grey1;
522 color: @grey1;
523 text-decoration: none;
523 text-decoration: none;
524 cursor: pointer;
524 cursor: pointer;
525 }
525 }
526 }
526 }
527
527
528 // GRID
528 // GRID
529 .sorting,
529 .sorting,
530 .sorting_desc,
530 .sorting_desc,
531 .sorting_asc {
531 .sorting_asc {
532 cursor: pointer;
532 cursor: pointer;
533 }
533 }
534 .sorting_desc:after {
534 .sorting_desc:after {
535 content: "\00A0\25B2";
535 content: "\00A0\25B2";
536 font-size: .75em;
536 font-size: .75em;
537 }
537 }
538 .sorting_asc:after {
538 .sorting_asc:after {
539 content: "\00A0\25BC";
539 content: "\00A0\25BC";
540 font-size: .68em;
540 font-size: .68em;
541 }
541 }
542
542
543
543
544 .user_auth_tokens {
544 .user_auth_tokens {
545
545
546 &.truncate {
546 &.truncate {
547 white-space: nowrap;
547 white-space: nowrap;
548 overflow: hidden;
548 overflow: hidden;
549 text-overflow: ellipsis;
549 text-overflow: ellipsis;
550 }
550 }
551
551
552 .fields .field .input {
552 .fields .field .input {
553 margin: 0;
553 margin: 0;
554 }
554 }
555
555
556 input#description {
556 input#description {
557 width: 100px;
557 width: 100px;
558 margin: 0;
558 margin: 0;
559 }
559 }
560
560
561 .drop-menu {
561 .drop-menu {
562 // TODO: johbo: Remove this, should work out of the box when
562 // TODO: johbo: Remove this, should work out of the box when
563 // having multiple inputs inline
563 // having multiple inputs inline
564 margin: 0 0 0 5px;
564 margin: 0 0 0 5px;
565 }
565 }
566 }
566 }
567 #user_list_table {
567 #user_list_table {
568 .closed {
568 .closed {
569 background-color: @grey6;
569 background-color: @grey6;
570 }
570 }
571 }
571 }
572
572
573
573
574 input {
574 input {
575 &.disabled {
575 &.disabled {
576 opacity: .5;
576 opacity: .5;
577 }
577 }
578 }
578 }
579
579
580 // remove extra padding in firefox
580 // remove extra padding in firefox
581 input::-moz-focus-inner { border:0; padding:0 }
581 input::-moz-focus-inner { border:0; padding:0 }
582
582
583 .adjacent input {
583 .adjacent input {
584 margin-bottom: @padding;
584 margin-bottom: @padding;
585 }
585 }
586
586
587 .permissions_boxes {
587 .permissions_boxes {
588 display: block;
588 display: block;
589 }
589 }
590
590
591 //TODO: lisa: this should be in tables
591 //TODO: lisa: this should be in tables
592 .show_more_col {
592 .show_more_col {
593 width: 20px;
593 width: 20px;
594 }
594 }
595
595
596 //FORMS
596 //FORMS
597
597
598 .medium-inline,
598 .medium-inline,
599 input#description.medium-inline {
599 input#description.medium-inline {
600 display: inline;
600 display: inline;
601 width: @medium-inline-input-width;
601 width: @medium-inline-input-width;
602 min-width: 100px;
602 min-width: 100px;
603 }
603 }
604
604
605 select {
605 select {
606 //reset
606 //reset
607 -webkit-appearance: none;
607 -webkit-appearance: none;
608 -moz-appearance: none;
608 -moz-appearance: none;
609
609
610 display: inline-block;
610 display: inline-block;
611 height: 28px;
611 height: 28px;
612 width: auto;
612 width: auto;
613 margin: 0 @padding @padding 0;
613 margin: 0 @padding @padding 0;
614 padding: 0 18px 0 8px;
614 padding: 0 18px 0 8px;
615 line-height:1em;
615 line-height:1em;
616 font-size: @basefontsize;
616 font-size: @basefontsize;
617 border: @border-thickness solid @rcblue;
617 border: @border-thickness solid @rcblue;
618 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
618 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
619 color: @rcblue;
619 color: @rcblue;
620
620
621 &:after {
621 &:after {
622 content: "\00A0\25BE";
622 content: "\00A0\25BE";
623 }
623 }
624
624
625 &:focus {
625 &:focus {
626 outline: none;
626 outline: none;
627 }
627 }
628 }
628 }
629
629
630 option {
630 option {
631 &:focus {
631 &:focus {
632 outline: none;
632 outline: none;
633 }
633 }
634 }
634 }
635
635
636 input,
636 input,
637 textarea {
637 textarea {
638 padding: @input-padding;
638 padding: @input-padding;
639 border: @input-border-thickness solid @border-highlight-color;
639 border: @input-border-thickness solid @border-highlight-color;
640 .border-radius (@border-radius);
640 .border-radius (@border-radius);
641 font-family: @text-light;
641 font-family: @text-light;
642 font-size: @basefontsize;
642 font-size: @basefontsize;
643
643
644 &.input-sm {
644 &.input-sm {
645 padding: 5px;
645 padding: 5px;
646 }
646 }
647
647
648 &#description {
648 &#description {
649 min-width: @input-description-minwidth;
649 min-width: @input-description-minwidth;
650 min-height: 1em;
650 min-height: 1em;
651 padding: 10px;
651 padding: 10px;
652 }
652 }
653 }
653 }
654
654
655 .field-sm {
655 .field-sm {
656 input,
656 input,
657 textarea {
657 textarea {
658 padding: 5px;
658 padding: 5px;
659 }
659 }
660 }
660 }
661
661
662 textarea {
662 textarea {
663 display: block;
663 display: block;
664 clear: both;
664 clear: both;
665 width: 100%;
665 width: 100%;
666 min-height: 100px;
666 min-height: 100px;
667 margin-bottom: @padding;
667 margin-bottom: @padding;
668 .box-sizing(border-box);
668 .box-sizing(border-box);
669 overflow: auto;
669 overflow: auto;
670 }
670 }
671
671
672 label {
672 label {
673 font-family: @text-light;
673 font-family: @text-light;
674 }
674 }
675
675
676 // GRAVATARS
676 // GRAVATARS
677 // centers gravatar on username to the right
677 // centers gravatar on username to the right
678
678
679 .gravatar {
679 .gravatar {
680 display: inline;
680 display: inline;
681 min-width: 16px;
681 min-width: 16px;
682 min-height: 16px;
682 min-height: 16px;
683 margin: -5px 0;
683 margin: -5px 0;
684 padding: 0;
684 padding: 0;
685 line-height: 1em;
685 line-height: 1em;
686 border: 1px solid @grey4;
686 border: 1px solid @grey4;
687
687
688 &.gravatar-large {
688 &.gravatar-large {
689 margin: -0.5em .25em -0.5em 0;
689 margin: -0.5em .25em -0.5em 0;
690 }
690 }
691
691
692 & + .user {
692 & + .user {
693 display: inline;
693 display: inline;
694 margin: 0;
694 margin: 0;
695 padding: 0 0 0 .17em;
695 padding: 0 0 0 .17em;
696 line-height: 1em;
696 line-height: 1em;
697 }
697 }
698 }
698 }
699
699
700 .user-inline-data {
700 .user-inline-data {
701 display: inline-block;
701 display: inline-block;
702 float: left;
702 float: left;
703 padding-left: .5em;
703 padding-left: .5em;
704 line-height: 1.3em;
704 line-height: 1.3em;
705 }
705 }
706
706
707 .rc-user { // gravatar + user wrapper
707 .rc-user { // gravatar + user wrapper
708 float: left;
708 float: left;
709 position: relative;
709 position: relative;
710 min-width: 100px;
710 min-width: 100px;
711 max-width: 200px;
711 max-width: 200px;
712 min-height: (@gravatar-size + @border-thickness * 2); // account for border
712 min-height: (@gravatar-size + @border-thickness * 2); // account for border
713 display: block;
713 display: block;
714 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
714 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
715
715
716
716
717 .gravatar {
717 .gravatar {
718 display: block;
718 display: block;
719 position: absolute;
719 position: absolute;
720 top: 0;
720 top: 0;
721 left: 0;
721 left: 0;
722 min-width: @gravatar-size;
722 min-width: @gravatar-size;
723 min-height: @gravatar-size;
723 min-height: @gravatar-size;
724 margin: 0;
724 margin: 0;
725 }
725 }
726
726
727 .user {
727 .user {
728 display: block;
728 display: block;
729 max-width: 175px;
729 max-width: 175px;
730 padding-top: 2px;
730 padding-top: 2px;
731 overflow: hidden;
731 overflow: hidden;
732 text-overflow: ellipsis;
732 text-overflow: ellipsis;
733 }
733 }
734 }
734 }
735
735
736 .gist-gravatar,
736 .gist-gravatar,
737 .journal_container {
737 .journal_container {
738 .gravatar-large {
738 .gravatar-large {
739 margin: 0 .5em -10px 0;
739 margin: 0 .5em -10px 0;
740 }
740 }
741 }
741 }
742
742
743
743
744 // ADMIN SETTINGS
744 // ADMIN SETTINGS
745
745
746 // Tag Patterns
746 // Tag Patterns
747 .tag_patterns {
747 .tag_patterns {
748 .tag_input {
748 .tag_input {
749 margin-bottom: @padding;
749 margin-bottom: @padding;
750 }
750 }
751 }
751 }
752
752
753 .locked_input {
753 .locked_input {
754 position: relative;
754 position: relative;
755
755
756 input {
756 input {
757 display: inline;
757 display: inline;
758 margin-top: 3px;
758 margin-top: 3px;
759 }
759 }
760
760
761 br {
761 br {
762 display: none;
762 display: none;
763 }
763 }
764
764
765 .error-message {
765 .error-message {
766 float: left;
766 float: left;
767 width: 100%;
767 width: 100%;
768 }
768 }
769
769
770 .lock_input_button {
770 .lock_input_button {
771 display: inline;
771 display: inline;
772 }
772 }
773
773
774 .help-block {
774 .help-block {
775 clear: both;
775 clear: both;
776 }
776 }
777 }
777 }
778
778
779 // Notifications
779 // Notifications
780
780
781 .notifications_buttons {
781 .notifications_buttons {
782 margin: 0 0 @space 0;
782 margin: 0 0 @space 0;
783 padding: 0;
783 padding: 0;
784
784
785 .btn {
785 .btn {
786 display: inline-block;
786 display: inline-block;
787 }
787 }
788 }
788 }
789
789
790 .notification-list {
790 .notification-list {
791
791
792 div {
792 div {
793 display: inline-block;
793 display: inline-block;
794 vertical-align: middle;
794 vertical-align: middle;
795 }
795 }
796
796
797 .container {
797 .container {
798 display: block;
798 display: block;
799 margin: 0 0 @padding 0;
799 margin: 0 0 @padding 0;
800 }
800 }
801
801
802 .delete-notifications {
802 .delete-notifications {
803 margin-left: @padding;
803 margin-left: @padding;
804 text-align: right;
804 text-align: right;
805 cursor: pointer;
805 cursor: pointer;
806 }
806 }
807
807
808 .read-notifications {
808 .read-notifications {
809 margin-left: @padding/2;
809 margin-left: @padding/2;
810 text-align: right;
810 text-align: right;
811 width: 35px;
811 width: 35px;
812 cursor: pointer;
812 cursor: pointer;
813 }
813 }
814
814
815 .icon-minus-sign {
815 .icon-minus-sign {
816 color: @alert2;
816 color: @alert2;
817 }
817 }
818
818
819 .icon-ok-sign {
819 .icon-ok-sign {
820 color: @alert1;
820 color: @alert1;
821 }
821 }
822 }
822 }
823
823
824 .user_settings {
824 .user_settings {
825 float: left;
825 float: left;
826 clear: both;
826 clear: both;
827 display: block;
827 display: block;
828 width: 100%;
828 width: 100%;
829
829
830 .gravatar_box {
830 .gravatar_box {
831 margin-bottom: @padding;
831 margin-bottom: @padding;
832
832
833 &:after {
833 &:after {
834 content: " ";
834 content: " ";
835 clear: both;
835 clear: both;
836 width: 100%;
836 width: 100%;
837 }
837 }
838 }
838 }
839
839
840 .fields .field {
840 .fields .field {
841 clear: both;
841 clear: both;
842 }
842 }
843 }
843 }
844
844
845 .advanced_settings {
845 .advanced_settings {
846 margin-bottom: @space;
846 margin-bottom: @space;
847
847
848 .help-block {
848 .help-block {
849 margin-left: 0;
849 margin-left: 0;
850 }
850 }
851
851
852 button + .help-block {
852 button + .help-block {
853 margin-top: @padding;
853 margin-top: @padding;
854 }
854 }
855 }
855 }
856
856
857 // admin settings radio buttons and labels
857 // admin settings radio buttons and labels
858 .label-2 {
858 .label-2 {
859 float: left;
859 float: left;
860 width: @label2-width;
860 width: @label2-width;
861
861
862 label {
862 label {
863 color: @grey1;
863 color: @grey1;
864 }
864 }
865 }
865 }
866 .checkboxes {
866 .checkboxes {
867 float: left;
867 float: left;
868 width: @checkboxes-width;
868 width: @checkboxes-width;
869 margin-bottom: @padding;
869 margin-bottom: @padding;
870
870
871 .checkbox {
871 .checkbox {
872 width: 100%;
872 width: 100%;
873
873
874 label {
874 label {
875 margin: 0;
875 margin: 0;
876 padding: 0;
876 padding: 0;
877 }
877 }
878 }
878 }
879
879
880 .checkbox + .checkbox {
880 .checkbox + .checkbox {
881 display: inline-block;
881 display: inline-block;
882 }
882 }
883
883
884 label {
884 label {
885 margin-right: 1em;
885 margin-right: 1em;
886 }
886 }
887 }
887 }
888
888
889 // CHANGELOG
889 // CHANGELOG
890 .container_header {
890 .container_header {
891 float: left;
891 float: left;
892 display: block;
892 display: block;
893 width: 100%;
893 width: 100%;
894 margin: @padding 0 @padding;
894 margin: @padding 0 @padding;
895
895
896 #filter_changelog {
896 #filter_changelog {
897 float: left;
897 float: left;
898 margin-right: @padding;
898 margin-right: @padding;
899 }
899 }
900
900
901 .breadcrumbs_light {
901 .breadcrumbs_light {
902 display: inline-block;
902 display: inline-block;
903 }
903 }
904 }
904 }
905
905
906 .info_box {
906 .info_box {
907 float: right;
907 float: right;
908 }
908 }
909
909
910
910
911 #graph_nodes {
911 #graph_nodes {
912 padding-top: 43px;
912 padding-top: 43px;
913 }
913 }
914
914
915 #graph_content{
915 #graph_content{
916
916
917 // adjust for table headers so that graph renders properly
917 // adjust for table headers so that graph renders properly
918 // #graph_nodes padding - table cell padding
918 // #graph_nodes padding - table cell padding
919 padding-top: (@space - (@basefontsize * 2.4));
919 padding-top: (@space - (@basefontsize * 2.4));
920
920
921 &.graph_full_width {
921 &.graph_full_width {
922 width: 100%;
922 width: 100%;
923 max-width: 100%;
923 max-width: 100%;
924 }
924 }
925 }
925 }
926
926
927 #graph {
927 #graph {
928 .flag_status {
928 .flag_status {
929 margin: 0;
929 margin: 0;
930 }
930 }
931
931
932 .pagination-left {
932 .pagination-left {
933 float: left;
933 float: left;
934 clear: both;
934 clear: both;
935 }
935 }
936
936
937 .log-container {
937 .log-container {
938 max-width: 345px;
938 max-width: 345px;
939
939
940 .message{
940 .message{
941 max-width: 340px;
941 max-width: 340px;
942 }
942 }
943 }
943 }
944
944
945 .graph-col-wrapper {
945 .graph-col-wrapper {
946 padding-left: 110px;
946 padding-left: 110px;
947
947
948 #graph_nodes {
948 #graph_nodes {
949 width: 100px;
949 width: 100px;
950 margin-left: -110px;
950 margin-left: -110px;
951 float: left;
951 float: left;
952 clear: left;
952 clear: left;
953 }
953 }
954 }
954 }
955 }
955 }
956
956
957 #filter_changelog {
957 #filter_changelog {
958 float: left;
958 float: left;
959 }
959 }
960
960
961
961
962 //--- THEME ------------------//
962 //--- THEME ------------------//
963
963
964 #logo {
964 #logo {
965 float: left;
965 float: left;
966 margin: 9px 0 0 0;
966 margin: 9px 0 0 0;
967
967
968 .header {
968 .header {
969 background-color: transparent;
969 background-color: transparent;
970 }
970 }
971
971
972 a {
972 a {
973 display: inline-block;
973 display: inline-block;
974 }
974 }
975
975
976 img {
976 img {
977 height:30px;
977 height:30px;
978 }
978 }
979 }
979 }
980
980
981 .logo-wrapper {
981 .logo-wrapper {
982 float:left;
982 float:left;
983 }
983 }
984
984
985 .branding{
985 .branding{
986 float: left;
986 float: left;
987 padding: 9px 2px;
987 padding: 9px 2px;
988 line-height: 1em;
988 line-height: 1em;
989 font-size: @navigation-fontsize;
989 font-size: @navigation-fontsize;
990 }
990 }
991
991
992 img {
992 img {
993 border: none;
993 border: none;
994 outline: none;
994 outline: none;
995 }
995 }
996 user-profile-header
996 user-profile-header
997 label {
997 label {
998
998
999 input[type="checkbox"] {
999 input[type="checkbox"] {
1000 margin-right: 1em;
1000 margin-right: 1em;
1001 }
1001 }
1002 input[type="radio"] {
1002 input[type="radio"] {
1003 margin-right: 1em;
1003 margin-right: 1em;
1004 }
1004 }
1005 }
1005 }
1006
1006
1007 .flag_status {
1007 .flag_status {
1008 margin: 2px 8px 6px 2px;
1008 margin: 2px 8px 6px 2px;
1009 &.under_review {
1009 &.under_review {
1010 .circle(5px, @alert3);
1010 .circle(5px, @alert3);
1011 }
1011 }
1012 &.approved {
1012 &.approved {
1013 .circle(5px, @alert1);
1013 .circle(5px, @alert1);
1014 }
1014 }
1015 &.rejected,
1015 &.rejected,
1016 &.forced_closed{
1016 &.forced_closed{
1017 .circle(5px, @alert2);
1017 .circle(5px, @alert2);
1018 }
1018 }
1019 &.not_reviewed {
1019 &.not_reviewed {
1020 .circle(5px, @grey5);
1020 .circle(5px, @grey5);
1021 }
1021 }
1022 }
1022 }
1023
1023
1024 .flag_status_comment_box {
1024 .flag_status_comment_box {
1025 margin: 5px 6px 0px 2px;
1025 margin: 5px 6px 0px 2px;
1026 }
1026 }
1027 .test_pattern_preview {
1027 .test_pattern_preview {
1028 margin: @space 0;
1028 margin: @space 0;
1029
1029
1030 p {
1030 p {
1031 margin-bottom: 0;
1031 margin-bottom: 0;
1032 border-bottom: @border-thickness solid @border-default-color;
1032 border-bottom: @border-thickness solid @border-default-color;
1033 color: @grey3;
1033 color: @grey3;
1034 }
1034 }
1035
1035
1036 .btn {
1036 .btn {
1037 margin-bottom: @padding;
1037 margin-bottom: @padding;
1038 }
1038 }
1039 }
1039 }
1040 #test_pattern_result {
1040 #test_pattern_result {
1041 display: none;
1041 display: none;
1042 &:extend(pre);
1042 &:extend(pre);
1043 padding: .9em;
1043 padding: .9em;
1044 color: @grey3;
1044 color: @grey3;
1045 background-color: @grey7;
1045 background-color: @grey7;
1046 border-right: @border-thickness solid @border-default-color;
1046 border-right: @border-thickness solid @border-default-color;
1047 border-bottom: @border-thickness solid @border-default-color;
1047 border-bottom: @border-thickness solid @border-default-color;
1048 border-left: @border-thickness solid @border-default-color;
1048 border-left: @border-thickness solid @border-default-color;
1049 }
1049 }
1050
1050
1051 #repo_vcs_settings {
1051 #repo_vcs_settings {
1052 #inherit_overlay_vcs_default {
1052 #inherit_overlay_vcs_default {
1053 display: none;
1053 display: none;
1054 }
1054 }
1055 #inherit_overlay_vcs_custom {
1055 #inherit_overlay_vcs_custom {
1056 display: custom;
1056 display: custom;
1057 }
1057 }
1058 &.inherited {
1058 &.inherited {
1059 #inherit_overlay_vcs_default {
1059 #inherit_overlay_vcs_default {
1060 display: block;
1060 display: block;
1061 }
1061 }
1062 #inherit_overlay_vcs_custom {
1062 #inherit_overlay_vcs_custom {
1063 display: none;
1063 display: none;
1064 }
1064 }
1065 }
1065 }
1066 }
1066 }
1067
1067
1068 .issue-tracker-link {
1068 .issue-tracker-link {
1069 color: @rcblue;
1069 color: @rcblue;
1070 }
1070 }
1071
1071
1072 // Issue Tracker Table Show/Hide
1072 // Issue Tracker Table Show/Hide
1073 #repo_issue_tracker {
1073 #repo_issue_tracker {
1074 #inherit_overlay {
1074 #inherit_overlay {
1075 display: none;
1075 display: none;
1076 }
1076 }
1077 #custom_overlay {
1077 #custom_overlay {
1078 display: custom;
1078 display: custom;
1079 }
1079 }
1080 &.inherited {
1080 &.inherited {
1081 #inherit_overlay {
1081 #inherit_overlay {
1082 display: block;
1082 display: block;
1083 }
1083 }
1084 #custom_overlay {
1084 #custom_overlay {
1085 display: none;
1085 display: none;
1086 }
1086 }
1087 }
1087 }
1088 }
1088 }
1089 table.issuetracker {
1089 table.issuetracker {
1090 &.readonly {
1090 &.readonly {
1091 tr, td {
1091 tr, td {
1092 color: @grey3;
1092 color: @grey3;
1093 }
1093 }
1094 }
1094 }
1095 .edit {
1095 .edit {
1096 display: none;
1096 display: none;
1097 }
1097 }
1098 .editopen {
1098 .editopen {
1099 .edit {
1099 .edit {
1100 display: inline;
1100 display: inline;
1101 }
1101 }
1102 .entry {
1102 .entry {
1103 display: none;
1103 display: none;
1104 }
1104 }
1105 }
1105 }
1106 tr td.td-action {
1106 tr td.td-action {
1107 min-width: 117px;
1107 min-width: 117px;
1108 }
1108 }
1109 td input {
1109 td input {
1110 max-width: none;
1110 max-width: none;
1111 min-width: 30px;
1111 min-width: 30px;
1112 width: 80%;
1112 width: 80%;
1113 }
1113 }
1114 .issuetracker_pref input {
1114 .issuetracker_pref input {
1115 width: 40%;
1115 width: 40%;
1116 }
1116 }
1117 input.edit_issuetracker_update {
1117 input.edit_issuetracker_update {
1118 margin-right: 0;
1118 margin-right: 0;
1119 width: auto;
1119 width: auto;
1120 }
1120 }
1121 }
1121 }
1122
1122
1123 table.integrations {
1123 table.integrations {
1124 .td-icon {
1124 .td-icon {
1125 width: 20px;
1125 width: 20px;
1126 .integration-icon {
1126 .integration-icon {
1127 height: 20px;
1127 height: 20px;
1128 width: 20px;
1128 width: 20px;
1129 }
1129 }
1130 }
1130 }
1131 }
1131 }
1132
1132
1133 .integrations {
1133 .integrations {
1134 a.integration-box {
1134 a.integration-box {
1135 color: @text-color;
1135 color: @text-color;
1136 &:hover {
1136 &:hover {
1137 .panel {
1137 .panel {
1138 background: #fbfbfb;
1138 background: #fbfbfb;
1139 }
1139 }
1140 }
1140 }
1141 .integration-icon {
1141 .integration-icon {
1142 width: 30px;
1142 width: 30px;
1143 height: 30px;
1143 height: 30px;
1144 margin-right: 20px;
1144 margin-right: 20px;
1145 float: left;
1145 float: left;
1146 }
1146 }
1147
1147
1148 .panel-body {
1148 .panel-body {
1149 padding: 10px;
1149 padding: 10px;
1150 }
1150 }
1151 .panel {
1151 .panel {
1152 margin-bottom: 10px;
1152 margin-bottom: 10px;
1153 }
1153 }
1154 h2 {
1154 h2 {
1155 display: inline-block;
1155 display: inline-block;
1156 margin: 0;
1156 margin: 0;
1157 min-width: 140px;
1157 min-width: 140px;
1158 }
1158 }
1159 }
1159 }
1160 }
1160 }
1161
1161
1162 //Permissions Settings
1162 //Permissions Settings
1163 #add_perm {
1163 #add_perm {
1164 margin: 0 0 @padding;
1164 margin: 0 0 @padding;
1165 cursor: pointer;
1165 cursor: pointer;
1166 }
1166 }
1167
1167
1168 .perm_ac {
1168 .perm_ac {
1169 input {
1169 input {
1170 width: 95%;
1170 width: 95%;
1171 }
1171 }
1172 }
1172 }
1173
1173
1174 .autocomplete-suggestions {
1174 .autocomplete-suggestions {
1175 width: auto !important; // overrides autocomplete.js
1175 width: auto !important; // overrides autocomplete.js
1176 margin: 0;
1176 margin: 0;
1177 border: @border-thickness solid @rcblue;
1177 border: @border-thickness solid @rcblue;
1178 border-radius: @border-radius;
1178 border-radius: @border-radius;
1179 color: @rcblue;
1179 color: @rcblue;
1180 background-color: white;
1180 background-color: white;
1181 }
1181 }
1182 .autocomplete-selected {
1182 .autocomplete-selected {
1183 background: #F0F0F0;
1183 background: #F0F0F0;
1184 }
1184 }
1185 .ac-container-wrap {
1185 .ac-container-wrap {
1186 margin: 0;
1186 margin: 0;
1187 padding: 8px;
1187 padding: 8px;
1188 border-bottom: @border-thickness solid @rclightblue;
1188 border-bottom: @border-thickness solid @rclightblue;
1189 list-style-type: none;
1189 list-style-type: none;
1190 cursor: pointer;
1190 cursor: pointer;
1191
1191
1192 &:hover {
1192 &:hover {
1193 background-color: @rclightblue;
1193 background-color: @rclightblue;
1194 }
1194 }
1195
1195
1196 img {
1196 img {
1197 height: @gravatar-size;
1197 height: @gravatar-size;
1198 width: @gravatar-size;
1198 width: @gravatar-size;
1199 margin-right: 1em;
1199 margin-right: 1em;
1200 }
1200 }
1201
1201
1202 strong {
1202 strong {
1203 font-weight: normal;
1203 font-weight: normal;
1204 }
1204 }
1205 }
1205 }
1206
1206
1207 // Settings Dropdown
1207 // Settings Dropdown
1208 .user-menu .container {
1208 .user-menu .container {
1209 padding: 0 4px;
1209 padding: 0 4px;
1210 margin: 0;
1210 margin: 0;
1211 }
1211 }
1212
1212
1213 .user-menu .gravatar {
1213 .user-menu .gravatar {
1214 cursor: pointer;
1214 cursor: pointer;
1215 }
1215 }
1216
1216
1217 .codeblock {
1217 .codeblock {
1218 margin-bottom: @padding;
1218 margin-bottom: @padding;
1219 clear: both;
1219 clear: both;
1220
1220
1221 .stats{
1221 .stats{
1222 overflow: hidden;
1222 overflow: hidden;
1223 }
1223 }
1224
1224
1225 .message{
1225 .message{
1226 textarea{
1226 textarea{
1227 margin: 0;
1227 margin: 0;
1228 }
1228 }
1229 }
1229 }
1230
1230
1231 .code-header {
1231 .code-header {
1232 .stats {
1232 .stats {
1233 line-height: 2em;
1233 line-height: 2em;
1234
1234
1235 .revision_id {
1235 .revision_id {
1236 margin-left: 0;
1236 margin-left: 0;
1237 }
1237 }
1238 .buttons {
1238 .buttons {
1239 padding-right: 0;
1239 padding-right: 0;
1240 }
1240 }
1241 }
1241 }
1242
1242
1243 .item{
1243 .item{
1244 margin-right: 0.5em;
1244 margin-right: 0.5em;
1245 }
1245 }
1246 }
1246 }
1247
1247
1248 #editor_container{
1248 #editor_container{
1249 position: relative;
1249 position: relative;
1250 margin: @padding;
1250 margin: @padding;
1251 }
1251 }
1252 }
1252 }
1253
1253
1254 #file_history_container {
1254 #file_history_container {
1255 display: none;
1255 display: none;
1256 }
1256 }
1257
1257
1258 .file-history-inner {
1258 .file-history-inner {
1259 margin-bottom: 10px;
1259 margin-bottom: 10px;
1260 }
1260 }
1261
1261
1262 // Pull Requests
1262 // Pull Requests
1263 .summary-details {
1263 .summary-details {
1264 width: 72%;
1264 width: 72%;
1265 }
1265 }
1266 .pr-summary {
1266 .pr-summary {
1267 border-bottom: @border-thickness solid @grey5;
1267 border-bottom: @border-thickness solid @grey5;
1268 margin-bottom: @space;
1268 margin-bottom: @space;
1269 }
1269 }
1270 .reviewers-title {
1270 .reviewers-title {
1271 width: 25%;
1271 width: 25%;
1272 min-width: 200px;
1272 min-width: 200px;
1273 }
1273 }
1274 .reviewers {
1274 .reviewers {
1275 width: 25%;
1275 width: 25%;
1276 min-width: 200px;
1276 min-width: 200px;
1277 }
1277 }
1278 .reviewers ul li {
1278 .reviewers ul li {
1279 position: relative;
1279 position: relative;
1280 width: 100%;
1280 width: 100%;
1281 margin-bottom: 8px;
1281 margin-bottom: 8px;
1282 }
1282 }
1283 .reviewers_member {
1283 .reviewers_member {
1284 width: 100%;
1284 width: 100%;
1285 overflow: auto;
1285 overflow: auto;
1286 }
1286 }
1287 .reviewer_reason {
1287 .reviewer_reason {
1288 padding-left: 20px;
1288 padding-left: 20px;
1289 }
1289 }
1290 .reviewer_status {
1290 .reviewer_status {
1291 display: inline-block;
1291 display: inline-block;
1292 vertical-align: top;
1292 vertical-align: top;
1293 width: 7%;
1293 width: 7%;
1294 min-width: 20px;
1294 min-width: 20px;
1295 height: 1.2em;
1295 height: 1.2em;
1296 margin-top: 3px;
1296 margin-top: 3px;
1297 line-height: 1em;
1297 line-height: 1em;
1298 }
1298 }
1299
1299
1300 .reviewer_name {
1300 .reviewer_name {
1301 display: inline-block;
1301 display: inline-block;
1302 max-width: 83%;
1302 max-width: 83%;
1303 padding-right: 20px;
1303 padding-right: 20px;
1304 vertical-align: middle;
1304 vertical-align: middle;
1305 line-height: 1;
1305 line-height: 1;
1306
1306
1307 .rc-user {
1307 .rc-user {
1308 min-width: 0;
1308 min-width: 0;
1309 margin: -2px 1em 0 0;
1309 margin: -2px 1em 0 0;
1310 }
1310 }
1311
1311
1312 .reviewer {
1312 .reviewer {
1313 float: left;
1313 float: left;
1314 }
1314 }
1315
1315
1316 &.to-delete {
1316 &.to-delete {
1317 .user,
1317 .user,
1318 .reviewer {
1318 .reviewer {
1319 text-decoration: line-through;
1319 text-decoration: line-through;
1320 }
1320 }
1321 }
1321 }
1322 }
1322 }
1323
1323
1324 .reviewer_member_remove {
1324 .reviewer_member_remove {
1325 position: absolute;
1325 position: absolute;
1326 right: 0;
1326 right: 0;
1327 top: 0;
1327 top: 0;
1328 width: 16px;
1328 width: 16px;
1329 margin-bottom: 10px;
1329 margin-bottom: 10px;
1330 padding: 0;
1330 padding: 0;
1331 color: black;
1331 color: black;
1332 }
1332 }
1333 .reviewer_member_status {
1333 .reviewer_member_status {
1334 margin-top: 5px;
1334 margin-top: 5px;
1335 }
1335 }
1336 .pr-summary #summary{
1336 .pr-summary #summary{
1337 width: 100%;
1337 width: 100%;
1338 }
1338 }
1339 .pr-summary .action_button:hover {
1339 .pr-summary .action_button:hover {
1340 border: 0;
1340 border: 0;
1341 cursor: pointer;
1341 cursor: pointer;
1342 }
1342 }
1343 .pr-details-title {
1343 .pr-details-title {
1344 padding-bottom: 8px;
1344 padding-bottom: 8px;
1345 border-bottom: @border-thickness solid @grey5;
1345 border-bottom: @border-thickness solid @grey5;
1346
1347 .action_button.disabled {
1348 color: @grey4;
1349 cursor: inherit;
1350 }
1346 .action_button {
1351 .action_button {
1347 color: @rcblue;
1352 color: @rcblue;
1348 }
1353 }
1349 }
1354 }
1350 .pr-details-content {
1355 .pr-details-content {
1351 margin-top: @textmargin;
1356 margin-top: @textmargin;
1352 margin-bottom: @textmargin;
1357 margin-bottom: @textmargin;
1353 }
1358 }
1354 .pr-description {
1359 .pr-description {
1355 white-space:pre-wrap;
1360 white-space:pre-wrap;
1356 }
1361 }
1357 .group_members {
1362 .group_members {
1358 margin-top: 0;
1363 margin-top: 0;
1359 padding: 0;
1364 padding: 0;
1360 list-style: outside none none;
1365 list-style: outside none none;
1361
1366
1362 img {
1367 img {
1363 height: @gravatar-size;
1368 height: @gravatar-size;
1364 width: @gravatar-size;
1369 width: @gravatar-size;
1365 margin-right: .5em;
1370 margin-right: .5em;
1366 margin-left: 3px;
1371 margin-left: 3px;
1367 }
1372 }
1368 }
1373 }
1369 .reviewer_ac .ac-input {
1374 .reviewer_ac .ac-input {
1370 width: 92%;
1375 width: 92%;
1371 margin-bottom: 1em;
1376 margin-bottom: 1em;
1372 }
1377 }
1373 #update_commits {
1378 #update_commits {
1374 float: right;
1379 float: right;
1375 }
1380 }
1376 .compare_view_commits tr{
1381 .compare_view_commits tr{
1377 height: 20px;
1382 height: 20px;
1378 }
1383 }
1379 .compare_view_commits td {
1384 .compare_view_commits td {
1380 vertical-align: top;
1385 vertical-align: top;
1381 padding-top: 10px;
1386 padding-top: 10px;
1382 }
1387 }
1383 .compare_view_commits .author {
1388 .compare_view_commits .author {
1384 margin-left: 5px;
1389 margin-left: 5px;
1385 }
1390 }
1386
1391
1387 .compare_view_files {
1392 .compare_view_files {
1388 width: 100%;
1393 width: 100%;
1389
1394
1390 td {
1395 td {
1391 vertical-align: middle;
1396 vertical-align: middle;
1392 }
1397 }
1393 }
1398 }
1394
1399
1395 .compare_view_filepath {
1400 .compare_view_filepath {
1396 color: @grey1;
1401 color: @grey1;
1397 }
1402 }
1398
1403
1399 .show_more {
1404 .show_more {
1400 display: inline-block;
1405 display: inline-block;
1401 position: relative;
1406 position: relative;
1402 vertical-align: middle;
1407 vertical-align: middle;
1403 width: 4px;
1408 width: 4px;
1404 height: @basefontsize;
1409 height: @basefontsize;
1405
1410
1406 &:after {
1411 &:after {
1407 content: "\00A0\25BE";
1412 content: "\00A0\25BE";
1408 display: inline-block;
1413 display: inline-block;
1409 width:10px;
1414 width:10px;
1410 line-height: 5px;
1415 line-height: 5px;
1411 font-size: 12px;
1416 font-size: 12px;
1412 cursor: pointer;
1417 cursor: pointer;
1413 }
1418 }
1414 }
1419 }
1415
1420
1416 .journal_more .show_more {
1421 .journal_more .show_more {
1417 display: inline;
1422 display: inline;
1418
1423
1419 &:after {
1424 &:after {
1420 content: none;
1425 content: none;
1421 }
1426 }
1422 }
1427 }
1423
1428
1424 .open .show_more:after,
1429 .open .show_more:after,
1425 .select2-dropdown-open .show_more:after {
1430 .select2-dropdown-open .show_more:after {
1426 .rotate(180deg);
1431 .rotate(180deg);
1427 margin-left: 4px;
1432 margin-left: 4px;
1428 }
1433 }
1429
1434
1430
1435
1431 .compare_view_commits .collapse_commit:after {
1436 .compare_view_commits .collapse_commit:after {
1432 cursor: pointer;
1437 cursor: pointer;
1433 content: "\00A0\25B4";
1438 content: "\00A0\25B4";
1434 margin-left: -3px;
1439 margin-left: -3px;
1435 font-size: 17px;
1440 font-size: 17px;
1436 color: @grey4;
1441 color: @grey4;
1437 }
1442 }
1438
1443
1439 .diff_links {
1444 .diff_links {
1440 margin-left: 8px;
1445 margin-left: 8px;
1441 }
1446 }
1442
1447
1443 p.ancestor {
1448 p.ancestor {
1444 margin: @padding 0;
1449 margin: @padding 0;
1445 }
1450 }
1446
1451
1447 .cs_icon_td input[type="checkbox"] {
1452 .cs_icon_td input[type="checkbox"] {
1448 display: none;
1453 display: none;
1449 }
1454 }
1450
1455
1451 .cs_icon_td .expand_file_icon:after {
1456 .cs_icon_td .expand_file_icon:after {
1452 cursor: pointer;
1457 cursor: pointer;
1453 content: "\00A0\25B6";
1458 content: "\00A0\25B6";
1454 font-size: 12px;
1459 font-size: 12px;
1455 color: @grey4;
1460 color: @grey4;
1456 }
1461 }
1457
1462
1458 .cs_icon_td .collapse_file_icon:after {
1463 .cs_icon_td .collapse_file_icon:after {
1459 cursor: pointer;
1464 cursor: pointer;
1460 content: "\00A0\25BC";
1465 content: "\00A0\25BC";
1461 font-size: 12px;
1466 font-size: 12px;
1462 color: @grey4;
1467 color: @grey4;
1463 }
1468 }
1464
1469
1465 /*new binary
1470 /*new binary
1466 NEW_FILENODE = 1
1471 NEW_FILENODE = 1
1467 DEL_FILENODE = 2
1472 DEL_FILENODE = 2
1468 MOD_FILENODE = 3
1473 MOD_FILENODE = 3
1469 RENAMED_FILENODE = 4
1474 RENAMED_FILENODE = 4
1470 COPIED_FILENODE = 5
1475 COPIED_FILENODE = 5
1471 CHMOD_FILENODE = 6
1476 CHMOD_FILENODE = 6
1472 BIN_FILENODE = 7
1477 BIN_FILENODE = 7
1473 */
1478 */
1474 .cs_files_expand {
1479 .cs_files_expand {
1475 font-size: @basefontsize + 5px;
1480 font-size: @basefontsize + 5px;
1476 line-height: 1.8em;
1481 line-height: 1.8em;
1477 float: right;
1482 float: right;
1478 }
1483 }
1479
1484
1480 .cs_files_expand span{
1485 .cs_files_expand span{
1481 color: @rcblue;
1486 color: @rcblue;
1482 cursor: pointer;
1487 cursor: pointer;
1483 }
1488 }
1484 .cs_files {
1489 .cs_files {
1485 clear: both;
1490 clear: both;
1486 padding-bottom: @padding;
1491 padding-bottom: @padding;
1487
1492
1488 .cur_cs {
1493 .cur_cs {
1489 margin: 10px 2px;
1494 margin: 10px 2px;
1490 font-weight: bold;
1495 font-weight: bold;
1491 }
1496 }
1492
1497
1493 .node {
1498 .node {
1494 float: left;
1499 float: left;
1495 }
1500 }
1496
1501
1497 .changes {
1502 .changes {
1498 float: right;
1503 float: right;
1499 color: white;
1504 color: white;
1500 font-size: @basefontsize - 4px;
1505 font-size: @basefontsize - 4px;
1501 margin-top: 4px;
1506 margin-top: 4px;
1502 opacity: 0.6;
1507 opacity: 0.6;
1503 filter: Alpha(opacity=60); /* IE8 and earlier */
1508 filter: Alpha(opacity=60); /* IE8 and earlier */
1504
1509
1505 .added {
1510 .added {
1506 background-color: @alert1;
1511 background-color: @alert1;
1507 float: left;
1512 float: left;
1508 text-align: center;
1513 text-align: center;
1509 }
1514 }
1510
1515
1511 .deleted {
1516 .deleted {
1512 background-color: @alert2;
1517 background-color: @alert2;
1513 float: left;
1518 float: left;
1514 text-align: center;
1519 text-align: center;
1515 }
1520 }
1516
1521
1517 .bin {
1522 .bin {
1518 background-color: @alert1;
1523 background-color: @alert1;
1519 text-align: center;
1524 text-align: center;
1520 }
1525 }
1521
1526
1522 /*new binary*/
1527 /*new binary*/
1523 .bin.bin1 {
1528 .bin.bin1 {
1524 background-color: @alert1;
1529 background-color: @alert1;
1525 text-align: center;
1530 text-align: center;
1526 }
1531 }
1527
1532
1528 /*deleted binary*/
1533 /*deleted binary*/
1529 .bin.bin2 {
1534 .bin.bin2 {
1530 background-color: @alert2;
1535 background-color: @alert2;
1531 text-align: center;
1536 text-align: center;
1532 }
1537 }
1533
1538
1534 /*mod binary*/
1539 /*mod binary*/
1535 .bin.bin3 {
1540 .bin.bin3 {
1536 background-color: @grey2;
1541 background-color: @grey2;
1537 text-align: center;
1542 text-align: center;
1538 }
1543 }
1539
1544
1540 /*rename file*/
1545 /*rename file*/
1541 .bin.bin4 {
1546 .bin.bin4 {
1542 background-color: @alert4;
1547 background-color: @alert4;
1543 text-align: center;
1548 text-align: center;
1544 }
1549 }
1545
1550
1546 /*copied file*/
1551 /*copied file*/
1547 .bin.bin5 {
1552 .bin.bin5 {
1548 background-color: @alert4;
1553 background-color: @alert4;
1549 text-align: center;
1554 text-align: center;
1550 }
1555 }
1551
1556
1552 /*chmod file*/
1557 /*chmod file*/
1553 .bin.bin6 {
1558 .bin.bin6 {
1554 background-color: @grey2;
1559 background-color: @grey2;
1555 text-align: center;
1560 text-align: center;
1556 }
1561 }
1557 }
1562 }
1558 }
1563 }
1559
1564
1560 .cs_files .cs_added, .cs_files .cs_A,
1565 .cs_files .cs_added, .cs_files .cs_A,
1561 .cs_files .cs_added, .cs_files .cs_M,
1566 .cs_files .cs_added, .cs_files .cs_M,
1562 .cs_files .cs_added, .cs_files .cs_D {
1567 .cs_files .cs_added, .cs_files .cs_D {
1563 height: 16px;
1568 height: 16px;
1564 padding-right: 10px;
1569 padding-right: 10px;
1565 margin-top: 7px;
1570 margin-top: 7px;
1566 text-align: left;
1571 text-align: left;
1567 }
1572 }
1568
1573
1569 .cs_icon_td {
1574 .cs_icon_td {
1570 min-width: 16px;
1575 min-width: 16px;
1571 width: 16px;
1576 width: 16px;
1572 }
1577 }
1573
1578
1574 .pull-request-merge {
1579 .pull-request-merge {
1575 padding: 10px 0;
1580 padding: 10px 0;
1576 margin-top: 10px;
1581 margin-top: 10px;
1577 margin-bottom: 20px;
1582 margin-bottom: 20px;
1578 }
1583 }
1579
1584
1580 .pull-request-merge .pull-request-wrap {
1585 .pull-request-merge .pull-request-wrap {
1581 height: 25px;
1586 height: 25px;
1582 padding: 5px 0;
1587 padding: 5px 0;
1583 }
1588 }
1584
1589
1585 .pull-request-merge span {
1590 .pull-request-merge span {
1586 margin-right: 10px;
1591 margin-right: 10px;
1587 }
1592 }
1588 #close_pull_request {
1593 #close_pull_request {
1589 margin-right: 0px;
1594 margin-right: 0px;
1590 }
1595 }
1591
1596
1592 .empty_data {
1597 .empty_data {
1593 color: @grey4;
1598 color: @grey4;
1594 }
1599 }
1595
1600
1596 #changeset_compare_view_content {
1601 #changeset_compare_view_content {
1597 margin-bottom: @space;
1602 margin-bottom: @space;
1598 clear: both;
1603 clear: both;
1599 width: 100%;
1604 width: 100%;
1600 box-sizing: border-box;
1605 box-sizing: border-box;
1601 .border-radius(@border-radius);
1606 .border-radius(@border-radius);
1602
1607
1603 .help-block {
1608 .help-block {
1604 margin: @padding 0;
1609 margin: @padding 0;
1605 color: @text-color;
1610 color: @text-color;
1606 }
1611 }
1607
1612
1608 .empty_data {
1613 .empty_data {
1609 margin: @padding 0;
1614 margin: @padding 0;
1610 }
1615 }
1611
1616
1612 .alert {
1617 .alert {
1613 margin-bottom: @space;
1618 margin-bottom: @space;
1614 }
1619 }
1615 }
1620 }
1616
1621
1617 .table_disp {
1622 .table_disp {
1618 .status {
1623 .status {
1619 width: auto;
1624 width: auto;
1620
1625
1621 .flag_status {
1626 .flag_status {
1622 float: left;
1627 float: left;
1623 }
1628 }
1624 }
1629 }
1625 }
1630 }
1626
1631
1627 .status_box_menu {
1632 .status_box_menu {
1628 margin: 0;
1633 margin: 0;
1629 }
1634 }
1630
1635
1631 .notification-table{
1636 .notification-table{
1632 margin-bottom: @space;
1637 margin-bottom: @space;
1633 display: table;
1638 display: table;
1634 width: 100%;
1639 width: 100%;
1635
1640
1636 .container{
1641 .container{
1637 display: table-row;
1642 display: table-row;
1638
1643
1639 .notification-header{
1644 .notification-header{
1640 border-bottom: @border-thickness solid @border-default-color;
1645 border-bottom: @border-thickness solid @border-default-color;
1641 }
1646 }
1642
1647
1643 .notification-subject{
1648 .notification-subject{
1644 display: table-cell;
1649 display: table-cell;
1645 }
1650 }
1646 }
1651 }
1647 }
1652 }
1648
1653
1649 // Notifications
1654 // Notifications
1650 .notification-header{
1655 .notification-header{
1651 display: table;
1656 display: table;
1652 width: 100%;
1657 width: 100%;
1653 padding: floor(@basefontsize/2) 0;
1658 padding: floor(@basefontsize/2) 0;
1654 line-height: 1em;
1659 line-height: 1em;
1655
1660
1656 .desc, .delete-notifications, .read-notifications{
1661 .desc, .delete-notifications, .read-notifications{
1657 display: table-cell;
1662 display: table-cell;
1658 text-align: left;
1663 text-align: left;
1659 }
1664 }
1660
1665
1661 .desc{
1666 .desc{
1662 width: 1163px;
1667 width: 1163px;
1663 }
1668 }
1664
1669
1665 .delete-notifications, .read-notifications{
1670 .delete-notifications, .read-notifications{
1666 width: 35px;
1671 width: 35px;
1667 min-width: 35px; //fixes when only one button is displayed
1672 min-width: 35px; //fixes when only one button is displayed
1668 }
1673 }
1669 }
1674 }
1670
1675
1671 .notification-body {
1676 .notification-body {
1672 .markdown-block,
1677 .markdown-block,
1673 .rst-block {
1678 .rst-block {
1674 padding: @padding 0;
1679 padding: @padding 0;
1675 }
1680 }
1676
1681
1677 .notification-subject {
1682 .notification-subject {
1678 padding: @textmargin 0;
1683 padding: @textmargin 0;
1679 border-bottom: @border-thickness solid @border-default-color;
1684 border-bottom: @border-thickness solid @border-default-color;
1680 }
1685 }
1681 }
1686 }
1682
1687
1683
1688
1684 .notifications_buttons{
1689 .notifications_buttons{
1685 float: right;
1690 float: right;
1686 }
1691 }
1687
1692
1688 #notification-status{
1693 #notification-status{
1689 display: inline;
1694 display: inline;
1690 }
1695 }
1691
1696
1692 // Repositories
1697 // Repositories
1693
1698
1694 #summary.fields{
1699 #summary.fields{
1695 display: table;
1700 display: table;
1696
1701
1697 .field{
1702 .field{
1698 display: table-row;
1703 display: table-row;
1699
1704
1700 .label-summary{
1705 .label-summary{
1701 display: table-cell;
1706 display: table-cell;
1702 min-width: @label-summary-minwidth;
1707 min-width: @label-summary-minwidth;
1703 padding-top: @padding/2;
1708 padding-top: @padding/2;
1704 padding-bottom: @padding/2;
1709 padding-bottom: @padding/2;
1705 padding-right: @padding/2;
1710 padding-right: @padding/2;
1706 }
1711 }
1707
1712
1708 .input{
1713 .input{
1709 display: table-cell;
1714 display: table-cell;
1710 padding: @padding/2;
1715 padding: @padding/2;
1711
1716
1712 input{
1717 input{
1713 min-width: 29em;
1718 min-width: 29em;
1714 padding: @padding/4;
1719 padding: @padding/4;
1715 }
1720 }
1716 }
1721 }
1717 .statistics, .downloads{
1722 .statistics, .downloads{
1718 .disabled{
1723 .disabled{
1719 color: @grey4;
1724 color: @grey4;
1720 }
1725 }
1721 }
1726 }
1722 }
1727 }
1723 }
1728 }
1724
1729
1725 #summary{
1730 #summary{
1726 width: 70%;
1731 width: 70%;
1727 }
1732 }
1728
1733
1729
1734
1730 // Journal
1735 // Journal
1731 .journal.title {
1736 .journal.title {
1732 h5 {
1737 h5 {
1733 float: left;
1738 float: left;
1734 margin: 0;
1739 margin: 0;
1735 width: 70%;
1740 width: 70%;
1736 }
1741 }
1737
1742
1738 ul {
1743 ul {
1739 float: right;
1744 float: right;
1740 display: inline-block;
1745 display: inline-block;
1741 margin: 0;
1746 margin: 0;
1742 width: 30%;
1747 width: 30%;
1743 text-align: right;
1748 text-align: right;
1744
1749
1745 li {
1750 li {
1746 display: inline;
1751 display: inline;
1747 font-size: @journal-fontsize;
1752 font-size: @journal-fontsize;
1748 line-height: 1em;
1753 line-height: 1em;
1749
1754
1750 &:before { content: none; }
1755 &:before { content: none; }
1751 }
1756 }
1752 }
1757 }
1753 }
1758 }
1754
1759
1755 .filterexample {
1760 .filterexample {
1756 position: absolute;
1761 position: absolute;
1757 top: 95px;
1762 top: 95px;
1758 left: @contentpadding;
1763 left: @contentpadding;
1759 color: @rcblue;
1764 color: @rcblue;
1760 font-size: 11px;
1765 font-size: 11px;
1761 font-family: @text-regular;
1766 font-family: @text-regular;
1762 cursor: help;
1767 cursor: help;
1763
1768
1764 &:hover {
1769 &:hover {
1765 color: @rcdarkblue;
1770 color: @rcdarkblue;
1766 }
1771 }
1767
1772
1768 @media (max-width:768px) {
1773 @media (max-width:768px) {
1769 position: relative;
1774 position: relative;
1770 top: auto;
1775 top: auto;
1771 left: auto;
1776 left: auto;
1772 display: block;
1777 display: block;
1773 }
1778 }
1774 }
1779 }
1775
1780
1776
1781
1777 #journal{
1782 #journal{
1778 margin-bottom: @space;
1783 margin-bottom: @space;
1779
1784
1780 .journal_day{
1785 .journal_day{
1781 margin-bottom: @textmargin/2;
1786 margin-bottom: @textmargin/2;
1782 padding-bottom: @textmargin/2;
1787 padding-bottom: @textmargin/2;
1783 font-size: @journal-fontsize;
1788 font-size: @journal-fontsize;
1784 border-bottom: @border-thickness solid @border-default-color;
1789 border-bottom: @border-thickness solid @border-default-color;
1785 }
1790 }
1786
1791
1787 .journal_container{
1792 .journal_container{
1788 margin-bottom: @space;
1793 margin-bottom: @space;
1789
1794
1790 .journal_user{
1795 .journal_user{
1791 display: inline-block;
1796 display: inline-block;
1792 }
1797 }
1793 .journal_action_container{
1798 .journal_action_container{
1794 display: block;
1799 display: block;
1795 margin-top: @textmargin;
1800 margin-top: @textmargin;
1796
1801
1797 div{
1802 div{
1798 display: inline;
1803 display: inline;
1799 }
1804 }
1800
1805
1801 div.journal_action_params{
1806 div.journal_action_params{
1802 display: block;
1807 display: block;
1803 }
1808 }
1804
1809
1805 div.journal_repo:after{
1810 div.journal_repo:after{
1806 content: "\A";
1811 content: "\A";
1807 white-space: pre;
1812 white-space: pre;
1808 }
1813 }
1809
1814
1810 div.date{
1815 div.date{
1811 display: block;
1816 display: block;
1812 margin-bottom: @textmargin;
1817 margin-bottom: @textmargin;
1813 }
1818 }
1814 }
1819 }
1815 }
1820 }
1816 }
1821 }
1817
1822
1818 // Files
1823 // Files
1819 .edit-file-title {
1824 .edit-file-title {
1820 border-bottom: @border-thickness solid @border-default-color;
1825 border-bottom: @border-thickness solid @border-default-color;
1821
1826
1822 .breadcrumbs {
1827 .breadcrumbs {
1823 margin-bottom: 0;
1828 margin-bottom: 0;
1824 }
1829 }
1825 }
1830 }
1826
1831
1827 .edit-file-fieldset {
1832 .edit-file-fieldset {
1828 margin-top: @sidebarpadding;
1833 margin-top: @sidebarpadding;
1829
1834
1830 .fieldset {
1835 .fieldset {
1831 .left-label {
1836 .left-label {
1832 width: 13%;
1837 width: 13%;
1833 }
1838 }
1834 .right-content {
1839 .right-content {
1835 width: 87%;
1840 width: 87%;
1836 max-width: 100%;
1841 max-width: 100%;
1837 }
1842 }
1838 .filename-label {
1843 .filename-label {
1839 margin-top: 13px;
1844 margin-top: 13px;
1840 }
1845 }
1841 .commit-message-label {
1846 .commit-message-label {
1842 margin-top: 4px;
1847 margin-top: 4px;
1843 }
1848 }
1844 .file-upload-input {
1849 .file-upload-input {
1845 input {
1850 input {
1846 display: none;
1851 display: none;
1847 }
1852 }
1848 }
1853 }
1849 p {
1854 p {
1850 margin-top: 5px;
1855 margin-top: 5px;
1851 }
1856 }
1852
1857
1853 }
1858 }
1854 .custom-path-link {
1859 .custom-path-link {
1855 margin-left: 5px;
1860 margin-left: 5px;
1856 }
1861 }
1857 #commit {
1862 #commit {
1858 resize: vertical;
1863 resize: vertical;
1859 }
1864 }
1860 }
1865 }
1861
1866
1862 .delete-file-preview {
1867 .delete-file-preview {
1863 max-height: 250px;
1868 max-height: 250px;
1864 }
1869 }
1865
1870
1866 .new-file,
1871 .new-file,
1867 #filter_activate,
1872 #filter_activate,
1868 #filter_deactivate {
1873 #filter_deactivate {
1869 float: left;
1874 float: left;
1870 margin: 0 0 0 15px;
1875 margin: 0 0 0 15px;
1871 }
1876 }
1872
1877
1873 h3.files_location{
1878 h3.files_location{
1874 line-height: 2.4em;
1879 line-height: 2.4em;
1875 }
1880 }
1876
1881
1877 .browser-nav {
1882 .browser-nav {
1878 display: table;
1883 display: table;
1879 margin-bottom: @space;
1884 margin-bottom: @space;
1880
1885
1881
1886
1882 .info_box {
1887 .info_box {
1883 display: inline-table;
1888 display: inline-table;
1884 height: 2.5em;
1889 height: 2.5em;
1885
1890
1886 .browser-cur-rev, .info_box_elem {
1891 .browser-cur-rev, .info_box_elem {
1887 display: table-cell;
1892 display: table-cell;
1888 vertical-align: middle;
1893 vertical-align: middle;
1889 }
1894 }
1890
1895
1891 .info_box_elem {
1896 .info_box_elem {
1892 border-top: @border-thickness solid @rcblue;
1897 border-top: @border-thickness solid @rcblue;
1893 border-bottom: @border-thickness solid @rcblue;
1898 border-bottom: @border-thickness solid @rcblue;
1894
1899
1895 #at_rev, a {
1900 #at_rev, a {
1896 padding: 0.6em 0.9em;
1901 padding: 0.6em 0.9em;
1897 margin: 0;
1902 margin: 0;
1898 .box-shadow(none);
1903 .box-shadow(none);
1899 border: 0;
1904 border: 0;
1900 height: 12px;
1905 height: 12px;
1901 }
1906 }
1902
1907
1903 input#at_rev {
1908 input#at_rev {
1904 max-width: 50px;
1909 max-width: 50px;
1905 text-align: right;
1910 text-align: right;
1906 }
1911 }
1907
1912
1908 &.previous {
1913 &.previous {
1909 border: @border-thickness solid @rcblue;
1914 border: @border-thickness solid @rcblue;
1910 .disabled {
1915 .disabled {
1911 color: @grey4;
1916 color: @grey4;
1912 cursor: not-allowed;
1917 cursor: not-allowed;
1913 }
1918 }
1914 }
1919 }
1915
1920
1916 &.next {
1921 &.next {
1917 border: @border-thickness solid @rcblue;
1922 border: @border-thickness solid @rcblue;
1918 .disabled {
1923 .disabled {
1919 color: @grey4;
1924 color: @grey4;
1920 cursor: not-allowed;
1925 cursor: not-allowed;
1921 }
1926 }
1922 }
1927 }
1923 }
1928 }
1924
1929
1925 .browser-cur-rev {
1930 .browser-cur-rev {
1926
1931
1927 span{
1932 span{
1928 margin: 0;
1933 margin: 0;
1929 color: @rcblue;
1934 color: @rcblue;
1930 height: 12px;
1935 height: 12px;
1931 display: inline-block;
1936 display: inline-block;
1932 padding: 0.7em 1em ;
1937 padding: 0.7em 1em ;
1933 border: @border-thickness solid @rcblue;
1938 border: @border-thickness solid @rcblue;
1934 margin-right: @padding;
1939 margin-right: @padding;
1935 }
1940 }
1936 }
1941 }
1937 }
1942 }
1938
1943
1939 .search_activate {
1944 .search_activate {
1940 display: table-cell;
1945 display: table-cell;
1941 vertical-align: middle;
1946 vertical-align: middle;
1942
1947
1943 input, label{
1948 input, label{
1944 margin: 0;
1949 margin: 0;
1945 padding: 0;
1950 padding: 0;
1946 }
1951 }
1947
1952
1948 input{
1953 input{
1949 margin-left: @textmargin;
1954 margin-left: @textmargin;
1950 }
1955 }
1951
1956
1952 }
1957 }
1953 }
1958 }
1954
1959
1955 .browser-cur-rev{
1960 .browser-cur-rev{
1956 margin-bottom: @textmargin;
1961 margin-bottom: @textmargin;
1957 }
1962 }
1958
1963
1959 #node_filter_box_loading{
1964 #node_filter_box_loading{
1960 .info_text;
1965 .info_text;
1961 }
1966 }
1962
1967
1963 .browser-search {
1968 .browser-search {
1964 margin: -25px 0px 5px 0px;
1969 margin: -25px 0px 5px 0px;
1965 }
1970 }
1966
1971
1967 .node-filter {
1972 .node-filter {
1968 font-size: @repo-title-fontsize;
1973 font-size: @repo-title-fontsize;
1969 padding: 4px 0px 0px 0px;
1974 padding: 4px 0px 0px 0px;
1970
1975
1971 .node-filter-path {
1976 .node-filter-path {
1972 float: left;
1977 float: left;
1973 color: @grey4;
1978 color: @grey4;
1974 }
1979 }
1975 .node-filter-input {
1980 .node-filter-input {
1976 float: left;
1981 float: left;
1977 margin: -2px 0px 0px 2px;
1982 margin: -2px 0px 0px 2px;
1978 input {
1983 input {
1979 padding: 2px;
1984 padding: 2px;
1980 border: none;
1985 border: none;
1981 font-size: @repo-title-fontsize;
1986 font-size: @repo-title-fontsize;
1982 }
1987 }
1983 }
1988 }
1984 }
1989 }
1985
1990
1986
1991
1987 .browser-result{
1992 .browser-result{
1988 td a{
1993 td a{
1989 margin-left: 0.5em;
1994 margin-left: 0.5em;
1990 display: inline-block;
1995 display: inline-block;
1991
1996
1992 em{
1997 em{
1993 font-family: @text-bold;
1998 font-family: @text-bold;
1994 }
1999 }
1995 }
2000 }
1996 }
2001 }
1997
2002
1998 .browser-highlight{
2003 .browser-highlight{
1999 background-color: @grey5-alpha;
2004 background-color: @grey5-alpha;
2000 }
2005 }
2001
2006
2002
2007
2003 // Search
2008 // Search
2004
2009
2005 .search-form{
2010 .search-form{
2006 #q {
2011 #q {
2007 width: @search-form-width;
2012 width: @search-form-width;
2008 }
2013 }
2009 .fields{
2014 .fields{
2010 margin: 0 0 @space;
2015 margin: 0 0 @space;
2011 }
2016 }
2012
2017
2013 label{
2018 label{
2014 display: inline-block;
2019 display: inline-block;
2015 margin-right: @textmargin;
2020 margin-right: @textmargin;
2016 padding-top: 0.25em;
2021 padding-top: 0.25em;
2017 }
2022 }
2018
2023
2019
2024
2020 .results{
2025 .results{
2021 clear: both;
2026 clear: both;
2022 margin: 0 0 @padding;
2027 margin: 0 0 @padding;
2023 }
2028 }
2024 }
2029 }
2025
2030
2026 div.search-feedback-items {
2031 div.search-feedback-items {
2027 display: inline-block;
2032 display: inline-block;
2028 padding:0px 0px 0px 96px;
2033 padding:0px 0px 0px 96px;
2029 }
2034 }
2030
2035
2031 div.search-code-body {
2036 div.search-code-body {
2032 background-color: #ffffff; padding: 5px 0 5px 10px;
2037 background-color: #ffffff; padding: 5px 0 5px 10px;
2033 pre {
2038 pre {
2034 .match { background-color: #faffa6;}
2039 .match { background-color: #faffa6;}
2035 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2040 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2036 }
2041 }
2037 }
2042 }
2038
2043
2039 .expand_commit.search {
2044 .expand_commit.search {
2040 .show_more.open {
2045 .show_more.open {
2041 height: auto;
2046 height: auto;
2042 max-height: none;
2047 max-height: none;
2043 }
2048 }
2044 }
2049 }
2045
2050
2046 .search-results {
2051 .search-results {
2047
2052
2048 h2 {
2053 h2 {
2049 margin-bottom: 0;
2054 margin-bottom: 0;
2050 }
2055 }
2051 .codeblock {
2056 .codeblock {
2052 border: none;
2057 border: none;
2053 background: transparent;
2058 background: transparent;
2054 }
2059 }
2055
2060
2056 .codeblock-header {
2061 .codeblock-header {
2057 border: none;
2062 border: none;
2058 background: transparent;
2063 background: transparent;
2059 }
2064 }
2060
2065
2061 .code-body {
2066 .code-body {
2062 border: @border-thickness solid @border-default-color;
2067 border: @border-thickness solid @border-default-color;
2063 .border-radius(@border-radius);
2068 .border-radius(@border-radius);
2064 }
2069 }
2065
2070
2066 .td-commit {
2071 .td-commit {
2067 &:extend(pre);
2072 &:extend(pre);
2068 border-bottom: @border-thickness solid @border-default-color;
2073 border-bottom: @border-thickness solid @border-default-color;
2069 }
2074 }
2070
2075
2071 .message {
2076 .message {
2072 height: auto;
2077 height: auto;
2073 max-width: 350px;
2078 max-width: 350px;
2074 white-space: normal;
2079 white-space: normal;
2075 text-overflow: initial;
2080 text-overflow: initial;
2076 overflow: visible;
2081 overflow: visible;
2077
2082
2078 .match { background-color: #faffa6;}
2083 .match { background-color: #faffa6;}
2079 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2084 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2080 }
2085 }
2081
2086
2082 }
2087 }
2083
2088
2084 table.rctable td.td-search-results div {
2089 table.rctable td.td-search-results div {
2085 max-width: 100%;
2090 max-width: 100%;
2086 }
2091 }
2087
2092
2088 #tip-box, .tip-box{
2093 #tip-box, .tip-box{
2089 padding: @menupadding/2;
2094 padding: @menupadding/2;
2090 display: block;
2095 display: block;
2091 border: @border-thickness solid @border-highlight-color;
2096 border: @border-thickness solid @border-highlight-color;
2092 .border-radius(@border-radius);
2097 .border-radius(@border-radius);
2093 background-color: white;
2098 background-color: white;
2094 z-index: 99;
2099 z-index: 99;
2095 white-space: pre-wrap;
2100 white-space: pre-wrap;
2096 }
2101 }
2097
2102
2098 #linktt {
2103 #linktt {
2099 width: 79px;
2104 width: 79px;
2100 }
2105 }
2101
2106
2102 #help_kb .modal-content{
2107 #help_kb .modal-content{
2103 max-width: 750px;
2108 max-width: 750px;
2104 margin: 10% auto;
2109 margin: 10% auto;
2105
2110
2106 table{
2111 table{
2107 td,th{
2112 td,th{
2108 border-bottom: none;
2113 border-bottom: none;
2109 line-height: 2.5em;
2114 line-height: 2.5em;
2110 }
2115 }
2111 th{
2116 th{
2112 padding-bottom: @textmargin/2;
2117 padding-bottom: @textmargin/2;
2113 }
2118 }
2114 td.keys{
2119 td.keys{
2115 text-align: center;
2120 text-align: center;
2116 }
2121 }
2117 }
2122 }
2118
2123
2119 .block-left{
2124 .block-left{
2120 width: 45%;
2125 width: 45%;
2121 margin-right: 5%;
2126 margin-right: 5%;
2122 }
2127 }
2123 .modal-footer{
2128 .modal-footer{
2124 clear: both;
2129 clear: both;
2125 }
2130 }
2126 .key.tag{
2131 .key.tag{
2127 padding: 0.5em;
2132 padding: 0.5em;
2128 background-color: @rcblue;
2133 background-color: @rcblue;
2129 color: white;
2134 color: white;
2130 border-color: @rcblue;
2135 border-color: @rcblue;
2131 .box-shadow(none);
2136 .box-shadow(none);
2132 }
2137 }
2133 }
2138 }
2134
2139
2135
2140
2136
2141
2137 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2142 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2138
2143
2139 @import 'statistics-graph';
2144 @import 'statistics-graph';
2140 @import 'tables';
2145 @import 'tables';
2141 @import 'forms';
2146 @import 'forms';
2142 @import 'diff';
2147 @import 'diff';
2143 @import 'summary';
2148 @import 'summary';
2144 @import 'navigation';
2149 @import 'navigation';
2145
2150
2146 //--- SHOW/HIDE SECTIONS --//
2151 //--- SHOW/HIDE SECTIONS --//
2147
2152
2148 .btn-collapse {
2153 .btn-collapse {
2149 float: right;
2154 float: right;
2150 text-align: right;
2155 text-align: right;
2151 font-family: @text-light;
2156 font-family: @text-light;
2152 font-size: @basefontsize;
2157 font-size: @basefontsize;
2153 cursor: pointer;
2158 cursor: pointer;
2154 border: none;
2159 border: none;
2155 color: @rcblue;
2160 color: @rcblue;
2156 }
2161 }
2157
2162
2158 table.rctable,
2163 table.rctable,
2159 table.dataTable {
2164 table.dataTable {
2160 .btn-collapse {
2165 .btn-collapse {
2161 float: right;
2166 float: right;
2162 text-align: right;
2167 text-align: right;
2163 }
2168 }
2164 }
2169 }
2165
2170
2166
2171
2167 // TODO: johbo: Fix for IE10, this avoids that we see a border
2172 // TODO: johbo: Fix for IE10, this avoids that we see a border
2168 // and padding around checkboxes and radio boxes. Move to the right place,
2173 // and padding around checkboxes and radio boxes. Move to the right place,
2169 // or better: Remove this once we did the form refactoring.
2174 // or better: Remove this once we did the form refactoring.
2170 input[type=checkbox],
2175 input[type=checkbox],
2171 input[type=radio] {
2176 input[type=radio] {
2172 padding: 0;
2177 padding: 0;
2173 border: none;
2178 border: none;
2174 }
2179 }
2175
2180
2176 .toggle-ajax-spinner{
2181 .toggle-ajax-spinner{
2177 height: 16px;
2182 height: 16px;
2178 width: 16px;
2183 width: 16px;
2179 }
2184 }
@@ -1,621 +1,636 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 <span id="pr-title">
11 <span id="pr-title">
12 ${c.pull_request.title}
12 ${c.pull_request.title}
13 %if c.pull_request.is_closed():
13 %if c.pull_request.is_closed():
14 (${_('Closed')})
14 (${_('Closed')})
15 %endif
15 %endif
16 </span>
16 </span>
17 <div id="pr-title-edit" class="input" style="display: none;">
17 <div id="pr-title-edit" class="input" style="display: none;">
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 </div>
19 </div>
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_nav()">
22 <%def name="menu_bar_nav()">
23 ${self.menu_items(active='repositories')}
23 ${self.menu_items(active='repositories')}
24 </%def>
24 </%def>
25
25
26 <%def name="menu_bar_subnav()">
26 <%def name="menu_bar_subnav()">
27 ${self.repo_menu(active='showpullrequest')}
27 ${self.repo_menu(active='showpullrequest')}
28 </%def>
28 </%def>
29
29
30 <%def name="main()">
30 <%def name="main()">
31 <script type="text/javascript">
31 <script type="text/javascript">
32 // TODO: marcink switch this to pyroutes
32 // TODO: marcink switch this to pyroutes
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 </script>
35 </script>
36 <div class="box">
36 <div class="box">
37 <div class="title">
37 <div class="title">
38 ${self.repo_page_title(c.rhodecode_db_repo)}
38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 </div>
39 </div>
40
40
41 ${self.breadcrumbs()}
41 ${self.breadcrumbs()}
42
42
43
43
44 <div class="box pr-summary">
44 <div class="box pr-summary">
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
47 <div class="pr-details-title">
47 <div class="pr-details-title">
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
49 %if c.allowed_to_update:
50 <span id="open_edit_pullrequest" class="block-right action_button">${_('Edit')}</span>
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 <span id="close_edit_pullrequest" class="block-right action_button" style="display: none;">${_('Close')}</span>
51 % if c.allowed_to_delete:
52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 ${h.end_form()}
56 % else:
57 ${_('Delete')}
58 % endif
59 </div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel edit')}</div>
52 %endif
62 %endif
53 </div>
63 </div>
54
64
55 <div id="summary" class="fields pr-details-content">
65 <div id="summary" class="fields pr-details-content">
56 <div class="field">
66 <div class="field">
57 <div class="label-summary">
67 <div class="label-summary">
58 <label>${_('Origin')}:</label>
68 <label>${_('Origin')}:</label>
59 </div>
69 </div>
60 <div class="input">
70 <div class="input">
61 <div class="pr-origininfo">
71 <div class="pr-origininfo">
62 ## branch link is only valid if it is a branch
72 ## branch link is only valid if it is a branch
63 <span class="tag">
73 <span class="tag">
64 %if c.pull_request.source_ref_parts.type == 'branch':
74 %if c.pull_request.source_ref_parts.type == 'branch':
65 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
66 %else:
76 %else:
67 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
68 %endif
78 %endif
69 </span>
79 </span>
70 <span class="clone-url">
80 <span class="clone-url">
71 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
72 </span>
82 </span>
73 </div>
83 </div>
74 <div class="pr-pullinfo">
84 <div class="pr-pullinfo">
75 %if h.is_hg(c.pull_request.source_repo):
85 %if h.is_hg(c.pull_request.source_repo):
76 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
77 %elif h.is_git(c.pull_request.source_repo):
87 %elif h.is_git(c.pull_request.source_repo):
78 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
79 %endif
89 %endif
80 </div>
90 </div>
81 </div>
91 </div>
82 </div>
92 </div>
83 <div class="field">
93 <div class="field">
84 <div class="label-summary">
94 <div class="label-summary">
85 <label>${_('Target')}:</label>
95 <label>${_('Target')}:</label>
86 </div>
96 </div>
87 <div class="input">
97 <div class="input">
88 <div class="pr-targetinfo">
98 <div class="pr-targetinfo">
89 ## branch link is only valid if it is a branch
99 ## branch link is only valid if it is a branch
90 <span class="tag">
100 <span class="tag">
91 %if c.pull_request.target_ref_parts.type == 'branch':
101 %if c.pull_request.target_ref_parts.type == 'branch':
92 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
93 %else:
103 %else:
94 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
95 %endif
105 %endif
96 </span>
106 </span>
97 <span class="clone-url">
107 <span class="clone-url">
98 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
99 </span>
109 </span>
100 </div>
110 </div>
101 </div>
111 </div>
102 </div>
112 </div>
103
113
104 ## Link to the shadow repository.
114 ## Link to the shadow repository.
105 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
115 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
106 <div class="field">
116 <div class="field">
107 <div class="label-summary">
117 <div class="label-summary">
108 <label>Merge:</label>
118 <label>Merge:</label>
109 </div>
119 </div>
110 <div class="input">
120 <div class="input">
111 <div class="pr-mergeinfo">
121 <div class="pr-mergeinfo">
112 %if h.is_hg(c.pull_request.target_repo):
122 %if h.is_hg(c.pull_request.target_repo):
113 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
114 %elif h.is_git(c.pull_request.target_repo):
124 %elif h.is_git(c.pull_request.target_repo):
115 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
116 %endif
126 %endif
117 </div>
127 </div>
118 </div>
128 </div>
119 </div>
129 </div>
120 %endif
130 %endif
121
131
122 <div class="field">
132 <div class="field">
123 <div class="label-summary">
133 <div class="label-summary">
124 <label>${_('Review')}:</label>
134 <label>${_('Review')}:</label>
125 </div>
135 </div>
126 <div class="input">
136 <div class="input">
127 %if c.pull_request_review_status:
137 %if c.pull_request_review_status:
128 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
138 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
129 <span class="changeset-status-lbl tooltip">
139 <span class="changeset-status-lbl tooltip">
130 %if c.pull_request.is_closed():
140 %if c.pull_request.is_closed():
131 ${_('Closed')},
141 ${_('Closed')},
132 %endif
142 %endif
133 ${h.commit_status_lbl(c.pull_request_review_status)}
143 ${h.commit_status_lbl(c.pull_request_review_status)}
134 </span>
144 </span>
135 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
145 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
136 %endif
146 %endif
137 </div>
147 </div>
138 </div>
148 </div>
139 <div class="field">
149 <div class="field">
140 <div class="pr-description-label label-summary">
150 <div class="pr-description-label label-summary">
141 <label>${_('Description')}:</label>
151 <label>${_('Description')}:</label>
142 </div>
152 </div>
143 <div id="pr-desc" class="input">
153 <div id="pr-desc" class="input">
144 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
154 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
145 </div>
155 </div>
146 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
156 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
147 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
157 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
148 </div>
158 </div>
149 </div>
159 </div>
150 <div class="field">
160 <div class="field">
151 <div class="label-summary">
161 <div class="label-summary">
152 <label>${_('Comments')}:</label>
162 <label>${_('Comments')}:</label>
153 </div>
163 </div>
154 <div class="input">
164 <div class="input">
155 <div>
165 <div>
156 <div class="comments-number">
166 <div class="comments-number">
157 %if c.comments:
167 %if c.comments:
158 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
168 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
159 %else:
169 %else:
160 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
170 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
161 %endif
171 %endif
162 %if c.inline_cnt:
172 %if c.inline_cnt:
163 ## this is replaced with a proper link to first comment via JS linkifyComments() func
173 ## this is replaced with a proper link to first comment via JS linkifyComments() func
164 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
174 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
165 %else:
175 %else:
166 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
176 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
167 %endif
177 %endif
168
178
169 % if c.outdated_cnt:
179 % if c.outdated_cnt:
170 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
180 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
171 % endif
181 % endif
172 </div>
182 </div>
173 </div>
183 </div>
174 </div>
184 </div>
175 </div>
185 </div>
176 <div id="pr-save" class="field" style="display: none;">
186 <div id="pr-save" class="field" style="display: none;">
177 <div class="label-summary"></div>
187 <div class="label-summary"></div>
178 <div class="input">
188 <div class="input">
179 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
189 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
180 </div>
190 </div>
181 </div>
191 </div>
182 </div>
192 </div>
183 </div>
193 </div>
184 <div>
194 <div>
185 ## AUTHOR
195 ## AUTHOR
186 <div class="reviewers-title block-right">
196 <div class="reviewers-title block-right">
187 <div class="pr-details-title">
197 <div class="pr-details-title">
188 ${_('Author')}
198 ${_('Author')}
189 </div>
199 </div>
190 </div>
200 </div>
191 <div class="block-right pr-details-content reviewers">
201 <div class="block-right pr-details-content reviewers">
192 <ul class="group_members">
202 <ul class="group_members">
193 <li>
203 <li>
194 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
204 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
195 </li>
205 </li>
196 </ul>
206 </ul>
197 </div>
207 </div>
198 ## REVIEWERS
208 ## REVIEWERS
199 <div class="reviewers-title block-right">
209 <div class="reviewers-title block-right">
200 <div class="pr-details-title">
210 <div class="pr-details-title">
201 ${_('Pull request reviewers')}
211 ${_('Pull request reviewers')}
202 %if c.allowed_to_update:
212 %if c.allowed_to_update:
203 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
213 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
204 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
214 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
205 %endif
215 %endif
206 </div>
216 </div>
207 </div>
217 </div>
208 <div id="reviewers" class="block-right pr-details-content reviewers">
218 <div id="reviewers" class="block-right pr-details-content reviewers">
209 ## members goes here !
219 ## members goes here !
210 <input type="hidden" name="__start__" value="review_members:sequence">
220 <input type="hidden" name="__start__" value="review_members:sequence">
211 <ul id="review_members" class="group_members">
221 <ul id="review_members" class="group_members">
212 %for member,reasons,status in c.pull_request_reviewers:
222 %for member,reasons,status in c.pull_request_reviewers:
213 <li id="reviewer_${member.user_id}">
223 <li id="reviewer_${member.user_id}">
214 <div class="reviewers_member">
224 <div class="reviewers_member">
215 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
225 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
216 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
226 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
217 </div>
227 </div>
218 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
228 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
219 ${self.gravatar_with_user(member.email, 16)}
229 ${self.gravatar_with_user(member.email, 16)}
220 </div>
230 </div>
221 <input type="hidden" name="__start__" value="reviewer:mapping">
231 <input type="hidden" name="__start__" value="reviewer:mapping">
222 <input type="hidden" name="__start__" value="reasons:sequence">
232 <input type="hidden" name="__start__" value="reasons:sequence">
223 %for reason in reasons:
233 %for reason in reasons:
224 <div class="reviewer_reason">- ${reason}</div>
234 <div class="reviewer_reason">- ${reason}</div>
225 <input type="hidden" name="reason" value="${reason}">
235 <input type="hidden" name="reason" value="${reason}">
226
236
227 %endfor
237 %endfor
228 <input type="hidden" name="__end__" value="reasons:sequence">
238 <input type="hidden" name="__end__" value="reasons:sequence">
229 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
239 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
230 <input type="hidden" name="__end__" value="reviewer:mapping">
240 <input type="hidden" name="__end__" value="reviewer:mapping">
231 %if c.allowed_to_update:
241 %if c.allowed_to_update:
232 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
242 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
233 <i class="icon-remove-sign" ></i>
243 <i class="icon-remove-sign" ></i>
234 </div>
244 </div>
235 %endif
245 %endif
236 </div>
246 </div>
237 </li>
247 </li>
238 %endfor
248 %endfor
239 </ul>
249 </ul>
240 <input type="hidden" name="__end__" value="review_members:sequence">
250 <input type="hidden" name="__end__" value="review_members:sequence">
241 %if not c.pull_request.is_closed():
251 %if not c.pull_request.is_closed():
242 <div id="add_reviewer_input" class='ac' style="display: none;">
252 <div id="add_reviewer_input" class='ac' style="display: none;">
243 %if c.allowed_to_update:
253 %if c.allowed_to_update:
244 <div class="reviewer_ac">
254 <div class="reviewer_ac">
245 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
255 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
246 <div id="reviewers_container"></div>
256 <div id="reviewers_container"></div>
247 </div>
257 </div>
248 <div>
258 <div>
249 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
259 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
250 </div>
260 </div>
251 %endif
261 %endif
252 </div>
262 </div>
253 %endif
263 %endif
254 </div>
264 </div>
255 </div>
265 </div>
256 </div>
266 </div>
257 <div class="box">
267 <div class="box">
258 ##DIFF
268 ##DIFF
259 <div class="table" >
269 <div class="table" >
260 <div id="changeset_compare_view_content">
270 <div id="changeset_compare_view_content">
261 ##CS
271 ##CS
262 % if c.missing_requirements:
272 % if c.missing_requirements:
263 <div class="box">
273 <div class="box">
264 <div class="alert alert-warning">
274 <div class="alert alert-warning">
265 <div>
275 <div>
266 <strong>${_('Missing requirements:')}</strong>
276 <strong>${_('Missing requirements:')}</strong>
267 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
277 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
268 </div>
278 </div>
269 </div>
279 </div>
270 </div>
280 </div>
271 % elif c.missing_commits:
281 % elif c.missing_commits:
272 <div class="box">
282 <div class="box">
273 <div class="alert alert-warning">
283 <div class="alert alert-warning">
274 <div>
284 <div>
275 <strong>${_('Missing commits')}:</strong>
285 <strong>${_('Missing commits')}:</strong>
276 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
286 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
277 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
287 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
278 </div>
288 </div>
279 </div>
289 </div>
280 </div>
290 </div>
281 % endif
291 % endif
282 <div class="compare_view_commits_title">
292 <div class="compare_view_commits_title">
283 % if c.allowed_to_update and not c.pull_request.is_closed():
293 % if c.allowed_to_update and not c.pull_request.is_closed():
284 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
294 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
285 % endif
295 % endif
286 % if len(c.commit_ranges):
296 % if len(c.commit_ranges):
287 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
297 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
288 % endif
298 % endif
289 </div>
299 </div>
290 % if not c.missing_commits:
300 % if not c.missing_commits:
291 <%include file="/compare/compare_commits.html" />
301 <%include file="/compare/compare_commits.html" />
292 ## FILES
302 ## FILES
293 <div class="cs_files_title">
303 <div class="cs_files_title">
294 <span class="cs_files_expand">
304 <span class="cs_files_expand">
295 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
305 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
296 </span>
306 </span>
297 <h2>
307 <h2>
298 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
308 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
299 </h2>
309 </h2>
300 </div>
310 </div>
301 % endif
311 % endif
302 <div class="cs_files">
312 <div class="cs_files">
303 %if not c.files and not c.missing_commits:
313 %if not c.files and not c.missing_commits:
304 <span class="empty_data">${_('No files')}</span>
314 <span class="empty_data">${_('No files')}</span>
305 %endif
315 %endif
306 <table class="compare_view_files">
316 <table class="compare_view_files">
307 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
317 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
308 %for FID, change, path, stats in c.files:
318 %for FID, change, path, stats in c.files:
309 <tr class="cs_${change} collapse_file" fid="${FID}">
319 <tr class="cs_${change} collapse_file" fid="${FID}">
310 <td class="cs_icon_td">
320 <td class="cs_icon_td">
311 <span class="collapse_file_icon" fid="${FID}"></span>
321 <span class="collapse_file_icon" fid="${FID}"></span>
312 </td>
322 </td>
313 <td class="cs_icon_td">
323 <td class="cs_icon_td">
314 <div class="flag_status not_reviewed hidden"></div>
324 <div class="flag_status not_reviewed hidden"></div>
315 </td>
325 </td>
316 <td class="cs_${change}" id="a_${FID}">
326 <td class="cs_${change}" id="a_${FID}">
317 <div class="node">
327 <div class="node">
318 <a href="#a_${FID}">
328 <a href="#a_${FID}">
319 <i class="icon-file-${change.lower()}"></i>
329 <i class="icon-file-${change.lower()}"></i>
320 ${h.safe_unicode(path)}
330 ${h.safe_unicode(path)}
321 </a>
331 </a>
322 </div>
332 </div>
323 </td>
333 </td>
324 <td>
334 <td>
325 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
335 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
326 <div class="comment-bubble pull-right" data-path="${path}">
336 <div class="comment-bubble pull-right" data-path="${path}">
327 <i class="icon-comment"></i>
337 <i class="icon-comment"></i>
328 </div>
338 </div>
329 </td>
339 </td>
330 </tr>
340 </tr>
331 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
341 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
332 <td></td>
342 <td></td>
333 <td></td>
343 <td></td>
334 <td class="cs_${change}">
344 <td class="cs_${change}">
335 %if c.target_repo.repo_name == c.repo_name:
345 %if c.target_repo.repo_name == c.repo_name:
336 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
346 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
337 %else:
347 %else:
338 ## this is slightly different case later, since the other repo can have this
348 ## this is slightly different case later, since the other repo can have this
339 ## file in other state than the origin repo
349 ## file in other state than the origin repo
340 ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
350 ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
341 %endif
351 %endif
342 </td>
352 </td>
343 <td class="td-actions rc-form">
353 <td class="td-actions rc-form">
344 <div data-comment-id="${FID}" class="btn-link show-inline-comments comments-visible">
354 <div data-comment-id="${FID}" class="btn-link show-inline-comments comments-visible">
345 <span class="comments-show">${_('Show comments')}</span>
355 <span class="comments-show">${_('Show comments')}</span>
346 <span class="comments-hide">${_('Hide comments')}</span>
356 <span class="comments-hide">${_('Hide comments')}</span>
347 </div>
357 </div>
348 </td>
358 </td>
349 </tr>
359 </tr>
350 <tr id="tr_${FID}">
360 <tr id="tr_${FID}">
351 <td></td>
361 <td></td>
352 <td></td>
362 <td></td>
353 <td class="injected_diff" colspan="2">
363 <td class="injected_diff" colspan="2">
354 ${diff_block.diff_block_simple([c.changes[FID]])}
364 ${diff_block.diff_block_simple([c.changes[FID]])}
355 </td>
365 </td>
356 </tr>
366 </tr>
357
367
358 ## Loop through inline comments
368 ## Loop through inline comments
359 % if c.outdated_comments.get(path,False):
369 % if c.outdated_comments.get(path,False):
360 <tr class="outdated">
370 <tr class="outdated">
361 <td></td>
371 <td></td>
362 <td></td>
372 <td></td>
363 <td colspan="2">
373 <td colspan="2">
364 <p>${_('Outdated Inline Comments')}:</p>
374 <p>${_('Outdated Inline Comments')}:</p>
365 </td>
375 </td>
366 </tr>
376 </tr>
367 <tr class="outdated">
377 <tr class="outdated">
368 <td></td>
378 <td></td>
369 <td></td>
379 <td></td>
370 <td colspan="2" class="outdated_comment_block">
380 <td colspan="2" class="outdated_comment_block">
371 % for line, comments in c.outdated_comments[path].iteritems():
381 % for line, comments in c.outdated_comments[path].iteritems():
372 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
382 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
373 % for co in comments:
383 % for co in comments:
374 ${comment.comment_block_outdated(co)}
384 ${comment.comment_block_outdated(co)}
375 % endfor
385 % endfor
376 </div>
386 </div>
377 % endfor
387 % endfor
378 </td>
388 </td>
379 </tr>
389 </tr>
380 % endif
390 % endif
381 %endfor
391 %endfor
382 ## Loop through inline comments for deleted files
392 ## Loop through inline comments for deleted files
383 %for path in c.deleted_files:
393 %for path in c.deleted_files:
384 <tr class="outdated deleted">
394 <tr class="outdated deleted">
385 <td></td>
395 <td></td>
386 <td></td>
396 <td></td>
387 <td>${path}</td>
397 <td>${path}</td>
388 </tr>
398 </tr>
389 <tr class="outdated deleted">
399 <tr class="outdated deleted">
390 <td></td>
400 <td></td>
391 <td></td>
401 <td></td>
392 <td>(${_('Removed')})</td>
402 <td>(${_('Removed')})</td>
393 </tr>
403 </tr>
394 % if path in c.outdated_comments:
404 % if path in c.outdated_comments:
395 <tr class="outdated deleted">
405 <tr class="outdated deleted">
396 <td></td>
406 <td></td>
397 <td></td>
407 <td></td>
398 <td colspan="2">
408 <td colspan="2">
399 <p>${_('Outdated Inline Comments')}:</p>
409 <p>${_('Outdated Inline Comments')}:</p>
400 </td>
410 </td>
401 </tr>
411 </tr>
402 <tr class="outdated">
412 <tr class="outdated">
403 <td></td>
413 <td></td>
404 <td></td>
414 <td></td>
405 <td colspan="2" class="outdated_comment_block">
415 <td colspan="2" class="outdated_comment_block">
406 % for line, comments in c.outdated_comments[path].iteritems():
416 % for line, comments in c.outdated_comments[path].iteritems():
407 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
417 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
408 % for co in comments:
418 % for co in comments:
409 ${comment.comment_block_outdated(co)}
419 ${comment.comment_block_outdated(co)}
410 % endfor
420 % endfor
411 </div>
421 </div>
412 % endfor
422 % endfor
413 </td>
423 </td>
414 </tr>
424 </tr>
415 % endif
425 % endif
416 %endfor
426 %endfor
417 </table>
427 </table>
418 </div>
428 </div>
419 % if c.limited_diff:
429 % if c.limited_diff:
420 <h5>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
430 <h5>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
421 % endif
431 % endif
422 </div>
432 </div>
423 </div>
433 </div>
424
434
425 % if c.limited_diff:
435 % if c.limited_diff:
426 <p>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></p>
436 <p>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></p>
427 % endif
437 % endif
428
438
429 ## template for inline comment form
439 ## template for inline comment form
430 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
440 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
431 ${comment.comment_inline_form()}
441 ${comment.comment_inline_form()}
432
442
433 ## render comments and inlines
443 ## render comments and inlines
434 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
444 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
435
445
436 % if not c.pull_request.is_closed():
446 % if not c.pull_request.is_closed():
437 ## main comment form and it status
447 ## main comment form and it status
438 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
448 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
439 pull_request_id=c.pull_request.pull_request_id),
449 pull_request_id=c.pull_request.pull_request_id),
440 c.pull_request_review_status,
450 c.pull_request_review_status,
441 is_pull_request=True, change_status=c.allowed_to_change_status)}
451 is_pull_request=True, change_status=c.allowed_to_change_status)}
442 %endif
452 %endif
443
453
444 <script type="text/javascript">
454 <script type="text/javascript">
445 if (location.hash) {
455 if (location.hash) {
446 var result = splitDelimitedHash(location.hash);
456 var result = splitDelimitedHash(location.hash);
447 var line = $('html').find(result.loc);
457 var line = $('html').find(result.loc);
448 if (line.length > 0){
458 if (line.length > 0){
449 offsetScroll(line, 70);
459 offsetScroll(line, 70);
450 }
460 }
451 }
461 }
452 $(function(){
462 $(function(){
453 ReviewerAutoComplete('user');
463 ReviewerAutoComplete('user');
454 // custom code mirror
464 // custom code mirror
455 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
465 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
456
466
457 var PRDetails = {
467 var PRDetails = {
458 editButton: $('#open_edit_pullrequest'),
468 editButton: $('#open_edit_pullrequest'),
459 closeButton: $('#close_edit_pullrequest'),
469 closeButton: $('#close_edit_pullrequest'),
470 deleteButton: $('#delete_pullrequest'),
460 viewFields: $('#pr-desc, #pr-title'),
471 viewFields: $('#pr-desc, #pr-title'),
461 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
472 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
462
473
463 init: function() {
474 init: function() {
464 var that = this;
475 var that = this;
465 this.editButton.on('click', function(e) { that.edit(); });
476 this.editButton.on('click', function(e) { that.edit(); });
466 this.closeButton.on('click', function(e) { that.view(); });
477 this.closeButton.on('click', function(e) { that.view(); });
467 },
478 },
468
479
469 edit: function(event) {
480 edit: function(event) {
470 this.viewFields.hide();
481 this.viewFields.hide();
471 this.editButton.hide();
482 this.editButton.hide();
483 this.deleteButton.hide();
484 this.closeButton.show();
472 this.editFields.show();
485 this.editFields.show();
473 codeMirrorInstance.refresh();
486 codeMirrorInstance.refresh();
474 },
487 },
475
488
476 view: function(event) {
489 view: function(event) {
490 this.editButton.show();
491 this.deleteButton.show();
477 this.editFields.hide();
492 this.editFields.hide();
478 this.closeButton.hide();
493 this.closeButton.hide();
479 this.viewFields.show();
494 this.viewFields.show();
480 }
495 }
481 };
496 };
482
497
483 var ReviewersPanel = {
498 var ReviewersPanel = {
484 editButton: $('#open_edit_reviewers'),
499 editButton: $('#open_edit_reviewers'),
485 closeButton: $('#close_edit_reviewers'),
500 closeButton: $('#close_edit_reviewers'),
486 addButton: $('#add_reviewer_input'),
501 addButton: $('#add_reviewer_input'),
487 removeButtons: $('.reviewer_member_remove'),
502 removeButtons: $('.reviewer_member_remove'),
488
503
489 init: function() {
504 init: function() {
490 var that = this;
505 var that = this;
491 this.editButton.on('click', function(e) { that.edit(); });
506 this.editButton.on('click', function(e) { that.edit(); });
492 this.closeButton.on('click', function(e) { that.close(); });
507 this.closeButton.on('click', function(e) { that.close(); });
493 },
508 },
494
509
495 edit: function(event) {
510 edit: function(event) {
496 this.editButton.hide();
511 this.editButton.hide();
497 this.closeButton.show();
512 this.closeButton.show();
498 this.addButton.show();
513 this.addButton.show();
499 this.removeButtons.css('visibility', 'visible');
514 this.removeButtons.css('visibility', 'visible');
500 },
515 },
501
516
502 close: function(event) {
517 close: function(event) {
503 this.editButton.show();
518 this.editButton.show();
504 this.closeButton.hide();
519 this.closeButton.hide();
505 this.addButton.hide();
520 this.addButton.hide();
506 this.removeButtons.css('visibility', 'hidden');
521 this.removeButtons.css('visibility', 'hidden');
507 }
522 },
508 };
523 };
509
524
510 PRDetails.init();
525 PRDetails.init();
511 ReviewersPanel.init();
526 ReviewersPanel.init();
512
527
513 $('#show-outdated-comments').on('click', function(e){
528 $('#show-outdated-comments').on('click', function(e){
514 var button = $(this);
529 var button = $(this);
515 var outdated = $('.outdated');
530 var outdated = $('.outdated');
516 if (button.html() === "(Show)") {
531 if (button.html() === "(Show)") {
517 button.html("(Hide)");
532 button.html("(Hide)");
518 outdated.show();
533 outdated.show();
519 } else {
534 } else {
520 button.html("(Show)");
535 button.html("(Show)");
521 outdated.hide();
536 outdated.hide();
522 }
537 }
523 });
538 });
524
539
525 $('.show-inline-comments').on('change', function(e){
540 $('.show-inline-comments').on('change', function(e){
526 var show = 'none';
541 var show = 'none';
527 var target = e.currentTarget;
542 var target = e.currentTarget;
528 if(target.checked){
543 if(target.checked){
529 show = ''
544 show = ''
530 }
545 }
531 var boxid = $(target).attr('id_for');
546 var boxid = $(target).attr('id_for');
532 var comments = $('#{0} .inline-comments'.format(boxid));
547 var comments = $('#{0} .inline-comments'.format(boxid));
533 var fn_display = function(idx){
548 var fn_display = function(idx){
534 $(this).css('display', show);
549 $(this).css('display', show);
535 };
550 };
536 $(comments).each(fn_display);
551 $(comments).each(fn_display);
537 var btns = $('#{0} .inline-comments-button'.format(boxid));
552 var btns = $('#{0} .inline-comments-button'.format(boxid));
538 $(btns).each(fn_display);
553 $(btns).each(fn_display);
539 });
554 });
540
555
541 // inject comments into their proper positions
556 // inject comments into their proper positions
542 var file_comments = $('.inline-comment-placeholder');
557 var file_comments = $('.inline-comment-placeholder');
543 %if c.pull_request.is_closed():
558 %if c.pull_request.is_closed():
544 renderInlineComments(file_comments, false);
559 renderInlineComments(file_comments, false);
545 %else:
560 %else:
546 renderInlineComments(file_comments, true);
561 renderInlineComments(file_comments, true);
547 %endif
562 %endif
548 var commentTotals = {};
563 var commentTotals = {};
549 $.each(file_comments, function(i, comment) {
564 $.each(file_comments, function(i, comment) {
550 var path = $(comment).attr('path');
565 var path = $(comment).attr('path');
551 var comms = $(comment).children().length;
566 var comms = $(comment).children().length;
552 if (path in commentTotals) {
567 if (path in commentTotals) {
553 commentTotals[path] += comms;
568 commentTotals[path] += comms;
554 } else {
569 } else {
555 commentTotals[path] = comms;
570 commentTotals[path] = comms;
556 }
571 }
557 });
572 });
558 $.each(commentTotals, function(path, total) {
573 $.each(commentTotals, function(path, total) {
559 var elem = $('.comment-bubble[data-path="'+ path +'"]');
574 var elem = $('.comment-bubble[data-path="'+ path +'"]');
560 elem.css('visibility', 'visible');
575 elem.css('visibility', 'visible');
561 elem.html(elem.html() + ' ' + total );
576 elem.html(elem.html() + ' ' + total );
562 });
577 });
563
578
564 $('#merge_pull_request_form').submit(function() {
579 $('#merge_pull_request_form').submit(function() {
565 if (!$('#merge_pull_request').attr('disabled')) {
580 if (!$('#merge_pull_request').attr('disabled')) {
566 $('#merge_pull_request').attr('disabled', 'disabled');
581 $('#merge_pull_request').attr('disabled', 'disabled');
567 }
582 }
568 return true;
583 return true;
569 });
584 });
570
585
571 $('#edit_pull_request').on('click', function(e){
586 $('#edit_pull_request').on('click', function(e){
572 var title = $('#pr-title-input').val();
587 var title = $('#pr-title-input').val();
573 var description = codeMirrorInstance.getValue();
588 var description = codeMirrorInstance.getValue();
574 editPullRequest(
589 editPullRequest(
575 "${c.repo_name}", "${c.pull_request.pull_request_id}",
590 "${c.repo_name}", "${c.pull_request.pull_request_id}",
576 title, description);
591 title, description);
577 });
592 });
578
593
579 $('#update_pull_request').on('click', function(e){
594 $('#update_pull_request').on('click', function(e){
580 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
595 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
581 });
596 });
582
597
583 $('#update_commits').on('click', function(e){
598 $('#update_commits').on('click', function(e){
584 var isDisabled = !$(e.currentTarget).attr('disabled');
599 var isDisabled = !$(e.currentTarget).attr('disabled');
585 $(e.currentTarget).text(_gettext('Updating...'));
600 $(e.currentTarget).text(_gettext('Updating...'));
586 $(e.currentTarget).attr('disabled', 'disabled');
601 $(e.currentTarget).attr('disabled', 'disabled');
587 if(isDisabled){
602 if(isDisabled){
588 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
603 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
589 }
604 }
590
605
591 });
606 });
592 // fixing issue with caches on firefox
607 // fixing issue with caches on firefox
593 $('#update_commits').removeAttr("disabled");
608 $('#update_commits').removeAttr("disabled");
594
609
595 $('#close_pull_request').on('click', function(e){
610 $('#close_pull_request').on('click', function(e){
596 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
611 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
597 });
612 });
598
613
599 $('.show-inline-comments').on('click', function(e){
614 $('.show-inline-comments').on('click', function(e){
600 var boxid = $(this).attr('data-comment-id');
615 var boxid = $(this).attr('data-comment-id');
601 var button = $(this);
616 var button = $(this);
602
617
603 if(button.hasClass("comments-visible")) {
618 if(button.hasClass("comments-visible")) {
604 $('#{0} .inline-comments'.format(boxid)).each(function(index){
619 $('#{0} .inline-comments'.format(boxid)).each(function(index){
605 $(this).hide();
620 $(this).hide();
606 })
621 });
607 button.removeClass("comments-visible");
622 button.removeClass("comments-visible");
608 } else {
623 } else {
609 $('#{0} .inline-comments'.format(boxid)).each(function(index){
624 $('#{0} .inline-comments'.format(boxid)).each(function(index){
610 $(this).show();
625 $(this).show();
611 })
626 });
612 button.addClass("comments-visible");
627 button.addClass("comments-visible");
613 }
628 }
614 });
629 });
615 })
630 })
616 </script>
631 </script>
617
632
618 </div>
633 </div>
619 </div>
634 </div>
620
635
621 </%def>
636 </%def>
@@ -1,42 +1,42 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22
22
23 from rhodecode.controllers import pullrequests
23 from rhodecode.controllers import pullrequests
24 from rhodecode.lib.vcs.backends.base import (
24 from rhodecode.lib.vcs.backends.base import (
25 MergeFailureReason, MergeResponse)
25 MergeFailureReason, MergeResponse)
26 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.model.pull_request import PullRequestModel
27 from rhodecode.tests import assert_session_flash
27 from rhodecode.tests import assert_session_flash
28
28
29
29
30 def test_merge_pull_request_renders_failure_reason(user_regular):
30 def test_merge_pull_request_renders_failure_reason(app, user_regular):
31 pull_request = mock.Mock()
31 pull_request = mock.Mock()
32 controller = pullrequests.PullrequestsController()
32 controller = pullrequests.PullrequestsController()
33 model_patcher = mock.patch.multiple(
33 model_patcher = mock.patch.multiple(
34 PullRequestModel,
34 PullRequestModel,
35 merge=mock.Mock(return_value=MergeResponse(
35 merge=mock.Mock(return_value=MergeResponse(
36 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
36 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
37 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
37 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
38 with model_patcher:
38 with model_patcher:
39 controller._merge_pull_request(pull_request, user_regular, extras={})
39 controller._merge_pull_request(pull_request, user_regular, extras={})
40
40
41 assert_session_flash(msg=PullRequestModel.MERGE_STATUS_MESSAGES[
41 assert_session_flash(msg=PullRequestModel.MERGE_STATUS_MESSAGES[
42 MergeFailureReason.PUSH_FAILED])
42 MergeFailureReason.PUSH_FAILED])
@@ -1,1001 +1,1064 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 from webob.exc import HTTPNotFound
23 from webob.exc import HTTPNotFound
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.vcs.nodes import FileNode
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)
29 PullRequest, ChangesetStatus, UserLog, Notification)
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.model.repo import RepoModel
33 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
34 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
34 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
35
36
36
37
37 @pytest.mark.usefixtures('app', 'autologin_user')
38 @pytest.mark.usefixtures('app', 'autologin_user')
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.backends("git", "hg")
39 class TestPullrequestsController:
40 class TestPullrequestsController:
40
41
41 def test_index(self, backend):
42 def test_index(self, backend):
42 self.app.get(url(
43 self.app.get(url(
43 controller='pullrequests', action='index',
44 controller='pullrequests', action='index',
44 repo_name=backend.repo_name))
45 repo_name=backend.repo_name))
45
46
46 def test_option_menu_create_pull_request_exists(self, backend):
47 def test_option_menu_create_pull_request_exists(self, backend):
47 repo_name = backend.repo_name
48 repo_name = backend.repo_name
48 response = self.app.get(url('summary_home', repo_name=repo_name))
49 response = self.app.get(url('summary_home', repo_name=repo_name))
49
50
50 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
51 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
51 'pullrequest', repo_name=repo_name)
52 'pullrequest', repo_name=repo_name)
52 response.mustcontain(create_pr_link)
53 response.mustcontain(create_pr_link)
53
54
54 def test_global_redirect_of_pr(self, backend, pr_util):
55 def test_global_redirect_of_pr(self, backend, pr_util):
55 pull_request = pr_util.create_pull_request()
56 pull_request = pr_util.create_pull_request()
56
57
57 response = self.app.get(
58 response = self.app.get(
58 url('pull_requests_global',
59 url('pull_requests_global',
59 pull_request_id=pull_request.pull_request_id))
60 pull_request_id=pull_request.pull_request_id))
60
61
61 repo_name = pull_request.target_repo.repo_name
62 repo_name = pull_request.target_repo.repo_name
62 redirect_url = url('pullrequest_show', repo_name=repo_name,
63 redirect_url = url('pullrequest_show', repo_name=repo_name,
63 pull_request_id=pull_request.pull_request_id)
64 pull_request_id=pull_request.pull_request_id)
64 assert response.status == '302 Found'
65 assert response.status == '302 Found'
65 assert redirect_url in response.location
66 assert redirect_url in response.location
66
67
67 def test_create_pr_form_with_raw_commit_id(self, backend):
68 def test_create_pr_form_with_raw_commit_id(self, backend):
68 repo = backend.repo
69 repo = backend.repo
69
70
70 self.app.get(
71 self.app.get(
71 url(controller='pullrequests', action='index',
72 url(controller='pullrequests', action='index',
72 repo_name=repo.repo_name,
73 repo_name=repo.repo_name,
73 commit=repo.get_commit().raw_id),
74 commit=repo.get_commit().raw_id),
74 status=200)
75 status=200)
75
76
76 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
77 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
77 def test_show(self, pr_util, pr_merge_enabled):
78 def test_show(self, pr_util, pr_merge_enabled):
78 pull_request = pr_util.create_pull_request(
79 pull_request = pr_util.create_pull_request(
79 mergeable=pr_merge_enabled, enable_notifications=False)
80 mergeable=pr_merge_enabled, enable_notifications=False)
80
81
81 response = self.app.get(url(
82 response = self.app.get(url(
82 controller='pullrequests', action='show',
83 controller='pullrequests', action='show',
83 repo_name=pull_request.target_repo.scm_instance().name,
84 repo_name=pull_request.target_repo.scm_instance().name,
84 pull_request_id=str(pull_request.pull_request_id)))
85 pull_request_id=str(pull_request.pull_request_id)))
85
86
86 for commit_id in pull_request.revisions:
87 for commit_id in pull_request.revisions:
87 response.mustcontain(commit_id)
88 response.mustcontain(commit_id)
88
89
89 assert pull_request.target_ref_parts.type in response
90 assert pull_request.target_ref_parts.type in response
90 assert pull_request.target_ref_parts.name in response
91 assert pull_request.target_ref_parts.name in response
91 target_clone_url = pull_request.target_repo.clone_url()
92 target_clone_url = pull_request.target_repo.clone_url()
92 assert target_clone_url in response
93 assert target_clone_url in response
93
94
94 assert 'class="pull-request-merge"' in response
95 assert 'class="pull-request-merge"' in response
95 assert (
96 assert (
96 'Server-side pull request merging is disabled.'
97 'Server-side pull request merging is disabled.'
97 in response) != pr_merge_enabled
98 in response) != pr_merge_enabled
98
99
99 def test_close_status_visibility(self, pr_util, csrf_token):
100 def test_close_status_visibility(self, pr_util, csrf_token):
100 from rhodecode.tests.functional.test_login import login_url, logut_url
101 from rhodecode.tests.functional.test_login import login_url, logut_url
101 # Logout
102 # Logout
102 response = self.app.post(
103 response = self.app.post(
103 logut_url,
104 logut_url,
104 params={'csrf_token': csrf_token})
105 params={'csrf_token': csrf_token})
105 # Login as regular user
106 # Login as regular user
106 response = self.app.post(login_url,
107 response = self.app.post(login_url,
107 {'username': 'test_regular',
108 {'username': 'test_regular',
108 'password': 'test12'})
109 'password': 'test12'})
109
110
110 pull_request = pr_util.create_pull_request(author='test_regular')
111 pull_request = pr_util.create_pull_request(author='test_regular')
111
112
112 response = self.app.get(url(
113 response = self.app.get(url(
113 controller='pullrequests', action='show',
114 controller='pullrequests', action='show',
114 repo_name=pull_request.target_repo.scm_instance().name,
115 repo_name=pull_request.target_repo.scm_instance().name,
115 pull_request_id=str(pull_request.pull_request_id)))
116 pull_request_id=str(pull_request.pull_request_id)))
116
117
117 assert 'Server-side pull request merging is disabled.' in response
118 assert 'Server-side pull request merging is disabled.' in response
118 assert 'value="forced_closed"' in response
119 assert 'value="forced_closed"' in response
119
120
120 def test_show_invalid_commit_id(self, pr_util):
121 def test_show_invalid_commit_id(self, pr_util):
121 # Simulating invalid revisions which will cause a lookup error
122 # Simulating invalid revisions which will cause a lookup error
122 pull_request = pr_util.create_pull_request()
123 pull_request = pr_util.create_pull_request()
123 pull_request.revisions = ['invalid']
124 pull_request.revisions = ['invalid']
124 Session().add(pull_request)
125 Session().add(pull_request)
125 Session().commit()
126 Session().commit()
126
127
127 response = self.app.get(url(
128 response = self.app.get(url(
128 controller='pullrequests', action='show',
129 controller='pullrequests', action='show',
129 repo_name=pull_request.target_repo.scm_instance().name,
130 repo_name=pull_request.target_repo.scm_instance().name,
130 pull_request_id=str(pull_request.pull_request_id)))
131 pull_request_id=str(pull_request.pull_request_id)))
131
132
132 for commit_id in pull_request.revisions:
133 for commit_id in pull_request.revisions:
133 response.mustcontain(commit_id)
134 response.mustcontain(commit_id)
134
135
135 def test_show_invalid_source_reference(self, pr_util):
136 def test_show_invalid_source_reference(self, pr_util):
136 pull_request = pr_util.create_pull_request()
137 pull_request = pr_util.create_pull_request()
137 pull_request.source_ref = 'branch:b:invalid'
138 pull_request.source_ref = 'branch:b:invalid'
138 Session().add(pull_request)
139 Session().add(pull_request)
139 Session().commit()
140 Session().commit()
140
141
141 self.app.get(url(
142 self.app.get(url(
142 controller='pullrequests', action='show',
143 controller='pullrequests', action='show',
143 repo_name=pull_request.target_repo.scm_instance().name,
144 repo_name=pull_request.target_repo.scm_instance().name,
144 pull_request_id=str(pull_request.pull_request_id)))
145 pull_request_id=str(pull_request.pull_request_id)))
145
146
146 def test_edit_title_description(self, pr_util, csrf_token):
147 def test_edit_title_description(self, pr_util, csrf_token):
147 pull_request = pr_util.create_pull_request()
148 pull_request = pr_util.create_pull_request()
148 pull_request_id = pull_request.pull_request_id
149 pull_request_id = pull_request.pull_request_id
149
150
150 response = self.app.post(
151 response = self.app.post(
151 url(controller='pullrequests', action='update',
152 url(controller='pullrequests', action='update',
152 repo_name=pull_request.target_repo.repo_name,
153 repo_name=pull_request.target_repo.repo_name,
153 pull_request_id=str(pull_request_id)),
154 pull_request_id=str(pull_request_id)),
154 params={
155 params={
155 'edit_pull_request': 'true',
156 'edit_pull_request': 'true',
156 '_method': 'put',
157 '_method': 'put',
157 'title': 'New title',
158 'title': 'New title',
158 'description': 'New description',
159 'description': 'New description',
159 'csrf_token': csrf_token})
160 'csrf_token': csrf_token})
160
161
161 assert_session_flash(
162 assert_session_flash(
162 response, u'Pull request title & description updated.',
163 response, u'Pull request title & description updated.',
163 category='success')
164 category='success')
164
165
165 pull_request = PullRequest.get(pull_request_id)
166 pull_request = PullRequest.get(pull_request_id)
166 assert pull_request.title == 'New title'
167 assert pull_request.title == 'New title'
167 assert pull_request.description == 'New description'
168 assert pull_request.description == 'New description'
168
169
169 def test_edit_title_description_closed(self, pr_util, csrf_token):
170 def test_edit_title_description_closed(self, pr_util, csrf_token):
170 pull_request = pr_util.create_pull_request()
171 pull_request = pr_util.create_pull_request()
171 pull_request_id = pull_request.pull_request_id
172 pull_request_id = pull_request.pull_request_id
172 pr_util.close()
173 pr_util.close()
173
174
174 response = self.app.post(
175 response = self.app.post(
175 url(controller='pullrequests', action='update',
176 url(controller='pullrequests', action='update',
176 repo_name=pull_request.target_repo.repo_name,
177 repo_name=pull_request.target_repo.repo_name,
177 pull_request_id=str(pull_request_id)),
178 pull_request_id=str(pull_request_id)),
178 params={
179 params={
179 'edit_pull_request': 'true',
180 'edit_pull_request': 'true',
180 '_method': 'put',
181 '_method': 'put',
181 'title': 'New title',
182 'title': 'New title',
182 'description': 'New description',
183 'description': 'New description',
183 'csrf_token': csrf_token})
184 'csrf_token': csrf_token})
184
185
185 assert_session_flash(
186 assert_session_flash(
186 response, u'Cannot update closed pull requests.',
187 response, u'Cannot update closed pull requests.',
187 category='error')
188 category='error')
188
189
189 def test_update_invalid_source_reference(self, pr_util, csrf_token):
190 def test_update_invalid_source_reference(self, pr_util, csrf_token):
190 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
191 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
191
192
192 pull_request = pr_util.create_pull_request()
193 pull_request = pr_util.create_pull_request()
193 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
194 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
194 Session().add(pull_request)
195 Session().add(pull_request)
195 Session().commit()
196 Session().commit()
196
197
197 pull_request_id = pull_request.pull_request_id
198 pull_request_id = pull_request.pull_request_id
198
199
199 response = self.app.post(
200 response = self.app.post(
200 url(controller='pullrequests', action='update',
201 url(controller='pullrequests', action='update',
201 repo_name=pull_request.target_repo.repo_name,
202 repo_name=pull_request.target_repo.repo_name,
202 pull_request_id=str(pull_request_id)),
203 pull_request_id=str(pull_request_id)),
203 params={'update_commits': 'true', '_method': 'put',
204 params={'update_commits': 'true', '_method': 'put',
204 'csrf_token': csrf_token})
205 'csrf_token': csrf_token})
205
206
206 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
207 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
207 UpdateFailureReason.MISSING_SOURCE_REF]
208 UpdateFailureReason.MISSING_SOURCE_REF]
208 assert_session_flash(response, expected_msg, category='error')
209 assert_session_flash(response, expected_msg, category='error')
209
210
210 def test_missing_target_reference(self, pr_util, csrf_token):
211 def test_missing_target_reference(self, pr_util, csrf_token):
211 from rhodecode.lib.vcs.backends.base import MergeFailureReason
212 from rhodecode.lib.vcs.backends.base import MergeFailureReason
212 pull_request = pr_util.create_pull_request(
213 pull_request = pr_util.create_pull_request(
213 approved=True, mergeable=True)
214 approved=True, mergeable=True)
214 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
215 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
215 Session().add(pull_request)
216 Session().add(pull_request)
216 Session().commit()
217 Session().commit()
217
218
218 pull_request_id = pull_request.pull_request_id
219 pull_request_id = pull_request.pull_request_id
219 pull_request_url = url(
220 pull_request_url = url(
220 controller='pullrequests', action='show',
221 controller='pullrequests', action='show',
221 repo_name=pull_request.target_repo.repo_name,
222 repo_name=pull_request.target_repo.repo_name,
222 pull_request_id=str(pull_request_id))
223 pull_request_id=str(pull_request_id))
223
224
224 response = self.app.get(pull_request_url)
225 response = self.app.get(pull_request_url)
225
226
226 assertr = AssertResponse(response)
227 assertr = AssertResponse(response)
227 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
228 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
228 MergeFailureReason.MISSING_TARGET_REF]
229 MergeFailureReason.MISSING_TARGET_REF]
229 assertr.element_contains(
230 assertr.element_contains(
230 'span[data-role="merge-message"]', str(expected_msg))
231 'span[data-role="merge-message"]', str(expected_msg))
231
232
232 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
233 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
233 pull_request = pr_util.create_pull_request(approved=True)
234 pull_request = pr_util.create_pull_request(approved=True)
234 pull_request_id = pull_request.pull_request_id
235 pull_request_id = pull_request.pull_request_id
235 author = pull_request.user_id
236 author = pull_request.user_id
236 repo = pull_request.target_repo.repo_id
237 repo = pull_request.target_repo.repo_id
237
238
238 self.app.post(
239 self.app.post(
239 url(controller='pullrequests',
240 url(controller='pullrequests',
240 action='comment',
241 action='comment',
241 repo_name=pull_request.target_repo.scm_instance().name,
242 repo_name=pull_request.target_repo.scm_instance().name,
242 pull_request_id=str(pull_request_id)),
243 pull_request_id=str(pull_request_id)),
243 params={
244 params={
244 'changeset_status':
245 'changeset_status':
245 ChangesetStatus.STATUS_APPROVED + '_closed',
246 ChangesetStatus.STATUS_APPROVED + '_closed',
246 'change_changeset_status': 'on',
247 'change_changeset_status': 'on',
247 'text': '',
248 'text': '',
248 'csrf_token': csrf_token},
249 'csrf_token': csrf_token},
249 status=302)
250 status=302)
250
251
251 action = 'user_closed_pull_request:%d' % pull_request_id
252 action = 'user_closed_pull_request:%d' % pull_request_id
252 journal = UserLog.query()\
253 journal = UserLog.query()\
253 .filter(UserLog.user_id == author)\
254 .filter(UserLog.user_id == author)\
254 .filter(UserLog.repository_id == repo)\
255 .filter(UserLog.repository_id == repo)\
255 .filter(UserLog.action == action)\
256 .filter(UserLog.action == action)\
256 .all()
257 .all()
257 assert len(journal) == 1
258 assert len(journal) == 1
258
259
259 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
260 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
260 pull_request = pr_util.create_pull_request()
261 pull_request = pr_util.create_pull_request()
261 pull_request_id = pull_request.pull_request_id
262 pull_request_id = pull_request.pull_request_id
262 response = self.app.post(
263 response = self.app.post(
263 url(controller='pullrequests',
264 url(controller='pullrequests',
264 action='update',
265 action='update',
265 repo_name=pull_request.target_repo.scm_instance().name,
266 repo_name=pull_request.target_repo.scm_instance().name,
266 pull_request_id=str(pull_request.pull_request_id)),
267 pull_request_id=str(pull_request.pull_request_id)),
267 params={'close_pull_request': 'true', '_method': 'put',
268 params={'close_pull_request': 'true', '_method': 'put',
268 'csrf_token': csrf_token})
269 'csrf_token': csrf_token})
269
270
270 pull_request = PullRequest.get(pull_request_id)
271 pull_request = PullRequest.get(pull_request_id)
271
272
272 assert response.json is True
273 assert response.json is True
273 assert pull_request.is_closed()
274 assert pull_request.is_closed()
274
275
275 # check only the latest status, not the review status
276 # check only the latest status, not the review status
276 status = ChangesetStatusModel().get_status(
277 status = ChangesetStatusModel().get_status(
277 pull_request.source_repo, pull_request=pull_request)
278 pull_request.source_repo, pull_request=pull_request)
278 assert status == ChangesetStatus.STATUS_REJECTED
279 assert status == ChangesetStatus.STATUS_REJECTED
279
280
280 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
281 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
281 pull_request = pr_util.create_pull_request()
282 pull_request = pr_util.create_pull_request()
282 pull_request_id = pull_request.pull_request_id
283 pull_request_id = pull_request.pull_request_id
283 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
284 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
284 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
285 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
285 author = pull_request.user_id
286 author = pull_request.user_id
286 repo = pull_request.target_repo.repo_id
287 repo = pull_request.target_repo.repo_id
287 self.app.post(
288 self.app.post(
288 url(controller='pullrequests',
289 url(controller='pullrequests',
289 action='comment',
290 action='comment',
290 repo_name=pull_request.target_repo.scm_instance().name,
291 repo_name=pull_request.target_repo.scm_instance().name,
291 pull_request_id=str(pull_request_id)),
292 pull_request_id=str(pull_request_id)),
292 params={
293 params={
293 'changeset_status': 'forced_closed',
294 'changeset_status': 'forced_closed',
294 'csrf_token': csrf_token},
295 'csrf_token': csrf_token},
295 status=302)
296 status=302)
296
297
297 pull_request = PullRequest.get(pull_request_id)
298 pull_request = PullRequest.get(pull_request_id)
298
299
299 action = 'user_closed_pull_request:%d' % pull_request_id
300 action = 'user_closed_pull_request:%d' % pull_request_id
300 journal = UserLog.query().filter(
301 journal = UserLog.query().filter(
301 UserLog.user_id == author,
302 UserLog.user_id == author,
302 UserLog.repository_id == repo,
303 UserLog.repository_id == repo,
303 UserLog.action == action).all()
304 UserLog.action == action).all()
304 assert len(journal) == 1
305 assert len(journal) == 1
305
306
306 # check only the latest status, not the review status
307 # check only the latest status, not the review status
307 status = ChangesetStatusModel().get_status(
308 status = ChangesetStatusModel().get_status(
308 pull_request.source_repo, pull_request=pull_request)
309 pull_request.source_repo, pull_request=pull_request)
309 assert status == ChangesetStatus.STATUS_REJECTED
310 assert status == ChangesetStatus.STATUS_REJECTED
310
311
311 def test_create_pull_request(self, backend, csrf_token):
312 def test_create_pull_request(self, backend, csrf_token):
312 commits = [
313 commits = [
313 {'message': 'ancestor'},
314 {'message': 'ancestor'},
314 {'message': 'change'},
315 {'message': 'change'},
315 {'message': 'change2'},
316 {'message': 'change2'},
316 ]
317 ]
317 commit_ids = backend.create_master_repo(commits)
318 commit_ids = backend.create_master_repo(commits)
318 target = backend.create_repo(heads=['ancestor'])
319 target = backend.create_repo(heads=['ancestor'])
319 source = backend.create_repo(heads=['change2'])
320 source = backend.create_repo(heads=['change2'])
320
321
321 response = self.app.post(
322 response = self.app.post(
322 url(
323 url(
323 controller='pullrequests',
324 controller='pullrequests',
324 action='create',
325 action='create',
325 repo_name=source.repo_name
326 repo_name=source.repo_name
326 ),
327 ),
327 [
328 [
328 ('source_repo', source.repo_name),
329 ('source_repo', source.repo_name),
329 ('source_ref', 'branch:default:' + commit_ids['change2']),
330 ('source_ref', 'branch:default:' + commit_ids['change2']),
330 ('target_repo', target.repo_name),
331 ('target_repo', target.repo_name),
331 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
332 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
332 ('pullrequest_desc', 'Description'),
333 ('pullrequest_desc', 'Description'),
333 ('pullrequest_title', 'Title'),
334 ('pullrequest_title', 'Title'),
334 ('__start__', 'review_members:sequence'),
335 ('__start__', 'review_members:sequence'),
335 ('__start__', 'reviewer:mapping'),
336 ('__start__', 'reviewer:mapping'),
336 ('user_id', '1'),
337 ('user_id', '1'),
337 ('__start__', 'reasons:sequence'),
338 ('__start__', 'reasons:sequence'),
338 ('reason', 'Some reason'),
339 ('reason', 'Some reason'),
339 ('__end__', 'reasons:sequence'),
340 ('__end__', 'reasons:sequence'),
340 ('__end__', 'reviewer:mapping'),
341 ('__end__', 'reviewer:mapping'),
341 ('__end__', 'review_members:sequence'),
342 ('__end__', 'review_members:sequence'),
342 ('__start__', 'revisions:sequence'),
343 ('__start__', 'revisions:sequence'),
343 ('revisions', commit_ids['change']),
344 ('revisions', commit_ids['change']),
344 ('revisions', commit_ids['change2']),
345 ('revisions', commit_ids['change2']),
345 ('__end__', 'revisions:sequence'),
346 ('__end__', 'revisions:sequence'),
346 ('user', ''),
347 ('user', ''),
347 ('csrf_token', csrf_token),
348 ('csrf_token', csrf_token),
348 ],
349 ],
349 status=302)
350 status=302)
350
351
351 location = response.headers['Location']
352 location = response.headers['Location']
352 pull_request_id = int(location.rsplit('/', 1)[1])
353 pull_request_id = int(location.rsplit('/', 1)[1])
353 pull_request = PullRequest.get(pull_request_id)
354 pull_request = PullRequest.get(pull_request_id)
354
355
355 # check that we have now both revisions
356 # check that we have now both revisions
356 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
357 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
357 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
358 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
358 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
359 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
359 assert pull_request.target_ref == expected_target_ref
360 assert pull_request.target_ref == expected_target_ref
360
361
361 def test_reviewer_notifications(self, backend, csrf_token):
362 def test_reviewer_notifications(self, backend, csrf_token):
362 # We have to use the app.post for this test so it will create the
363 # We have to use the app.post for this test so it will create the
363 # notifications properly with the new PR
364 # notifications properly with the new PR
364 commits = [
365 commits = [
365 {'message': 'ancestor',
366 {'message': 'ancestor',
366 'added': [FileNode('file_A', content='content_of_ancestor')]},
367 'added': [FileNode('file_A', content='content_of_ancestor')]},
367 {'message': 'change',
368 {'message': 'change',
368 'added': [FileNode('file_a', content='content_of_change')]},
369 'added': [FileNode('file_a', content='content_of_change')]},
369 {'message': 'change-child'},
370 {'message': 'change-child'},
370 {'message': 'ancestor-child', 'parents': ['ancestor'],
371 {'message': 'ancestor-child', 'parents': ['ancestor'],
371 'added': [
372 'added': [
372 FileNode('file_B', content='content_of_ancestor_child')]},
373 FileNode('file_B', content='content_of_ancestor_child')]},
373 {'message': 'ancestor-child-2'},
374 {'message': 'ancestor-child-2'},
374 ]
375 ]
375 commit_ids = backend.create_master_repo(commits)
376 commit_ids = backend.create_master_repo(commits)
376 target = backend.create_repo(heads=['ancestor-child'])
377 target = backend.create_repo(heads=['ancestor-child'])
377 source = backend.create_repo(heads=['change'])
378 source = backend.create_repo(heads=['change'])
378
379
379 response = self.app.post(
380 response = self.app.post(
380 url(
381 url(
381 controller='pullrequests',
382 controller='pullrequests',
382 action='create',
383 action='create',
383 repo_name=source.repo_name
384 repo_name=source.repo_name
384 ),
385 ),
385 [
386 [
386 ('source_repo', source.repo_name),
387 ('source_repo', source.repo_name),
387 ('source_ref', 'branch:default:' + commit_ids['change']),
388 ('source_ref', 'branch:default:' + commit_ids['change']),
388 ('target_repo', target.repo_name),
389 ('target_repo', target.repo_name),
389 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
390 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
390 ('pullrequest_desc', 'Description'),
391 ('pullrequest_desc', 'Description'),
391 ('pullrequest_title', 'Title'),
392 ('pullrequest_title', 'Title'),
392 ('__start__', 'review_members:sequence'),
393 ('__start__', 'review_members:sequence'),
393 ('__start__', 'reviewer:mapping'),
394 ('__start__', 'reviewer:mapping'),
394 ('user_id', '2'),
395 ('user_id', '2'),
395 ('__start__', 'reasons:sequence'),
396 ('__start__', 'reasons:sequence'),
396 ('reason', 'Some reason'),
397 ('reason', 'Some reason'),
397 ('__end__', 'reasons:sequence'),
398 ('__end__', 'reasons:sequence'),
398 ('__end__', 'reviewer:mapping'),
399 ('__end__', 'reviewer:mapping'),
399 ('__end__', 'review_members:sequence'),
400 ('__end__', 'review_members:sequence'),
400 ('__start__', 'revisions:sequence'),
401 ('__start__', 'revisions:sequence'),
401 ('revisions', commit_ids['change']),
402 ('revisions', commit_ids['change']),
402 ('__end__', 'revisions:sequence'),
403 ('__end__', 'revisions:sequence'),
403 ('user', ''),
404 ('user', ''),
404 ('csrf_token', csrf_token),
405 ('csrf_token', csrf_token),
405 ],
406 ],
406 status=302)
407 status=302)
407
408
408 location = response.headers['Location']
409 location = response.headers['Location']
409 pull_request_id = int(location.rsplit('/', 1)[1])
410 pull_request_id = int(location.rsplit('/', 1)[1])
410 pull_request = PullRequest.get(pull_request_id)
411 pull_request = PullRequest.get(pull_request_id)
411
412
412 # Check that a notification was made
413 # Check that a notification was made
413 notifications = Notification.query()\
414 notifications = Notification.query()\
414 .filter(Notification.created_by == pull_request.author.user_id,
415 .filter(Notification.created_by == pull_request.author.user_id,
415 Notification.type_ == Notification.TYPE_PULL_REQUEST,
416 Notification.type_ == Notification.TYPE_PULL_REQUEST,
416 Notification.subject.contains("wants you to review "
417 Notification.subject.contains("wants you to review "
417 "pull request #%d"
418 "pull request #%d"
418 % pull_request_id))
419 % pull_request_id))
419 assert len(notifications.all()) == 1
420 assert len(notifications.all()) == 1
420
421
421 # Change reviewers and check that a notification was made
422 # Change reviewers and check that a notification was made
422 PullRequestModel().update_reviewers(
423 PullRequestModel().update_reviewers(
423 pull_request.pull_request_id, [(1, [])])
424 pull_request.pull_request_id, [(1, [])])
424 assert len(notifications.all()) == 2
425 assert len(notifications.all()) == 2
425
426
426 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
427 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
427 csrf_token):
428 csrf_token):
428 commits = [
429 commits = [
429 {'message': 'ancestor',
430 {'message': 'ancestor',
430 'added': [FileNode('file_A', content='content_of_ancestor')]},
431 'added': [FileNode('file_A', content='content_of_ancestor')]},
431 {'message': 'change',
432 {'message': 'change',
432 'added': [FileNode('file_a', content='content_of_change')]},
433 'added': [FileNode('file_a', content='content_of_change')]},
433 {'message': 'change-child'},
434 {'message': 'change-child'},
434 {'message': 'ancestor-child', 'parents': ['ancestor'],
435 {'message': 'ancestor-child', 'parents': ['ancestor'],
435 'added': [
436 'added': [
436 FileNode('file_B', content='content_of_ancestor_child')]},
437 FileNode('file_B', content='content_of_ancestor_child')]},
437 {'message': 'ancestor-child-2'},
438 {'message': 'ancestor-child-2'},
438 ]
439 ]
439 commit_ids = backend.create_master_repo(commits)
440 commit_ids = backend.create_master_repo(commits)
440 target = backend.create_repo(heads=['ancestor-child'])
441 target = backend.create_repo(heads=['ancestor-child'])
441 source = backend.create_repo(heads=['change'])
442 source = backend.create_repo(heads=['change'])
442
443
443 response = self.app.post(
444 response = self.app.post(
444 url(
445 url(
445 controller='pullrequests',
446 controller='pullrequests',
446 action='create',
447 action='create',
447 repo_name=source.repo_name
448 repo_name=source.repo_name
448 ),
449 ),
449 [
450 [
450 ('source_repo', source.repo_name),
451 ('source_repo', source.repo_name),
451 ('source_ref', 'branch:default:' + commit_ids['change']),
452 ('source_ref', 'branch:default:' + commit_ids['change']),
452 ('target_repo', target.repo_name),
453 ('target_repo', target.repo_name),
453 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
454 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
454 ('pullrequest_desc', 'Description'),
455 ('pullrequest_desc', 'Description'),
455 ('pullrequest_title', 'Title'),
456 ('pullrequest_title', 'Title'),
456 ('__start__', 'review_members:sequence'),
457 ('__start__', 'review_members:sequence'),
457 ('__start__', 'reviewer:mapping'),
458 ('__start__', 'reviewer:mapping'),
458 ('user_id', '1'),
459 ('user_id', '1'),
459 ('__start__', 'reasons:sequence'),
460 ('__start__', 'reasons:sequence'),
460 ('reason', 'Some reason'),
461 ('reason', 'Some reason'),
461 ('__end__', 'reasons:sequence'),
462 ('__end__', 'reasons:sequence'),
462 ('__end__', 'reviewer:mapping'),
463 ('__end__', 'reviewer:mapping'),
463 ('__end__', 'review_members:sequence'),
464 ('__end__', 'review_members:sequence'),
464 ('__start__', 'revisions:sequence'),
465 ('__start__', 'revisions:sequence'),
465 ('revisions', commit_ids['change']),
466 ('revisions', commit_ids['change']),
466 ('__end__', 'revisions:sequence'),
467 ('__end__', 'revisions:sequence'),
467 ('user', ''),
468 ('user', ''),
468 ('csrf_token', csrf_token),
469 ('csrf_token', csrf_token),
469 ],
470 ],
470 status=302)
471 status=302)
471
472
472 location = response.headers['Location']
473 location = response.headers['Location']
473 pull_request_id = int(location.rsplit('/', 1)[1])
474 pull_request_id = int(location.rsplit('/', 1)[1])
474 pull_request = PullRequest.get(pull_request_id)
475 pull_request = PullRequest.get(pull_request_id)
475
476
476 # target_ref has to point to the ancestor's commit_id in order to
477 # target_ref has to point to the ancestor's commit_id in order to
477 # show the correct diff
478 # show the correct diff
478 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
479 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
479 assert pull_request.target_ref == expected_target_ref
480 assert pull_request.target_ref == expected_target_ref
480
481
481 # Check generated diff contents
482 # Check generated diff contents
482 response = response.follow()
483 response = response.follow()
483 assert 'content_of_ancestor' not in response.body
484 assert 'content_of_ancestor' not in response.body
484 assert 'content_of_ancestor-child' not in response.body
485 assert 'content_of_ancestor-child' not in response.body
485 assert 'content_of_change' in response.body
486 assert 'content_of_change' in response.body
486
487
487 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
488 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
488 # Clear any previous calls to rcextensions
489 # Clear any previous calls to rcextensions
489 rhodecode.EXTENSIONS.calls.clear()
490 rhodecode.EXTENSIONS.calls.clear()
490
491
491 pull_request = pr_util.create_pull_request(
492 pull_request = pr_util.create_pull_request(
492 approved=True, mergeable=True)
493 approved=True, mergeable=True)
493 pull_request_id = pull_request.pull_request_id
494 pull_request_id = pull_request.pull_request_id
494 repo_name = pull_request.target_repo.scm_instance().name,
495 repo_name = pull_request.target_repo.scm_instance().name,
495
496
496 response = self.app.post(
497 response = self.app.post(
497 url(controller='pullrequests',
498 url(controller='pullrequests',
498 action='merge',
499 action='merge',
499 repo_name=str(repo_name[0]),
500 repo_name=str(repo_name[0]),
500 pull_request_id=str(pull_request_id)),
501 pull_request_id=str(pull_request_id)),
501 params={'csrf_token': csrf_token}).follow()
502 params={'csrf_token': csrf_token}).follow()
502
503
503 pull_request = PullRequest.get(pull_request_id)
504 pull_request = PullRequest.get(pull_request_id)
504
505
505 assert response.status_int == 200
506 assert response.status_int == 200
506 assert pull_request.is_closed()
507 assert pull_request.is_closed()
507 assert_pull_request_status(
508 assert_pull_request_status(
508 pull_request, ChangesetStatus.STATUS_APPROVED)
509 pull_request, ChangesetStatus.STATUS_APPROVED)
509
510
510 # Check the relevant log entries were added
511 # Check the relevant log entries were added
511 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
512 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
512 actions = [log.action for log in user_logs]
513 actions = [log.action for log in user_logs]
513 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
514 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
514 expected_actions = [
515 expected_actions = [
515 u'user_closed_pull_request:%d' % pull_request_id,
516 u'user_closed_pull_request:%d' % pull_request_id,
516 u'user_merged_pull_request:%d' % pull_request_id,
517 u'user_merged_pull_request:%d' % pull_request_id,
517 # The action below reflect that the post push actions were executed
518 # The action below reflect that the post push actions were executed
518 u'user_commented_pull_request:%d' % pull_request_id,
519 u'user_commented_pull_request:%d' % pull_request_id,
519 u'push:%s' % ','.join(pr_commit_ids),
520 u'push:%s' % ','.join(pr_commit_ids),
520 ]
521 ]
521 assert actions == expected_actions
522 assert actions == expected_actions
522
523
523 # Check post_push rcextension was really executed
524 # Check post_push rcextension was really executed
524 push_calls = rhodecode.EXTENSIONS.calls['post_push']
525 push_calls = rhodecode.EXTENSIONS.calls['post_push']
525 assert len(push_calls) == 1
526 assert len(push_calls) == 1
526 unused_last_call_args, last_call_kwargs = push_calls[0]
527 unused_last_call_args, last_call_kwargs = push_calls[0]
527 assert last_call_kwargs['action'] == 'push'
528 assert last_call_kwargs['action'] == 'push'
528 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
529 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
529
530
530 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
531 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
531 pull_request = pr_util.create_pull_request(mergeable=False)
532 pull_request = pr_util.create_pull_request(mergeable=False)
532 pull_request_id = pull_request.pull_request_id
533 pull_request_id = pull_request.pull_request_id
533 pull_request = PullRequest.get(pull_request_id)
534 pull_request = PullRequest.get(pull_request_id)
534
535
535 response = self.app.post(
536 response = self.app.post(
536 url(controller='pullrequests',
537 url(controller='pullrequests',
537 action='merge',
538 action='merge',
538 repo_name=pull_request.target_repo.scm_instance().name,
539 repo_name=pull_request.target_repo.scm_instance().name,
539 pull_request_id=str(pull_request.pull_request_id)),
540 pull_request_id=str(pull_request.pull_request_id)),
540 params={'csrf_token': csrf_token}).follow()
541 params={'csrf_token': csrf_token}).follow()
541
542
542 assert response.status_int == 200
543 assert response.status_int == 200
543 assert 'Server-side pull request merging is disabled.' in response.body
544 assert 'Server-side pull request merging is disabled.' in response.body
544
545
545 @pytest.mark.skip_backends('svn')
546 @pytest.mark.skip_backends('svn')
546 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
547 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
547 pull_request = pr_util.create_pull_request(mergeable=True)
548 pull_request = pr_util.create_pull_request(mergeable=True)
548 pull_request_id = pull_request.pull_request_id
549 pull_request_id = pull_request.pull_request_id
549 repo_name = pull_request.target_repo.scm_instance().name,
550 repo_name = pull_request.target_repo.scm_instance().name,
550
551
551 response = self.app.post(
552 response = self.app.post(
552 url(controller='pullrequests',
553 url(controller='pullrequests',
553 action='merge',
554 action='merge',
554 repo_name=str(repo_name[0]),
555 repo_name=str(repo_name[0]),
555 pull_request_id=str(pull_request_id)),
556 pull_request_id=str(pull_request_id)),
556 params={'csrf_token': csrf_token}).follow()
557 params={'csrf_token': csrf_token}).follow()
557
558
558 pull_request = PullRequest.get(pull_request_id)
559 pull_request = PullRequest.get(pull_request_id)
559
560
560 assert response.status_int == 200
561 assert response.status_int == 200
561 assert ' Reviewer approval is pending.' in response.body
562 assert ' Reviewer approval is pending.' in response.body
562
563
563 def test_update_source_revision(self, backend, csrf_token):
564 def test_update_source_revision(self, backend, csrf_token):
564 commits = [
565 commits = [
565 {'message': 'ancestor'},
566 {'message': 'ancestor'},
566 {'message': 'change'},
567 {'message': 'change'},
567 {'message': 'change-2'},
568 {'message': 'change-2'},
568 ]
569 ]
569 commit_ids = backend.create_master_repo(commits)
570 commit_ids = backend.create_master_repo(commits)
570 target = backend.create_repo(heads=['ancestor'])
571 target = backend.create_repo(heads=['ancestor'])
571 source = backend.create_repo(heads=['change'])
572 source = backend.create_repo(heads=['change'])
572
573
573 # create pr from a in source to A in target
574 # create pr from a in source to A in target
574 pull_request = PullRequest()
575 pull_request = PullRequest()
575 pull_request.source_repo = source
576 pull_request.source_repo = source
576 # TODO: johbo: Make sure that we write the source ref this way!
577 # TODO: johbo: Make sure that we write the source ref this way!
577 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
578 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
578 branch=backend.default_branch_name, commit_id=commit_ids['change'])
579 branch=backend.default_branch_name, commit_id=commit_ids['change'])
579 pull_request.target_repo = target
580 pull_request.target_repo = target
580
581
581 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
582 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
582 branch=backend.default_branch_name,
583 branch=backend.default_branch_name,
583 commit_id=commit_ids['ancestor'])
584 commit_id=commit_ids['ancestor'])
584 pull_request.revisions = [commit_ids['change']]
585 pull_request.revisions = [commit_ids['change']]
585 pull_request.title = u"Test"
586 pull_request.title = u"Test"
586 pull_request.description = u"Description"
587 pull_request.description = u"Description"
587 pull_request.author = UserModel().get_by_username(
588 pull_request.author = UserModel().get_by_username(
588 TEST_USER_ADMIN_LOGIN)
589 TEST_USER_ADMIN_LOGIN)
589 Session().add(pull_request)
590 Session().add(pull_request)
590 Session().commit()
591 Session().commit()
591 pull_request_id = pull_request.pull_request_id
592 pull_request_id = pull_request.pull_request_id
592
593
593 # source has ancestor - change - change-2
594 # source has ancestor - change - change-2
594 backend.pull_heads(source, heads=['change-2'])
595 backend.pull_heads(source, heads=['change-2'])
595
596
596 # update PR
597 # update PR
597 self.app.post(
598 self.app.post(
598 url(controller='pullrequests', action='update',
599 url(controller='pullrequests', action='update',
599 repo_name=target.repo_name,
600 repo_name=target.repo_name,
600 pull_request_id=str(pull_request_id)),
601 pull_request_id=str(pull_request_id)),
601 params={'update_commits': 'true', '_method': 'put',
602 params={'update_commits': 'true', '_method': 'put',
602 'csrf_token': csrf_token})
603 'csrf_token': csrf_token})
603
604
604 # check that we have now both revisions
605 # check that we have now both revisions
605 pull_request = PullRequest.get(pull_request_id)
606 pull_request = PullRequest.get(pull_request_id)
606 assert pull_request.revisions == [
607 assert pull_request.revisions == [
607 commit_ids['change-2'], commit_ids['change']]
608 commit_ids['change-2'], commit_ids['change']]
608
609
609 # TODO: johbo: this should be a test on its own
610 # TODO: johbo: this should be a test on its own
610 response = self.app.get(url(
611 response = self.app.get(url(
611 controller='pullrequests', action='index',
612 controller='pullrequests', action='index',
612 repo_name=target.repo_name))
613 repo_name=target.repo_name))
613 assert response.status_int == 200
614 assert response.status_int == 200
614 assert 'Pull request updated to' in response.body
615 assert 'Pull request updated to' in response.body
615 assert 'with 1 added, 0 removed commits.' in response.body
616 assert 'with 1 added, 0 removed commits.' in response.body
616
617
617 def test_update_target_revision(self, backend, csrf_token):
618 def test_update_target_revision(self, backend, csrf_token):
618 commits = [
619 commits = [
619 {'message': 'ancestor'},
620 {'message': 'ancestor'},
620 {'message': 'change'},
621 {'message': 'change'},
621 {'message': 'ancestor-new', 'parents': ['ancestor']},
622 {'message': 'ancestor-new', 'parents': ['ancestor']},
622 {'message': 'change-rebased'},
623 {'message': 'change-rebased'},
623 ]
624 ]
624 commit_ids = backend.create_master_repo(commits)
625 commit_ids = backend.create_master_repo(commits)
625 target = backend.create_repo(heads=['ancestor'])
626 target = backend.create_repo(heads=['ancestor'])
626 source = backend.create_repo(heads=['change'])
627 source = backend.create_repo(heads=['change'])
627
628
628 # create pr from a in source to A in target
629 # create pr from a in source to A in target
629 pull_request = PullRequest()
630 pull_request = PullRequest()
630 pull_request.source_repo = source
631 pull_request.source_repo = source
631 # TODO: johbo: Make sure that we write the source ref this way!
632 # TODO: johbo: Make sure that we write the source ref this way!
632 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
633 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
633 branch=backend.default_branch_name, commit_id=commit_ids['change'])
634 branch=backend.default_branch_name, commit_id=commit_ids['change'])
634 pull_request.target_repo = target
635 pull_request.target_repo = target
635 # TODO: johbo: Target ref should be branch based, since tip can jump
636 # TODO: johbo: Target ref should be branch based, since tip can jump
636 # from branch to branch
637 # from branch to branch
637 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
638 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
638 branch=backend.default_branch_name,
639 branch=backend.default_branch_name,
639 commit_id=commit_ids['ancestor'])
640 commit_id=commit_ids['ancestor'])
640 pull_request.revisions = [commit_ids['change']]
641 pull_request.revisions = [commit_ids['change']]
641 pull_request.title = u"Test"
642 pull_request.title = u"Test"
642 pull_request.description = u"Description"
643 pull_request.description = u"Description"
643 pull_request.author = UserModel().get_by_username(
644 pull_request.author = UserModel().get_by_username(
644 TEST_USER_ADMIN_LOGIN)
645 TEST_USER_ADMIN_LOGIN)
645 Session().add(pull_request)
646 Session().add(pull_request)
646 Session().commit()
647 Session().commit()
647 pull_request_id = pull_request.pull_request_id
648 pull_request_id = pull_request.pull_request_id
648
649
649 # target has ancestor - ancestor-new
650 # target has ancestor - ancestor-new
650 # source has ancestor - ancestor-new - change-rebased
651 # source has ancestor - ancestor-new - change-rebased
651 backend.pull_heads(target, heads=['ancestor-new'])
652 backend.pull_heads(target, heads=['ancestor-new'])
652 backend.pull_heads(source, heads=['change-rebased'])
653 backend.pull_heads(source, heads=['change-rebased'])
653
654
654 # update PR
655 # update PR
655 self.app.post(
656 self.app.post(
656 url(controller='pullrequests', action='update',
657 url(controller='pullrequests', action='update',
657 repo_name=target.repo_name,
658 repo_name=target.repo_name,
658 pull_request_id=str(pull_request_id)),
659 pull_request_id=str(pull_request_id)),
659 params={'update_commits': 'true', '_method': 'put',
660 params={'update_commits': 'true', '_method': 'put',
660 'csrf_token': csrf_token},
661 'csrf_token': csrf_token},
661 status=200)
662 status=200)
662
663
663 # check that we have now both revisions
664 # check that we have now both revisions
664 pull_request = PullRequest.get(pull_request_id)
665 pull_request = PullRequest.get(pull_request_id)
665 assert pull_request.revisions == [commit_ids['change-rebased']]
666 assert pull_request.revisions == [commit_ids['change-rebased']]
666 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
667 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
667 branch=backend.default_branch_name,
668 branch=backend.default_branch_name,
668 commit_id=commit_ids['ancestor-new'])
669 commit_id=commit_ids['ancestor-new'])
669
670
670 # TODO: johbo: This should be a test on its own
671 # TODO: johbo: This should be a test on its own
671 response = self.app.get(url(
672 response = self.app.get(url(
672 controller='pullrequests', action='index',
673 controller='pullrequests', action='index',
673 repo_name=target.repo_name))
674 repo_name=target.repo_name))
674 assert response.status_int == 200
675 assert response.status_int == 200
675 assert 'Pull request updated to' in response.body
676 assert 'Pull request updated to' in response.body
676 assert 'with 1 added, 1 removed commits.' in response.body
677 assert 'with 1 added, 1 removed commits.' in response.body
677
678
678 def test_update_of_ancestor_reference(self, backend, csrf_token):
679 def test_update_of_ancestor_reference(self, backend, csrf_token):
679 commits = [
680 commits = [
680 {'message': 'ancestor'},
681 {'message': 'ancestor'},
681 {'message': 'change'},
682 {'message': 'change'},
682 {'message': 'change-2'},
683 {'message': 'change-2'},
683 {'message': 'ancestor-new', 'parents': ['ancestor']},
684 {'message': 'ancestor-new', 'parents': ['ancestor']},
684 {'message': 'change-rebased'},
685 {'message': 'change-rebased'},
685 ]
686 ]
686 commit_ids = backend.create_master_repo(commits)
687 commit_ids = backend.create_master_repo(commits)
687 target = backend.create_repo(heads=['ancestor'])
688 target = backend.create_repo(heads=['ancestor'])
688 source = backend.create_repo(heads=['change'])
689 source = backend.create_repo(heads=['change'])
689
690
690 # create pr from a in source to A in target
691 # create pr from a in source to A in target
691 pull_request = PullRequest()
692 pull_request = PullRequest()
692 pull_request.source_repo = source
693 pull_request.source_repo = source
693 # TODO: johbo: Make sure that we write the source ref this way!
694 # TODO: johbo: Make sure that we write the source ref this way!
694 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
695 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
695 branch=backend.default_branch_name,
696 branch=backend.default_branch_name,
696 commit_id=commit_ids['change'])
697 commit_id=commit_ids['change'])
697 pull_request.target_repo = target
698 pull_request.target_repo = target
698 # TODO: johbo: Target ref should be branch based, since tip can jump
699 # TODO: johbo: Target ref should be branch based, since tip can jump
699 # from branch to branch
700 # from branch to branch
700 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
701 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
701 branch=backend.default_branch_name,
702 branch=backend.default_branch_name,
702 commit_id=commit_ids['ancestor'])
703 commit_id=commit_ids['ancestor'])
703 pull_request.revisions = [commit_ids['change']]
704 pull_request.revisions = [commit_ids['change']]
704 pull_request.title = u"Test"
705 pull_request.title = u"Test"
705 pull_request.description = u"Description"
706 pull_request.description = u"Description"
706 pull_request.author = UserModel().get_by_username(
707 pull_request.author = UserModel().get_by_username(
707 TEST_USER_ADMIN_LOGIN)
708 TEST_USER_ADMIN_LOGIN)
708 Session().add(pull_request)
709 Session().add(pull_request)
709 Session().commit()
710 Session().commit()
710 pull_request_id = pull_request.pull_request_id
711 pull_request_id = pull_request.pull_request_id
711
712
712 # target has ancestor - ancestor-new
713 # target has ancestor - ancestor-new
713 # source has ancestor - ancestor-new - change-rebased
714 # source has ancestor - ancestor-new - change-rebased
714 backend.pull_heads(target, heads=['ancestor-new'])
715 backend.pull_heads(target, heads=['ancestor-new'])
715 backend.pull_heads(source, heads=['change-rebased'])
716 backend.pull_heads(source, heads=['change-rebased'])
716
717
717 # update PR
718 # update PR
718 self.app.post(
719 self.app.post(
719 url(controller='pullrequests', action='update',
720 url(controller='pullrequests', action='update',
720 repo_name=target.repo_name,
721 repo_name=target.repo_name,
721 pull_request_id=str(pull_request_id)),
722 pull_request_id=str(pull_request_id)),
722 params={'update_commits': 'true', '_method': 'put',
723 params={'update_commits': 'true', '_method': 'put',
723 'csrf_token': csrf_token},
724 'csrf_token': csrf_token},
724 status=200)
725 status=200)
725
726
726 # Expect the target reference to be updated correctly
727 # Expect the target reference to be updated correctly
727 pull_request = PullRequest.get(pull_request_id)
728 pull_request = PullRequest.get(pull_request_id)
728 assert pull_request.revisions == [commit_ids['change-rebased']]
729 assert pull_request.revisions == [commit_ids['change-rebased']]
729 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
730 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
730 branch=backend.default_branch_name,
731 branch=backend.default_branch_name,
731 commit_id=commit_ids['ancestor-new'])
732 commit_id=commit_ids['ancestor-new'])
732 assert pull_request.target_ref == expected_target_ref
733 assert pull_request.target_ref == expected_target_ref
733
734
734 def test_remove_pull_request_branch(self, backend_git, csrf_token):
735 def test_remove_pull_request_branch(self, backend_git, csrf_token):
735 branch_name = 'development'
736 branch_name = 'development'
736 commits = [
737 commits = [
737 {'message': 'initial-commit'},
738 {'message': 'initial-commit'},
738 {'message': 'old-feature'},
739 {'message': 'old-feature'},
739 {'message': 'new-feature', 'branch': branch_name},
740 {'message': 'new-feature', 'branch': branch_name},
740 ]
741 ]
741 repo = backend_git.create_repo(commits)
742 repo = backend_git.create_repo(commits)
742 commit_ids = backend_git.commit_ids
743 commit_ids = backend_git.commit_ids
743
744
744 pull_request = PullRequest()
745 pull_request = PullRequest()
745 pull_request.source_repo = repo
746 pull_request.source_repo = repo
746 pull_request.target_repo = repo
747 pull_request.target_repo = repo
747 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
748 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
748 branch=branch_name, commit_id=commit_ids['new-feature'])
749 branch=branch_name, commit_id=commit_ids['new-feature'])
749 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
750 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
750 branch=backend_git.default_branch_name,
751 branch=backend_git.default_branch_name,
751 commit_id=commit_ids['old-feature'])
752 commit_id=commit_ids['old-feature'])
752 pull_request.revisions = [commit_ids['new-feature']]
753 pull_request.revisions = [commit_ids['new-feature']]
753 pull_request.title = u"Test"
754 pull_request.title = u"Test"
754 pull_request.description = u"Description"
755 pull_request.description = u"Description"
755 pull_request.author = UserModel().get_by_username(
756 pull_request.author = UserModel().get_by_username(
756 TEST_USER_ADMIN_LOGIN)
757 TEST_USER_ADMIN_LOGIN)
757 Session().add(pull_request)
758 Session().add(pull_request)
758 Session().commit()
759 Session().commit()
759
760
760 vcs = repo.scm_instance()
761 vcs = repo.scm_instance()
761 vcs.remove_ref('refs/heads/{}'.format(branch_name))
762 vcs.remove_ref('refs/heads/{}'.format(branch_name))
762
763
763 response = self.app.get(url(
764 response = self.app.get(url(
764 controller='pullrequests', action='show',
765 controller='pullrequests', action='show',
765 repo_name=repo.repo_name,
766 repo_name=repo.repo_name,
766 pull_request_id=str(pull_request.pull_request_id)))
767 pull_request_id=str(pull_request.pull_request_id)))
767
768
768 assert response.status_int == 200
769 assert response.status_int == 200
769 assert_response = AssertResponse(response)
770 assert_response = AssertResponse(response)
770 assert_response.element_contains(
771 assert_response.element_contains(
771 '#changeset_compare_view_content .alert strong',
772 '#changeset_compare_view_content .alert strong',
772 'Missing commits')
773 'Missing commits')
773 assert_response.element_contains(
774 assert_response.element_contains(
774 '#changeset_compare_view_content .alert',
775 '#changeset_compare_view_content .alert',
775 'This pull request cannot be displayed, because one or more'
776 'This pull request cannot be displayed, because one or more'
776 ' commits no longer exist in the source repository.')
777 ' commits no longer exist in the source repository.')
777
778
778 def test_strip_commits_from_pull_request(
779 def test_strip_commits_from_pull_request(
779 self, backend, pr_util, csrf_token):
780 self, backend, pr_util, csrf_token):
780 commits = [
781 commits = [
781 {'message': 'initial-commit'},
782 {'message': 'initial-commit'},
782 {'message': 'old-feature'},
783 {'message': 'old-feature'},
783 {'message': 'new-feature', 'parents': ['initial-commit']},
784 {'message': 'new-feature', 'parents': ['initial-commit']},
784 ]
785 ]
785 pull_request = pr_util.create_pull_request(
786 pull_request = pr_util.create_pull_request(
786 commits, target_head='initial-commit', source_head='new-feature',
787 commits, target_head='initial-commit', source_head='new-feature',
787 revisions=['new-feature'])
788 revisions=['new-feature'])
788
789
789 vcs = pr_util.source_repository.scm_instance()
790 vcs = pr_util.source_repository.scm_instance()
790 if backend.alias == 'git':
791 if backend.alias == 'git':
791 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
792 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
792 else:
793 else:
793 vcs.strip(pr_util.commit_ids['new-feature'])
794 vcs.strip(pr_util.commit_ids['new-feature'])
794
795
795 response = self.app.get(url(
796 response = self.app.get(url(
796 controller='pullrequests', action='show',
797 controller='pullrequests', action='show',
797 repo_name=pr_util.target_repository.repo_name,
798 repo_name=pr_util.target_repository.repo_name,
798 pull_request_id=str(pull_request.pull_request_id)))
799 pull_request_id=str(pull_request.pull_request_id)))
799
800
800 assert response.status_int == 200
801 assert response.status_int == 200
801 assert_response = AssertResponse(response)
802 assert_response = AssertResponse(response)
802 assert_response.element_contains(
803 assert_response.element_contains(
803 '#changeset_compare_view_content .alert strong',
804 '#changeset_compare_view_content .alert strong',
804 'Missing commits')
805 'Missing commits')
805 assert_response.element_contains(
806 assert_response.element_contains(
806 '#changeset_compare_view_content .alert',
807 '#changeset_compare_view_content .alert',
807 'This pull request cannot be displayed, because one or more'
808 'This pull request cannot be displayed, because one or more'
808 ' commits no longer exist in the source repository.')
809 ' commits no longer exist in the source repository.')
809 assert_response.element_contains(
810 assert_response.element_contains(
810 '#update_commits',
811 '#update_commits',
811 'Update commits')
812 'Update commits')
812
813
813 def test_strip_commits_and_update(
814 def test_strip_commits_and_update(
814 self, backend, pr_util, csrf_token):
815 self, backend, pr_util, csrf_token):
815 commits = [
816 commits = [
816 {'message': 'initial-commit'},
817 {'message': 'initial-commit'},
817 {'message': 'old-feature'},
818 {'message': 'old-feature'},
818 {'message': 'new-feature', 'parents': ['old-feature']},
819 {'message': 'new-feature', 'parents': ['old-feature']},
819 ]
820 ]
820 pull_request = pr_util.create_pull_request(
821 pull_request = pr_util.create_pull_request(
821 commits, target_head='old-feature', source_head='new-feature',
822 commits, target_head='old-feature', source_head='new-feature',
822 revisions=['new-feature'], mergeable=True)
823 revisions=['new-feature'], mergeable=True)
823
824
824 vcs = pr_util.source_repository.scm_instance()
825 vcs = pr_util.source_repository.scm_instance()
825 if backend.alias == 'git':
826 if backend.alias == 'git':
826 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
827 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
827 else:
828 else:
828 vcs.strip(pr_util.commit_ids['new-feature'])
829 vcs.strip(pr_util.commit_ids['new-feature'])
829
830
830 response = self.app.post(
831 response = self.app.post(
831 url(controller='pullrequests', action='update',
832 url(controller='pullrequests', action='update',
832 repo_name=pull_request.target_repo.repo_name,
833 repo_name=pull_request.target_repo.repo_name,
833 pull_request_id=str(pull_request.pull_request_id)),
834 pull_request_id=str(pull_request.pull_request_id)),
834 params={'update_commits': 'true', '_method': 'put',
835 params={'update_commits': 'true', '_method': 'put',
835 'csrf_token': csrf_token})
836 'csrf_token': csrf_token})
836
837
837 assert response.status_int == 200
838 assert response.status_int == 200
838 assert response.body == 'true'
839 assert response.body == 'true'
839
840
840 # Make sure that after update, it won't raise 500 errors
841 # Make sure that after update, it won't raise 500 errors
841 response = self.app.get(url(
842 response = self.app.get(url(
842 controller='pullrequests', action='show',
843 controller='pullrequests', action='show',
843 repo_name=pr_util.target_repository.repo_name,
844 repo_name=pr_util.target_repository.repo_name,
844 pull_request_id=str(pull_request.pull_request_id)))
845 pull_request_id=str(pull_request.pull_request_id)))
845
846
846 assert response.status_int == 200
847 assert response.status_int == 200
847 assert_response = AssertResponse(response)
848 assert_response = AssertResponse(response)
848 assert_response.element_contains(
849 assert_response.element_contains(
849 '#changeset_compare_view_content .alert strong',
850 '#changeset_compare_view_content .alert strong',
850 'Missing commits')
851 'Missing commits')
851
852
852 def test_branch_is_a_link(self, pr_util):
853 def test_branch_is_a_link(self, pr_util):
853 pull_request = pr_util.create_pull_request()
854 pull_request = pr_util.create_pull_request()
854 pull_request.source_ref = 'branch:origin:1234567890abcdef'
855 pull_request.source_ref = 'branch:origin:1234567890abcdef'
855 pull_request.target_ref = 'branch:target:abcdef1234567890'
856 pull_request.target_ref = 'branch:target:abcdef1234567890'
856 Session().add(pull_request)
857 Session().add(pull_request)
857 Session().commit()
858 Session().commit()
858
859
859 response = self.app.get(url(
860 response = self.app.get(url(
860 controller='pullrequests', action='show',
861 controller='pullrequests', action='show',
861 repo_name=pull_request.target_repo.scm_instance().name,
862 repo_name=pull_request.target_repo.scm_instance().name,
862 pull_request_id=str(pull_request.pull_request_id)))
863 pull_request_id=str(pull_request.pull_request_id)))
863 assert response.status_int == 200
864 assert response.status_int == 200
864 assert_response = AssertResponse(response)
865 assert_response = AssertResponse(response)
865
866
866 origin = assert_response.get_element('.pr-origininfo .tag')
867 origin = assert_response.get_element('.pr-origininfo .tag')
867 origin_children = origin.getchildren()
868 origin_children = origin.getchildren()
868 assert len(origin_children) == 1
869 assert len(origin_children) == 1
869 target = assert_response.get_element('.pr-targetinfo .tag')
870 target = assert_response.get_element('.pr-targetinfo .tag')
870 target_children = target.getchildren()
871 target_children = target.getchildren()
871 assert len(target_children) == 1
872 assert len(target_children) == 1
872
873
873 expected_origin_link = url(
874 expected_origin_link = url(
874 'changelog_home',
875 'changelog_home',
875 repo_name=pull_request.source_repo.scm_instance().name,
876 repo_name=pull_request.source_repo.scm_instance().name,
876 branch='origin')
877 branch='origin')
877 expected_target_link = url(
878 expected_target_link = url(
878 'changelog_home',
879 'changelog_home',
879 repo_name=pull_request.target_repo.scm_instance().name,
880 repo_name=pull_request.target_repo.scm_instance().name,
880 branch='target')
881 branch='target')
881 assert origin_children[0].attrib['href'] == expected_origin_link
882 assert origin_children[0].attrib['href'] == expected_origin_link
882 assert origin_children[0].text == 'branch: origin'
883 assert origin_children[0].text == 'branch: origin'
883 assert target_children[0].attrib['href'] == expected_target_link
884 assert target_children[0].attrib['href'] == expected_target_link
884 assert target_children[0].text == 'branch: target'
885 assert target_children[0].text == 'branch: target'
885
886
886 def test_bookmark_is_not_a_link(self, pr_util):
887 def test_bookmark_is_not_a_link(self, pr_util):
887 pull_request = pr_util.create_pull_request()
888 pull_request = pr_util.create_pull_request()
888 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
889 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
889 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
890 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
890 Session().add(pull_request)
891 Session().add(pull_request)
891 Session().commit()
892 Session().commit()
892
893
893 response = self.app.get(url(
894 response = self.app.get(url(
894 controller='pullrequests', action='show',
895 controller='pullrequests', action='show',
895 repo_name=pull_request.target_repo.scm_instance().name,
896 repo_name=pull_request.target_repo.scm_instance().name,
896 pull_request_id=str(pull_request.pull_request_id)))
897 pull_request_id=str(pull_request.pull_request_id)))
897 assert response.status_int == 200
898 assert response.status_int == 200
898 assert_response = AssertResponse(response)
899 assert_response = AssertResponse(response)
899
900
900 origin = assert_response.get_element('.pr-origininfo .tag')
901 origin = assert_response.get_element('.pr-origininfo .tag')
901 assert origin.text.strip() == 'bookmark: origin'
902 assert origin.text.strip() == 'bookmark: origin'
902 assert origin.getchildren() == []
903 assert origin.getchildren() == []
903
904
904 target = assert_response.get_element('.pr-targetinfo .tag')
905 target = assert_response.get_element('.pr-targetinfo .tag')
905 assert target.text.strip() == 'bookmark: target'
906 assert target.text.strip() == 'bookmark: target'
906 assert target.getchildren() == []
907 assert target.getchildren() == []
907
908
908 def test_tag_is_not_a_link(self, pr_util):
909 def test_tag_is_not_a_link(self, pr_util):
909 pull_request = pr_util.create_pull_request()
910 pull_request = pr_util.create_pull_request()
910 pull_request.source_ref = 'tag:origin:1234567890abcdef'
911 pull_request.source_ref = 'tag:origin:1234567890abcdef'
911 pull_request.target_ref = 'tag:target:abcdef1234567890'
912 pull_request.target_ref = 'tag:target:abcdef1234567890'
912 Session().add(pull_request)
913 Session().add(pull_request)
913 Session().commit()
914 Session().commit()
914
915
915 response = self.app.get(url(
916 response = self.app.get(url(
916 controller='pullrequests', action='show',
917 controller='pullrequests', action='show',
917 repo_name=pull_request.target_repo.scm_instance().name,
918 repo_name=pull_request.target_repo.scm_instance().name,
918 pull_request_id=str(pull_request.pull_request_id)))
919 pull_request_id=str(pull_request.pull_request_id)))
919 assert response.status_int == 200
920 assert response.status_int == 200
920 assert_response = AssertResponse(response)
921 assert_response = AssertResponse(response)
921
922
922 origin = assert_response.get_element('.pr-origininfo .tag')
923 origin = assert_response.get_element('.pr-origininfo .tag')
923 assert origin.text.strip() == 'tag: origin'
924 assert origin.text.strip() == 'tag: origin'
924 assert origin.getchildren() == []
925 assert origin.getchildren() == []
925
926
926 target = assert_response.get_element('.pr-targetinfo .tag')
927 target = assert_response.get_element('.pr-targetinfo .tag')
927 assert target.text.strip() == 'tag: target'
928 assert target.text.strip() == 'tag: target'
928 assert target.getchildren() == []
929 assert target.getchildren() == []
929
930
930 def test_description_is_escaped_on_index_page(self, backend, pr_util):
931 def test_description_is_escaped_on_index_page(self, backend, pr_util):
931 xss_description = "<script>alert('Hi!')</script>"
932 xss_description = "<script>alert('Hi!')</script>"
932 pull_request = pr_util.create_pull_request(description=xss_description)
933 pull_request = pr_util.create_pull_request(description=xss_description)
933 response = self.app.get(url(
934 response = self.app.get(url(
934 controller='pullrequests', action='show_all',
935 controller='pullrequests', action='show_all',
935 repo_name=pull_request.target_repo.repo_name))
936 repo_name=pull_request.target_repo.repo_name))
936 response.mustcontain(
937 response.mustcontain(
937 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
938 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
938
939
939 @pytest.mark.parametrize('mergeable', [True, False])
940 @pytest.mark.parametrize('mergeable', [True, False])
940 def test_shadow_repository_link(
941 def test_shadow_repository_link(
941 self, mergeable, pr_util, http_host_stub):
942 self, mergeable, pr_util, http_host_stub):
942 """
943 """
943 Check that the pull request summary page displays a link to the shadow
944 Check that the pull request summary page displays a link to the shadow
944 repository if the pull request is mergeable. If it is not mergeable
945 repository if the pull request is mergeable. If it is not mergeable
945 the link should not be displayed.
946 the link should not be displayed.
946 """
947 """
947 pull_request = pr_util.create_pull_request(
948 pull_request = pr_util.create_pull_request(
948 mergeable=mergeable, enable_notifications=False)
949 mergeable=mergeable, enable_notifications=False)
949 target_repo = pull_request.target_repo.scm_instance()
950 target_repo = pull_request.target_repo.scm_instance()
950 pr_id = pull_request.pull_request_id
951 pr_id = pull_request.pull_request_id
951 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
952 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
952 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
953 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
953
954
954 response = self.app.get(url(
955 response = self.app.get(url(
955 controller='pullrequests', action='show',
956 controller='pullrequests', action='show',
956 repo_name=target_repo.name,
957 repo_name=target_repo.name,
957 pull_request_id=str(pr_id)))
958 pull_request_id=str(pr_id)))
958
959
959 assertr = AssertResponse(response)
960 assertr = AssertResponse(response)
960 if mergeable:
961 if mergeable:
961 assertr.element_value_contains(
962 assertr.element_value_contains(
962 'div.pr-mergeinfo input', shadow_url)
963 'div.pr-mergeinfo input', shadow_url)
963 assertr.element_value_contains(
964 assertr.element_value_contains(
964 'div.pr-mergeinfo input', 'pr-merge')
965 'div.pr-mergeinfo input', 'pr-merge')
965 else:
966 else:
966 assertr.no_element_exists('div.pr-mergeinfo')
967 assertr.no_element_exists('div.pr-mergeinfo')
967
968
968
969
970 @pytest.mark.usefixtures('app')
971 @pytest.mark.backends("git", "hg")
972 class TestPullrequestsControllerDelete(object):
973 def test_pull_request_delete_button_permissions_admin(
974 self, autologin_user, user_admin, pr_util):
975 pull_request = pr_util.create_pull_request(
976 author=user_admin.username, enable_notifications=False)
977
978 response = self.app.get(url(
979 controller='pullrequests', action='show',
980 repo_name=pull_request.target_repo.scm_instance().name,
981 pull_request_id=str(pull_request.pull_request_id)))
982
983 response.mustcontain('id="delete_pullrequest"')
984 response.mustcontain('Confirm to delete this pull request')
985
986 def test_pull_request_delete_button_permissions_owner(
987 self, autologin_regular_user, user_regular, pr_util):
988 pull_request = pr_util.create_pull_request(
989 author=user_regular.username, enable_notifications=False)
990
991 response = self.app.get(url(
992 controller='pullrequests', action='show',
993 repo_name=pull_request.target_repo.scm_instance().name,
994 pull_request_id=str(pull_request.pull_request_id)))
995
996 response.mustcontain('id="delete_pullrequest"')
997 response.mustcontain('Confirm to delete this pull request')
998
999 def test_pull_request_delete_button_permissions_forbidden(
1000 self, autologin_regular_user, user_regular, user_admin, pr_util):
1001 pull_request = pr_util.create_pull_request(
1002 author=user_admin.username, enable_notifications=False)
1003
1004 response = self.app.get(url(
1005 controller='pullrequests', action='show',
1006 repo_name=pull_request.target_repo.scm_instance().name,
1007 pull_request_id=str(pull_request.pull_request_id)))
1008 response.mustcontain(no=['id="delete_pullrequest"'])
1009 response.mustcontain(no=['Confirm to delete this pull request'])
1010
1011 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1012 self, autologin_regular_user, user_regular, user_admin, pr_util,
1013 user_util):
1014
1015 pull_request = pr_util.create_pull_request(
1016 author=user_admin.username, enable_notifications=False)
1017
1018 user_util.grant_user_permission_to_repo(
1019 pull_request.target_repo, user_regular,
1020 'repository.write')
1021
1022 response = self.app.get(url(
1023 controller='pullrequests', action='show',
1024 repo_name=pull_request.target_repo.scm_instance().name,
1025 pull_request_id=str(pull_request.pull_request_id)))
1026
1027 response.mustcontain('id="open_edit_pullrequest"')
1028 response.mustcontain('id="delete_pullrequest"')
1029 response.mustcontain(no=['Confirm to delete this pull request'])
1030
1031
969 def assert_pull_request_status(pull_request, expected_status):
1032 def assert_pull_request_status(pull_request, expected_status):
970 status = ChangesetStatusModel().calculated_review_status(
1033 status = ChangesetStatusModel().calculated_review_status(
971 pull_request=pull_request)
1034 pull_request=pull_request)
972 assert status == expected_status
1035 assert status == expected_status
973
1036
974
1037
975 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1038 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
976 @pytest.mark.usefixtures("autologin_user")
1039 @pytest.mark.usefixtures("autologin_user")
977 def test_redirects_to_repo_summary_for_svn_repositories(
1040 def test_redirects_to_repo_summary_for_svn_repositories(
978 backend_svn, app, action):
1041 backend_svn, app, action):
979 denied_actions = ['show_all', 'index', 'create']
1042 denied_actions = ['show_all', 'index', 'create']
980 for action in denied_actions:
1043 for action in denied_actions:
981 response = app.get(url(
1044 response = app.get(url(
982 controller='pullrequests', action=action,
1045 controller='pullrequests', action=action,
983 repo_name=backend_svn.repo_name))
1046 repo_name=backend_svn.repo_name))
984 assert response.status_int == 302
1047 assert response.status_int == 302
985
1048
986 # Not allowed, redirect to the summary
1049 # Not allowed, redirect to the summary
987 redirected = response.follow()
1050 redirected = response.follow()
988 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1051 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
989
1052
990 # URL adds leading slash and path doesn't have it
1053 # URL adds leading slash and path doesn't have it
991 assert redirected.req.path == summary_url
1054 assert redirected.req.path == summary_url
992
1055
993
1056
994 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1057 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
995 # TODO: johbo: Global import not possible because models.forms blows up
1058 # TODO: johbo: Global import not possible because models.forms blows up
996 from rhodecode.controllers.pullrequests import PullrequestsController
1059 from rhodecode.controllers.pullrequests import PullrequestsController
997 controller = PullrequestsController()
1060 controller = PullrequestsController()
998 patcher = mock.patch(
1061 patcher = mock.patch(
999 'rhodecode.model.db.BaseModel.get', return_value=None)
1062 'rhodecode.model.db.BaseModel.get', return_value=None)
1000 with pytest.raises(HTTPNotFound), patcher:
1063 with pytest.raises(HTTPNotFound), patcher:
1001 controller._delete_comment(1)
1064 controller._delete_comment(1)
General Comments 0
You need to be logged in to leave comments. Login now