def mergeEditConflict(self, origrev): """ Try to merge current page version with new version the user tried to save @param origrev: the original revision the user was editing @rtype: bool @return: merge success status """ from MoinMoin.util import diff3 allow_conflicts = 1 # Get current editor text savetext = self.get_raw_body() # The original text from the revision the user was editing original_text = Page(self.request, self.page_name, rev=origrev).get_raw_body() # The current revision someone else saved saved_text = Page(self.request, self.page_name).get_raw_body() # And try to merge all into one with edit conflict separators verynewtext = diff3.text_merge(original_text, saved_text, savetext, allow_conflicts, "\n---- /!\ '''Edit conflict - other version:''' ----\n", "\n---- /!\ '''Edit conflict - your version:''' ----\n", "\n---- /!\ '''End of edit conflict''' ----\n") if verynewtext: self.set_raw_body(verynewtext) return True return False
def run(yielder, sp): # XXX add locking, acquire read-lock on sp if debug: self.log_status(ActionClass.INFO, raw_suffix="Processing %r" % sp) local_pagename = sp.local_name if not self.request.user.may.write(local_pagename): self.log_status(ActionClass.WARN, _("Skipped page %s because of no write access to local page."), (local_pagename, )) return current_page = PageEditor(self.request, local_pagename) # YYY direct access comment = u"Local Merge - %r" % (remote.get_interwiki_name() or remote.get_iwid()) tags = TagStore(current_page) matching_tags = tags.fetch(iwid_full=remote.iwid_full, direction=match_direction) matching_tags.sort() if debug: self.log_status(ActionClass.INFO, raw_suffix="Tags: %r <<BR>> All: %r" % (matching_tags, tags.tags)) # some default values for non matching tags normalised_name = None remote_rev = None local_rev = sp.local_rev # merge against the newest version old_contents = "" if matching_tags: newest_tag = matching_tags[-1] local_change = newest_tag.current_rev != sp.local_rev remote_change = newest_tag.remote_rev != sp.remote_rev # handle some cases where we cannot continue for this page if not remote_change and (direction == DOWN or not local_change): return # no changes done, next page if sp.local_deleted and sp.remote_deleted: return if sp.remote_deleted and not local_change: msg = local.delete_page(sp.local_name, comment) if not msg: self.log_status(ActionClass.INFO, _("Deleted page %s locally."), (sp.name, )) else: self.log_status(ActionClass.ERROR, _("Error while deleting page %s locally:"), (sp.name, ), msg) return if sp.local_deleted and not remote_change: if direction == DOWN: return yield remote.delete_page_pre(sp.remote_name, sp.remote_rev, local_full_iwid) msg = remote.delete_page_post(yielder.fetch_result()) if not msg: self.log_status(ActionClass.INFO, _("Deleted page %s remotely."), (sp.name, )) else: self.log_status(ActionClass.ERROR, _("Error while deleting page %s remotely:"), (sp.name, ), msg) return if sp.local_mime_type != MIMETYPE_MOIN and not (local_change ^ remote_change): self.log_status(ActionClass.WARN, _("The item %s cannot be merged automatically but was changed in both wikis. Please delete it in one of both wikis and try again."), (sp.name, )) return if sp.local_mime_type != sp.remote_mime_type: self.log_status(ActionClass.WARN, _("The item %s has different mime types in both wikis and cannot be merged. Please delete it in one of both wikis or unify the mime type, and try again."), (sp.name, )) return if newest_tag.normalised_name != sp.name: self.log_status(ActionClass.WARN, _("The item %s was renamed locally. This is not implemented yet. Therefore the full synchronisation history is lost for this page."), (sp.name, )) # XXX implement renames else: normalised_name = newest_tag.normalised_name local_rev = newest_tag.current_rev remote_rev = newest_tag.remote_rev old_contents = Page(self.request, local_pagename, rev=newest_tag.current_rev).get_raw_body_str() # YYY direct access else: if (sp.local_deleted and not sp.remote_rev) or ( sp.remote_deleted and not sp.local_rev): return self.log_status(ActionClass.INFO, _("Synchronising page %s with remote page %s ..."), (local_pagename, sp.remote_name)) if direction == DOWN: remote_rev = None # always fetch the full page, ignore remote conflict check patch_base_contents = "" else: patch_base_contents = old_contents # retrieve remote contents diff if remote_rev != sp.remote_rev: if sp.remote_deleted: # ignore remote changes current_remote_rev = sp.remote_rev is_remote_conflict = False diff = None self.log_status(ActionClass.WARN, _("The page %s was deleted remotely but changed locally."), (sp.name, )) else: yield remote.get_diff_pre(sp.remote_name, remote_rev, None, normalised_name) diff_result = remote.get_diff_post(yielder.fetch_result()) if diff_result is None: self.log_status(ActionClass.ERROR, _("The page %s could not be synced. The remote page was renamed. This is not supported yet. You may want to delete one of the pages to get it synced."), (sp.remote_name, )) return is_remote_conflict = diff_result["conflict"] assert diff_result["diffversion"] == 1 diff = diff_result["diff"] current_remote_rev = diff_result["current"] else: current_remote_rev = remote_rev if sp.local_mime_type == MIMETYPE_MOIN: is_remote_conflict = wikiutil.containsConflictMarker(old_contents.decode("utf-8")) else: is_remote_conflict = NotImplemented diff = None # do not sync if the conflict is remote and local, or if it is local # and the page has never been synchronised if (sp.local_mime_type == MIMETYPE_MOIN and wikiutil.containsConflictMarker(current_page.get_raw_body()) # YYY direct access and (remote_rev is None or is_remote_conflict)): self.log_status(ActionClass.WARN, _("Skipped page %s because of a locally or remotely unresolved conflict."), (local_pagename, )) return if remote_rev is None and direction == BOTH: self.log_status(ActionClass.INFO, _("This is the first synchronisation between the local and the remote wiki for the page %s."), (sp.name, )) # calculate remote page contents from diff if sp.remote_deleted: remote_contents = "" elif diff is None: remote_contents = old_contents else: remote_contents = patch(patch_base_contents, decompress(diff)) if diff is None: # only a local change if debug: self.log_status(ActionClass.INFO, raw_suffix="Only local changes for %r" % sp.name) merged_text_raw = current_page.get_raw_body_str() if sp.local_mime_type == MIMETYPE_MOIN: merged_text = merged_text_raw.decode("utf-8") elif local_rev == sp.local_rev: if debug: self.log_status(ActionClass.INFO, raw_suffix="Only remote changes for %r" % sp.name) merged_text_raw = remote_contents if sp.local_mime_type == MIMETYPE_MOIN: merged_text = merged_text_raw.decode("utf-8") else: # this is guaranteed by a check above assert sp.local_mime_type == MIMETYPE_MOIN remote_contents_unicode = remote_contents.decode("utf-8") # here, the actual 3-way merge happens merged_text = diff3.text_merge(old_contents.decode("utf-8"), remote_contents_unicode, current_page.get_raw_body(), 1, *conflict_markers) # YYY direct access if debug: self.log_status(ActionClass.INFO, raw_suffix="Merging %r, %r and %r into %r" % (old_contents.decode("utf-8"), remote_contents_unicode, current_page.get_raw_body(), merged_text)) merged_text_raw = merged_text.encode("utf-8") # generate binary diff diff = textdiff(remote_contents, merged_text_raw) if debug: self.log_status(ActionClass.INFO, raw_suffix="Diff against %r" % remote_contents) # XXX upgrade to write lock try: local_change_done = True current_page.saveText(merged_text, sp.local_rev or 0, comment=comment) # YYY direct access except PageEditor.Unchanged: local_change_done = False except PageEditor.EditConflict: local_change_done = False assert False, "You stumbled on a problem with the current storage system - I cannot lock pages" new_local_rev = current_page.get_real_rev() # YYY direct access def rollback_local_change(): # YYY direct local access comment = u"Wikisync rollback" rev = new_local_rev - 1 revstr = '%08d' % rev oldpg = Page(self.request, sp.local_name, rev=rev) pg = PageEditor(self.request, sp.local_name) if not oldpg.exists(): pg.deletePage(comment) else: try: savemsg = pg.saveText(oldpg.get_raw_body(), 0, comment=comment, extra=revstr, action="SAVE/REVERT") except PageEditor.Unchanged: pass return sp.local_name if local_change_done: self.register_rollback(rollback_local_change) if direction == BOTH: yield remote.merge_diff_pre(sp.remote_name, compress(diff), new_local_rev, current_remote_rev, current_remote_rev, local_full_iwid, sp.name) try: very_current_remote_rev = remote.merge_diff_post(yielder.fetch_result()) except NotAllowedException: self.log_status(ActionClass.ERROR, _("The page %s could not be merged because you are not allowed to modify the page in the remote wiki."), (sp.name, )) return else: very_current_remote_rev = current_remote_rev if local_change_done: self.remove_rollback(rollback_local_change) # this is needed at least for direction both and cgi sync to standalone for immutable pages on both # servers. It is not needed for the opposite direction try: tags.add(remote_wiki=remote_full_iwid, remote_rev=very_current_remote_rev, current_rev=new_local_rev, direction=direction, normalised_name=sp.name) except: self.log_status(ActionClass.ERROR, _("The page %s could not be merged because you are not allowed to modify the page in the remote wiki."), (sp.name, )) return if sp.local_mime_type != MIMETYPE_MOIN or not wikiutil.containsConflictMarker(merged_text): self.log_status(ActionClass.INFO, _("Page %s successfully merged."), (sp.name, )) elif is_remote_conflict: self.log_status(ActionClass.WARN, _("Page %s contains conflicts that were introduced on the remote side."), (sp.name, )) else: self.log_status(ActionClass.WARN, _("Page %s merged with conflicts."), (sp.name, ))
def testTextMerge(self): """ util.diff3.text_merge: test correct merging """ in1 = """AAA 001 AAA 002 AAA 003 AAA 004 AAA 005 AAA 006 AAA 007 AAA 008 AAA 009 AAA 010 AAA 011 AAA 012 AAA 013 AAA 014 """ in2 = """AAA 001 AAA 002 AAA 005 AAA 006 AAA 007 AAA 008 BBB 001 BBB 002 AAA 009 AAA 010 BBB 003 """ in3 = """AAA 001 AAA 002 AAA 003 AAA 004 AAA 005 AAA 006 AAA 007 AAA 008 CCC 001 CCC 002 CCC 003 AAA 012 AAA 013 AAA 014 """ result = diff3.text_merge(in1, in2, in3) expected = """AAA 001 AAA 002 AAA 005 AAA 006 AAA 007 AAA 008 <<<<<<<<<<<<<<<<<<<<<<<<< BBB 001 BBB 002 AAA 009 AAA 010 BBB 003 ========================= CCC 001 CCC 002 CCC 003 AAA 012 AAA 013 AAA 014 >>>>>>>>>>>>>>>>>>>>>>>>> """ assert result == expected, ('Expected "%(expected)s" but got "%(result)s"') % locals()
def testTextMerge(self): """ util.diff3.text_merge: test correct merging """ in1 = """AAA 001 AAA 002 AAA 003 AAA 004 AAA 005 AAA 006 AAA 007 AAA 008 AAA 009 AAA 010 AAA 011 AAA 012 AAA 013 AAA 014 """ in2 = """AAA 001 AAA 002 AAA 005 AAA 006 AAA 007 AAA 008 BBB 001 BBB 002 AAA 009 AAA 010 BBB 003 """ in3 = """AAA 001 AAA 002 AAA 003 AAA 004 AAA 005 AAA 006 AAA 007 AAA 008 CCC 001 CCC 002 CCC 003 AAA 012 AAA 013 AAA 014 """ result = diff3.text_merge(in1, in2, in3) expected = """AAA 001 AAA 002 AAA 005 AAA 006 AAA 007 AAA 008 <<<<<<<<<<<<<<<<<<<<<<<<< BBB 001 BBB 002 AAA 009 AAA 010 BBB 003 ========================= CCC 001 CCC 002 CCC 003 AAA 012 AAA 013 AAA 014 >>>>>>>>>>>>>>>>>>>>>>>>> """ assert result == expected, 'Expected "%(expected)s" but got "%(result)s"' % locals()