[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