diff --git a/yokadi/tests/textpulluitestcase.py b/yokadi/tests/textpulluitestcase.py index 6ca88569..a349a2ad 100644 --- a/yokadi/tests/textpulluitestcase.py +++ b/yokadi/tests/textpulluitestcase.py @@ -1,3 +1,4 @@ +import textwrap import unittest from tempfile import TemporaryDirectory @@ -6,7 +7,8 @@ from yokadi.tests.pulltestcase import createBothModifiedConflictFixture from yokadi.tests.pulltestcase import createModifiedDeletedConflictFixture from yokadi.ycli import tui -from yokadi.ycli.synccmd import TextPullUi +from yokadi.ycli.synccmd import TextPullUi, prepareConflictText, shortenText, SHORTENED_SUFFIX, \ + SHORTENED_TEXT_MAX_LENGTH from yokadi.sync import ALIASES_DIRNAME from yokadi.sync.syncmanager import SyncManager @@ -50,3 +52,60 @@ def testAddRename(self): self.assertEqual(renames, { ALIASES_DIRNAME: [("a", "a_1"), ("b", "b_1")] }) + + def testPrepareConflictText(self): + data = ( + ("foo", "bar", "L> foo\nR> bar\n"), + ( + textwrap.dedent("""\ + Common + Local1 + More common + Local2 + Local3 + Even more common"""), + textwrap.dedent("""\ + Common + Remote1 + More common + Remote2 + Even more common"""), + textwrap.dedent("""\ + Common + L> Local1 + R> Remote1 + More common + R> Remote2 + L> Local2 + L> Local3 + Even more common + """), + ) + ) + + for local, remote, expected in data: + output = prepareConflictText(local, remote) + self.assertEqual(output, expected) + + def testShortenText(self): + data = ( + ("foo", "foo"), + ( + textwrap.dedent("""\ + Common + Local1 + More common + Local2 + Local3 + Even more common"""), + "Common" + SHORTENED_SUFFIX + ), + ( + "a" * 160, + "a" * (SHORTENED_TEXT_MAX_LENGTH - len(SHORTENED_SUFFIX)) + SHORTENED_SUFFIX + ) + ) + + for src, expected in data: + output = shortenText(src) + self.assertEqual(output, expected) diff --git a/yokadi/ycli/synccmd.py b/yokadi/ycli/synccmd.py index dbfc70b9..08fd4329 100644 --- a/yokadi/ycli/synccmd.py +++ b/yokadi/ycli/synccmd.py @@ -1,6 +1,7 @@ import os from cmd import Cmd from collections import defaultdict +from difflib import Differ from yokadi.core import basepaths from yokadi.core.yokadioptionparser import YokadiOptionParser @@ -12,6 +13,12 @@ from yokadi.ycli import tui +LOCAL_PREFIX = "L> " +REMOTE_PREFIX = "R> " + +SHORTENED_SUFFIX = " (...)" +SHORTENED_TEXT_MAX_LENGTH = 40 + # Keys are a tuple of (prompt, fieldName) HEADER_INFO = { ALIASES_DIRNAME: ("Alias named \"{}\"", "name"), @@ -32,6 +39,37 @@ def printConflictObjectHeader(obj): print("\n# {}".format(prompt)) +def prepareConflictText(local, remote): + differ = Differ() + diff = differ.compare(local.splitlines(keepends=True), + remote.splitlines(keepends=True)) + lines = [] + for line in diff: + code = line[0] + rest = line[2:] + if rest[-1] != "\n": + rest += "\n" + if code == "?": + continue + if code == "-": + lines.append(LOCAL_PREFIX + rest) + elif code == "+": + lines.append(REMOTE_PREFIX + rest) + else: + lines.append(rest) + return "".join(lines) + + +def shortenText(text): + """Takes a potentially multi-line text and returns a one-line, shortened version of it""" + cr = text.find("\n") + if cr >= 0: + text = text[:cr] + if cr >= 0 or len(text) > SHORTENED_TEXT_MAX_LENGTH: + text = text[:SHORTENED_TEXT_MAX_LENGTH - len(SHORTENED_SUFFIX)] + SHORTENED_SUFFIX + return text + + class TextPullUi(PullUi): def __init__(self): self._renames = defaultdict(list) @@ -53,16 +91,20 @@ def resolveBothModifiedObject(self, obj): printConflictObjectHeader(obj) for key in set(obj.conflictingKeys): oldValue = obj.ancestor[key] - print("\nConflict on \"{}\" key. Old value was \"{}\".\n".format(key, oldValue)) + print("\nConflict on \"{}\" key. Old value was \"{}\".\n".format(key, shortenText(oldValue))) answers = ( - (1, "Local value: \"{}\"".format(obj.local[key])), - (2, "Remote value: \"{}\"".format(obj.remote[key])) + (1, "Local value: \"{}\"".format(shortenText(obj.local[key]))), + (2, "Remote value: \"{}\"".format(shortenText(obj.remote[key]))), + (3, "Edit"), ) answer = tui.selectFromList(answers, prompt="Which version do you want to keep".format(key), default=None) if answer == 1: value = obj.local[key] - else: + elif answer == 2: value = obj.remote[key] + else: + conflictText = prepareConflictText(obj.local[key], obj.remote[key]) + value = tui.editText(conflictText) obj.selectValue(key, value) def resolveModifiedDeletedObject(self, obj):