##// END OF EJS Templates
compare: add options for ignore whitespace and increase context
Mads Kiilerich -
r4309:60ae17de default
parent child Browse files
Show More
@@ -1,312 +1,321 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.compare
15 kallithea.controllers.compare
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 compare controller for pylons showing differences between two
18 compare controller for pylons showing differences between two
19 repos, branches, bookmarks or tips
19 repos, branches, bookmarks or tips
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: May 6, 2012
23 :created_on: May 6, 2012
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29
29
30 import logging
30 import logging
31 import traceback
31 import traceback
32 import re
32 import re
33
33
34 from webob.exc import HTTPNotFound, HTTPBadRequest
34 from webob.exc import HTTPNotFound, HTTPBadRequest
35 from pylons import request, response, session, tmpl_context as c, url
35 from pylons import request, response, session, tmpl_context as c, url
36 from pylons.controllers.util import abort, redirect
36 from pylons.controllers.util import abort, redirect
37 from pylons.i18n.translation import _
37 from pylons.i18n.translation import _
38
38
39 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
39 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
40 from kallithea.lib.vcs.utils import safe_str
40 from kallithea.lib.vcs.utils import safe_str
41 from kallithea.lib.vcs.utils.hgcompat import unionrepo
41 from kallithea.lib.vcs.utils.hgcompat import unionrepo
42 from kallithea.lib import helpers as h
42 from kallithea.lib import helpers as h
43 from kallithea.lib.base import BaseRepoController, render
43 from kallithea.lib.base import BaseRepoController, render
44 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
44 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 from kallithea.lib import diffs
45 from kallithea.lib import diffs
46 from kallithea.model.db import Repository
46 from kallithea.model.db import Repository
47 from kallithea.lib.diffs import LimitedDiffContainer
47 from kallithea.lib.diffs import LimitedDiffContainer
48
48 from kallithea.controllers.changeset import anchor_url, _ignorews_url,\
49 _context_url, get_line_ctx, get_ignore_ws
49
50
50 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
51
52
52
53
53 class CompareController(BaseRepoController):
54 class CompareController(BaseRepoController):
54
55
55 def __before__(self):
56 def __before__(self):
56 super(CompareController, self).__before__()
57 super(CompareController, self).__before__()
57
58
58 def __get_rev_or_redirect(self, ref, repo, redirect_after=True,
59 def __get_rev_or_redirect(self, ref, repo, redirect_after=True,
59 partial=False):
60 partial=False):
60 """
61 """
61 Safe way to get changeset if error occur it redirects to changeset with
62 Safe way to get changeset if error occur it redirects to changeset with
62 proper message. If partial is set then don't do redirect raise Exception
63 proper message. If partial is set then don't do redirect raise Exception
63 instead
64 instead
64
65
65 :param ref:
66 :param ref:
66 :param repo:
67 :param repo:
67 :param redirect_after:
68 :param redirect_after:
68 :param partial:
69 :param partial:
69 """
70 """
70 rev = ref[1] # default and used for git
71 rev = ref[1] # default and used for git
71 if repo.scm_instance.alias == 'hg':
72 if repo.scm_instance.alias == 'hg':
72 # lookup up the exact node id
73 # lookup up the exact node id
73 _revset_predicates = {
74 _revset_predicates = {
74 'branch': 'branch',
75 'branch': 'branch',
75 'book': 'bookmark',
76 'book': 'bookmark',
76 'tag': 'tag',
77 'tag': 'tag',
77 'rev': 'id',
78 'rev': 'id',
78 }
79 }
79 rev_spec = "max(%s(%%s))" % _revset_predicates[ref[0]]
80 rev_spec = "max(%s(%%s))" % _revset_predicates[ref[0]]
80 revs = repo.scm_instance._repo.revs(rev_spec, safe_str(ref[1]))
81 revs = repo.scm_instance._repo.revs(rev_spec, safe_str(ref[1]))
81 if revs:
82 if revs:
82 rev = revs[-1]
83 rev = revs[-1]
83 # else: TODO: just report 'not found'
84 # else: TODO: just report 'not found'
84
85
85 try:
86 try:
86 return repo.scm_instance.get_changeset(rev).raw_id
87 return repo.scm_instance.get_changeset(rev).raw_id
87 except EmptyRepositoryError, e:
88 except EmptyRepositoryError, e:
88 if not redirect_after:
89 if not redirect_after:
89 return None
90 return None
90 h.flash(h.literal(_('There are no changesets yet')),
91 h.flash(h.literal(_('There are no changesets yet')),
91 category='warning')
92 category='warning')
92 redirect(url('summary_home', repo_name=repo.repo_name))
93 redirect(url('summary_home', repo_name=repo.repo_name))
93
94
94 except RepositoryError, e:
95 except RepositoryError, e:
95 log.error(traceback.format_exc())
96 log.error(traceback.format_exc())
96 h.flash(safe_str(e), category='warning')
97 h.flash(safe_str(e), category='warning')
97 if not partial:
98 if not partial:
98 redirect(h.url('summary_home', repo_name=repo.repo_name))
99 redirect(h.url('summary_home', repo_name=repo.repo_name))
99 raise HTTPBadRequest()
100 raise HTTPBadRequest()
100
101
101 def _get_changesets(self, alias, org_repo, org_rev, other_repo, other_rev):
102 def _get_changesets(self, alias, org_repo, org_rev, other_repo, other_rev):
102 """
103 """
103 Returns lists of changesets that can be merged from org_repo@org_rev
104 Returns lists of changesets that can be merged from org_repo@org_rev
104 to other_repo@other_rev
105 to other_repo@other_rev
105 ... and the other way
106 ... and the other way
106 ... and the ancestor that would be used for merge
107 ... and the ancestor that would be used for merge
107
108
108 :param org_repo: repo object, that is most likely the orginal repo we forked from
109 :param org_repo: repo object, that is most likely the orginal repo we forked from
109 :param org_rev: the revision we want our compare to be made
110 :param org_rev: the revision we want our compare to be made
110 :param other_repo: repo object, mostl likely the fork of org_repo. It hass
111 :param other_repo: repo object, mostl likely the fork of org_repo. It hass
111 all changesets that we need to obtain
112 all changesets that we need to obtain
112 :param other_rev: revision we want out compare to be made on other_repo
113 :param other_rev: revision we want out compare to be made on other_repo
113 """
114 """
114 ancestor = None
115 ancestor = None
115 if org_rev == other_rev:
116 if org_rev == other_rev:
116 org_changesets = []
117 org_changesets = []
117 other_changesets = []
118 other_changesets = []
118 ancestor = org_rev
119 ancestor = org_rev
119
120
120 elif alias == 'hg':
121 elif alias == 'hg':
121 #case two independent repos
122 #case two independent repos
122 if org_repo != other_repo:
123 if org_repo != other_repo:
123 hgrepo = unionrepo.unionrepository(other_repo.baseui,
124 hgrepo = unionrepo.unionrepository(other_repo.baseui,
124 other_repo.path,
125 other_repo.path,
125 org_repo.path)
126 org_repo.path)
126 # all ancestors of other_rev will be in other_repo and
127 # all ancestors of other_rev will be in other_repo and
127 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
128 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
128
129
129 #no remote compare do it on the same repository
130 #no remote compare do it on the same repository
130 else:
131 else:
131 hgrepo = other_repo._repo
132 hgrepo = other_repo._repo
132
133
133 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
134 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
134 if ancestors:
135 if ancestors:
135 # pick arbitrary ancestor - but there is usually only one
136 # pick arbitrary ancestor - but there is usually only one
136 ancestor = hgrepo[ancestors[0]].hex()
137 ancestor = hgrepo[ancestors[0]].hex()
137
138
138 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
139 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
139 other_rev, org_rev, org_rev)
140 other_rev, org_rev, org_rev)
140 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
141 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
141 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
142 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
142 org_rev, other_rev, other_rev)
143 org_rev, other_rev, other_rev)
143
144
144 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
145 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
145
146
146 elif alias == 'git':
147 elif alias == 'git':
147 if org_repo != other_repo:
148 if org_repo != other_repo:
148 from dulwich.repo import Repo
149 from dulwich.repo import Repo
149 from dulwich.client import SubprocessGitClient
150 from dulwich.client import SubprocessGitClient
150
151
151 gitrepo = Repo(org_repo.path)
152 gitrepo = Repo(org_repo.path)
152 SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
153 SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
153
154
154 gitrepo_remote = Repo(other_repo.path)
155 gitrepo_remote = Repo(other_repo.path)
155 SubprocessGitClient(thin_packs=False).fetch(org_repo.path, gitrepo_remote)
156 SubprocessGitClient(thin_packs=False).fetch(org_repo.path, gitrepo_remote)
156
157
157 revs = []
158 revs = []
158 for x in gitrepo_remote.get_walker(include=[other_rev],
159 for x in gitrepo_remote.get_walker(include=[other_rev],
159 exclude=[org_rev]):
160 exclude=[org_rev]):
160 revs.append(x.commit.id)
161 revs.append(x.commit.id)
161
162
162 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
163 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
163 if other_changesets:
164 if other_changesets:
164 ancestor = other_changesets[0].parents[0].raw_id
165 ancestor = other_changesets[0].parents[0].raw_id
165 else:
166 else:
166 # no changesets from other repo, ancestor is the other_rev
167 # no changesets from other repo, ancestor is the other_rev
167 ancestor = other_rev
168 ancestor = other_rev
168
169
169 else:
170 else:
170 so, se = org_repo.run_git_command(
171 so, se = org_repo.run_git_command(
171 'log --reverse --pretty="format: %%H" -s %s..%s'
172 'log --reverse --pretty="format: %%H" -s %s..%s'
172 % (org_rev, other_rev)
173 % (org_rev, other_rev)
173 )
174 )
174 other_changesets = [org_repo.get_changeset(cs)
175 other_changesets = [org_repo.get_changeset(cs)
175 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
176 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
176 org_changesets = []
177 org_changesets = []
177
178
178 else:
179 else:
179 raise Exception('Bad alias only git and hg is allowed')
180 raise Exception('Bad alias only git and hg is allowed')
180
181
181 return other_changesets, org_changesets, ancestor
182 return other_changesets, org_changesets, ancestor
182
183
183 @LoginRequired()
184 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
186 'repository.admin')
186 def index(self, repo_name):
187 def index(self, repo_name):
187 c.compare_home = True
188 c.compare_home = True
188 org_repo = c.db_repo.repo_name
189 org_repo = c.db_repo.repo_name
189 other_repo = request.GET.get('other_repo', org_repo)
190 other_repo = request.GET.get('other_repo', org_repo)
190 c.org_repo = Repository.get_by_repo_name(org_repo)
191 c.org_repo = Repository.get_by_repo_name(org_repo)
191 c.other_repo = Repository.get_by_repo_name(other_repo)
192 c.other_repo = Repository.get_by_repo_name(other_repo)
192 c.org_ref_name = c.other_ref_name = _('Select changeset')
193 c.org_ref_name = c.other_ref_name = _('Select changeset')
193 return render('compare/compare_diff.html')
194 return render('compare/compare_diff.html')
194
195
195 @LoginRequired()
196 @LoginRequired()
196 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
197 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
197 'repository.admin')
198 'repository.admin')
198 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
199 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
199 # org_ref will be evaluated in org_repo
200 # org_ref will be evaluated in org_repo
200 org_repo = c.db_repo.repo_name
201 org_repo = c.db_repo.repo_name
201 org_ref = (org_ref_type, org_ref_name)
202 org_ref = (org_ref_type, org_ref_name)
202 # other_ref will be evaluated in other_repo
203 # other_ref will be evaluated in other_repo
203 other_ref = (other_ref_type, other_ref_name)
204 other_ref = (other_ref_type, other_ref_name)
204 other_repo = request.GET.get('other_repo', org_repo)
205 other_repo = request.GET.get('other_repo', org_repo)
205 # If merge is True:
206 # If merge is True:
206 # Show what org would get if merged with other:
207 # Show what org would get if merged with other:
207 # List changesets that are ancestors of other but not of org.
208 # List changesets that are ancestors of other but not of org.
208 # New changesets in org is thus ignored.
209 # New changesets in org is thus ignored.
209 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
210 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
210 # If merge is False:
211 # If merge is False:
211 # Make a raw diff from org to other, no matter if related or not.
212 # Make a raw diff from org to other, no matter if related or not.
212 # Changesets in one and not in the other will be ignored
213 # Changesets in one and not in the other will be ignored
213 merge = bool(request.GET.get('merge'))
214 merge = bool(request.GET.get('merge'))
214 # fulldiff disables cut_off_limit
215 # fulldiff disables cut_off_limit
215 c.fulldiff = request.GET.get('fulldiff')
216 c.fulldiff = request.GET.get('fulldiff')
216 # partial uses compare_cs.html template directly
217 # partial uses compare_cs.html template directly
217 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
218 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
218 # as_form puts hidden input field with changeset revisions
219 # as_form puts hidden input field with changeset revisions
219 c.as_form = partial and request.GET.get('as_form')
220 c.as_form = partial and request.GET.get('as_form')
220 # swap url for compare_diff page - never partial and never as_form
221 # swap url for compare_diff page - never partial and never as_form
221 c.swap_url = h.url('compare_url',
222 c.swap_url = h.url('compare_url',
222 repo_name=other_repo,
223 repo_name=other_repo,
223 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
224 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
224 other_repo=org_repo,
225 other_repo=org_repo,
225 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
226 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
226 merge=merge or '')
227 merge=merge or '')
227
228
229 # set callbacks for generating markup for icons
230 c.ignorews_url = _ignorews_url
231 c.context_url = _context_url
232 ignore_whitespace = request.GET.get('ignorews') == '1'
233 line_context = request.GET.get('context', 3)
234
228 org_repo = Repository.get_by_repo_name(org_repo)
235 org_repo = Repository.get_by_repo_name(org_repo)
229 other_repo = Repository.get_by_repo_name(other_repo)
236 other_repo = Repository.get_by_repo_name(other_repo)
230
237
231 if org_repo is None:
238 if org_repo is None:
232 msg = 'Could not find org repo %s' % org_repo
239 msg = 'Could not find org repo %s' % org_repo
233 log.error(msg)
240 log.error(msg)
234 h.flash(msg, category='error')
241 h.flash(msg, category='error')
235 return redirect(url('compare_home', repo_name=c.repo_name))
242 return redirect(url('compare_home', repo_name=c.repo_name))
236
243
237 if other_repo is None:
244 if other_repo is None:
238 msg = 'Could not find other repo %s' % other_repo
245 msg = 'Could not find other repo %s' % other_repo
239 log.error(msg)
246 log.error(msg)
240 h.flash(msg, category='error')
247 h.flash(msg, category='error')
241 return redirect(url('compare_home', repo_name=c.repo_name))
248 return redirect(url('compare_home', repo_name=c.repo_name))
242
249
243 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
250 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
244 msg = 'compare of two different kind of remote repos not available'
251 msg = 'compare of two different kind of remote repos not available'
245 log.error(msg)
252 log.error(msg)
246 h.flash(msg, category='error')
253 h.flash(msg, category='error')
247 return redirect(url('compare_home', repo_name=c.repo_name))
254 return redirect(url('compare_home', repo_name=c.repo_name))
248
255
249 c.org_rev = self.__get_rev_or_redirect(ref=org_ref, repo=org_repo, partial=partial)
256 c.org_rev = self.__get_rev_or_redirect(ref=org_ref, repo=org_repo, partial=partial)
250 c.other_rev = self.__get_rev_or_redirect(ref=other_ref, repo=other_repo, partial=partial)
257 c.other_rev = self.__get_rev_or_redirect(ref=other_ref, repo=other_repo, partial=partial)
251
258
252 c.compare_home = False
259 c.compare_home = False
253 c.org_repo = org_repo
260 c.org_repo = org_repo
254 c.other_repo = other_repo
261 c.other_repo = other_repo
255 c.org_ref_name = org_ref_name
262 c.org_ref_name = org_ref_name
256 c.other_ref_name = other_ref_name
263 c.other_ref_name = other_ref_name
257 c.org_ref_type = org_ref_type
264 c.org_ref_type = org_ref_type
258 c.other_ref_type = other_ref_type
265 c.other_ref_type = other_ref_type
259
266
260 c.cs_ranges, c.cs_ranges_org, c.ancestor = self._get_changesets(
267 c.cs_ranges, c.cs_ranges_org, c.ancestor = self._get_changesets(
261 org_repo.scm_instance.alias, org_repo.scm_instance, c.org_rev,
268 org_repo.scm_instance.alias, org_repo.scm_instance, c.org_rev,
262 other_repo.scm_instance, c.other_rev)
269 other_repo.scm_instance, c.other_rev)
263 c.statuses = c.db_repo.statuses(
270 c.statuses = c.db_repo.statuses(
264 [x.raw_id for x in c.cs_ranges])
271 [x.raw_id for x in c.cs_ranges])
265
272
266 if partial:
273 if partial:
267 return render('compare/compare_cs.html')
274 return render('compare/compare_cs.html')
268 if merge and c.ancestor:
275 if merge and c.ancestor:
269 # case we want a simple diff without incoming changesets,
276 # case we want a simple diff without incoming changesets,
270 # previewing what will be merged.
277 # previewing what will be merged.
271 # Make the diff on the other repo (which is known to have other_rev)
278 # Make the diff on the other repo (which is known to have other_rev)
272 log.debug('Using ancestor %s as rev1 instead of %s'
279 log.debug('Using ancestor %s as rev1 instead of %s'
273 % (c.ancestor, c.org_rev))
280 % (c.ancestor, c.org_rev))
274 rev1 = c.ancestor
281 rev1 = c.ancestor
275 org_repo = other_repo
282 org_repo = other_repo
276 else: # comparing tips, not necessarily linearly related
283 else: # comparing tips, not necessarily linearly related
277 if merge:
284 if merge:
278 log.error('Unable to find ancestor revision')
285 log.error('Unable to find ancestor revision')
279 if org_repo != other_repo:
286 if org_repo != other_repo:
280 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
287 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
281 raise HTTPNotFound
288 raise HTTPNotFound
282 rev1 = c.org_rev
289 rev1 = c.org_rev
283
290
284 diff_limit = self.cut_off_limit if not c.fulldiff else None
291 diff_limit = self.cut_off_limit if not c.fulldiff else None
285
292
286 log.debug('running diff between %s and %s in %s'
293 log.debug('running diff between %s and %s in %s'
287 % (rev1, c.other_rev, org_repo.scm_instance.path))
294 % (rev1, c.other_rev, org_repo.scm_instance.path))
288 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.other_rev)
295 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.other_rev,
296 ignore_whitespace=ignore_whitespace,
297 context=line_context)
289
298
290 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
299 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
291 diff_limit=diff_limit)
300 diff_limit=diff_limit)
292 _parsed = diff_processor.prepare()
301 _parsed = diff_processor.prepare()
293
302
294 c.limited_diff = False
303 c.limited_diff = False
295 if isinstance(_parsed, LimitedDiffContainer):
304 if isinstance(_parsed, LimitedDiffContainer):
296 c.limited_diff = True
305 c.limited_diff = True
297
306
298 c.files = []
307 c.files = []
299 c.changes = {}
308 c.changes = {}
300 c.lines_added = 0
309 c.lines_added = 0
301 c.lines_deleted = 0
310 c.lines_deleted = 0
302 for f in _parsed:
311 for f in _parsed:
303 st = f['stats']
312 st = f['stats']
304 if not st['binary']:
313 if not st['binary']:
305 c.lines_added += st['added']
314 c.lines_added += st['added']
306 c.lines_deleted += st['deleted']
315 c.lines_deleted += st['deleted']
307 fid = h.FID('', f['filename'])
316 fid = h.FID('', f['filename'])
308 c.files.append([fid, f['operation'], f['filename'], f['stats']])
317 c.files.append([fid, f['operation'], f['filename'], f['stats']])
309 htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
318 htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
310 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
319 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
311
320
312 return render('compare/compare_diff.html')
321 return render('compare/compare_diff.html')
@@ -1,581 +1,589 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.pullrequests
15 kallithea.controllers.pullrequests
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pull requests controller for Kallithea for initializing pull requests
18 pull requests controller for Kallithea for initializing pull requests
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: May 7, 2012
22 :created_on: May 7, 2012
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31
31
32 from webob.exc import HTTPNotFound, HTTPForbidden
32 from webob.exc import HTTPNotFound, HTTPForbidden
33 from collections import defaultdict
33 from collections import defaultdict
34 from itertools import groupby
34 from itertools import groupby
35
35
36 from pylons import request, tmpl_context as c, url
36 from pylons import request, tmpl_context as c, url
37 from pylons.controllers.util import redirect
37 from pylons.controllers.util import redirect
38 from pylons.i18n.translation import _
38 from pylons.i18n.translation import _
39
39
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.utils import action_logger, jsonify
47 from kallithea.lib.utils import action_logger, jsonify
48 from kallithea.lib.vcs.utils import safe_str
48 from kallithea.lib.vcs.utils import safe_str
49 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
49 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
50 from kallithea.lib.diffs import LimitedDiffContainer
50 from kallithea.lib.diffs import LimitedDiffContainer
51 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
51 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
52 PullRequestReviewers
52 PullRequestReviewers
53 from kallithea.model.pull_request import PullRequestModel
53 from kallithea.model.pull_request import PullRequestModel
54 from kallithea.model.meta import Session
54 from kallithea.model.meta import Session
55 from kallithea.model.repo import RepoModel
55 from kallithea.model.repo import RepoModel
56 from kallithea.model.comment import ChangesetCommentsModel
56 from kallithea.model.comment import ChangesetCommentsModel
57 from kallithea.model.changeset_status import ChangesetStatusModel
57 from kallithea.model.changeset_status import ChangesetStatusModel
58 from kallithea.model.forms import PullRequestForm
58 from kallithea.model.forms import PullRequestForm
59 from kallithea.lib.utils2 import safe_int
59 from kallithea.lib.utils2 import safe_int
60 from kallithea.controllers.changeset import anchor_url, _ignorews_url,\
61 _context_url, get_line_ctx, get_ignore_ws
60
62
61 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
62
64
63
65
64 class PullrequestsController(BaseRepoController):
66 class PullrequestsController(BaseRepoController):
65
67
66 def __before__(self):
68 def __before__(self):
67 super(PullrequestsController, self).__before__()
69 super(PullrequestsController, self).__before__()
68 repo_model = RepoModel()
70 repo_model = RepoModel()
69 c.users_array = repo_model.get_users_js()
71 c.users_array = repo_model.get_users_js()
70 c.user_groups_array = repo_model.get_user_groups_js()
72 c.user_groups_array = repo_model.get_user_groups_js()
71
73
72 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
74 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
73 """return a structure with repo's interesting changesets, suitable for
75 """return a structure with repo's interesting changesets, suitable for
74 the selectors in pullrequest.html
76 the selectors in pullrequest.html
75
77
76 rev: a revision that must be in the list somehow and selected by default
78 rev: a revision that must be in the list somehow and selected by default
77 branch: a branch that must be in the list and selected by default - even if closed
79 branch: a branch that must be in the list and selected by default - even if closed
78 branch_rev: a revision of which peers should be preferred and available."""
80 branch_rev: a revision of which peers should be preferred and available."""
79 # list named branches that has been merged to this named branch - it should probably merge back
81 # list named branches that has been merged to this named branch - it should probably merge back
80 peers = []
82 peers = []
81
83
82 if rev:
84 if rev:
83 rev = safe_str(rev)
85 rev = safe_str(rev)
84
86
85 if branch:
87 if branch:
86 branch = safe_str(branch)
88 branch = safe_str(branch)
87
89
88 if branch_rev:
90 if branch_rev:
89 branch_rev = safe_str(branch_rev)
91 branch_rev = safe_str(branch_rev)
90 # not restricting to merge() would also get branch point and be better
92 # not restricting to merge() would also get branch point and be better
91 # (especially because it would get the branch point) ... but is currently too expensive
93 # (especially because it would get the branch point) ... but is currently too expensive
92 otherbranches = {}
94 otherbranches = {}
93 for i in repo._repo.revs(
95 for i in repo._repo.revs(
94 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
96 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
95 branch_rev, branch_rev):
97 branch_rev, branch_rev):
96 cs = repo.get_changeset(i)
98 cs = repo.get_changeset(i)
97 otherbranches[cs.branch] = cs.raw_id
99 otherbranches[cs.branch] = cs.raw_id
98 for abranch, node in otherbranches.iteritems():
100 for abranch, node in otherbranches.iteritems():
99 selected = 'branch:%s:%s' % (abranch, node)
101 selected = 'branch:%s:%s' % (abranch, node)
100 peers.append((selected, abranch))
102 peers.append((selected, abranch))
101
103
102 selected = None
104 selected = None
103
105
104 branches = []
106 branches = []
105 for abranch, branchrev in repo.branches.iteritems():
107 for abranch, branchrev in repo.branches.iteritems():
106 n = 'branch:%s:%s' % (abranch, branchrev)
108 n = 'branch:%s:%s' % (abranch, branchrev)
107 branches.append((n, abranch))
109 branches.append((n, abranch))
108 if rev == branchrev:
110 if rev == branchrev:
109 selected = n
111 selected = n
110 if branch == abranch:
112 if branch == abranch:
111 selected = n
113 selected = n
112 branch = None
114 branch = None
113
115
114 if branch: # branch not in list - it is probably closed
116 if branch: # branch not in list - it is probably closed
115 revs = repo._repo.revs('max(branch(%s))', branch)
117 revs = repo._repo.revs('max(branch(%s))', branch)
116 if revs:
118 if revs:
117 cs = repo.get_changeset(revs[0])
119 cs = repo.get_changeset(revs[0])
118 selected = 'branch:%s:%s' % (branch, cs.raw_id)
120 selected = 'branch:%s:%s' % (branch, cs.raw_id)
119 branches.append((selected, branch))
121 branches.append((selected, branch))
120
122
121 bookmarks = []
123 bookmarks = []
122 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
124 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
123 n = 'book:%s:%s' % (bookmark, bookmarkrev)
125 n = 'book:%s:%s' % (bookmark, bookmarkrev)
124 bookmarks.append((n, bookmark))
126 bookmarks.append((n, bookmark))
125 if rev == bookmarkrev:
127 if rev == bookmarkrev:
126 selected = n
128 selected = n
127
129
128 tags = []
130 tags = []
129 for tag, tagrev in repo.tags.iteritems():
131 for tag, tagrev in repo.tags.iteritems():
130 n = 'tag:%s:%s' % (tag, tagrev)
132 n = 'tag:%s:%s' % (tag, tagrev)
131 tags.append((n, tag))
133 tags.append((n, tag))
132 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
134 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
133 selected = n
135 selected = n
134
136
135 # prio 1: rev was selected as existing entry above
137 # prio 1: rev was selected as existing entry above
136
138
137 # prio 2: create special entry for rev; rev _must_ be used
139 # prio 2: create special entry for rev; rev _must_ be used
138 specials = []
140 specials = []
139 if rev and selected is None:
141 if rev and selected is None:
140 selected = 'rev:%s:%s' % (rev, rev)
142 selected = 'rev:%s:%s' % (rev, rev)
141 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
143 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
142
144
143 # prio 3: most recent peer branch
145 # prio 3: most recent peer branch
144 if peers and not selected:
146 if peers and not selected:
145 selected = peers[0][0][0]
147 selected = peers[0][0][0]
146
148
147 # prio 4: tip revision
149 # prio 4: tip revision
148 if not selected:
150 if not selected:
149 if h.is_hg(repo):
151 if h.is_hg(repo):
150 if 'tip' in repo.tags:
152 if 'tip' in repo.tags:
151 selected = 'tag:tip:%s' % repo.tags['tip']
153 selected = 'tag:tip:%s' % repo.tags['tip']
152 else:
154 else:
153 selected = 'tag:null:0'
155 selected = 'tag:null:0'
154 tags.append((selected, 'null'))
156 tags.append((selected, 'null'))
155 else:
157 else:
156 if 'master' in repo.branches:
158 if 'master' in repo.branches:
157 selected = 'branch:master:%s' % repo.branches['master']
159 selected = 'branch:master:%s' % repo.branches['master']
158 else:
160 else:
159 k, v = repo.branches.items()[0]
161 k, v = repo.branches.items()[0]
160 selected = 'branch:%s:%s' % (k, v)
162 selected = 'branch:%s:%s' % (k, v)
161
163
162 groups = [(specials, _("Special")),
164 groups = [(specials, _("Special")),
163 (peers, _("Peer branches")),
165 (peers, _("Peer branches")),
164 (bookmarks, _("Bookmarks")),
166 (bookmarks, _("Bookmarks")),
165 (branches, _("Branches")),
167 (branches, _("Branches")),
166 (tags, _("Tags")),
168 (tags, _("Tags")),
167 ]
169 ]
168 return [g for g in groups if g[0]], selected
170 return [g for g in groups if g[0]], selected
169
171
170 def _get_is_allowed_change_status(self, pull_request):
172 def _get_is_allowed_change_status(self, pull_request):
171 owner = self.authuser.user_id == pull_request.user_id
173 owner = self.authuser.user_id == pull_request.user_id
172 reviewer = self.authuser.user_id in [x.user_id for x in
174 reviewer = self.authuser.user_id in [x.user_id for x in
173 pull_request.reviewers]
175 pull_request.reviewers]
174 return self.authuser.admin or owner or reviewer
176 return self.authuser.admin or owner or reviewer
175
177
176 def _load_compare_data(self, pull_request, enable_comments=True):
178 def _load_compare_data(self, pull_request, enable_comments=True):
177 """
179 """
178 Load context data needed for generating compare diff
180 Load context data needed for generating compare diff
179
181
180 :param pull_request:
182 :param pull_request:
181 """
183 """
182 c.org_repo = pull_request.org_repo
184 c.org_repo = pull_request.org_repo
183 (c.org_ref_type,
185 (c.org_ref_type,
184 c.org_ref_name,
186 c.org_ref_name,
185 c.org_rev) = pull_request.org_ref.split(':')
187 c.org_rev) = pull_request.org_ref.split(':')
186
188
187 c.other_repo = c.org_repo
189 c.other_repo = c.org_repo
188 (c.other_ref_type,
190 (c.other_ref_type,
189 c.other_ref_name,
191 c.other_ref_name,
190 c.other_rev) = pull_request.other_ref.split(':')
192 c.other_rev) = pull_request.other_ref.split(':')
191
193
192 c.cs_ranges = [c.org_repo.get_changeset(x) for x in pull_request.revisions]
194 c.cs_ranges = [c.org_repo.get_changeset(x) for x in pull_request.revisions]
193 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
195 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
194
196
195 c.statuses = c.org_repo.statuses([x.raw_id for x in c.cs_ranges])
197 c.statuses = c.org_repo.statuses([x.raw_id for x in c.cs_ranges])
196
198
199 ignore_whitespace = request.GET.get('ignorews') == '1'
200 line_context = request.GET.get('context', 3)
201 c.ignorews_url = _ignorews_url
202 c.context_url = _context_url
197 c.fulldiff = request.GET.get('fulldiff')
203 c.fulldiff = request.GET.get('fulldiff')
198 diff_limit = self.cut_off_limit if not c.fulldiff else None
204 diff_limit = self.cut_off_limit if not c.fulldiff else None
199
205
200 # we swap org/other ref since we run a simple diff on one repo
206 # we swap org/other ref since we run a simple diff on one repo
201 log.debug('running diff between %s and %s in %s'
207 log.debug('running diff between %s and %s in %s'
202 % (c.other_rev, c.org_rev, c.org_repo.scm_instance.path))
208 % (c.other_rev, c.org_rev, c.org_repo.scm_instance.path))
203 txtdiff = c.org_repo.scm_instance.get_diff(rev1=safe_str(c.other_rev), rev2=safe_str(c.org_rev))
209 txtdiff = c.org_repo.scm_instance.get_diff(rev1=safe_str(c.other_rev), rev2=safe_str(c.org_rev),
210 ignore_whitespace=ignore_whitespace,
211 context=line_context)
204
212
205 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
213 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
206 diff_limit=diff_limit)
214 diff_limit=diff_limit)
207 _parsed = diff_processor.prepare()
215 _parsed = diff_processor.prepare()
208
216
209 c.limited_diff = False
217 c.limited_diff = False
210 if isinstance(_parsed, LimitedDiffContainer):
218 if isinstance(_parsed, LimitedDiffContainer):
211 c.limited_diff = True
219 c.limited_diff = True
212
220
213 c.files = []
221 c.files = []
214 c.changes = {}
222 c.changes = {}
215 c.lines_added = 0
223 c.lines_added = 0
216 c.lines_deleted = 0
224 c.lines_deleted = 0
217
225
218 for f in _parsed:
226 for f in _parsed:
219 st = f['stats']
227 st = f['stats']
220 c.lines_added += st['added']
228 c.lines_added += st['added']
221 c.lines_deleted += st['deleted']
229 c.lines_deleted += st['deleted']
222 fid = h.FID('', f['filename'])
230 fid = h.FID('', f['filename'])
223 c.files.append([fid, f['operation'], f['filename'], f['stats']])
231 c.files.append([fid, f['operation'], f['filename'], f['stats']])
224 htmldiff = diff_processor.as_html(enable_comments=enable_comments,
232 htmldiff = diff_processor.as_html(enable_comments=enable_comments,
225 parsed_lines=[f])
233 parsed_lines=[f])
226 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
234 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
227
235
228 @LoginRequired()
236 @LoginRequired()
229 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
230 'repository.admin')
238 'repository.admin')
231 def show_all(self, repo_name):
239 def show_all(self, repo_name):
232 c.from_ = request.GET.get('from_') or ''
240 c.from_ = request.GET.get('from_') or ''
233 c.closed = request.GET.get('closed') or ''
241 c.closed = request.GET.get('closed') or ''
234 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
242 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
235 c.repo_name = repo_name
243 c.repo_name = repo_name
236 p = safe_int(request.GET.get('page', 1), 1)
244 p = safe_int(request.GET.get('page', 1), 1)
237
245
238 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
246 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
239
247
240 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
248 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
241
249
242 if request.environ.get('HTTP_X_PARTIAL_XHR'):
250 if request.environ.get('HTTP_X_PARTIAL_XHR'):
243 return c.pullrequest_data
251 return c.pullrequest_data
244
252
245 return render('/pullrequests/pullrequest_show_all.html')
253 return render('/pullrequests/pullrequest_show_all.html')
246
254
247 @LoginRequired()
255 @LoginRequired()
248 def show_my(self): # my_account_my_pullrequests
256 def show_my(self): # my_account_my_pullrequests
249 c.show_closed = request.GET.get('pr_show_closed')
257 c.show_closed = request.GET.get('pr_show_closed')
250 return render('/pullrequests/pullrequest_show_my.html')
258 return render('/pullrequests/pullrequest_show_my.html')
251
259
252 @NotAnonymous()
260 @NotAnonymous()
253 def show_my_data(self):
261 def show_my_data(self):
254 c.show_closed = request.GET.get('pr_show_closed')
262 c.show_closed = request.GET.get('pr_show_closed')
255
263
256 def _filter(pr):
264 def _filter(pr):
257 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
265 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
258 if not c.show_closed:
266 if not c.show_closed:
259 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
267 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
260 return s
268 return s
261
269
262 c.my_pull_requests = _filter(PullRequest.query()\
270 c.my_pull_requests = _filter(PullRequest.query()\
263 .filter(PullRequest.user_id ==
271 .filter(PullRequest.user_id ==
264 self.authuser.user_id)\
272 self.authuser.user_id)\
265 .all())
273 .all())
266
274
267 c.participate_in_pull_requests = _filter(PullRequest.query()\
275 c.participate_in_pull_requests = _filter(PullRequest.query()\
268 .join(PullRequestReviewers)\
276 .join(PullRequestReviewers)\
269 .filter(PullRequestReviewers.user_id ==
277 .filter(PullRequestReviewers.user_id ==
270 self.authuser.user_id)\
278 self.authuser.user_id)\
271 )
279 )
272
280
273 return render('/pullrequests/pullrequest_show_my_data.html')
281 return render('/pullrequests/pullrequest_show_my_data.html')
274
282
275 @LoginRequired()
283 @LoginRequired()
276 @NotAnonymous()
284 @NotAnonymous()
277 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
285 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
278 'repository.admin')
286 'repository.admin')
279 def index(self):
287 def index(self):
280 org_repo = c.db_repo
288 org_repo = c.db_repo
281
289
282 try:
290 try:
283 org_repo.scm_instance.get_changeset()
291 org_repo.scm_instance.get_changeset()
284 except EmptyRepositoryError, e:
292 except EmptyRepositoryError, e:
285 h.flash(h.literal(_('There are no changesets yet')),
293 h.flash(h.literal(_('There are no changesets yet')),
286 category='warning')
294 category='warning')
287 redirect(url('summary_home', repo_name=org_repo.repo_name))
295 redirect(url('summary_home', repo_name=org_repo.repo_name))
288
296
289 org_rev = request.GET.get('rev_end')
297 org_rev = request.GET.get('rev_end')
290 # rev_start is not directly useful - its parent could however be used
298 # rev_start is not directly useful - its parent could however be used
291 # as default for other and thus give a simple compare view
299 # as default for other and thus give a simple compare view
292 #other_rev = request.POST.get('rev_start')
300 #other_rev = request.POST.get('rev_start')
293 branch = request.GET.get('branch')
301 branch = request.GET.get('branch')
294
302
295 c.org_repos = []
303 c.org_repos = []
296 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
304 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
297 c.default_org_repo = org_repo.repo_name
305 c.default_org_repo = org_repo.repo_name
298 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, rev=org_rev, branch=branch)
306 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, rev=org_rev, branch=branch)
299
307
300 c.other_repos = []
308 c.other_repos = []
301 other_repos_info = {}
309 other_repos_info = {}
302
310
303 def add_other_repo(repo, branch_rev=None):
311 def add_other_repo(repo, branch_rev=None):
304 if repo.repo_name in other_repos_info: # shouldn't happen
312 if repo.repo_name in other_repos_info: # shouldn't happen
305 return
313 return
306 c.other_repos.append((repo.repo_name, repo.repo_name))
314 c.other_repos.append((repo.repo_name, repo.repo_name))
307 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
315 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
308 other_repos_info[repo.repo_name] = {
316 other_repos_info[repo.repo_name] = {
309 'user': dict(user_id=repo.user.user_id,
317 'user': dict(user_id=repo.user.user_id,
310 username=repo.user.username,
318 username=repo.user.username,
311 firstname=repo.user.firstname,
319 firstname=repo.user.firstname,
312 lastname=repo.user.lastname,
320 lastname=repo.user.lastname,
313 gravatar_link=h.gravatar_url(repo.user.email, 14)),
321 gravatar_link=h.gravatar_url(repo.user.email, 14)),
314 'description': repo.description.split('\n', 1)[0],
322 'description': repo.description.split('\n', 1)[0],
315 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
323 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
316 }
324 }
317
325
318 # add org repo to other so we can open pull request against peer branches on itself
326 # add org repo to other so we can open pull request against peer branches on itself
319 add_other_repo(org_repo, branch_rev=org_rev)
327 add_other_repo(org_repo, branch_rev=org_rev)
320 c.default_other_repo = org_repo.repo_name
328 c.default_other_repo = org_repo.repo_name
321
329
322 # gather forks and add to this list ... even though it is rare to
330 # gather forks and add to this list ... even though it is rare to
323 # request forks to pull from their parent
331 # request forks to pull from their parent
324 for fork in org_repo.forks:
332 for fork in org_repo.forks:
325 add_other_repo(fork)
333 add_other_repo(fork)
326
334
327 # add parents of this fork also, but only if it's not empty
335 # add parents of this fork also, but only if it's not empty
328 if org_repo.parent and org_repo.parent.scm_instance.revisions:
336 if org_repo.parent and org_repo.parent.scm_instance.revisions:
329 add_other_repo(org_repo.parent)
337 add_other_repo(org_repo.parent)
330 c.default_other_repo = org_repo.parent.repo_name
338 c.default_other_repo = org_repo.parent.repo_name
331
339
332 c.default_other_repo_info = other_repos_info[c.default_other_repo]
340 c.default_other_repo_info = other_repos_info[c.default_other_repo]
333 c.other_repos_info = json.dumps(other_repos_info)
341 c.other_repos_info = json.dumps(other_repos_info)
334
342
335 return render('/pullrequests/pullrequest.html')
343 return render('/pullrequests/pullrequest.html')
336
344
337 @LoginRequired()
345 @LoginRequired()
338 @NotAnonymous()
346 @NotAnonymous()
339 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
347 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
340 'repository.admin')
348 'repository.admin')
341 def create(self, repo_name):
349 def create(self, repo_name):
342 repo = RepoModel()._get_repo(repo_name)
350 repo = RepoModel()._get_repo(repo_name)
343 try:
351 try:
344 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
352 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
345 except formencode.Invalid, errors:
353 except formencode.Invalid, errors:
346 log.error(traceback.format_exc())
354 log.error(traceback.format_exc())
347 if errors.error_dict.get('revisions'):
355 if errors.error_dict.get('revisions'):
348 msg = 'Revisions: %s' % errors.error_dict['revisions']
356 msg = 'Revisions: %s' % errors.error_dict['revisions']
349 elif errors.error_dict.get('pullrequest_title'):
357 elif errors.error_dict.get('pullrequest_title'):
350 msg = _('Pull request requires a title with min. 3 chars')
358 msg = _('Pull request requires a title with min. 3 chars')
351 else:
359 else:
352 msg = _('Error creating pull request: %s') % errors.msg
360 msg = _('Error creating pull request: %s') % errors.msg
353
361
354 h.flash(msg, 'error')
362 h.flash(msg, 'error')
355 return redirect(url('pullrequest_home', repo_name=repo_name)) ## would rather just go back to form ...
363 return redirect(url('pullrequest_home', repo_name=repo_name)) ## would rather just go back to form ...
356
364
357 org_repo = _form['org_repo']
365 org_repo = _form['org_repo']
358 org_ref = _form['org_ref'] # will end with merge_rev but have symbolic name
366 org_ref = _form['org_ref'] # will end with merge_rev but have symbolic name
359 other_repo = _form['other_repo']
367 other_repo = _form['other_repo']
360 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev'] # could be calculated from other_ref ...
368 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev'] # could be calculated from other_ref ...
361 revisions = [x for x in reversed(_form['revisions'])]
369 revisions = [x for x in reversed(_form['revisions'])]
362 reviewers = _form['review_members']
370 reviewers = _form['review_members']
363
371
364 title = _form['pullrequest_title']
372 title = _form['pullrequest_title']
365 if not title:
373 if not title:
366 title = '%s#%s to %s' % (org_repo, org_ref.split(':', 2)[1], other_repo)
374 title = '%s#%s to %s' % (org_repo, org_ref.split(':', 2)[1], other_repo)
367 description = _form['pullrequest_desc']
375 description = _form['pullrequest_desc']
368 try:
376 try:
369 pull_request = PullRequestModel().create(
377 pull_request = PullRequestModel().create(
370 self.authuser.user_id, org_repo, org_ref, other_repo,
378 self.authuser.user_id, org_repo, org_ref, other_repo,
371 other_ref, revisions, reviewers, title, description
379 other_ref, revisions, reviewers, title, description
372 )
380 )
373 Session().commit()
381 Session().commit()
374 h.flash(_('Successfully opened new pull request'),
382 h.flash(_('Successfully opened new pull request'),
375 category='success')
383 category='success')
376 except Exception:
384 except Exception:
377 h.flash(_('Error occurred while creating pull request'),
385 h.flash(_('Error occurred while creating pull request'),
378 category='error')
386 category='error')
379 log.error(traceback.format_exc())
387 log.error(traceback.format_exc())
380 return redirect(url('pullrequest_home', repo_name=repo_name))
388 return redirect(url('pullrequest_home', repo_name=repo_name))
381
389
382 return redirect(url('pullrequest_show', repo_name=other_repo,
390 return redirect(url('pullrequest_show', repo_name=other_repo,
383 pull_request_id=pull_request.pull_request_id))
391 pull_request_id=pull_request.pull_request_id))
384
392
385 @LoginRequired()
393 @LoginRequired()
386 @NotAnonymous()
394 @NotAnonymous()
387 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
388 'repository.admin')
396 'repository.admin')
389 @jsonify
397 @jsonify
390 def update(self, repo_name, pull_request_id):
398 def update(self, repo_name, pull_request_id):
391 pull_request = PullRequest.get_or_404(pull_request_id)
399 pull_request = PullRequest.get_or_404(pull_request_id)
392 if pull_request.is_closed():
400 if pull_request.is_closed():
393 raise HTTPForbidden()
401 raise HTTPForbidden()
394 #only owner or admin can update it
402 #only owner or admin can update it
395 owner = pull_request.author.user_id == c.authuser.user_id
403 owner = pull_request.author.user_id == c.authuser.user_id
396 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
404 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
397 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
405 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
398 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
406 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
399 request.POST.get('reviewers_ids', '').split(',')))
407 request.POST.get('reviewers_ids', '').split(',')))
400
408
401 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
409 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
402 Session().commit()
410 Session().commit()
403 return True
411 return True
404 raise HTTPForbidden()
412 raise HTTPForbidden()
405
413
406 @LoginRequired()
414 @LoginRequired()
407 @NotAnonymous()
415 @NotAnonymous()
408 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
416 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
409 'repository.admin')
417 'repository.admin')
410 @jsonify
418 @jsonify
411 def delete(self, repo_name, pull_request_id):
419 def delete(self, repo_name, pull_request_id):
412 pull_request = PullRequest.get_or_404(pull_request_id)
420 pull_request = PullRequest.get_or_404(pull_request_id)
413 #only owner can delete it !
421 #only owner can delete it !
414 if pull_request.author.user_id == c.authuser.user_id:
422 if pull_request.author.user_id == c.authuser.user_id:
415 PullRequestModel().delete(pull_request)
423 PullRequestModel().delete(pull_request)
416 Session().commit()
424 Session().commit()
417 h.flash(_('Successfully deleted pull request'),
425 h.flash(_('Successfully deleted pull request'),
418 category='success')
426 category='success')
419 return redirect(url('my_account_pullrequests'))
427 return redirect(url('my_account_pullrequests'))
420 raise HTTPForbidden()
428 raise HTTPForbidden()
421
429
422 @LoginRequired()
430 @LoginRequired()
423 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
424 'repository.admin')
432 'repository.admin')
425 def show(self, repo_name, pull_request_id):
433 def show(self, repo_name, pull_request_id):
426 repo_model = RepoModel()
434 repo_model = RepoModel()
427 c.users_array = repo_model.get_users_js()
435 c.users_array = repo_model.get_users_js()
428 c.user_groups_array = repo_model.get_user_groups_js()
436 c.user_groups_array = repo_model.get_user_groups_js()
429 c.pull_request = PullRequest.get_or_404(pull_request_id)
437 c.pull_request = PullRequest.get_or_404(pull_request_id)
430 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
438 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
431 cc_model = ChangesetCommentsModel()
439 cc_model = ChangesetCommentsModel()
432 cs_model = ChangesetStatusModel()
440 cs_model = ChangesetStatusModel()
433 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
441 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
434 pull_request=c.pull_request,
442 pull_request=c.pull_request,
435 with_revisions=True)
443 with_revisions=True)
436
444
437 cs_statuses = defaultdict(list)
445 cs_statuses = defaultdict(list)
438 for st in _cs_statuses:
446 for st in _cs_statuses:
439 cs_statuses[st.author.username] += [st]
447 cs_statuses[st.author.username] += [st]
440
448
441 c.pull_request_reviewers = []
449 c.pull_request_reviewers = []
442 c.pull_request_pending_reviewers = []
450 c.pull_request_pending_reviewers = []
443 for o in c.pull_request.reviewers:
451 for o in c.pull_request.reviewers:
444 st = cs_statuses.get(o.user.username, None)
452 st = cs_statuses.get(o.user.username, None)
445 if st:
453 if st:
446 sorter = lambda k: k.version
454 sorter = lambda k: k.version
447 st = [(x, list(y)[0])
455 st = [(x, list(y)[0])
448 for x, y in (groupby(sorted(st, key=sorter), sorter))]
456 for x, y in (groupby(sorted(st, key=sorter), sorter))]
449 else:
457 else:
450 c.pull_request_pending_reviewers.append(o.user)
458 c.pull_request_pending_reviewers.append(o.user)
451 c.pull_request_reviewers.append([o.user, st])
459 c.pull_request_reviewers.append([o.user, st])
452
460
453 # pull_requests repo_name we opened it against
461 # pull_requests repo_name we opened it against
454 # ie. other_repo must match
462 # ie. other_repo must match
455 if repo_name != c.pull_request.other_repo.repo_name:
463 if repo_name != c.pull_request.other_repo.repo_name:
456 raise HTTPNotFound
464 raise HTTPNotFound
457
465
458 # load compare data into template context
466 # load compare data into template context
459 enable_comments = not c.pull_request.is_closed()
467 enable_comments = not c.pull_request.is_closed()
460 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
468 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
461
469
462 # inline comments
470 # inline comments
463 c.inline_cnt = 0
471 c.inline_cnt = 0
464 c.inline_comments = cc_model.get_inline_comments(
472 c.inline_comments = cc_model.get_inline_comments(
465 c.db_repo.repo_id,
473 c.db_repo.repo_id,
466 pull_request=pull_request_id)
474 pull_request=pull_request_id)
467 # count inline comments
475 # count inline comments
468 for __, lines in c.inline_comments:
476 for __, lines in c.inline_comments:
469 for comments in lines.values():
477 for comments in lines.values():
470 c.inline_cnt += len(comments)
478 c.inline_cnt += len(comments)
471 # comments
479 # comments
472 c.comments = cc_model.get_comments(c.db_repo.repo_id,
480 c.comments = cc_model.get_comments(c.db_repo.repo_id,
473 pull_request=pull_request_id)
481 pull_request=pull_request_id)
474
482
475 # (badly named) pull-request status calculation based on reviewer votes
483 # (badly named) pull-request status calculation based on reviewer votes
476 c.current_changeset_status = cs_model.calculate_status(
484 c.current_changeset_status = cs_model.calculate_status(
477 c.pull_request_reviewers,
485 c.pull_request_reviewers,
478 )
486 )
479 c.changeset_statuses = ChangesetStatus.STATUSES
487 c.changeset_statuses = ChangesetStatus.STATUSES
480
488
481 c.as_form = False
489 c.as_form = False
482 c.ancestor = None # there is one - but right here we don't know which
490 c.ancestor = None # there is one - but right here we don't know which
483 return render('/pullrequests/pullrequest_show.html')
491 return render('/pullrequests/pullrequest_show.html')
484
492
485 @LoginRequired()
493 @LoginRequired()
486 @NotAnonymous()
494 @NotAnonymous()
487 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
495 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
488 'repository.admin')
496 'repository.admin')
489 @jsonify
497 @jsonify
490 def comment(self, repo_name, pull_request_id):
498 def comment(self, repo_name, pull_request_id):
491 pull_request = PullRequest.get_or_404(pull_request_id)
499 pull_request = PullRequest.get_or_404(pull_request_id)
492 if pull_request.is_closed():
500 if pull_request.is_closed():
493 raise HTTPForbidden()
501 raise HTTPForbidden()
494
502
495 status = request.POST.get('changeset_status')
503 status = request.POST.get('changeset_status')
496 change_status = request.POST.get('change_changeset_status')
504 change_status = request.POST.get('change_changeset_status')
497 text = request.POST.get('text')
505 text = request.POST.get('text')
498 close_pr = request.POST.get('save_close')
506 close_pr = request.POST.get('save_close')
499
507
500 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
508 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
501 if status and change_status and allowed_to_change_status:
509 if status and change_status and allowed_to_change_status:
502 _def = (_('Status change -> %s')
510 _def = (_('Status change -> %s')
503 % ChangesetStatus.get_status_lbl(status))
511 % ChangesetStatus.get_status_lbl(status))
504 if close_pr:
512 if close_pr:
505 _def = _('Closing with') + ' ' + _def
513 _def = _('Closing with') + ' ' + _def
506 text = text or _def
514 text = text or _def
507 comm = ChangesetCommentsModel().create(
515 comm = ChangesetCommentsModel().create(
508 text=text,
516 text=text,
509 repo=c.db_repo.repo_id,
517 repo=c.db_repo.repo_id,
510 user=c.authuser.user_id,
518 user=c.authuser.user_id,
511 pull_request=pull_request_id,
519 pull_request=pull_request_id,
512 f_path=request.POST.get('f_path'),
520 f_path=request.POST.get('f_path'),
513 line_no=request.POST.get('line'),
521 line_no=request.POST.get('line'),
514 status_change=(ChangesetStatus.get_status_lbl(status)
522 status_change=(ChangesetStatus.get_status_lbl(status)
515 if status and change_status
523 if status and change_status
516 and allowed_to_change_status else None),
524 and allowed_to_change_status else None),
517 closing_pr=close_pr
525 closing_pr=close_pr
518 )
526 )
519
527
520 action_logger(self.authuser,
528 action_logger(self.authuser,
521 'user_commented_pull_request:%s' % pull_request_id,
529 'user_commented_pull_request:%s' % pull_request_id,
522 c.db_repo, self.ip_addr, self.sa)
530 c.db_repo, self.ip_addr, self.sa)
523
531
524 if allowed_to_change_status:
532 if allowed_to_change_status:
525 # get status if set !
533 # get status if set !
526 if status and change_status:
534 if status and change_status:
527 ChangesetStatusModel().set_status(
535 ChangesetStatusModel().set_status(
528 c.db_repo.repo_id,
536 c.db_repo.repo_id,
529 status,
537 status,
530 c.authuser.user_id,
538 c.authuser.user_id,
531 comm,
539 comm,
532 pull_request=pull_request_id
540 pull_request=pull_request_id
533 )
541 )
534
542
535 if close_pr:
543 if close_pr:
536 if status in ['rejected', 'approved']:
544 if status in ['rejected', 'approved']:
537 PullRequestModel().close_pull_request(pull_request_id)
545 PullRequestModel().close_pull_request(pull_request_id)
538 action_logger(self.authuser,
546 action_logger(self.authuser,
539 'user_closed_pull_request:%s' % pull_request_id,
547 'user_closed_pull_request:%s' % pull_request_id,
540 c.db_repo, self.ip_addr, self.sa)
548 c.db_repo, self.ip_addr, self.sa)
541 else:
549 else:
542 h.flash(_('Closing pull request on other statuses than '
550 h.flash(_('Closing pull request on other statuses than '
543 'rejected or approved forbidden'),
551 'rejected or approved forbidden'),
544 category='warning')
552 category='warning')
545
553
546 Session().commit()
554 Session().commit()
547
555
548 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
556 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
549 return redirect(h.url('pullrequest_show', repo_name=repo_name,
557 return redirect(h.url('pullrequest_show', repo_name=repo_name,
550 pull_request_id=pull_request_id))
558 pull_request_id=pull_request_id))
551
559
552 data = {
560 data = {
553 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
561 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
554 }
562 }
555 if comm:
563 if comm:
556 c.co = comm
564 c.co = comm
557 data.update(comm.get_dict())
565 data.update(comm.get_dict())
558 data.update({'rendered_text':
566 data.update({'rendered_text':
559 render('changeset/changeset_comment_block.html')})
567 render('changeset/changeset_comment_block.html')})
560
568
561 return data
569 return data
562
570
563 @LoginRequired()
571 @LoginRequired()
564 @NotAnonymous()
572 @NotAnonymous()
565 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
573 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
566 'repository.admin')
574 'repository.admin')
567 @jsonify
575 @jsonify
568 def delete_comment(self, repo_name, comment_id):
576 def delete_comment(self, repo_name, comment_id):
569 co = ChangesetComment.get(comment_id)
577 co = ChangesetComment.get(comment_id)
570 if co.pull_request.is_closed():
578 if co.pull_request.is_closed():
571 #don't allow deleting comments on closed pull request
579 #don't allow deleting comments on closed pull request
572 raise HTTPForbidden()
580 raise HTTPForbidden()
573
581
574 owner = co.author.user_id == c.authuser.user_id
582 owner = co.author.user_id == c.authuser.user_id
575 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
583 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
576 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
584 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
577 ChangesetCommentsModel().delete(comment=co)
585 ChangesetCommentsModel().delete(comment=co)
578 Session().commit()
586 Session().commit()
579 return True
587 return True
580 else:
588 else:
581 raise HTTPForbidden()
589 raise HTTPForbidden()
@@ -1,92 +1,94 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ##usage:
2 ##usage:
3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 ## ${diff_block.diff_block(change)}
4 ## ${diff_block.diff_block(change)}
5 ##
5 ##
6 <%def name="diff_block(change)">
6 <%def name="diff_block(change)">
7 <div class="diff-collapse">
7 <div class="diff-collapse">
8 <span target="${'diff-container-%s' % (id(change))}" class="diff-collapse-button">&uarr; ${_('Collapse diff')} &uarr;</span>
8 <span target="${'diff-container-%s' % (id(change))}" class="diff-collapse-button">&uarr; ${_('Collapse diff')} &uarr;</span>
9 </div>
9 </div>
10 <div class="diff-container" id="${'diff-container-%s' % (id(change))}">
10 <div class="diff-container" id="${'diff-container-%s' % (id(change))}">
11 %for FID,(cs1, cs2, change, path, diff, stats) in change.iteritems():
11 %for FID,(cs1, cs2, change, path, diff, stats) in change.iteritems():
12 <div id="${FID}_target" style="clear:both;margin-top:25px"></div>
12 <div id="${FID}_target" style="clear:both;margin-top:25px"></div>
13 <div id="${FID}" class="diffblock margined comm">
13 <div id="${FID}" class="diffblock margined comm">
14 <div class="code-header">
14 <div class="code-header">
15 <div class="changeset_header">
15 <div class="changeset_header">
16 <div class="changeset_file">
16 <div class="changeset_file">
17 ${h.link_to_if(change!='D',h.safe_unicode(path),h.url('files_home',repo_name=c.repo_name,
17 ${h.link_to_if(change!='D',h.safe_unicode(path),h.url('files_home',repo_name=c.repo_name,
18 revision=cs2,f_path=h.safe_unicode(path)))}
18 revision=cs2,f_path=h.safe_unicode(path)))}
19 </div>
19 </div>
20 <div class="diff-actions">
20 <div class="diff-actions">
21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full diff for this file'))}">
21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full diff for this file'))}">
22 <img class="icon" src="${h.url('/images/icons/page_white_go.png')}"/>
22 <img class="icon" src="${h.url('/images/icons/page_white_go.png')}"/>
23 </a>
23 </a>
24 <a href="${h.url('files_diff_2way_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full side-by-side diff for this file'))}">
24 <a href="${h.url('files_diff_2way_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full side-by-side diff for this file'))}">
25 <img class="icon" src="${h.url('/images/icons/application_double.png')}"/>
25 <img class="icon" src="${h.url('/images/icons/application_double.png')}"/>
26 </a>
26 </a>
27 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='raw')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
27 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='raw')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
28 <img class="icon" src="${h.url('/images/icons/page_white.png')}"/>
28 <img class="icon" src="${h.url('/images/icons/page_white.png')}"/>
29 </a>
29 </a>
30 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
30 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
31 <img class="icon" src="${h.url('/images/icons/page_save.png')}"/>
31 <img class="icon" src="${h.url('/images/icons/page_save.png')}"/>
32 </a>
32 </a>
33 ${c.ignorews_url(request.GET, h.FID(cs2,path))}
33 ${c.ignorews_url(request.GET, h.FID(cs2,path))}
34 ${c.context_url(request.GET, h.FID(cs2,path))}
34 ${c.context_url(request.GET, h.FID(cs2,path))}
35 </div>
35 </div>
36 <span style="float:right;margin-top:-3px">
36 <span style="float:right;margin-top:-3px">
37 <label>
37 <label>
38 ${_('Show inline comments')}
38 ${_('Show inline comments')}
39 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(cs2,path))}
39 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(cs2,path))}
40 </label>
40 </label>
41 </span>
41 </span>
42 </div>
42 </div>
43 </div>
43 </div>
44 <div class="code-body">
44 <div class="code-body">
45 <div class="full_f_path" path="${h.safe_unicode(path)}"></div>
45 <div class="full_f_path" path="${h.safe_unicode(path)}"></div>
46 ${diff|n}
46 ${diff|n}
47 </div>
47 </div>
48 </div>
48 </div>
49 %endfor
49 %endfor
50 </div>
50 </div>
51 </%def>
51 </%def>
52
52
53 <%def name="diff_block_simple(change)">
53 <%def name="diff_block_simple(change)">
54
54
55 %for op,filenode_path,diff in change:
55 %for op,filenode_path,diff in change:
56 <div id="${h.FID('',filenode_path)}_target" style="clear:both;margin-top:25px"></div>
56 <div id="${h.FID('',filenode_path)}_target" style="clear:both;margin-top:25px"></div>
57 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
57 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
58 <div class="code-header">
58 <div class="code-header">
59 <div class="changeset_header">
59 <div class="changeset_header">
60 <div class="changeset_file">
60 <div class="changeset_file">
61 ${h.safe_unicode(filenode_path)} |
61 ${h.safe_unicode(filenode_path)} |
62 ## TODO: link to ancestor and head of other instead of exactly other
62 ## TODO: link to ancestor and head of other instead of exactly other
63 %if op == 'A':
63 %if op == 'A':
64 ${_('Added')}
64 ${_('Added')}
65 <a class="spantag" href="${h.url('files_home', repo_name=c.org_repo.repo_name, f_path=filenode_path, revision=c.org_rev)}">${h.short_id(c.org_ref_name) if c.org_ref_type=='rev' else c.org_ref_name}</a>
65 <a class="spantag" href="${h.url('files_home', repo_name=c.org_repo.repo_name, f_path=filenode_path, revision=c.org_rev)}">${h.short_id(c.org_ref_name) if c.org_ref_type=='rev' else c.org_ref_name}</a>
66 %elif op == 'M':
66 %elif op == 'M':
67 <a class="spantag" href="${h.url('files_home', repo_name=c.org_repo.repo_name, f_path=filenode_path, revision=c.org_rev)}">${h.short_id(c.org_ref_name) if c.org_ref_type=='rev' else c.org_ref_name}</a>
67 <a class="spantag" href="${h.url('files_home', repo_name=c.org_repo.repo_name, f_path=filenode_path, revision=c.org_rev)}">${h.short_id(c.org_ref_name) if c.org_ref_type=='rev' else c.org_ref_name}</a>
68 <i class="icon-arrow-right"></i>
68 <i class="icon-arrow-right"></i>
69 <a class="spantag" href="${h.url('files_home', repo_name=c.other_repo.repo_name, f_path=filenode_path, revision=c.other_rev)}">${h.short_id(c.other_ref_name) if c.other_ref_type=='rev' else c.other_ref_name}</a>
69 <a class="spantag" href="${h.url('files_home', repo_name=c.other_repo.repo_name, f_path=filenode_path, revision=c.other_rev)}">${h.short_id(c.other_ref_name) if c.other_ref_type=='rev' else c.other_ref_name}</a>
70 %elif op == 'D':
70 %elif op == 'D':
71 ${_('Deleted')}
71 ${_('Deleted')}
72 <a class="spantag" href="${h.url('files_home', repo_name=c.other_repo.repo_name, f_path=filenode_path, revision=c.other_rev)}">${h.short_id(c.other_ref_name) if c.other_ref_type=='rev' else c.other_ref_name}</a>
72 <a class="spantag" href="${h.url('files_home', repo_name=c.other_repo.repo_name, f_path=filenode_path, revision=c.other_rev)}">${h.short_id(c.other_ref_name) if c.other_ref_type=='rev' else c.other_ref_name}</a>
73 %else:
73 %else:
74 ${op}???
74 ${op}???
75 %endif
75 %endif
76 </div>
76 </div>
77 <div class="diff-actions">
77 <div class="diff-actions">
78 %if c.other_repo.repo_name == c.repo_name:
78 %if c.other_repo.repo_name == c.repo_name:
79 <a href="${h.url('files_diff_2way_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode_path),diff2=c.other_rev,diff1=c.org_rev,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full side-by-side diff for this file'))}">
79 <a href="${h.url('files_diff_2way_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode_path),diff2=c.other_rev,diff1=c.org_rev,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full side-by-side diff for this file'))}">
80 <img class="icon" src="${h.url('/images/icons/application_double.png')}"/>
80 <img class="icon" src="${h.url('/images/icons/application_double.png')}"/>
81 </a>
81 </a>
82 %endif
82 %endif
83 </div>
83 ${c.ignorews_url(request.GET)}
84 ${c.context_url(request.GET)}
85 </div>
84 </div>
86 </div>
85 </div>
87 </div>
86 <div class="code-body">
88 <div class="code-body">
87 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}"></div>
89 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}"></div>
88 ${diff|n}
90 ${diff|n}
89 </div>
91 </div>
90 </div>
92 </div>
91 %endfor
93 %endfor
92 </%def>
94 </%def>
@@ -1,205 +1,208 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 %if c.compare_home:
5 %if c.compare_home:
6 ${_('%s Compare') % c.repo_name}
6 ${_('%s Compare') % c.repo_name}
7 %else:
7 %else:
8 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.org_repo.repo_name, c.org_ref_name)} &gt; ${'%s@%s' % (c.other_repo.repo_name, c.other_ref_name)}
8 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.org_repo.repo_name, c.org_ref_name)} &gt; ${'%s@%s' % (c.other_repo.repo_name, c.other_ref_name)}
9 %endif
9 %endif
10 %if c.site_name:
10 %if c.site_name:
11 &middot; ${c.site_name}
11 &middot; ${c.site_name}
12 %endif
12 %endif
13 </%def>
13 </%def>
14
14
15 <%def name="breadcrumbs_links()">
15 <%def name="breadcrumbs_links()">
16 ${_('Compare revisions')}
16 ${_('Compare revisions')}
17 </%def>
17 </%def>
18
18
19 <%def name="page_nav()">
19 <%def name="page_nav()">
20 ${self.menu('repositories')}
20 ${self.menu('repositories')}
21 </%def>
21 </%def>
22
22
23 <%def name="main()">
23 <%def name="main()">
24 ${self.repo_context_bar('changelog')}
24 ${self.repo_context_bar('changelog')}
25 <div class="box">
25 <div class="box">
26 <!-- box / title -->
26 <!-- box / title -->
27 <div class="title">
27 <div class="title">
28 ${self.breadcrumbs()}
28 ${self.breadcrumbs()}
29 </div>
29 </div>
30 <div class="table">
30 <div class="table">
31 <div id="body" class="diffblock">
31 <div id="body" class="diffblock">
32 <div class="code-header">
32 <div class="code-header">
33 <div>
33 <div>
34 ${h.hidden('compare_org')} <i class="icon-ellipsis-horizontal" style="color: #999; vertical-align: -12px; padding: 0px 0px 0px 2px"></i> ${h.hidden('compare_other')}
34 ${h.hidden('compare_org')} <i class="icon-ellipsis-horizontal" style="color: #999; vertical-align: -12px; padding: 0px 0px 0px 2px"></i> ${h.hidden('compare_other')}
35 %if not c.compare_home:
35 %if not c.compare_home:
36 <a class="btn btn-small" href="${c.swap_url}"><i class="icon-refresh"></i> ${_('Swap')}</a>
36 <a class="btn btn-small" href="${c.swap_url}"><i class="icon-refresh"></i> ${_('Swap')}</a>
37 %endif
37 %endif
38 <div id="compare_revs" class="btn btn-small"><i class="icon-loop"></i> ${_('Compare Revisions')}</div>
38 <div id="compare_revs" class="btn btn-small"><i class="icon-loop"></i> ${_('Compare Revisions')}</div>
39 </div>
39 </div>
40 </div>
40 </div>
41 </div>
41 </div>
42
42
43 %if c.compare_home:
43 %if c.compare_home:
44 <div id="changeset_compare_view_content">
44 <div id="changeset_compare_view_content">
45 <div style="color:#999;font-size: 18px">${_('Compare revisions, branches, bookmarks or tags.')}</div>
45 <div style="color:#999;font-size: 18px">${_('Compare revisions, branches, bookmarks or tags.')}</div>
46 </div>
46 </div>
47 %else:
47 %else:
48 <div id="changeset_compare_view_content">
48 <div id="changeset_compare_view_content">
49 ##CS
49 ##CS
50 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
50 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
51 <%include file="compare_cs.html" />
51 <%include file="compare_cs.html" />
52
52
53 ## FILES
53 ## FILES
54 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
54 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
55
55
56 % if c.limited_diff:
56 % if c.limited_diff:
57 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
57 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
58 % else:
58 % else:
59 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
59 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
60 %endif
60 %endif
61
61
62 ${c.ignorews_url(request.GET)}
63 ${c.context_url(request.GET)}
64
62 </div>
65 </div>
63 <div class="cs_files">
66 <div class="cs_files">
64 %if not c.files:
67 %if not c.files:
65 <span class="empty_data">${_('No files')}</span>
68 <span class="empty_data">${_('No files')}</span>
66 %endif
69 %endif
67 %for fid, change, f, stat in c.files:
70 %for fid, change, f, stat in c.files:
68 <div class="cs_${change}">
71 <div class="cs_${change}">
69 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid, **request.GET.mixed()))}</div>
72 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid, **request.GET.mixed()))}</div>
70 <div class="changes">${h.fancy_file_stats(stat)}</div>
73 <div class="changes">${h.fancy_file_stats(stat)}</div>
71 </div>
74 </div>
72 %endfor
75 %endfor
73 </div>
76 </div>
74 % if c.limited_diff:
77 % if c.limited_diff:
75 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff')}</a></h5>
78 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff')}</a></h5>
76 % endif
79 % endif
77 </div>
80 </div>
78
81
79 ## diff block
82 ## diff block
80 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
83 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
81 %for fid, change, f, stat in c.files:
84 %for fid, change, f, stat in c.files:
82 ${diff_block.diff_block_simple([c.changes[fid]])}
85 ${diff_block.diff_block_simple([c.changes[fid]])}
83 %endfor
86 %endfor
84 % if c.limited_diff:
87 % if c.limited_diff:
85 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff')}</a></h4>
88 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff')}</a></h4>
86 % endif
89 % endif
87 %endif
90 %endif
88 </div>
91 </div>
89
92
90 </div>
93 </div>
91 <script type="text/javascript">
94 <script type="text/javascript">
92
95
93 $(document).ready(function(){
96 $(document).ready(function(){
94 var cache = {}
97 var cache = {}
95 $("#compare_org").select2({
98 $("#compare_org").select2({
96 placeholder: "${'%s@%s' % (c.org_repo.repo_name, c.org_ref_name)}",
99 placeholder: "${'%s@%s' % (c.org_repo.repo_name, c.org_ref_name)}",
97 formatSelection: function(obj){
100 formatSelection: function(obj){
98 return '{0}@{1}'.format("${c.org_repo.repo_name}", obj.text)
101 return '{0}@{1}'.format("${c.org_repo.repo_name}", obj.text)
99 },
102 },
100 dropdownAutoWidth: true,
103 dropdownAutoWidth: true,
101 query: function(query){
104 query: function(query){
102 var key = 'cache';
105 var key = 'cache';
103 var cached = cache[key] ;
106 var cached = cache[key] ;
104 if(cached) {
107 if(cached) {
105 var data = {results: []};
108 var data = {results: []};
106 //filter results
109 //filter results
107 $.each(cached.results, function(){
110 $.each(cached.results, function(){
108 var section = this.text;
111 var section = this.text;
109 var children = [];
112 var children = [];
110 $.each(this.children, function(){
113 $.each(this.children, function(){
111 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
114 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
112 children.push({'id': this.id, 'text': this.text})
115 children.push({'id': this.id, 'text': this.text})
113 }
116 }
114 })
117 })
115 data.results.push({'text': section, 'children': children})
118 data.results.push({'text': section, 'children': children})
116 });
119 });
117 //push the typed in changeset
120 //push the typed in changeset
118 data.results.push({'text':_TM['Specify changeset'],
121 data.results.push({'text':_TM['Specify changeset'],
119 'children': [{'id': query.term, 'text': query.term, 'type': 'rev'}]})
122 'children': [{'id': query.term, 'text': query.term, 'type': 'rev'}]})
120 query.callback(data);
123 query.callback(data);
121 }else{
124 }else{
122 $.ajax({
125 $.ajax({
123 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.org_repo.repo_name}'}),
126 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.org_repo.repo_name}'}),
124 data: {},
127 data: {},
125 dataType: 'json',
128 dataType: 'json',
126 type: 'GET',
129 type: 'GET',
127 success: function(data) {
130 success: function(data) {
128 cache[key] = data;
131 cache[key] = data;
129 query.callback(data);
132 query.callback(data);
130 }
133 }
131 })
134 })
132 }
135 }
133 },
136 },
134 });
137 });
135 $("#compare_other").select2({
138 $("#compare_other").select2({
136 placeholder: "${'%s@%s' % (c.other_repo.repo_name, c.other_ref_name)}",
139 placeholder: "${'%s@%s' % (c.other_repo.repo_name, c.other_ref_name)}",
137 dropdownAutoWidth: true,
140 dropdownAutoWidth: true,
138 formatSelection: function(obj){
141 formatSelection: function(obj){
139 return '{0}@{1}'.format("${c.other_repo.repo_name}", obj.text)
142 return '{0}@{1}'.format("${c.other_repo.repo_name}", obj.text)
140 },
143 },
141 query: function(query){
144 query: function(query){
142 var key = 'cache2';
145 var key = 'cache2';
143 var cached = cache[key] ;
146 var cached = cache[key] ;
144 if(cached) {
147 if(cached) {
145 var data = {results: []};
148 var data = {results: []};
146 //filter results
149 //filter results
147 $.each(cached.results, function(){
150 $.each(cached.results, function(){
148 var section = this.text;
151 var section = this.text;
149 var children = [];
152 var children = [];
150 $.each(this.children, function(){
153 $.each(this.children, function(){
151 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
154 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
152 children.push({'id': this.id, 'text': this.text})
155 children.push({'id': this.id, 'text': this.text})
153 }
156 }
154 })
157 })
155 data.results.push({'text': section, 'children': children})
158 data.results.push({'text': section, 'children': children})
156 });
159 });
157 //push the typed in changeset
160 //push the typed in changeset
158 data.results.push({'text':_TM['Specify changeset'],
161 data.results.push({'text':_TM['Specify changeset'],
159 'children': [{'id': query.term, 'text': query.term, 'type': 'rev'}]})
162 'children': [{'id': query.term, 'text': query.term, 'type': 'rev'}]})
160 query.callback(data);
163 query.callback(data);
161 }else{
164 }else{
162 $.ajax({
165 $.ajax({
163 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.other_repo.repo_name}'}),
166 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.other_repo.repo_name}'}),
164 data: {},
167 data: {},
165 dataType: 'json',
168 dataType: 'json',
166 type: 'GET',
169 type: 'GET',
167 success: function(data) {
170 success: function(data) {
168 cache[key] = data;
171 cache[key] = data;
169 query.callback({results: data.results});
172 query.callback({results: data.results});
170 }
173 }
171 })
174 })
172 }
175 }
173 },
176 },
174 });
177 });
175
178
176 var values_changed = function() {
179 var values_changed = function() {
177 var values = $('#compare_org').select2('data') && $('#compare_other').select2('data');
180 var values = $('#compare_org').select2('data') && $('#compare_other').select2('data');
178 if (values) {
181 if (values) {
179 $('#compare_revs').removeClass("disabled");
182 $('#compare_revs').removeClass("disabled");
180 // TODO: the swap button ... if any
183 // TODO: the swap button ... if any
181 } else {
184 } else {
182 $('#compare_revs').addClass("disabled");
185 $('#compare_revs').addClass("disabled");
183 // TODO: the swap button ... if any
186 // TODO: the swap button ... if any
184 }
187 }
185 }
188 }
186 values_changed();
189 values_changed();
187 $('#compare_org').change(values_changed);
190 $('#compare_org').change(values_changed);
188 $('#compare_other').change(values_changed);
191 $('#compare_other').change(values_changed);
189 $('#compare_revs').on('click', function(e){
192 $('#compare_revs').on('click', function(e){
190 var org = $('#compare_org').select2('data');
193 var org = $('#compare_org').select2('data');
191 var other = $('#compare_other').select2('data');
194 var other = $('#compare_other').select2('data');
192 if (!org || !other) {
195 if (!org || !other) {
193 return;
196 return;
194 }
197 }
195
198
196 var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='__other_ref_type__',org_ref_name='__org__',other_ref_type='__org_ref_type__',other_ref_name='__other__', other_repo=c.other_repo.repo_name)}";
199 var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='__other_ref_type__',org_ref_name='__org__',other_ref_type='__org_ref_type__',other_ref_name='__other__', other_repo=c.other_repo.repo_name)}";
197 var u = compare_url.replace('__other_ref_type__',org.type)
200 var u = compare_url.replace('__other_ref_type__',org.type)
198 .replace('__org__',org.text)
201 .replace('__org__',org.text)
199 .replace('__org_ref_type__',other.type)
202 .replace('__org_ref_type__',other.type)
200 .replace('__other__',other.text);
203 .replace('__other__',other.text);
201 window.location = u;
204 window.location = u;
202 });
205 });
203 });
206 });
204 </script>
207 </script>
205 </%def>
208 </%def>
@@ -1,269 +1,271 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 %if c.site_name:
5 %if c.site_name:
6 &middot; ${c.site_name}
6 &middot; ${c.site_name}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 ${_('Pull request #%s') % c.pull_request.pull_request_id}
11 ${_('Pull request #%s') % c.pull_request.pull_request_id}
12 </%def>
12 </%def>
13
13
14 <%def name="page_nav()">
14 <%def name="page_nav()">
15 ${self.menu('repositories')}
15 ${self.menu('repositories')}
16 </%def>
16 </%def>
17
17
18 <%def name="main()">
18 <%def name="main()">
19 ${self.repo_context_bar('showpullrequest')}
19 ${self.repo_context_bar('showpullrequest')}
20 <div class="box">
20 <div class="box">
21 <!-- box / title -->
21 <!-- box / title -->
22 <div class="title">
22 <div class="title">
23 ${self.breadcrumbs()}
23 ${self.breadcrumbs()}
24 </div>
24 </div>
25
25
26 <div class="form pr-box" style="float: left">
26 <div class="form pr-box" style="float: left">
27 <div class="pr-details-title ${'closed' if c.pull_request.is_closed() else ''}">
27 <div class="pr-details-title ${'closed' if c.pull_request.is_closed() else ''}">
28 ${_('Title')}: ${c.pull_request.title}
28 ${_('Title')}: ${c.pull_request.title}
29 %if c.pull_request.is_closed():
29 %if c.pull_request.is_closed():
30 (${_('Closed')})
30 (${_('Closed')})
31 %endif
31 %endif
32 </div>
32 </div>
33 <div id="summary" class="fields">
33 <div id="summary" class="fields">
34 <div class="field">
34 <div class="field">
35 <div class="label-summary">
35 <div class="label-summary">
36 <label>${_('Review status')}:</label>
36 <label>${_('Review status')}:</label>
37 </div>
37 </div>
38 <div class="input">
38 <div class="input">
39 <div class="changeset-status-container" style="float:none;clear:both">
39 <div class="changeset-status-container" style="float:none;clear:both">
40 %if c.current_changeset_status:
40 %if c.current_changeset_status:
41 <div class="changeset-status-ico" style="padding:0px 4px 0px 0px">
41 <div class="changeset-status-ico" style="padding:0px 4px 0px 0px">
42 <img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" title="${_('Pull request status calculated from votes')}"/></div>
42 <img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" title="${_('Pull request status calculated from votes')}"/></div>
43 <div class="changeset-status-lbl tooltip" title="${_('Pull request status calculated from votes')}">
43 <div class="changeset-status-lbl tooltip" title="${_('Pull request status calculated from votes')}">
44 %if c.pull_request.is_closed():
44 %if c.pull_request.is_closed():
45 ${_('Closed')},
45 ${_('Closed')},
46 %endif
46 %endif
47 ${h.changeset_status_lbl(c.current_changeset_status)}
47 ${h.changeset_status_lbl(c.current_changeset_status)}
48 </div>
48 </div>
49
49
50 %endif
50 %endif
51 </div>
51 </div>
52 </div>
52 </div>
53 </div>
53 </div>
54 <div class="field">
54 <div class="field">
55 <div class="label-summary">
55 <div class="label-summary">
56 <label>${_('Still not reviewed by')}:</label>
56 <label>${_('Still not reviewed by')}:</label>
57 </div>
57 </div>
58 <div class="input">
58 <div class="input">
59 % if len(c.pull_request_pending_reviewers) > 0:
59 % if len(c.pull_request_pending_reviewers) > 0:
60 <div class="tooltip" title="${h.tooltip(', '.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
60 <div class="tooltip" title="${h.tooltip(', '.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
61 %else:
61 %else:
62 <div>${_('Pull request was reviewed by all reviewers')}</div>
62 <div>${_('Pull request was reviewed by all reviewers')}</div>
63 %endif
63 %endif
64 </div>
64 </div>
65 </div>
65 </div>
66 <div class="field">
66 <div class="field">
67 <div class="label-summary">
67 <div class="label-summary">
68 <label>${_('Origin repository')}:</label>
68 <label>${_('Origin repository')}:</label>
69 </div>
69 </div>
70 <div class="input">
70 <div class="input">
71 <div>
71 <div>
72 <span><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
72 <span><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
73
73
74 ## branch link is only valid if it is a branch
74 ## branch link is only valid if it is a branch
75 <span class="spantag"><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name, anchor=c.org_ref_name)}">${c.org_ref_type}: ${c.org_ref_name}</a></span>
75 <span class="spantag"><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name, anchor=c.org_ref_name)}">${c.org_ref_type}: ${c.org_ref_name}</a></span>
76 </div>
76 </div>
77 </div>
77 </div>
78 </div>
78 </div>
79 <div class="field">
79 <div class="field">
80 <div class="label-summary">
80 <div class="label-summary">
81 <label>${_('Pull changes')}:</label>
81 <label>${_('Pull changes')}:</label>
82 </div>
82 </div>
83 <div class="input">
83 <div class="input">
84 <div>
84 <div>
85 ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
85 ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
86 %if h.is_hg(c.pull_request.org_repo):
86 %if h.is_hg(c.pull_request.org_repo):
87 <span style="font-family: monospace">hg pull ${c.pull_request.org_repo.clone_url()} -r ${h.short_id(c.cs_ranges[-1].raw_id)}</span>
87 <span style="font-family: monospace">hg pull ${c.pull_request.org_repo.clone_url()} -r ${h.short_id(c.cs_ranges[-1].raw_id)}</span>
88 %elif h.is_git(c.pull_request.org_repo):
88 %elif h.is_git(c.pull_request.org_repo):
89 <span style="font-family: monospace">git pull ${c.pull_request.org_repo.clone_url()} ${c.pull_request.org_ref_parts[1]}</span>
89 <span style="font-family: monospace">git pull ${c.pull_request.org_repo.clone_url()} ${c.pull_request.org_ref_parts[1]}</span>
90 %endif
90 %endif
91 </div>
91 </div>
92 </div>
92 </div>
93 </div>
93 </div>
94 <div class="field">
94 <div class="field">
95 <div class="label-summary">
95 <div class="label-summary">
96 <label>${_('Description')}:</label>
96 <label>${_('Description')}:</label>
97 </div>
97 </div>
98 <div class="input">
98 <div class="input">
99 <div style="white-space:pre-wrap">${h.urlify_commit(c.pull_request.description, c.repo_name)}</div>
99 <div style="white-space:pre-wrap">${h.urlify_commit(c.pull_request.description, c.repo_name)}</div>
100 </div>
100 </div>
101 </div>
101 </div>
102 <div class="field">
102 <div class="field">
103 <div class="label-summary">
103 <div class="label-summary">
104 <label>${_('Created on')}:</label>
104 <label>${_('Created on')}:</label>
105 </div>
105 </div>
106 <div class="input">
106 <div class="input">
107 <div>${h.fmt_date(c.pull_request.created_on)}</div>
107 <div>${h.fmt_date(c.pull_request.created_on)}</div>
108 </div>
108 </div>
109 </div>
109 </div>
110 </div>
110 </div>
111 </div>
111 </div>
112 ## REVIEWERS
112 ## REVIEWERS
113 <div style="float:left; border-left:1px dashed #eee">
113 <div style="float:left; border-left:1px dashed #eee">
114 <div class="pr-details-title">${_('Pull request reviewers')}</div>
114 <div class="pr-details-title">${_('Pull request reviewers')}</div>
115 <div id="reviewers" style="padding:0px 0px 5px 10px">
115 <div id="reviewers" style="padding:0px 0px 5px 10px">
116 ## members goes here !
116 ## members goes here !
117 <div>
117 <div>
118 <ul id="review_members" class="group_members">
118 <ul id="review_members" class="group_members">
119 %for member,status in c.pull_request_reviewers:
119 %for member,status in c.pull_request_reviewers:
120 <li id="reviewer_${member.user_id}">
120 <li id="reviewer_${member.user_id}">
121 <div class="reviewers_member">
121 <div class="reviewers_member">
122 <div class="reviewer_status tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
122 <div class="reviewer_status tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
123 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
123 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
124 </div>
124 </div>
125 <div class="reviewer_gravatar gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
125 <div class="reviewer_gravatar gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
126 <div style="float:left;">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
126 <div style="float:left;">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
127 <input type="hidden" value="${member.user_id}" name="review_members" />
127 <input type="hidden" value="${member.user_id}" name="review_members" />
128 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.user_id == c.authuser.user_id):
128 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.user_id == c.authuser.user_id):
129 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
129 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
130 <i class="icon-remove-sign" style="color: #FF4444;"></i>
130 <i class="icon-remove-sign" style="color: #FF4444;"></i>
131 </div>
131 </div>
132 %endif
132 %endif
133 </div>
133 </div>
134 </li>
134 </li>
135 %endfor
135 %endfor
136 </ul>
136 </ul>
137 </div>
137 </div>
138 %if not c.pull_request.is_closed():
138 %if not c.pull_request.is_closed():
139 <div class='ac'>
139 <div class='ac'>
140 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.author.user_id == c.authuser.user_id:
140 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.author.user_id == c.authuser.user_id:
141 <div class="reviewer_ac">
141 <div class="reviewer_ac">
142 ${h.text('user', class_='yui-ac-input')}
142 ${h.text('user', class_='yui-ac-input')}
143 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
143 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
144 <div id="reviewers_container"></div>
144 <div id="reviewers_container"></div>
145 </div>
145 </div>
146 <div style="padding:0px 10px">
146 <div style="padding:0px 10px">
147 <span id="update_pull_request" class="btn btn-mini">${_('Save Changes')}</span>
147 <span id="update_pull_request" class="btn btn-mini">${_('Save Changes')}</span>
148 </div>
148 </div>
149 %endif
149 %endif
150 </div>
150 </div>
151 %endif
151 %endif
152 </div>
152 </div>
153 </div>
153 </div>
154
154
155 <div style="overflow: auto; clear: both">
155 <div style="overflow: auto; clear: both">
156 ##DIFF
156 ##DIFF
157 <div class="table" style="float:left;clear:none">
157 <div class="table" style="float:left;clear:none">
158 <div id="body" class="diffblock">
158 <div class="diffblock">
159 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
159 <div style="padding:5px">
160 ${_('Compare view')}
161 </div>
160 </div>
162 </div>
161 <div id="changeset_compare_view_content">
163 <div id="changeset_compare_view_content">
162 ##CS
164 ##CS
163 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
165 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
164 <%include file="/compare/compare_cs.html" />
166 <%include file="/compare/compare_cs.html" />
165
167
166 ## FILES
168 ## FILES
167 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
169 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
168
170
169 % if c.limited_diff:
171 % if c.limited_diff:
170 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
172 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
171 % else:
173 % else:
172 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
174 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
173 %endif
175 %endif
174
176
175 </div>
177 </div>
176 <div class="cs_files">
178 <div class="cs_files">
177 %if not c.files:
179 %if not c.files:
178 <span class="empty_data">${_('No files')}</span>
180 <span class="empty_data">${_('No files')}</span>
179 %endif
181 %endif
180 %for fid, change, f, stat in c.files:
182 %for fid, change, f, stat in c.files:
181 <div class="cs_${change}">
183 <div class="cs_${change}">
182 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
184 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
183 <div class="changes">${h.fancy_file_stats(stat)}</div>
185 <div class="changes">${h.fancy_file_stats(stat)}</div>
184 </div>
186 </div>
185 %endfor
187 %endfor
186 </div>
188 </div>
187 % if c.limited_diff:
189 % if c.limited_diff:
188 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
190 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
189 % endif
191 % endif
190 </div>
192 </div>
191 </div>
193 </div>
192 </div>
194 </div>
193 <script>
195 <script>
194 var _USERS_AC_DATA = ${c.users_array|n};
196 var _USERS_AC_DATA = ${c.users_array|n};
195 var _GROUPS_AC_DATA = ${c.user_groups_array|n};
197 var _GROUPS_AC_DATA = ${c.user_groups_array|n};
196 // TODO: switch this to pyroutes
198 // TODO: switch this to pyroutes
197 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
199 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
198 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
200 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
199
201
200 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
202 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
201 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
203 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
202 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
204 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
203
205
204 </script>
206 </script>
205
207
206 ## diff block
208 ## diff block
207 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
209 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
208 %for fid, change, f, stat in c.files:
210 %for fid, change, f, stat in c.files:
209 ${diff_block.diff_block_simple([c.changes[fid]])}
211 ${diff_block.diff_block_simple([c.changes[fid]])}
210 %endfor
212 %endfor
211 % if c.limited_diff:
213 % if c.limited_diff:
212 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h4>
214 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h4>
213 % endif
215 % endif
214
216
215
217
216 ## template for inline comment form
218 ## template for inline comment form
217 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
219 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
218 ${comment.comment_inline_form()}
220 ${comment.comment_inline_form()}
219
221
220 ## render comments and inlines
222 ## render comments and inlines
221 ${comment.generate_comments(include_pr=True)}
223 ${comment.generate_comments(include_pr=True)}
222
224
223 % if not c.pull_request.is_closed():
225 % if not c.pull_request.is_closed():
224 ## main comment form and it status
226 ## main comment form and it status
225 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
227 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
226 pull_request_id=c.pull_request.pull_request_id),
228 pull_request_id=c.pull_request.pull_request_id),
227 c.current_changeset_status,
229 c.current_changeset_status,
228 is_pr=True, change_status=c.allowed_to_change_status)}
230 is_pr=True, change_status=c.allowed_to_change_status)}
229 %endif
231 %endif
230
232
231 <script type="text/javascript">
233 <script type="text/javascript">
232 YUE.onDOMReady(function(){
234 YUE.onDOMReady(function(){
233 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
235 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
234
236
235 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
237 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
236 var show = 'none';
238 var show = 'none';
237 var target = e.currentTarget;
239 var target = e.currentTarget;
238 if(target.checked){
240 if(target.checked){
239 var show = ''
241 var show = ''
240 }
242 }
241 var boxid = YUD.getAttribute(target,'id_for');
243 var boxid = YUD.getAttribute(target,'id_for');
242 var comments = YUQ('#{0} .inline-comments'.format(boxid));
244 var comments = YUQ('#{0} .inline-comments'.format(boxid));
243 for(c in comments){
245 for(c in comments){
244 YUD.setStyle(comments[c],'display',show);
246 YUD.setStyle(comments[c],'display',show);
245 }
247 }
246 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
248 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
247 for(c in btns){
249 for(c in btns){
248 YUD.setStyle(btns[c],'display',show);
250 YUD.setStyle(btns[c],'display',show);
249 }
251 }
250 })
252 })
251
253
252 YUE.on(YUQ('.line'),'click',function(e){
254 YUE.on(YUQ('.line'),'click',function(e){
253 var tr = e.currentTarget;
255 var tr = e.currentTarget;
254 injectInlineForm(tr);
256 injectInlineForm(tr);
255 });
257 });
256
258
257 // inject comments into they proper positions
259 // inject comments into they proper positions
258 var file_comments = YUQ('.inline-comment-placeholder');
260 var file_comments = YUQ('.inline-comment-placeholder');
259 renderInlineComments(file_comments);
261 renderInlineComments(file_comments);
260
262
261 YUE.on(YUD.get('update_pull_request'),'click',function(e){
263 YUE.on(YUD.get('update_pull_request'),'click',function(e){
262 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
264 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
263 })
265 })
264 })
266 })
265 </script>
267 </script>
266
268
267 </div>
269 </div>
268
270
269 </%def>
271 </%def>
General Comments 0
You need to be logged in to leave comments. Login now