mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-03-25 08:55:50 +00:00
Introduce conflict resolution via commands
Fix #127 More options of conflict resolution are discussed there, but they all require extra dependencies. This new API allows the user to plug in third-party scripts to do those.
This commit is contained in:
parent
e62e3a26f6
commit
22568571c2
5 changed files with 110 additions and 4 deletions
|
|
@ -17,6 +17,8 @@ Version 0.13.0
|
|||
cannot have a storage section ``[storage foo]`` and a pair ``[pair foo]`` in
|
||||
your config, they have to have different names. This is done such that
|
||||
console output is always unambigous. See :gh:`459`.
|
||||
- Custom commands can now be used for conflict resolution during sync. See
|
||||
:gh:`127`.
|
||||
|
||||
Version 0.12.1
|
||||
==============
|
||||
|
|
|
|||
|
|
@ -87,9 +87,19 @@ Pair Section
|
|||
|
||||
Valid values are:
|
||||
|
||||
- ``null``, where an error is shown and no changes are done.
|
||||
- ``"a wins"`` and ``"b wins"``, where the whole item is taken from one side.
|
||||
Vdirsyncer will not attempt to merge the two items.
|
||||
- ``null``, the default, where an error is shown and no changes are done.
|
||||
- ``["command", "vimdiff"]``: ``vimdiff <a> <b>`` will be called where
|
||||
``<a>`` and ``<b>`` are temporary files that contain the item of each side
|
||||
respectively. The files need to be exactly the same when the command
|
||||
returns.
|
||||
|
||||
- ``vimdiff`` can be replaced with any other command. For example, in POSIX
|
||||
``["command", "cp"]`` is equivalent to ``"a wins"``.
|
||||
- Additional list items will be forwarded as arguments. For example,
|
||||
``["command", "vimdiff", "--noplugin"]`` runs ``vimdiff --noplugin``.
|
||||
|
||||
Vdirsyncer never attempts to "automatically merge" the two items.
|
||||
|
||||
- ``metadata``: Metadata keys that should be synchronized when ``vdirsyncer
|
||||
metasync`` is executed. Example::
|
||||
|
|
|
|||
|
|
@ -409,3 +409,43 @@ def test_no_configured_pairs(tmpdir, runner, cmd):
|
|||
result = runner.invoke([cmd])
|
||||
assert result.output == 'critical: Nothing to do.\n'
|
||||
assert result.exception.code == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize('resolution,expect_foo,expect_bar', [
|
||||
(['command', 'cp'], 'UID:lol\nfööcontent', 'UID:lol\nfööcontent')
|
||||
])
|
||||
def test_conflict_resolution(tmpdir, runner, resolution, expect_foo,
|
||||
expect_bar):
|
||||
runner.write_with_general(dedent('''
|
||||
[pair foobar]
|
||||
a = foo
|
||||
b = bar
|
||||
collections = null
|
||||
conflict_resolution = {val}
|
||||
|
||||
[storage foo]
|
||||
type = filesystem
|
||||
fileext = .txt
|
||||
path = {base}/foo
|
||||
|
||||
[storage bar]
|
||||
type = filesystem
|
||||
fileext = .txt
|
||||
path = {base}/bar
|
||||
'''.format(base=str(tmpdir), val=json.dumps(resolution))))
|
||||
|
||||
foo = tmpdir.join('foo')
|
||||
bar = tmpdir.join('bar')
|
||||
fooitem = foo.join('lol.txt').ensure()
|
||||
fooitem.write('UID:lol\nfööcontent')
|
||||
baritem = bar.join('lol.txt').ensure()
|
||||
baritem.write('UID:lol\nbööcontent')
|
||||
|
||||
r = runner.invoke(['discover'])
|
||||
assert not r.exception
|
||||
|
||||
r = runner.invoke(['sync'])
|
||||
assert not r.exception
|
||||
|
||||
assert fooitem.read() == expect_foo
|
||||
assert baritem.read() == expect_bar
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import os
|
|||
import string
|
||||
from itertools import chain
|
||||
|
||||
from click_threading import get_ui_worker
|
||||
|
||||
from . import cli_logger
|
||||
from .fetchparams import expand_fetch_params
|
||||
from .. import PROJECT_HOME, exceptions
|
||||
|
|
@ -251,12 +253,32 @@ class PairConfig(object):
|
|||
self.name_b = options.pop('b')
|
||||
self.options = options
|
||||
|
||||
conflict_resolution = self.options.get('conflict_resolution', None)
|
||||
self._set_conflict_resolution()
|
||||
self._set_collections()
|
||||
|
||||
def _set_conflict_resolution(self):
|
||||
conflict_resolution = self.options.pop('conflict_resolution', None)
|
||||
if conflict_resolution in (None, 'a wins', 'b wins'):
|
||||
self.conflict_resolution = conflict_resolution
|
||||
elif isinstance(conflict_resolution, list) and \
|
||||
len(conflict_resolution) > 1 and \
|
||||
conflict_resolution[0] == 'command':
|
||||
def resolve(a, b):
|
||||
a_name = self.config_a['instance_name']
|
||||
b_name = self.config_b['instance_name']
|
||||
command = conflict_resolution[1:]
|
||||
def inner():
|
||||
return resolve_conflict_via_command(a, b, command, a_name,
|
||||
b_name)
|
||||
|
||||
ui_worker = get_ui_worker()
|
||||
ui_worker.put(inner)
|
||||
|
||||
self.conflict_resolution = resolve
|
||||
else:
|
||||
raise ValueError('Invalid value for `conflict_resolution`.')
|
||||
|
||||
def _set_collections(self):
|
||||
try:
|
||||
collections = self.options['collections']
|
||||
except KeyError:
|
||||
|
|
@ -279,3 +301,35 @@ class CollectionConfig(object):
|
|||
self.name = name
|
||||
self.config_a = config_a
|
||||
self.config_b = config_b
|
||||
|
||||
|
||||
def resolve_conflict_via_command(a, b, command, a_name, b_name):
|
||||
import tempfile
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from ..utils.vobject import Item
|
||||
|
||||
dir = tempfile.mkdtemp(prefix='vdirsyncer-conflict.')
|
||||
try:
|
||||
a_tmp = os.path.join(dir, a_name)
|
||||
b_tmp = os.path.join(dir, b_name)
|
||||
|
||||
with open(a_tmp, 'w') as f:
|
||||
f.write(a.raw)
|
||||
with open(b_tmp, 'w') as f:
|
||||
f.write(b.raw)
|
||||
|
||||
subprocess.check_call(command + [a_tmp, b_tmp])
|
||||
|
||||
with open(a_tmp) as f:
|
||||
new_a = f.read()
|
||||
with open(b_tmp) as f:
|
||||
new_b = f.read()
|
||||
|
||||
if new_a != new_b:
|
||||
raise exceptions.UserError('The two files are not completely '
|
||||
'equal.')
|
||||
return Item(new_a)
|
||||
finally:
|
||||
shutil.rmtree(dir)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def sync_collection(wq, collection, general, force_delete):
|
|||
b = storage_instance_from_config(collection.config_b)
|
||||
sync(
|
||||
a, b, status,
|
||||
conflict_resolution=pair.options.get('conflict_resolution', None),
|
||||
conflict_resolution=pair.conflict_resolution,
|
||||
force_delete=force_delete
|
||||
)
|
||||
except:
|
||||
|
|
|
|||
Loading…
Reference in a new issue