[Moin-devel] Merging edit conflicts

Florian Festi festifn at rupert.informatik.uni-stuttgart.de
Fri Feb 14 04:45:04 EST 2003


After all this discussion about edit lock, I had to try if there isn't a
better solution. Here is how far I got.

If an edit conflict occures the two changes are merged and the editor is
send back to the user with a message that someone else edited the page
meanwhile. If edit conflicts could not solved both versions are included
seperated by lines (quite patch like). If this seams to be too complicated
for the user it is possible to send the editor only if there are no
unresolvable conflicts and show the "Someone was faster than you!" message
otherwise.

In addition the message shown above the editor contains a link to the diff
that shows what has been change meanwhile.

The patch is against the CVS. patch.py has to be copied to MoinMoin/util/

	cu

		Florian Festi
-------------- next part --------------
--- ../../moin/MoinMoin/PageEditor.py	Thu Feb 13 13:53:41 2003
+++ PageEditor.py	Fri Feb 14 13:53:57 2003
@@ -35,6 +35,7 @@
             Keywords:
                 preview - if given, show this text in preview mode
                 comment - comment field (when preview is true)
+                msg - if given, display message in header area
         """
         from cgi import escape
         try:
@@ -68,10 +69,13 @@
             title = self._('Preview of "%(pagename)s"')
             self.set_raw_body(preview.replace("\r", ""))
 
+        msg = kw.get('msg', None) 
+
         # send header stuff
         wikiutil.send_title(self.request, 
             title % {'pagename': self.split_title(),},
-            pagename=self.page_name, body_class="editor")
+            pagename=self.page_name, body_class="editor",
+            msg=msg)
 
         # get request parameters
         try:
@@ -465,8 +469,29 @@
             msg = self._('<b>Page is immutable!</b>')
         elif not newtext:
             msg = self._("""<b>You cannot save empty pages.</b>""")
-        elif datestamp != '0' and datestamp != str(os.path.getmtime(self._text_filename())):
-            msg = self._("""<b>Sorry, someone else saved the page while you edited it.
+        elif newtext == self.get_raw_body():
+            msg = self._("""<b>You did not change the page content, not saved!</b>""")
+        # Edit conflict
+        if datestamp != '0' and datestamp != str(os.path.getmtime(self._text_filename())):
+            import difflib
+            from MoinMoin.util import patch
+
+            allow_conflicts = 1
+
+            # calculate merged content
+            oldtext = Page(self.page_name, date=datestamp).get_raw_body()
+            actualtext = self.get_raw_body()
+            false = lambda x : None
+            d = difflib.Differ(false, false)
+            diff = list(d.compare(oldtext, newtext))
+            verynewtext = patch.apply_patch(actualtext, diff, allow_conflicts,
+                           '----- /!\ Edit conflict! Other version: -----\n',
+                           '----- /!\ Edit conflict! Your version: -----\n',
+                           '----- /!\ End of edit conflict -----\n')
+
+            # found conflicts between versions but that was not allowd
+            if not verynewtext:
+                msg = self._("""<b>Sorry, someone else saved the page while you edited it.
 <p>Please do the following: Use the back button of your browser, and cut&paste
 your changes from there. Then go forward to here, and click EditText again.
 Now re-add your changes to the current page contents.
@@ -475,16 +500,28 @@
 delete the changes of the other person, which is excessively rude!</em></b>
 """)
 
-            if backup_url:
-                msg += self._('<p><b>A backup of your changes is'
-                    ' <a href="%(backup_url)s">here</a>.</b></p>') % locals()
-        elif newtext == self.get_raw_body():
-            msg = self._("""<b>You did not change the page content, not saved!</b>""")
-
+                if backup_url:
+                    msg += self._('<p><b>A backup of your changes is'
+                                  ' <a href="%(backup_url)s">here</a>.</b></p>') % locals()
+                self.request.reset()
+                self.send_page(self.request, msg=msg)
+                return
+            # merged version
+            else:
+                msg = self._("""Edit conflict!
+<p>Please review page and save then. Do not save this page as it is!</p>
+<p>Have a look at the diff of %(difflink)s to see what have been changed.</p> 
+""") % {'difflink':self.link_to(querystr='action=diff&date=' + datestamp)}
+                verynewtext = ''.join(verynewtext)
+                self.request.form['datestamp'].value = os.path.getmtime(self._text_filename())
+                self.sendEditor(msg=msg, comment=kw.get('comment', ''),
+                                preview=verynewtext)
+                return
         # save only if no error occured (msg is empty)
         if not msg:
             # set success msg
-            msg = self._("""<b>Thank you for your changes.
+            if not msg:
+                msg = self._("""<b>Thank you for your changes.
 Your attention to detail is appreciated.</b>""")
 
             # write the page file
@@ -503,6 +540,7 @@
             # send notification mails
             if config.mail_smarthost and kw.get('notify', 0):
                 msg = msg + "<p>" + self._notifySubscribers(kw.get('comment', ''))
+            self.request.reset()
+            self.send_page(self.request, msg=msg)
 
-        return msg
 
--- ../../moin/MoinMoin/wikiaction.py	Thu Feb 13 13:53:42 2003
+++ wikiaction.py	Fri Feb 14 00:14:42 2003
@@ -569,8 +569,8 @@
     else:
         savemsg = pg.saveText(savetext, datestamp,
             stripspaces=rstrip, notify=notify, comment=comment)
-        request.reset()
-        pg.send_page(request, msg=savemsg)
+        #request.reset()
+        #pg.send_page(request, msg=savemsg)
         ## webapi.http_redirect(request, pg.url())
 
 
-------------- next part --------------
#!/usr/bin/python2

"""
   Applys diffs generated by list(difflib.Differ.compare())
   text must be a list of strings
   
   Copyright (c) 2003 Florian Festi
   All rights reserved, see COPYING for details.
"""



import difflib

def apply_patch(text, patch, allow_collisions=1,
                marker1 = '<-----------------------------------------\n',
                marker2 = '------------------------------------------\n',
                marker3 = '>-----------------------------------------\n'):
    """
    
    """
    text_len = len(text)
    patch_len = len(patch)

    text_nr = 0
    patch_nr = 0

    result = []

    while ((text_nr < text_len) and (patch_nr < patch_len)):
        if patch[patch_nr][0] == '?':
            patch_nr = patch_nr + 1
        # matching line
        elif ((patch[patch_nr][0] == ' ') and
              (patch[patch_nr][2:] == text[text_nr])):
            result.append(text[text_nr])
            text_nr = text_nr + 1
            patch_nr = patch_nr + 1
        # deleted line
        elif (patch[patch_nr][0] == '-' and
              patch[patch_nr][2:] == text[text_nr]):
            text_nr = text_nr + 1
            patch_nr = patch_nr + 1
        # not matching line
        elif (patch[patch_nr][0] in ['-', ' ']):
            (text_idx, patch_idx, count) = find_match(text, patch,
                                                      text_nr, patch_nr,
                                                      min_count = 5)
            # incusion in text
            if patch_idx == patch_nr:
                result = result + text[text_nr:text_idx]
                text_nr = text_idx
            # deletion in text
            elif ((text_idx == text_nr) and
                  unchanged(patch, patch_nr, patch_idx)):
                patch_nr = patch_idx
            # collision
            else:
                if not allow_collisions: return None
                result.append(marker1)
                result.extend(text[text_nr:text_idx])
                result.append(marker2)
                result.extend(restore(patch,patch_nr, patch_idx))
                result.append(marker3)
                text_nr = text_idx
                patch_nr = patch_idx
        # new line
        elif (patch[patch_nr][0] == '+'):
            result.append(patch[patch_nr][2:])
            patch_nr = patch_nr + 1
        else: raise ValueError # wrong character in patch

    # additional lines in text
    if text_nr < text_len:
        result.extend(text[text_nr:])
    if patch_nr < patch_len:
        # conflict in last lines which are deleted in text
        if not unchanged(patch, patch_nr, pathc_len):
            result.extend([marker1, marker2])
            result.extend(restore(patch, patch_nr, patch_len, which=2))
            result.append(marker3)
         #else: last lines deleted in text
         
    return result

def next(patch, nr, which = 2):
    """ returns the next index for thew next line
        which == 1 -> old version
        which == 2 -> new version
    """
    if which == 2: wanted = [' ', '+']
    elif which == 1: wanted = [' ', '-']
    else: raise IndexError
    nr = nr + 1
    while (nr < len(patch)):
        if patch[nr][0] in wanted : return nr
        nr = nr + 1
    return nr

def restore(patch, from_, to, which=2):
    """ returns the text generated by patch[_from:to]
        which == 1 -> old version
        which == 2 -> new version                
    """
    result = []
    i = from_
    if to > len(patch): to = len(patch)
    while i < to:
        result.append(patch[i][2:])
        i = next(patch, i, which)
    return result

def match(text, patch, text_nr = 0, patch_nr=0, which = 1, max = 0):
    """ returns how many lines between text and patch do match
        following the given positions
        if max>0: stop after max matches 
    """
    count = 0
    text_len = len(text)
    patch_len = len(patch)            
    while ((text_nr < text_len) and (patch_nr < patch_len) and
           (max and count < max)):
        if text[text_nr] != patch[patch_nr][2:]: return count
        text_nr = text_nr +1
        patch_nr = next(patch, patch_nr, which)
        count = count + 1
    return count

def unchanged(patch, first, last):
    """
    return if the patch[first:last] does change anything 
    """
    i = first
    patch_len = len(patch)
    while i < last:
        if patch[i][0] in ['+','-']: return 0
        i = i + 1
    return 1

def find_match(text, patch, text_nr, patch_nr, min_count = 3):
    """
    Look for the next position where text and patch match after
    the given positions. min_count matching lines are needed for a valid
    match
    """
    text_len = len(text)
    patch_len = len(patch)
    hit1 = None
    hit2 = None
    text_idx = text_nr
    patch_idx = patch_nr
    while ((text_idx < text_len) and (patch_idx < patch_len)):
        i =  text_nr
        while i <= text_idx:
            hit_count = match(text, patch, i, patch_idx, which=1, max=10)
            if hit_count >= min_count:
                hit1 = (i, patch_idx, hit_count)
                break
            i = i + 1
        i = patch_nr
        while i < patch_idx:
            hit_count = match(text, patch, text_idx, i, which=1,  max=10) 
            if hit_count >= min_count:
                hit2 = (text_idx, i, hit_count)
                break
            else: hit_count = 0
            i = next(patch, i)

        if hit1 or hit2: break
        text_idx = text_idx + 1
        patch_idx = next(patch, patch_idx)

    if hit1 and hit2:
        #XXXX which one?
        return hit1
    elif hit1: return hit1
    elif hit2: return hit2
    else: return (text_len, patch_len, 0)

def main():
    from pprint import pprint

    text0 = """AAA 001
AAA 002
AAA 003
AAA 004
AAA 005
AAA 006
AAA 007
AAA 008
AAA 009
AAA 010""".split('\n')

    text1 = """AAA 001
AAA 002
AAA 005
AAA 006
AAA 007
AAA 008
BBB 001
BBB 002
AAA 009
AAA 010
BBB 003""".split('\n')

    text2 = """AAA 001
AAA 002
AAA 003
AAA 004
AAA 005
AAA 006
AAA 007
AAA 008
CCC 001
CCC 002
CCC 003
CCC 004""".split('\n')

    print text1

    d = difflib.Differ()

    diff = list(d.compare(text0, text1))

    # pprint(diff)
    # print '================================================================='

    text = apply_patch(text2, diff)
    pprint(text)

if __name__ == '__main__': main()


More information about the Moin-devel mailing list