# HG changeset patch # User Marcin Kuzminski # Date 2012-11-05 18:57:29 # Node ID 32471bd1f4ee8f1dd30b3f051c503f734ad7ccdc # Parent af0d7d8acb22e8bb82b9857264865f4c87893285 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 diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -47,7 +47,7 @@ from rhodecode.model.db import Changeset from rhodecode.model.comment import ChangesetCommentsModel from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.meta import Session -from rhodecode.lib.diffs import wrapped_diff +from rhodecode.lib.diffs import LimitedDiffContainer from rhodecode.model.repo import RepoModel from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError from rhodecode.lib.vcs.backends.base import EmptyChangeset @@ -109,7 +109,13 @@ def _ignorews_url(GET, fileid=None): def get_line_ctx(fid, GET): ln_ctx_global = GET.get('context') - ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid)) + if fid: + ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid)) + else: + _ln_ctx = filter(lambda k: k.startswith('C'), GET) + ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global + if ln_ctx: + ln_ctx = [ln_ctx] if ln_ctx: retval = ln_ctx[0].split(':')[-1] @@ -119,7 +125,7 @@ def get_line_ctx(fid, GET): try: return int(retval) except: - return + return 3 def _context_url(GET, fileid=None): @@ -174,7 +180,7 @@ class ChangesetController(BaseRepoContro c.users_groups_array = repo_model.get_users_groups_js() def index(self, revision): - + method = request.GET.get('diff', 'show') c.anchor_url = anchor_url c.ignorews_url = _ignorews_url c.context_url = _context_url @@ -206,95 +212,63 @@ class ChangesetController(BaseRepoContro c.lines_added = 0 # count of lines added c.lines_deleted = 0 # count of lines removes - cumulative_diff = 0 - c.cut_off = False # defines if cut off limit is reached c.changeset_statuses = ChangesetStatus.STATUSES c.comments = [] c.statuses = [] c.inline_comments = [] c.inline_cnt = 0 + # Iterate over ranges (default changeset view is always one changeset) for changeset in c.cs_ranges: - - c.statuses.extend([ChangesetStatusModel()\ - .get_status(c.rhodecode_db_repo.repo_id, - changeset.raw_id)]) + inlines = [] + if method == 'show': + c.statuses.extend([ChangesetStatusModel()\ + .get_status(c.rhodecode_db_repo.repo_id, + changeset.raw_id)]) - c.comments.extend(ChangesetCommentsModel()\ - .get_comments(c.rhodecode_db_repo.repo_id, - revision=changeset.raw_id)) - inlines = ChangesetCommentsModel()\ - .get_inline_comments(c.rhodecode_db_repo.repo_id, - revision=changeset.raw_id) - c.inline_comments.extend(inlines) + c.comments.extend(ChangesetCommentsModel()\ + .get_comments(c.rhodecode_db_repo.repo_id, + revision=changeset.raw_id)) + inlines = ChangesetCommentsModel()\ + .get_inline_comments(c.rhodecode_db_repo.repo_id, + revision=changeset.raw_id) + c.inline_comments.extend(inlines) + c.changes[changeset.raw_id] = [] - try: - changeset_parent = changeset.parents[0] - except IndexError: - changeset_parent = None + + cs2 = changeset.raw_id + cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset() + context_lcl = get_line_ctx('', request.GET) + ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET) - #================================================================== - # ADDED FILES - #================================================================== - for node in changeset.added: - fid = h.FID(revision, node.path) - line_context_lcl = get_line_ctx(fid, request.GET) - ign_whitespace_lcl = get_ignore_ws(fid, request.GET) - lim = self.cut_off_limit - if cumulative_diff > self.cut_off_limit: - lim = -1 if limit_off is None else None - size, cs1, cs2, diff, st = wrapped_diff( - filenode_old=None, - filenode_new=node, - cut_off_limit=lim, - ignore_whitespace=ign_whitespace_lcl, - line_context=line_context_lcl, - enable_comments=enable_comments - ) - cumulative_diff += size - c.lines_added += st[0] - c.lines_deleted += st[1] - c.changes[changeset.raw_id].append( - ('added', node, diff, cs1, cs2, st) - ) - - #================================================================== - # CHANGED FILES - #================================================================== - for node in changeset.changed: - try: - filenode_old = changeset_parent.get_node(node.path) - except ChangesetError: - log.warning('Unable to fetch parent node for diff') - filenode_old = FileNode(node.path, '', EmptyChangeset()) - - fid = h.FID(revision, node.path) - line_context_lcl = get_line_ctx(fid, request.GET) - ign_whitespace_lcl = get_ignore_ws(fid, request.GET) - lim = self.cut_off_limit - if cumulative_diff > self.cut_off_limit: - lim = -1 if limit_off is None else None - size, cs1, cs2, diff, st = wrapped_diff( - filenode_old=filenode_old, - filenode_new=node, - cut_off_limit=lim, - ignore_whitespace=ign_whitespace_lcl, - line_context=line_context_lcl, - enable_comments=enable_comments - ) - cumulative_diff += size - c.lines_added += st[0] - c.lines_deleted += st[1] - c.changes[changeset.raw_id].append( - ('changed', node, diff, cs1, cs2, st) - ) - #================================================================== - # REMOVED FILES - #================================================================== - for node in changeset.removed: - c.changes[changeset.raw_id].append( - ('removed', node, None, None, None, (0, 0)) - ) + _diff = c.rhodecode_repo.get_diff(cs1, cs2, + ignore_whitespace=ign_whitespace_lcl, context=context_lcl) + diff_limit = self.cut_off_limit if not limit_off else None + diff_processor = diffs.DiffProcessor(_diff, + vcs=c.rhodecode_repo.alias, + format='gitdiff', + diff_limit=diff_limit) + cs_changes = OrderedDict() + if method == 'show': + _parsed = diff_processor.prepare() + c.limited_diff = False + if isinstance(_parsed, LimitedDiffContainer): + c.limited_diff = True + for f in _parsed: + st = f['stats'] + if st[0] != 'b': + c.lines_added += st[0] + c.lines_deleted += st[1] + fid = h.FID(changeset.raw_id, f['filename']) + diff = diff_processor.as_html(enable_comments=enable_comments, + parsed_lines=[f]) + cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'], + diff, st] + else: + # downloads/raw we only need RAW diff nothing else + diff = diff_processor.as_raw() + cs_changes[''] = [None, None, None, None, diff, None] + c.changes[changeset.raw_id] = cs_changes # count inline comments for __, lines in c.inline_comments: @@ -303,74 +277,24 @@ class ChangesetController(BaseRepoContro if len(c.cs_ranges) == 1: c.changeset = c.cs_ranges[0] - c.changes = c.changes[c.changeset.raw_id] - - return render('changeset/changeset.html') - else: - return render('changeset/changeset_range.html') + c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id + for x in c.changeset.parents]) + if method == 'download': + response.content_type = 'text/plain' + response.content_disposition = 'attachment; filename=%s.patch' \ + % revision + return render('changeset/raw_changeset.html') + elif method == 'raw': + response.content_type = 'text/plain' + return render('changeset/raw_changeset.html') + elif method == 'show': + if len(c.cs_ranges) == 1: + return render('changeset/changeset.html') + else: + return render('changeset/changeset_range.html') def raw_changeset(self, revision): - - method = request.GET.get('diff', 'show') - ignore_whitespace = request.GET.get('ignorews') == '1' - line_context = request.GET.get('context', 3) - try: - c.scm_type = c.rhodecode_repo.alias - c.changeset = c.rhodecode_repo.get_changeset(revision) - except RepositoryError: - log.error(traceback.format_exc()) - return redirect(url('home')) - else: - try: - c.changeset_parent = c.changeset.parents[0] - except IndexError: - c.changeset_parent = None - c.changes = [] - - for node in c.changeset.added: - filenode_old = FileNode(node.path, '') - if filenode_old.is_binary or node.is_binary: - diff = _('binary file') + '\n' - else: - f_gitdiff = diffs.get_gitdiff(filenode_old, node, - ignore_whitespace=ignore_whitespace, - context=line_context) - diff = diffs.DiffProcessor(f_gitdiff, - format='gitdiff').raw_diff() - - cs1 = None - cs2 = node.changeset.raw_id - c.changes.append(('added', node, diff, cs1, cs2)) - - for node in c.changeset.changed: - filenode_old = c.changeset_parent.get_node(node.path) - if filenode_old.is_binary or node.is_binary: - diff = _('binary file') - else: - f_gitdiff = diffs.get_gitdiff(filenode_old, node, - ignore_whitespace=ignore_whitespace, - context=line_context) - diff = diffs.DiffProcessor(f_gitdiff, - format='gitdiff').raw_diff() - - cs1 = filenode_old.changeset.raw_id - cs2 = node.changeset.raw_id - c.changes.append(('changed', node, diff, cs1, cs2)) - - response.content_type = 'text/plain' - - if method == 'download': - response.content_disposition = 'attachment; filename=%s.patch' \ - % revision - - c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id - for x in c.changeset.parents]) - - c.diffs = '' - for x in c.changes: - c.diffs += x[2] - - return render('changeset/raw_changeset.html') + return self.index(revision) @jsonify def comment(self, repo_name, revision): diff --git a/rhodecode/controllers/compare.py b/rhodecode/controllers/compare.py --- a/rhodecode/controllers/compare.py +++ b/rhodecode/controllers/compare.py @@ -142,7 +142,7 @@ class CompareController(BaseRepoControll for f in _parsed: fid = h.FID('', f['filename']) c.files.append([fid, f['operation'], f['filename'], f['stats']]) - diff = diff_processor.as_html(enable_comments=False, diff_lines=[f]) + diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f]) c.changes[fid] = [f['operation'], f['filename'], diff] return render('compare/compare_diff.html') diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py --- a/rhodecode/controllers/files.py +++ b/rhodecode/controllers/files.py @@ -438,6 +438,7 @@ class FilesController(BaseRepoController #special case if we want a show rev only, it's impl here #to reduce JS and callbacks + if request.GET.get('show_rev'): if str2bool(request.GET.get('annotate', 'False')): _url = url('files_annotate_home', repo_name=c.repo_name, @@ -450,18 +451,31 @@ class FilesController(BaseRepoController try: if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]: c.changeset_1 = c.rhodecode_repo.get_changeset(diff1) - node1 = c.changeset_1.get_node(f_path) + try: + node1 = c.changeset_1.get_node(f_path) + except NodeDoesNotExistError: + c.changeset_1 = EmptyChangeset(cs=diff1, + revision=c.changeset_1.revision, + repo=c.rhodecode_repo) + node1 = FileNode(f_path, '', changeset=c.changeset_1) else: c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo) - node1 = FileNode('.', '', changeset=c.changeset_1) + node1 = FileNode(f_path, '', changeset=c.changeset_1) if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]: c.changeset_2 = c.rhodecode_repo.get_changeset(diff2) - node2 = c.changeset_2.get_node(f_path) + try: + node2 = c.changeset_2.get_node(f_path) + except NodeDoesNotExistError: + c.changeset_2 = EmptyChangeset(cs=diff2, + revision=c.changeset_2.revision, + repo=c.rhodecode_repo) + node2 = FileNode(f_path, '', changeset=c.changeset_2) else: c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo) - node2 = FileNode('.', '', changeset=c.changeset_2) + node2 = FileNode(f_path, '', changeset=c.changeset_2) except RepositoryError: + log.error(traceback.format_exc()) return redirect(url('files_home', repo_name=c.repo_name, f_path=f_path)) @@ -476,7 +490,7 @@ class FilesController(BaseRepoController response.content_disposition = ( 'attachment; filename=%s' % diff_name ) - return diff.raw_diff() + return diff.as_raw() elif c.action == 'raw': _diff = diffs.get_gitdiff(node1, node2, @@ -484,7 +498,7 @@ class FilesController(BaseRepoController context=line_context) diff = diffs.DiffProcessor(_diff, format='gitdiff') response.content_type = 'text/plain' - return diff.raw_diff() + return diff.as_raw() else: fid = h.FID(diff2, node2.path) @@ -498,8 +512,12 @@ class FilesController(BaseRepoController ignore_whitespace=ign_whitespace_lcl, line_context=line_context_lcl, enable_comments=False) - - c.changes = [('', node2, diff, cs1, cs2, st,)] + op = '' + filename = node1.path + cs_changes = { + 'fid': [cs1, cs2, op, filename, diff, st] + } + c.changes = cs_changes return render('files/file_diff.html') diff --git a/rhodecode/controllers/pullrequests.py b/rhodecode/controllers/pullrequests.py --- a/rhodecode/controllers/pullrequests.py +++ b/rhodecode/controllers/pullrequests.py @@ -299,7 +299,7 @@ class PullrequestsController(BaseRepoCon fid = h.FID('', f['filename']) c.files.append([fid, f['operation'], f['filename'], f['stats']]) diff = diff_processor.as_html(enable_comments=enable_comments, - diff_lines=[f]) + parsed_lines=[f]) c.changes[fid] = [f['operation'], f['filename'], diff] def show(self, repo_name, pull_request_id): diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py --- a/rhodecode/lib/diffs.py +++ b/rhodecode/lib/diffs.py @@ -28,6 +28,7 @@ import re import difflib import logging +import traceback from itertools import tee, imap @@ -127,54 +128,103 @@ def get_gitdiff(filenode_old, filenode_n new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET) vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path, - ignore_whitespace, context) + ignore_whitespace, context) return vcs_gitdiff +NEW_FILENODE = 1 +DEL_FILENODE = 2 +MOD_FILENODE = 3 +RENAMED_FILENODE = 4 +CHMOD_FILENODE = 5 + + +class DiffLimitExceeded(Exception): + pass + + +class LimitedDiffContainer(object): + + def __init__(self, diff_limit, cur_diff_size, diff): + self.diff = diff + self.diff_limit = diff_limit + self.cur_diff_size = cur_diff_size + + def __iter__(self): + for l in self.diff: + yield l + class DiffProcessor(object): """ - Give it a unified diff and it returns a list of the files that were + Give it a unified or git diff and it returns a list of the files that were mentioned in the diff together with a dict of meta information that can be used to render it in a HTML template. """ _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)') _newline_marker = '\\ No newline at end of file\n' + _git_header_re = re.compile(r""" + #^diff[ ]--git + [ ]a/(?P.+?)[ ]b/(?P.+?)\n + (?:^similarity[ ]index[ ](?P\d+)%\n + ^rename[ ]from[ ](?P\S+)\n + ^rename[ ]to[ ](?P\S+)(?:\n|$))? + (?:^old[ ]mode[ ](?P\d+)\n + ^new[ ]mode[ ](?P\d+)(?:\n|$))? + (?:^new[ ]file[ ]mode[ ](?P.+)(?:\n|$))? + (?:^deleted[ ]file[ ]mode[ ](?P.+)(?:\n|$))? + (?:^index[ ](?P[0-9A-Fa-f]+) + \.\.(?P[0-9A-Fa-f]+)[ ]?(?P.+)?(?:\n|$))? + (?:^---[ ](a/(?P.+)|/dev/null)(?:\n|$))? + (?:^\+\+\+[ ](b/(?P.+)|/dev/null)(?:\n|$))? + """, re.VERBOSE | re.MULTILINE) + _hg_header_re = re.compile(r""" + #^diff[ ]--git + [ ]a/(?P.+?)[ ]b/(?P.+?)\n + (?:^similarity[ ]index[ ](?P\d+)%(?:\n|$))? + (?:^rename[ ]from[ ](?P\S+)\n + ^rename[ ]to[ ](?P\S+)(?:\n|$))? + (?:^old[ ]mode[ ](?P\d+)\n + ^new[ ]mode[ ](?P\d+)(?:\n|$))? + (?:^new[ ]file[ ]mode[ ](?P.+)(?:\n|$))? + (?:^deleted[ ]file[ ]mode[ ](?P.+)(?:\n|$))? + (?:^index[ ](?P[0-9A-Fa-f]+) + \.\.(?P[0-9A-Fa-f]+)[ ]?(?P.+)?(?:\n|$))? + (?:^---[ ](a/(?P.+)|/dev/null)(?:\n|$))? + (?:^\+\+\+[ ](b/(?P.+)|/dev/null)(?:\n|$))? + """, re.VERBOSE | re.MULTILINE) - def __init__(self, diff, differ='diff', format='gitdiff'): - """ - :param diff: a text in diff format or generator - :param format: format of diff passed, `udiff` or `gitdiff` + def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None): """ - if isinstance(diff, basestring): - diff = [diff] + :param diff: a text in diff format + :param vcs: type of version controll hg or git + :param format: format of diff passed, `udiff` or `gitdiff` + :param diff_limit: define the size of diff that is considered "big" + based on that parameter cut off will be triggered, set to None + to show full diff + """ + if not isinstance(diff, basestring): + raise Exception('Diff must be a basestring got %s instead' % type(diff)) - self.__udiff = diff - self.__format = format + self._diff = diff + self._format = format self.adds = 0 self.removes = 0 - - if isinstance(self.__udiff, basestring): - self.lines = iter(self.__udiff.splitlines(1)) + # calculate diff size + self.diff_size = len(diff) + self.diff_limit = diff_limit + self.cur_diff_size = 0 + self.parsed = False + self.parsed_diff = [] + self.vcs = vcs - elif self.__format == 'gitdiff': - udiff_copy = self.copy_iterator() - self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy)) - else: - udiff_copy = self.copy_iterator() - self.lines = imap(self.escaper, udiff_copy) - - # Select a differ. - if differ == 'difflib': + if format == 'gitdiff': self.differ = self._highlight_line_difflib + self._parser = self._parse_gitdiff else: self.differ = self._highlight_line_udiff + self._parser = self._parse_udiff - def escaper(self, string): - return string.replace('&', '&')\ - .replace('<', '<')\ - .replace('>', '>') - - def copy_iterator(self): + def _copy_iterator(self): """ make a fresh copy of generator, we should not iterate thru an original as it's needed for repeating operations on @@ -183,58 +233,36 @@ class DiffProcessor(object): self.__udiff, iterator_copy = tee(self.__udiff) return iterator_copy - def _extract_rev(self, line1, line2): + def _escaper(self, string): """ - Extract the operation (A/M/D), filename and revision hint from a line. + Escaper for diff escapes special chars and checks the diff limit + + :param string: + :type string: """ - try: - if line1.startswith('--- ') and line2.startswith('+++ '): - l1 = line1[4:].split(None, 1) - old_filename = (l1[0].replace('a/', '', 1) - if len(l1) >= 1 else None) - old_rev = l1[1] if len(l1) == 2 else 'old' + self.cur_diff_size += len(string) - l2 = line2[4:].split(None, 1) - new_filename = (l2[0].replace('b/', '', 1) - if len(l1) >= 1 else None) - new_rev = l2[1] if len(l2) == 2 else 'new' - - filename = (old_filename - if old_filename != '/dev/null' else new_filename) + # escaper get's iterated on each .next() call and it checks if each + # parsed line doesn't exceed the diff limit + if self.diff_limit is not None and self.cur_diff_size > self.diff_limit: + raise DiffLimitExceeded('Diff Limit Exceeded') - operation = 'D' if new_filename == '/dev/null' else None - if not operation: - operation = 'M' if old_filename != '/dev/null' else 'A' - - return operation, filename, new_rev, old_rev - except (ValueError, IndexError): - pass + return safe_unicode(string).replace('&', '&')\ + .replace('<', '<')\ + .replace('>', '>') - return None, None, None, None - - def _parse_gitdiff(self, diffiterator): - def line_decoder(l): - if l.startswith('+') and not l.startswith('+++'): - self.adds += 1 - elif l.startswith('-') and not l.startswith('---'): - self.removes += 1 - return safe_unicode(l) + def _line_counter(self, l): + """ + Checks each line and bumps total adds/removes for this diff - output = list(diffiterator) - size = len(output) - - if size == 2: - l = [] - l.extend([output[0]]) - l.extend(output[1].splitlines(1)) - return map(line_decoder, l) - elif size == 1: - return map(line_decoder, output[0].splitlines(1)) - elif size == 0: - return [] - - raise Exception('wrong size of diff %s' % size) + :param l: + """ + if l.startswith('+') and not l.startswith('+++'): + self.adds += 1 + elif l.startswith('-') and not l.startswith('---'): + self.removes += 1 + return l def _highlight_line_difflib(self, line, next_): """ @@ -296,122 +324,108 @@ class DiffProcessor(object): do(line) do(next_) - def _parse_udiff(self, inline_diff=True): + def _get_header(self, diff_chunk): """ - Parse the diff an return data for the template. - """ - lineiter = self.lines + parses the diff header, and returns parts, and leftover diff + parts consists of 14 elements:: - files = [] - try: - line = lineiter.next() - while 1: - # continue until we found the old file - if not line.startswith('--- '): - line = lineiter.next() - continue + a_path, b_path, similarity_index, rename_from, rename_to, + old_mode, new_mode, new_file_mode, deleted_file_mode, + a_blob_id, b_blob_id, b_mode, a_file, b_file + + :param diff_chunk: + :type diff_chunk: + """ - chunks = [] - stats = [0, 0] - operation, filename, old_rev, new_rev = \ - self._extract_rev(line, lineiter.next()) - files.append({ - 'filename': filename, - 'old_revision': old_rev, - 'new_revision': new_rev, - 'chunks': chunks, - 'operation': operation, - 'stats': stats, - }) - - line = lineiter.next() + if self.vcs == 'git': + match = self._git_header_re.match(diff_chunk) + diff = diff_chunk[match.end():] + return match.groupdict(), imap(self._escaper, diff.splitlines(1)) + elif self.vcs == 'hg': + match = self._hg_header_re.match(diff_chunk) + diff = diff_chunk[match.end():] + return match.groupdict(), imap(self._escaper, diff.splitlines(1)) + else: + raise Exception('VCS type %s is not supported' % self.vcs) - while line: - match = self._chunk_re.match(line) - if not match: - break - - lines = [] - chunks.append(lines) + def _parse_gitdiff(self, inline_diff=True): + _files = [] + diff_container = lambda arg: arg - old_line, old_end, new_line, new_end = \ - [int(x or 1) for x in match.groups()[:-1]] - old_line -= 1 - new_line -= 1 - gr = match.groups() - context = len(gr) == 5 - old_end += old_line - new_end += new_line + ##split the diff in chunks of separate --git a/file b/file chunks + for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]: + binary = False + binary_msg = 'unknown binary' + head, diff = self._get_header(raw_diff) - if context: - # skip context only if it's first line - if int(gr[0]) > 1: - lines.append({ - 'old_lineno': '...', - 'new_lineno': '...', - 'action': 'context', - 'line': line, - }) - - line = lineiter.next() - - while old_line < old_end or new_line < new_end: - if line: - command = line[0] - if command in ['+', '-', ' ']: - #only modify the line if it's actually a diff - # thing - line = line[1:] - else: - command = ' ' - - affects_old = affects_new = False + if not head['a_file'] and head['b_file']: + op = 'A' + elif head['a_file'] and head['b_file']: + op = 'M' + elif head['a_file'] and not head['b_file']: + op = 'D' + else: + #probably we're dealing with a binary file 1 + binary = True + if head['deleted_file_mode']: + op = 'D' + stats = ['b', DEL_FILENODE] + binary_msg = 'deleted binary file' + elif head['new_file_mode']: + op = 'A' + stats = ['b', NEW_FILENODE] + binary_msg = 'new binary file %s' % head['new_file_mode'] + else: + if head['new_mode'] and head['old_mode']: + stats = ['b', CHMOD_FILENODE] + op = 'M' + binary_msg = ('modified binary file chmod %s => %s' + % (head['old_mode'], head['new_mode'])) + elif (head['rename_from'] and head['rename_to'] + and head['rename_from'] != head['rename_to']): + stats = ['b', RENAMED_FILENODE] + op = 'M' + binary_msg = ('file renamed from %s to %s' + % (head['rename_from'], head['rename_to'])) + else: + stats = ['b', MOD_FILENODE] + op = 'M' + binary_msg = 'modified binary file' - # ignore those if we don't expect them - if command in '#@': - continue - elif command == '+': - affects_new = True - action = 'add' - stats[0] += 1 - elif command == '-': - affects_old = True - action = 'del' - stats[1] += 1 - else: - affects_old = affects_new = True - action = 'unmod' + if not binary: + try: + chunks, stats = self._parse_lines(diff) + except DiffLimitExceeded: + diff_container = lambda _diff: LimitedDiffContainer( + self.diff_limit, + self.cur_diff_size, + _diff) + break + else: + chunks = [] + chunks.append([{ + 'old_lineno': '', + 'new_lineno': '', + 'action': 'binary', + 'line': binary_msg, + }]) - if line != self._newline_marker: - old_line += affects_old - new_line += affects_new - lines.append({ - 'old_lineno': affects_old and old_line or '', - 'new_lineno': affects_new and new_line or '', - 'action': action, - 'line': line - }) - - line = lineiter.next() - if line == self._newline_marker: - # we need to append to lines, since this is not - # counted in the line specs of diff - lines.append({ - 'old_lineno': '...', - 'new_lineno': '...', - 'action': 'context', - 'line': line - }) - - except StopIteration: - pass + _files.append({ + 'filename': head['b_path'], + 'old_revision': head['a_blob_id'], + 'new_revision': head['b_blob_id'], + 'chunks': chunks, + 'operation': op, + 'stats': stats, + }) sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation']) + if inline_diff is False: - return sorted(files, key=sorter) + return diff_container(sorted(_files, key=sorter)) # highlight inline changes - for diff_data in files: + for diff_data in _files: for chunk in diff_data['chunks']: lineiter = iter(chunk) try: @@ -426,14 +440,106 @@ class DiffProcessor(object): except StopIteration: pass - return sorted(files, key=sorter) + return diff_container(sorted(_files, key=sorter)) - def prepare(self, inline_diff=True): + def _parse_udiff(self, inline_diff=True): + raise NotImplementedError() + + def _parse_lines(self, diff): + """ + Parse the diff an return data for the template. """ - Prepare the passed udiff for HTML rendering. It'l return a list - of dicts - """ - return self._parse_udiff(inline_diff=inline_diff) + + lineiter = iter(diff) + stats = [0, 0] + + try: + chunks = [] + line = lineiter.next() + + while line: + lines = [] + chunks.append(lines) + + match = self._chunk_re.match(line) + + if not match: + break + + gr = match.groups() + (old_line, old_end, + new_line, new_end) = [int(x or 1) for x in gr[:-1]] + old_line -= 1 + new_line -= 1 + + context = len(gr) == 5 + old_end += old_line + new_end += new_line + + if context: + # skip context only if it's first line + if int(gr[0]) > 1: + lines.append({ + 'old_lineno': '...', + 'new_lineno': '...', + 'action': 'context', + 'line': line, + }) + + line = lineiter.next() + + while old_line < old_end or new_line < new_end: + if line: + command = line[0] + if command in ['+', '-', ' ']: + #only modify the line if it's actually a diff + # thing + line = line[1:] + else: + command = ' ' + + affects_old = affects_new = False + + # ignore those if we don't expect them + if command in '#@': + continue + elif command == '+': + affects_new = True + action = 'add' + stats[0] += 1 + elif command == '-': + affects_old = True + action = 'del' + stats[1] += 1 + else: + affects_old = affects_new = True + action = 'unmod' + + if line != self._newline_marker: + old_line += affects_old + new_line += affects_new + lines.append({ + 'old_lineno': affects_old and old_line or '', + 'new_lineno': affects_new and new_line or '', + 'action': action, + 'line': line + }) + + line = lineiter.next() + + if line == self._newline_marker: + # we need to append to lines, since this is not + # counted in the line specs of diff + lines.append({ + 'old_lineno': '...', + 'new_lineno': '...', + 'action': 'context', + 'line': line + }) + + except StopIteration: + pass + return chunks, stats def _safe_id(self, idstring): """Make a string safe for including in an id attribute. @@ -456,18 +562,25 @@ class DiffProcessor(object): idstring = re.sub(r'(?!-)\W', "", idstring).lower() return idstring - def raw_diff(self): + def prepare(self, inline_diff=True): + """ + Prepare the passed udiff for HTML rendering. It'l return a list + of dicts with diff information + """ + parsed = self._parser(inline_diff=inline_diff) + self.parsed = True + self.parsed_diff = parsed + return parsed + + def as_raw(self, diff_lines=None): """ Returns raw string as udiff """ - udiff_copy = self.copy_iterator() - if self.__format == 'gitdiff': - udiff_copy = self._parse_gitdiff(udiff_copy) - return u''.join(udiff_copy) + return u''.join(imap(self._line_counter, self._diff.splitlines(1))) def as_html(self, table_class='code-difftable', line_class='line', new_lineno_class='lineno old', old_lineno_class='lineno new', - code_class='code', enable_comments=False, diff_lines=None): + code_class='code', enable_comments=False, parsed_lines=None): """ Return given diff as html table with customized css classes """ @@ -483,13 +596,19 @@ class DiffProcessor(object): } else: return label - if diff_lines is None: - diff_lines = self.prepare() + if not self.parsed: + self.prepare() + + diff_lines = self.parsed_diff + if parsed_lines: + diff_lines = parsed_lines + _html_empty = True _html = [] _html.append('''\n''' % { 'table_class': table_class }) + for diff in diff_lines: for line in diff['chunks']: _html_empty = False @@ -582,9 +701,9 @@ class InMemoryBundleRepo(bundlerepositor def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None, - bundle_compare=False): + bundle_compare=False, context=3, ignore_whitespace=False): """ - General differ between branches, bookmarks or separate but releated + General differ between branches, bookmarks, revisions of two remote related repositories :param org_repo: @@ -598,8 +717,8 @@ def differ(org_repo, org_ref, other_repo """ bundlerepo = None - ignore_whitespace = False - context = 3 + ignore_whitespace = ignore_whitespace + context = context org_repo = org_repo.scm_instance._repo other_repo = other_repo.scm_instance._repo opts = diffopts(git=True, ignorews=ignore_whitespace, context=context) diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -891,27 +891,7 @@ def fancy_file_stats(stats): :param stats: two element list of added/deleted lines of code """ - - a, d, t = stats[0], stats[1], stats[0] + stats[1] - width = 100 - unit = float(width) / (t or 1) - - # needs > 9% of width to be visible or 0 to be hidden - a_p = max(9, unit * a) if a > 0 else 0 - d_p = max(9, unit * d) if d > 0 else 0 - p_sum = a_p + d_p - - if p_sum > width: - #adjust the percentage to be == 100% since we adjusted to 9 - if a_p > d_p: - a_p = a_p - (p_sum - width) - else: - d_p = d_p - (p_sum - width) - - a_v = a if a > 0 else '' - d_v = d if d > 0 else '' - - def cgen(l_type): + def cgen(l_type, a_v, d_v): mapping = {'tr': 'top-right-rounded-corner-mid', 'tl': 'top-left-rounded-corner-mid', 'br': 'bottom-right-rounded-corner-mid', @@ -931,11 +911,38 @@ def fancy_file_stats(stats): if l_type == 'd' and not a_v: return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl'])) + a, d = stats[0], stats[1] + width = 100 + + if a == 'b': + #binary mode + b_d = '
%s
' % (d, cgen('a', a_v='', d_v=0), 'bin') + b_a = '
%s
' % ('bin') + return literal('
%s%s
' % (width, b_a, b_d)) + + t = stats[0] + stats[1] + unit = float(width) / (t or 1) + + # needs > 9% of width to be visible or 0 to be hidden + a_p = max(9, unit * a) if a > 0 else 0 + d_p = max(9, unit * d) if d > 0 else 0 + p_sum = a_p + d_p + + if p_sum > width: + #adjust the percentage to be == 100% since we adjusted to 9 + if a_p > d_p: + a_p = a_p - (p_sum - width) + else: + d_p = d_p - (p_sum - width) + + a_v = a if a > 0 else '' + d_v = d if d > 0 else '' + d_a = '
%s
' % ( - cgen('a'), a_p, a_v + cgen('a', a_v, d_v), a_p, a_v ) d_d = '
%s
' % ( - cgen('d'), d_p, d_v + cgen('d', a_v, d_v), d_p, d_v ) return literal('
%s%s
' % (width, d_a, d_d)) diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -934,9 +934,9 @@ class EmptyChangeset(BaseChangeset): """ def __init__(self, cs='0' * 40, repo=None, requested_revision=None, - alias=None, message='', author='', date=''): + alias=None, revision=-1, message='', author='', date=''): self._empty_cs = cs - self.revision = -1 + self.revision = revision self.message = message self.author = author self.date = date diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py --- a/rhodecode/lib/vcs/backends/git/repository.py +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -520,7 +520,7 @@ class GitRepository(BaseRepository): :param context: How many lines before/after changed lines should be shown. Defaults to ``3``. """ - flags = ['-U%s' % context] + flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40'] if ignore_whitespace: flags.append('-w') @@ -540,6 +540,7 @@ class GitRepository(BaseRepository): if path: cmd += ' -- "%s"' % path + stdout, stderr = self.run_git_command(cmd) # If we used 'show' command, strip first few lines (until actual diff # starts) diff --git a/rhodecode/lib/vcs/backends/hg/repository.py b/rhodecode/lib/vcs/backends/hg/repository.py --- a/rhodecode/lib/vcs/backends/hg/repository.py +++ b/rhodecode/lib/vcs/backends/hg/repository.py @@ -242,8 +242,11 @@ class MercurialRepository(BaseRepository if rev1 != self.EMPTY_CHANGESET: self.get_changeset(rev1) self.get_changeset(rev2) + if path: + file_filter = match(self.path, '', [path]) + else: + file_filter = None - file_filter = match(self.path, '', [path]) return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter, opts=diffopts(git=True, ignorews=ignore_whitespace, diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css +++ b/rhodecode/public/css/style.css @@ -2506,6 +2506,42 @@ h3.files_location { font-size: 9px; padding: 2px 0px 2px 0px; } +/*new binary*/ +.cs_files .changes .bin1 { + background-color: #BBFFBB; + float: left; + text-align: center; + font-size: 9px; + padding: 2px 0px 2px 0px; +} + +/*deleted binary*/ +.cs_files .changes .bin2 { + background-color: #FF8888; + float: left; + text-align: center; + font-size: 9px; + padding: 2px 0px 2px 0px; +} + +/*mod binary*/ +.cs_files .changes .bin3 { + background-color: #DDDDDD; + float: left; + text-align: center; + font-size: 9px; + padding: 2px 0px 2px 0px; +} + +/*rename file*/ +.cs_files .changes .bin4 { + background-color: #6D99FF; + float: left; + text-align: center; + font-size: 9px; + padding: 2px 0px 2px 0px; +} + .cs_files .cs_added,.cs_files .cs_A { background: url("../images/icons/page_white_add.png") no-repeat scroll diff --git a/rhodecode/templates/changeset/changeset.html b/rhodecode/templates/changeset/changeset.html --- a/rhodecode/templates/changeset/changeset.html +++ b/rhodecode/templates/changeset/changeset.html @@ -24,6 +24,12 @@
${self.breadcrumbs()}
+
@@ -40,7 +46,7 @@ %endif
- + ${c.ignorews_url(request.GET)} ${c.context_url(request.GET)} @@ -100,37 +106,36 @@
+ % if c.limited_diff: + ${_('%s files affected:') % (len(c.changeset.affected_files))} + % else: ${_('%s files affected with %s insertions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)} - + %endif +
- %for change,filenode,diff,cs1,cs2,stat in c.changes: -
-
- %if change != 'removed': - ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path,request.GET)+"_target")} - %else: - ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))} - %endif -
-
${h.fancy_file_stats(stat)}
-
- %endfor - % if c.cut_off: - ${_('Changeset was too big and was cut off...')} - % endif + %for FID, (cs1, cs2, change, path, diff, stats) in c.changes[c.changeset.raw_id].iteritems(): +
+ +
${h.fancy_file_stats(stats)}
+
+ %endfor + % if c.limited_diff: +
${_('Changeset was too big and was cut off...')}
+ % endif
- + ## diff block <%namespace name="diff_block" file="/changeset/diff_block.html"/> - ${diff_block.diff_block(c.changes)} + ${diff_block.diff_block(c.changes[c.changeset.raw_id])} + + % if c.limited_diff: +

${_('Changeset was too big and was cut off...')}

+ % endif ## template for inline comment form <%namespace name="comment" file="/changeset/changeset_file_comment.html"/> diff --git a/rhodecode/templates/changeset/changeset_range.html b/rhodecode/templates/changeset/changeset_range.html --- a/rhodecode/templates/changeset/changeset_range.html +++ b/rhodecode/templates/changeset/changeset_range.html @@ -55,9 +55,9 @@
%for cs in c.cs_ranges:
${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
- %for change,filenode,diff,cs1,cs2,st in c.changes[cs.raw_id]: -
${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID(cs.raw_id,filenode.path)))}
- %endfor + %for FID, (cs1, cs2, change, path, diff, stats) in c.changes[cs.raw_id].iteritems(): +
${h.link_to(h.safe_unicode(path),h.url.current(anchor=FID))}
+ %endfor %endfor
diff --git a/rhodecode/templates/changeset/diff_block.html b/rhodecode/templates/changeset/diff_block.html --- a/rhodecode/templates/changeset/diff_block.html +++ b/rhodecode/templates/changeset/diff_block.html @@ -5,37 +5,37 @@ ## <%def name="diff_block(change)"> -%for op,filenode,diff,cs1,cs2,stat in change: - %if op !='removed': -
-
+%for FID,(cs1, cs2, change, path, diff, stats) in change.iteritems(): + ##%if op !='removed': +
+
- ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name, - revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))} + ${h.link_to_if(change!='removed',h.safe_unicode(path),h.url('files_home',repo_name=c.repo_name, + revision=cs2,f_path=h.safe_unicode(path)))}
- - - - ${c.ignorews_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))} - ${c.context_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))} + + + + ${c.ignorews_url(request.GET, h.FID(cs2,path))} + ${c.context_url(request.GET, h.FID(cs2,path))}
-
+
${diff|n}
- %endif + ##%endif %endfor diff --git a/rhodecode/templates/changeset/raw_changeset.html b/rhodecode/templates/changeset/raw_changeset.html --- a/rhodecode/templates/changeset/raw_changeset.html +++ b/rhodecode/templates/changeset/raw_changeset.html @@ -1,9 +1,11 @@ -%if h.is_hg(c.scm_type): -# ${c.scm_type.upper()} changeset patch +%if h.is_hg(c.rhodecode_repo): +# ${c.rhodecode_repo.alias.upper()} changeset patch # User ${c.changeset.author|n} # Date ${c.changeset.date} # Node ID ${c.changeset.raw_id} ${c.parent_tmpl} ${c.changeset.message} %endif -${c.diffs|n} +%for FID, (cs1, cs2, change, path, diff, stats) in c.changes[c.changeset.raw_id].iteritems(): +${diff|n} +%endfor diff --git a/rhodecode/tests/fixtures/git_diff_binary_and_normal.diff b/rhodecode/tests/fixtures/git_diff_binary_and_normal.diff new file mode 100644 --- /dev/null +++ b/rhodecode/tests/fixtures/git_diff_binary_and_normal.diff @@ -0,0 +1,570 @@ +diff --git a/img/baseline-10px.png b/img/baseline-10px.png +new file mode 100644 +index 0000000000000000000000000000000000000000..16095dcbf5c9ea41caeb1e3e41d647d425222ed1 +GIT binary patch +literal 152 +zcmeAS@N?(olHy`uVBq!ia0vp^j6j^i!3HGVb)pi0lw^r(L`iUdT1k0gQ7VIDN`6wR +zf@f}GdTLN=VoGJ<$y6JlA}dc9$B>F!Nx%O8w`Ue+77%bXFxq5j_~-xsZV_1~1zCBH +y)y@U((_~Lrb!=|_@`K?vV_&A58+!u-Gs6x+MGjBnI|qTLFnGH9xvXF!Nk9Jow`XSN + + +- Baseline ++ Twitter Baseline + + + +@@ -11,6 +11,7 @@ + + + ++ + + ++ + ++ ++ ++ ++ ++ ++ ++
++ ++
++ ${next.body()} ++ ++ ++ ++
++ ++
++ ++ ++ +\ No newline at end of file +diff --git a/pylons_app/templates/errors/error_document.html b/pylons_app/templates/errors/error_document.html +new file mode 100755 +--- /dev/null ++++ b/pylons_app/templates/errors/error_document.html +@@ -0,0 +1,39 @@ ++## -*- coding: utf-8 -*- ++ ++ ++ ++ ++ Error - ${c.error_message} ++ ++ ++ ++ ++ ++ ++
++

${c.error_message}

++ ++

${c.error_explanation}

++

${_('You will be redirected to %s in %s seconds') % (c.redirect_module,c.redirect_time)}

++
++ ++ ++ ++ +diff --git a/pylons_app/templates/index.html b/pylons_app/templates/index.html +new file mode 100644 +--- /dev/null ++++ b/pylons_app/templates/index.html +@@ -0,0 +1,161 @@ ++ ## -*- coding: utf-8 -*- ++<%inherit file = "base/base.html"/> ++ ++<%def name="page_title()"> ++ ${_('Wire transfer')} ++ ++ ++<%def name="body()"> ++ ++ ++ ++ ++ ++ ++
++ ++ ++ ++
++ ++

Hello we are Luminous

++

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do ++eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad ++minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ++ex ea commodo consequat.


++ ++

What we do

++ ++
++ ++ ++ ++
++

Design

++

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do ++eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad ++minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ++ex ea. More »

++
++ ++
++ ++
++ ++ ++ ++
++

Development

++

${h.secure_form('/home/make_payment',method='post',id="secure_form")} ++ ##Secure Form Tag for prevention of Cross-site request forgery (CSRF) attacks. ++ ##Generates form tags that include client-specific authorization tokens to be verified by the destined web app. ++ ++

++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++
${h.get_error('_authentication_token',c.form_errors)}
${_('Account number')}${h.text('account_number',size=44,maxlength=38)}${h.get_error('account_number',c.form_errors)}
${_('Title')}${h.textarea("title", "", cols=43, rows=5,maxlength=20)}${h.get_error('title',c.form_errors)}
${_('Recipient')}${h.select('recipient',1,c.recipients_list)}${h.get_error('recipient',c.form_errors)}
${_('Recipient address')}${h.text('recipient_address',size=44)}${h.get_error('recipient_address',c.form_errors)}
${_('Amount')}${h.text('amount',size='7')}zł${h.get_error('amount',c.form_errors)}
${h.submit('send',_('send'))}
++ ${h.end_form()}More »

++ ++ ++ ++ ++
++ ++ ++ ++
++

Marketing

++

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do ++eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad ++minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ++ex ea. More »

++
++ ++
++ ++ ++ ++ ++ ++ ++ ++<%def name="footer()"> ++ ++ ++ ++ +diff --git a/pylons_app/templates/index2.html b/pylons_app/templates/index2.html +new file mode 100644 +--- /dev/null ++++ b/pylons_app/templates/index2.html +@@ -0,0 +1,110 @@ ++ ## -*- coding: utf-8 -*- ++<%inherit file = "base/base.html"/> ++ ++<%def name="page_title()"> ++ ${_('Wire transfer')} ++ ++ ++<%def name="body()"> ++

${h.link('Home','/')} / ${_('Wire transfer')}

++ ${h.secure_form('/home/make_payment',method='post',id="secure_form")} ++ ##Secure Form Tag for prevention of Cross-site request forgery (CSRF) attacks. ++ ##Generates form tags that include client-specific authorization tokens to be verified by the destined web app. ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++
${h.get_error('_authentication_token',c.form_errors)}
${_('Account number')}${h.text('account_number',size=44,maxlength=38)}${h.get_error('account_number',c.form_errors)}
${_('Title')}${h.textarea("title", "", cols=43, rows=5,maxlength=20)}${h.get_error('title',c.form_errors)}
${_('Recipient')}${h.select('recipient',1,c.recipients_list)}${h.get_error('recipient',c.form_errors)}
${_('Recipient address')}${h.text('recipient_address',size=44)}${h.get_error('recipient_address',c.form_errors)}
${_('Amount')}${h.text('amount',size='7')}zł${h.get_error('amount',c.form_errors)}
${h.submit('send',_('send'))}
++ ${h.end_form()} ++ ++ ${c.name} ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/pylons_app/templates/payment_confirmation.html b/pylons_app/templates/payment_confirmation.html +new file mode 100644 +--- /dev/null ++++ b/pylons_app/templates/payment_confirmation.html +@@ -0,0 +1,19 @@ ++## -*- coding: utf-8 -*- ++<%inherit file="base/base.html"/> ++<%def name="page_title()"> ++ ${_('Wire transfer confirmation')} ++ ++ ++<%def name="body()"> ++

${h.link('Home','/')} / ${_('Wire transfer confirmation')}

++ ++ % for k,v in c.form_result.items(): ++ %if k not in ['_authentication_token','recipient','send']: ++ ++ ++ ++ ++ %endif ++ % endfor ++
${k}${v}
++ +\ No newline at end of file +diff --git a/pylons_app/tests/__init__.py b/pylons_app/tests/__init__.py +new file mode 100644 +--- /dev/null ++++ b/pylons_app/tests/__init__.py +@@ -0,0 +1,36 @@ ++"""Pylons application test package ++ ++This package assumes the Pylons environment is already loaded, such as ++when this script is imported from the `nosetests --with-pylons=test.ini` ++command. ++ ++This module initializes the application via ``websetup`` (`paster ++setup-app`) and provides the base testing objects. ++""" ++from unittest import TestCase ++ ++from paste.deploy import loadapp ++from paste.script.appinstall import SetupCommand ++from pylons import config, url ++from routes.util import URLGenerator ++from webtest import TestApp ++ ++import pylons.test ++ ++__all__ = ['environ', 'url', 'TestController'] ++ ++# Invoke websetup with the current config file ++SetupCommand('setup-app').run([config['__file__']]) ++ ++environ = {} ++ ++class TestController(TestCase): ++ ++ def __init__(self, *args, **kwargs): ++ if pylons.test.pylonsapp: ++ wsgiapp = pylons.test.pylonsapp ++ else: ++ wsgiapp = loadapp('config:%s' % config['__file__']) ++ self.app = TestApp(wsgiapp) ++ url._push_object(URLGenerator(config['routes.map'], environ)) ++ TestCase.__init__(self, *args, **kwargs) +diff --git a/pylons_app/tests/functional/__init__.py b/pylons_app/tests/functional/__init__.py +new file mode 100644 +diff --git a/pylons_app/tests/test_models.py b/pylons_app/tests/test_models.py +new file mode 100644 +diff --git a/pylons_app/websetup.py b/pylons_app/websetup.py +new file mode 100644 +--- /dev/null ++++ b/pylons_app/websetup.py +@@ -0,0 +1,9 @@ ++"""Setup the pylons_app application""" ++import logging ++from pylons_app.config.environment import load_environment ++log = logging.getLogger(__name__) ++ ++ ++def setup_app(command, conf, vars): ++ """Place any commands to setup pylons_app here""" ++ load_environment(conf.global_conf, conf.local_conf) +diff --git a/setup.cfg b/setup.cfg +new file mode 100644 +--- /dev/null ++++ b/setup.cfg +@@ -0,0 +1,31 @@ ++[egg_info] ++tag_build = dev ++tag_svn_revision = true ++ ++[easy_install] ++find_links = http://www.pylonshq.com/download/ ++ ++[nosetests] ++with-pylons = development.ini ++ ++# Babel configuration ++[compile_catalog] ++domain = pylons_app ++directory = pylons_app/i18n ++statistics = true ++ ++[extract_messages] ++add_comments = TRANSLATORS: ++output_file = pylons_app/i18n/pylons_app.pot ++width = 80 ++ ++[init_catalog] ++domain = pylons_app ++input_file = pylons_app/i18n/pylons_app.pot ++output_dir = pylons_app/i18n ++ ++[update_catalog] ++domain = pylons_app ++input_file = pylons_app/i18n/pylons_app.pot ++output_dir = pylons_app/i18n ++previous = true +diff --git a/setup.py b/setup.py +new file mode 100644 +--- /dev/null ++++ b/setup.py +@@ -0,0 +1,38 @@ ++try: ++ from setuptools import setup, find_packages ++except ImportError: ++ from ez_setup import use_setuptools ++ use_setuptools() ++ from setuptools import setup, find_packages ++ ++setup( ++ name = 'pylons_app', ++ version = '1.0', ++ description = '', ++ author = 'marcin kuzminski', ++ author_email = 'marcin@python-blog.com', ++ url = '', ++ install_requires = [ ++ "Pylons>=0.9.7,<=0.9.7.99", ++ "SQLAlchemy>=0.5,<=0.5.99", ++ "Mako>=0.2.2,<=0.2.99", ++ ], ++ setup_requires = ["PasteScript>=1.6.3"], ++ packages = find_packages(exclude = ['ez_setup']), ++ include_package_data = True, ++ test_suite = 'nose.collector', ++ package_data = {'pylons_app': ['i18n/*/LC_MESSAGES/*.mo']}, ++ message_extractors = {'pylons_app': [ ++ ('**.py', 'python', None), ++ ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}), ++ ('public/**', 'ignore', None)]}, ++ zip_safe = False, ++ paster_plugins = ['PasteScript', 'Pylons'], ++ entry_points = """ ++ [paste.app_factory] ++ main = pylons_app.config.middleware:make_app ++ ++ [paste.app_install] ++ main = pylons.util:PylonsInstaller ++ """, ++) diff --git a/rhodecode/tests/models/test_diff_parsers.py b/rhodecode/tests/models/test_diff_parsers.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/models/test_diff_parsers.py @@ -0,0 +1,78 @@ +import os +import unittest +from rhodecode.tests import * +from rhodecode.lib.diffs import DiffProcessor, NEW_FILENODE, DEL_FILENODE, \ + MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE + +dn = os.path.dirname +FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures') + +DIFF_FIXTURES = { + 'hg_diff_add_single_binary_file.diff': [ + (u'US Warszawa.jpg', 'A', ['b', NEW_FILENODE]), + ], + 'hg_diff_mod_single_binary_file.diff': [ + (u'US Warszawa.jpg', 'M', ['b', MOD_FILENODE]), + ], + 'hg_diff_del_single_binary_file.diff': [ + (u'US Warszawa.jpg', 'D', ['b', DEL_FILENODE]), + ], + 'hg_diff_binary_and_normal.diff': [ + (u'img/baseline-10px.png', 'A', ['b', NEW_FILENODE]), + (u'js/jquery/hashgrid.js', 'A', [340, 0]), + (u'index.html', 'M', [3, 2]), + (u'less/docs.less', 'M', [34, 0]), + (u'less/scaffolding.less', 'M', [1, 3]), + (u'readme.markdown', 'M', [1, 10]), + (u'img/baseline-20px.png', 'D', ['b', DEL_FILENODE]), + (u'js/global.js', 'D', [0, 75]) + ], + 'hg_diff_chmod.diff': [ + (u'file', 'M', ['b', CHMOD_FILENODE]), + ], + 'hg_diff_rename_file.diff': [ + (u'file_renamed', 'M', ['b', RENAMED_FILENODE]), + ], + 'git_diff_chmod.diff': [ + (u'work-horus.xls', 'M', ['b', CHMOD_FILENODE]), + ], + 'git_diff_rename_file.diff': [ + (u'file.xls', 'M', ['b', RENAMED_FILENODE]), + ], + 'git_diff_mod_single_binary_file.diff': [ + ('US Warszawa.jpg', 'M', ['b', MOD_FILENODE]) + + ], + 'git_diff_binary_and_normal.diff': [ + (u'img/baseline-10px.png', 'A', ['b', NEW_FILENODE]), + (u'js/jquery/hashgrid.js', 'A', [340, 0]), + (u'index.html', 'M', [3, 2]), + (u'less/docs.less', 'M', [34, 0]), + (u'less/scaffolding.less', 'M', [1, 3]), + (u'readme.markdown', 'M', [1, 10]), + (u'img/baseline-20px.png', 'D', ['b', DEL_FILENODE]), + (u'js/global.js', 'D', [0, 75]) + ], +# 'large_diff.diff': [ +# +# ], + + +} + + +def _diff_checker(fixture): + with open(os.path.join(FIXTURES, fixture)) as f: + diff = f.read() + + diff_proc = DiffProcessor(diff) + diff_proc_d = diff_proc.prepare() + data = [(x['filename'], x['operation'], x['stats']) for x in diff_proc_d] + expected_data = DIFF_FIXTURES[fixture] + + assert expected_data == data + + +def test_parse_diff(): + for fixture in DIFF_FIXTURES: + yield _diff_checker, fixture diff --git a/rhodecode/tests/scripts/test_crawler.py b/rhodecode/tests/scripts/test_crawler.py --- a/rhodecode/tests/scripts/test_crawler.py +++ b/rhodecode/tests/scripts/test_crawler.py @@ -58,9 +58,9 @@ if not BASE_URI.endswith('/'): print 'Crawling @ %s' % BASE_URI BASE_URI += '%s' -PROJECT_PATH = jn('/', 'home', 'marcink', 'hg_repos') +PROJECT_PATH = jn('/', 'home', 'marcink', 'repos') PROJECTS = [ - 'linux-magx-pbranch', + #'linux-magx-pbranch', 'CPython', 'rhodecode_tip', ] @@ -95,9 +95,12 @@ def test_changelog_walk(proj, pages=100) page = '/'.join((proj, 'changelog',)) - full_uri = (BASE_URI % page) + '?' + urllib.urlencode({'page':i}) + full_uri = (BASE_URI % page) + '?' + urllib.urlencode({'page': i}) s = time.time() f = o.open(full_uri) + + assert f.url == full_uri, 'URL:%s does not match %s' % (f.url, full_uri) + size = len(f.read()) e = time.time() - s total_time += e diff --git a/rhodecode/tests/vcs/test_git.py b/rhodecode/tests/vcs/test_git.py --- a/rhodecode/tests/vcs/test_git.py +++ b/rhodecode/tests/vcs/test_git.py @@ -640,19 +640,22 @@ class GitSpecificWithRepoTest(BackendTes def test_get_diff_runs_git_command_with_hashes(self): self.repo.run_git_command = mock.Mock(return_value=['', '']) self.repo.get_diff(0, 1) - self.repo.run_git_command.assert_called_once_with('diff -U%s %s %s' % + self.repo.run_git_command.assert_called_once_with( + 'diff -U%s --full-index --binary -p -M --abbrev=40 %s %s' % (3, self.repo._get_revision(0), self.repo._get_revision(1))) def test_get_diff_runs_git_command_with_str_hashes(self): self.repo.run_git_command = mock.Mock(return_value=['', '']) self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1) - self.repo.run_git_command.assert_called_once_with('show -U%s %s' % + self.repo.run_git_command.assert_called_once_with( + 'show -U%s --full-index --binary -p -M --abbrev=40 %s' % (3, self.repo._get_revision(1))) def test_get_diff_runs_git_command_with_path_if_its_given(self): self.repo.run_git_command = mock.Mock(return_value=['', '']) self.repo.get_diff(0, 1, 'foo') - self.repo.run_git_command.assert_called_once_with('diff -U%s %s %s -- "foo"' + self.repo.run_git_command.assert_called_once_with( + 'diff -U%s --full-index --binary -p -M --abbrev=40 %s %s -- "foo"' % (3, self.repo._get_revision(0), self.repo._get_revision(1))) diff --git a/rhodecode/tests/vcs/test_repository.py b/rhodecode/tests/vcs/test_repository.py --- a/rhodecode/tests/vcs/test_repository.py +++ b/rhodecode/tests/vcs/test_repository.py @@ -74,6 +74,7 @@ class RepositoryGetDiffTest(BackendTestM with self.assertRaises(ChangesetDoesNotExistError): self.repo.get_diff('a' * 40, 'b' * 40) + class GitRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase): backend_alias = 'git' @@ -81,7 +82,7 @@ class GitRepositoryGetDiffTest(Repositor initial_rev = self.repo.revisions[0] self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar new file mode 100644 -index 0000000..f6ea049 +index 0000000000000000000000000000000000000000..f6ea0495187600e7b2288c8ac19c5886383a4632 --- /dev/null +++ b/foobar @@ -0,0 +1 @@ @@ -89,7 +90,7 @@ index 0000000..f6ea049 \ No newline at end of file diff --git a/foobar2 b/foobar2 new file mode 100644 -index 0000000..e8c9d6b +index 0000000000000000000000000000000000000000..e8c9d6b98e3dce993a464935e1a53f50b56a3783 --- /dev/null +++ b/foobar2 @@ -0,0 +1 @@ @@ -100,7 +101,7 @@ index 0000000..e8c9d6b def test_second_changeset_diff(self): revs = self.repo.revisions self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar -index f6ea049..389865b 100644 +index f6ea0495187600e7b2288c8ac19c5886383a4632..389865bb681b358c9b102d79abd8d5f941e96551 100644 --- a/foobar +++ b/foobar @@ -1 +1 @@ @@ -110,7 +111,7 @@ index f6ea049..389865b 100644 \ No newline at end of file diff --git a/foobar3 b/foobar3 new file mode 100644 -index 0000000..c11c37d +index 0000000000000000000000000000000000000000..c11c37d41d33fb47741cff93fa5f9d798c1535b0 --- /dev/null +++ b/foobar3 @@ -0,0 +1 @@ @@ -122,14 +123,14 @@ index 0000000..c11c37d revs = self.repo.revisions self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar deleted file mode 100644 -index 389865b..0000000 +index 389865bb681b358c9b102d79abd8d5f941e96551..0000000000000000000000000000000000000000 --- a/foobar +++ /dev/null @@ -1 +0,0 @@ -FOOBAR \ No newline at end of file diff --git a/foobar3 b/foobar3 -index c11c37d..f932447 100644 +index c11c37d41d33fb47741cff93fa5f9d798c1535b0..f9324477362684ff692aaf5b9a81e01b9e9a671c 100644 --- a/foobar3 +++ b/foobar3 @@ -1 +1,3 @@