Mercurial > kallithea
comparison rhodecode/controllers/changeset.py @ 2995:32471bd1f4ee beta
Implemented generation of changesets based
on whole diff instead of per file diff. That
can give a big speed improvement for large changesets in
repositories with large history.
- improved handling of binary files
- show renames of binary files
- implemented new diff limit functionality
- unify diff generation between hg and git
- Added binary indicators for changed files,
- added diff lib tests
author | Marcin Kuzminski <marcin@python-works.com> |
---|---|
date | Mon, 05 Nov 2012 19:57:29 +0100 |
parents | b84a4ec93ab6 |
children | ebe3e388bbb3 |
comparison
equal
deleted
inserted
replaced
2994:af0d7d8acb22 | 2995:32471bd1f4ee |
---|---|
45 from rhodecode.lib import diffs | 45 from rhodecode.lib import diffs |
46 from rhodecode.model.db import ChangesetComment, ChangesetStatus | 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus |
47 from rhodecode.model.comment import ChangesetCommentsModel | 47 from rhodecode.model.comment import ChangesetCommentsModel |
48 from rhodecode.model.changeset_status import ChangesetStatusModel | 48 from rhodecode.model.changeset_status import ChangesetStatusModel |
49 from rhodecode.model.meta import Session | 49 from rhodecode.model.meta import Session |
50 from rhodecode.lib.diffs import wrapped_diff | 50 from rhodecode.lib.diffs import LimitedDiffContainer |
51 from rhodecode.model.repo import RepoModel | 51 from rhodecode.model.repo import RepoModel |
52 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError | 52 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError |
53 from rhodecode.lib.vcs.backends.base import EmptyChangeset | 53 from rhodecode.lib.vcs.backends.base import EmptyChangeset |
54 | 54 |
55 log = logging.getLogger(__name__) | 55 log = logging.getLogger(__name__) |
107 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip') | 107 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip') |
108 | 108 |
109 | 109 |
110 def get_line_ctx(fid, GET): | 110 def get_line_ctx(fid, GET): |
111 ln_ctx_global = GET.get('context') | 111 ln_ctx_global = GET.get('context') |
112 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid)) | 112 if fid: |
113 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid)) | |
114 else: | |
115 _ln_ctx = filter(lambda k: k.startswith('C'), GET) | |
116 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global | |
117 if ln_ctx: | |
118 ln_ctx = [ln_ctx] | |
113 | 119 |
114 if ln_ctx: | 120 if ln_ctx: |
115 retval = ln_ctx[0].split(':')[-1] | 121 retval = ln_ctx[0].split(':')[-1] |
116 else: | 122 else: |
117 retval = ln_ctx_global | 123 retval = ln_ctx_global |
118 | 124 |
119 try: | 125 try: |
120 return int(retval) | 126 return int(retval) |
121 except: | 127 except: |
122 return | 128 return 3 |
123 | 129 |
124 | 130 |
125 def _context_url(GET, fileid=None): | 131 def _context_url(GET, fileid=None): |
126 """ | 132 """ |
127 Generates url for context lines | 133 Generates url for context lines |
172 repo_model = RepoModel() | 178 repo_model = RepoModel() |
173 c.users_array = repo_model.get_users_js() | 179 c.users_array = repo_model.get_users_js() |
174 c.users_groups_array = repo_model.get_users_groups_js() | 180 c.users_groups_array = repo_model.get_users_groups_js() |
175 | 181 |
176 def index(self, revision): | 182 def index(self, revision): |
177 | 183 method = request.GET.get('diff', 'show') |
178 c.anchor_url = anchor_url | 184 c.anchor_url = anchor_url |
179 c.ignorews_url = _ignorews_url | 185 c.ignorews_url = _ignorews_url |
180 c.context_url = _context_url | 186 c.context_url = _context_url |
181 limit_off = request.GET.get('fulldiff') | 187 limit_off = request.GET.get('fulldiff') |
182 #get ranges of revisions if preset | 188 #get ranges of revisions if preset |
204 c.changes = OrderedDict() | 210 c.changes = OrderedDict() |
205 | 211 |
206 c.lines_added = 0 # count of lines added | 212 c.lines_added = 0 # count of lines added |
207 c.lines_deleted = 0 # count of lines removes | 213 c.lines_deleted = 0 # count of lines removes |
208 | 214 |
209 cumulative_diff = 0 | |
210 c.cut_off = False # defines if cut off limit is reached | |
211 c.changeset_statuses = ChangesetStatus.STATUSES | 215 c.changeset_statuses = ChangesetStatus.STATUSES |
212 c.comments = [] | 216 c.comments = [] |
213 c.statuses = [] | 217 c.statuses = [] |
214 c.inline_comments = [] | 218 c.inline_comments = [] |
215 c.inline_cnt = 0 | 219 c.inline_cnt = 0 |
220 | |
216 # Iterate over ranges (default changeset view is always one changeset) | 221 # Iterate over ranges (default changeset view is always one changeset) |
217 for changeset in c.cs_ranges: | 222 for changeset in c.cs_ranges: |
218 | 223 inlines = [] |
219 c.statuses.extend([ChangesetStatusModel()\ | 224 if method == 'show': |
220 .get_status(c.rhodecode_db_repo.repo_id, | 225 c.statuses.extend([ChangesetStatusModel()\ |
221 changeset.raw_id)]) | 226 .get_status(c.rhodecode_db_repo.repo_id, |
222 | 227 changeset.raw_id)]) |
223 c.comments.extend(ChangesetCommentsModel()\ | 228 |
224 .get_comments(c.rhodecode_db_repo.repo_id, | 229 c.comments.extend(ChangesetCommentsModel()\ |
225 revision=changeset.raw_id)) | 230 .get_comments(c.rhodecode_db_repo.repo_id, |
226 inlines = ChangesetCommentsModel()\ | 231 revision=changeset.raw_id)) |
227 .get_inline_comments(c.rhodecode_db_repo.repo_id, | 232 inlines = ChangesetCommentsModel()\ |
228 revision=changeset.raw_id) | 233 .get_inline_comments(c.rhodecode_db_repo.repo_id, |
229 c.inline_comments.extend(inlines) | 234 revision=changeset.raw_id) |
235 c.inline_comments.extend(inlines) | |
236 | |
230 c.changes[changeset.raw_id] = [] | 237 c.changes[changeset.raw_id] = [] |
231 try: | 238 |
232 changeset_parent = changeset.parents[0] | 239 cs2 = changeset.raw_id |
233 except IndexError: | 240 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset() |
234 changeset_parent = None | 241 context_lcl = get_line_ctx('', request.GET) |
235 | 242 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET) |
236 #================================================================== | 243 |
237 # ADDED FILES | 244 _diff = c.rhodecode_repo.get_diff(cs1, cs2, |
238 #================================================================== | 245 ignore_whitespace=ign_whitespace_lcl, context=context_lcl) |
239 for node in changeset.added: | 246 diff_limit = self.cut_off_limit if not limit_off else None |
240 fid = h.FID(revision, node.path) | 247 diff_processor = diffs.DiffProcessor(_diff, |
241 line_context_lcl = get_line_ctx(fid, request.GET) | 248 vcs=c.rhodecode_repo.alias, |
242 ign_whitespace_lcl = get_ignore_ws(fid, request.GET) | 249 format='gitdiff', |
243 lim = self.cut_off_limit | 250 diff_limit=diff_limit) |
244 if cumulative_diff > self.cut_off_limit: | 251 cs_changes = OrderedDict() |
245 lim = -1 if limit_off is None else None | 252 if method == 'show': |
246 size, cs1, cs2, diff, st = wrapped_diff( | 253 _parsed = diff_processor.prepare() |
247 filenode_old=None, | 254 c.limited_diff = False |
248 filenode_new=node, | 255 if isinstance(_parsed, LimitedDiffContainer): |
249 cut_off_limit=lim, | 256 c.limited_diff = True |
250 ignore_whitespace=ign_whitespace_lcl, | 257 for f in _parsed: |
251 line_context=line_context_lcl, | 258 st = f['stats'] |
252 enable_comments=enable_comments | 259 if st[0] != 'b': |
253 ) | 260 c.lines_added += st[0] |
254 cumulative_diff += size | 261 c.lines_deleted += st[1] |
255 c.lines_added += st[0] | 262 fid = h.FID(changeset.raw_id, f['filename']) |
256 c.lines_deleted += st[1] | 263 diff = diff_processor.as_html(enable_comments=enable_comments, |
257 c.changes[changeset.raw_id].append( | 264 parsed_lines=[f]) |
258 ('added', node, diff, cs1, cs2, st) | 265 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'], |
259 ) | 266 diff, st] |
260 | 267 else: |
261 #================================================================== | 268 # downloads/raw we only need RAW diff nothing else |
262 # CHANGED FILES | 269 diff = diff_processor.as_raw() |
263 #================================================================== | 270 cs_changes[''] = [None, None, None, None, diff, None] |
264 for node in changeset.changed: | 271 c.changes[changeset.raw_id] = cs_changes |
265 try: | |
266 filenode_old = changeset_parent.get_node(node.path) | |
267 except ChangesetError: | |
268 log.warning('Unable to fetch parent node for diff') | |
269 filenode_old = FileNode(node.path, '', EmptyChangeset()) | |
270 | |
271 fid = h.FID(revision, node.path) | |
272 line_context_lcl = get_line_ctx(fid, request.GET) | |
273 ign_whitespace_lcl = get_ignore_ws(fid, request.GET) | |
274 lim = self.cut_off_limit | |
275 if cumulative_diff > self.cut_off_limit: | |
276 lim = -1 if limit_off is None else None | |
277 size, cs1, cs2, diff, st = wrapped_diff( | |
278 filenode_old=filenode_old, | |
279 filenode_new=node, | |
280 cut_off_limit=lim, | |
281 ignore_whitespace=ign_whitespace_lcl, | |
282 line_context=line_context_lcl, | |
283 enable_comments=enable_comments | |
284 ) | |
285 cumulative_diff += size | |
286 c.lines_added += st[0] | |
287 c.lines_deleted += st[1] | |
288 c.changes[changeset.raw_id].append( | |
289 ('changed', node, diff, cs1, cs2, st) | |
290 ) | |
291 #================================================================== | |
292 # REMOVED FILES | |
293 #================================================================== | |
294 for node in changeset.removed: | |
295 c.changes[changeset.raw_id].append( | |
296 ('removed', node, None, None, None, (0, 0)) | |
297 ) | |
298 | 272 |
299 # count inline comments | 273 # count inline comments |
300 for __, lines in c.inline_comments: | 274 for __, lines in c.inline_comments: |
301 for comments in lines.values(): | 275 for comments in lines.values(): |
302 c.inline_cnt += len(comments) | 276 c.inline_cnt += len(comments) |
303 | 277 |
304 if len(c.cs_ranges) == 1: | 278 if len(c.cs_ranges) == 1: |
305 c.changeset = c.cs_ranges[0] | 279 c.changeset = c.cs_ranges[0] |
306 c.changes = c.changes[c.changeset.raw_id] | 280 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id |
307 | 281 for x in c.changeset.parents]) |
308 return render('changeset/changeset.html') | |
309 else: | |
310 return render('changeset/changeset_range.html') | |
311 | |
312 def raw_changeset(self, revision): | |
313 | |
314 method = request.GET.get('diff', 'show') | |
315 ignore_whitespace = request.GET.get('ignorews') == '1' | |
316 line_context = request.GET.get('context', 3) | |
317 try: | |
318 c.scm_type = c.rhodecode_repo.alias | |
319 c.changeset = c.rhodecode_repo.get_changeset(revision) | |
320 except RepositoryError: | |
321 log.error(traceback.format_exc()) | |
322 return redirect(url('home')) | |
323 else: | |
324 try: | |
325 c.changeset_parent = c.changeset.parents[0] | |
326 except IndexError: | |
327 c.changeset_parent = None | |
328 c.changes = [] | |
329 | |
330 for node in c.changeset.added: | |
331 filenode_old = FileNode(node.path, '') | |
332 if filenode_old.is_binary or node.is_binary: | |
333 diff = _('binary file') + '\n' | |
334 else: | |
335 f_gitdiff = diffs.get_gitdiff(filenode_old, node, | |
336 ignore_whitespace=ignore_whitespace, | |
337 context=line_context) | |
338 diff = diffs.DiffProcessor(f_gitdiff, | |
339 format='gitdiff').raw_diff() | |
340 | |
341 cs1 = None | |
342 cs2 = node.changeset.raw_id | |
343 c.changes.append(('added', node, diff, cs1, cs2)) | |
344 | |
345 for node in c.changeset.changed: | |
346 filenode_old = c.changeset_parent.get_node(node.path) | |
347 if filenode_old.is_binary or node.is_binary: | |
348 diff = _('binary file') | |
349 else: | |
350 f_gitdiff = diffs.get_gitdiff(filenode_old, node, | |
351 ignore_whitespace=ignore_whitespace, | |
352 context=line_context) | |
353 diff = diffs.DiffProcessor(f_gitdiff, | |
354 format='gitdiff').raw_diff() | |
355 | |
356 cs1 = filenode_old.changeset.raw_id | |
357 cs2 = node.changeset.raw_id | |
358 c.changes.append(('changed', node, diff, cs1, cs2)) | |
359 | |
360 response.content_type = 'text/plain' | |
361 | |
362 if method == 'download': | 282 if method == 'download': |
283 response.content_type = 'text/plain' | |
363 response.content_disposition = 'attachment; filename=%s.patch' \ | 284 response.content_disposition = 'attachment; filename=%s.patch' \ |
364 % revision | 285 % revision |
365 | 286 return render('changeset/raw_changeset.html') |
366 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id | 287 elif method == 'raw': |
367 for x in c.changeset.parents]) | 288 response.content_type = 'text/plain' |
368 | 289 return render('changeset/raw_changeset.html') |
369 c.diffs = '' | 290 elif method == 'show': |
370 for x in c.changes: | 291 if len(c.cs_ranges) == 1: |
371 c.diffs += x[2] | 292 return render('changeset/changeset.html') |
372 | 293 else: |
373 return render('changeset/raw_changeset.html') | 294 return render('changeset/changeset_range.html') |
295 | |
296 def raw_changeset(self, revision): | |
297 return self.index(revision) | |
374 | 298 |
375 @jsonify | 299 @jsonify |
376 def comment(self, repo_name, revision): | 300 def comment(self, repo_name, revision): |
377 status = request.POST.get('changeset_status') | 301 status = request.POST.get('changeset_status') |
378 change_status = request.POST.get('change_changeset_status') | 302 change_status = request.POST.get('change_changeset_status') |