##// END OF EJS Templates
diffs: drop diffs.differ...
Mads Kiilerich -
r3718:b2575bdb beta
parent child Browse files
Show More
@@ -1,188 +1,191 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.compare
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 compare controller for pylons showing differences between two
7 7 repos, branches, bookmarks or tips
8 8
9 9 :created_on: May 6, 2012
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28
29 29 from webob.exc import HTTPNotFound
30 30 from pylons import request, response, session, tmpl_context as c, url
31 31 from pylons.controllers.util import abort, redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.base import BaseRepoController, render
37 37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 38 from rhodecode.lib import diffs
39 39
40 40 from rhodecode.model.db import Repository
41 41 from rhodecode.model.pull_request import PullRequestModel
42 42 from webob.exc import HTTPBadRequest
43 43 from rhodecode.lib.diffs import LimitedDiffContainer
44 44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class CompareController(BaseRepoController):
50 50
51 51 @LoginRequired()
52 52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
53 53 'repository.admin')
54 54 def __before__(self):
55 55 super(CompareController, self).__before__()
56 56
57 57 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
58 58 partial=False):
59 59 """
60 60 Safe way to get changeset if error occur it redirects to changeset with
61 61 proper message. If partial is set then don't do redirect raise Exception
62 62 instead
63 63
64 64 :param rev: revision to fetch
65 65 :param repo: repo instance
66 66 """
67 67
68 68 try:
69 69 type_, rev = rev
70 70 return repo.scm_instance.get_changeset(rev)
71 71 except EmptyRepositoryError, e:
72 72 if not redirect_after:
73 73 return None
74 74 h.flash(h.literal(_('There are no changesets yet')),
75 75 category='warning')
76 76 redirect(url('summary_home', repo_name=repo.repo_name))
77 77
78 78 except RepositoryError, e:
79 79 log.error(traceback.format_exc())
80 80 h.flash(str(e), category='warning')
81 81 if not partial:
82 82 redirect(h.url('summary_home', repo_name=repo.repo_name))
83 83 raise HTTPBadRequest()
84 84
85 85 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
86 86 # org_ref will be evaluated in org_repo
87 87 org_repo = c.rhodecode_db_repo.repo_name
88 88 org_ref = (org_ref_type, org_ref)
89 89 # other_ref will be evaluated in other_repo
90 90 other_ref = (other_ref_type, other_ref)
91 91 other_repo = request.GET.get('other_repo', org_repo)
92 92 # If merge is True:
93 93 # Show what org would get if merged with other:
94 94 # List changesets that are ancestors of other but not of org.
95 95 # New changesets in org is thus ignored.
96 96 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
97 97 # If merge is False:
98 98 # Make a raw diff from org to other, no matter if related or not.
99 99 # Changesets in one and not in the other will be ignored
100 100 merge = bool(request.GET.get('merge'))
101 101 # fulldiff disables cut_off_limit
102 102 c.fulldiff = request.GET.get('fulldiff')
103 103 # partial uses compare_cs.html template directly
104 104 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
105 105 # as_form puts hidden input field with changeset revisions
106 106 c.as_form = partial and request.GET.get('as_form')
107 107 # swap url for compare_diff page - never partial and never as_form
108 108 c.swap_url = h.url('compare_url',
109 109 repo_name=other_repo,
110 110 org_ref_type=other_ref[0], org_ref=other_ref[1],
111 111 other_repo=org_repo,
112 112 other_ref_type=org_ref[0], other_ref=org_ref[1],
113 113 merge=merge or '')
114 114
115 115 org_repo = Repository.get_by_repo_name(org_repo)
116 116 other_repo = Repository.get_by_repo_name(other_repo)
117 117
118 118 if org_repo is None:
119 119 log.error('Could not find org repo %s' % org_repo)
120 120 raise HTTPNotFound
121 121 if other_repo is None:
122 122 log.error('Could not find other repo %s' % other_repo)
123 123 raise HTTPNotFound
124 124
125 125 if org_repo != other_repo and h.is_git(org_repo):
126 126 log.error('compare of two remote repos not available for GIT REPOS')
127 127 raise HTTPNotFound
128 128
129 129 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
130 130 log.error('compare of two different kind of remote repos not available')
131 131 raise HTTPNotFound
132 132
133 133 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
134 134 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
135 135
136 136 c.org_repo = org_repo
137 137 c.other_repo = other_repo
138 138 c.org_ref = org_ref[1]
139 139 c.other_ref = other_ref[1]
140 140 c.org_ref_type = org_ref[0]
141 141 c.other_ref_type = other_ref[0]
142 142
143 143 c.cs_ranges, c.ancestor = PullRequestModel().get_compare_data(
144 144 org_repo, org_ref, other_repo, other_ref, merge)
145 145
146 146 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
147 147 c.cs_ranges])
148 148 if partial:
149 149 assert c.ancestor
150 150 return render('compare/compare_cs.html')
151 151
152 152 if c.ancestor:
153 153 assert merge
154 154 # case we want a simple diff without incoming changesets,
155 155 # previewing what will be merged.
156 156 # Make the diff on the other repo (which is known to have other_ref)
157 157 log.debug('Using ancestor %s as org_ref instead of %s'
158 158 % (c.ancestor, org_ref))
159 159 org_ref = ('rev', c.ancestor)
160 160 org_repo = other_repo
161 161
162 162 diff_limit = self.cut_off_limit if not c.fulldiff else None
163 163
164 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref)
164 log.debug('running diff between %s@%s and %s@%s'
165 % (org_repo.scm_instance.path, org_ref,
166 other_repo.scm_instance.path, other_ref))
167 _diff = org_repo.scm_instance.get_diff(rev1=safe_str(org_ref[1]), rev2=safe_str(other_ref[1]))
165 168
166 169 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
167 170 diff_limit=diff_limit)
168 171 _parsed = diff_processor.prepare()
169 172
170 173 c.limited_diff = False
171 174 if isinstance(_parsed, LimitedDiffContainer):
172 175 c.limited_diff = True
173 176
174 177 c.files = []
175 178 c.changes = {}
176 179 c.lines_added = 0
177 180 c.lines_deleted = 0
178 181 for f in _parsed:
179 182 st = f['stats']
180 183 if st[0] != 'b':
181 184 c.lines_added += st[0]
182 185 c.lines_deleted += st[1]
183 186 fid = h.FID('', f['filename'])
184 187 c.files.append([fid, f['operation'], f['filename'], f['stats']])
185 188 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
186 189 c.changes[fid] = [f['operation'], f['filename'], diff]
187 190
188 191 return render('compare/compare_diff.html')
@@ -1,513 +1,517 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 27 import formencode
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden
30 30 from collections import defaultdict
31 31 from itertools import groupby
32 32
33 33 from pylons import request, response, session, tmpl_context as c, url
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.lib.compat import json
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 40 NotAnonymous
41 41 from rhodecode.lib.helpers import Page
42 42 from rhodecode.lib import helpers as h
43 43 from rhodecode.lib import diffs
44 44 from rhodecode.lib.utils import action_logger, jsonify
45 from rhodecode.lib.vcs.utils import safe_str
45 46 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
46 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
47 48 from rhodecode.lib.diffs import LimitedDiffContainer
48 49 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
49 50 ChangesetComment
50 51 from rhodecode.model.pull_request import PullRequestModel
51 52 from rhodecode.model.meta import Session
52 53 from rhodecode.model.repo import RepoModel
53 54 from rhodecode.model.comment import ChangesetCommentsModel
54 55 from rhodecode.model.changeset_status import ChangesetStatusModel
55 56 from rhodecode.model.forms import PullRequestForm
56 57 from mercurial import scmutil
57 58 from rhodecode.lib.utils2 import safe_int
58 59
59 60 log = logging.getLogger(__name__)
60 61
61 62
62 63 class PullrequestsController(BaseRepoController):
63 64
64 65 @LoginRequired()
65 66 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
66 67 'repository.admin')
67 68 def __before__(self):
68 69 super(PullrequestsController, self).__before__()
69 70 repo_model = RepoModel()
70 71 c.users_array = repo_model.get_users_js()
71 72 c.users_groups_array = repo_model.get_users_groups_js()
72 73
73 74 def _get_repo_refs(self, repo, rev=None, branch_rev=None):
74 75 """return a structure with repo's interesting changesets, suitable for
75 76 the selectors in pullrequest.html"""
76 77
77 78 # list named branches that has been merged to this named branch - it should probably merge back
78 79 peers = []
79 80 if branch_rev:
80 81 # not restricting to merge() would also get branch point and be better
81 82 # (especially because it would get the branch point) ... but is currently too expensive
82 83 revs = ["sort(parents(branch(id('%s')) and merge()) - branch(id('%s')))" %
83 84 (branch_rev, branch_rev)]
84 85 otherbranches = {}
85 86 for i in scmutil.revrange(repo._repo, revs):
86 87 cs = repo.get_changeset(i)
87 88 otherbranches[cs.branch] = cs.raw_id
88 89 for branch, node in otherbranches.iteritems():
89 90 selected = 'branch:%s:%s' % (branch, node)
90 91 peers.append((selected, branch))
91 92
92 93 selected = None
93 94 branches = []
94 95 for branch, branchrev in repo.branches.iteritems():
95 96 n = 'branch:%s:%s' % (branch, branchrev)
96 97 branches.append((n, branch))
97 98 if rev == branchrev:
98 99 selected = n
99 100 bookmarks = []
100 101 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
101 102 n = 'book:%s:%s' % (bookmark, bookmarkrev)
102 103 bookmarks.append((n, bookmark))
103 104 if rev == bookmarkrev:
104 105 selected = n
105 106 tags = []
106 107 for tag, tagrev in repo.tags.iteritems():
107 108 n = 'tag:%s:%s' % (tag, tagrev)
108 109 tags.append((n, tag))
109 110 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
110 111 selected = n
111 112
112 113 # prio 1: rev was selected as existing entry above
113 114
114 115 # prio 2: create special entry for rev; rev _must_ be used
115 116 specials = []
116 117 if rev and selected is None:
117 118 selected = 'rev:%s:%s' % (rev, rev)
118 119 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
119 120
120 121 # prio 3: most recent peer branch
121 122 if peers and not selected:
122 123 selected = peers[0][0][0]
123 124
124 125 # prio 4: tip revision
125 126 if not selected:
126 127 selected = 'tag:tip:%s' % repo.tags['tip']
127 128
128 129 groups = [(specials, _("Special")),
129 130 (peers, _("Peer branches")),
130 131 (bookmarks, _("Bookmarks")),
131 132 (branches, _("Branches")),
132 133 (tags, _("Tags")),
133 134 ]
134 135 return [g for g in groups if g[0]], selected
135 136
136 137 def _get_is_allowed_change_status(self, pull_request):
137 138 owner = self.rhodecode_user.user_id == pull_request.user_id
138 139 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
139 140 pull_request.reviewers]
140 141 return (self.rhodecode_user.admin or owner or reviewer)
141 142
142 143 def show_all(self, repo_name):
143 144 c.pull_requests = PullRequestModel().get_all(repo_name)
144 145 c.repo_name = repo_name
145 146 p = safe_int(request.params.get('page', 1), 1)
146 147
147 148 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
148 149
149 150 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
150 151
151 152 if request.environ.get('HTTP_X_PARTIAL_XHR'):
152 153 return c.pullrequest_data
153 154
154 155 return render('/pullrequests/pullrequest_show_all.html')
155 156
156 157 @NotAnonymous()
157 158 def index(self):
158 159 org_repo = c.rhodecode_db_repo
159 160
160 161 if org_repo.scm_instance.alias != 'hg':
161 162 log.error('Review not available for GIT REPOS')
162 163 raise HTTPNotFound
163 164
164 165 try:
165 166 org_repo.scm_instance.get_changeset()
166 167 except EmptyRepositoryError, e:
167 168 h.flash(h.literal(_('There are no changesets yet')),
168 169 category='warning')
169 170 redirect(url('summary_home', repo_name=org_repo.repo_name))
170 171
171 172 org_rev = request.GET.get('rev_end')
172 173 # rev_start is not directly useful - its parent could however be used
173 174 # as default for other and thus give a simple compare view
174 175 #other_rev = request.POST.get('rev_start')
175 176
176 177 c.org_repos = []
177 178 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
178 179 c.default_org_repo = org_repo.repo_name
179 180 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, org_rev)
180 181
181 182 c.other_repos = []
182 183 other_repos_info = {}
183 184
184 185 def add_other_repo(repo, branch_rev=None):
185 186 if repo.repo_name in other_repos_info: # shouldn't happen
186 187 return
187 188 c.other_repos.append((repo.repo_name, repo.repo_name))
188 189 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
189 190 other_repos_info[repo.repo_name] = {
190 191 'user': dict(user_id=repo.user.user_id,
191 192 username=repo.user.username,
192 193 firstname=repo.user.firstname,
193 194 lastname=repo.user.lastname,
194 195 gravatar_link=h.gravatar_url(repo.user.email, 14)),
195 196 'description': repo.description.split('\n', 1)[0],
196 197 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
197 198 }
198 199
199 200 # add org repo to other so we can open pull request against peer branches on itself
200 201 add_other_repo(org_repo, branch_rev=org_rev)
201 202 c.default_other_repo = org_repo.repo_name
202 203
203 204 # gather forks and add to this list ... even though it is rare to
204 205 # request forks to pull from their parent
205 206 for fork in org_repo.forks:
206 207 add_other_repo(fork)
207 208
208 209 # add parents of this fork also, but only if it's not empty
209 210 if org_repo.parent and org_repo.parent.scm_instance.revisions:
210 211 add_other_repo(org_repo.parent)
211 212 c.default_other_repo = org_repo.parent.repo_name
212 213
213 214 c.default_other_repo_info = other_repos_info[c.default_other_repo]
214 215 c.other_repos_info = json.dumps(other_repos_info)
215 216
216 217 return render('/pullrequests/pullrequest.html')
217 218
218 219 @NotAnonymous()
219 220 def create(self, repo_name):
220 221 repo = RepoModel()._get_repo(repo_name)
221 222 try:
222 223 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
223 224 except formencode.Invalid, errors:
224 225 log.error(traceback.format_exc())
225 226 if errors.error_dict.get('revisions'):
226 227 msg = 'Revisions: %s' % errors.error_dict['revisions']
227 228 elif errors.error_dict.get('pullrequest_title'):
228 229 msg = _('Pull request requires a title with min. 3 chars')
229 230 else:
230 231 msg = _('Error creating pull request')
231 232
232 233 h.flash(msg, 'error')
233 234 return redirect(url('pullrequest_home', repo_name=repo_name))
234 235
235 236 org_repo = _form['org_repo']
236 237 org_ref = 'rev:merge:%s' % _form['merge_rev']
237 238 other_repo = _form['other_repo']
238 239 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev']
239 240 revisions = _form['revisions']
240 241 reviewers = _form['review_members']
241 242
242 243 title = _form['pullrequest_title']
243 244 description = _form['pullrequest_desc']
244 245
245 246 try:
246 247 pull_request = PullRequestModel().create(
247 248 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
248 249 other_ref, revisions, reviewers, title, description
249 250 )
250 251 Session().commit()
251 252 h.flash(_('Successfully opened new pull request'),
252 253 category='success')
253 254 except Exception:
254 255 h.flash(_('Error occurred during sending pull request'),
255 256 category='error')
256 257 log.error(traceback.format_exc())
257 258 return redirect(url('pullrequest_home', repo_name=repo_name))
258 259
259 260 return redirect(url('pullrequest_show', repo_name=other_repo,
260 261 pull_request_id=pull_request.pull_request_id))
261 262
262 263 @NotAnonymous()
263 264 @jsonify
264 265 def update(self, repo_name, pull_request_id):
265 266 pull_request = PullRequest.get_or_404(pull_request_id)
266 267 if pull_request.is_closed():
267 268 raise HTTPForbidden()
268 269 #only owner or admin can update it
269 270 owner = pull_request.author.user_id == c.rhodecode_user.user_id
270 271 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
271 272 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
272 273 request.POST.get('reviewers_ids', '').split(',')))
273 274
274 275 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
275 276 Session().commit()
276 277 return True
277 278 raise HTTPForbidden()
278 279
279 280 @NotAnonymous()
280 281 @jsonify
281 282 def delete(self, repo_name, pull_request_id):
282 283 pull_request = PullRequest.get_or_404(pull_request_id)
283 284 #only owner can delete it !
284 285 if pull_request.author.user_id == c.rhodecode_user.user_id:
285 286 PullRequestModel().delete(pull_request)
286 287 Session().commit()
287 288 h.flash(_('Successfully deleted pull request'),
288 289 category='success')
289 290 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
290 291 raise HTTPForbidden()
291 292
292 293 def _load_compare_data(self, pull_request, enable_comments=True):
293 294 """
294 295 Load context data needed for generating compare diff
295 296
296 297 :param pull_request:
297 298 :type pull_request:
298 299 """
299 300 org_repo = pull_request.org_repo
300 301 (org_ref_type,
301 302 org_ref_name,
302 303 org_ref_rev) = pull_request.org_ref.split(':')
303 304
304 305 other_repo = org_repo
305 306 (other_ref_type,
306 307 other_ref_name,
307 308 other_ref_rev) = pull_request.other_ref.split(':')
308 309
309 310 # despite opening revisions for bookmarks/branches/tags, we always
310 311 # convert this to rev to prevent changes after bookmark or branch change
311 312 org_ref = ('rev', org_ref_rev)
312 313 other_ref = ('rev', other_ref_rev)
313 314
314 315 c.org_repo = org_repo
315 316 c.other_repo = other_repo
316 317
317 318 c.fulldiff = fulldiff = request.GET.get('fulldiff')
318 319
319 320 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
320 321
321 322 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
322 323
323 324 c.org_ref = org_ref[1]
324 325 c.org_ref_type = org_ref[0]
325 326 c.other_ref = other_ref[1]
326 327 c.other_ref_type = other_ref[0]
327 328
328 329 diff_limit = self.cut_off_limit if not fulldiff else None
329 330
330 331 #we swap org/other ref since we run a simple diff on one repo
331 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
332 log.debug('running diff between %s@%s and %s@%s'
333 % (org_repo.scm_instance.path, org_ref,
334 other_repo.scm_instance.path, other_ref))
335 _diff = org_repo.scm_instance.get_diff(rev1=safe_str(org_ref[1]), rev2=safe_str(other_ref[1]))
332 336
333 337 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
334 338 diff_limit=diff_limit)
335 339 _parsed = diff_processor.prepare()
336 340
337 341 c.limited_diff = False
338 342 if isinstance(_parsed, LimitedDiffContainer):
339 343 c.limited_diff = True
340 344
341 345 c.files = []
342 346 c.changes = {}
343 347 c.lines_added = 0
344 348 c.lines_deleted = 0
345 349 for f in _parsed:
346 350 st = f['stats']
347 351 if st[0] != 'b':
348 352 c.lines_added += st[0]
349 353 c.lines_deleted += st[1]
350 354 fid = h.FID('', f['filename'])
351 355 c.files.append([fid, f['operation'], f['filename'], f['stats']])
352 356 diff = diff_processor.as_html(enable_comments=enable_comments,
353 357 parsed_lines=[f])
354 358 c.changes[fid] = [f['operation'], f['filename'], diff]
355 359
356 360 def show(self, repo_name, pull_request_id):
357 361 repo_model = RepoModel()
358 362 c.users_array = repo_model.get_users_js()
359 363 c.users_groups_array = repo_model.get_users_groups_js()
360 364 c.pull_request = PullRequest.get_or_404(pull_request_id)
361 365 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
362 366 cc_model = ChangesetCommentsModel()
363 367 cs_model = ChangesetStatusModel()
364 368 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
365 369 pull_request=c.pull_request,
366 370 with_revisions=True)
367 371
368 372 cs_statuses = defaultdict(list)
369 373 for st in _cs_statuses:
370 374 cs_statuses[st.author.username] += [st]
371 375
372 376 c.pull_request_reviewers = []
373 377 c.pull_request_pending_reviewers = []
374 378 for o in c.pull_request.reviewers:
375 379 st = cs_statuses.get(o.user.username, None)
376 380 if st:
377 381 sorter = lambda k: k.version
378 382 st = [(x, list(y)[0])
379 383 for x, y in (groupby(sorted(st, key=sorter), sorter))]
380 384 else:
381 385 c.pull_request_pending_reviewers.append(o.user)
382 386 c.pull_request_reviewers.append([o.user, st])
383 387
384 388 # pull_requests repo_name we opened it against
385 389 # ie. other_repo must match
386 390 if repo_name != c.pull_request.other_repo.repo_name:
387 391 raise HTTPNotFound
388 392
389 393 # load compare data into template context
390 394 enable_comments = not c.pull_request.is_closed()
391 395 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
392 396
393 397 # inline comments
394 398 c.inline_cnt = 0
395 399 c.inline_comments = cc_model.get_inline_comments(
396 400 c.rhodecode_db_repo.repo_id,
397 401 pull_request=pull_request_id)
398 402 # count inline comments
399 403 for __, lines in c.inline_comments:
400 404 for comments in lines.values():
401 405 c.inline_cnt += len(comments)
402 406 # comments
403 407 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
404 408 pull_request=pull_request_id)
405 409
406 410 try:
407 411 cur_status = c.statuses[c.pull_request.revisions[0]][0]
408 412 except Exception:
409 413 log.error(traceback.format_exc())
410 414 cur_status = 'undefined'
411 415 if c.pull_request.is_closed() and 0:
412 416 c.current_changeset_status = cur_status
413 417 else:
414 418 # changeset(pull-request) status calulation based on reviewers
415 419 c.current_changeset_status = cs_model.calculate_status(
416 420 c.pull_request_reviewers,
417 421 )
418 422 c.changeset_statuses = ChangesetStatus.STATUSES
419 423
420 424 c.as_form = False
421 425 c.ancestor = None # there is one - but right here we don't know which
422 426 return render('/pullrequests/pullrequest_show.html')
423 427
424 428 @NotAnonymous()
425 429 @jsonify
426 430 def comment(self, repo_name, pull_request_id):
427 431 pull_request = PullRequest.get_or_404(pull_request_id)
428 432 if pull_request.is_closed():
429 433 raise HTTPForbidden()
430 434
431 435 status = request.POST.get('changeset_status')
432 436 change_status = request.POST.get('change_changeset_status')
433 437 text = request.POST.get('text')
434 438 close_pr = request.POST.get('save_close')
435 439
436 440 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
437 441 if status and change_status and allowed_to_change_status:
438 442 _def = (_('Status change -> %s')
439 443 % ChangesetStatus.get_status_lbl(status))
440 444 if close_pr:
441 445 _def = _('Closing with') + ' ' + _def
442 446 text = text or _def
443 447 comm = ChangesetCommentsModel().create(
444 448 text=text,
445 449 repo=c.rhodecode_db_repo.repo_id,
446 450 user=c.rhodecode_user.user_id,
447 451 pull_request=pull_request_id,
448 452 f_path=request.POST.get('f_path'),
449 453 line_no=request.POST.get('line'),
450 454 status_change=(ChangesetStatus.get_status_lbl(status)
451 455 if status and change_status
452 456 and allowed_to_change_status else None),
453 457 closing_pr=close_pr
454 458 )
455 459
456 460 action_logger(self.rhodecode_user,
457 461 'user_commented_pull_request:%s' % pull_request_id,
458 462 c.rhodecode_db_repo, self.ip_addr, self.sa)
459 463
460 464 if allowed_to_change_status:
461 465 # get status if set !
462 466 if status and change_status:
463 467 ChangesetStatusModel().set_status(
464 468 c.rhodecode_db_repo.repo_id,
465 469 status,
466 470 c.rhodecode_user.user_id,
467 471 comm,
468 472 pull_request=pull_request_id
469 473 )
470 474
471 475 if close_pr:
472 476 if status in ['rejected', 'approved']:
473 477 PullRequestModel().close_pull_request(pull_request_id)
474 478 action_logger(self.rhodecode_user,
475 479 'user_closed_pull_request:%s' % pull_request_id,
476 480 c.rhodecode_db_repo, self.ip_addr, self.sa)
477 481 else:
478 482 h.flash(_('Closing pull request on other statuses than '
479 483 'rejected or approved forbidden'),
480 484 category='warning')
481 485
482 486 Session().commit()
483 487
484 488 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
485 489 return redirect(h.url('pullrequest_show', repo_name=repo_name,
486 490 pull_request_id=pull_request_id))
487 491
488 492 data = {
489 493 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
490 494 }
491 495 if comm:
492 496 c.co = comm
493 497 data.update(comm.get_dict())
494 498 data.update({'rendered_text':
495 499 render('changeset/changeset_comment_block.html')})
496 500
497 501 return data
498 502
499 503 @NotAnonymous()
500 504 @jsonify
501 505 def delete_comment(self, repo_name, comment_id):
502 506 co = ChangesetComment.get(comment_id)
503 507 if co.pull_request.is_closed():
504 508 #don't allow deleting comments on closed pull request
505 509 raise HTTPForbidden()
506 510
507 511 owner = co.author.user_id == c.rhodecode_user.user_id
508 512 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
509 513 ChangesetCommentsModel().delete(comment=co)
510 514 Session().commit()
511 515 return True
512 516 else:
513 517 raise HTTPForbidden()
@@ -1,717 +1,684 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7 7
8 8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import difflib
30 30 import logging
31 31
32 32 from itertools import tee, imap
33 33
34 34 from pylons.i18n.translation import _
35 35
36 36 from rhodecode.lib.vcs.exceptions import VCSError
37 37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 38 from rhodecode.lib.vcs.backends.base import EmptyChangeset
39 39 from rhodecode.lib.helpers import escape
40 40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def wrap_to_table(str_):
46 46 return '''<table class="code-difftable">
47 47 <tr class="line no-comment">
48 48 <td class="lineno new"></td>
49 49 <td class="code no-comment"><pre>%s</pre></td>
50 50 </tr>
51 51 </table>''' % str_
52 52
53 53
54 54 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
55 55 ignore_whitespace=True, line_context=3,
56 56 enable_comments=False):
57 57 """
58 58 returns a wrapped diff into a table, checks for cut_off_limit and presents
59 59 proper message
60 60 """
61 61
62 62 if filenode_old is None:
63 63 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
64 64
65 65 if filenode_old.is_binary or filenode_new.is_binary:
66 66 diff = wrap_to_table(_('Binary file'))
67 67 stats = (0, 0)
68 68 size = 0
69 69
70 70 elif cut_off_limit != -1 and (cut_off_limit is None or
71 71 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
72 72
73 73 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
74 74 ignore_whitespace=ignore_whitespace,
75 75 context=line_context)
76 76 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
77 77
78 78 diff = diff_processor.as_html(enable_comments=enable_comments)
79 79 stats = diff_processor.stat()
80 80 size = len(diff or '')
81 81 else:
82 82 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
83 83 'diff menu to display this diff'))
84 84 stats = (0, 0)
85 85 size = 0
86 86 if not diff:
87 87 submodules = filter(lambda o: isinstance(o, SubModuleNode),
88 88 [filenode_new, filenode_old])
89 89 if submodules:
90 90 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
91 91 else:
92 92 diff = wrap_to_table(_('No changes detected'))
93 93
94 94 cs1 = filenode_old.changeset.raw_id
95 95 cs2 = filenode_new.changeset.raw_id
96 96
97 97 return size, cs1, cs2, diff, stats
98 98
99 99
100 100 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
101 101 """
102 102 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
103 103
104 104 :param ignore_whitespace: ignore whitespaces in diff
105 105 """
106 106 # make sure we pass in default context
107 107 context = context or 3
108 108 submodules = filter(lambda o: isinstance(o, SubModuleNode),
109 109 [filenode_new, filenode_old])
110 110 if submodules:
111 111 return ''
112 112
113 113 for filenode in (filenode_old, filenode_new):
114 114 if not isinstance(filenode, FileNode):
115 115 raise VCSError("Given object should be FileNode object, not %s"
116 116 % filenode.__class__)
117 117
118 118 repo = filenode_new.changeset.repository
119 119 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
120 120 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
121 121
122 122 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
123 123 ignore_whitespace, context)
124 124 return vcs_gitdiff
125 125
126 126 NEW_FILENODE = 1
127 127 DEL_FILENODE = 2
128 128 MOD_FILENODE = 3
129 129 RENAMED_FILENODE = 4
130 130 CHMOD_FILENODE = 5
131 131
132 132
133 133 class DiffLimitExceeded(Exception):
134 134 pass
135 135
136 136
137 137 class LimitedDiffContainer(object):
138 138
139 139 def __init__(self, diff_limit, cur_diff_size, diff):
140 140 self.diff = diff
141 141 self.diff_limit = diff_limit
142 142 self.cur_diff_size = cur_diff_size
143 143
144 144 def __iter__(self):
145 145 for l in self.diff:
146 146 yield l
147 147
148 148
149 149 class DiffProcessor(object):
150 150 """
151 151 Give it a unified or git diff and it returns a list of the files that were
152 152 mentioned in the diff together with a dict of meta information that
153 153 can be used to render it in a HTML template.
154 154 """
155 155 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
156 156 _newline_marker = re.compile(r'^\\ No newline at end of file')
157 157 _git_header_re = re.compile(r"""
158 158 #^diff[ ]--git
159 159 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
160 160 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
161 161 ^rename[ ]from[ ](?P<rename_from>\S+)\n
162 162 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
163 163 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
164 164 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
165 165 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
166 166 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
167 167 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
168 168 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
169 169 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
170 170 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
171 171 """, re.VERBOSE | re.MULTILINE)
172 172 _hg_header_re = re.compile(r"""
173 173 #^diff[ ]--git
174 174 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
175 175 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
176 176 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
177 177 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
178 178 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
179 179 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
180 180 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
181 181 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
182 182 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
183 183 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
184 184 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
185 185 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
186 186 """, re.VERBOSE | re.MULTILINE)
187 187
188 188 #used for inline highlighter word split
189 189 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
190 190
191 191 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
192 192 """
193 193 :param diff: a text in diff format
194 194 :param vcs: type of version controll hg or git
195 195 :param format: format of diff passed, `udiff` or `gitdiff`
196 196 :param diff_limit: define the size of diff that is considered "big"
197 197 based on that parameter cut off will be triggered, set to None
198 198 to show full diff
199 199 """
200 200 if not isinstance(diff, basestring):
201 201 raise Exception('Diff must be a basestring got %s instead' % type(diff))
202 202
203 203 self._diff = diff
204 204 self._format = format
205 205 self.adds = 0
206 206 self.removes = 0
207 207 # calculate diff size
208 208 self.diff_size = len(diff)
209 209 self.diff_limit = diff_limit
210 210 self.cur_diff_size = 0
211 211 self.parsed = False
212 212 self.parsed_diff = []
213 213 self.vcs = vcs
214 214
215 215 if format == 'gitdiff':
216 216 self.differ = self._highlight_line_difflib
217 217 self._parser = self._parse_gitdiff
218 218 else:
219 219 self.differ = self._highlight_line_udiff
220 220 self._parser = self._parse_udiff
221 221
222 222 def _copy_iterator(self):
223 223 """
224 224 make a fresh copy of generator, we should not iterate thru
225 225 an original as it's needed for repeating operations on
226 226 this instance of DiffProcessor
227 227 """
228 228 self.__udiff, iterator_copy = tee(self.__udiff)
229 229 return iterator_copy
230 230
231 231 def _escaper(self, string):
232 232 """
233 233 Escaper for diff escapes special chars and checks the diff limit
234 234
235 235 :param string:
236 236 :type string:
237 237 """
238 238
239 239 self.cur_diff_size += len(string)
240 240
241 241 # escaper get's iterated on each .next() call and it checks if each
242 242 # parsed line doesn't exceed the diff limit
243 243 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
244 244 raise DiffLimitExceeded('Diff Limit Exceeded')
245 245
246 246 return safe_unicode(string).replace('&', '&amp;')\
247 247 .replace('<', '&lt;')\
248 248 .replace('>', '&gt;')
249 249
250 250 def _line_counter(self, l):
251 251 """
252 252 Checks each line and bumps total adds/removes for this diff
253 253
254 254 :param l:
255 255 """
256 256 if l.startswith('+') and not l.startswith('+++'):
257 257 self.adds += 1
258 258 elif l.startswith('-') and not l.startswith('---'):
259 259 self.removes += 1
260 260 return safe_unicode(l)
261 261
262 262 def _highlight_line_difflib(self, line, next_):
263 263 """
264 264 Highlight inline changes in both lines.
265 265 """
266 266
267 267 if line['action'] == 'del':
268 268 old, new = line, next_
269 269 else:
270 270 old, new = next_, line
271 271
272 272 oldwords = self._token_re.split(old['line'])
273 273 newwords = self._token_re.split(new['line'])
274 274 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
275 275
276 276 oldfragments, newfragments = [], []
277 277 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
278 278 oldfrag = ''.join(oldwords[i1:i2])
279 279 newfrag = ''.join(newwords[j1:j2])
280 280 if tag != 'equal':
281 281 if oldfrag:
282 282 oldfrag = '<del>%s</del>' % oldfrag
283 283 if newfrag:
284 284 newfrag = '<ins>%s</ins>' % newfrag
285 285 oldfragments.append(oldfrag)
286 286 newfragments.append(newfrag)
287 287
288 288 old['line'] = "".join(oldfragments)
289 289 new['line'] = "".join(newfragments)
290 290
291 291 def _highlight_line_udiff(self, line, next_):
292 292 """
293 293 Highlight inline changes in both lines.
294 294 """
295 295 start = 0
296 296 limit = min(len(line['line']), len(next_['line']))
297 297 while start < limit and line['line'][start] == next_['line'][start]:
298 298 start += 1
299 299 end = -1
300 300 limit -= start
301 301 while -end <= limit and line['line'][end] == next_['line'][end]:
302 302 end -= 1
303 303 end += 1
304 304 if start or end:
305 305 def do(l):
306 306 last = end + len(l['line'])
307 307 if l['action'] == 'add':
308 308 tag = 'ins'
309 309 else:
310 310 tag = 'del'
311 311 l['line'] = '%s<%s>%s</%s>%s' % (
312 312 l['line'][:start],
313 313 tag,
314 314 l['line'][start:last],
315 315 tag,
316 316 l['line'][last:]
317 317 )
318 318 do(line)
319 319 do(next_)
320 320
321 321 def _get_header(self, diff_chunk):
322 322 """
323 323 parses the diff header, and returns parts, and leftover diff
324 324 parts consists of 14 elements::
325 325
326 326 a_path, b_path, similarity_index, rename_from, rename_to,
327 327 old_mode, new_mode, new_file_mode, deleted_file_mode,
328 328 a_blob_id, b_blob_id, b_mode, a_file, b_file
329 329
330 330 :param diff_chunk:
331 331 :type diff_chunk:
332 332 """
333 333
334 334 if self.vcs == 'git':
335 335 match = self._git_header_re.match(diff_chunk)
336 336 diff = diff_chunk[match.end():]
337 337 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
338 338 elif self.vcs == 'hg':
339 339 match = self._hg_header_re.match(diff_chunk)
340 340 diff = diff_chunk[match.end():]
341 341 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
342 342 else:
343 343 raise Exception('VCS type %s is not supported' % self.vcs)
344 344
345 345 def _clean_line(self, line, command):
346 346 if command in ['+', '-', ' ']:
347 347 #only modify the line if it's actually a diff thing
348 348 line = line[1:]
349 349 return line
350 350
351 351 def _parse_gitdiff(self, inline_diff=True):
352 352 _files = []
353 353 diff_container = lambda arg: arg
354 354
355 355 ##split the diff in chunks of separate --git a/file b/file chunks
356 356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
357 357 binary = False
358 358 binary_msg = 'unknown binary'
359 359 head, diff = self._get_header(raw_diff)
360 360
361 361 if not head['a_file'] and head['b_file']:
362 362 op = 'A'
363 363 elif head['a_file'] and head['b_file']:
364 364 op = 'M'
365 365 elif head['a_file'] and not head['b_file']:
366 366 op = 'D'
367 367 else:
368 368 #probably we're dealing with a binary file 1
369 369 binary = True
370 370 if head['deleted_file_mode']:
371 371 op = 'D'
372 372 stats = ['b', DEL_FILENODE]
373 373 binary_msg = 'deleted binary file'
374 374 elif head['new_file_mode']:
375 375 op = 'A'
376 376 stats = ['b', NEW_FILENODE]
377 377 binary_msg = 'new binary file %s' % head['new_file_mode']
378 378 else:
379 379 if head['new_mode'] and head['old_mode']:
380 380 stats = ['b', CHMOD_FILENODE]
381 381 op = 'M'
382 382 binary_msg = ('modified binary file chmod %s => %s'
383 383 % (head['old_mode'], head['new_mode']))
384 384 elif (head['rename_from'] and head['rename_to']
385 385 and head['rename_from'] != head['rename_to']):
386 386 stats = ['b', RENAMED_FILENODE]
387 387 op = 'M'
388 388 binary_msg = ('file renamed from %s to %s'
389 389 % (head['rename_from'], head['rename_to']))
390 390 else:
391 391 stats = ['b', MOD_FILENODE]
392 392 op = 'M'
393 393 binary_msg = 'modified binary file'
394 394
395 395 if not binary:
396 396 try:
397 397 chunks, stats = self._parse_lines(diff)
398 398 except DiffLimitExceeded:
399 399 diff_container = lambda _diff: LimitedDiffContainer(
400 400 self.diff_limit,
401 401 self.cur_diff_size,
402 402 _diff)
403 403 break
404 404 else:
405 405 chunks = []
406 406 chunks.append([{
407 407 'old_lineno': '',
408 408 'new_lineno': '',
409 409 'action': 'binary',
410 410 'line': binary_msg,
411 411 }])
412 412
413 413 _files.append({
414 414 'filename': head['b_path'],
415 415 'old_revision': head['a_blob_id'],
416 416 'new_revision': head['b_blob_id'],
417 417 'chunks': chunks,
418 418 'operation': op,
419 419 'stats': stats,
420 420 })
421 421
422 422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
423 423
424 424 if not inline_diff:
425 425 return diff_container(sorted(_files, key=sorter))
426 426
427 427 # highlight inline changes
428 428 for diff_data in _files:
429 429 for chunk in diff_data['chunks']:
430 430 lineiter = iter(chunk)
431 431 try:
432 432 while 1:
433 433 line = lineiter.next()
434 434 if line['action'] not in ['unmod', 'context']:
435 435 nextline = lineiter.next()
436 436 if nextline['action'] in ['unmod', 'context'] or \
437 437 nextline['action'] == line['action']:
438 438 continue
439 439 self.differ(line, nextline)
440 440 except StopIteration:
441 441 pass
442 442
443 443 return diff_container(sorted(_files, key=sorter))
444 444
445 445 def _parse_udiff(self, inline_diff=True):
446 446 raise NotImplementedError()
447 447
448 448 def _parse_lines(self, diff):
449 449 """
450 450 Parse the diff an return data for the template.
451 451 """
452 452
453 453 lineiter = iter(diff)
454 454 stats = [0, 0]
455 455
456 456 try:
457 457 chunks = []
458 458 line = lineiter.next()
459 459
460 460 while line:
461 461 lines = []
462 462 chunks.append(lines)
463 463
464 464 match = self._chunk_re.match(line)
465 465
466 466 if not match:
467 467 break
468 468
469 469 gr = match.groups()
470 470 (old_line, old_end,
471 471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
472 472 old_line -= 1
473 473 new_line -= 1
474 474
475 475 context = len(gr) == 5
476 476 old_end += old_line
477 477 new_end += new_line
478 478
479 479 if context:
480 480 # skip context only if it's first line
481 481 if int(gr[0]) > 1:
482 482 lines.append({
483 483 'old_lineno': '...',
484 484 'new_lineno': '...',
485 485 'action': 'context',
486 486 'line': line,
487 487 })
488 488
489 489 line = lineiter.next()
490 490
491 491 while old_line < old_end or new_line < new_end:
492 492 command = ' '
493 493 if line:
494 494 command = line[0]
495 495
496 496 affects_old = affects_new = False
497 497
498 498 # ignore those if we don't expect them
499 499 if command in '#@':
500 500 continue
501 501 elif command == '+':
502 502 affects_new = True
503 503 action = 'add'
504 504 stats[0] += 1
505 505 elif command == '-':
506 506 affects_old = True
507 507 action = 'del'
508 508 stats[1] += 1
509 509 else:
510 510 affects_old = affects_new = True
511 511 action = 'unmod'
512 512
513 513 if not self._newline_marker.match(line):
514 514 old_line += affects_old
515 515 new_line += affects_new
516 516 lines.append({
517 517 'old_lineno': affects_old and old_line or '',
518 518 'new_lineno': affects_new and new_line or '',
519 519 'action': action,
520 520 'line': self._clean_line(line, command)
521 521 })
522 522
523 523 line = lineiter.next()
524 524
525 525 if self._newline_marker.match(line):
526 526 # we need to append to lines, since this is not
527 527 # counted in the line specs of diff
528 528 lines.append({
529 529 'old_lineno': '...',
530 530 'new_lineno': '...',
531 531 'action': 'context',
532 532 'line': self._clean_line(line, command)
533 533 })
534 534
535 535 except StopIteration:
536 536 pass
537 537 return chunks, stats
538 538
539 539 def _safe_id(self, idstring):
540 540 """Make a string safe for including in an id attribute.
541 541
542 542 The HTML spec says that id attributes 'must begin with
543 543 a letter ([A-Za-z]) and may be followed by any number
544 544 of letters, digits ([0-9]), hyphens ("-"), underscores
545 545 ("_"), colons (":"), and periods (".")'. These regexps
546 546 are slightly over-zealous, in that they remove colons
547 547 and periods unnecessarily.
548 548
549 549 Whitespace is transformed into underscores, and then
550 550 anything which is not a hyphen or a character that
551 551 matches \w (alphanumerics and underscore) is removed.
552 552
553 553 """
554 554 # Transform all whitespace to underscore
555 555 idstring = re.sub(r'\s', "_", '%s' % idstring)
556 556 # Remove everything that is not a hyphen or a member of \w
557 557 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
558 558 return idstring
559 559
560 560 def prepare(self, inline_diff=True):
561 561 """
562 562 Prepare the passed udiff for HTML rendering. It'l return a list
563 563 of dicts with diff information
564 564 """
565 565 parsed = self._parser(inline_diff=inline_diff)
566 566 self.parsed = True
567 567 self.parsed_diff = parsed
568 568 return parsed
569 569
570 570 def as_raw(self, diff_lines=None):
571 571 """
572 572 Returns raw string diff
573 573 """
574 574 return self._diff
575 575 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
576 576
577 577 def as_html(self, table_class='code-difftable', line_class='line',
578 578 old_lineno_class='lineno old', new_lineno_class='lineno new',
579 579 code_class='code', enable_comments=False, parsed_lines=None):
580 580 """
581 581 Return given diff as html table with customized css classes
582 582 """
583 583 def _link_to_if(condition, label, url):
584 584 """
585 585 Generates a link if condition is meet or just the label if not.
586 586 """
587 587
588 588 if condition:
589 589 return '''<a href="%(url)s">%(label)s</a>''' % {
590 590 'url': url,
591 591 'label': label
592 592 }
593 593 else:
594 594 return label
595 595 if not self.parsed:
596 596 self.prepare()
597 597
598 598 diff_lines = self.parsed_diff
599 599 if parsed_lines:
600 600 diff_lines = parsed_lines
601 601
602 602 _html_empty = True
603 603 _html = []
604 604 _html.append('''<table class="%(table_class)s">\n''' % {
605 605 'table_class': table_class
606 606 })
607 607
608 608 for diff in diff_lines:
609 609 for line in diff['chunks']:
610 610 _html_empty = False
611 611 for change in line:
612 612 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
613 613 'lc': line_class,
614 614 'action': change['action']
615 615 })
616 616 anchor_old_id = ''
617 617 anchor_new_id = ''
618 618 anchor_old = "%(filename)s_o%(oldline_no)s" % {
619 619 'filename': self._safe_id(diff['filename']),
620 620 'oldline_no': change['old_lineno']
621 621 }
622 622 anchor_new = "%(filename)s_n%(oldline_no)s" % {
623 623 'filename': self._safe_id(diff['filename']),
624 624 'oldline_no': change['new_lineno']
625 625 }
626 626 cond_old = (change['old_lineno'] != '...' and
627 627 change['old_lineno'])
628 628 cond_new = (change['new_lineno'] != '...' and
629 629 change['new_lineno'])
630 630 if cond_old:
631 631 anchor_old_id = 'id="%s"' % anchor_old
632 632 if cond_new:
633 633 anchor_new_id = 'id="%s"' % anchor_new
634 634 ###########################################################
635 635 # OLD LINE NUMBER
636 636 ###########################################################
637 637 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
638 638 'a_id': anchor_old_id,
639 639 'olc': old_lineno_class
640 640 })
641 641
642 642 _html.append('''%(link)s''' % {
643 643 'link': _link_to_if(True, change['old_lineno'],
644 644 '#%s' % anchor_old)
645 645 })
646 646 _html.append('''</td>\n''')
647 647 ###########################################################
648 648 # NEW LINE NUMBER
649 649 ###########################################################
650 650
651 651 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
652 652 'a_id': anchor_new_id,
653 653 'nlc': new_lineno_class
654 654 })
655 655
656 656 _html.append('''%(link)s''' % {
657 657 'link': _link_to_if(True, change['new_lineno'],
658 658 '#%s' % anchor_new)
659 659 })
660 660 _html.append('''</td>\n''')
661 661 ###########################################################
662 662 # CODE
663 663 ###########################################################
664 664 comments = '' if enable_comments else 'no-comment'
665 665 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
666 666 'cc': code_class,
667 667 'inc': comments
668 668 })
669 669 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
670 670 'code': change['line']
671 671 })
672 672
673 673 _html.append('''\t</td>''')
674 674 _html.append('''\n</tr>\n''')
675 675 _html.append('''</table>''')
676 676 if _html_empty:
677 677 return None
678 678 return ''.join(_html)
679 679
680 680 def stat(self):
681 681 """
682 682 Returns tuple of added, and removed lines for this instance
683 683 """
684 684 return self.adds, self.removes
685
686
687 def differ(org_repo, org_ref, other_repo, other_ref,
688 context=3, ignore_whitespace=False):
689 """
690 General differ between branches, bookmarks, revisions of two remote or
691 local but related repositories
692
693 :param org_repo:
694 :param org_ref:
695 :param other_repo:
696 :type other_repo:
697 :type other_ref:
698 """
699
700 org_repo_scm = org_repo.scm_instance
701 other_repo_scm = other_repo.scm_instance
702
703 org_repo = org_repo_scm._repo
704 other_repo = other_repo_scm._repo
705
706 org_ref = safe_str(org_ref[1])
707 other_ref = safe_str(other_ref[1])
708
709 if org_repo_scm == other_repo_scm:
710 log.debug('running diff between %s@%s and %s@%s'
711 % (org_repo.path, org_ref,
712 other_repo.path, other_ref))
713 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
714 ignore_whitespace=ignore_whitespace, context=context)
715 return _diff
716
717 return '' # FIXME: when is it ever relevant to return nothing?
General Comments 0
You need to be logged in to leave comments. Login now