├── .codeclimate.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.cfg ├── setup.py ├── src ├── diff_highlight.py └── highlights │ ├── __init__.py │ ├── command.py │ └── pprint.py ├── tests ├── __init__.py ├── highlights │ ├── __init__.py │ ├── test_command.py │ └── test_pprint.py └── test_diff_highlight.py └── tox.ini /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Python: true 3 | exclude_paths: 4 | - "tests/*.py" 5 | - "tests/highlights/*.py" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | *.swp 4 | 5 | .coverage 6 | .tox/ 7 | _build/ 8 | bin/ 9 | dist/ 10 | include/ 11 | lib/ 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.5 3 | env: 4 | matrix: 5 | - TOXENV=py26 6 | - TOXENV=py27 7 | - TOXENV=py33 8 | - TOXENV=py34 9 | - TOXENV=py35 10 | - TOXENV=hg35 11 | - TOXENV=hg36 12 | - TOXENV=hg41 13 | - TOXENV=coverage 14 | install: pip install docutils tox 15 | script: tox 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | `diff-highlight`: pretty diff highlighter; emphasis changed words in diff 2 | 3 | diff-highlight adds word highlighting feature to git, mercurial and other diff viewers. 4 | 5 | .. image:: https://travis-ci.org/tk0miya/diff-highlight.svg?branch=master 6 | :target: https://travis-ci.org/tk0miya/diff-highlight 7 | 8 | .. image:: https://coveralls.io/repos/tk0miya/diff-highlight/badge.png?branch=master 9 | :target: https://coveralls.io/r/tk0miya/diff-highlight?branch=master 10 | 11 | .. image:: https://codeclimate.com/github/tk0miya/diff-highlight/badges/gpa.svg 12 | :target: https://codeclimate.com/github/tk0miya/diff-highlight 13 | 14 | Features 15 | ======== 16 | * Add highlights to diff output 17 | * mercurial extension for diff highlighting 18 | 19 | Setup 20 | ===== 21 | 22 | Use easy_install or pip:: 23 | 24 | $ sudo easy_install diff-highlight 25 | 26 | Or 27 | 28 | $ sudo pip install diff-highlight 29 | 30 | Applying to git 31 | --------------- 32 | 33 | Add pager settings to your $HOME/.gitconfig to enable word highlights:: 34 | 35 | [pager] 36 | log = diff-highlight | less 37 | show = diff-highlight | less 38 | diff = diff-highlight | less 39 | 40 | 41 | and to use diff-highlight for `git add -p`:: 42 | 43 | [interactive] 44 | diffFilter = diff-highlight 45 | 46 | Applying to mercurial 47 | --------------------- 48 | 49 | Add `color` and `diff_highlight` extensions to your $HOME/.hgrc to enable word highlights:: 50 | 51 | [extensions] 52 | color = 53 | diff_highlight = 54 | 55 | 56 | Requirements 57 | ============ 58 | * Python 2.6 or 2.7, or Python 3.2, 3.3, 3.4 (or higher 59 | (mercurial extension works on python 2.x only) 60 | 61 | License 62 | ======= 63 | Apache License 2.0 64 | (`highlights/pprint.py` is under PSFL). 65 | 66 | 67 | History 68 | ======= 69 | 70 | 1.2.0 (2016-02-07) 71 | ------------------- 72 | * Grouping indented hunks 73 | * Fix #1: highlight if large text appended 74 | * Fix mercurial extension has been broken since mercurial-3.7.0 75 | 76 | 1.1.0 (2015-07-12) 77 | ------------------- 78 | * Drop py24 and py25 support 79 | * Support git styled diff 80 | 81 | 1.0.3 (2015-03-30) 82 | ------------------- 83 | * Ignore IOError on showing result 84 | 85 | 1.0.2 (2014-06-08) 86 | ------------------- 87 | * Fix result of diff-highlight commannd is broken when diff-text includes new file 88 | (thanks @troter) 89 | 90 | 1.0.1 (2013-12-22) 91 | ------------------- 92 | * Fix diff-highlight command failed with python 2.4 93 | 94 | 1.0.0 (2013-12-22) 95 | ------------------- 96 | * Add diff-highlight command 97 | * Support python 2.4, 2.5, 3.2 and 3.3 98 | 99 | 0.1.0 (2013-12-20) 100 | ------------------- 101 | * first release 102 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build] 2 | build-base = _build 3 | 4 | [sdist] 5 | formats = gztar 6 | 7 | [wheel] 8 | universal = 1 9 | 10 | [aliases] 11 | release = check -r -s register sdist bdist_wheel upload 12 | 13 | [check] 14 | strict = 1 15 | restructuredtext = 1 16 | 17 | [flake8] 18 | ignore=_ 19 | max_line_length=128 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from setuptools import setup, find_packages 4 | 5 | classifiers = [ 6 | "Development Status :: 5 - Production/Stable", 7 | "Intended Audience :: System Administrators", 8 | "License :: OSI Approved :: Apache Software License", 9 | "Programming Language :: Python", 10 | "Programming Language :: Python :: 2.6", 11 | "Programming Language :: Python :: 2.7", 12 | "Programming Language :: Python :: 3.2", 13 | "Programming Language :: Python :: 3.3", 14 | "Programming Language :: Python :: 3.4", 15 | "Programming Language :: Python :: 3.5", 16 | "Topic :: Software Development", 17 | "Topic :: Software Development :: Version Control", 18 | "Topic :: Text Processing :: Filters", 19 | ] 20 | 21 | test_requires = ['nose', 'flake8', 'mock', 'six'] 22 | 23 | if sys.version_info < (2, 7): 24 | test_requires.append('unittest2') 25 | 26 | if sys.version_info < (3, 0): 27 | test_requires.append('mercurial') 28 | 29 | setup( 30 | name='diff-highlight', 31 | version='1.2.0', 32 | description='pretty diff highlighter; emphasis changed words in diff', 33 | long_description=open("README.rst").read(), 34 | classifiers=classifiers, 35 | keywords=['mercurial', 'git', 'diff', 'highlight'], 36 | author='Takeshi Komiya', 37 | author_email='i.tkomiya at gmail.com', 38 | url='https://github.com/tk0miya/diff-highlight', 39 | license='Apache License 2.0', 40 | py_modules=['diff_highlight'], 41 | packages=find_packages('src'), 42 | package_dir={'': 'src'}, 43 | include_package_data=True, 44 | extras_require={ 45 | 'testing': test_requires, 46 | }, 47 | tests_require=test_requires, 48 | entry_points=""" 49 | [console_scripts] 50 | diff-highlight = highlights.command:highlight_main 51 | """ 52 | ) 53 | -------------------------------------------------------------------------------- /src/diff_highlight.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2013 Takeshi KOMIYA 3 | # Copyright (c) 2001-2013 Python Software Foundation; All Rights Reserved 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from hgext import color 18 | from mercurial import extensions 19 | from mercurial.i18n import _ 20 | from highlights.pprint import INSERTED, DELETED, pprint_hunk 21 | 22 | INSERT_NORM = 'diff.inserted' 23 | INSERT_EMPH = 'diff.inserted_highlight' 24 | DELETE_NORM = 'diff.deleted' 25 | DELETE_EMPH = 'diff.deleted_highlight' 26 | 27 | 28 | class colorui(color.colorui): 29 | hunk = None 30 | tab = None 31 | 32 | def __init__(self, src=None): 33 | super(colorui, self).__init__(src) 34 | self.hunk = [] 35 | 36 | def write(self, *args, **opts): 37 | label = opts.get('label') 38 | if label in (INSERT_NORM, DELETE_NORM): 39 | if self.tab is not None: 40 | change = self.hunk.pop() 41 | self.hunk.append((change[0] + self.tab + "".join(args), change[1])) 42 | self.tab = None 43 | else: 44 | self.hunk.append(("".join(args), opts)) 45 | elif label == 'diff.trailingwhitespace': # merge to hunk 46 | change = self.hunk.pop() 47 | self.hunk.append((change[0] + "".join(args), change[1])) 48 | elif label == 'diff.tab': 49 | self.tab = "".join(args) 50 | elif label == '' and args == ("\n",) and self.hunk: 51 | self.hunk.append((args[0], opts)) 52 | else: 53 | self.flush_hunk() 54 | super(colorui, self).write(*args, **opts) 55 | 56 | def flush(self): 57 | self.flush_hunk() 58 | super(colorui, self).flush() 59 | 60 | def flush_hunk(self): 61 | if self.hunk is None: # not initialized yet 62 | return 63 | 64 | hunk = [(ret[0].decode('utf-8'), ret[1]) for ret in self.hunk] 65 | new = [c[0] for c in hunk if c[1]['label'] == INSERT_NORM] 66 | old = [c[0] for c in hunk if c[1]['label'] == DELETE_NORM] 67 | 68 | write = super(colorui, self).write 69 | for string, style, highlighted in pprint_hunk(new, 0, len(new), 70 | old, 0, len(old)): 71 | if style == INSERTED: 72 | if highlighted: 73 | write(string.encode('utf-8'), label=INSERT_EMPH) 74 | else: 75 | write(string.encode('utf-8'), label=INSERT_NORM) 76 | elif style == DELETED: 77 | if highlighted: 78 | write(string.encode('utf-8'), label=DELETE_EMPH) 79 | else: 80 | write(string.encode('utf-8'), label=DELETE_NORM) 81 | else: 82 | write(string.encode('utf-8'), label='') 83 | 84 | self.hunk = [] 85 | 86 | 87 | def uisetup(ui): 88 | if ui.plain(): 89 | return 90 | 91 | try: 92 | extensions.find('color') 93 | except KeyError: 94 | ui.warn(_("warning: 'diff-highlight' requires 'color' extension " 95 | "to be enabled, but not\n")) 96 | return 97 | 98 | if not isinstance(ui, colorui): 99 | colorui.__bases__ = (ui.__class__,) 100 | ui.__class__ = colorui 101 | 102 | def colorconfig(orig, *args, **kwargs): 103 | ret = orig(*args, **kwargs) 104 | 105 | try: 106 | from mercurial.color import _styles as styles 107 | except ImportError: 108 | styles = color._styles 109 | if INSERT_EMPH not in styles: 110 | styles[INSERT_EMPH] = styles[INSERT_NORM] + ' inverse' 111 | 112 | if DELETE_EMPH not in styles: 113 | styles[DELETE_EMPH] = styles[DELETE_NORM] + ' inverse' 114 | 115 | return ret 116 | 117 | extensions.wrapfunction(color, 'configstyles', colorconfig) 118 | -------------------------------------------------------------------------------- /src/highlights/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk0miya/diff-highlight/4c98c6d0dde9884ae48747ededc26496852d4493/src/highlights/__init__.py -------------------------------------------------------------------------------- /src/highlights/command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2013 Takeshi KOMIYA 3 | # Copyright (c) 2001-2013 Python Software Foundation; All Rights Reserved 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import re 18 | import sys 19 | from highlights.pprint import INSERTED, DELETED, pprint_hunk 20 | 21 | colortable = {'none': 0, 'red': 31, 'green': 32} 22 | 23 | 24 | def highlight_main(): 25 | try: 26 | new, old = [], [] 27 | in_header = True 28 | for rawline in sys.stdin: 29 | if sys.version_info < (3, 0): 30 | rawline = rawline.decode('utf-8') 31 | 32 | # strip ESC chars and CR/LF 33 | stripped = re.sub('\x1b\[[0-9;]*m', '', rawline.rstrip("\r\n")) 34 | 35 | if in_header: 36 | if re.match('^(@|commit \w+$)', stripped): 37 | in_header = False 38 | else: 39 | if not re.match('^(?:[ +\-@\\\\]|diff)', stripped): 40 | in_header = True 41 | 42 | if not in_header and stripped.startswith('+'): 43 | new.append(stripped) 44 | elif not in_header and stripped.startswith('-'): 45 | old.append(stripped) 46 | else: 47 | show_hunk(new, old) 48 | new, old = [], [] 49 | write(rawline) 50 | 51 | show_hunk(new, old) # flush last hunk 52 | except IOError: 53 | pass 54 | 55 | 56 | def show_hunk(new, old): 57 | for string, style, highlighted, in pprint_hunk(new, 0, len(new), 58 | old, 0, len(old)): 59 | 60 | if style == INSERTED: 61 | if highlighted: 62 | write(string, 'green', True) 63 | else: 64 | write(string, 'green') 65 | elif style == DELETED: 66 | if highlighted: 67 | write(string, 'red', True) 68 | else: 69 | write(string, 'red') 70 | else: 71 | write(string) 72 | 73 | 74 | def write(string, color=None, highlight=False): 75 | if color: 76 | sys.stdout.write("\x1b[%dm" % colortable[color]) 77 | 78 | if highlight: 79 | sys.stdout.write("\x1b[7m") 80 | 81 | if sys.version_info < (3, 0): 82 | sys.stdout.write(string.encode('utf-8')) 83 | else: 84 | sys.stdout.write(string) 85 | 86 | if highlight or color: 87 | sys.stdout.write("\x1b[%dm" % colortable['none']) 88 | -------------------------------------------------------------------------------- /src/highlights/pprint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2013 Takeshi KOMIYA 4 | # Copyright (c) 2001-2013 Python Software Foundation; All Rights Reserved 5 | # 6 | # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 7 | # -------------------------------------------- 8 | # 9 | # 1. This LICENSE AGREEMENT is between the Python Software Foundation 10 | # ("PSF"), and the Individual or Organization ("Licensee") accessing and 11 | # otherwise using this software ("Python") in source or binary form and 12 | # its associated documentation. 13 | # 14 | # 2. Subject to the terms and conditions of this License Agreement, PSF hereby 15 | # grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, 16 | # analyze, test, perform and/or display publicly, prepare derivative works, 17 | # distribute, and otherwise use Python alone or in any derivative version, 18 | # provided, however, that PSF's License Agreement and PSF's notice of copyright, 19 | # i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 20 | # 2011, 2012, 2013 Python Software Foundation; All Rights Reserved" are retained 21 | # in Python alone or in any derivative version prepared by Licensee. 22 | # 23 | # 3. In the event Licensee prepares a derivative work that is based on 24 | # or incorporates Python or any part thereof, and wants to make 25 | # the derivative work available to others as provided herein, then 26 | # Licensee hereby agrees to include in any such work a brief summary of 27 | # the changes made to Python. 28 | # 29 | # 4. PSF is making Python available to Licensee on an "AS IS" 30 | # basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 31 | # IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND 32 | # DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 33 | # FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 34 | # INFRINGE ANY THIRD PARTY RIGHTS. 35 | # 36 | # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 37 | # FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 38 | # A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 39 | # OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 40 | # 41 | # 6. This License Agreement will automatically terminate upon a material 42 | # breach of its terms and conditions. 43 | # 44 | # 7. Nothing in this License Agreement shall be deemed to create any 45 | # relationship of agency, partnership, or joint venture between PSF and 46 | # Licensee. This License Agreement does not grant permission to use PSF 47 | # trademarks or trade name in a trademark sense to endorse or promote 48 | # products or services of Licensee, or any third party. 49 | # 50 | # 8. By copying, installing or otherwise using Python, Licensee 51 | # agrees to be bound by the terms and conditions of this License 52 | # Agreement. 53 | # 54 | # flake8: NOQA 55 | 56 | import re 57 | from difflib import SequenceMatcher 58 | 59 | NORMAL = 0 60 | INSERTED = 1 61 | DELETED = 2 62 | 63 | # magic numbers 64 | APPENDED_CHECK_THRESHOLD = 40 65 | 66 | 67 | def pprint_hunk(new, new_lo, new_hi, old, old_lo, old_hi): 68 | lines = [[]] 69 | for hunk in highlight_hunk(new, new_lo, new_hi, old, old_lo, old_hi): 70 | lines[-1].append(hunk) 71 | if hunk[0] == "\n": 72 | lines.append([]) 73 | if lines[-1] == []: 74 | lines.pop() 75 | 76 | return arrange_indented_hunks(lines) 77 | 78 | 79 | def highlight_hunk(new, new_lo, new_hi, old, old_lo, old_hi): 80 | # derived from difflib.py (Python stdlib) Differ#_fancy_replace() 81 | best_ratio, cutoff = 0.59, 0.60 82 | 83 | cruncher = SequenceMatcher(None) 84 | for j in range(old_lo, old_hi): 85 | cruncher.set_seq2(old[j]) 86 | for i in range(new_lo, new_hi): 87 | cruncher.set_seq1(new[i]) 88 | if (cruncher.real_quick_ratio() > best_ratio and 89 | cruncher.quick_ratio() > best_ratio and 90 | cruncher.ratio() > best_ratio): 91 | best_ratio, best_i, best_j = cruncher.ratio(), i, j 92 | elif len(old[j]) >= APPENDED_CHECK_THRESHOLD: 93 | # not close matches, but similar (changed a little and appended large texts) 94 | matched = sum(b[2] for b in cruncher.get_matching_blocks()) 95 | ratio = float(matched) / len(old[j]) 96 | if ratio > best_ratio: 97 | best_ratio, best_i, best_j = ratio, i, j 98 | 99 | # no non-identical "pretty close" pair 100 | if best_ratio < cutoff: 101 | for line in old[old_lo:old_hi]: 102 | yield line, DELETED, False 103 | yield "\n", NORMAL, False 104 | for line in new[new_lo:new_hi]: 105 | yield line, INSERTED, False 106 | yield "\n", NORMAL, False 107 | 108 | return 109 | 110 | for hunk in highlight_hunk_helper(new, new_lo, best_i, old, old_lo, best_j): 111 | yield hunk 112 | 113 | for hunk in highlight_pair(cruncher, new[best_i], old[best_j]): 114 | yield hunk 115 | 116 | for hunk in highlight_hunk_helper(new, best_i + 1, new_hi, 117 | old, best_j + 1, old_hi): 118 | yield hunk 119 | 120 | 121 | def highlight_hunk_helper(new, new_lo, new_hi, old, old_lo, old_hi): 122 | # derived from difflib.py (Python stdlib) Differ#_fancy_helper() 123 | if new_lo < new_hi: 124 | if old_lo < old_hi: 125 | for hunk in highlight_hunk(new, new_lo, new_hi, old, old_lo, old_hi): 126 | yield hunk 127 | else: 128 | for line in new[new_lo:new_hi]: 129 | yield line, INSERTED, False 130 | yield "\n", NORMAL, False 131 | elif old_lo < old_hi: 132 | for line in old[old_lo:old_hi]: 133 | yield line, DELETED, False 134 | yield "\n", NORMAL, False 135 | 136 | 137 | def highlight_pair(cruncher, newline, oldline): 138 | new = [[newline[0], INSERTED, False]] 139 | old = [[oldline[0], DELETED, False]] 140 | 141 | cruncher.set_seqs(newline[1:], oldline[1:]) 142 | for tag, new1, new2, old1, old2 in cruncher.get_opcodes(): 143 | new_piece = newline[new1 + 1:new2 + 1] 144 | old_piece = oldline[old1 + 1:old2 + 1] 145 | if tag == 'equal': 146 | new.append([new_piece, INSERTED, False]) 147 | old.append([old_piece, DELETED, False]) 148 | else: 149 | new.append([new_piece, INSERTED, True]) 150 | old.append([old_piece, DELETED, True]) 151 | 152 | # change highlighting: character base -> word base 153 | for i in range(len(new) - 1, 1, -1): 154 | if is_mergeable(new, old, i): 155 | new[i - 2][0] += new[i - 1][0] 156 | old[i - 2][0] += old[i - 1][0] 157 | del new[i - 1] 158 | del old[i - 1] 159 | 160 | # optimize ESC chars 161 | for i in range(len(new) - 1, 0, -1): 162 | if new[i][1:] == new[i - 1][1:]: 163 | new[i - 1][0] += new[i][0] 164 | del new[i] 165 | 166 | for i in range(len(old) - 1, 0, -1): 167 | if old[i][1:] == old[i - 1][1:]: 168 | old[i - 1][0] += old[i][0] 169 | del old[i] 170 | 171 | # write highlighted lines 172 | merged = old + [['\n', NORMAL, False]] + new + [['\n', NORMAL, False]] 173 | for string, style, highlighted in merged: 174 | yield string, style, highlighted 175 | 176 | 177 | def is_mergeable(new, old, i): 178 | chars = '[a-zA-Z0-9_.]' 179 | startswith_word = lambda s: re.match('^%s' % chars, s[0]) 180 | endswith_word = lambda s: re.search('%s$' % chars, s[0]) 181 | is_word = lambda s: re.match('^(%s+|\s+)$' % chars, s[0]) 182 | 183 | marks = '[ !"#$%&\'()*+,\-./:;<=>?@\[\\]^_{|}~]' 184 | startswith_mark = lambda s: re.match('^%s' % marks, s[0]) 185 | endswith_mark = lambda s: re.search('%s$' % marks, s[0]) 186 | is_mark = lambda s: re.match('^(%s+|\s+)$' % marks, s[0]) 187 | 188 | n1, n2, n3 = new[i - 2: i + 1] 189 | o1, o2, o3 = old[i - 2: i + 1] 190 | if ((n1[2], n2[2], n3[2]) != (True, False, True) and 191 | (o1[2], o2[2], o3[2]) != (True, False, True)): 192 | return False 193 | 194 | # WORD1 ends with word(alnum) and WORD2 is word 195 | if ((endswith_word(n1) and is_word(n2)) or 196 | (endswith_word(o1) and is_word(o2))): 197 | return True 198 | 199 | # WORD2 is word and WORD3 starts with word(alnum) 200 | if ((is_word(n2) and startswith_word(n3)) or 201 | (is_word(o2) and startswith_word(o3))): 202 | return True 203 | 204 | # WORD1 ends with any marks and WORD2 is any marks 205 | if ((endswith_mark(n1) and is_mark(n2)) or 206 | (endswith_mark(o1) and is_mark(o2))): 207 | return True 208 | 209 | # WORD2 is any marks and WORD3 starts with any marks 210 | if ((is_mark(n2) and startswith_mark(n3)) or 211 | (is_mark(o2) and startswith_mark(o3))): 212 | return True 213 | 214 | return False 215 | 216 | 217 | def arrange_indented_hunks(lines): 218 | result = [] 219 | 220 | def typeof(line): 221 | return line[0][0][0:1] # +, - or other 222 | 223 | def flushline(line): 224 | result.extend(line) 225 | 226 | def flushlines(lines): 227 | for line in lines[:]: 228 | flushline(line) 229 | lines.remove(line) 230 | 231 | def only_indented(deleted, inserted): 232 | if (inserted[0][0] == "+" and re.match('^\s*$', inserted[1][0]) and len(inserted) == 4 and 233 | deleted[0][0] == "-" and re.match('^\s*$', deleted[1][0]) and len(deleted) == 4): 234 | return True 235 | else: 236 | return False 237 | 238 | lastline = [("", NORMAL)] # dummy 239 | inserted = [] 240 | deleted = [] 241 | for line in lines: 242 | if typeof(lastline) == typeof(line) or typeof(line) not in ('+', '-'): 243 | flushlines(deleted) 244 | flushlines(inserted) 245 | 246 | if typeof(line) == '-': 247 | deleted.append(line) 248 | elif typeof(line) == '+': 249 | if deleted and only_indented(deleted[-1], line): 250 | inserted.append(line) 251 | else: 252 | flushlines(deleted) 253 | flushline(line) 254 | else: 255 | flushline(line) 256 | 257 | lastline = line 258 | 259 | flushlines(deleted) 260 | flushlines(inserted) 261 | 262 | return result 263 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk0miya/diff-highlight/4c98c6d0dde9884ae48747ededc26496852d4493/tests/__init__.py -------------------------------------------------------------------------------- /tests/highlights/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk0miya/diff-highlight/4c98c6d0dde9884ae48747ededc26496852d4493/tests/highlights/__init__.py -------------------------------------------------------------------------------- /tests/highlights/test_command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from mock import patch 4 | from highlights.command import highlight_main 5 | 6 | if sys.version_info < (2, 7): 7 | import unittest2 as unittest 8 | else: 9 | import unittest 10 | 11 | if sys.version_info < (3, 0): 12 | from cStringIO import StringIO 13 | else: 14 | from io import StringIO 15 | 16 | version_info = sys.version_info 17 | 18 | 19 | # ESC utility 20 | def start(*colors): 21 | return "".join("\x1b[%dm" % c for c in colors) 22 | 23 | 24 | stop = start(0) 25 | 26 | 27 | def restart(*colors): 28 | return stop + start(*colors) 29 | 30 | 31 | class TestHighlightCommand(unittest.TestCase): 32 | @patch("highlights.command.sys") 33 | def test_highlight_main(self, sys): 34 | diff = ["\x1b[33m@@ -10,4 +10,6 @@\x1b[m\n", 35 | " \n", 36 | "\x1b[31m-print 'nice', 'boat'\x1b[m\n", 37 | "\x1b[31m-print \"bye world\"\x1b[m\n", 38 | "\x1b[32m+print 'hello', 'world'\x1b[m\n", 39 | "\x1b[32m+\x1b[m\n", 40 | "\x1b[32m+\x1b[m\n", 41 | "\x1b[32m+print 'bye world'\x1b[m\n", 42 | " \n"] 43 | sys.stdin = diff 44 | sys.stdout = StringIO() 45 | sys.version_info = version_info 46 | 47 | highlight_main() 48 | 49 | lines = sys.stdout.getvalue().splitlines() 50 | 51 | self.assertEqual(9, len(lines)) 52 | self.assertEqual("\x1b[33m@@ -10,4 +10,6 @@\x1b[m", lines[0]) 53 | self.assertEqual(" ", lines[1]) 54 | self.assertEqual(("%s-print '%snice%s', '%sboat%s'%s" % 55 | (start(31), restart(31, 7), restart(31), 56 | restart(31, 7), restart(31), stop)), 57 | lines[2]) 58 | self.assertEqual(("%s+print '%shello%s', '%sworld%s'%s" % 59 | (start(32), restart(32, 7), restart(32), 60 | restart(32, 7), restart(32), stop)), 61 | lines[3]) 62 | self.assertEqual("%s+%s" % (start(32), stop), lines[4]) 63 | self.assertEqual("%s+%s" % (start(32), stop), lines[5]) 64 | self.assertEqual(("%s-print %s\"%sbye world%s\"%s" % 65 | (start(31), restart(31, 7), restart(31), 66 | restart(31, 7), stop)), 67 | lines[6]) 68 | self.assertEqual(("%s+print %s'%sbye world%s'%s" % 69 | (start(32), restart(32, 7), restart(32), 70 | restart(32, 7), stop)), 71 | lines[7]) 72 | self.assertEqual(" ", lines[8]) 73 | 74 | @patch("highlights.command.sys") 75 | def test_highlight_including_new_file(self, sys): 76 | # diff including new file (new.txt) 77 | diff = ["--- /dev/null\n", 78 | "+++ b/new.txt\n", 79 | "@@ -0,0 +1,1 @@\n", 80 | "+aaa\n", 81 | "diff --git a/exist.txt b/exist.txt\n", 82 | "index 1d95c52..8bffa50 100644\n", 83 | "@@ -0,0 +1,1 @@\n", 84 | "-bbbb\n", 85 | "+aaaa\n"] 86 | sys.stdin = diff 87 | sys.stdout = StringIO() 88 | sys.version_info = version_info 89 | 90 | highlight_main() 91 | 92 | lines = sys.stdout.getvalue().splitlines() 93 | 94 | self.assertEqual(9, len(lines)) 95 | self.assertEqual("--- /dev/null", lines[0]) 96 | self.assertEqual("+++ b/new.txt", lines[1]) 97 | self.assertEqual("@@ -0,0 +1,1 @@", lines[2]) 98 | self.assertEqual("%s+aaa%s" % (start(32), stop), lines[3]) 99 | self.assertEqual("diff --git a/exist.txt b/exist.txt", lines[4]) 100 | self.assertEqual("index 1d95c52..8bffa50 100644", lines[5]) 101 | self.assertEqual("@@ -0,0 +1,1 @@", lines[6]) 102 | self.assertEqual("%s-bbbb%s" % (start(31), stop), lines[7]) 103 | self.assertEqual("%s+aaaa%s" % (start(32), stop), lines[8]) 104 | 105 | @patch("highlights.command.sys") 106 | def test_highlight_for_git_diff(self, sys): 107 | # git styled diff 108 | diff = ["commit 59f7d0c38d29e3796f554a5e3c60b8ca55a69814\n", 109 | "Author: Takeshi KOMIYA \n", 110 | "Date: Sun Jul 12 14:21:55 2015 +0900\n", 111 | "\n", 112 | " add bar\n", 113 | "\n", 114 | "diff --git a/bar b/bar\n", 115 | "new file mode 100644\n", 116 | "index 0000000..5716ca5\n", 117 | "--- /dev/null\n", 118 | "+++ b/bar\n", 119 | "@@ -0,0 +1 @@\n", 120 | "+a\n", 121 | "b\n", 122 | "c\n"] 123 | sys.stdin = diff 124 | sys.stdout = StringIO() 125 | sys.version_info = version_info 126 | 127 | highlight_main() 128 | 129 | lines = sys.stdout.getvalue().splitlines() 130 | 131 | self.assertEqual(15, len(lines)) 132 | self.assertEqual("commit 59f7d0c38d29e3796f554a5e3c60b8ca55a69814", lines[0]) 133 | self.assertEqual("Author: Takeshi KOMIYA ", lines[1]) 134 | self.assertEqual("Date: Sun Jul 12 14:21:55 2015 +0900", lines[2]) 135 | self.assertEqual("", lines[3]) 136 | self.assertEqual(" add bar", lines[4]) 137 | self.assertEqual("", lines[5]) 138 | self.assertEqual("diff --git a/bar b/bar", lines[6]) 139 | self.assertEqual("new file mode 100644", lines[7]) 140 | self.assertEqual("index 0000000..5716ca5", lines[8]) 141 | self.assertEqual("--- /dev/null", lines[9]) 142 | self.assertEqual("+++ b/bar", lines[10]) 143 | self.assertEqual("@@ -0,0 +1 @@", lines[11]) 144 | self.assertEqual("%s+a%s" % (start(32), stop), lines[12]) 145 | self.assertEqual("b", lines[13]) 146 | self.assertEqual("c", lines[14]) 147 | -------------------------------------------------------------------------------- /tests/highlights/test_pprint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | from six import u 5 | from difflib import SequenceMatcher 6 | from highlights.pprint import NORMAL, INSERTED, DELETED 7 | from highlights.pprint import pprint_hunk, highlight_pair, is_mergeable 8 | 9 | if sys.version_info < (2, 7): 10 | import unittest2 as unittest 11 | else: 12 | import unittest 13 | 14 | 15 | class TestPPrint(unittest.TestCase): 16 | def test_pprint_hunk(self): 17 | # new, new_lo, new_hi, old, old_lo, old_hi): 18 | new = [u("+print 'hello', 'world'"), 19 | u("+"), 20 | u("+"), 21 | u("+print 'bye world'")] 22 | old = [u("-print 'nice', 'boat'"), 23 | u("-print \"bye world\"")] 24 | ret = pprint_hunk(new, 0, 4, old, 0, 2) 25 | 26 | pairs = list(ret) 27 | self.assertEqual(26, len(pairs)) 28 | self.assertEqual((u("-print '"), DELETED, False), pairs[0]) 29 | self.assertEqual((u("nice"), DELETED, True), pairs[1]) 30 | self.assertEqual((u("', '"), DELETED, False), pairs[2]) 31 | self.assertEqual((u("boat"), DELETED, True), pairs[3]) 32 | self.assertEqual((u("'"), DELETED, False), pairs[4]) 33 | self.assertEqual((u("\n"), NORMAL, False), pairs[5]) 34 | self.assertEqual((u("+print '"), INSERTED, False), pairs[6]) 35 | self.assertEqual((u("hello"), INSERTED, True), pairs[7]) 36 | self.assertEqual((u("', '"), INSERTED, False), pairs[8]) 37 | self.assertEqual((u("world"), INSERTED, True), pairs[9]) 38 | self.assertEqual((u("'"), INSERTED, False), pairs[10]) 39 | self.assertEqual((u("\n"), NORMAL, False), pairs[11]) 40 | self.assertEqual((u("+"), INSERTED, False), pairs[12]) 41 | self.assertEqual((u("\n"), NORMAL, False), pairs[13]) 42 | self.assertEqual((u("+"), INSERTED, False), pairs[14]) 43 | self.assertEqual((u("\n"), NORMAL, False), pairs[15]) 44 | self.assertEqual((u("-print "), DELETED, False), pairs[16]) 45 | self.assertEqual((u("\""), DELETED, True), pairs[17]) 46 | self.assertEqual((u("bye world"), DELETED, False), pairs[18]) 47 | self.assertEqual((u("\""), DELETED, True), pairs[19]) 48 | self.assertEqual((u("\n"), NORMAL, False), pairs[20]) 49 | self.assertEqual((u("+print "), INSERTED, False), pairs[21]) 50 | self.assertEqual((u("'"), INSERTED, True), pairs[22]) 51 | self.assertEqual((u("bye world"), INSERTED, False), pairs[23]) 52 | self.assertEqual((u("'"), INSERTED, True), pairs[24]) 53 | self.assertEqual((u("\n"), NORMAL, False), pairs[25]) 54 | 55 | def test_highlight_pair(self): 56 | cruncher = SequenceMatcher() 57 | ret = highlight_pair(cruncher, 58 | "+print 'hello', 'world'", 59 | "-print 'nice', 'boat'") 60 | 61 | pairs = list(ret) 62 | self.assertEqual(12, len(pairs)) 63 | self.assertEqual((u("-print '"), DELETED, False), pairs[0]) 64 | self.assertEqual((u("nice"), DELETED, True), pairs[1]) 65 | self.assertEqual((u("', '"), DELETED, False), pairs[2]) 66 | self.assertEqual((u("boat"), DELETED, True), pairs[3]) 67 | self.assertEqual((u("'"), DELETED, False), pairs[4]) 68 | self.assertEqual((u("\n"), NORMAL, False), pairs[5]) 69 | self.assertEqual((u("+print '"), INSERTED, False), pairs[6]) 70 | self.assertEqual((u("hello"), INSERTED, True), pairs[7]) 71 | self.assertEqual((u("', '"), INSERTED, False), pairs[8]) 72 | self.assertEqual((u("world"), INSERTED, True), pairs[9]) 73 | self.assertEqual((u("'"), INSERTED, False), pairs[10]) 74 | self.assertEqual((u("\n"), NORMAL, False), pairs[11]) 75 | 76 | def test_highlight_appended_text(self): 77 | # new, new_lo, new_hi, old, old_lo, old_hi): 78 | new = [u("+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " 79 | "incididunt ut labore et commodo magna aliqua. Ut enim ad minim veniam, quis " 80 | "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " 81 | "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " 82 | "fugiat nulla pariatur.")] 83 | old = [u("-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " 84 | "incididunt ut labore et dolore magna aliqua.")] 85 | ret = pprint_hunk(new, 0, 1, old, 0, 1) 86 | 87 | pairs = list(ret) 88 | self.assertEqual(10, len(pairs)) 89 | self.assertEqual((u("-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " 90 | "tempor incididunt ut labore et "), DELETED, False), pairs[0]) 91 | self.assertEqual((u("dolore"), DELETED, True), pairs[1]) 92 | self.assertEqual((u(" magna aliqua."), DELETED, False), pairs[2]) 93 | self.assertEqual((u(""), DELETED, True), pairs[3]) 94 | self.assertEqual((u("\n"), NORMAL, False), pairs[4]) 95 | self.assertEqual((u("+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " 96 | "tempor incididunt ut labore et "), INSERTED, False), pairs[5]) 97 | self.assertEqual((u("commodo"), INSERTED, True), pairs[6]) 98 | self.assertEqual((u(" magna aliqua."), INSERTED, False), pairs[7]) 99 | self.assertEqual((u(" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi " 100 | "ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit " 101 | "in voluptate velit esse cillum dolore eu fugiat nulla pariatur."), 102 | INSERTED, True), pairs[8]) 103 | self.assertEqual((u("\n"), NORMAL, False), pairs[9]) 104 | 105 | def test_highlight_indented_lines(self): 106 | # new, new_lo, new_hi, old, old_lo, old_hi): 107 | new = [u("+class Foo(object):"), 108 | u("+ def hello(name):"), 109 | u("+ print 'Hello ', name")] 110 | old = [u("-def hello(name):"), 111 | u("- print 'Hello ', name")] 112 | ret = pprint_hunk(new, 0, 3, old, 0, 2) 113 | 114 | pairs = list(ret) 115 | self.assertEqual(18, len(pairs)) 116 | self.assertEqual((u("+class Foo(object):"), INSERTED, False), pairs[0]) 117 | self.assertEqual((u("\n"), NORMAL, False), pairs[1]) 118 | self.assertEqual((u("-"), DELETED, False), pairs[2]) 119 | self.assertEqual((u(""), DELETED, True), pairs[3]) 120 | self.assertEqual((u("def hello(name):"), DELETED, False), pairs[4]) 121 | self.assertEqual((u("\n"), NORMAL, False), pairs[5]) 122 | self.assertEqual((u("-"), DELETED, False), pairs[6]) 123 | self.assertEqual((u(""), DELETED, True), pairs[7]) 124 | self.assertEqual((u(" print 'Hello ', name"), DELETED, False), pairs[8]) 125 | self.assertEqual((u("\n"), NORMAL, False), pairs[9]) 126 | self.assertEqual((u("+"), INSERTED, False), pairs[10]) 127 | self.assertEqual((u(" "), INSERTED, True), pairs[11]) 128 | self.assertEqual((u("def hello(name):"), INSERTED, False), pairs[12]) 129 | self.assertEqual((u("\n"), NORMAL, False), pairs[13]) 130 | self.assertEqual((u("+"), INSERTED, False), pairs[14]) 131 | self.assertEqual((u(" "), INSERTED, True), pairs[15]) 132 | self.assertEqual((u(" print 'Hello ', name"), INSERTED, False), pairs[16]) 133 | self.assertEqual((u("\n"), NORMAL, False), pairs[17]) 134 | 135 | def test_is_mergeable(self): 136 | # True/False/True -> ok 137 | new = [('A', None, True), ('B', None, False), ('C', None, True)] 138 | old = [('', None, False), ('B', None, False), ('', None, False)] 139 | self.assertTrue(is_mergeable(new, old, 2)) 140 | self.assertTrue(is_mergeable(old, new, 2)) 141 | 142 | # False/False/True -> NG 143 | new = [('A', None, False), ('B', None, False), ('C', None, True)] 144 | old = [('A', None, False), ('B', None, False), ('', None, False)] 145 | self.assertFalse(is_mergeable(new, old, 2)) 146 | self.assertFalse(is_mergeable(old, new, 2)) 147 | 148 | # True/False/False -> NG 149 | new = [('A', None, True), ('B', None, False), ('C', None, False)] 150 | old = [('', None, False), ('B', None, False), ('C', None, False)] 151 | self.assertFalse(is_mergeable(new, old, 2)) 152 | self.assertFalse(is_mergeable(old, new, 2)) 153 | 154 | # False/False/False -> NG 155 | new = [('A', None, False), ('B', None, False), ('C', None, False)] 156 | old = [('A', None, False), ('B', None, False), ('C', None, False)] 157 | self.assertFalse(is_mergeable(new, old, 2)) 158 | self.assertFalse(is_mergeable(old, new, 2)) 159 | 160 | # word/word/word -> ok 161 | new = [('A', None, True), ('B', None, False), ('C', None, True)] 162 | old = [('', None, False), ('B', None, False), ('', None, False)] 163 | self.assertTrue(is_mergeable(new, old, 2)) 164 | self.assertTrue(is_mergeable(old, new, 2)) 165 | 166 | # word/space/word -> ok 167 | new = [('A', None, True), (' ', None, False), ('C', None, True)] 168 | old = [('', None, False), (' ', None, False), ('', None, False)] 169 | self.assertTrue(is_mergeable(new, old, 2)) 170 | self.assertTrue(is_mergeable(old, new, 2)) 171 | 172 | # word/mark/word -> NG 173 | new = [('A', None, True), (',', None, False), ('C', None, True)] 174 | old = [('', None, False), (',', None, False), ('', None, False)] 175 | self.assertFalse(is_mergeable(new, old, 2)) 176 | self.assertFalse(is_mergeable(old, new, 2)) 177 | 178 | # word/word/mark -> ok 179 | new = [('A', None, True), ('B', None, False), (',', None, True)] 180 | old = [('', None, False), ('B', None, False), ('', None, False)] 181 | self.assertTrue(is_mergeable(new, old, 2)) 182 | self.assertTrue(is_mergeable(old, new, 2)) 183 | 184 | # mark/word/word -> ok 185 | new = [(',', None, True), ('B', None, False), ('C', None, True)] 186 | old = [('', None, False), ('B', None, False), ('', None, False)] 187 | self.assertTrue(is_mergeable(new, old, 2)) 188 | self.assertTrue(is_mergeable(old, new, 2)) 189 | 190 | # mark/mark/mark -> ok 191 | new = [(',', None, True), (':', None, False), ('.', None, True)] 192 | old = [('', None, False), (':', None, False), ('', None, False)] 193 | self.assertTrue(is_mergeable(new, old, 2)) 194 | self.assertTrue(is_mergeable(old, new, 2)) 195 | 196 | # mark/word/mark -> ok 197 | new = [(',', None, True), ('B', None, False), ('.', None, True)] 198 | old = [('', None, False), ('B', None, False), ('', None, False)] 199 | self.assertTrue(is_mergeable(new, old, 2)) 200 | self.assertTrue(is_mergeable(old, new, 2)) 201 | 202 | # mark/space/mark -> ok 203 | new = [(',', None, True), (' ', None, False), ('.', None, True)] 204 | old = [('', None, False), (' ', None, False), ('', None, False)] 205 | self.assertTrue(is_mergeable(new, old, 2)) 206 | self.assertTrue(is_mergeable(old, new, 2)) 207 | 208 | # A/ is /C => A/ is /C -> NG 209 | new = [('A', None, True), (' is ', None, False), ('C', None, True)] 210 | old = [('', None, False), (' is ', None, False), ('', None, False)] 211 | self.assertFalse(is_mergeable(new, old, 2)) 212 | self.assertFalse(is_mergeable(old, new, 2)) 213 | 214 | # A/ = /C => A/ = /C -> NG 215 | new = [('A', None, True), (' = ', None, False), ('C', None, True)] 216 | old = [('', None, False), (' = ', None, False), ('', None, False)] 217 | self.assertFalse(is_mergeable(new, old, 2)) 218 | self.assertFalse(is_mergeable(old, new, 2)) 219 | -------------------------------------------------------------------------------- /tests/test_diff_highlight.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | if sys.version_info < (2, 7): 5 | import unittest2 as unittest 6 | else: 7 | import unittest 8 | 9 | if sys.version_info < (3, 0): 10 | try: 11 | from mercurial import color 12 | except ImportError: 13 | from hgext import color 14 | from diff_highlight import colorui 15 | from mercurial.util import version as mercurial_version 16 | else: 17 | color = None 18 | 19 | 20 | class TestDiffHighlight(unittest.TestCase): 21 | @unittest.skipIf(color is None, "mercurial is not supported py3") 22 | def test_colorui(self): 23 | import curses 24 | curses.setupterm("xterm", 1) 25 | color._styles['diff.inserted_highlight'] = 'green inverse' 26 | color._styles['diff.deleted_highlight'] = 'red inverse' 27 | 28 | ui = colorui() 29 | if mercurial_version() >= "3.7.0": 30 | ui.pushbuffer(labeled=True) 31 | else: 32 | ui.pushbuffer() 33 | 34 | ui.write("@@ -10,4 +10,6 @@") 35 | ui.write("\n", '') 36 | ui.write(" ", '') 37 | ui.write("\n", '') 38 | ui.write("-print 'nice', 'boat'", label='diff.deleted') 39 | ui.write("-print \"bye world\"", label='diff.deleted') 40 | ui.write("+print 'hello', 'world'", label='diff.inserted') 41 | ui.write("+", label='diff.inserted') 42 | ui.write("+", label='diff.inserted') 43 | ui.write("+print 'bye world'", label='diff.inserted') 44 | ui.write(" ", '') 45 | ui.write("\n", '') 46 | 47 | stop = "\x1b(B\x1b[m" 48 | 49 | def start(*colors): 50 | return stop + "".join("\x1b[%dm" % c for c in colors) 51 | 52 | def restart(*colors): 53 | return stop + start(*colors) 54 | 55 | if mercurial_version() >= "3.7.0": 56 | lines = ui.popbuffer().splitlines() 57 | else: 58 | lines = ui.popbuffer(True).splitlines() 59 | self.assertEqual(9, len(lines)) 60 | self.assertEqual("@@ -10,4 +10,6 @@", lines[0]) 61 | self.assertEqual(" ", lines[1]) 62 | self.assertEqual(("%s-print '%snice%s', '%sboat%s'%s" % 63 | (start(31), restart(31, 7), restart(31), 64 | restart(31, 7), restart(31), stop)), 65 | lines[2]) 66 | self.assertEqual(("%s+print '%shello%s', '%sworld%s'%s" % 67 | (start(32), restart(32, 7), restart(32), 68 | restart(32, 7), restart(32), stop)), 69 | lines[3]) 70 | self.assertEqual("%s+%s" % (start(32), stop), lines[4]) 71 | self.assertEqual("%s+%s" % (start(32), stop), lines[5]) 72 | self.assertEqual(("%s-print %s\"%sbye world%s\"%s" % 73 | (start(31), restart(31, 7), restart(31), 74 | restart(31, 7), stop)), 75 | lines[6]) 76 | self.assertEqual(("%s+print %s'%sbye world%s'%s" % 77 | (start(32), restart(32, 7), restart(32), 78 | restart(32, 7), stop)), 79 | lines[7]) 80 | self.assertEqual(" ", lines[8]) 81 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py26,py27,py32,py33,py34,py35,hg35,hg36,hg41 3 | 4 | [testenv] 5 | deps= 6 | nose 7 | passenv= 8 | TRAVIS* 9 | commands= 10 | nosetests 11 | flake8 src tests 12 | 13 | [testenv:py2_common] 14 | deps= 15 | {[testenv]deps} 16 | six 17 | flake8 18 | mercurial 19 | 20 | [testenv:py3_common] 21 | deps= 22 | {[testenv]deps} 23 | six 24 | mock 25 | flake8 26 | 27 | [testenv:py26] 28 | deps= 29 | {[testenv:py2_common]deps} 30 | mock < 1.1.0 31 | unittest2 32 | commands= 33 | nosetests 34 | 35 | [testenv:py27] 36 | deps= 37 | {[testenv:py2_common]deps} 38 | mock 39 | 40 | [testenv:py32] 41 | deps= 42 | {[testenv:py3_common]deps} 43 | 44 | [testenv:py33] 45 | deps= 46 | {[testenv:py3_common]deps} 47 | 48 | [testenv:py34] 49 | deps= 50 | {[testenv:py3_common]deps} 51 | 52 | [testenv:py35] 53 | deps= 54 | {[testenv:py3_common]deps} 55 | 56 | [testenv:hg35] 57 | basepython= python2.7 58 | deps= 59 | {[testenv:py3_common]deps} 60 | mercurial<3.6 61 | 62 | [testenv:hg36] 63 | basepython= python2.7 64 | deps= 65 | {[testenv:py3_common]deps} 66 | mercurial<3.7 67 | 68 | [testenv:hg41] 69 | basepython= python2.7 70 | deps= 71 | {[testenv:py3_common]deps} 72 | mercurial<4.2 73 | 74 | [testenv:coverage] 75 | basepython= python2.7 76 | deps= 77 | {[testenv:py27]deps} 78 | coverage 79 | coveralls 80 | commands= 81 | nosetests --with-coverage --cover-package=highlights 82 | coveralls 83 | --------------------------------------------------------------------------------