| OLD | NEW |
| 1 # Copyright 2008 Google Inc. | 1 # Copyright 2008 Google Inc. |
| 2 # | 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. | 4 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at | 5 # You may obtain a copy of the License at |
| 6 # | 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # | 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and | 12 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. | 13 # limitations under the License. |
| 14 | 14 |
| 15 """Diff rendering in HTML for Rietveld.""" | 15 """Diff rendering in HTML for Rietveld.""" |
| 16 | 16 |
| 17 # Python imports | 17 # Python imports |
| 18 import re | 18 import re |
| 19 import cgi | 19 import cgi |
| 20 import difflib | 20 import difflib |
| 21 import logging | 21 import logging |
| 22 import urlparse | 22 import urlparse |
| 23 | 23 |
| 24 # AppEngine imports | 24 # AppEngine imports |
| 25 from google.appengine.api import urlfetch | 25 from google.appengine.api import urlfetch |
| 26 from google.appengine.api import users | 26 from google.appengine.api import users |
| 27 from google.appengine.ext import db | 27 from google.appengine.ext import db |
| 28 | 28 |
| 29 # Django imports | 29 # Django imports |
| 30 from django.template import loader | 30 from django.template import loader |
| 31 | 31 |
| 32 # Local imports | 32 # Local imports |
| 33 import models | 33 import models |
| 34 import patching | 34 import patching |
| 35 import intra_region_diff | 35 import intra_region_diff |
| 36 | 36 |
| 37 | 37 |
| 38 class FetchError(Exception): | 38 class FetchError(Exception): |
| 39 """Exception raised by FetchBase() when a URL problem occurs.""" | 39 """Exception raised by FetchBase() when a URL problem occurs.""" |
| 40 | 40 |
| 41 | 41 |
| 42 def ParsePatchSet(patchset): | 42 def ParsePatchSet(patchset): |
| 43 """Patch a patch set into individual patches. | 43 """Patch a patch set into individual patches. |
| 44 | 44 |
| 45 Args: | 45 Args: |
| 46 patchset: a models.PatchSet instance. | 46 patchset: a models.PatchSet instance. |
| 47 | 47 |
| 48 Returns: | 48 Returns: |
| 49 A list of models.Patch instances. | 49 A list of models.Patch instances. |
| 50 """ | 50 """ |
| 51 patches = [] | 51 patches = [] |
| 52 filename = lines = None | 52 filename = lines = None |
| 53 for line in patchset.data.splitlines(True): | 53 for line in patchset.data.splitlines(True): |
| 54 if line.startswith('Index:'): | 54 if line.startswith('Index:'): |
| 55 if filename and lines: | 55 if filename and lines: |
| 56 patch = models.Patch(patchset=patchset, text=_ToText(lines), | 56 patch = models.Patch(patchset=patchset, text=_ToText(lines), |
| 57 filename=filename, parent=patchset) | 57 filename=filename, parent=patchset) |
| 58 patches.append(patch) | 58 patches.append(patch) |
| 59 unused, filename = line.split(':', 1) | 59 unused, filename = line.split(':', 1) |
| 60 filename = filename.strip() | 60 filename = filename.strip() |
| 61 lines = [line] | 61 lines = [line] |
| 62 continue | 62 continue |
| 63 if lines is not None: | 63 if lines is not None: |
| 64 lines.append(line) | 64 lines.append(line) |
| 65 if filename and lines: | 65 if filename and lines: |
| 66 patch = models.Patch(patchset=patchset, text=_ToText(lines), | 66 patch = models.Patch(patchset=patchset, text=_ToText(lines), |
| 67 filename=filename, parent=patchset) | 67 filename=filename, parent=patchset) |
| 68 patches.append(patch) | 68 patches.append(patch) |
| 69 if not patches: |
| 70 # let's try to convert Mercurial |
| 71 logging.info(patchset.data) |
| 72 lines = patchset.data.splitlines(True) |
| 73 filename = "README" |
| 74 if filename and lines: |
| 75 patch = models.Patch(patchset=patchset, text=_ToText(lines), |
| 76 filename=filename, parent=patchset) |
| 77 patches.append(patch) |
| 69 return patches | 78 return patches |
| 70 | 79 |
| 71 | 80 |
| 72 def FetchBase(base, patch): | 81 def FetchBase(base, patch): |
| 73 """Fetch the content of the file to which the file is relative. | 82 """Fetch the content of the file to which the file is relative. |
| 74 | 83 |
| 75 Args: | 84 Args: |
| 76 base: the base property of the Issue to which the Patch belongs. | 85 base: the base property of the Issue to which the Patch belongs. |
| 77 patch: a models.Patch instance. | 86 patch: a models.Patch instance. |
| 78 | 87 |
| 79 Returns: | 88 Returns: |
| 80 A models.Content instance. | 89 A models.Content instance. |
| 81 | 90 |
| 82 Raises: | 91 Raises: |
| 83 FetchError: For any kind of problem fetching the content. | 92 FetchError: For any kind of problem fetching the content. |
| 84 """ | 93 """ |
| 85 filename, lines = patch.filename, patch.lines | 94 filename, lines = patch.filename, patch.lines |
| 86 rev = patching.ParseRevision(lines) | 95 rev = patching.ParseRevision(lines) |
| 87 if rev is not None: | 96 if rev is not None: |
| 88 if rev == 0: | 97 if rev == 0: |
| 89 # rev=0 means it's a new file. | 98 # rev=0 means it's a new file. |
| 90 return models.Content(text=db.Text(u''), parent=patch) | 99 return models.Content(text=db.Text(u''), parent=patch) |
| 91 url = _MakeUrl(base, filename, rev) | 100 url = _MakeUrl(base, filename, rev) |
| 92 logging.info('Fetching %s', url) | 101 logging.info('Fetching %s', url) |
| 93 try: | 102 try: |
| 94 result = urlfetch.fetch(url) | 103 result = urlfetch.fetch(url) |
| 95 except Exception, err: | 104 except Exception, err: |
| 96 msg = 'Error fetching %s: %s: %s' % (url, err.__class__.__name__, err) | 105 msg = 'Error fetching %s: %s: %s' % (url, err.__class__.__name__, err) |
| 97 logging.error(msg) | 106 logging.error(msg) |
| 98 raise FetchError(msg) | 107 raise FetchError(msg) |
| 99 if result.status_code != 200: | 108 if result.status_code != 200: |
| 100 msg = 'Error fetching %s: HTTP status %s' % (url, result.status_code) | 109 msg = 'Error fetching %s: HTTP status %s' % (url, result.status_code) |
| 101 logging.error(msg) | 110 logging.error(msg) |
| 102 raise FetchError(msg) | 111 raise FetchError(msg) |
| 103 lines = result.content.splitlines(True) | 112 lines = result.content.splitlines(True) |
| 104 # TODO(guido): Handle non-ASCII text better. | 113 # TODO(guido): Handle non-ASCII text better. |
| 105 for i, line in enumerate(lines): | 114 for i, line in enumerate(lines): |
| 106 try: | 115 try: |
| 107 line.decode('ascii') | 116 line.decode('ascii') |
| 108 except UnicodeError, err: | 117 except UnicodeError, err: |
| 109 logging.warn('Line %d: %r is not ASCII', i+1, line) | 118 logging.warn('Line %d: %r is not ASCII', i+1, line) |
| 110 uni = line.decode('ascii', 'replace') | 119 uni = line.decode('ascii', 'replace') |
| 111 lines[i] = uni.encode('ascii', 'replace') | 120 lines[i] = uni.encode('ascii', 'replace') |
| 112 return models.Content(text=_ToText(lines), parent=patch) | 121 return models.Content(text=_ToText(lines), parent=patch) |
| 113 | 122 |
| 114 | 123 |
| 115 def _MakeUrl(base, filename, rev): | 124 def _MakeUrl(base, filename, rev): |
| 116 """Helper for FetchBase() to construct the URL to fetch. | 125 """Helper for FetchBase() to construct the URL to fetch. |
| 117 | 126 |
| 118 Args: | 127 Args: |
| 119 base: The base property of the Issue to which the Patch belongs. | 128 base: The base property of the Issue to which the Patch belongs. |
| 120 filename: The filename property of the Patch instance. | 129 filename: The filename property of the Patch instance. |
| 121 rev: Revision number, or None for head revision. | 130 rev: Revision number, or None for head revision. |
| 122 | 131 |
| 123 Returns: | 132 Returns: |
| 124 A URL referring to the given revision of the file. | 133 A URL referring to the given revision of the file. |
| 125 """ | 134 """ |
| 135 return "http://hg.sympy.org/sympy/raw-file/tip/" + filename |
| 126 scheme, netloc, path, params, query, fragment = urlparse.urlparse(base) | 136 scheme, netloc, path, params, query, fragment = urlparse.urlparse(base) |
| 127 if netloc.endswith(".googlecode.com"): | 137 if netloc.endswith(".googlecode.com"): |
| 128 # Handle Google code repositories | 138 # Handle Google code repositories |
| 129 assert rev is not None, "Can't access googlecode.com without a revision" | 139 assert rev is not None, "Can't access googlecode.com without a revision" |
| 130 assert path.startswith("/svn/"), "Malformed googlecode.com URL" | 140 assert path.startswith("/svn/"), "Malformed googlecode.com URL" |
| 131 path = path[5:] # Strip "/svn/" | 141 path = path[5:] # Strip "/svn/" |
| 132 url = "%s://%s/svn-history/r%d/%s/%s" % (scheme, netloc, rev, | 142 url = "%s://%s/svn-history/r%d/%s/%s" % (scheme, netloc, rev, |
| 133 path, filename) | 143 path, filename) |
| 134 return url | 144 return url |
| 135 # Default for viewvc-based URLs (svn.python.org) | 145 # Default for viewvc-based URLs (svn.python.org) |
| 136 url = base | 146 url = base |
| 137 if not url.endswith('/'): | 147 if not url.endswith('/'): |
| 138 url += '/' | 148 url += '/' |
| 139 url += filename | 149 url += filename |
| 140 if rev is not None: | 150 if rev is not None: |
| 141 url += '?rev=%s' % rev | 151 url += '?rev=%s' % rev |
| 142 return url | 152 return url |
| 143 | 153 |
| 144 | 154 |
| 145 def RenderDiffTableRows(request, old_lines, chunks, patch, | 155 def RenderDiffTableRows(request, old_lines, chunks, patch, |
| 146 colwidth=80, debug=False): | 156 colwidth=80, debug=False): |
| 147 """Render the HTML table rows for a side-by-side diff for a patch. | 157 """Render the HTML table rows for a side-by-side diff for a patch. |
| 148 | 158 |
| 149 Args: | 159 Args: |
| 150 request: Django Request object. | 160 request: Django Request object. |
| 151 old_lines: List of lines representing the original file. | 161 old_lines: List of lines representing the original file. |
| 152 chunks: List of chunks as returned by patching.ParsePatch(). | 162 chunks: List of chunks as returned by patching.ParsePatch(). |
| 153 patch: A models.Patch instance. | 163 patch: A models.Patch instance. |
| 154 colwidth: Optional column width (default 80). | 164 colwidth: Optional column width (default 80). |
| 155 debug: Optional debugging flag (default False). | 165 debug: Optional debugging flag (default False). |
| 156 | 166 |
| 157 Yields: | 167 Yields: |
| 158 Strings, each of which represents the text rendering one complete | 168 Strings, each of which represents the text rendering one complete |
| 159 pair of lines of the side-by-side diff, possibly including comments. | 169 pair of lines of the side-by-side diff, possibly including comments. |
| 160 Each yielded string may consist of several <tr> elements. | 170 Each yielded string may consist of several <tr> elements. |
| 161 """ | 171 """ |
| 162 buffer = [] | 172 buffer = [] |
| 163 for tag, text in _RenderDiffTableRows(request, old_lines, chunks, patch, | 173 for tag, text in _RenderDiffTableRows(request, old_lines, chunks, patch, |
| 164 colwidth, debug): | 174 colwidth, debug): |
| 165 if tag == 'equal': | 175 if tag == 'equal': |
| 166 buffer.append(text) | 176 buffer.append(text) |
| 167 continue | 177 continue |
| 168 else: | 178 else: |
| 169 for t in _ShortenBuffer(buffer): | 179 for t in _ShortenBuffer(buffer): |
| 170 yield t | 180 yield t |
| 171 buffer = [] | 181 buffer = [] |
| 172 yield text | 182 yield text |
| 173 if tag == 'error': | 183 if tag == 'error': |
| 174 yield None | 184 yield None |
| 175 break | 185 break |
| (...skipping 402 matching lines...) Show 10 above Show 10 below |
| 578 Returns: | 588 Returns: |
| 579 A tuple (old_len, new_len) representing len(old_lines) and | 589 A tuple (old_len, new_len) representing len(old_lines) and |
| 580 len(new_lines), where new_lines is the list representing the | 590 len(new_lines), where new_lines is the list representing the |
| 581 result of applying the patch chunks to old_lines, however, without | 591 result of applying the patch chunks to old_lines, however, without |
| 582 actually computing new_lines. | 592 actually computing new_lines. |
| 583 """ | 593 """ |
| 584 old_len = len(old_lines) | 594 old_len = len(old_lines) |
| 585 new_len = old_len | 595 new_len = old_len |
| 586 if chunks: | 596 if chunks: |
| 587 (old_a, old_b), (new_a, new_b), old_lines, new_lines = chunks[-1] | 597 (old_a, old_b), (new_a, new_b), old_lines, new_lines = chunks[-1] |
| 588 new_len += new_b - old_b | 598 new_len += new_b - old_b |
| 589 return old_len, new_len | 599 return old_len, new_len |
| 590 | 600 |
| 591 | 601 |
| 592 def _MarkupNumber(ndigits, number, tag): | 602 def _MarkupNumber(ndigits, number, tag): |
| 593 """Format a number in HTML in a given width with extra markup. | 603 """Format a number in HTML in a given width with extra markup. |
| 594 | 604 |
| 595 Args: | 605 Args: |
| 596 ndigits: the total width available for formatting | 606 ndigits: the total width available for formatting |
| 597 number: the number to be formatted | 607 number: the number to be formatted |
| 598 tag: HTML tag name, e.g. 'u' | 608 tag: HTML tag name, e.g. 'u' |
| 599 | 609 |
| 600 Returns: | 610 Returns: |
| 601 An HTML string that displays as ndigits wide, with the | 611 An HTML string that displays as ndigits wide, with the |
| 602 number right-aligned and surrounded by an HTML tag; for example, | 612 number right-aligned and surrounded by an HTML tag; for example, |
| 603 _MarkupNumber(42, 4, 'u') returns ' <u>42</u>'. | 613 _MarkupNumber(42, 4, 'u') returns ' <u>42</u>'. |
| 604 """ | 614 """ |
| 605 formatted_number = str(number) | 615 formatted_number = str(number) |
| 606 space_prefix = ' ' * (ndigits - len(formatted_number)) | 616 space_prefix = ' ' * (ndigits - len(formatted_number)) |
| 607 return '%s<%s>%s</%s>' % (space_prefix, tag, formatted_number, tag) | 617 return '%s<%s>%s</%s>' % (space_prefix, tag, formatted_number, tag) |
| 608 | 618 |
| 609 | 619 |
| 610 def _ExpandTemplate(name, **params): | 620 def _ExpandTemplate(name, **params): |
| 611 """Wrapper around django.template.loader.render_to_string(). | 621 """Wrapper around django.template.loader.render_to_string(). |
| 612 | 622 |
| 613 For convenience, this takes keyword arguments instead of a dict. | 623 For convenience, this takes keyword arguments instead of a dict. |
| 614 """ | 624 """ |
| 615 return loader.render_to_string(name, params) | 625 return loader.render_to_string(name, params) |
| 616 | 626 |
| 617 | 627 |
| 618 def _ToText(lines): | 628 def _ToText(lines): |
| 619 """Helper to turn a list of lines into a db.Text instance. | 629 """Helper to turn a list of lines into a db.Text instance. |
| 620 | 630 |
| 621 Args: | 631 Args: |
| 622 lines: list of strings. | 632 lines: list of strings. |
| 623 | 633 |
| 624 Returns: | 634 Returns: |
| 625 A db.Text instance. | 635 A db.Text instance. |
| 626 """ | 636 """ |
| 627 return db.Text(''.join(lines), encoding='utf-8') | 637 return db.Text(''.join(lines), encoding='utf-8') |
| OLD | NEW |