##// END OF EJS Templates
pull requests: add a 'Calculated' comment on current_changeset_status
Mads Kiilerich -
r4053:3625fd19 default
parent child Browse files
Show More
@@ -1,550 +1,550 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.pullrequests
3 rhodecode.controllers.pullrequests
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull requests controller for rhodecode for initializing pull requests
6 pull requests controller for rhodecode for initializing pull requests
7
7
8 :created_on: May 7, 2012
8 :created_on: May 7, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import formencode
27 import formencode
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden
29 from webob.exc import HTTPNotFound, HTTPForbidden
30 from collections import defaultdict
30 from collections import defaultdict
31 from itertools import groupby
31 from itertools import groupby
32
32
33 from pylons import request, tmpl_context as c, url
33 from pylons import request, tmpl_context as c, url
34 from pylons.controllers.util import redirect
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 from rhodecode.lib.compat import json
37 from rhodecode.lib.compat import json
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 NotAnonymous
40 NotAnonymous
41 from rhodecode.lib.helpers import Page
41 from rhodecode.lib.helpers import Page
42 from rhodecode.lib import helpers as h
42 from rhodecode.lib import helpers as h
43 from rhodecode.lib import diffs
43 from rhodecode.lib import diffs
44 from rhodecode.lib.utils import action_logger, jsonify
44 from rhodecode.lib.utils import action_logger, jsonify
45 from rhodecode.lib.vcs.utils import safe_str
45 from rhodecode.lib.vcs.utils import safe_str
46 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
46 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
47 from rhodecode.lib.diffs import LimitedDiffContainer
47 from rhodecode.lib.diffs import LimitedDiffContainer
48 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment
48 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52 from rhodecode.model.comment import ChangesetCommentsModel
52 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.changeset_status import ChangesetStatusModel
54 from rhodecode.model.forms import PullRequestForm
54 from rhodecode.model.forms import PullRequestForm
55 from rhodecode.lib.utils2 import safe_int
55 from rhodecode.lib.utils2 import safe_int
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class PullrequestsController(BaseRepoController):
60 class PullrequestsController(BaseRepoController):
61
61
62 def __before__(self):
62 def __before__(self):
63 super(PullrequestsController, self).__before__()
63 super(PullrequestsController, self).__before__()
64 repo_model = RepoModel()
64 repo_model = RepoModel()
65 c.users_array = repo_model.get_users_js()
65 c.users_array = repo_model.get_users_js()
66 c.users_groups_array = repo_model.get_users_groups_js()
66 c.users_groups_array = repo_model.get_users_groups_js()
67
67
68 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
68 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
69 """return a structure with repo's interesting changesets, suitable for
69 """return a structure with repo's interesting changesets, suitable for
70 the selectors in pullrequest.html
70 the selectors in pullrequest.html
71
71
72 rev: a revision that must be in the list somehow and selected by default
72 rev: a revision that must be in the list somehow and selected by default
73 branch: a branch that must be in the list and selected by default - even if closed
73 branch: a branch that must be in the list and selected by default - even if closed
74 branch_rev: a revision of which peers should be preferred and available."""
74 branch_rev: a revision of which peers should be preferred and available."""
75 # list named branches that has been merged to this named branch - it should probably merge back
75 # list named branches that has been merged to this named branch - it should probably merge back
76 peers = []
76 peers = []
77
77
78 if rev:
78 if rev:
79 rev = safe_str(rev)
79 rev = safe_str(rev)
80
80
81 if branch:
81 if branch:
82 branch = safe_str(branch)
82 branch = safe_str(branch)
83
83
84 if branch_rev:
84 if branch_rev:
85 branch_rev = safe_str(branch_rev)
85 branch_rev = safe_str(branch_rev)
86 # not restricting to merge() would also get branch point and be better
86 # not restricting to merge() would also get branch point and be better
87 # (especially because it would get the branch point) ... but is currently too expensive
87 # (especially because it would get the branch point) ... but is currently too expensive
88 otherbranches = {}
88 otherbranches = {}
89 for i in repo._repo.revs(
89 for i in repo._repo.revs(
90 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
90 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
91 branch_rev, branch_rev):
91 branch_rev, branch_rev):
92 cs = repo.get_changeset(i)
92 cs = repo.get_changeset(i)
93 otherbranches[cs.branch] = cs.raw_id
93 otherbranches[cs.branch] = cs.raw_id
94 for abranch, node in otherbranches.iteritems():
94 for abranch, node in otherbranches.iteritems():
95 selected = 'branch:%s:%s' % (abranch, node)
95 selected = 'branch:%s:%s' % (abranch, node)
96 peers.append((selected, abranch))
96 peers.append((selected, abranch))
97
97
98 selected = None
98 selected = None
99
99
100 branches = []
100 branches = []
101 for abranch, branchrev in repo.branches.iteritems():
101 for abranch, branchrev in repo.branches.iteritems():
102 n = 'branch:%s:%s' % (abranch, branchrev)
102 n = 'branch:%s:%s' % (abranch, branchrev)
103 branches.append((n, abranch))
103 branches.append((n, abranch))
104 if rev == branchrev:
104 if rev == branchrev:
105 selected = n
105 selected = n
106 if branch == abranch:
106 if branch == abranch:
107 selected = n
107 selected = n
108 branch = None
108 branch = None
109 if branch: # branch not in list - it is probably closed
109 if branch: # branch not in list - it is probably closed
110 revs = repo._repo.revs('max(branch(%s))', branch)
110 revs = repo._repo.revs('max(branch(%s))', branch)
111 if revs:
111 if revs:
112 cs = repo.get_changeset(revs[0])
112 cs = repo.get_changeset(revs[0])
113 selected = 'branch:%s:%s' % (branch, cs.raw_id)
113 selected = 'branch:%s:%s' % (branch, cs.raw_id)
114 branches.append((selected, branch))
114 branches.append((selected, branch))
115
115
116 bookmarks = []
116 bookmarks = []
117 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
117 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
118 n = 'book:%s:%s' % (bookmark, bookmarkrev)
118 n = 'book:%s:%s' % (bookmark, bookmarkrev)
119 bookmarks.append((n, bookmark))
119 bookmarks.append((n, bookmark))
120 if rev == bookmarkrev:
120 if rev == bookmarkrev:
121 selected = n
121 selected = n
122
122
123 tags = []
123 tags = []
124 for tag, tagrev in repo.tags.iteritems():
124 for tag, tagrev in repo.tags.iteritems():
125 n = 'tag:%s:%s' % (tag, tagrev)
125 n = 'tag:%s:%s' % (tag, tagrev)
126 tags.append((n, tag))
126 tags.append((n, tag))
127 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
127 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
128 selected = n
128 selected = n
129
129
130 # prio 1: rev was selected as existing entry above
130 # prio 1: rev was selected as existing entry above
131
131
132 # prio 2: create special entry for rev; rev _must_ be used
132 # prio 2: create special entry for rev; rev _must_ be used
133 specials = []
133 specials = []
134 if rev and selected is None:
134 if rev and selected is None:
135 selected = 'rev:%s:%s' % (rev, rev)
135 selected = 'rev:%s:%s' % (rev, rev)
136 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
136 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
137
137
138 # prio 3: most recent peer branch
138 # prio 3: most recent peer branch
139 if peers and not selected:
139 if peers and not selected:
140 selected = peers[0][0][0]
140 selected = peers[0][0][0]
141
141
142 # prio 4: tip revision
142 # prio 4: tip revision
143 if not selected:
143 if not selected:
144 selected = 'tag:tip:%s' % repo.tags['tip']
144 selected = 'tag:tip:%s' % repo.tags['tip']
145
145
146 groups = [(specials, _("Special")),
146 groups = [(specials, _("Special")),
147 (peers, _("Peer branches")),
147 (peers, _("Peer branches")),
148 (bookmarks, _("Bookmarks")),
148 (bookmarks, _("Bookmarks")),
149 (branches, _("Branches")),
149 (branches, _("Branches")),
150 (tags, _("Tags")),
150 (tags, _("Tags")),
151 ]
151 ]
152 return [g for g in groups if g[0]], selected
152 return [g for g in groups if g[0]], selected
153
153
154 def _get_is_allowed_change_status(self, pull_request):
154 def _get_is_allowed_change_status(self, pull_request):
155 owner = self.rhodecode_user.user_id == pull_request.user_id
155 owner = self.rhodecode_user.user_id == pull_request.user_id
156 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
156 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
157 pull_request.reviewers]
157 pull_request.reviewers]
158 return (self.rhodecode_user.admin or owner or reviewer)
158 return (self.rhodecode_user.admin or owner or reviewer)
159
159
160 def _load_compare_data(self, pull_request, enable_comments=True):
160 def _load_compare_data(self, pull_request, enable_comments=True):
161 """
161 """
162 Load context data needed for generating compare diff
162 Load context data needed for generating compare diff
163
163
164 :param pull_request:
164 :param pull_request:
165 """
165 """
166 org_repo = pull_request.org_repo
166 org_repo = pull_request.org_repo
167 (org_ref_type,
167 (org_ref_type,
168 org_ref_name,
168 org_ref_name,
169 org_ref_rev) = pull_request.org_ref.split(':')
169 org_ref_rev) = pull_request.org_ref.split(':')
170
170
171 other_repo = org_repo
171 other_repo = org_repo
172 (other_ref_type,
172 (other_ref_type,
173 other_ref_name,
173 other_ref_name,
174 other_ref_rev) = pull_request.other_ref.split(':')
174 other_ref_rev) = pull_request.other_ref.split(':')
175
175
176 # despite opening revisions for bookmarks/branches/tags, we always
176 # despite opening revisions for bookmarks/branches/tags, we always
177 # convert this to rev to prevent changes after bookmark or branch change
177 # convert this to rev to prevent changes after bookmark or branch change
178 org_ref = ('rev', org_ref_rev)
178 org_ref = ('rev', org_ref_rev)
179 other_ref = ('rev', other_ref_rev)
179 other_ref = ('rev', other_ref_rev)
180
180
181 c.org_repo = org_repo
181 c.org_repo = org_repo
182 c.other_repo = other_repo
182 c.other_repo = other_repo
183
183
184 c.fulldiff = fulldiff = request.GET.get('fulldiff')
184 c.fulldiff = fulldiff = request.GET.get('fulldiff')
185
185
186 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
186 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
187
187
188 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
188 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
189
189
190 c.org_ref = org_ref[1]
190 c.org_ref = org_ref[1]
191 c.org_ref_type = org_ref[0]
191 c.org_ref_type = org_ref[0]
192 c.other_ref = other_ref[1]
192 c.other_ref = other_ref[1]
193 c.other_ref_type = other_ref[0]
193 c.other_ref_type = other_ref[0]
194
194
195 diff_limit = self.cut_off_limit if not fulldiff else None
195 diff_limit = self.cut_off_limit if not fulldiff else None
196
196
197 # we swap org/other ref since we run a simple diff on one repo
197 # we swap org/other ref since we run a simple diff on one repo
198 log.debug('running diff between %s and %s in %s'
198 log.debug('running diff between %s and %s in %s'
199 % (other_ref, org_ref, org_repo.scm_instance.path))
199 % (other_ref, org_ref, org_repo.scm_instance.path))
200 txtdiff = org_repo.scm_instance.get_diff(rev1=safe_str(other_ref[1]), rev2=safe_str(org_ref[1]))
200 txtdiff = org_repo.scm_instance.get_diff(rev1=safe_str(other_ref[1]), rev2=safe_str(org_ref[1]))
201
201
202 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
202 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
203 diff_limit=diff_limit)
203 diff_limit=diff_limit)
204 _parsed = diff_processor.prepare()
204 _parsed = diff_processor.prepare()
205
205
206 c.limited_diff = False
206 c.limited_diff = False
207 if isinstance(_parsed, LimitedDiffContainer):
207 if isinstance(_parsed, LimitedDiffContainer):
208 c.limited_diff = True
208 c.limited_diff = True
209
209
210 c.files = []
210 c.files = []
211 c.changes = {}
211 c.changes = {}
212 c.lines_added = 0
212 c.lines_added = 0
213 c.lines_deleted = 0
213 c.lines_deleted = 0
214
214
215 for f in _parsed:
215 for f in _parsed:
216 st = f['stats']
216 st = f['stats']
217 c.lines_added += st['added']
217 c.lines_added += st['added']
218 c.lines_deleted += st['deleted']
218 c.lines_deleted += st['deleted']
219 fid = h.FID('', f['filename'])
219 fid = h.FID('', f['filename'])
220 c.files.append([fid, f['operation'], f['filename'], f['stats']])
220 c.files.append([fid, f['operation'], f['filename'], f['stats']])
221 htmldiff = diff_processor.as_html(enable_comments=enable_comments,
221 htmldiff = diff_processor.as_html(enable_comments=enable_comments,
222 parsed_lines=[f])
222 parsed_lines=[f])
223 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
223 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
224
224
225 @LoginRequired()
225 @LoginRequired()
226 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
226 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
227 'repository.admin')
227 'repository.admin')
228 def show_all(self, repo_name):
228 def show_all(self, repo_name):
229 c.from_ = request.GET.get('from_') or ''
229 c.from_ = request.GET.get('from_') or ''
230 c.closed = request.GET.get('closed') or ''
230 c.closed = request.GET.get('closed') or ''
231 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
231 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
232 c.repo_name = repo_name
232 c.repo_name = repo_name
233 p = safe_int(request.GET.get('page', 1), 1)
233 p = safe_int(request.GET.get('page', 1), 1)
234
234
235 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
235 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
236
236
237 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
237 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
238
238
239 if request.environ.get('HTTP_X_PARTIAL_XHR'):
239 if request.environ.get('HTTP_X_PARTIAL_XHR'):
240 return c.pullrequest_data
240 return c.pullrequest_data
241
241
242 return render('/pullrequests/pullrequest_show_all.html')
242 return render('/pullrequests/pullrequest_show_all.html')
243
243
244 @LoginRequired()
244 @LoginRequired()
245 @NotAnonymous()
245 @NotAnonymous()
246 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
246 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
247 'repository.admin')
247 'repository.admin')
248 def index(self):
248 def index(self):
249 org_repo = c.rhodecode_db_repo
249 org_repo = c.rhodecode_db_repo
250
250
251 if org_repo.scm_instance.alias != 'hg':
251 if org_repo.scm_instance.alias != 'hg':
252 log.error('Review not available for GIT REPOS')
252 log.error('Review not available for GIT REPOS')
253 raise HTTPNotFound
253 raise HTTPNotFound
254
254
255 try:
255 try:
256 org_repo.scm_instance.get_changeset()
256 org_repo.scm_instance.get_changeset()
257 except EmptyRepositoryError, e:
257 except EmptyRepositoryError, e:
258 h.flash(h.literal(_('There are no changesets yet')),
258 h.flash(h.literal(_('There are no changesets yet')),
259 category='warning')
259 category='warning')
260 redirect(url('summary_home', repo_name=org_repo.repo_name))
260 redirect(url('summary_home', repo_name=org_repo.repo_name))
261
261
262 org_rev = request.GET.get('rev_end')
262 org_rev = request.GET.get('rev_end')
263 # rev_start is not directly useful - its parent could however be used
263 # rev_start is not directly useful - its parent could however be used
264 # as default for other and thus give a simple compare view
264 # as default for other and thus give a simple compare view
265 #other_rev = request.POST.get('rev_start')
265 #other_rev = request.POST.get('rev_start')
266 branch = request.GET.get('branch')
266 branch = request.GET.get('branch')
267
267
268 c.org_repos = []
268 c.org_repos = []
269 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
269 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
270 c.default_org_repo = org_repo.repo_name
270 c.default_org_repo = org_repo.repo_name
271 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, rev=org_rev, branch=branch)
271 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, rev=org_rev, branch=branch)
272
272
273 c.other_repos = []
273 c.other_repos = []
274 other_repos_info = {}
274 other_repos_info = {}
275
275
276 def add_other_repo(repo, branch_rev=None):
276 def add_other_repo(repo, branch_rev=None):
277 if repo.repo_name in other_repos_info: # shouldn't happen
277 if repo.repo_name in other_repos_info: # shouldn't happen
278 return
278 return
279 c.other_repos.append((repo.repo_name, repo.repo_name))
279 c.other_repos.append((repo.repo_name, repo.repo_name))
280 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
280 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
281 other_repos_info[repo.repo_name] = {
281 other_repos_info[repo.repo_name] = {
282 'user': dict(user_id=repo.user.user_id,
282 'user': dict(user_id=repo.user.user_id,
283 username=repo.user.username,
283 username=repo.user.username,
284 firstname=repo.user.firstname,
284 firstname=repo.user.firstname,
285 lastname=repo.user.lastname,
285 lastname=repo.user.lastname,
286 gravatar_link=h.gravatar_url(repo.user.email, 14)),
286 gravatar_link=h.gravatar_url(repo.user.email, 14)),
287 'description': repo.description.split('\n', 1)[0],
287 'description': repo.description.split('\n', 1)[0],
288 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
288 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
289 }
289 }
290
290
291 # add org repo to other so we can open pull request against peer branches on itself
291 # add org repo to other so we can open pull request against peer branches on itself
292 add_other_repo(org_repo, branch_rev=org_rev)
292 add_other_repo(org_repo, branch_rev=org_rev)
293 c.default_other_repo = org_repo.repo_name
293 c.default_other_repo = org_repo.repo_name
294
294
295 # gather forks and add to this list ... even though it is rare to
295 # gather forks and add to this list ... even though it is rare to
296 # request forks to pull from their parent
296 # request forks to pull from their parent
297 for fork in org_repo.forks:
297 for fork in org_repo.forks:
298 add_other_repo(fork)
298 add_other_repo(fork)
299
299
300 # add parents of this fork also, but only if it's not empty
300 # add parents of this fork also, but only if it's not empty
301 if org_repo.parent and org_repo.parent.scm_instance.revisions:
301 if org_repo.parent and org_repo.parent.scm_instance.revisions:
302 add_other_repo(org_repo.parent)
302 add_other_repo(org_repo.parent)
303 c.default_other_repo = org_repo.parent.repo_name
303 c.default_other_repo = org_repo.parent.repo_name
304
304
305 c.default_other_repo_info = other_repos_info[c.default_other_repo]
305 c.default_other_repo_info = other_repos_info[c.default_other_repo]
306 c.other_repos_info = json.dumps(other_repos_info)
306 c.other_repos_info = json.dumps(other_repos_info)
307
307
308 return render('/pullrequests/pullrequest.html')
308 return render('/pullrequests/pullrequest.html')
309
309
310 @LoginRequired()
310 @LoginRequired()
311 @NotAnonymous()
311 @NotAnonymous()
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 'repository.admin')
313 'repository.admin')
314 def create(self, repo_name):
314 def create(self, repo_name):
315 repo = RepoModel()._get_repo(repo_name)
315 repo = RepoModel()._get_repo(repo_name)
316 try:
316 try:
317 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
317 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
318 except formencode.Invalid, errors:
318 except formencode.Invalid, errors:
319 log.error(traceback.format_exc())
319 log.error(traceback.format_exc())
320 if errors.error_dict.get('revisions'):
320 if errors.error_dict.get('revisions'):
321 msg = 'Revisions: %s' % errors.error_dict['revisions']
321 msg = 'Revisions: %s' % errors.error_dict['revisions']
322 elif errors.error_dict.get('pullrequest_title'):
322 elif errors.error_dict.get('pullrequest_title'):
323 msg = _('Pull request requires a title with min. 3 chars')
323 msg = _('Pull request requires a title with min. 3 chars')
324 else:
324 else:
325 msg = _('Error creating pull request')
325 msg = _('Error creating pull request')
326
326
327 h.flash(msg, 'error')
327 h.flash(msg, 'error')
328 return redirect(url('pullrequest_home', repo_name=repo_name))
328 return redirect(url('pullrequest_home', repo_name=repo_name))
329
329
330 org_repo = _form['org_repo']
330 org_repo = _form['org_repo']
331 org_ref = _form['org_ref'] # will end with merge_rev but have symbolic name
331 org_ref = _form['org_ref'] # will end with merge_rev but have symbolic name
332 other_repo = _form['other_repo']
332 other_repo = _form['other_repo']
333 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev'] # could be calculated from other_ref ...
333 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev'] # could be calculated from other_ref ...
334 revisions = [x for x in reversed(_form['revisions'])]
334 revisions = [x for x in reversed(_form['revisions'])]
335 reviewers = _form['review_members']
335 reviewers = _form['review_members']
336
336
337 title = _form['pullrequest_title']
337 title = _form['pullrequest_title']
338 description = _form['pullrequest_desc']
338 description = _form['pullrequest_desc']
339 try:
339 try:
340 pull_request = PullRequestModel().create(
340 pull_request = PullRequestModel().create(
341 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
341 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
342 other_ref, revisions, reviewers, title, description
342 other_ref, revisions, reviewers, title, description
343 )
343 )
344 Session().commit()
344 Session().commit()
345 h.flash(_('Successfully opened new pull request'),
345 h.flash(_('Successfully opened new pull request'),
346 category='success')
346 category='success')
347 except Exception:
347 except Exception:
348 h.flash(_('Error occurred during sending pull request'),
348 h.flash(_('Error occurred during sending pull request'),
349 category='error')
349 category='error')
350 log.error(traceback.format_exc())
350 log.error(traceback.format_exc())
351 return redirect(url('pullrequest_home', repo_name=repo_name))
351 return redirect(url('pullrequest_home', repo_name=repo_name))
352
352
353 return redirect(url('pullrequest_show', repo_name=other_repo,
353 return redirect(url('pullrequest_show', repo_name=other_repo,
354 pull_request_id=pull_request.pull_request_id))
354 pull_request_id=pull_request.pull_request_id))
355
355
356 @LoginRequired()
356 @LoginRequired()
357 @NotAnonymous()
357 @NotAnonymous()
358 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
358 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
359 'repository.admin')
359 'repository.admin')
360 @jsonify
360 @jsonify
361 def update(self, repo_name, pull_request_id):
361 def update(self, repo_name, pull_request_id):
362 pull_request = PullRequest.get_or_404(pull_request_id)
362 pull_request = PullRequest.get_or_404(pull_request_id)
363 if pull_request.is_closed():
363 if pull_request.is_closed():
364 raise HTTPForbidden()
364 raise HTTPForbidden()
365 #only owner or admin can update it
365 #only owner or admin can update it
366 owner = pull_request.author.user_id == c.rhodecode_user.user_id
366 owner = pull_request.author.user_id == c.rhodecode_user.user_id
367 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
367 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
368 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
368 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
369 request.POST.get('reviewers_ids', '').split(',')))
369 request.POST.get('reviewers_ids', '').split(',')))
370
370
371 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
371 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
372 Session().commit()
372 Session().commit()
373 return True
373 return True
374 raise HTTPForbidden()
374 raise HTTPForbidden()
375
375
376 @LoginRequired()
376 @LoginRequired()
377 @NotAnonymous()
377 @NotAnonymous()
378 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
378 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
379 'repository.admin')
379 'repository.admin')
380 @jsonify
380 @jsonify
381 def delete(self, repo_name, pull_request_id):
381 def delete(self, repo_name, pull_request_id):
382 pull_request = PullRequest.get_or_404(pull_request_id)
382 pull_request = PullRequest.get_or_404(pull_request_id)
383 #only owner can delete it !
383 #only owner can delete it !
384 if pull_request.author.user_id == c.rhodecode_user.user_id:
384 if pull_request.author.user_id == c.rhodecode_user.user_id:
385 PullRequestModel().delete(pull_request)
385 PullRequestModel().delete(pull_request)
386 Session().commit()
386 Session().commit()
387 h.flash(_('Successfully deleted pull request'),
387 h.flash(_('Successfully deleted pull request'),
388 category='success')
388 category='success')
389 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
389 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
390 raise HTTPForbidden()
390 raise HTTPForbidden()
391
391
392 @LoginRequired()
392 @LoginRequired()
393 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
393 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 'repository.admin')
394 'repository.admin')
395 def show(self, repo_name, pull_request_id):
395 def show(self, repo_name, pull_request_id):
396 repo_model = RepoModel()
396 repo_model = RepoModel()
397 c.users_array = repo_model.get_users_js()
397 c.users_array = repo_model.get_users_js()
398 c.users_groups_array = repo_model.get_users_groups_js()
398 c.users_groups_array = repo_model.get_users_groups_js()
399 c.pull_request = PullRequest.get_or_404(pull_request_id)
399 c.pull_request = PullRequest.get_or_404(pull_request_id)
400 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
400 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
401 cc_model = ChangesetCommentsModel()
401 cc_model = ChangesetCommentsModel()
402 cs_model = ChangesetStatusModel()
402 cs_model = ChangesetStatusModel()
403 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
403 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
404 pull_request=c.pull_request,
404 pull_request=c.pull_request,
405 with_revisions=True)
405 with_revisions=True)
406
406
407 cs_statuses = defaultdict(list)
407 cs_statuses = defaultdict(list)
408 for st in _cs_statuses:
408 for st in _cs_statuses:
409 cs_statuses[st.author.username] += [st]
409 cs_statuses[st.author.username] += [st]
410
410
411 c.pull_request_reviewers = []
411 c.pull_request_reviewers = []
412 c.pull_request_pending_reviewers = []
412 c.pull_request_pending_reviewers = []
413 for o in c.pull_request.reviewers:
413 for o in c.pull_request.reviewers:
414 st = cs_statuses.get(o.user.username, None)
414 st = cs_statuses.get(o.user.username, None)
415 if st:
415 if st:
416 sorter = lambda k: k.version
416 sorter = lambda k: k.version
417 st = [(x, list(y)[0])
417 st = [(x, list(y)[0])
418 for x, y in (groupby(sorted(st, key=sorter), sorter))]
418 for x, y in (groupby(sorted(st, key=sorter), sorter))]
419 else:
419 else:
420 c.pull_request_pending_reviewers.append(o.user)
420 c.pull_request_pending_reviewers.append(o.user)
421 c.pull_request_reviewers.append([o.user, st])
421 c.pull_request_reviewers.append([o.user, st])
422
422
423 # pull_requests repo_name we opened it against
423 # pull_requests repo_name we opened it against
424 # ie. other_repo must match
424 # ie. other_repo must match
425 if repo_name != c.pull_request.other_repo.repo_name:
425 if repo_name != c.pull_request.other_repo.repo_name:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 # load compare data into template context
428 # load compare data into template context
429 enable_comments = not c.pull_request.is_closed()
429 enable_comments = not c.pull_request.is_closed()
430 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
430 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
431
431
432 # inline comments
432 # inline comments
433 c.inline_cnt = 0
433 c.inline_cnt = 0
434 c.inline_comments = cc_model.get_inline_comments(
434 c.inline_comments = cc_model.get_inline_comments(
435 c.rhodecode_db_repo.repo_id,
435 c.rhodecode_db_repo.repo_id,
436 pull_request=pull_request_id)
436 pull_request=pull_request_id)
437 # count inline comments
437 # count inline comments
438 for __, lines in c.inline_comments:
438 for __, lines in c.inline_comments:
439 for comments in lines.values():
439 for comments in lines.values():
440 c.inline_cnt += len(comments)
440 c.inline_cnt += len(comments)
441 # comments
441 # comments
442 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
442 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
443 pull_request=pull_request_id)
443 pull_request=pull_request_id)
444
444
445 # changeset(pull-request) status calulation based on reviewers
445 # (badly named) pull-request status calculation based on reviewer votes
446 c.current_changeset_status = cs_model.calculate_status(
446 c.current_changeset_status = cs_model.calculate_status(
447 c.pull_request_reviewers,
447 c.pull_request_reviewers,
448 )
448 )
449 c.changeset_statuses = ChangesetStatus.STATUSES
449 c.changeset_statuses = ChangesetStatus.STATUSES
450
450
451 c.as_form = False
451 c.as_form = False
452 c.ancestor = None # there is one - but right here we don't know which
452 c.ancestor = None # there is one - but right here we don't know which
453 return render('/pullrequests/pullrequest_show.html')
453 return render('/pullrequests/pullrequest_show.html')
454
454
455 @LoginRequired()
455 @LoginRequired()
456 @NotAnonymous()
456 @NotAnonymous()
457 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
457 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
458 'repository.admin')
458 'repository.admin')
459 @jsonify
459 @jsonify
460 def comment(self, repo_name, pull_request_id):
460 def comment(self, repo_name, pull_request_id):
461 pull_request = PullRequest.get_or_404(pull_request_id)
461 pull_request = PullRequest.get_or_404(pull_request_id)
462 if pull_request.is_closed():
462 if pull_request.is_closed():
463 raise HTTPForbidden()
463 raise HTTPForbidden()
464
464
465 status = request.POST.get('changeset_status')
465 status = request.POST.get('changeset_status')
466 change_status = request.POST.get('change_changeset_status')
466 change_status = request.POST.get('change_changeset_status')
467 text = request.POST.get('text')
467 text = request.POST.get('text')
468 close_pr = request.POST.get('save_close')
468 close_pr = request.POST.get('save_close')
469
469
470 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
470 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
471 if status and change_status and allowed_to_change_status:
471 if status and change_status and allowed_to_change_status:
472 _def = (_('Status change -> %s')
472 _def = (_('Status change -> %s')
473 % ChangesetStatus.get_status_lbl(status))
473 % ChangesetStatus.get_status_lbl(status))
474 if close_pr:
474 if close_pr:
475 _def = _('Closing with') + ' ' + _def
475 _def = _('Closing with') + ' ' + _def
476 text = text or _def
476 text = text or _def
477 comm = ChangesetCommentsModel().create(
477 comm = ChangesetCommentsModel().create(
478 text=text,
478 text=text,
479 repo=c.rhodecode_db_repo.repo_id,
479 repo=c.rhodecode_db_repo.repo_id,
480 user=c.rhodecode_user.user_id,
480 user=c.rhodecode_user.user_id,
481 pull_request=pull_request_id,
481 pull_request=pull_request_id,
482 f_path=request.POST.get('f_path'),
482 f_path=request.POST.get('f_path'),
483 line_no=request.POST.get('line'),
483 line_no=request.POST.get('line'),
484 status_change=(ChangesetStatus.get_status_lbl(status)
484 status_change=(ChangesetStatus.get_status_lbl(status)
485 if status and change_status
485 if status and change_status
486 and allowed_to_change_status else None),
486 and allowed_to_change_status else None),
487 closing_pr=close_pr
487 closing_pr=close_pr
488 )
488 )
489
489
490 action_logger(self.rhodecode_user,
490 action_logger(self.rhodecode_user,
491 'user_commented_pull_request:%s' % pull_request_id,
491 'user_commented_pull_request:%s' % pull_request_id,
492 c.rhodecode_db_repo, self.ip_addr, self.sa)
492 c.rhodecode_db_repo, self.ip_addr, self.sa)
493
493
494 if allowed_to_change_status:
494 if allowed_to_change_status:
495 # get status if set !
495 # get status if set !
496 if status and change_status:
496 if status and change_status:
497 ChangesetStatusModel().set_status(
497 ChangesetStatusModel().set_status(
498 c.rhodecode_db_repo.repo_id,
498 c.rhodecode_db_repo.repo_id,
499 status,
499 status,
500 c.rhodecode_user.user_id,
500 c.rhodecode_user.user_id,
501 comm,
501 comm,
502 pull_request=pull_request_id
502 pull_request=pull_request_id
503 )
503 )
504
504
505 if close_pr:
505 if close_pr:
506 if status in ['rejected', 'approved']:
506 if status in ['rejected', 'approved']:
507 PullRequestModel().close_pull_request(pull_request_id)
507 PullRequestModel().close_pull_request(pull_request_id)
508 action_logger(self.rhodecode_user,
508 action_logger(self.rhodecode_user,
509 'user_closed_pull_request:%s' % pull_request_id,
509 'user_closed_pull_request:%s' % pull_request_id,
510 c.rhodecode_db_repo, self.ip_addr, self.sa)
510 c.rhodecode_db_repo, self.ip_addr, self.sa)
511 else:
511 else:
512 h.flash(_('Closing pull request on other statuses than '
512 h.flash(_('Closing pull request on other statuses than '
513 'rejected or approved forbidden'),
513 'rejected or approved forbidden'),
514 category='warning')
514 category='warning')
515
515
516 Session().commit()
516 Session().commit()
517
517
518 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
518 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
519 return redirect(h.url('pullrequest_show', repo_name=repo_name,
519 return redirect(h.url('pullrequest_show', repo_name=repo_name,
520 pull_request_id=pull_request_id))
520 pull_request_id=pull_request_id))
521
521
522 data = {
522 data = {
523 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
523 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
524 }
524 }
525 if comm:
525 if comm:
526 c.co = comm
526 c.co = comm
527 data.update(comm.get_dict())
527 data.update(comm.get_dict())
528 data.update({'rendered_text':
528 data.update({'rendered_text':
529 render('changeset/changeset_comment_block.html')})
529 render('changeset/changeset_comment_block.html')})
530
530
531 return data
531 return data
532
532
533 @LoginRequired()
533 @LoginRequired()
534 @NotAnonymous()
534 @NotAnonymous()
535 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
535 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
536 'repository.admin')
536 'repository.admin')
537 @jsonify
537 @jsonify
538 def delete_comment(self, repo_name, comment_id):
538 def delete_comment(self, repo_name, comment_id):
539 co = ChangesetComment.get(comment_id)
539 co = ChangesetComment.get(comment_id)
540 if co.pull_request.is_closed():
540 if co.pull_request.is_closed():
541 #don't allow deleting comments on closed pull request
541 #don't allow deleting comments on closed pull request
542 raise HTTPForbidden()
542 raise HTTPForbidden()
543
543
544 owner = co.author.user_id == c.rhodecode_user.user_id
544 owner = co.author.user_id == c.rhodecode_user.user_id
545 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
545 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
546 ChangesetCommentsModel().delete(comment=co)
546 ChangesetCommentsModel().delete(comment=co)
547 Session().commit()
547 Session().commit()
548 return True
548 return True
549 else:
549 else:
550 raise HTTPForbidden()
550 raise HTTPForbidden()
@@ -1,193 +1,193 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.changeset_status
3 rhodecode.model.changeset_status
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6
6
7 :created_on: Apr 30, 2012
7 :created_on: Apr 30, 2012
8 :author: marcink
8 :author: marcink
9 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
9 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :license: GPLv3, see COPYING for more details.
10 :license: GPLv3, see COPYING for more details.
11 """
11 """
12 # This program is free software: you can redistribute it and/or modify
12 # This program is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
15 # (at your option) any later version.
16 #
16 #
17 # This program is distributed in the hope that it will be useful,
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
20 # GNU General Public License for more details.
21 #
21 #
22 # You should have received a copy of the GNU General Public License
22 # You should have received a copy of the GNU General Public License
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24
24
25
25
26 import logging
26 import logging
27 from collections import defaultdict
27 from collections import defaultdict
28
28
29 from rhodecode.model import BaseModel
29 from rhodecode.model import BaseModel
30 from rhodecode.model.db import ChangesetStatus, PullRequest
30 from rhodecode.model.db import ChangesetStatus, PullRequest
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class ChangesetStatusModel(BaseModel):
36 class ChangesetStatusModel(BaseModel):
37
37
38 cls = ChangesetStatus
38 cls = ChangesetStatus
39
39
40 def __get_changeset_status(self, changeset_status):
40 def __get_changeset_status(self, changeset_status):
41 return self._get_instance(ChangesetStatus, changeset_status)
41 return self._get_instance(ChangesetStatus, changeset_status)
42
42
43 def __get_pull_request(self, pull_request):
43 def __get_pull_request(self, pull_request):
44 return self._get_instance(PullRequest, pull_request)
44 return self._get_instance(PullRequest, pull_request)
45
45
46 def _get_status_query(self, repo, revision, pull_request,
46 def _get_status_query(self, repo, revision, pull_request,
47 with_revisions=False):
47 with_revisions=False):
48 repo = self._get_repo(repo)
48 repo = self._get_repo(repo)
49
49
50 q = ChangesetStatus.query()\
50 q = ChangesetStatus.query()\
51 .filter(ChangesetStatus.repo == repo)
51 .filter(ChangesetStatus.repo == repo)
52 if not with_revisions:
52 if not with_revisions:
53 q = q.filter(ChangesetStatus.version == 0)
53 q = q.filter(ChangesetStatus.version == 0)
54
54
55 if revision:
55 if revision:
56 q = q.filter(ChangesetStatus.revision == revision)
56 q = q.filter(ChangesetStatus.revision == revision)
57 elif pull_request:
57 elif pull_request:
58 pull_request = self.__get_pull_request(pull_request)
58 pull_request = self.__get_pull_request(pull_request)
59 q = q.filter(ChangesetStatus.pull_request == pull_request)
59 q = q.filter(ChangesetStatus.pull_request == pull_request)
60 else:
60 else:
61 raise Exception('Please specify revision or pull_request')
61 raise Exception('Please specify revision or pull_request')
62 q.order_by(ChangesetStatus.version.asc())
62 q.order_by(ChangesetStatus.version.asc())
63 return q
63 return q
64
64
65 def calculate_status(self, statuses_by_reviewers):
65 def calculate_status(self, statuses_by_reviewers):
66 """
66 """
67 leading one wins, if number of occurrences are equal than weaker wins
67 approved if consensus
68 (old description: leading one wins, if number of occurrences are equal than weaker wins)
68
69
69 :param statuses_by_reviewers:
70 :param statuses_by_reviewers:
70 """
71 """
71 status = None
72 votes = defaultdict(int)
72 votes = defaultdict(int)
73 reviewers_number = len(statuses_by_reviewers)
73 reviewers_number = len(statuses_by_reviewers)
74 for user, statuses in statuses_by_reviewers:
74 for user, statuses in statuses_by_reviewers:
75 if statuses:
75 if statuses:
76 ver, latest = statuses[0]
76 ver, latest = statuses[0]
77 votes[latest.status] += 1
77 votes[latest.status] += 1
78 else:
78 else:
79 votes[ChangesetStatus.DEFAULT] += 1
79 votes[ChangesetStatus.DEFAULT] += 1
80
80
81 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
81 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
82 return ChangesetStatus.STATUS_APPROVED
82 return ChangesetStatus.STATUS_APPROVED
83 else:
83 else:
84 return ChangesetStatus.STATUS_UNDER_REVIEW
84 return ChangesetStatus.STATUS_UNDER_REVIEW
85
85
86 def get_statuses(self, repo, revision=None, pull_request=None,
86 def get_statuses(self, repo, revision=None, pull_request=None,
87 with_revisions=False):
87 with_revisions=False):
88 q = self._get_status_query(repo, revision, pull_request,
88 q = self._get_status_query(repo, revision, pull_request,
89 with_revisions)
89 with_revisions)
90 return q.all()
90 return q.all()
91
91
92 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
92 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
93 """
93 """
94 Returns latest status of changeset for given revision or for given
94 Returns latest status of changeset for given revision or for given
95 pull request. Statuses are versioned inside a table itself and
95 pull request. Statuses are versioned inside a table itself and
96 version == 0 is always the current one
96 version == 0 is always the current one
97
97
98 :param repo:
98 :param repo:
99 :param revision: 40char hash or None
99 :param revision: 40char hash or None
100 :param pull_request: pull_request reference
100 :param pull_request: pull_request reference
101 :param as_str: return status as string not object
101 :param as_str: return status as string not object
102 """
102 """
103 q = self._get_status_query(repo, revision, pull_request)
103 q = self._get_status_query(repo, revision, pull_request)
104
104
105 # need to use first here since there can be multiple statuses
105 # need to use first here since there can be multiple statuses
106 # returned from pull_request
106 # returned from pull_request
107 status = q.first()
107 status = q.first()
108 if as_str:
108 if as_str:
109 status = status.status if status else status
109 status = status.status if status else status
110 st = status or ChangesetStatus.DEFAULT
110 st = status or ChangesetStatus.DEFAULT
111 return str(st)
111 return str(st)
112 return status
112 return status
113
113
114 def set_status(self, repo, status, user, comment=None, revision=None,
114 def set_status(self, repo, status, user, comment=None, revision=None,
115 pull_request=None, dont_allow_on_closed_pull_request=False):
115 pull_request=None, dont_allow_on_closed_pull_request=False):
116 """
116 """
117 Creates new status for changeset or updates the old ones bumping their
117 Creates new status for changeset or updates the old ones bumping their
118 version, leaving the current status at
118 version, leaving the current status at
119
119
120 :param repo:
120 :param repo:
121 :param revision:
121 :param revision:
122 :param status:
122 :param status:
123 :param user:
123 :param user:
124 :param comment:
124 :param comment:
125 :param dont_allow_on_closed_pull_request: don't allow a status change
125 :param dont_allow_on_closed_pull_request: don't allow a status change
126 if last status was for pull request and it's closed. We shouldn't
126 if last status was for pull request and it's closed. We shouldn't
127 mess around this manually
127 mess around this manually
128 """
128 """
129 repo = self._get_repo(repo)
129 repo = self._get_repo(repo)
130
130
131 q = ChangesetStatus.query()
131 q = ChangesetStatus.query()
132 if not comment:
132 if not comment:
133 from rhodecode.model.comment import ChangesetCommentsModel
133 from rhodecode.model.comment import ChangesetCommentsModel
134 comment = ChangesetCommentsModel().create(
134 comment = ChangesetCommentsModel().create(
135 text='Auto status change to %s' % (ChangesetStatus.get_status_lbl(status)),
135 text='Auto status change to %s' % (ChangesetStatus.get_status_lbl(status)),
136 repo=repo,
136 repo=repo,
137 user=user,
137 user=user,
138 pull_request=pull_request,
138 pull_request=pull_request,
139 send_email=False
139 send_email=False
140 )
140 )
141 if revision:
141 if revision:
142 q = q.filter(ChangesetStatus.repo == repo)
142 q = q.filter(ChangesetStatus.repo == repo)
143 q = q.filter(ChangesetStatus.revision == revision)
143 q = q.filter(ChangesetStatus.revision == revision)
144 elif pull_request:
144 elif pull_request:
145 pull_request = self.__get_pull_request(pull_request)
145 pull_request = self.__get_pull_request(pull_request)
146 q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
146 q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
147 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
147 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
148 cur_statuses = q.all()
148 cur_statuses = q.all()
149
149
150 #if statuses exists and last is associated with a closed pull request
150 #if statuses exists and last is associated with a closed pull request
151 # we need to check if we can allow this status change
151 # we need to check if we can allow this status change
152 if (dont_allow_on_closed_pull_request and cur_statuses
152 if (dont_allow_on_closed_pull_request and cur_statuses
153 and getattr(cur_statuses[0].pull_request, 'status', '')
153 and getattr(cur_statuses[0].pull_request, 'status', '')
154 == PullRequest.STATUS_CLOSED):
154 == PullRequest.STATUS_CLOSED):
155 raise StatusChangeOnClosedPullRequestError(
155 raise StatusChangeOnClosedPullRequestError(
156 'Changing status on closed pull request is not allowed'
156 'Changing status on closed pull request is not allowed'
157 )
157 )
158
158
159 #update all current statuses with older version
159 #update all current statuses with older version
160 if cur_statuses:
160 if cur_statuses:
161 for st in cur_statuses:
161 for st in cur_statuses:
162 st.version += 1
162 st.version += 1
163 self.sa.add(st)
163 self.sa.add(st)
164
164
165 def _create_status(user, repo, status, comment, revision, pull_request):
165 def _create_status(user, repo, status, comment, revision, pull_request):
166 new_status = ChangesetStatus()
166 new_status = ChangesetStatus()
167 new_status.author = self._get_user(user)
167 new_status.author = self._get_user(user)
168 new_status.repo = self._get_repo(repo)
168 new_status.repo = self._get_repo(repo)
169 new_status.status = status
169 new_status.status = status
170 new_status.comment = comment
170 new_status.comment = comment
171 new_status.revision = revision
171 new_status.revision = revision
172 new_status.pull_request = pull_request
172 new_status.pull_request = pull_request
173 return new_status
173 return new_status
174
174
175 if revision:
175 if revision:
176 new_status = _create_status(user=user, repo=repo, status=status,
176 new_status = _create_status(user=user, repo=repo, status=status,
177 comment=comment, revision=revision,
177 comment=comment, revision=revision,
178 pull_request=None)
178 pull_request=None)
179 self.sa.add(new_status)
179 self.sa.add(new_status)
180 return new_status
180 return new_status
181 elif pull_request:
181 elif pull_request:
182 #pull request can have more than one revision associated to it
182 #pull request can have more than one revision associated to it
183 #we need to create new version for each one
183 #we need to create new version for each one
184 new_statuses = []
184 new_statuses = []
185 repo = pull_request.org_repo
185 repo = pull_request.org_repo
186 for rev in pull_request.revisions:
186 for rev in pull_request.revisions:
187 new_status = _create_status(user=user, repo=repo,
187 new_status = _create_status(user=user, repo=repo,
188 status=status, comment=comment,
188 status=status, comment=comment,
189 revision=rev,
189 revision=rev,
190 pull_request=pull_request)
190 pull_request=pull_request)
191 new_statuses.append(new_status)
191 new_statuses.append(new_status)
192 self.sa.add(new_status)
192 self.sa.add(new_status)
193 return new_statuses
193 return new_statuses
@@ -1,257 +1,257 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)} &middot; ${c.rhodecode_name}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)} &middot; ${c.rhodecode_name}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('Pull request #%s') % c.pull_request.pull_request_id}
8 ${_('Pull request #%s') % c.pull_request.pull_request_id}
9 </%def>
9 </%def>
10
10
11 <%def name="page_nav()">
11 <%def name="page_nav()">
12 ${self.menu('repositories')}
12 ${self.menu('repositories')}
13 </%def>
13 </%def>
14
14
15 <%def name="main()">
15 <%def name="main()">
16 ${self.repo_context_bar('showpullrequest')}
16 ${self.repo_context_bar('showpullrequest')}
17 <div class="box">
17 <div class="box">
18 <!-- box / title -->
18 <!-- box / title -->
19 <div class="title">
19 <div class="title">
20 ${self.breadcrumbs()}
20 ${self.breadcrumbs()}
21 </div>
21 </div>
22
22
23 <h3 class="${'closed' if c.pull_request.is_closed() else ''}">
23 <h3 class="${'closed' if c.pull_request.is_closed() else ''}">
24 <img src="${h.url('/images/icons/flag_status_%s.png' % str(c.pull_request.last_review_status))}" />
24 <img src="${h.url('/images/icons/flag_status_%s.png' % str(c.pull_request.last_review_status))}" />
25 ${_('Title')}: ${c.pull_request.title}
25 ${_('Title')}: ${c.pull_request.title}
26 %if c.pull_request.is_closed():
26 %if c.pull_request.is_closed():
27 (${_('Closed')})
27 (${_('Closed')})
28 %endif
28 %endif
29 </h3>
29 </h3>
30
30
31 <div class="form">
31 <div class="form">
32 <div id="summary" class="fields">
32 <div id="summary" class="fields">
33 <div class="field">
33 <div class="field">
34 <div class="label-summary">
34 <div class="label-summary">
35 <label>${_('Review status')}:</label>
35 <label>${_('Review status')}:</label>
36 </div>
36 </div>
37 <div class="input">
37 <div class="input">
38 <div class="changeset-status-container" style="float:none;clear:both">
38 <div class="changeset-status-container" style="float:none;clear:both">
39 %if c.current_changeset_status:
39 %if c.current_changeset_status:
40 <div title="${_('Pull request status')}" class="changeset-status-lbl">
40 <div title="${_('Pull request status calculated from votes')}" class="changeset-status-lbl">
41 %if c.pull_request.is_closed():
41 %if c.pull_request.is_closed():
42 ${_('Closed')},
42 ${_('Closed')},
43 %endif
43 %endif
44 ${h.changeset_status_lbl(c.current_changeset_status)}
44 ${h.changeset_status_lbl(c.current_changeset_status)}
45 </div>
45 </div>
46 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
46 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" title="${_('Pull request status calculated from votes')}"/></div>
47 %endif
47 %endif
48 </div>
48 </div>
49 </div>
49 </div>
50 </div>
50 </div>
51 <div class="field">
51 <div class="field">
52 <div class="label-summary">
52 <div class="label-summary">
53 <label>${_('Still not reviewed by')}:</label>
53 <label>${_('Still not reviewed by')}:</label>
54 </div>
54 </div>
55 <div class="input">
55 <div class="input">
56 % if len(c.pull_request_pending_reviewers) > 0:
56 % if len(c.pull_request_pending_reviewers) > 0:
57 <div class="tooltip" title="${h.tooltip(', '.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
57 <div class="tooltip" title="${h.tooltip(', '.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
58 %else:
58 %else:
59 <div>${_('Pull request was reviewed by all reviewers')}</div>
59 <div>${_('Pull request was reviewed by all reviewers')}</div>
60 %endif
60 %endif
61 </div>
61 </div>
62 </div>
62 </div>
63 <div class="field">
63 <div class="field">
64 <div class="label-summary">
64 <div class="label-summary">
65 <label>${_('Origin repository')}:</label>
65 <label>${_('Origin repository')}:</label>
66 </div>
66 </div>
67 <div class="input">
67 <div class="input">
68 <div>
68 <div>
69 ##%if h.is_hg(c.pull_request.org_repo):
69 ##%if h.is_hg(c.pull_request.org_repo):
70 ## <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
70 ## <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
71 ##%elif h.is_git(c.pull_request.org_repo):
71 ##%elif h.is_git(c.pull_request.org_repo):
72 ## <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
72 ## <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
73 ##%endif
73 ##%endif
74 <span class="spantag">${c.pull_request.org_ref_parts[0]}: ${c.pull_request.org_ref_parts[1]}</span>
74 <span class="spantag">${c.pull_request.org_ref_parts[0]}: ${c.pull_request.org_ref_parts[1]}</span>
75 <span>
75 <span>
76 %if h.is_hg(c.pull_request.org_repo):
76 %if h.is_hg(c.pull_request.org_repo):
77 | ${_('Pull changes')} <span style="font-family: monospace">hg pull -r ${h.short_id(c.cs_ranges[-1].raw_id)} <a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
77 | ${_('Pull changes')} <span style="font-family: monospace">hg pull -r ${h.short_id(c.cs_ranges[-1].raw_id)} <a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
78 %elif h.is_git(c.pull_request.org_repo):
78 %elif h.is_git(c.pull_request.org_repo):
79 | ${_('Pull changes')}
79 | ${_('Pull changes')}
80 %endif
80 %endif
81 </div>
81 </div>
82 </div>
82 </div>
83 </div>
83 </div>
84 <div class="field">
84 <div class="field">
85 <div class="label-summary">
85 <div class="label-summary">
86 <label>${_('Description')}:</label>
86 <label>${_('Description')}:</label>
87 </div>
87 </div>
88 <div class="input">
88 <div class="input">
89 <div style="white-space:pre-wrap">${h.urlify_commit(c.pull_request.description)}</div>
89 <div style="white-space:pre-wrap">${h.urlify_commit(c.pull_request.description)}</div>
90 </div>
90 </div>
91 </div>
91 </div>
92 <div class="field">
92 <div class="field">
93 <div class="label-summary">
93 <div class="label-summary">
94 <label>${_('Created on')}:</label>
94 <label>${_('Created on')}:</label>
95 </div>
95 </div>
96 <div class="input">
96 <div class="input">
97 <div>${h.fmt_date(c.pull_request.created_on)}</div>
97 <div>${h.fmt_date(c.pull_request.created_on)}</div>
98 </div>
98 </div>
99 </div>
99 </div>
100 </div>
100 </div>
101 </div>
101 </div>
102
102
103 <div style="overflow: auto;">
103 <div style="overflow: auto;">
104 ##DIFF
104 ##DIFF
105 <div class="table" style="float:left;clear:none">
105 <div class="table" style="float:left;clear:none">
106 <div id="body" class="diffblock">
106 <div id="body" class="diffblock">
107 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
107 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
108 </div>
108 </div>
109 <div id="changeset_compare_view_content">
109 <div id="changeset_compare_view_content">
110 ##CS
110 ##CS
111 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
111 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
112 <%include file="/compare/compare_cs.html" />
112 <%include file="/compare/compare_cs.html" />
113
113
114 ## FILES
114 ## FILES
115 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
115 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
116
116
117 % if c.limited_diff:
117 % if c.limited_diff:
118 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
118 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
119 % else:
119 % else:
120 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
120 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
121 %endif
121 %endif
122
122
123 </div>
123 </div>
124 <div class="cs_files">
124 <div class="cs_files">
125 %if not c.files:
125 %if not c.files:
126 <span class="empty_data">${_('No files')}</span>
126 <span class="empty_data">${_('No files')}</span>
127 %endif
127 %endif
128 %for fid, change, f, stat in c.files:
128 %for fid, change, f, stat in c.files:
129 <div class="cs_${change}">
129 <div class="cs_${change}">
130 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
130 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
131 <div class="changes">${h.fancy_file_stats(stat)}</div>
131 <div class="changes">${h.fancy_file_stats(stat)}</div>
132 </div>
132 </div>
133 %endfor
133 %endfor
134 </div>
134 </div>
135 % if c.limited_diff:
135 % if c.limited_diff:
136 <h5>${_('Changeset 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>
136 <h5>${_('Changeset 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>
137 % endif
137 % endif
138 </div>
138 </div>
139 </div>
139 </div>
140 ## REVIEWERS
140 ## REVIEWERS
141 <div style="float:left; border-left:1px dashed #eee">
141 <div style="float:left; border-left:1px dashed #eee">
142 <h4>${_('Pull request reviewers')}</h4>
142 <h4>${_('Pull request reviewers')}</h4>
143 <div id="reviewers" style="padding:0px 0px 5px 10px">
143 <div id="reviewers" style="padding:0px 0px 5px 10px">
144 ## members goes here !
144 ## members goes here !
145 <div class="group_members_wrap" style="min-height:45px">
145 <div class="group_members_wrap" style="min-height:45px">
146 <ul id="review_members" class="group_members">
146 <ul id="review_members" class="group_members">
147 %for member,status in c.pull_request_reviewers:
147 %for member,status in c.pull_request_reviewers:
148 <li id="reviewer_${member.user_id}">
148 <li id="reviewer_${member.user_id}">
149 <div class="reviewers_member">
149 <div class="reviewers_member">
150 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
150 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
151 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
151 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
152 </div>
152 </div>
153 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
153 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
154 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
154 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
155 <input type="hidden" value="${member.user_id}" name="review_members" />
155 <input type="hidden" value="${member.user_id}" name="review_members" />
156 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.user_id == c.rhodecode_user.user_id):
156 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.user_id == c.rhodecode_user.user_id):
157 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
157 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
158 %endif
158 %endif
159 </div>
159 </div>
160 </li>
160 </li>
161 %endfor
161 %endfor
162 </ul>
162 </ul>
163 </div>
163 </div>
164 %if not c.pull_request.is_closed():
164 %if not c.pull_request.is_closed():
165 <div class='ac'>
165 <div class='ac'>
166 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
166 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
167 <div class="reviewer_ac">
167 <div class="reviewer_ac">
168 ${h.text('user', class_='yui-ac-input')}
168 ${h.text('user', class_='yui-ac-input')}
169 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
169 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
170 <div id="reviewers_container"></div>
170 <div id="reviewers_container"></div>
171 </div>
171 </div>
172 <div style="padding:0px 10px">
172 <div style="padding:0px 10px">
173 <span id="update_pull_request" class="ui-btn xsmall">${_('Save changes')}</span>
173 <span id="update_pull_request" class="ui-btn xsmall">${_('Save changes')}</span>
174 </div>
174 </div>
175 %endif
175 %endif
176 </div>
176 </div>
177 %endif
177 %endif
178 </div>
178 </div>
179 </div>
179 </div>
180 </div>
180 </div>
181 <script>
181 <script>
182 var _USERS_AC_DATA = ${c.users_array|n};
182 var _USERS_AC_DATA = ${c.users_array|n};
183 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
183 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
184 // TODO: switch this to pyroutes
184 // TODO: switch this to pyroutes
185 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
185 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
186 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
186 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
187
187
188 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
188 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
189 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
189 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
190 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
190 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
191
191
192 </script>
192 </script>
193
193
194 ## diff block
194 ## diff block
195 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
195 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
196 %for fid, change, f, stat in c.files:
196 %for fid, change, f, stat in c.files:
197 ${diff_block.diff_block_simple([c.changes[fid]])}
197 ${diff_block.diff_block_simple([c.changes[fid]])}
198 %endfor
198 %endfor
199 % if c.limited_diff:
199 % if c.limited_diff:
200 <h4>${_('Changeset 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></h4>
200 <h4>${_('Changeset 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></h4>
201 % endif
201 % endif
202
202
203
203
204 ## template for inline comment form
204 ## template for inline comment form
205 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
205 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
206 ${comment.comment_inline_form()}
206 ${comment.comment_inline_form()}
207
207
208 ## render comments and inlines
208 ## render comments and inlines
209 ${comment.generate_comments(include_pr=True)}
209 ${comment.generate_comments(include_pr=True)}
210
210
211 % if not c.pull_request.is_closed():
211 % if not c.pull_request.is_closed():
212 ## main comment form and it status
212 ## main comment form and it status
213 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
213 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
214 pull_request_id=c.pull_request.pull_request_id),
214 pull_request_id=c.pull_request.pull_request_id),
215 c.current_changeset_status,
215 c.current_changeset_status,
216 is_pr=True, change_status=c.allowed_to_change_status)}
216 is_pr=True, change_status=c.allowed_to_change_status)}
217 %endif
217 %endif
218
218
219 <script type="text/javascript">
219 <script type="text/javascript">
220 YUE.onDOMReady(function(){
220 YUE.onDOMReady(function(){
221 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
221 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
222
222
223 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
223 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
224 var show = 'none';
224 var show = 'none';
225 var target = e.currentTarget;
225 var target = e.currentTarget;
226 if(target.checked){
226 if(target.checked){
227 var show = ''
227 var show = ''
228 }
228 }
229 var boxid = YUD.getAttribute(target,'id_for');
229 var boxid = YUD.getAttribute(target,'id_for');
230 var comments = YUQ('#{0} .inline-comments'.format(boxid));
230 var comments = YUQ('#{0} .inline-comments'.format(boxid));
231 for(c in comments){
231 for(c in comments){
232 YUD.setStyle(comments[c],'display',show);
232 YUD.setStyle(comments[c],'display',show);
233 }
233 }
234 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
234 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
235 for(c in btns){
235 for(c in btns){
236 YUD.setStyle(btns[c],'display',show);
236 YUD.setStyle(btns[c],'display',show);
237 }
237 }
238 })
238 })
239
239
240 YUE.on(YUQ('.line'),'click',function(e){
240 YUE.on(YUQ('.line'),'click',function(e){
241 var tr = e.currentTarget;
241 var tr = e.currentTarget;
242 injectInlineForm(tr);
242 injectInlineForm(tr);
243 });
243 });
244
244
245 // inject comments into they proper positions
245 // inject comments into they proper positions
246 var file_comments = YUQ('.inline-comment-placeholder');
246 var file_comments = YUQ('.inline-comment-placeholder');
247 renderInlineComments(file_comments);
247 renderInlineComments(file_comments);
248
248
249 YUE.on(YUD.get('update_pull_request'),'click',function(e){
249 YUE.on(YUD.get('update_pull_request'),'click',function(e){
250 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
250 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
251 })
251 })
252 })
252 })
253 </script>
253 </script>
254
254
255 </div>
255 </div>
256
256
257 </%def>
257 </%def>
General Comments 0
You need to be logged in to leave comments. Login now