##// END OF EJS Templates
py3: replace list comprehension with for-loop due to scope...
Lars Kruse -
r6788:bfd7fc9a default
parent child Browse files
Show More
@@ -1,746 +1,747 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.pullrequests
15 kallithea.controllers.pullrequests
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pull requests controller for Kallithea for initializing pull requests
18 pull requests controller for Kallithea for initializing pull requests
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: May 7, 2012
22 :created_on: May 7, 2012
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31
31
32 from tg import request, tmpl_context as c
32 from tg import request, tmpl_context as c
33 from tg.i18n import ugettext as _
33 from tg.i18n import ugettext as _
34 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
34 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
35
35
36 from kallithea.config.routing import url
36 from kallithea.config.routing import url
37 from kallithea.lib import helpers as h
37 from kallithea.lib import helpers as h
38 from kallithea.lib import diffs
38 from kallithea.lib import diffs
39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator, \
39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator, \
40 NotAnonymous
40 NotAnonymous
41 from kallithea.lib.base import BaseRepoController, render, jsonify
41 from kallithea.lib.base import BaseRepoController, render, jsonify
42 from kallithea.lib.diffs import LimitedDiffContainer
42 from kallithea.lib.diffs import LimitedDiffContainer
43 from kallithea.lib.page import Page
43 from kallithea.lib.page import Page
44 from kallithea.lib.utils import action_logger
44 from kallithea.lib.utils import action_logger
45 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
45 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
46 from kallithea.lib.vcs.utils import safe_str
46 from kallithea.lib.vcs.utils import safe_str
47 from kallithea.lib.vcs.utils.hgcompat import unionrepo
47 from kallithea.lib.vcs.utils.hgcompat import unionrepo
48 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
48 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
49 PullRequestReviewer, Repository, User
49 PullRequestReviewer, Repository, User
50 from kallithea.model.pull_request import CreatePullRequestAction, CreatePullRequestIterationAction, PullRequestModel
50 from kallithea.model.pull_request import CreatePullRequestAction, CreatePullRequestIterationAction, PullRequestModel
51 from kallithea.model.meta import Session
51 from kallithea.model.meta import Session
52 from kallithea.model.repo import RepoModel
52 from kallithea.model.repo import RepoModel
53 from kallithea.model.comment import ChangesetCommentsModel
53 from kallithea.model.comment import ChangesetCommentsModel
54 from kallithea.model.changeset_status import ChangesetStatusModel
54 from kallithea.model.changeset_status import ChangesetStatusModel
55 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
55 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
56 from kallithea.lib.utils2 import safe_int
56 from kallithea.lib.utils2 import safe_int
57 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
57 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
58 create_comment
58 create_comment
59 from kallithea.controllers.compare import CompareController
59 from kallithea.controllers.compare import CompareController
60 from kallithea.lib.graphmod import graph_data
60 from kallithea.lib.graphmod import graph_data
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 def _get_reviewer(user_id):
65 def _get_reviewer(user_id):
66 """Look up user by ID and validate it as a potential reviewer."""
66 """Look up user by ID and validate it as a potential reviewer."""
67 try:
67 try:
68 user = User.get(int(user_id))
68 user = User.get(int(user_id))
69 except ValueError:
69 except ValueError:
70 user = None
70 user = None
71
71
72 if user is None or user.is_default_user:
72 if user is None or user.is_default_user:
73 h.flash(_('Invalid reviewer "%s" specified') % user_id, category='error')
73 h.flash(_('Invalid reviewer "%s" specified') % user_id, category='error')
74 raise HTTPBadRequest()
74 raise HTTPBadRequest()
75
75
76 return user
76 return user
77
77
78
78
79 class PullrequestsController(BaseRepoController):
79 class PullrequestsController(BaseRepoController):
80
80
81 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
81 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
82 """return a structure with repo's interesting changesets, suitable for
82 """return a structure with repo's interesting changesets, suitable for
83 the selectors in pullrequest.html
83 the selectors in pullrequest.html
84
84
85 rev: a revision that must be in the list somehow and selected by default
85 rev: a revision that must be in the list somehow and selected by default
86 branch: a branch that must be in the list and selected by default - even if closed
86 branch: a branch that must be in the list and selected by default - even if closed
87 branch_rev: a revision of which peers should be preferred and available."""
87 branch_rev: a revision of which peers should be preferred and available."""
88 # list named branches that has been merged to this named branch - it should probably merge back
88 # list named branches that has been merged to this named branch - it should probably merge back
89 peers = []
89 peers = []
90
90
91 if rev:
91 if rev:
92 rev = safe_str(rev)
92 rev = safe_str(rev)
93
93
94 if branch:
94 if branch:
95 branch = safe_str(branch)
95 branch = safe_str(branch)
96
96
97 if branch_rev:
97 if branch_rev:
98 branch_rev = safe_str(branch_rev)
98 branch_rev = safe_str(branch_rev)
99 # a revset not restricting to merge() would be better
99 # a revset not restricting to merge() would be better
100 # (especially because it would get the branch point)
100 # (especially because it would get the branch point)
101 # ... but is currently too expensive
101 # ... but is currently too expensive
102 # including branches of children could be nice too
102 # including branches of children could be nice too
103 peerbranches = set()
103 peerbranches = set()
104 for i in repo._repo.revs(
104 for i in repo._repo.revs(
105 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
105 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
106 branch_rev, branch_rev):
106 branch_rev, branch_rev):
107 abranch = repo.get_changeset(i).branch
107 abranch = repo.get_changeset(i).branch
108 if abranch not in peerbranches:
108 if abranch not in peerbranches:
109 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
109 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
110 peers.append((n, abranch))
110 peers.append((n, abranch))
111 peerbranches.add(abranch)
111 peerbranches.add(abranch)
112
112
113 selected = None
113 selected = None
114 tiprev = repo.tags.get('tip')
114 tiprev = repo.tags.get('tip')
115 tipbranch = None
115 tipbranch = None
116
116
117 branches = []
117 branches = []
118 for abranch, branchrev in repo.branches.iteritems():
118 for abranch, branchrev in repo.branches.iteritems():
119 n = 'branch:%s:%s' % (abranch, branchrev)
119 n = 'branch:%s:%s' % (abranch, branchrev)
120 desc = abranch
120 desc = abranch
121 if branchrev == tiprev:
121 if branchrev == tiprev:
122 tipbranch = abranch
122 tipbranch = abranch
123 desc = '%s (current tip)' % desc
123 desc = '%s (current tip)' % desc
124 branches.append((n, desc))
124 branches.append((n, desc))
125 if rev == branchrev:
125 if rev == branchrev:
126 selected = n
126 selected = n
127 if branch == abranch:
127 if branch == abranch:
128 if not rev:
128 if not rev:
129 selected = n
129 selected = n
130 branch = None
130 branch = None
131 if branch: # branch not in list - it is probably closed
131 if branch: # branch not in list - it is probably closed
132 branchrev = repo.closed_branches.get(branch)
132 branchrev = repo.closed_branches.get(branch)
133 if branchrev:
133 if branchrev:
134 n = 'branch:%s:%s' % (branch, branchrev)
134 n = 'branch:%s:%s' % (branch, branchrev)
135 branches.append((n, _('%s (closed)') % branch))
135 branches.append((n, _('%s (closed)') % branch))
136 selected = n
136 selected = n
137 branch = None
137 branch = None
138 if branch:
138 if branch:
139 log.debug('branch %r not found in %s', branch, repo)
139 log.debug('branch %r not found in %s', branch, repo)
140
140
141 bookmarks = []
141 bookmarks = []
142 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
142 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
143 n = 'book:%s:%s' % (bookmark, bookmarkrev)
143 n = 'book:%s:%s' % (bookmark, bookmarkrev)
144 bookmarks.append((n, bookmark))
144 bookmarks.append((n, bookmark))
145 if rev == bookmarkrev:
145 if rev == bookmarkrev:
146 selected = n
146 selected = n
147
147
148 tags = []
148 tags = []
149 for tag, tagrev in repo.tags.iteritems():
149 for tag, tagrev in repo.tags.iteritems():
150 if tag == 'tip':
150 if tag == 'tip':
151 continue
151 continue
152 n = 'tag:%s:%s' % (tag, tagrev)
152 n = 'tag:%s:%s' % (tag, tagrev)
153 tags.append((n, tag))
153 tags.append((n, tag))
154 # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
154 # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
155
155
156 # prio 1: rev was selected as existing entry above
156 # prio 1: rev was selected as existing entry above
157
157
158 # prio 2: create special entry for rev; rev _must_ be used
158 # prio 2: create special entry for rev; rev _must_ be used
159 specials = []
159 specials = []
160 if rev and selected is None:
160 if rev and selected is None:
161 selected = 'rev:%s:%s' % (rev, rev)
161 selected = 'rev:%s:%s' % (rev, rev)
162 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
162 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
163
163
164 # prio 3: most recent peer branch
164 # prio 3: most recent peer branch
165 if peers and not selected:
165 if peers and not selected:
166 selected = peers[0][0]
166 selected = peers[0][0]
167
167
168 # prio 4: tip revision
168 # prio 4: tip revision
169 if not selected:
169 if not selected:
170 if h.is_hg(repo):
170 if h.is_hg(repo):
171 if tipbranch:
171 if tipbranch:
172 selected = 'branch:%s:%s' % (tipbranch, tiprev)
172 selected = 'branch:%s:%s' % (tipbranch, tiprev)
173 else:
173 else:
174 selected = 'tag:null:' + repo.EMPTY_CHANGESET
174 selected = 'tag:null:' + repo.EMPTY_CHANGESET
175 tags.append((selected, 'null'))
175 tags.append((selected, 'null'))
176 else:
176 else:
177 if 'master' in repo.branches:
177 if 'master' in repo.branches:
178 selected = 'branch:master:%s' % repo.branches['master']
178 selected = 'branch:master:%s' % repo.branches['master']
179 else:
179 else:
180 k, v = repo.branches.items()[0]
180 k, v = repo.branches.items()[0]
181 selected = 'branch:%s:%s' % (k, v)
181 selected = 'branch:%s:%s' % (k, v)
182
182
183 groups = [(specials, _("Special")),
183 groups = [(specials, _("Special")),
184 (peers, _("Peer branches")),
184 (peers, _("Peer branches")),
185 (bookmarks, _("Bookmarks")),
185 (bookmarks, _("Bookmarks")),
186 (branches, _("Branches")),
186 (branches, _("Branches")),
187 (tags, _("Tags")),
187 (tags, _("Tags")),
188 ]
188 ]
189 return [g for g in groups if g[0]], selected
189 return [g for g in groups if g[0]], selected
190
190
191 def _get_is_allowed_change_status(self, pull_request):
191 def _get_is_allowed_change_status(self, pull_request):
192 if pull_request.is_closed():
192 if pull_request.is_closed():
193 return False
193 return False
194
194
195 owner = request.authuser.user_id == pull_request.owner_id
195 owner = request.authuser.user_id == pull_request.owner_id
196 reviewer = PullRequestReviewer.query() \
196 reviewer = PullRequestReviewer.query() \
197 .filter(PullRequestReviewer.pull_request == pull_request) \
197 .filter(PullRequestReviewer.pull_request == pull_request) \
198 .filter(PullRequestReviewer.user_id == request.authuser.user_id) \
198 .filter(PullRequestReviewer.user_id == request.authuser.user_id) \
199 .count() != 0
199 .count() != 0
200
200
201 return request.authuser.admin or owner or reviewer
201 return request.authuser.admin or owner or reviewer
202
202
203 @LoginRequired()
203 @LoginRequired()
204 @HasRepoPermissionLevelDecorator('read')
204 @HasRepoPermissionLevelDecorator('read')
205 def show_all(self, repo_name):
205 def show_all(self, repo_name):
206 c.from_ = request.GET.get('from_') or ''
206 c.from_ = request.GET.get('from_') or ''
207 c.closed = request.GET.get('closed') or ''
207 c.closed = request.GET.get('closed') or ''
208 p = safe_int(request.GET.get('page'), 1)
208 p = safe_int(request.GET.get('page'), 1)
209
209
210 q = PullRequest.query(include_closed=c.closed, sorted=True)
210 q = PullRequest.query(include_closed=c.closed, sorted=True)
211 if c.from_:
211 if c.from_:
212 q = q.filter_by(org_repo=c.db_repo)
212 q = q.filter_by(org_repo=c.db_repo)
213 else:
213 else:
214 q = q.filter_by(other_repo=c.db_repo)
214 q = q.filter_by(other_repo=c.db_repo)
215 c.pull_requests = q.all()
215 c.pull_requests = q.all()
216
216
217 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
217 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
218
218
219 return render('/pullrequests/pullrequest_show_all.html')
219 return render('/pullrequests/pullrequest_show_all.html')
220
220
221 @LoginRequired()
221 @LoginRequired()
222 @NotAnonymous()
222 @NotAnonymous()
223 def show_my(self):
223 def show_my(self):
224 c.closed = request.GET.get('closed') or ''
224 c.closed = request.GET.get('closed') or ''
225
225
226 c.my_pull_requests = PullRequest.query(
226 c.my_pull_requests = PullRequest.query(
227 include_closed=c.closed,
227 include_closed=c.closed,
228 sorted=True,
228 sorted=True,
229 ).filter_by(owner_id=request.authuser.user_id).all()
229 ).filter_by(owner_id=request.authuser.user_id).all()
230
230
231 c.participate_in_pull_requests = []
231 c.participate_in_pull_requests = []
232 c.participate_in_pull_requests_todo = []
232 c.participate_in_pull_requests_todo = []
233 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
233 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
234 for pr in PullRequest.query(
234 for pr in PullRequest.query(
235 include_closed=c.closed,
235 include_closed=c.closed,
236 reviewer_id=request.authuser.user_id,
236 reviewer_id=request.authuser.user_id,
237 sorted=True,
237 sorted=True,
238 ):
238 ):
239 status = pr.user_review_status(request.authuser.user_id) # very inefficient!!!
239 status = pr.user_review_status(request.authuser.user_id) # very inefficient!!!
240 if status in done_status:
240 if status in done_status:
241 c.participate_in_pull_requests.append(pr)
241 c.participate_in_pull_requests.append(pr)
242 else:
242 else:
243 c.participate_in_pull_requests_todo.append(pr)
243 c.participate_in_pull_requests_todo.append(pr)
244
244
245 return render('/pullrequests/pullrequest_show_my.html')
245 return render('/pullrequests/pullrequest_show_my.html')
246
246
247 @LoginRequired()
247 @LoginRequired()
248 @NotAnonymous()
248 @NotAnonymous()
249 @HasRepoPermissionLevelDecorator('read')
249 @HasRepoPermissionLevelDecorator('read')
250 def index(self):
250 def index(self):
251 org_repo = c.db_repo
251 org_repo = c.db_repo
252 org_scm_instance = org_repo.scm_instance
252 org_scm_instance = org_repo.scm_instance
253 try:
253 try:
254 org_scm_instance.get_changeset()
254 org_scm_instance.get_changeset()
255 except EmptyRepositoryError as e:
255 except EmptyRepositoryError as e:
256 h.flash(h.literal(_('There are no changesets yet')),
256 h.flash(h.literal(_('There are no changesets yet')),
257 category='warning')
257 category='warning')
258 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
258 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
259
259
260 org_rev = request.GET.get('rev_end')
260 org_rev = request.GET.get('rev_end')
261 # rev_start is not directly useful - its parent could however be used
261 # rev_start is not directly useful - its parent could however be used
262 # as default for other and thus give a simple compare view
262 # as default for other and thus give a simple compare view
263 rev_start = request.GET.get('rev_start')
263 rev_start = request.GET.get('rev_start')
264 other_rev = None
264 other_rev = None
265 if rev_start:
265 if rev_start:
266 starters = org_repo.get_changeset(rev_start).parents
266 starters = org_repo.get_changeset(rev_start).parents
267 if starters:
267 if starters:
268 other_rev = starters[0].raw_id
268 other_rev = starters[0].raw_id
269 else:
269 else:
270 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
270 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
271 branch = request.GET.get('branch')
271 branch = request.GET.get('branch')
272
272
273 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
273 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
274 c.default_cs_repo = org_repo.repo_name
274 c.default_cs_repo = org_repo.repo_name
275 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
275 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
276
276
277 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
277 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
278 if default_cs_ref_type != 'branch':
278 if default_cs_ref_type != 'branch':
279 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
279 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
280
280
281 # add org repo to other so we can open pull request against peer branches on itself
281 # add org repo to other so we can open pull request against peer branches on itself
282 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
282 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
283
283
284 if org_repo.parent:
284 if org_repo.parent:
285 # add parent of this fork also and select it.
285 # add parent of this fork also and select it.
286 # use the same branch on destination as on source, if available.
286 # use the same branch on destination as on source, if available.
287 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
287 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
288 c.a_repo = org_repo.parent
288 c.a_repo = org_repo.parent
289 c.a_refs, c.default_a_ref = self._get_repo_refs(
289 c.a_refs, c.default_a_ref = self._get_repo_refs(
290 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
290 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
291
291
292 else:
292 else:
293 c.a_repo = org_repo
293 c.a_repo = org_repo
294 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
294 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
295
295
296 # gather forks and add to this list ... even though it is rare to
296 # gather forks and add to this list ... even though it is rare to
297 # request forks to pull from their parent
297 # request forks to pull from their parent
298 for fork in org_repo.forks:
298 for fork in org_repo.forks:
299 c.a_repos.append((fork.repo_name, fork.repo_name))
299 c.a_repos.append((fork.repo_name, fork.repo_name))
300
300
301 return render('/pullrequests/pullrequest.html')
301 return render('/pullrequests/pullrequest.html')
302
302
303 @LoginRequired()
303 @LoginRequired()
304 @NotAnonymous()
304 @NotAnonymous()
305 @HasRepoPermissionLevelDecorator('read')
305 @HasRepoPermissionLevelDecorator('read')
306 @jsonify
306 @jsonify
307 def repo_info(self, repo_name):
307 def repo_info(self, repo_name):
308 repo = c.db_repo
308 repo = c.db_repo
309 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
309 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
310 return {
310 return {
311 'description': repo.description.split('\n', 1)[0],
311 'description': repo.description.split('\n', 1)[0],
312 'selected_ref': selected_ref,
312 'selected_ref': selected_ref,
313 'refs': refs,
313 'refs': refs,
314 }
314 }
315
315
316 @LoginRequired()
316 @LoginRequired()
317 @NotAnonymous()
317 @NotAnonymous()
318 @HasRepoPermissionLevelDecorator('read')
318 @HasRepoPermissionLevelDecorator('read')
319 def create(self, repo_name):
319 def create(self, repo_name):
320 repo = c.db_repo
320 repo = c.db_repo
321 try:
321 try:
322 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
322 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
323 except formencode.Invalid as errors:
323 except formencode.Invalid as errors:
324 log.error(traceback.format_exc())
324 log.error(traceback.format_exc())
325 log.error(str(errors))
325 log.error(str(errors))
326 msg = _('Error creating pull request: %s') % errors.msg
326 msg = _('Error creating pull request: %s') % errors.msg
327 h.flash(msg, 'error')
327 h.flash(msg, 'error')
328 raise HTTPBadRequest
328 raise HTTPBadRequest
329
329
330 # heads up: org and other might seem backward here ...
330 # heads up: org and other might seem backward here ...
331 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
331 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
332 org_repo = Repository.guess_instance(_form['org_repo'])
332 org_repo = Repository.guess_instance(_form['org_repo'])
333
333
334 other_ref = _form['other_ref'] # will have symbolic name and head revision
334 other_ref = _form['other_ref'] # will have symbolic name and head revision
335 other_repo = Repository.guess_instance(_form['other_repo'])
335 other_repo = Repository.guess_instance(_form['other_repo'])
336
336
337 reviewers = []
337 reviewers = []
338
338
339 title = _form['pullrequest_title']
339 title = _form['pullrequest_title']
340 description = _form['pullrequest_desc'].strip()
340 description = _form['pullrequest_desc'].strip()
341 owner = User.get(request.authuser.user_id)
341 owner = User.get(request.authuser.user_id)
342
342
343 try:
343 try:
344 cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers)
344 cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers)
345 except CreatePullRequestAction.ValidationError as e:
345 except CreatePullRequestAction.ValidationError as e:
346 h.flash(str(e), category='error', logf=log.error)
346 h.flash(str(e), category='error', logf=log.error)
347 raise HTTPNotFound
347 raise HTTPNotFound
348
348
349 try:
349 try:
350 pull_request = cmd.execute()
350 pull_request = cmd.execute()
351 Session().commit()
351 Session().commit()
352 except Exception:
352 except Exception:
353 h.flash(_('Error occurred while creating pull request'),
353 h.flash(_('Error occurred while creating pull request'),
354 category='error')
354 category='error')
355 log.error(traceback.format_exc())
355 log.error(traceback.format_exc())
356 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
356 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
357
357
358 h.flash(_('Successfully opened new pull request'),
358 h.flash(_('Successfully opened new pull request'),
359 category='success')
359 category='success')
360 raise HTTPFound(location=pull_request.url())
360 raise HTTPFound(location=pull_request.url())
361
361
362 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers):
362 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers):
363 owner = User.get(request.authuser.user_id)
363 owner = User.get(request.authuser.user_id)
364 new_org_rev = self._get_ref_rev(old_pull_request.org_repo, 'rev', new_rev)
364 new_org_rev = self._get_ref_rev(old_pull_request.org_repo, 'rev', new_rev)
365 new_other_rev = self._get_ref_rev(old_pull_request.other_repo, old_pull_request.other_ref_parts[0], old_pull_request.other_ref_parts[1])
365 new_other_rev = self._get_ref_rev(old_pull_request.other_repo, old_pull_request.other_ref_parts[0], old_pull_request.other_ref_parts[1])
366 try:
366 try:
367 cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers)
367 cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers)
368 except CreatePullRequestAction.ValidationError as e:
368 except CreatePullRequestAction.ValidationError as e:
369 h.flash(str(e), category='error', logf=log.error)
369 h.flash(str(e), category='error', logf=log.error)
370 raise HTTPNotFound
370 raise HTTPNotFound
371
371
372 try:
372 try:
373 pull_request = cmd.execute()
373 pull_request = cmd.execute()
374 Session().commit()
374 Session().commit()
375 except Exception:
375 except Exception:
376 h.flash(_('Error occurred while creating pull request'),
376 h.flash(_('Error occurred while creating pull request'),
377 category='error')
377 category='error')
378 log.error(traceback.format_exc())
378 log.error(traceback.format_exc())
379 raise HTTPFound(location=old_pull_request.url())
379 raise HTTPFound(location=old_pull_request.url())
380
380
381 h.flash(_('New pull request iteration created'),
381 h.flash(_('New pull request iteration created'),
382 category='success')
382 category='success')
383 raise HTTPFound(location=pull_request.url())
383 raise HTTPFound(location=pull_request.url())
384
384
385 # pullrequest_post for PR editing
385 # pullrequest_post for PR editing
386 @LoginRequired()
386 @LoginRequired()
387 @NotAnonymous()
387 @NotAnonymous()
388 @HasRepoPermissionLevelDecorator('read')
388 @HasRepoPermissionLevelDecorator('read')
389 def post(self, repo_name, pull_request_id):
389 def post(self, repo_name, pull_request_id):
390 pull_request = PullRequest.get_or_404(pull_request_id)
390 pull_request = PullRequest.get_or_404(pull_request_id)
391 if pull_request.is_closed():
391 if pull_request.is_closed():
392 raise HTTPForbidden()
392 raise HTTPForbidden()
393 assert pull_request.other_repo.repo_name == repo_name
393 assert pull_request.other_repo.repo_name == repo_name
394 #only owner or admin can update it
394 #only owner or admin can update it
395 owner = pull_request.owner_id == request.authuser.user_id
395 owner = pull_request.owner_id == request.authuser.user_id
396 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
396 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
397 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
397 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
398 raise HTTPForbidden()
398 raise HTTPForbidden()
399
399
400 _form = PullRequestPostForm()().to_python(request.POST)
400 _form = PullRequestPostForm()().to_python(request.POST)
401
401
402 cur_reviewers = set(pull_request.get_reviewer_users())
402 cur_reviewers = set(pull_request.get_reviewer_users())
403 new_reviewers = set(_get_reviewer(s) for s in _form['review_members'])
403 new_reviewers = set(_get_reviewer(s) for s in _form['review_members'])
404 old_reviewers = set(_get_reviewer(s) for s in _form['org_review_members'])
404 old_reviewers = set(_get_reviewer(s) for s in _form['org_review_members'])
405
405
406 other_added = cur_reviewers - old_reviewers
406 other_added = cur_reviewers - old_reviewers
407 other_removed = old_reviewers - cur_reviewers
407 other_removed = old_reviewers - cur_reviewers
408
408
409 if other_added:
409 if other_added:
410 h.flash(_('Meanwhile, the following reviewers have been added: %s') %
410 h.flash(_('Meanwhile, the following reviewers have been added: %s') %
411 (', '.join(u.username for u in other_added)),
411 (', '.join(u.username for u in other_added)),
412 category='warning')
412 category='warning')
413 if other_removed:
413 if other_removed:
414 h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
414 h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
415 (', '.join(u.username for u in other_removed)),
415 (', '.join(u.username for u in other_removed)),
416 category='warning')
416 category='warning')
417
417
418 if _form['updaterev']:
418 if _form['updaterev']:
419 return self.create_new_iteration(pull_request,
419 return self.create_new_iteration(pull_request,
420 _form['updaterev'],
420 _form['updaterev'],
421 _form['pullrequest_title'],
421 _form['pullrequest_title'],
422 _form['pullrequest_desc'],
422 _form['pullrequest_desc'],
423 new_reviewers)
423 new_reviewers)
424
424
425 added_reviewers = new_reviewers - old_reviewers - cur_reviewers
425 added_reviewers = new_reviewers - old_reviewers - cur_reviewers
426 removed_reviewers = (old_reviewers - new_reviewers) & cur_reviewers
426 removed_reviewers = (old_reviewers - new_reviewers) & cur_reviewers
427
427
428 old_description = pull_request.description
428 old_description = pull_request.description
429 pull_request.title = _form['pullrequest_title']
429 pull_request.title = _form['pullrequest_title']
430 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
430 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
431 pull_request.owner = User.get_by_username(_form['owner'])
431 pull_request.owner = User.get_by_username(_form['owner'])
432 user = User.get(request.authuser.user_id)
432 user = User.get(request.authuser.user_id)
433
433
434 PullRequestModel().mention_from_description(user, pull_request, old_description)
434 PullRequestModel().mention_from_description(user, pull_request, old_description)
435 PullRequestModel().add_reviewers(user, pull_request, added_reviewers)
435 PullRequestModel().add_reviewers(user, pull_request, added_reviewers)
436 PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers)
436 PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers)
437
437
438 Session().commit()
438 Session().commit()
439 h.flash(_('Pull request updated'), category='success')
439 h.flash(_('Pull request updated'), category='success')
440
440
441 raise HTTPFound(location=pull_request.url())
441 raise HTTPFound(location=pull_request.url())
442
442
443 @LoginRequired()
443 @LoginRequired()
444 @NotAnonymous()
444 @NotAnonymous()
445 @HasRepoPermissionLevelDecorator('read')
445 @HasRepoPermissionLevelDecorator('read')
446 @jsonify
446 @jsonify
447 def delete(self, repo_name, pull_request_id):
447 def delete(self, repo_name, pull_request_id):
448 pull_request = PullRequest.get_or_404(pull_request_id)
448 pull_request = PullRequest.get_or_404(pull_request_id)
449 #only owner can delete it !
449 #only owner can delete it !
450 if pull_request.owner_id == request.authuser.user_id:
450 if pull_request.owner_id == request.authuser.user_id:
451 PullRequestModel().delete(pull_request)
451 PullRequestModel().delete(pull_request)
452 Session().commit()
452 Session().commit()
453 h.flash(_('Successfully deleted pull request'),
453 h.flash(_('Successfully deleted pull request'),
454 category='success')
454 category='success')
455 raise HTTPFound(location=url('my_pullrequests'))
455 raise HTTPFound(location=url('my_pullrequests'))
456 raise HTTPForbidden()
456 raise HTTPForbidden()
457
457
458 @LoginRequired()
458 @LoginRequired()
459 @HasRepoPermissionLevelDecorator('read')
459 @HasRepoPermissionLevelDecorator('read')
460 def show(self, repo_name, pull_request_id, extra=None):
460 def show(self, repo_name, pull_request_id, extra=None):
461 repo_model = RepoModel()
461 repo_model = RepoModel()
462 c.users_array = repo_model.get_users_js()
462 c.users_array = repo_model.get_users_js()
463 c.user_groups_array = repo_model.get_user_groups_js()
463 c.user_groups_array = repo_model.get_user_groups_js()
464 c.pull_request = PullRequest.get_or_404(pull_request_id)
464 c.pull_request = PullRequest.get_or_404(pull_request_id)
465 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
465 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
466 cc_model = ChangesetCommentsModel()
466 cc_model = ChangesetCommentsModel()
467 cs_model = ChangesetStatusModel()
467 cs_model = ChangesetStatusModel()
468
468
469 # pull_requests repo_name we opened it against
469 # pull_requests repo_name we opened it against
470 # ie. other_repo must match
470 # ie. other_repo must match
471 if repo_name != c.pull_request.other_repo.repo_name:
471 if repo_name != c.pull_request.other_repo.repo_name:
472 raise HTTPNotFound
472 raise HTTPNotFound
473
473
474 # load compare data into template context
474 # load compare data into template context
475 c.cs_repo = c.pull_request.org_repo
475 c.cs_repo = c.pull_request.org_repo
476 (c.cs_ref_type,
476 (c.cs_ref_type,
477 c.cs_ref_name,
477 c.cs_ref_name,
478 c.cs_rev) = c.pull_request.org_ref.split(':')
478 c.cs_rev) = c.pull_request.org_ref.split(':')
479
479
480 c.a_repo = c.pull_request.other_repo
480 c.a_repo = c.pull_request.other_repo
481 (c.a_ref_type,
481 (c.a_ref_type,
482 c.a_ref_name,
482 c.a_ref_name,
483 c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
483 c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
484
484
485 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
485 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
486 try:
486 try:
487 c.cs_ranges = [org_scm_instance.get_changeset(x)
487 c.cs_ranges = []
488 for x in c.pull_request.revisions]
488 for x in c.pull_request.revisions:
489 c.cs_ranges.append(org_scm_instance.get_changeset(x))
489 except ChangesetDoesNotExistError:
490 except ChangesetDoesNotExistError:
490 c.cs_ranges = []
491 c.cs_ranges = []
491 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
492 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
492 'error')
493 'error')
493 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
494 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
494 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
495 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
495 c.jsdata = graph_data(org_scm_instance, revs)
496 c.jsdata = graph_data(org_scm_instance, revs)
496
497
497 c.is_range = False
498 c.is_range = False
498 try:
499 try:
499 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
500 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
500 cs_a = org_scm_instance.get_changeset(c.a_rev)
501 cs_a = org_scm_instance.get_changeset(c.a_rev)
501 root_parents = c.cs_ranges[0].parents
502 root_parents = c.cs_ranges[0].parents
502 c.is_range = cs_a in root_parents
503 c.is_range = cs_a in root_parents
503 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
504 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
504 except ChangesetDoesNotExistError: # probably because c.a_rev not found
505 except ChangesetDoesNotExistError: # probably because c.a_rev not found
505 pass
506 pass
506 except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
507 except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
507 pass
508 pass
508
509
509 avail_revs = set()
510 avail_revs = set()
510 avail_show = []
511 avail_show = []
511 c.cs_branch_name = c.cs_ref_name
512 c.cs_branch_name = c.cs_ref_name
512 c.a_branch_name = None
513 c.a_branch_name = None
513 other_scm_instance = c.a_repo.scm_instance
514 other_scm_instance = c.a_repo.scm_instance
514 c.update_msg = ""
515 c.update_msg = ""
515 c.update_msg_other = ""
516 c.update_msg_other = ""
516 try:
517 try:
517 if not c.cs_ranges:
518 if not c.cs_ranges:
518 c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
519 c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
519 elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
520 elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
520 if c.cs_ref_type != 'branch':
521 if c.cs_ref_type != 'branch':
521 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
522 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
522 c.a_branch_name = c.a_ref_name
523 c.a_branch_name = c.a_ref_name
523 if c.a_ref_type != 'branch':
524 if c.a_ref_type != 'branch':
524 try:
525 try:
525 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
526 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
526 except EmptyRepositoryError:
527 except EmptyRepositoryError:
527 c.a_branch_name = 'null' # not a branch name ... but close enough
528 c.a_branch_name = 'null' # not a branch name ... but close enough
528 # candidates: descendants of old head that are on the right branch
529 # candidates: descendants of old head that are on the right branch
529 # and not are the old head itself ...
530 # and not are the old head itself ...
530 # and nothing at all if old head is a descendant of target ref name
531 # and nothing at all if old head is a descendant of target ref name
531 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
532 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
532 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
533 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
533 elif c.pull_request.is_closed():
534 elif c.pull_request.is_closed():
534 c.update_msg = _('This pull request has been closed and can not be updated.')
535 c.update_msg = _('This pull request has been closed and can not be updated.')
535 else: # look for descendants of PR head on source branch in org repo
536 else: # look for descendants of PR head on source branch in org repo
536 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
537 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
537 revs[0], c.cs_branch_name)
538 revs[0], c.cs_branch_name)
538 if len(avail_revs) > 1: # more than just revs[0]
539 if len(avail_revs) > 1: # more than just revs[0]
539 # also show changesets that not are descendants but would be merged in
540 # also show changesets that not are descendants but would be merged in
540 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
541 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
541 if org_scm_instance.path != other_scm_instance.path:
542 if org_scm_instance.path != other_scm_instance.path:
542 # Note: org_scm_instance.path must come first so all
543 # Note: org_scm_instance.path must come first so all
543 # valid revision numbers are 100% org_scm compatible
544 # valid revision numbers are 100% org_scm compatible
544 # - both for avail_revs and for revset results
545 # - both for avail_revs and for revset results
545 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
546 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
546 org_scm_instance.path,
547 org_scm_instance.path,
547 other_scm_instance.path)
548 other_scm_instance.path)
548 else:
549 else:
549 hgrepo = org_scm_instance._repo
550 hgrepo = org_scm_instance._repo
550 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
551 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
551 avail_revs, revs[0], targethead))
552 avail_revs, revs[0], targethead))
552 c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
553 c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
553 else:
554 else:
554 show = set()
555 show = set()
555 avail_revs = set() # drop revs[0]
556 avail_revs = set() # drop revs[0]
556 c.update_msg = _('No additional changesets found for iterating on this pull request.')
557 c.update_msg = _('No additional changesets found for iterating on this pull request.')
557
558
558 # TODO: handle branch heads that not are tip-most
559 # TODO: handle branch heads that not are tip-most
559 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
560 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
560 if brevs:
561 if brevs:
561 # also show changesets that are on branch but neither ancestors nor descendants
562 # also show changesets that are on branch but neither ancestors nor descendants
562 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
563 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
563 show.add(revs[0]) # make sure graph shows this so we can see how they relate
564 show.add(revs[0]) # make sure graph shows this so we can see how they relate
564 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
565 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
565 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
566 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
566
567
567 avail_show = sorted(show, reverse=True)
568 avail_show = sorted(show, reverse=True)
568
569
569 elif org_scm_instance.alias == 'git':
570 elif org_scm_instance.alias == 'git':
570 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
571 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
571 c.update_msg = _("Git pull requests don't support iterating yet.")
572 c.update_msg = _("Git pull requests don't support iterating yet.")
572 except ChangesetDoesNotExistError:
573 except ChangesetDoesNotExistError:
573 c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev
574 c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev
574
575
575 c.avail_revs = avail_revs
576 c.avail_revs = avail_revs
576 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
577 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
577 c.avail_jsdata = graph_data(org_scm_instance, avail_show)
578 c.avail_jsdata = graph_data(org_scm_instance, avail_show)
578
579
579 raw_ids = [x.raw_id for x in c.cs_ranges]
580 raw_ids = [x.raw_id for x in c.cs_ranges]
580 c.cs_comments = c.cs_repo.get_comments(raw_ids)
581 c.cs_comments = c.cs_repo.get_comments(raw_ids)
581 c.cs_statuses = c.cs_repo.statuses(raw_ids)
582 c.cs_statuses = c.cs_repo.statuses(raw_ids)
582
583
583 ignore_whitespace = request.GET.get('ignorews') == '1'
584 ignore_whitespace = request.GET.get('ignorews') == '1'
584 line_context = safe_int(request.GET.get('context'), 3)
585 line_context = safe_int(request.GET.get('context'), 3)
585 c.ignorews_url = _ignorews_url
586 c.ignorews_url = _ignorews_url
586 c.context_url = _context_url
587 c.context_url = _context_url
587 c.fulldiff = request.GET.get('fulldiff')
588 c.fulldiff = request.GET.get('fulldiff')
588 diff_limit = self.cut_off_limit if not c.fulldiff else None
589 diff_limit = self.cut_off_limit if not c.fulldiff else None
589
590
590 # we swap org/other ref since we run a simple diff on one repo
591 # we swap org/other ref since we run a simple diff on one repo
591 log.debug('running diff between %s and %s in %s',
592 log.debug('running diff between %s and %s in %s',
592 c.a_rev, c.cs_rev, org_scm_instance.path)
593 c.a_rev, c.cs_rev, org_scm_instance.path)
593 try:
594 try:
594 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
595 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
595 ignore_whitespace=ignore_whitespace,
596 ignore_whitespace=ignore_whitespace,
596 context=line_context)
597 context=line_context)
597 except ChangesetDoesNotExistError:
598 except ChangesetDoesNotExistError:
598 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
599 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
599 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
600 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
600 diff_limit=diff_limit)
601 diff_limit=diff_limit)
601 _parsed = diff_processor.prepare()
602 _parsed = diff_processor.prepare()
602
603
603 c.limited_diff = False
604 c.limited_diff = False
604 if isinstance(_parsed, LimitedDiffContainer):
605 if isinstance(_parsed, LimitedDiffContainer):
605 c.limited_diff = True
606 c.limited_diff = True
606
607
607 c.file_diff_data = []
608 c.file_diff_data = []
608 c.lines_added = 0
609 c.lines_added = 0
609 c.lines_deleted = 0
610 c.lines_deleted = 0
610
611
611 for f in _parsed:
612 for f in _parsed:
612 st = f['stats']
613 st = f['stats']
613 c.lines_added += st['added']
614 c.lines_added += st['added']
614 c.lines_deleted += st['deleted']
615 c.lines_deleted += st['deleted']
615 filename = f['filename']
616 filename = f['filename']
616 fid = h.FID('', filename)
617 fid = h.FID('', filename)
617 diff = diff_processor.as_html(enable_comments=True,
618 diff = diff_processor.as_html(enable_comments=True,
618 parsed_lines=[f])
619 parsed_lines=[f])
619 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st))
620 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st))
620
621
621 # inline comments
622 # inline comments
622 c.inline_cnt = 0
623 c.inline_cnt = 0
623 c.inline_comments = cc_model.get_inline_comments(
624 c.inline_comments = cc_model.get_inline_comments(
624 c.db_repo.repo_id,
625 c.db_repo.repo_id,
625 pull_request=pull_request_id)
626 pull_request=pull_request_id)
626 # count inline comments
627 # count inline comments
627 for __, lines in c.inline_comments:
628 for __, lines in c.inline_comments:
628 for comments in lines.values():
629 for comments in lines.values():
629 c.inline_cnt += len(comments)
630 c.inline_cnt += len(comments)
630 # comments
631 # comments
631 c.comments = cc_model.get_comments(c.db_repo.repo_id, pull_request=pull_request_id)
632 c.comments = cc_model.get_comments(c.db_repo.repo_id, pull_request=pull_request_id)
632
633
633 # (badly named) pull-request status calculation based on reviewer votes
634 # (badly named) pull-request status calculation based on reviewer votes
634 (c.pull_request_reviewers,
635 (c.pull_request_reviewers,
635 c.pull_request_pending_reviewers,
636 c.pull_request_pending_reviewers,
636 c.current_voting_result,
637 c.current_voting_result,
637 ) = cs_model.calculate_pull_request_result(c.pull_request)
638 ) = cs_model.calculate_pull_request_result(c.pull_request)
638 c.changeset_statuses = ChangesetStatus.STATUSES
639 c.changeset_statuses = ChangesetStatus.STATUSES
639
640
640 c.is_ajax_preview = False
641 c.is_ajax_preview = False
641 c.ancestors = None # [c.a_rev] ... but that is shown in an other way
642 c.ancestors = None # [c.a_rev] ... but that is shown in an other way
642 return render('/pullrequests/pullrequest_show.html')
643 return render('/pullrequests/pullrequest_show.html')
643
644
644 @LoginRequired()
645 @LoginRequired()
645 @NotAnonymous()
646 @NotAnonymous()
646 @HasRepoPermissionLevelDecorator('read')
647 @HasRepoPermissionLevelDecorator('read')
647 @jsonify
648 @jsonify
648 def comment(self, repo_name, pull_request_id):
649 def comment(self, repo_name, pull_request_id):
649 pull_request = PullRequest.get_or_404(pull_request_id)
650 pull_request = PullRequest.get_or_404(pull_request_id)
650
651
651 status = request.POST.get('changeset_status')
652 status = request.POST.get('changeset_status')
652 close_pr = request.POST.get('save_close')
653 close_pr = request.POST.get('save_close')
653 delete = request.POST.get('save_delete')
654 delete = request.POST.get('save_delete')
654 f_path = request.POST.get('f_path')
655 f_path = request.POST.get('f_path')
655 line_no = request.POST.get('line')
656 line_no = request.POST.get('line')
656
657
657 if (status or close_pr or delete) and (f_path or line_no):
658 if (status or close_pr or delete) and (f_path or line_no):
658 # status votes and closing is only possible in general comments
659 # status votes and closing is only possible in general comments
659 raise HTTPBadRequest()
660 raise HTTPBadRequest()
660
661
661 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
662 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
662 if not allowed_to_change_status:
663 if not allowed_to_change_status:
663 if status or close_pr:
664 if status or close_pr:
664 h.flash(_('No permission to change pull request status'), 'error')
665 h.flash(_('No permission to change pull request status'), 'error')
665 raise HTTPForbidden()
666 raise HTTPForbidden()
666
667
667 if delete == "delete":
668 if delete == "delete":
668 if (pull_request.owner_id == request.authuser.user_id or
669 if (pull_request.owner_id == request.authuser.user_id or
669 h.HasPermissionAny('hg.admin')() or
670 h.HasPermissionAny('hg.admin')() or
670 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
671 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
671 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
672 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
672 ) and not pull_request.is_closed():
673 ) and not pull_request.is_closed():
673 PullRequestModel().delete(pull_request)
674 PullRequestModel().delete(pull_request)
674 Session().commit()
675 Session().commit()
675 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
676 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
676 category='success')
677 category='success')
677 return {
678 return {
678 'location': url('my_pullrequests'), # or repo pr list?
679 'location': url('my_pullrequests'), # or repo pr list?
679 }
680 }
680 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
681 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
681 raise HTTPForbidden()
682 raise HTTPForbidden()
682
683
683 text = request.POST.get('text', '').strip()
684 text = request.POST.get('text', '').strip()
684
685
685 comment = create_comment(
686 comment = create_comment(
686 text,
687 text,
687 status,
688 status,
688 pull_request_id=pull_request_id,
689 pull_request_id=pull_request_id,
689 f_path=f_path,
690 f_path=f_path,
690 line_no=line_no,
691 line_no=line_no,
691 closing_pr=close_pr,
692 closing_pr=close_pr,
692 )
693 )
693
694
694 action_logger(request.authuser,
695 action_logger(request.authuser,
695 'user_commented_pull_request:%s' % pull_request_id,
696 'user_commented_pull_request:%s' % pull_request_id,
696 c.db_repo, request.ip_addr)
697 c.db_repo, request.ip_addr)
697
698
698 if status:
699 if status:
699 ChangesetStatusModel().set_status(
700 ChangesetStatusModel().set_status(
700 c.db_repo.repo_id,
701 c.db_repo.repo_id,
701 status,
702 status,
702 request.authuser.user_id,
703 request.authuser.user_id,
703 comment,
704 comment,
704 pull_request=pull_request_id
705 pull_request=pull_request_id
705 )
706 )
706
707
707 if close_pr:
708 if close_pr:
708 PullRequestModel().close_pull_request(pull_request_id)
709 PullRequestModel().close_pull_request(pull_request_id)
709 action_logger(request.authuser,
710 action_logger(request.authuser,
710 'user_closed_pull_request:%s' % pull_request_id,
711 'user_closed_pull_request:%s' % pull_request_id,
711 c.db_repo, request.ip_addr)
712 c.db_repo, request.ip_addr)
712
713
713 Session().commit()
714 Session().commit()
714
715
715 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
716 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
716 raise HTTPFound(location=pull_request.url())
717 raise HTTPFound(location=pull_request.url())
717
718
718 data = {
719 data = {
719 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
720 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
720 }
721 }
721 if comment is not None:
722 if comment is not None:
722 c.comment = comment
723 c.comment = comment
723 data.update(comment.get_dict())
724 data.update(comment.get_dict())
724 data.update({'rendered_text':
725 data.update({'rendered_text':
725 render('changeset/changeset_comment_block.html')})
726 render('changeset/changeset_comment_block.html')})
726
727
727 return data
728 return data
728
729
729 @LoginRequired()
730 @LoginRequired()
730 @NotAnonymous()
731 @NotAnonymous()
731 @HasRepoPermissionLevelDecorator('read')
732 @HasRepoPermissionLevelDecorator('read')
732 @jsonify
733 @jsonify
733 def delete_comment(self, repo_name, comment_id):
734 def delete_comment(self, repo_name, comment_id):
734 co = ChangesetComment.get(comment_id)
735 co = ChangesetComment.get(comment_id)
735 if co.pull_request.is_closed():
736 if co.pull_request.is_closed():
736 #don't allow deleting comments on closed pull request
737 #don't allow deleting comments on closed pull request
737 raise HTTPForbidden()
738 raise HTTPForbidden()
738
739
739 owner = co.author_id == request.authuser.user_id
740 owner = co.author_id == request.authuser.user_id
740 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
741 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
741 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
742 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
742 ChangesetCommentsModel().delete(comment=co)
743 ChangesetCommentsModel().delete(comment=co)
743 Session().commit()
744 Session().commit()
744 return True
745 return True
745 else:
746 else:
746 raise HTTPForbidden()
747 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now