##// END OF EJS Templates
pull requests: the node for null is not '0' but 40 * '0'...
Mads Kiilerich -
r4555:0ece1a4c default
parent child Browse files
Show More
@@ -1,763 +1,763 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 import re
31 import re
32
32
33 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
33 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
34
34
35 from pylons import request, tmpl_context as c, url
35 from pylons import request, tmpl_context as c, url
36 from pylons.controllers.util import redirect
36 from pylons.controllers.util import redirect
37 from pylons.i18n.translation import _
37 from pylons.i18n.translation import _
38
38
39 from kallithea.lib.compat import json
39 from kallithea.lib.compat import json
40 from kallithea.lib.base import BaseRepoController, render
40 from kallithea.lib.base import BaseRepoController, render
41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
42 NotAnonymous
42 NotAnonymous
43 from kallithea.lib.helpers import Page
43 from kallithea.lib.helpers import Page
44 from kallithea.lib import helpers as h
44 from kallithea.lib import helpers as h
45 from kallithea.lib import diffs
45 from kallithea.lib import diffs
46 from kallithea.lib.utils import action_logger, jsonify
46 from kallithea.lib.utils import action_logger, jsonify
47 from kallithea.lib.vcs.utils import safe_str
47 from kallithea.lib.vcs.utils import safe_str
48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
49 from kallithea.lib.diffs import LimitedDiffContainer
49 from kallithea.lib.diffs import LimitedDiffContainer
50 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
50 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
51 PullRequestReviewers
51 PullRequestReviewers
52 from kallithea.model.pull_request import PullRequestModel
52 from kallithea.model.pull_request import PullRequestModel
53 from kallithea.model.meta import Session
53 from kallithea.model.meta import Session
54 from kallithea.model.repo import RepoModel
54 from kallithea.model.repo import RepoModel
55 from kallithea.model.comment import ChangesetCommentsModel
55 from kallithea.model.comment import ChangesetCommentsModel
56 from kallithea.model.changeset_status import ChangesetStatusModel
56 from kallithea.model.changeset_status import ChangesetStatusModel
57 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
57 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
58 from kallithea.lib.utils2 import safe_int
58 from kallithea.lib.utils2 import safe_int
59 from kallithea.controllers.changeset import _ignorews_url,\
59 from kallithea.controllers.changeset import _ignorews_url,\
60 _context_url, get_line_ctx, get_ignore_ws
60 _context_url, get_line_ctx, get_ignore_ws
61 from kallithea.controllers.compare import CompareController
61 from kallithea.controllers.compare import CompareController
62 from kallithea.lib.graphmod import graph_data
62 from kallithea.lib.graphmod import graph_data
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 class PullrequestsController(BaseRepoController):
67 class PullrequestsController(BaseRepoController):
68
68
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71 repo_model = RepoModel()
71 repo_model = RepoModel()
72 c.users_array = repo_model.get_users_js()
72 c.users_array = repo_model.get_users_js()
73 c.user_groups_array = repo_model.get_user_groups_js()
73 c.user_groups_array = repo_model.get_user_groups_js()
74
74
75 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
75 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
76 """return a structure with repo's interesting changesets, suitable for
76 """return a structure with repo's interesting changesets, suitable for
77 the selectors in pullrequest.html
77 the selectors in pullrequest.html
78
78
79 rev: a revision that must be in the list somehow and selected by default
79 rev: a revision that must be in the list somehow and selected by default
80 branch: a branch that must be in the list and selected by default - even if closed
80 branch: a branch that must be in the list and selected by default - even if closed
81 branch_rev: a revision of which peers should be preferred and available."""
81 branch_rev: a revision of which peers should be preferred and available."""
82 # list named branches that has been merged to this named branch - it should probably merge back
82 # list named branches that has been merged to this named branch - it should probably merge back
83 peers = []
83 peers = []
84
84
85 if rev:
85 if rev:
86 rev = safe_str(rev)
86 rev = safe_str(rev)
87
87
88 if branch:
88 if branch:
89 branch = safe_str(branch)
89 branch = safe_str(branch)
90
90
91 if branch_rev:
91 if branch_rev:
92 branch_rev = safe_str(branch_rev)
92 branch_rev = safe_str(branch_rev)
93 # a revset not restricting to merge() would be better
93 # a revset not restricting to merge() would be better
94 # (especially because it would get the branch point)
94 # (especially because it would get the branch point)
95 # ... but is currently too expensive
95 # ... but is currently too expensive
96 # including branches of children could be nice too
96 # including branches of children could be nice too
97 peerbranches = set()
97 peerbranches = set()
98 for i in repo._repo.revs(
98 for i in repo._repo.revs(
99 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
99 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
100 branch_rev, branch_rev):
100 branch_rev, branch_rev):
101 abranch = repo.get_changeset(i).branch
101 abranch = repo.get_changeset(i).branch
102 if abranch not in peerbranches:
102 if abranch not in peerbranches:
103 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
103 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
104 peers.append((n, abranch))
104 peers.append((n, abranch))
105 peerbranches.add(abranch)
105 peerbranches.add(abranch)
106
106
107 selected = None
107 selected = None
108 tiprev = repo.tags.get('tip')
108 tiprev = repo.tags.get('tip')
109 tipbranch = None
109 tipbranch = None
110
110
111 branches = []
111 branches = []
112 for abranch, branchrev in repo.branches.iteritems():
112 for abranch, branchrev in repo.branches.iteritems():
113 n = 'branch:%s:%s' % (abranch, branchrev)
113 n = 'branch:%s:%s' % (abranch, branchrev)
114 desc = abranch
114 desc = abranch
115 if branchrev == tiprev:
115 if branchrev == tiprev:
116 tipbranch = abranch
116 tipbranch = abranch
117 desc = '%s (current tip)' % desc
117 desc = '%s (current tip)' % desc
118 branches.append((n, desc))
118 branches.append((n, desc))
119 if rev == branchrev:
119 if rev == branchrev:
120 selected = n
120 selected = n
121 if branch == abranch:
121 if branch == abranch:
122 if not rev:
122 if not rev:
123 selected = n
123 selected = n
124 branch = None
124 branch = None
125 if branch: # branch not in list - it is probably closed
125 if branch: # branch not in list - it is probably closed
126 branchrev = repo.closed_branches.get(branch)
126 branchrev = repo.closed_branches.get(branch)
127 if branchrev:
127 if branchrev:
128 n = 'branch:%s:%s' % (branch, branchrev)
128 n = 'branch:%s:%s' % (branch, branchrev)
129 branches.append((n, _('%s (closed)') % branch))
129 branches.append((n, _('%s (closed)') % branch))
130 selected = n
130 selected = n
131 branch = None
131 branch = None
132 if branch:
132 if branch:
133 log.error('branch %r not found in %s', branch, repo)
133 log.error('branch %r not found in %s', branch, repo)
134
134
135 bookmarks = []
135 bookmarks = []
136 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
136 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
137 n = 'book:%s:%s' % (bookmark, bookmarkrev)
137 n = 'book:%s:%s' % (bookmark, bookmarkrev)
138 bookmarks.append((n, bookmark))
138 bookmarks.append((n, bookmark))
139 if rev == bookmarkrev:
139 if rev == bookmarkrev:
140 selected = n
140 selected = n
141
141
142 tags = []
142 tags = []
143 for tag, tagrev in repo.tags.iteritems():
143 for tag, tagrev in repo.tags.iteritems():
144 if tag == 'tip':
144 if tag == 'tip':
145 continue
145 continue
146 n = 'tag:%s:%s' % (tag, tagrev)
146 n = 'tag:%s:%s' % (tag, tagrev)
147 tags.append((n, tag))
147 tags.append((n, tag))
148 if rev == tagrev:
148 if rev == tagrev:
149 selected = n
149 selected = n
150
150
151 # prio 1: rev was selected as existing entry above
151 # prio 1: rev was selected as existing entry above
152
152
153 # prio 2: create special entry for rev; rev _must_ be used
153 # prio 2: create special entry for rev; rev _must_ be used
154 specials = []
154 specials = []
155 if rev and selected is None:
155 if rev and selected is None:
156 selected = 'rev:%s:%s' % (rev, rev)
156 selected = 'rev:%s:%s' % (rev, rev)
157 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
157 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
158
158
159 # prio 3: most recent peer branch
159 # prio 3: most recent peer branch
160 if peers and not selected:
160 if peers and not selected:
161 selected = peers[0][0]
161 selected = peers[0][0]
162
162
163 # prio 4: tip revision
163 # prio 4: tip revision
164 if not selected:
164 if not selected:
165 if h.is_hg(repo):
165 if h.is_hg(repo):
166 if tipbranch:
166 if tipbranch:
167 selected = 'branch:%s:%s' % (tipbranch, tiprev)
167 selected = 'branch:%s:%s' % (tipbranch, tiprev)
168 else:
168 else:
169 selected = 'tag:null:0'
169 selected = 'tag:null:' + repo.EMPTY_CHANGESET
170 tags.append((selected, 'null'))
170 tags.append((selected, 'null'))
171 else:
171 else:
172 if 'master' in repo.branches:
172 if 'master' in repo.branches:
173 selected = 'branch:master:%s' % repo.branches['master']
173 selected = 'branch:master:%s' % repo.branches['master']
174 else:
174 else:
175 k, v = repo.branches.items()[0]
175 k, v = repo.branches.items()[0]
176 selected = 'branch:%s:%s' % (k, v)
176 selected = 'branch:%s:%s' % (k, v)
177
177
178 groups = [(specials, _("Special")),
178 groups = [(specials, _("Special")),
179 (peers, _("Peer branches")),
179 (peers, _("Peer branches")),
180 (bookmarks, _("Bookmarks")),
180 (bookmarks, _("Bookmarks")),
181 (branches, _("Branches")),
181 (branches, _("Branches")),
182 (tags, _("Tags")),
182 (tags, _("Tags")),
183 ]
183 ]
184 return [g for g in groups if g[0]], selected
184 return [g for g in groups if g[0]], selected
185
185
186 def _get_is_allowed_change_status(self, pull_request):
186 def _get_is_allowed_change_status(self, pull_request):
187 if pull_request.is_closed():
187 if pull_request.is_closed():
188 return False
188 return False
189
189
190 owner = self.authuser.user_id == pull_request.user_id
190 owner = self.authuser.user_id == pull_request.user_id
191 reviewer = self.authuser.user_id in [x.user_id for x in
191 reviewer = self.authuser.user_id in [x.user_id for x in
192 pull_request.reviewers]
192 pull_request.reviewers]
193 return self.authuser.admin or owner or reviewer
193 return self.authuser.admin or owner or reviewer
194
194
195 @LoginRequired()
195 @LoginRequired()
196 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
196 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
197 'repository.admin')
197 'repository.admin')
198 def show_all(self, repo_name):
198 def show_all(self, repo_name):
199 c.from_ = request.GET.get('from_') or ''
199 c.from_ = request.GET.get('from_') or ''
200 c.closed = request.GET.get('closed') or ''
200 c.closed = request.GET.get('closed') or ''
201 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
201 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
202 c.repo_name = repo_name
202 c.repo_name = repo_name
203 p = safe_int(request.GET.get('page', 1), 1)
203 p = safe_int(request.GET.get('page', 1), 1)
204
204
205 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
205 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
206
206
207 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
207 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
208
208
209 if request.environ.get('HTTP_X_PARTIAL_XHR'):
209 if request.environ.get('HTTP_X_PARTIAL_XHR'):
210 return c.pullrequest_data
210 return c.pullrequest_data
211
211
212 return render('/pullrequests/pullrequest_show_all.html')
212 return render('/pullrequests/pullrequest_show_all.html')
213
213
214 @LoginRequired()
214 @LoginRequired()
215 def show_my(self): # my_account_my_pullrequests
215 def show_my(self): # my_account_my_pullrequests
216 c.show_closed = request.GET.get('pr_show_closed')
216 c.show_closed = request.GET.get('pr_show_closed')
217 return render('/pullrequests/pullrequest_show_my.html')
217 return render('/pullrequests/pullrequest_show_my.html')
218
218
219 @NotAnonymous()
219 @NotAnonymous()
220 def show_my_data(self):
220 def show_my_data(self):
221 c.show_closed = request.GET.get('pr_show_closed')
221 c.show_closed = request.GET.get('pr_show_closed')
222
222
223 def _filter(pr):
223 def _filter(pr):
224 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
224 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
225 if not c.show_closed:
225 if not c.show_closed:
226 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
226 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
227 return s
227 return s
228
228
229 c.my_pull_requests = _filter(PullRequest.query()\
229 c.my_pull_requests = _filter(PullRequest.query()\
230 .filter(PullRequest.user_id ==
230 .filter(PullRequest.user_id ==
231 self.authuser.user_id)\
231 self.authuser.user_id)\
232 .all())
232 .all())
233
233
234 c.participate_in_pull_requests = _filter(PullRequest.query()\
234 c.participate_in_pull_requests = _filter(PullRequest.query()\
235 .join(PullRequestReviewers)\
235 .join(PullRequestReviewers)\
236 .filter(PullRequestReviewers.user_id ==
236 .filter(PullRequestReviewers.user_id ==
237 self.authuser.user_id)\
237 self.authuser.user_id)\
238 )
238 )
239
239
240 return render('/pullrequests/pullrequest_show_my_data.html')
240 return render('/pullrequests/pullrequest_show_my_data.html')
241
241
242 @LoginRequired()
242 @LoginRequired()
243 @NotAnonymous()
243 @NotAnonymous()
244 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
244 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
245 'repository.admin')
245 'repository.admin')
246 def index(self):
246 def index(self):
247 org_repo = c.db_repo
247 org_repo = c.db_repo
248 org_scm_instance = org_repo.scm_instance
248 org_scm_instance = org_repo.scm_instance
249 try:
249 try:
250 org_scm_instance.get_changeset()
250 org_scm_instance.get_changeset()
251 except EmptyRepositoryError, e:
251 except EmptyRepositoryError, e:
252 h.flash(h.literal(_('There are no changesets yet')),
252 h.flash(h.literal(_('There are no changesets yet')),
253 category='warning')
253 category='warning')
254 redirect(url('summary_home', repo_name=org_repo.repo_name))
254 redirect(url('summary_home', repo_name=org_repo.repo_name))
255
255
256 org_rev = request.GET.get('rev_end')
256 org_rev = request.GET.get('rev_end')
257 # rev_start is not directly useful - its parent could however be used
257 # rev_start is not directly useful - its parent could however be used
258 # as default for other and thus give a simple compare view
258 # as default for other and thus give a simple compare view
259 #other_rev = request.POST.get('rev_start')
259 #other_rev = request.POST.get('rev_start')
260 branch = request.GET.get('branch')
260 branch = request.GET.get('branch')
261
261
262 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
262 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
263 c.default_cs_repo = org_repo.repo_name
263 c.default_cs_repo = org_repo.repo_name
264 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
264 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
265
265
266 # add org repo to other so we can open pull request against peer branches on itself
266 # add org repo to other so we can open pull request against peer branches on itself
267 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
267 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
268
268
269 # add parent of this fork also and select it
269 # add parent of this fork also and select it
270 if org_repo.parent:
270 if org_repo.parent:
271 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
271 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
272 c.a_repo = org_repo.parent
272 c.a_repo = org_repo.parent
273 c.a_refs, c.default_a_ref = self._get_repo_refs(org_repo.parent.scm_instance)
273 c.a_refs, c.default_a_ref = self._get_repo_refs(org_repo.parent.scm_instance)
274 else:
274 else:
275 c.a_repo = org_repo
275 c.a_repo = org_repo
276 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance) # without rev and branch
276 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance) # without rev and branch
277
277
278 # gather forks and add to this list ... even though it is rare to
278 # gather forks and add to this list ... even though it is rare to
279 # request forks to pull from their parent
279 # request forks to pull from their parent
280 for fork in org_repo.forks:
280 for fork in org_repo.forks:
281 c.a_repos.append((fork.repo_name, fork.repo_name))
281 c.a_repos.append((fork.repo_name, fork.repo_name))
282
282
283 return render('/pullrequests/pullrequest.html')
283 return render('/pullrequests/pullrequest.html')
284
284
285 @LoginRequired()
285 @LoginRequired()
286 @NotAnonymous()
286 @NotAnonymous()
287 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
287 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
288 'repository.admin')
288 'repository.admin')
289 @jsonify
289 @jsonify
290 def repo_info(self, repo_name):
290 def repo_info(self, repo_name):
291 repo = RepoModel()._get_repo(repo_name)
291 repo = RepoModel()._get_repo(repo_name)
292 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
292 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
293 return {
293 return {
294 'description': repo.description.split('\n', 1)[0],
294 'description': repo.description.split('\n', 1)[0],
295 'selected_ref': selected_ref,
295 'selected_ref': selected_ref,
296 'refs': refs,
296 'refs': refs,
297 'user': dict(user_id=repo.user.user_id,
297 'user': dict(user_id=repo.user.user_id,
298 username=repo.user.username,
298 username=repo.user.username,
299 firstname=repo.user.firstname,
299 firstname=repo.user.firstname,
300 lastname=repo.user.lastname,
300 lastname=repo.user.lastname,
301 gravatar_link=h.gravatar_url(repo.user.email, 14)),
301 gravatar_link=h.gravatar_url(repo.user.email, 14)),
302 }
302 }
303
303
304 @LoginRequired()
304 @LoginRequired()
305 @NotAnonymous()
305 @NotAnonymous()
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 'repository.admin')
307 'repository.admin')
308 def create(self, repo_name):
308 def create(self, repo_name):
309 repo = RepoModel()._get_repo(repo_name)
309 repo = RepoModel()._get_repo(repo_name)
310 try:
310 try:
311 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
311 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
312 except formencode.Invalid, errors:
312 except formencode.Invalid, errors:
313 log.error(traceback.format_exc())
313 log.error(traceback.format_exc())
314 log.error(str(errors))
314 log.error(str(errors))
315 msg = _('Error creating pull request: %s') % errors.msg
315 msg = _('Error creating pull request: %s') % errors.msg
316 h.flash(msg, 'error')
316 h.flash(msg, 'error')
317 raise HTTPBadRequest
317 raise HTTPBadRequest
318
318
319 # heads up: org and other might seem backward here ...
319 # heads up: org and other might seem backward here ...
320 org_repo_name = _form['org_repo']
320 org_repo_name = _form['org_repo']
321 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
321 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
322 org_repo = RepoModel()._get_repo(org_repo_name)
322 org_repo = RepoModel()._get_repo(org_repo_name)
323 (org_ref_type,
323 (org_ref_type,
324 org_ref_name,
324 org_ref_name,
325 org_rev) = org_ref.split(':')
325 org_rev) = org_ref.split(':')
326
326
327 other_repo_name = _form['other_repo']
327 other_repo_name = _form['other_repo']
328 other_ref = _form['other_ref'] # will have symbolic name and head revision
328 other_ref = _form['other_ref'] # will have symbolic name and head revision
329 other_repo = RepoModel()._get_repo(other_repo_name)
329 other_repo = RepoModel()._get_repo(other_repo_name)
330 (other_ref_type,
330 (other_ref_type,
331 other_ref_name,
331 other_ref_name,
332 other_rev) = other_ref.split(':')
332 other_rev) = other_ref.split(':')
333
333
334 cs_ranges, _cs_ranges_not, ancestor_rev = \
334 cs_ranges, _cs_ranges_not, ancestor_rev = \
335 CompareController._get_changesets(org_repo.scm_instance.alias,
335 CompareController._get_changesets(org_repo.scm_instance.alias,
336 other_repo.scm_instance, other_rev, # org and other "swapped"
336 other_repo.scm_instance, other_rev, # org and other "swapped"
337 org_repo.scm_instance, org_rev,
337 org_repo.scm_instance, org_rev,
338 )
338 )
339 revisions = [cs.raw_id for cs in cs_ranges]
339 revisions = [cs.raw_id for cs in cs_ranges]
340
340
341 # hack: ancestor_rev is not an other_rev but we want to show the
341 # hack: ancestor_rev is not an other_rev but we want to show the
342 # requested destination and have the exact ancestor
342 # requested destination and have the exact ancestor
343 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
343 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
344
344
345 reviewers = _form['review_members']
345 reviewers = _form['review_members']
346
346
347 title = _form['pullrequest_title']
347 title = _form['pullrequest_title']
348 if not title:
348 if not title:
349 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
349 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
350 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
350 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
351 description = _form['pullrequest_desc'].strip() or _('No description')
351 description = _form['pullrequest_desc'].strip() or _('No description')
352 try:
352 try:
353 pull_request = PullRequestModel().create(
353 pull_request = PullRequestModel().create(
354 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
354 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
355 other_ref, revisions, reviewers, title, description
355 other_ref, revisions, reviewers, title, description
356 )
356 )
357 Session().commit()
357 Session().commit()
358 h.flash(_('Successfully opened new pull request'),
358 h.flash(_('Successfully opened new pull request'),
359 category='success')
359 category='success')
360 except Exception:
360 except Exception:
361 h.flash(_('Error occurred while creating pull request'),
361 h.flash(_('Error occurred while creating pull request'),
362 category='error')
362 category='error')
363 log.error(traceback.format_exc())
363 log.error(traceback.format_exc())
364 return redirect(url('pullrequest_home', repo_name=repo_name))
364 return redirect(url('pullrequest_home', repo_name=repo_name))
365
365
366 return redirect(pull_request.url())
366 return redirect(pull_request.url())
367
367
368 @LoginRequired()
368 @LoginRequired()
369 @NotAnonymous()
369 @NotAnonymous()
370 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
370 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
371 'repository.admin')
371 'repository.admin')
372 def copy_update(self, repo_name, pull_request_id):
372 def copy_update(self, repo_name, pull_request_id):
373 old_pull_request = PullRequest.get_or_404(pull_request_id)
373 old_pull_request = PullRequest.get_or_404(pull_request_id)
374 assert old_pull_request.other_repo.repo_name == repo_name
374 assert old_pull_request.other_repo.repo_name == repo_name
375 if old_pull_request.is_closed():
375 if old_pull_request.is_closed():
376 raise HTTPForbidden()
376 raise HTTPForbidden()
377
377
378 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
378 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
379 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
379 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
380 updaterev = request.POST.get('updaterev')
380 updaterev = request.POST.get('updaterev')
381 if updaterev:
381 if updaterev:
382 new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
382 new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
383 else:
383 else:
384 # assert org_ref_type == 'branch', org_ref_type # TODO: what if not?
384 # assert org_ref_type == 'branch', org_ref_type # TODO: what if not?
385 new_org_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name)
385 new_org_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name)
386
386
387 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
387 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
388 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
388 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
389 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
389 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
390 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
390 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
391
391
392 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
392 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
393 other_repo.scm_instance, new_other_rev, # org and other "swapped"
393 other_repo.scm_instance, new_other_rev, # org and other "swapped"
394 org_repo.scm_instance, new_org_rev)
394 org_repo.scm_instance, new_org_rev)
395
395
396 old_revisions = set(old_pull_request.revisions)
396 old_revisions = set(old_pull_request.revisions)
397 revisions = [cs.raw_id for cs in cs_ranges]
397 revisions = [cs.raw_id for cs in cs_ranges]
398 new_revisions = [r for r in revisions if r not in old_revisions]
398 new_revisions = [r for r in revisions if r not in old_revisions]
399 lost = old_revisions.difference(revisions)
399 lost = old_revisions.difference(revisions)
400
400
401 infos = ['','', 'This is an update of %s "%s".' %
401 infos = ['','', 'This is an update of %s "%s".' %
402 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
402 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
403 pull_request_id=pull_request_id),
403 pull_request_id=pull_request_id),
404 old_pull_request.title)]
404 old_pull_request.title)]
405
405
406 if lost:
406 if lost:
407 infos.append(_('Missing changesets since the previous version:'))
407 infos.append(_('Missing changesets since the previous version:'))
408 for r in old_pull_request.revisions:
408 for r in old_pull_request.revisions:
409 if r in lost:
409 if r in lost:
410 desc = org_repo.get_changeset(r).message.split('\n')[0]
410 desc = org_repo.get_changeset(r).message.split('\n')[0]
411 infos.append(' %s "%s"' % (h.short_id(r), desc))
411 infos.append(' %s "%s"' % (h.short_id(r), desc))
412
412
413 if new_revisions:
413 if new_revisions:
414 infos.append(_('New changesets on %s %s since the previous version:') % (org_ref_type, org_ref_name))
414 infos.append(_('New changesets on %s %s since the previous version:') % (org_ref_type, org_ref_name))
415 for r in reversed(revisions):
415 for r in reversed(revisions):
416 if r in new_revisions:
416 if r in new_revisions:
417 desc = org_repo.get_changeset(r).message.split('\n')[0]
417 desc = org_repo.get_changeset(r).message.split('\n')[0]
418 infos.append(' %s %s' % (h.short_id(r), h.shorter(desc, 80)))
418 infos.append(' %s %s' % (h.short_id(r), h.shorter(desc, 80)))
419
419
420 if ancestor_rev == other_rev:
420 if ancestor_rev == other_rev:
421 infos.append(_("Ancestor didn't change - show diff since previous version:"))
421 infos.append(_("Ancestor didn't change - show diff since previous version:"))
422 infos.append(h.canonical_url('compare_url',
422 infos.append(h.canonical_url('compare_url',
423 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
423 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
424 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
424 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
425 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
425 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
426 )) # note: linear diff, merge or not doesn't matter
426 )) # note: linear diff, merge or not doesn't matter
427 else:
427 else:
428 infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
428 infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
429 else:
429 else:
430 infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
430 infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
431 # TODO: fail?
431 # TODO: fail?
432
432
433 # hack: ancestor_rev is not an other_ref but we want to show the
433 # hack: ancestor_rev is not an other_ref but we want to show the
434 # requested destination and have the exact ancestor
434 # requested destination and have the exact ancestor
435 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
435 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
436 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
436 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
437
437
438 reviewers = [r.user_id for r in old_pull_request.reviewers]
438 reviewers = [r.user_id for r in old_pull_request.reviewers]
439 try:
439 try:
440 old_title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', old_pull_request.title).groups()
440 old_title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', old_pull_request.title).groups()
441 v = int(old_v) + 1
441 v = int(old_v) + 1
442 except (AttributeError, ValueError):
442 except (AttributeError, ValueError):
443 old_title = old_pull_request.title
443 old_title = old_pull_request.title
444 v = 2
444 v = 2
445 title = '%s (v%s)' % (old_title.strip(), v)
445 title = '%s (v%s)' % (old_title.strip(), v)
446 description = (old_pull_request.description.rstrip() +
446 description = (old_pull_request.description.rstrip() +
447 '\n'.join(infos))
447 '\n'.join(infos))
448
448
449 try:
449 try:
450 pull_request = PullRequestModel().create(
450 pull_request = PullRequestModel().create(
451 self.authuser.user_id,
451 self.authuser.user_id,
452 old_pull_request.org_repo.repo_name, new_org_ref,
452 old_pull_request.org_repo.repo_name, new_org_ref,
453 old_pull_request.other_repo.repo_name, new_other_ref,
453 old_pull_request.other_repo.repo_name, new_other_ref,
454 revisions, reviewers, title, description
454 revisions, reviewers, title, description
455 )
455 )
456 except Exception:
456 except Exception:
457 h.flash(_('Error occurred while creating pull request'),
457 h.flash(_('Error occurred while creating pull request'),
458 category='error')
458 category='error')
459 log.error(traceback.format_exc())
459 log.error(traceback.format_exc())
460 return redirect(old_pull_request.url())
460 return redirect(old_pull_request.url())
461
461
462 ChangesetCommentsModel().create(
462 ChangesetCommentsModel().create(
463 text=_('Closed, replaced by %s .') % h.canonical_url('pullrequest_show',
463 text=_('Closed, replaced by %s .') % h.canonical_url('pullrequest_show',
464 repo_name=old_pull_request.other_repo.repo_name,
464 repo_name=old_pull_request.other_repo.repo_name,
465 pull_request_id=pull_request.pull_request_id),
465 pull_request_id=pull_request.pull_request_id),
466 repo=old_pull_request.other_repo.repo_id,
466 repo=old_pull_request.other_repo.repo_id,
467 user=c.authuser.user_id,
467 user=c.authuser.user_id,
468 pull_request=pull_request_id,
468 pull_request=pull_request_id,
469 closing_pr=True)
469 closing_pr=True)
470 PullRequestModel().close_pull_request(pull_request_id)
470 PullRequestModel().close_pull_request(pull_request_id)
471
471
472 Session().commit()
472 Session().commit()
473 h.flash(_('Pull request update created'),
473 h.flash(_('Pull request update created'),
474 category='success')
474 category='success')
475
475
476 return redirect(pull_request.url())
476 return redirect(pull_request.url())
477
477
478 # pullrequest_post for PR editing
478 # pullrequest_post for PR editing
479 @LoginRequired()
479 @LoginRequired()
480 @NotAnonymous()
480 @NotAnonymous()
481 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
481 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
482 'repository.admin')
482 'repository.admin')
483 def post(self, repo_name, pull_request_id):
483 def post(self, repo_name, pull_request_id):
484 repo = RepoModel()._get_repo(repo_name)
484 repo = RepoModel()._get_repo(repo_name)
485 pull_request = PullRequest.get_or_404(pull_request_id)
485 pull_request = PullRequest.get_or_404(pull_request_id)
486 old_description = pull_request.description
486 old_description = pull_request.description
487
487
488 _form = PullRequestPostForm()().to_python(request.POST)
488 _form = PullRequestPostForm()().to_python(request.POST)
489
489
490 pull_request.title = _form['pullrequest_title']
490 pull_request.title = _form['pullrequest_title']
491 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
491 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
492
492
493 PullRequestModel().mention_from_description(pull_request, old_description)
493 PullRequestModel().mention_from_description(pull_request, old_description)
494
494
495 Session().commit()
495 Session().commit()
496 h.flash(_('Pull request updated'), category='success')
496 h.flash(_('Pull request updated'), category='success')
497
497
498 return redirect(pull_request.url())
498 return redirect(pull_request.url())
499
499
500 # pullrequest_update for updating reviewer list
500 # pullrequest_update for updating reviewer list
501 @LoginRequired()
501 @LoginRequired()
502 @NotAnonymous()
502 @NotAnonymous()
503 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
503 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
504 'repository.admin')
504 'repository.admin')
505 @jsonify
505 @jsonify
506 def update(self, repo_name, pull_request_id):
506 def update(self, repo_name, pull_request_id):
507 pull_request = PullRequest.get_or_404(pull_request_id)
507 pull_request = PullRequest.get_or_404(pull_request_id)
508 if pull_request.is_closed():
508 if pull_request.is_closed():
509 raise HTTPForbidden()
509 raise HTTPForbidden()
510 #only owner or admin can update it
510 #only owner or admin can update it
511 owner = pull_request.author.user_id == c.authuser.user_id
511 owner = pull_request.author.user_id == c.authuser.user_id
512 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
512 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
513 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
513 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
514 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
514 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
515 request.POST.get('reviewers_ids', '').split(',')))
515 request.POST.get('reviewers_ids', '').split(',')))
516
516
517 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
517 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
518 Session().commit()
518 Session().commit()
519 return True
519 return True
520 raise HTTPForbidden()
520 raise HTTPForbidden()
521
521
522 @LoginRequired()
522 @LoginRequired()
523 @NotAnonymous()
523 @NotAnonymous()
524 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
524 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
525 'repository.admin')
525 'repository.admin')
526 @jsonify
526 @jsonify
527 def delete(self, repo_name, pull_request_id):
527 def delete(self, repo_name, pull_request_id):
528 pull_request = PullRequest.get_or_404(pull_request_id)
528 pull_request = PullRequest.get_or_404(pull_request_id)
529 #only owner can delete it !
529 #only owner can delete it !
530 if pull_request.author.user_id == c.authuser.user_id:
530 if pull_request.author.user_id == c.authuser.user_id:
531 PullRequestModel().delete(pull_request)
531 PullRequestModel().delete(pull_request)
532 Session().commit()
532 Session().commit()
533 h.flash(_('Successfully deleted pull request'),
533 h.flash(_('Successfully deleted pull request'),
534 category='success')
534 category='success')
535 return redirect(url('my_pullrequests'))
535 return redirect(url('my_pullrequests'))
536 raise HTTPForbidden()
536 raise HTTPForbidden()
537
537
538 @LoginRequired()
538 @LoginRequired()
539 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
539 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
540 'repository.admin')
540 'repository.admin')
541 def show(self, repo_name, pull_request_id, extra=None):
541 def show(self, repo_name, pull_request_id, extra=None):
542 repo_model = RepoModel()
542 repo_model = RepoModel()
543 c.users_array = repo_model.get_users_js()
543 c.users_array = repo_model.get_users_js()
544 c.user_groups_array = repo_model.get_user_groups_js()
544 c.user_groups_array = repo_model.get_user_groups_js()
545 c.pull_request = PullRequest.get_or_404(pull_request_id)
545 c.pull_request = PullRequest.get_or_404(pull_request_id)
546 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
546 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
547 cc_model = ChangesetCommentsModel()
547 cc_model = ChangesetCommentsModel()
548 cs_model = ChangesetStatusModel()
548 cs_model = ChangesetStatusModel()
549
549
550 # pull_requests repo_name we opened it against
550 # pull_requests repo_name we opened it against
551 # ie. other_repo must match
551 # ie. other_repo must match
552 if repo_name != c.pull_request.other_repo.repo_name:
552 if repo_name != c.pull_request.other_repo.repo_name:
553 raise HTTPNotFound
553 raise HTTPNotFound
554
554
555 # load compare data into template context
555 # load compare data into template context
556 c.cs_repo = c.pull_request.org_repo
556 c.cs_repo = c.pull_request.org_repo
557 (c.cs_ref_type,
557 (c.cs_ref_type,
558 c.cs_ref_name,
558 c.cs_ref_name,
559 c.cs_rev) = c.pull_request.org_ref.split(':')
559 c.cs_rev) = c.pull_request.org_ref.split(':')
560
560
561 c.a_repo = c.pull_request.other_repo
561 c.a_repo = c.pull_request.other_repo
562 (c.a_ref_type,
562 (c.a_ref_type,
563 c.a_ref_name,
563 c.a_ref_name,
564 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
564 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
565
565
566 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
566 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
567 c.cs_repo = c.cs_repo
567 c.cs_repo = c.cs_repo
568 c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
568 c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
569 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
569 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
570 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
570 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
571 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
571 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
572
572
573 c.available = []
573 c.available = []
574 c.cs_branch_name = c.cs_ref_name
574 c.cs_branch_name = c.cs_ref_name
575 other_scm_instance = c.a_repo.scm_instance
575 other_scm_instance = c.a_repo.scm_instance
576 c.update_msg = ""
576 c.update_msg = ""
577 c.update_msg_other = ""
577 c.update_msg_other = ""
578 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
578 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
579 if c.cs_ref_type != 'branch':
579 if c.cs_ref_type != 'branch':
580 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
580 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
581 other_branch_name = c.a_ref_name
581 other_branch_name = c.a_ref_name
582 if c.a_ref_type != 'branch':
582 if c.a_ref_type != 'branch':
583 try:
583 try:
584 other_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
584 other_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
585 except EmptyRepositoryError:
585 except EmptyRepositoryError:
586 other_branch_name = 'null' # not a branch name ... but close enough
586 other_branch_name = 'null' # not a branch name ... but close enough
587 # candidates: descendants of old head that are on the right branch
587 # candidates: descendants of old head that are on the right branch
588 # and not are the old head itself ...
588 # and not are the old head itself ...
589 # and nothing at all if old head is a descendent of target ref name
589 # and nothing at all if old head is a descendent of target ref name
590 if other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, other_branch_name):
590 if other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, other_branch_name):
591 c.update_msg = _('This pull request has already been merged to %s.') % other_branch_name
591 c.update_msg = _('This pull request has already been merged to %s.') % other_branch_name
592 else: # look for children of PR head on source branch in org repo
592 else: # look for children of PR head on source branch in org repo
593 arevs = org_scm_instance._repo.revs('%s:: & branch(%s) - %s',
593 arevs = org_scm_instance._repo.revs('%s:: & branch(%s) - %s',
594 revs[0], c.cs_branch_name, revs[0])
594 revs[0], c.cs_branch_name, revs[0])
595 if arevs:
595 if arevs:
596 if c.pull_request.is_closed():
596 if c.pull_request.is_closed():
597 c.update_msg = _('This pull request has been closed and can not be updated with descendent changes on %s:') % c.cs_branch_name
597 c.update_msg = _('This pull request has been closed and can not be updated with descendent changes on %s:') % c.cs_branch_name
598 else:
598 else:
599 c.update_msg = _('This pull request can be updated with descendent changes on %s:') % c.cs_branch_name
599 c.update_msg = _('This pull request can be updated with descendent changes on %s:') % c.cs_branch_name
600 c.available = [org_scm_instance.get_changeset(x) for x in arevs]
600 c.available = [org_scm_instance.get_changeset(x) for x in arevs]
601 else:
601 else:
602 c.update_msg = _('No changesets found for updating this pull request.')
602 c.update_msg = _('No changesets found for updating this pull request.')
603
603
604 revs = org_scm_instance._repo.revs('head() & not (%s::) & branch(%s) & !closed()', revs[0], c.cs_branch_name)
604 revs = org_scm_instance._repo.revs('head() & not (%s::) & branch(%s) & !closed()', revs[0], c.cs_branch_name)
605 if revs:
605 if revs:
606 c.update_msg_other = _('Note: Branch %s also contains unrelated changes, such as %s.') % (c.cs_branch_name,
606 c.update_msg_other = _('Note: Branch %s also contains unrelated changes, such as %s.') % (c.cs_branch_name,
607 h.short_id(org_scm_instance.get_changeset((max(revs))).raw_id))
607 h.short_id(org_scm_instance.get_changeset((max(revs))).raw_id))
608 else:
608 else:
609 c.update_msg_other = _('Branch %s does not contain other changes.') % c.cs_branch_name
609 c.update_msg_other = _('Branch %s does not contain other changes.') % c.cs_branch_name
610 elif org_scm_instance.alias == 'git':
610 elif org_scm_instance.alias == 'git':
611 c.update_msg = _("Git pull requests don't support updates yet.")
611 c.update_msg = _("Git pull requests don't support updates yet.")
612
612
613 raw_ids = [x.raw_id for x in c.cs_ranges]
613 raw_ids = [x.raw_id for x in c.cs_ranges]
614 c.cs_comments = c.cs_repo.get_comments(raw_ids)
614 c.cs_comments = c.cs_repo.get_comments(raw_ids)
615 c.statuses = c.cs_repo.statuses(raw_ids)
615 c.statuses = c.cs_repo.statuses(raw_ids)
616
616
617 ignore_whitespace = request.GET.get('ignorews') == '1'
617 ignore_whitespace = request.GET.get('ignorews') == '1'
618 line_context = request.GET.get('context', 3)
618 line_context = request.GET.get('context', 3)
619 c.ignorews_url = _ignorews_url
619 c.ignorews_url = _ignorews_url
620 c.context_url = _context_url
620 c.context_url = _context_url
621 c.fulldiff = request.GET.get('fulldiff')
621 c.fulldiff = request.GET.get('fulldiff')
622 diff_limit = self.cut_off_limit if not c.fulldiff else None
622 diff_limit = self.cut_off_limit if not c.fulldiff else None
623
623
624 # we swap org/other ref since we run a simple diff on one repo
624 # we swap org/other ref since we run a simple diff on one repo
625 log.debug('running diff between %s and %s in %s'
625 log.debug('running diff between %s and %s in %s'
626 % (c.a_rev, c.cs_rev, org_scm_instance.path))
626 % (c.a_rev, c.cs_rev, org_scm_instance.path))
627 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
627 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
628 ignore_whitespace=ignore_whitespace,
628 ignore_whitespace=ignore_whitespace,
629 context=line_context)
629 context=line_context)
630
630
631 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
631 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
632 diff_limit=diff_limit)
632 diff_limit=diff_limit)
633 _parsed = diff_processor.prepare()
633 _parsed = diff_processor.prepare()
634
634
635 c.limited_diff = False
635 c.limited_diff = False
636 if isinstance(_parsed, LimitedDiffContainer):
636 if isinstance(_parsed, LimitedDiffContainer):
637 c.limited_diff = True
637 c.limited_diff = True
638
638
639 c.files = []
639 c.files = []
640 c.changes = {}
640 c.changes = {}
641 c.lines_added = 0
641 c.lines_added = 0
642 c.lines_deleted = 0
642 c.lines_deleted = 0
643
643
644 for f in _parsed:
644 for f in _parsed:
645 st = f['stats']
645 st = f['stats']
646 c.lines_added += st['added']
646 c.lines_added += st['added']
647 c.lines_deleted += st['deleted']
647 c.lines_deleted += st['deleted']
648 fid = h.FID('', f['filename'])
648 fid = h.FID('', f['filename'])
649 c.files.append([fid, f['operation'], f['filename'], f['stats']])
649 c.files.append([fid, f['operation'], f['filename'], f['stats']])
650 htmldiff = diff_processor.as_html(enable_comments=True,
650 htmldiff = diff_processor.as_html(enable_comments=True,
651 parsed_lines=[f])
651 parsed_lines=[f])
652 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
652 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
653
653
654 # inline comments
654 # inline comments
655 c.inline_cnt = 0
655 c.inline_cnt = 0
656 c.inline_comments = cc_model.get_inline_comments(
656 c.inline_comments = cc_model.get_inline_comments(
657 c.db_repo.repo_id,
657 c.db_repo.repo_id,
658 pull_request=pull_request_id)
658 pull_request=pull_request_id)
659 # count inline comments
659 # count inline comments
660 for __, lines in c.inline_comments:
660 for __, lines in c.inline_comments:
661 for comments in lines.values():
661 for comments in lines.values():
662 c.inline_cnt += len(comments)
662 c.inline_cnt += len(comments)
663 # comments
663 # comments
664 c.comments = cc_model.get_comments(c.db_repo.repo_id,
664 c.comments = cc_model.get_comments(c.db_repo.repo_id,
665 pull_request=pull_request_id)
665 pull_request=pull_request_id)
666
666
667 # (badly named) pull-request status calculation based on reviewer votes
667 # (badly named) pull-request status calculation based on reviewer votes
668 (c.pull_request_reviewers,
668 (c.pull_request_reviewers,
669 c.pull_request_pending_reviewers,
669 c.pull_request_pending_reviewers,
670 c.current_voting_result,
670 c.current_voting_result,
671 ) = cs_model.calculate_pull_request_result(c.pull_request)
671 ) = cs_model.calculate_pull_request_result(c.pull_request)
672 c.changeset_statuses = ChangesetStatus.STATUSES
672 c.changeset_statuses = ChangesetStatus.STATUSES
673
673
674 c.as_form = False
674 c.as_form = False
675 c.ancestor = None # there is one - but right here we don't know which
675 c.ancestor = None # there is one - but right here we don't know which
676 return render('/pullrequests/pullrequest_show.html')
676 return render('/pullrequests/pullrequest_show.html')
677
677
678 @LoginRequired()
678 @LoginRequired()
679 @NotAnonymous()
679 @NotAnonymous()
680 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
680 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
681 'repository.admin')
681 'repository.admin')
682 @jsonify
682 @jsonify
683 def comment(self, repo_name, pull_request_id):
683 def comment(self, repo_name, pull_request_id):
684 pull_request = PullRequest.get_or_404(pull_request_id)
684 pull_request = PullRequest.get_or_404(pull_request_id)
685
685
686 status = 0
686 status = 0
687 close_pr = False
687 close_pr = False
688 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
688 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
689 if allowed_to_change_status:
689 if allowed_to_change_status:
690 status = request.POST.get('changeset_status')
690 status = request.POST.get('changeset_status')
691 close_pr = request.POST.get('save_close')
691 close_pr = request.POST.get('save_close')
692 text = request.POST.get('text', '').strip() or _('No comments.')
692 text = request.POST.get('text', '').strip() or _('No comments.')
693 if close_pr:
693 if close_pr:
694 text = _('Closing.') + '\n' + text
694 text = _('Closing.') + '\n' + text
695
695
696 comm = ChangesetCommentsModel().create(
696 comm = ChangesetCommentsModel().create(
697 text=text,
697 text=text,
698 repo=c.db_repo.repo_id,
698 repo=c.db_repo.repo_id,
699 user=c.authuser.user_id,
699 user=c.authuser.user_id,
700 pull_request=pull_request_id,
700 pull_request=pull_request_id,
701 f_path=request.POST.get('f_path'),
701 f_path=request.POST.get('f_path'),
702 line_no=request.POST.get('line'),
702 line_no=request.POST.get('line'),
703 status_change=(ChangesetStatus.get_status_lbl(status)
703 status_change=(ChangesetStatus.get_status_lbl(status)
704 if status and allowed_to_change_status else None),
704 if status and allowed_to_change_status else None),
705 closing_pr=close_pr
705 closing_pr=close_pr
706 )
706 )
707
707
708 action_logger(self.authuser,
708 action_logger(self.authuser,
709 'user_commented_pull_request:%s' % pull_request_id,
709 'user_commented_pull_request:%s' % pull_request_id,
710 c.db_repo, self.ip_addr, self.sa)
710 c.db_repo, self.ip_addr, self.sa)
711
711
712 if allowed_to_change_status:
712 if allowed_to_change_status:
713 # get status if set !
713 # get status if set !
714 if status:
714 if status:
715 ChangesetStatusModel().set_status(
715 ChangesetStatusModel().set_status(
716 c.db_repo.repo_id,
716 c.db_repo.repo_id,
717 status,
717 status,
718 c.authuser.user_id,
718 c.authuser.user_id,
719 comm,
719 comm,
720 pull_request=pull_request_id
720 pull_request=pull_request_id
721 )
721 )
722
722
723 if close_pr:
723 if close_pr:
724 PullRequestModel().close_pull_request(pull_request_id)
724 PullRequestModel().close_pull_request(pull_request_id)
725 action_logger(self.authuser,
725 action_logger(self.authuser,
726 'user_closed_pull_request:%s' % pull_request_id,
726 'user_closed_pull_request:%s' % pull_request_id,
727 c.db_repo, self.ip_addr, self.sa)
727 c.db_repo, self.ip_addr, self.sa)
728
728
729 Session().commit()
729 Session().commit()
730
730
731 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
731 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
732 return redirect(pull_request.url())
732 return redirect(pull_request.url())
733
733
734 data = {
734 data = {
735 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
735 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
736 }
736 }
737 if comm:
737 if comm:
738 c.co = comm
738 c.co = comm
739 data.update(comm.get_dict())
739 data.update(comm.get_dict())
740 data.update({'rendered_text':
740 data.update({'rendered_text':
741 render('changeset/changeset_comment_block.html')})
741 render('changeset/changeset_comment_block.html')})
742
742
743 return data
743 return data
744
744
745 @LoginRequired()
745 @LoginRequired()
746 @NotAnonymous()
746 @NotAnonymous()
747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
748 'repository.admin')
748 'repository.admin')
749 @jsonify
749 @jsonify
750 def delete_comment(self, repo_name, comment_id):
750 def delete_comment(self, repo_name, comment_id):
751 co = ChangesetComment.get(comment_id)
751 co = ChangesetComment.get(comment_id)
752 if co.pull_request.is_closed():
752 if co.pull_request.is_closed():
753 #don't allow deleting comments on closed pull request
753 #don't allow deleting comments on closed pull request
754 raise HTTPForbidden()
754 raise HTTPForbidden()
755
755
756 owner = co.author.user_id == c.authuser.user_id
756 owner = co.author.user_id == c.authuser.user_id
757 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
757 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
758 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
758 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
759 ChangesetCommentsModel().delete(comment=co)
759 ChangesetCommentsModel().delete(comment=co)
760 Session().commit()
760 Session().commit()
761 return True
761 return True
762 else:
762 else:
763 raise HTTPForbidden()
763 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now