pax_global_header00006660000000000000000000000064137061472250014521gustar00rootroot0000000000000052 comment=3b1b51f4cb816e9dc81cc89b2daf089cbc78c84c three-merge-0.1.1/000077500000000000000000000000001370614722500137245ustar00rootroot00000000000000three-merge-0.1.1/.github/000077500000000000000000000000001370614722500152645ustar00rootroot00000000000000three-merge-0.1.1/.github/workflows/000077500000000000000000000000001370614722500173215ustar00rootroot00000000000000three-merge-0.1.1/.github/workflows/linux-tests.yml000066400000000000000000000021631370614722500223450ustar00rootroot00000000000000name: Linux tests on: push: branches: - master pull_request: branches: - master jobs: linux: name: Linux Py${{ matrix.PYTHON_VERSION }} runs-on: ubuntu-latest env: PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} RUNNER_OS: "ubuntu" strategy: fail-fast: false matrix: PYTHON_VERSION: ["3.5", "3.6", "3.7", "3.8"] steps: - name: Checkout branch/PR uses: actions/checkout@v1 - name: Install Conda uses: goanpeca/setup-miniconda@v1 with: activate-environment: test auto-update-conda: true auto-activate-base: false python-version: ${{ matrix.PYTHON_VERSION }} - name: Install build/test dependencies shell: bash -l {0} run: pip install diff-match-patch pytest pytest-cov coverage codecov - name: Run tests shell: bash -l {0} run: pytest -v -x --cov=three_merge three_merge/tests three-merge-0.1.1/.github/workflows/mac-tests.yml000066400000000000000000000021561370614722500217500ustar00rootroot00000000000000name: MacOS tests on: push: branches: - master pull_request: branches: - master jobs: linux: name: MacOS Py${{ matrix.PYTHON_VERSION }} runs-on: macos-latest env: PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} RUNNER_OS: "macos" strategy: fail-fast: false matrix: PYTHON_VERSION: ["3.5", "3.6", "3.7", "3.8"] steps: - name: Checkout branch uses: actions/checkout@v1 - name: Install Conda uses: goanpeca/setup-miniconda@v1 with: activate-environment: test auto-update-conda: true auto-activate-base: false python-version: ${{ matrix.PYTHON_VERSION }} - name: Install build/test dependencies shell: bash -l {0} run: pip install diff-match-patch pytest pytest-cov coverage codecov - name: Run tests shell: bash -l {0} run: pytest -v -x --cov=three_merge three_merge/tests three-merge-0.1.1/.github/workflows/windows-tests.yml000066400000000000000000000021661370614722500227030ustar00rootroot00000000000000name: Windows tests on: push: branches: - master pull_request: branches: - master jobs: linux: name: Windows Py${{ matrix.PYTHON_VERSION }} runs-on: windows-latest env: PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} RUNNER_OS: "windows" strategy: fail-fast: false matrix: PYTHON_VERSION: ["3.5", "3.6", "3.7", "3.8"] steps: - name: Checkout branch uses: actions/checkout@v1 - name: Install Conda uses: goanpeca/setup-miniconda@v1 with: activate-environment: test auto-update-conda: true auto-activate-base: false python-version: ${{ matrix.PYTHON_VERSION }} - name: Install build/test dependencies shell: bash -l {0} run: pip install diff-match-patch pytest pytest-cov coverage codecov - name: Run tests shell: bash -l {0} run: pytest -v -x --cov=three_merge three_merge/tests three-merge-0.1.1/.gitignore000066400000000000000000000034401370614722500157150ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # VSCode files .vscode/ three-merge-0.1.1/CHANGELOG.md000066400000000000000000000032061370614722500155360ustar00rootroot00000000000000## Version 0.1.1 (2020/07/22) ### Issues Closed * [Issue 8](https://github.com/spyder-ide/three-merge/issues/8) - Release v0.1.1 In this release 1 issue was closed. ### Pull Requests Merged * [PR 7](https://github.com/spyder-ide/three-merge/pull/7) - PR: Fix error with source text update on preserved strings and empty preservations, by [@andfoy](https://github.com/andfoy) In this release 1 pull request was closed. ## Version 0.1.0 (2020/07/21) ### Issues Closed * [Issue 6](https://github.com/spyder-ide/three-merge/issues/6) - Release v0.1.0 * [Issue 4](https://github.com/spyder-ide/three-merge/issues/4) - Add RELEASE instructions ([PR 5](https://github.com/spyder-ide/three-merge/pull/5) by [@andfoy](https://github.com/andfoy)) * [Issue 3](https://github.com/spyder-ide/three-merge/issues/3) - Improve README ([PR 5](https://github.com/spyder-ide/three-merge/pull/5) by [@andfoy](https://github.com/andfoy)) * [Issue 1](https://github.com/spyder-ide/three-merge/issues/1) - Add tests ([PR 2](https://github.com/spyder-ide/three-merge/pull/2) by [@andfoy](https://github.com/andfoy)) In this release 4 issues were closed. ### Pull Requests Merged * [PR 5](https://github.com/spyder-ide/three-merge/pull/5) - PR: Improve README and add RELEASE instructions, by [@andfoy](https://github.com/andfoy) ([4](https://github.com/spyder-ide/three-merge/issues/4), [3](https://github.com/spyder-ide/three-merge/issues/3)) * [PR 2](https://github.com/spyder-ide/three-merge/pull/2) - PR: Add tests and configure CI, by [@andfoy](https://github.com/andfoy) ([1](https://github.com/spyder-ide/three-merge/issues/1)) In this release 2 pull requests were closed. three-merge-0.1.1/LICENSE000066400000000000000000000020531370614722500147310ustar00rootroot00000000000000MIT License Copyright (c) 2020 Spyder IDE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. three-merge-0.1.1/MANIFEST.in000066400000000000000000000001171370614722500154610ustar00rootroot00000000000000include CHANGELOG.md include LICENSE include README.md prune three_merge/tests three-merge-0.1.1/README.md000066400000000000000000000071101370614722500152020ustar00rootroot00000000000000# three-merge [![Project License - MIT](https://img.shields.io/pypi/l/three-merge.svg)](https://raw.githubusercontent.com/spyder-ide/three-merge/master/LICENSE) [![pypi version](https://img.shields.io/pypi/v/three-merge.svg)](https://pypi.org/project/three-merge/) [![conda version](https://img.shields.io/conda/vn/conda-forge/three-merge.svg)](https://www.anaconda.com/download/) [![download count](https://img.shields.io/conda/dn/conda-forge/three-merge.svg)](https://www.anaconda.com/download/) [![Downloads](https://pepy.tech/badge/three-merge)](https://pepy.tech/project/three-merge) [![PyPI status](https://img.shields.io/pypi/status/three-merge.svg)](https://github.com/spyder-ide/three-merge) ![Linux tests](https://github.com/spyder-ide/three-merge/workflows/Linux%20tests/badge.svg) ![MacOS tests](https://github.com/spyder-ide/three-merge/workflows/MacOS%20tests/badge.svg) ![Windows tests](https://github.com/spyder-ide/three-merge/workflows/Windows%20tests/badge.svg) *Copyright © 2020– Spyder Project Contributors* ## Overview Simple Python library to perform a 3-way merge between strings, based on [diff-match-patch](https://github.com/google/diff-match-patch). This library performs merges at a character level, as opposed to most VCS systems, which opt for a line-based approach. ## Installing To install three-merge, you can use both conda or pip package managers: ```bash # Using conda (Recommended) conda install three-merge -c spyder-ide # Using pip pip install three-merge ``` ## Dependencies This package depends on [diff-match-patch](https://github.com/google/diff-match-patch) to compute and track the differences across the source and target strings with respect to the base one. ## Installing locally To install and develop three-merge locally, you will need to install diff-match-patch: ```bash # Using conda conda install diff-match-patch # Using pip pip install diff-match-patch ``` Then, you can install the package locally using pip: ```bash pip install -U -e . ``` ## Running tests We use pytest to run tests as it follows: ```bash pytest -x -v three_merge/tests ``` ## Package usage Three-merge provides a ``merge`` function to merge changes from two strings (source, target) with respect a original string (base). This library is able to handle additions, deletions and preserved sections across both strings, while detecting and highlighting possible merge conflicts (like Git). ```python # Package import from three_merge import merge # Strings have non-conflicting additions base = '123456789101112' source = '0123456789101112' target = '12345678910111213' # merged = '012345678910111213' merged = merge(source, target, base) # Strings have an addition conflict base = '123456789101112' source = '123a456789101112' target = '123b456789101112' # merged = '123<<<<<<< ++ a ======= ++ b >>>>>>>456789101112' merged = merge(source, target, base) # Strings have non-conflicting addition/deletions base = '123456789101112' source = '123456789ab101112' target = '123789101112' # merged = '123789ab101112' merged = merge(source, target, base) ``` For more examples, please take a look at our [tests](https://github.com/spyder-ide/three-merge/blob/master/three_merge/tests/test_merge.py). ## Changelog Please see our [CHANGELOG](https://github.com/spyder-ide/three-merge/blob/master/CHANGELOG.md) file to learn more about our new features and improvements. ## Contribution guidelines We follow PEP8 and PEP257 for all Python modules. We use MyPy type annotations for all functions and classes declared on this package. Feel free to send a PR or create an issue if you have any problem/question. three-merge-0.1.1/RELEASE.md000066400000000000000000000012611370614722500153260ustar00rootroot00000000000000To release a new version of three-merge: 1. git fetch upstream && git checkout upstream/master 2. Close milestone on GitHub 3. git clean -xfdi 4. Update CHANGELOG.md with loghub 5. git add -A && git commit -m "Update Changelog" 6. Update release version in ``__init__.py`` (set release version, remove 'dev0') 7. git add -A && git commit -m "Release vX.X.X" 8. python setup.py sdist 9. python setup.py bdist_wheel --universal 10. twine check 11. twine upload 12. git tag -a vX.X.X -m "Release vX.X.X" 13. Update development version in ``__init__.py`` (add 'dev0' and increment minor) 14. git add -A && git commit -m "Back to work" 15. git push upstream master 16. git push upstream --tags three-merge-0.1.1/setup.cfg000066400000000000000000000003111370614722500155400ustar00rootroot00000000000000[check-manifest] ignore = .checkignore .github .github/* .github/workflows/* .codecov.yml .coveragerc .coverage RELEASE.md doc doc/* ignore-bad-ideas = *.mo three-merge-0.1.1/setup.py000066400000000000000000000045131370614722500154410ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) Spyder Project Contributors # # Licensed under the terms of the MIT License # (see LICENSE for details) # ----------------------------------------------------------------------------- """Setup script for three_merge.""" # Standard library imports import ast import os import os.path as osp # Third party imports from setuptools import find_packages, setup HERE = osp.dirname(osp.abspath(__file__)) def get_version(module='three_merge'): """Get version.""" with open(os.path.join(HERE, module, '__init__.py'), 'r') as f: data = f.read() lines = data.split('\n') for line in lines: if line.startswith('VERSION_INFO'): version_tuple = ast.literal_eval(line.split('=')[-1].strip()) version = '.'.join(map(str, version_tuple)) break return version def get_description(): """Get long description.""" with open(os.path.join(HERE, 'README.md'), 'r') as f: data = f.read() return data REQUIREMENTS = [ 'diff-match-patch' ] EXTRAS_REQUIRE = { 'test': [ 'pytest', 'pytest-cov', 'flaky', 'pytest-timeout' ] } setup( name='three-merge', version=get_version(), keywords=['Merge', 'Files', 'Three-way'], url='https://github.com/spyder-ide/three-merge', license='MIT', author='Spyder Project Contributors', author_email='spyder.python@gmail.com', description='Simple library for merging two strings with respect ' 'to a base one', long_description=get_description(), long_description_content_type='text/markdown', packages=find_packages(exclude=['contrib', 'docs']), install_requires=REQUIREMENTS, include_package_data=True, classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: MacOS', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8' ], extras_require=EXTRAS_REQUIRE ) three-merge-0.1.1/three_merge/000077500000000000000000000000001370614722500162125ustar00rootroot00000000000000three-merge-0.1.1/three_merge/__init__.py000066400000000000000000000005411370614722500203230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see LICENSE for details) """Three-way merge between two strings with respect to a base one.""" # Local imports from .merge import merge merge # Package version VERSION_INFO = (0, 1, 1) __version__ = '.'.join(map(str, VERSION_INFO)) three-merge-0.1.1/three_merge/merge.py000066400000000000000000000255431370614722500176740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see LICENSE for details) """Main three_merge function.""" # Third-party imports from diff_match_patch import diff_match_patch # Constants DIFFER = diff_match_patch() PRESERVED = 0 DELETION = -1 ADDITION = 1 def merge(source: str, target: str, base: str) -> str: diff1_l = DIFFER.diff_main(base, source) diff2_l = DIFFER.diff_main(base, target) diff1 = iter(diff1_l) diff2 = iter(diff2_l) composed_text = [] source = next(diff1, None) target = next(diff2, None) while source is not None and target is not None: source_status, source_text = source target_status, target_text = target if source_status == PRESERVED and target_status == PRESERVED: # Base is preserved for both source and target if len(source_text) > len(target_text): # Addition performed by target advance = True composed_text.append(target_text) _, (_, invariant) = DIFFER.diff_main(target_text, source_text) target = next(diff2, None) while invariant != '' and target is not None: # Apply target changes until invariant is preserved # target = next(diff2, None) target_status, target_text = target if target_status == DELETION: if len(target_text) > len(invariant): target_text = target_text[len(invariant):] invariant = '' target = (target_status, target_text) else: invariant = invariant[len(target_text):] target = next(diff2, None) elif target_status == ADDITION: composed_text.append(target_text) target = next(diff2, None) else: # Recompute invariant and advance source if len(invariant) > len(target_text): assert invariant[:len(target_text)] == target_text source = ( source_status, invariant[len(target_text):]) composed_text.append(target_text) invariant = '' advance = False target = next(diff2, None) else: target_text = target_text[len(invariant):] composed_text.append(invariant) invariant = '' target = (target_status, target_text) if advance: source = next(diff1, None) elif len(source_text) < len(target_text): # Addition performed by source advance = True composed_text.append(source_text) _, (_, invariant) = DIFFER.diff_main(source_text, target_text) source = next(diff1, None) while invariant != '' and target is not None: # Apply source changes until invariant is preserved source_status, source_text = source if source_status == DELETION: if len(source_text) > len(invariant): source_text = source_text[len(invariant):] invariant = '' source = (source_status, source_text) else: invariant = invariant[len(source_text):] source = next(diff1, None) elif source_status == ADDITION: composed_text.append(source_text) source = next(diff1, None) else: # Recompute invariant and advance source # invariant = invariant[:len(source_text)] if len(invariant) > len(source_text): assert invariant[:len(source_text)] == source_text target = ( target_status, invariant[len(source_text):]) composed_text.append(source_text) invariant = '' advance = False source = next(diff1, None) else: source_text = source_text[len(invariant):] composed_text.append(invariant) invariant = '' source = (source_status, source_text) if advance: target = next(diff2, None) else: # Source and target are equal composed_text.append(source_text) source = next(diff1, None) target = next(diff2, None) elif source_status == ADDITION and target_status == PRESERVED: # Source is adding text composed_text.append(source_text) source = next(diff1, None) elif source_status == PRESERVED and target_status == ADDITION: # Target is adding text composed_text.append(target_text) target = next(diff2, None) elif source_status == DELETION and target_status == PRESERVED: if len(target_text) > len(source_text): # Take target text, remove the corresponding part from source target_text = target_text[len(source_text):] # composed_text.append(target_text) # source = diff1.pop(0) target = (target_status, target_text) source = next(diff1, None) elif len(target_text) < len(source_text): source_text = source_text[len(target_text):] source = (source_status, source_text) target = next(diff2, None) elif source_status == PRESERVED and target_status == DELETION: if len(source_text) > len(target_text): # Take source text, remove the corresponding part from target source_text = source_text[len(target_text):] source = (source_status, source_text) target = next(diff2, None) elif len(source_text) < len(target_text): # Advance to next source target_text = target_text[len(source_text):] target = (target_status, target_text) source = next(diff1, None) elif source_status == DELETION and target_status == ADDITION: # Merge conflict composed_text.append('<<<<<<< ++ {0} '.format(target_text)) composed_text.append('======= -- {0} '.format(source_text)) composed_text.append('>>>>>>>') source = next(diff1, None) target = next(diff2, None) if target is not None: target_status, target_text = target if target_text.startswith(source_text): target_text = target_text[len(source_text):] target = (target_status, target_text) elif source_status == ADDITION and target_status == DELETION: # Merge conflict composed_text.append('<<<<<<< ++ {0} '.format(source_text)) composed_text.append('======= -- {0} '.format(target_text)) composed_text.append('>>>>>>>') source = next(diff1, None) target = next(diff2, None) if source is not None: source_status, source_text = source if source_text.startswith(target_text): source_text = source_text[len(target_text):] source = (source_status, source_text) elif source_status == ADDITION and target_status == ADDITION: # Possible merge conflict if len(source_text) >= len(target_text): if source_text.startswith(target_text): composed_text.append(source_text) else: # Merge conflict composed_text.append('<<<<<<< ++ {0} '.format(source_text)) composed_text.append('======= ++ {0} '.format(target_text)) composed_text.append('>>>>>>>') else: if target_text.startswith(source_text): composed_text.append(target_text) else: # Merge conflict composed_text.append('<<<<<<< ++ {0} '.format(source_text)) composed_text.append('======= ++ {0} '.format(target_text)) composed_text.append('>>>>>>>') source = next(diff1, None) target = next(diff2, None) elif source_status == DELETION and target_status == DELETION: # Possible merge conflict merge_conflict = False if len(source_text) > len(target_text): if source_text.startswith(target_text): # Peek target to delete preserved text source_text = source_text[len(target_text):] source = (source_status, source_text) target = next(diff2, None) else: merge_conflict = True elif len(target_text) > len(source_text): if target_text.startswith(source_text): target_text = target_text[len(source_text):] target = (target_status, target_text) source = next(diff1, None) else: merge_conflict = True else: if target_text == source_text: # Both source and target remove the same text source = next(diff1, None) target = next(diff2, None) else: merge_conflict = True if merge_conflict: composed_text.append('<<<<<<< -- {0} '.format(source_text)) composed_text.append('======= -- {0} '.format(target_text)) composed_text.append('>>>>>>>') while source is not None: source_status, source_text = source assert source_status == ADDITION or source_status == PRESERVED if source_status == ADDITION: composed_text.append(source_text) source = next(diff1, None) while target is not None: target_status, target_text = target assert target_status == ADDITION or source_status == PRESERVED if target_status == ADDITION: composed_text.append(target_text) target = next(diff2, None) return ''.join(composed_text) three-merge-0.1.1/three_merge/tests/000077500000000000000000000000001370614722500173545ustar00rootroot00000000000000three-merge-0.1.1/three_merge/tests/__init__.py000066400000000000000000000000001370614722500214530ustar00rootroot00000000000000three-merge-0.1.1/three_merge/tests/test_merge.py000066400000000000000000000065161370614722500220740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see LICENSE for details) """Three way merge tests.""" # Local imports from three_merge import merge def test_unrelated_additions(): base = '123456789101112' source = '0123456789101112' target = '12345678910111213' expected = '012345678910111213' merged = merge(source, target, base) assert merged == expected merged = merge(target, source, base) assert merged == expected def test_unrelated_deletions(): base = '123456789101112' source = '1256789101112' target = '12345678912' expected = '125678912' merged = merge(source, target, base) assert merged == expected merged = merge(target, source, base) assert merged == expected def test_multiple_additions(): base = '123456789101112' source = '1234(56789101112' target = '1234567)89101112' expected = '1234(567)89101112' merged = merge(source, target, base) assert merged == expected merged = merge(target, source, base) assert merged == expected def test_multiple_deletions(): base = '123456789101112' source = '126789101112' target = '123489101112' expected = '1289101112' merged = merge(source, target, base) assert merged == expected # breakpoint() merged = merge(target, source, base) assert merged == expected def test_addition_deletion(): base = '123456789101112' source = '123456789ab101112' target = '123789101112' expected = '123789ab101112' merged = merge(source, target, base) assert merged == expected merged = merge(target, source, base) assert merged == expected def test_addition_conflict(): base = '123456789101112' source = '123a456789101112' target = '123b456789101112' expected = '123<<<<<<< ++ a ======= ++ b >>>>>>>456789101112' merged = merge(source, target, base) assert merged == expected expected = '123<<<<<<< ++ b ======= ++ a >>>>>>>456789101112' merged = merge(target, source, base) assert merged == expected def test_addition_deletion_conflict(): base = '123456789101112' source = '123a456789101112' target = '12356789101112' expected = '123<<<<<<< ++ a ======= -- 4 >>>>>>>56789101112' merged = merge(source, target, base) assert merged == expected expected = '123<<<<<<< ++ a ======= -- 4 >>>>>>>56789101112' merged = merge(target, source, base) assert merged == expected def test_deletion_composition(): base = '123456789101112' source = '12346789101112' target = '12346789101112' expected = '12346789101112' merged = merge(source, target, base) assert merged == expected def test_deletion_addition_conflict(): base = '123456789101112' source = '123789101112' target = '1234a56789101112' expected = '123<<<<<<< ++ a ======= -- 56 >>>>>>>789101112' merged = merge(source, target, base) assert expected == merged merged = merge(target, source, base) assert merged == expected def test_triple_addition(): base = '123456789101112' source = '123\n 456789101112' target = '12345678910(11)12' expected = '123\n 45678910(11)12' merged = merge(source, target, base) assert merged == expected merged = merge(target, source, base) assert merged == expected