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