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')