The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env python
#
#  revert_tests.py:  testing 'svn revert'.
#
#  Subversion is a tool for revision control.
#  See http://subversion.apache.org for more information.
#
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
######################################################################

# General modules
import re, os, stat, shutil

# Our testing module
import svntest
from svntest import wc, main, actions
from svntest.actions import run_and_verify_svn
from svntest.main import file_append, file_write, run_svn

# (abbreviation)
Skip = svntest.testcase.Skip_deco
SkipUnless = svntest.testcase.SkipUnless_deco
XFail = svntest.testcase.XFail_deco
Issues = svntest.testcase.Issues_deco
Issue = svntest.testcase.Issue_deco
Wimp = svntest.testcase.Wimp_deco
Item = svntest.wc.StateItem


######################################################################
# Helpers

def revert_replacement_with_props(sbox, wc_copy):
  """Helper implementing the core of
  revert_{repos,wc}_to_wc_replace_with_props().

  Uses a working copy (when wc_copy == True) or a URL (when wc_copy ==
  False) source to copy from."""

  sbox.build()
  wc_dir = sbox.wc_dir

  # Use a temp file to set properties with wildcards in their values
  # otherwise Win32/VS2005 will expand them
  prop_path = os.path.join(wc_dir, 'proptmp')
  svntest.main.file_append(prop_path, '*')

  # Set props on file which is copy-source later on
  pi_path = os.path.join(wc_dir, 'A', 'D', 'G', 'pi')
  rho_path = os.path.join(wc_dir, 'A', 'D', 'G', 'rho')
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'ps', 'phony-prop', '-F', prop_path,
                                     pi_path)
  os.remove(prop_path)
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'ps', 'svn:eol-style', 'LF', rho_path)

  # Verify props having been set
  expected_disk = svntest.main.greek_state.copy()
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_disk.tweak('A/D/G/pi',
                      props={ 'phony-prop': '*' })
  expected_disk.tweak('A/D/G/rho',
                      props={ 'svn:eol-style': 'LF' })

  actual_disk = svntest.tree.build_tree_from_wc(wc_dir, 1)
  svntest.tree.compare_trees("disk", actual_disk, expected_disk.old_tree())

  # Commit props
  expected_output = svntest.wc.State(wc_dir, {
    'A/D/G/pi':  Item(verb='Sending'),
    'A/D/G/rho': Item(verb='Sending'),
    })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/D/G/pi',  wc_rev='2')
  expected_status.tweak('A/D/G/rho', wc_rev='2')
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

  # Bring wc into sync
  svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir)

  # File scheduled for deletion
  svntest.actions.run_and_verify_svn(None, None, [], 'rm', rho_path)

  # Status before attempting copies
  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.tweak('A/D/G/rho', status='D ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # The copy shouldn't fail
  if wc_copy:
    pi_src = os.path.join(wc_dir, 'A', 'D', 'G', 'pi')
  else:
    pi_src = sbox.repo_url + '/A/D/G/pi'

  svntest.actions.run_and_verify_svn(None, None, [],
                                     'cp', pi_src, rho_path)

  # Verify both content and props have been copied
  if wc_copy:
    props = { 'phony-prop' : '*' }
  else:
    props = { 'phony-prop' : '*' }

  expected_disk.tweak('A/D/G/rho',
                      contents="This is the file 'pi'.\n",
                      props=props)
  actual_disk = svntest.tree.build_tree_from_wc(wc_dir, 1)
  svntest.tree.compare_trees("disk", actual_disk, expected_disk.old_tree())

  # Now revert
  expected_status.tweak('A/D/G/rho', status='R ', copied='+', wc_rev='-')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  expected_status.tweak('A/D/G/rho', status='  ', copied=None, wc_rev='2')
  expected_output = ["Reverted '" + rho_path + "'\n"]
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', '-R', wc_dir)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Check disk status
  expected_disk = svntest.main.greek_state.copy()
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_disk.tweak('A/D/G/pi',
                      props={ 'phony-prop': '*' })
  expected_disk.tweak('A/D/G/rho',
                      props={ 'svn:eol-style': 'LF' })
  actual_disk = svntest.tree.build_tree_from_wc(wc_dir, 1)
  svntest.tree.compare_trees("disk", actual_disk, expected_disk.old_tree())




######################################################################
# Tests
#
#   Each test must return on success or raise on failure.


#----------------------------------------------------------------------

def revert_from_wc_root(sbox):
  "revert relative to wc root"

  sbox.build(read_only = True)
  wc_dir = sbox.wc_dir

  os.chdir(wc_dir)

  # Mostly taken from basic_revert
  # Modify some files and props.
  beta_path = os.path.join('A', 'B', 'E', 'beta')
  gamma_path = os.path.join('A', 'D', 'gamma')
  iota_path = 'iota'
  rho_path = os.path.join('A', 'D', 'G', 'rho')
  zeta_path = os.path.join('A', 'D', 'H', 'zeta')
  svntest.main.file_append(beta_path, "Added some text to 'beta'.\n")
  svntest.main.file_append(iota_path, "Added some text to 'iota'.\n")
  svntest.main.file_append(rho_path, "Added some text to 'rho'.\n")
  svntest.main.file_append(zeta_path, "Added some text to 'zeta'.\n")

  svntest.actions.run_and_verify_svn("Add command", None, [],
                                     'add', zeta_path)
  svntest.actions.run_and_verify_svn("Add prop command", None, [],
                                     'ps', 'random-prop', 'propvalue',
                                     gamma_path)
  svntest.actions.run_and_verify_svn("Add prop command", None, [],
                                     'ps', 'random-prop', 'propvalue',
                                     iota_path)
  svntest.actions.run_and_verify_svn("Add prop command", None, [],
                                     'ps', 'random-prop', 'propvalue',
                                     '.')
  svntest.actions.run_and_verify_svn("Add prop command", None, [],
                                     'ps', 'random-prop', 'propvalue',
                                     'A')

  # Verify modified status.
  expected_output = svntest.actions.get_virginal_state('', 1)
  expected_output.tweak('A/B/E/beta', 'A/D/G/rho', status='M ')
  expected_output.tweak('iota', status='MM')
  expected_output.tweak('', 'A/D/gamma', 'A', status=' M')
  expected_output.add({
    'A/D/H/zeta' : Item(status='A ', wc_rev=0),
    })

  svntest.actions.run_and_verify_status('', expected_output)

  # Run revert
  svntest.actions.run_and_verify_svn("Revert command", None, [],
                                     'revert', beta_path)

  svntest.actions.run_and_verify_svn("Revert command", None, [],
                                     'revert', gamma_path)

  svntest.actions.run_and_verify_svn("Revert command", None, [],
                                     'revert', iota_path)

  svntest.actions.run_and_verify_svn("Revert command", None, [],
                                     'revert', rho_path)

  svntest.actions.run_and_verify_svn("Revert command", None, [],
                                     'revert', zeta_path)

  svntest.actions.run_and_verify_svn("Revert command", None, [],
                                     'revert', '.')

  svntest.actions.run_and_verify_svn("Revert command", None, [],
                                     'revert', 'A')

  # Verify unmodified status.
  expected_output = svntest.actions.get_virginal_state('', 1)

  svntest.actions.run_and_verify_status('', expected_output)

@Issue(1663)
def revert_reexpand_keyword(sbox):
  "revert reexpands manually contracted keyword"

  # This is for issue #1663.  The bug is that if the only difference
  # between a locally modified working file and the base version of
  # same was that the former had a contracted keyword that would be
  # expanded in the latter, then 'svn revert' wouldn't notice the
  # difference, and therefore wouldn't revert.  And why wouldn't it
  # notice?  Since text bases are always stored with keywords
  # contracted, and working files are contracted before comparison
  # with text base, there would appear to be no difference when the
  # contraction is the only difference.  For most commands, this is
  # correct -- but revert's job is to restore the working file, not
  # the text base.

  sbox.build()
  wc_dir = sbox.wc_dir
  newfile_path = os.path.join(wc_dir, "newfile")
  unexpanded_contents = "This is newfile: $Rev$.\n"

  # Put an unexpanded keyword into iota.
  svntest.main.file_write(newfile_path, unexpanded_contents)

  # Commit, without svn:keywords property set.
  svntest.main.run_svn(None, 'add', newfile_path)
  svntest.main.run_svn(None,
                       'commit', '-m', 'r2', newfile_path)

  # Set the property and commit.  This should expand the keyword.
  svntest.main.run_svn(None, 'propset', 'svn:keywords', 'rev', newfile_path)
  svntest.main.run_svn(None,
                       'commit', '-m', 'r3', newfile_path)

  # Verify that the keyword got expanded.
  def check_expanded(path):
    fp = open(path, 'r')
    lines = fp.readlines()
    fp.close()
    if lines[0] != "This is newfile: $Rev: 3 $.\n":
      raise svntest.Failure

  check_expanded(newfile_path)

  # Now un-expand the keyword again.
  svntest.main.file_write(newfile_path, unexpanded_contents)

  # Revert the file.  The keyword should reexpand.
  svntest.main.run_svn(None, 'revert', newfile_path)

  # Verify that the keyword got re-expanded.
  check_expanded(newfile_path)

  # Ok, the first part of this test was written in 2004. We are now in 2011
  # and note that there is more to test:

  # If the recorded timestamp and size match the file then revert won't
  # reinstall the file as the file was not modified when last compared in
  # the repository normal form.
  #
  # The easiest way to get the information recorded would be calling cleanup,
  # because that 'repairs' the recorded information. But some developers
  # (including me) would call that cheating, so I just use a failed commit.

  # Un-expand the keyword again.
  svntest.main.file_write(newfile_path, unexpanded_contents)

  # And now we trick svn in ignoring the file on newfile_path
  newfile2_path = newfile_path + '2'
  svntest.main.file_write(newfile2_path, 'This is file 2')
  svntest.main.run_svn(None, 'add', newfile2_path)
  os.remove(newfile2_path)

  # This commit fails because newfile2_path is missing, but only after
  # we call svn_wc__internal_file_modified_p() on new_file.
  svntest.actions.run_and_verify_commit(wc_dir, None, None, "2' is scheduled"+
                                        " for addition, but is missing",
                                        newfile_path, newfile2_path,
                                        '-m', "Shouldn't be committed")

  # Revert the file.  The file is not reverted!
  svntest.actions.run_and_verify_svn(None, [], [], 'revert', newfile_path)


#----------------------------------------------------------------------
# Regression test for issue #1775:
# Should be able to revert a file with no properties i.e. no prop-base
@Issue(1775)
def revert_replaced_file_without_props(sbox):
  "revert a replaced file with no properties"

  sbox.build()
  wc_dir = sbox.wc_dir

  file1_path = os.path.join(wc_dir, 'file1')

  # Add a new file, file1, that has no prop-base
  svntest.main.file_append(file1_path, "This is the file 'file1' revision 2.")
  svntest.actions.run_and_verify_svn(None, None, [], 'add', file1_path)

  # commit file1
  expected_output = svntest.wc.State(wc_dir, {
    'file1' : Item(verb='Adding')
    })

  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'file1' : Item(status='  ', wc_rev=2),
    })

  svntest.actions.run_and_verify_commit(wc_dir, expected_output,
                                        expected_status, None, wc_dir)

  # delete file1
  svntest.actions.run_and_verify_svn(None, None, [], 'rm', file1_path)

  # test that file1 is scheduled for deletion.
  expected_status.tweak('file1', status='D ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # recreate and add file1
  svntest.main.file_append(file1_path, "This is the file 'file1' revision 3.")
  svntest.actions.run_and_verify_svn(None, None, [], 'add', file1_path)

  # Test to see if file1 is schedule for replacement
  expected_status.tweak('file1', status='R ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # revert file1
  svntest.actions.run_and_verify_svn(None, ["Reverted '" + file1_path + "'\n"],
                                     [], 'revert', file1_path)

  # test that file1 really was reverted
  expected_status.tweak('file1', status='  ', wc_rev=2)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

#----------------------------------------------------------------------
# Regression test for issue #876:
# svn revert of an svn move'd file does not revert the file
@XFail()
@Issue(876)
def revert_moved_file(sbox):
    "revert a moved file"

    sbox.build(read_only = True)
    wc_dir = sbox.wc_dir
    iota_path = os.path.join(wc_dir, 'iota')
    iota_path_moved = os.path.join(wc_dir, 'iota_moved')

    svntest.actions.run_and_verify_svn(None, None, [], 'mv', iota_path,
                                        iota_path_moved)
    expected_output = svntest.actions.get_virginal_state(wc_dir, 1)
    expected_output.tweak('iota', status='D ')
    expected_output.add({
      'iota_moved' : Item(status='A ', copied='+', wc_rev='-'),
    })
    svntest.actions.run_and_verify_status(wc_dir, expected_output)

    # now revert the file iota
    svntest.actions.run_and_verify_svn(None,
      ["Reverted '" + iota_path + "'\n"], [], 'revert', iota_path)

    # at this point, svn status on iota_path_moved should return nothing
    # since it should disappear on reverting the move, and since svn status
    # on a non-existent file returns nothing.

    svntest.actions.run_and_verify_svn(None, [], [],
                                      'status', '-v', iota_path_moved)


#----------------------------------------------------------------------
# Test for issue 2135
#
# It is like merge_file_replace (in merge_tests.py), but reverts file
# instead of commit.
@Issue(2135)
def revert_file_merge_replace_with_history(sbox):
  "revert a merge replacement of file with history"

  sbox.build()
  wc_dir = sbox.wc_dir

  # File scheduled for deletion
  rho_path = os.path.join(wc_dir, 'A', 'D', 'G', 'rho')
  svntest.actions.run_and_verify_svn(None, None, [], 'rm', rho_path)

  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/D/G/rho', status='D ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  expected_output = svntest.wc.State(wc_dir, {
    'A/D/G/rho': Item(verb='Deleting'),
    })

  expected_status.remove('A/D/G/rho')

  # Commit rev 2
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)
  # create new rho file
  svntest.main.file_write(rho_path, "new rho\n")

  # Add the new file
  svntest.actions.run_and_verify_svn(None, None, [], 'add', rho_path)

  # Commit revsion 3
  expected_status.add({
    'A/D/G/rho' : Item(status='A ', wc_rev='0')
    })
  svntest.actions.run_and_verify_status(wc_dir, expected_status)
  expected_output = svntest.wc.State(wc_dir, {
    'A/D/G/rho': Item(verb='Adding'),
    })

  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        None, None, wc_dir)

  # Update working copy
  expected_output = svntest.wc.State(wc_dir, {})
  expected_disk   = svntest.main.greek_state.copy()
  expected_disk.tweak('A/D/G/rho', contents='new rho\n' )
  expected_status.tweak(wc_rev='3')
  expected_status.tweak('A/D/G/rho', status='  ')

  svntest.actions.run_and_verify_update(wc_dir,
                                        expected_output,
                                        expected_disk,
                                        expected_status)

  # merge changes from r3:1
  expected_output = svntest.wc.State(wc_dir, {
    'A/D/G/rho': Item(status='R ')
    })
  expected_mergeinfo_output = svntest.wc.State(wc_dir, {
    '' : Item(status=' U')
    })
  expected_elision_output = svntest.wc.State(wc_dir, {
    '' : Item(status=' U')
    })
  expected_status.tweak('A/D/G/rho', status='R ', copied='+', wc_rev='-')
  expected_skip = wc.State(wc_dir, { })
  expected_disk.tweak('A/D/G/rho', contents="This is the file 'rho'.\n")
  svntest.actions.run_and_verify_merge(wc_dir, '3', '1',
                                       sbox.repo_url, None,
                                       expected_output,
                                       expected_mergeinfo_output,
                                       expected_elision_output,
                                       expected_disk,
                                       expected_status,
                                       expected_skip)

  # Now revert
  svntest.actions.run_and_verify_svn(None,
                                     None,
                                     [], 'revert', rho_path)

  # test that rho really was reverted
  expected_status.tweak('A/D/G/rho', copied=None, status='  ', wc_rev=3)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  actual_disk = svntest.tree.build_tree_from_wc(wc_dir, 1)
  expected_disk.tweak('A/D/G/rho', contents="new rho\n")
  svntest.tree.compare_trees("disk", actual_disk, expected_disk.old_tree())

  # Make sure the revert removed the copy from information.
  expected_infos = [
      { 'Copied' : None }
    ]
  svntest.actions.run_and_verify_info(expected_infos, rho_path)

def revert_wc_to_wc_replace_with_props(sbox):
  "revert svn cp PATH PATH replace file with props"

  revert_replacement_with_props(sbox, 1)

def revert_repos_to_wc_replace_with_props(sbox):
  "revert svn cp URL PATH replace file with props"

  revert_replacement_with_props(sbox, 0)

def revert_after_second_replace(sbox):
  "revert file after second replace"

  sbox.build(read_only = True)
  wc_dir = sbox.wc_dir

  # File scheduled for deletion
  rho_path = os.path.join(wc_dir, 'A', 'D', 'G', 'rho')
  svntest.actions.run_and_verify_svn(None, None, [], 'rm', rho_path)

  # Status before attempting copy
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/D/G/rho', status='D ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Replace file for the first time
  pi_src = os.path.join(wc_dir, 'A', 'D', 'G', 'pi')

  svntest.actions.run_and_verify_svn(None, None, [],
                                     'cp', pi_src, rho_path)

  expected_status.tweak('A/D/G/rho', status='R ', copied='+', wc_rev='-')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Now delete replaced file.
  svntest.actions.run_and_verify_svn(None, None, [], 'rm', '--force', rho_path)

  # Status should be same as after first delete
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/D/G/rho', status='D ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Replace file for the second time
  pi_src = os.path.join(wc_dir, 'A', 'D', 'G', 'pi')

  svntest.actions.run_and_verify_svn(None, None, [], 'cp', pi_src, rho_path)

  expected_status.tweak('A/D/G/rho', status='R ', copied='+', wc_rev='-')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Now revert
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'revert', '-R', wc_dir)

  # Check disk status
  expected_disk = svntest.main.greek_state.copy()
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  actual_disk = svntest.tree.build_tree_from_wc(wc_dir, 1)
  svntest.tree.compare_trees("disk", actual_disk, expected_disk.old_tree())


#----------------------------------------------------------------------
# Tests for issue #2517.
#
# Manual conflict resolution leads to spurious revert report.
@Issue(2517)
def revert_after_manual_conflict_resolution__text(sbox):
  "revert after manual text-conflict resolution"

  # Make two working copies
  sbox.build()
  wc_dir_1 = sbox.wc_dir
  wc_dir_2 = sbox.add_wc_path('other')
  svntest.actions.duplicate_dir(wc_dir_1, wc_dir_2)

  # Cause a (text) conflict
  iota_path_1 = os.path.join(wc_dir_1, 'iota')
  iota_path_2 = os.path.join(wc_dir_2, 'iota')
  svntest.main.file_write(iota_path_1, 'Modified iota text')
  svntest.main.file_write(iota_path_2, 'Conflicting iota text')
  svntest.main.run_svn(None,
                       'commit', '-m', 'r2', wc_dir_1)
  svntest.main.run_svn(None,
                       'update', wc_dir_2)

  # Resolve the conflict "manually"
  svntest.main.file_write(iota_path_2, 'Modified iota text')
  os.remove(iota_path_2 + '.mine')
  os.remove(iota_path_2 + '.r1')
  os.remove(iota_path_2 + '.r2')

  # Verify no output from status, diff, or revert
  svntest.actions.run_and_verify_svn(None, [], [], "status", wc_dir_2)
  svntest.actions.run_and_verify_svn(None, [], [], "diff", wc_dir_2)
  svntest.actions.run_and_verify_svn(None, [], [], "revert", "-R", wc_dir_2)

def revert_after_manual_conflict_resolution__prop(sbox):
  "revert after manual property-conflict resolution"

  # Make two working copies
  sbox.build()
  wc_dir_1 = sbox.wc_dir
  wc_dir_2 = sbox.add_wc_path('other')
  svntest.actions.duplicate_dir(wc_dir_1, wc_dir_2)

  # Cause a (property) conflict
  iota_path_1 = os.path.join(wc_dir_1, 'iota')
  iota_path_2 = os.path.join(wc_dir_2, 'iota')
  svntest.main.run_svn(None, 'propset', 'foo', '1', iota_path_1)
  svntest.main.run_svn(None, 'propset', 'foo', '2', iota_path_2)
  svntest.main.run_svn(None,
                       'commit', '-m', 'r2', wc_dir_1)
  svntest.main.run_svn(None,
                       'update', wc_dir_2)

  # Resolve the conflict "manually"
  svntest.main.run_svn(None, 'propset', 'foo', '1', iota_path_2)
  os.remove(iota_path_2 + '.prej')

  # Verify no output from status, diff, or revert
  svntest.actions.run_and_verify_svn(None, [], [], "status", wc_dir_2)
  svntest.actions.run_and_verify_svn(None, [], [], "diff", wc_dir_2)
  svntest.actions.run_and_verify_svn(None, [], [], "revert", "-R", wc_dir_2)

def revert_propset__dir(sbox):
  "revert a simple propset on a dir"

  sbox.build(read_only = True)
  wc_dir = sbox.wc_dir
  a_path = os.path.join(wc_dir, 'A')
  svntest.main.run_svn(None, 'propset', 'foo', 'x', a_path)
  expected_output = re.escape("Reverted '" + a_path + "'")
  svntest.actions.run_and_verify_svn(None, expected_output, [], "revert",
                                     a_path)

def revert_propset__file(sbox):
  "revert a simple propset on a file"

  sbox.build(read_only = True)
  wc_dir = sbox.wc_dir
  iota_path = os.path.join(wc_dir, 'iota')
  svntest.main.run_svn(None, 'propset', 'foo', 'x', iota_path)
  expected_output = re.escape("Reverted '" + iota_path + "'")
  svntest.actions.run_and_verify_svn(None, expected_output, [], "revert",
                                     iota_path)

def revert_propdel__dir(sbox):
  "revert a simple propdel on a dir"

  sbox.build()
  wc_dir = sbox.wc_dir
  a_path = os.path.join(wc_dir, 'A')
  svntest.main.run_svn(None, 'propset', 'foo', 'x', a_path)
  svntest.main.run_svn(None,
                       'commit', '-m', 'ps', a_path)
  svntest.main.run_svn(None, 'propdel', 'foo', a_path)
  expected_output = re.escape("Reverted '" + a_path + "'")
  svntest.actions.run_and_verify_svn(None, expected_output, [], "revert",
                                     a_path)

def revert_propdel__file(sbox):
  "revert a simple propdel on a file"

  sbox.build()
  wc_dir = sbox.wc_dir
  iota_path = os.path.join(wc_dir, 'iota')
  svntest.main.run_svn(None, 'propset', 'foo', 'x', iota_path)
  svntest.main.run_svn(None,
                       'commit', '-m', 'ps', iota_path)
  svntest.main.run_svn(None, 'propdel', 'foo', iota_path)
  expected_output = re.escape("Reverted '" + iota_path + "'")
  svntest.actions.run_and_verify_svn(None, expected_output, [], "revert",
                                     iota_path)

def revert_replaced_with_history_file_1(sbox):
  "revert a committed replace-with-history == no-op"

  sbox.build()
  wc_dir = sbox.wc_dir
  iota_path = os.path.join(wc_dir, 'iota')
  mu_path = os.path.join(wc_dir, 'A', 'mu')

  # Remember the original text of 'mu'
  exit_code, text_r1, err = svntest.actions.run_and_verify_svn(None, None, [],
                                                               'cat', mu_path)
  # delete mu and replace it with a copy of iota
  svntest.main.run_svn(None, 'rm', mu_path)
  svntest.main.run_svn(None, 'mv', iota_path, mu_path)

  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/mu', status='  ', wc_rev=2)
  expected_status.remove('iota')
  expected_output = svntest.wc.State(wc_dir, {
    'iota': Item(verb='Deleting'),
    'A/mu': Item(verb='Replacing'),
    })
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

  # update the working copy
  svntest.main.run_svn(None, 'up', wc_dir)

  # now revert back to the state in r1
  expected_output = svntest.wc.State(wc_dir, {
    'A/mu': Item(status='R '),
    'iota': Item(status='A ')
    })
  expected_mergeinfo_output = svntest.wc.State(wc_dir, {
    '': Item(status=' U'),
    })
  expected_elision_output = svntest.wc.State(wc_dir, {
    '': Item(status=' U'),
    })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.tweak('A/mu', status='R ', copied='+', wc_rev='-')
  expected_status.tweak('iota', status='A ', copied='+', wc_rev='-')
  expected_skip = wc.State(wc_dir, { })
  expected_disk = svntest.main.greek_state.copy()
  svntest.actions.run_and_verify_merge(wc_dir, '2', '1',
                                       sbox.repo_url, None,
                                       expected_output,
                                       expected_mergeinfo_output,
                                       expected_elision_output,
                                       expected_disk,
                                       expected_status,
                                       expected_skip)

  # and commit in r3
  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.tweak('A/mu', status='  ', wc_rev=3)
  expected_status.tweak('iota', status='  ', wc_rev=3)
  expected_output = svntest.wc.State(wc_dir, {
    'iota': Item(verb='Adding'),
    'A/mu': Item(verb='Replacing'),
    })
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

  # Verify the content of 'mu'
  svntest.actions.run_and_verify_svn(None, text_r1, [], 'cat', mu_path)

  # situation: no local modifications, mu has its original content again.

  # revert 'mu' locally, shouldn't change a thing.
  svntest.actions.run_and_verify_svn(None, [], [], "revert",
                                     mu_path)

  # Verify the content of 'mu'
  svntest.actions.run_and_verify_svn(None, text_r1, [], 'cat', mu_path)

#----------------------------------------------------------------------
# Test for issue #2804.
@Issue(2804)
def status_of_missing_dir_after_revert(sbox):
  "status after schedule-delete, revert, and local rm"

  sbox.build(read_only = True)
  wc_dir = sbox.wc_dir
  A_D_G_path = os.path.join(wc_dir, "A", "D", "G")

  svntest.actions.run_and_verify_svn(None, None, [], "rm", A_D_G_path)
  expected_output = re.escape("Reverted '" + A_D_G_path + "'")
  svntest.actions.run_and_verify_svn(None, expected_output, [], "revert",
                                     A_D_G_path)

  deletes = [
     "D       " + os.path.join(A_D_G_path, "pi") + "\n",
     "D       " + os.path.join(A_D_G_path, "rho") + "\n",
     "D       " + os.path.join(A_D_G_path, "tau") + "\n"
  ]
  expected_output = svntest.verify.UnorderedOutput(deletes)
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     "status", wc_dir)

  svntest.main.safe_rmtree(A_D_G_path)

  expected_output = ["!       " + A_D_G_path + "\n"]

  if svntest.main.wc_is_singledb(wc_dir):
    expected_output.extend(deletes)

  expected_output = svntest.verify.UnorderedOutput(expected_output)

  svntest.actions.run_and_verify_svn(None, expected_output, [], "status",
                                     wc_dir)

  # When using single-db, we can get back to the virginal state.
  if svntest.main.wc_is_singledb(wc_dir):
    svntest.actions.run_and_verify_svn(None, None, [], "revert",
                                       "-R", A_D_G_path)

    expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
    svntest.actions.run_and_verify_status(wc_dir, expected_status)

#----------------------------------------------------------------------
# Test for issue #2804 with replaced directory
@Issue(2804)
def status_of_missing_dir_after_revert_replaced_with_history_dir(sbox):
  "status after replace+, revert, and local rm"

  sbox.build()
  wc_dir = sbox.wc_dir
  repo_url = sbox.repo_url

  # delete A/D/G and commit
  G_path = os.path.join(wc_dir, "A", "D", "G")
  svntest.actions.run_and_verify_svn(None, None, [], "rm", G_path)
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.remove('A/D/G', 'A/D/G/rho', 'A/D/G/pi', 'A/D/G/tau')
  expected_output = svntest.wc.State(wc_dir, {
    'A/D/G': Item(verb='Deleting'),
    })
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

  # copy A/D/G from A/B/E and commit
  E_path = os.path.join(wc_dir, "A", "B", "E")
  svntest.actions.run_and_verify_svn(None, None, [], "cp", E_path, G_path)
  expected_status.add({
    'A/D/G' : Item(status='  ', wc_rev='3'),
    'A/D/G/alpha' : Item(status='  ', wc_rev='3'),
    'A/D/G/beta' : Item(status='  ', wc_rev='3')
    })
  expected_output = svntest.wc.State(wc_dir, {
    'A/D/G': Item(verb='Adding'),
    })
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

  # update the working copy
  svntest.main.run_svn(None, 'up', wc_dir)

  # now rollback to r1, thereby reinstating the old 'G'
  expected_output = svntest.wc.State(wc_dir, {
    'A/D/G': Item(status='R '),
    'A/D/G/rho': Item(status='A '),
    'A/D/G/pi': Item(status='A '),
    'A/D/G/tau': Item(status='A '),
    })
  expected_mergeinfo_output = svntest.wc.State(wc_dir, {
    '': Item(status=' U'),
    })
  expected_elision_output = svntest.wc.State(wc_dir, {
    '': Item(status=' U'),
    })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 3)
  expected_status.tweak('A/D/G', status='R ', copied='+', wc_rev='-')
  expected_status.tweak('A/D/G/rho',
                        'A/D/G/pi',
                        'A/D/G/tau',
                        copied='+', wc_rev='-')
  expected_status.add({
    'A/D/G/alpha' : Item(status='D ', wc_rev='3'),
    'A/D/G/beta' : Item(status='D ', wc_rev='3'),
    })

  expected_skip = wc.State(wc_dir, { })
  expected_disk   = svntest.main.greek_state.copy()
  svntest.actions.run_and_verify_merge(wc_dir, '3', '1',
                                       sbox.repo_url, None,
                                       expected_output,
                                       expected_mergeinfo_output,
                                       expected_elision_output,
                                       expected_disk,
                                       expected_status,
                                       expected_skip,
                                       dry_run = 0)

  # now test if the revert works ok
  revert_paths = [G_path] + [os.path.join(G_path, child)
                             for child in ['alpha', 'beta', 'pi', 'rho', 'tau']]

  expected_output = svntest.verify.UnorderedOutput([
    "Reverted '%s'\n" % path for path in revert_paths])

  svntest.actions.run_and_verify_svn(None, expected_output, [], "revert", "-R",
                                     G_path)

  svntest.actions.run_and_verify_svn(None, [], [],
                                     "status", wc_dir)

  svntest.main.safe_rmtree(G_path)

  if svntest.main.wc_is_singledb(wc_dir):
    expected_output = svntest.verify.UnorderedOutput(
      ["!       " + G_path + "\n",
       "!       " + os.path.join(G_path, "alpha") + "\n",
       "!       " + os.path.join(G_path, "beta") + "\n"])
  else:
    expected_output = svntest.verify.UnorderedOutput(
      ["!       " + G_path + "\n"])
  svntest.actions.run_and_verify_svn(None, expected_output, [], "status",
                                     wc_dir)

# Test for issue #2928.
@Issue(2928)
def revert_replaced_with_history_file_2(sbox):
  "reverted replace with history restores checksum"

  sbox.build()
  wc_dir = sbox.wc_dir
  iota_path = os.path.join(wc_dir, 'iota')
  mu_path = os.path.join(wc_dir, 'A', 'mu')

  # Delete mu and replace it with a copy of iota
  svntest.main.run_svn(None, 'rm', mu_path)
  svntest.main.run_svn(None, 'cp', iota_path, mu_path)

  # Revert mu.
  svntest.main.run_svn(None, 'revert', mu_path)

  # If we make local mods to the reverted mu the commit will
  # fail if the checksum is incorrect.
  svntest.main.file_write(mu_path, "new text")
  expected_output = svntest.wc.State(wc_dir, {
    'A/mu': Item(verb='Sending'),
    })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/mu', status='  ', wc_rev=2)
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

#----------------------------------------------------------------------

def revert_tree_conflicts_in_updated_files(sbox):
  "revert tree conflicts in updated files"

  # See use cases 1-3 in notes/tree-conflicts/use-cases.txt for background.

  svntest.actions.build_greek_tree_conflicts(sbox)
  wc_dir = sbox.wc_dir
  G = os.path.join(wc_dir, 'A', 'D', 'G')
  G_pi  = os.path.join(G, 'pi');
  G_rho = os.path.join(G, 'rho');
  G_tau = os.path.join(G, 'tau');

  # Duplicate wc for tests
  wc_dir_2 =  sbox.add_wc_path('2')
  svntest.actions.duplicate_dir(wc_dir, wc_dir_2)
  G2 = os.path.join(wc_dir_2, 'A', 'D', 'G')
  G2_pi  = os.path.join(G2, 'pi');
  G2_rho = os.path.join(G2, 'rho');
  G2_tau = os.path.join(G2, 'tau');

  # Expectations
  expected_output = svntest.verify.UnorderedOutput(
   ["Reverted '%s'\n" % G_pi,
    "Reverted '%s'\n" % G_rho,
    "Reverted '%s'\n" % G_tau,
    ])

  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.tweak('A/D/G/pi',  status='  ')
  expected_status.remove('A/D/G/rho')
  expected_status.remove('A/D/G/tau')

  expected_disk = svntest.main.greek_state.copy()
  expected_disk.remove('A/D/G/rho')
  expected_disk.tweak('A/D/G/pi',
                      contents="This is the file 'pi'.\nIncoming edit.\n")
  expected_disk.remove('A/D/G/tau')

  # Revert individually in wc
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', G_pi, G_rho, G_tau)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)
  svntest.actions.verify_disk(wc_dir, expected_disk)

  # Expectations
  expected_output = svntest.verify.UnorderedOutput(
   ["Reverted '%s'\n" % G2_pi,
    "Reverted '%s'\n" % G2_rho,
    "Reverted '%s'\n" % G2_tau,
    ])

  expected_status.wc_dir = wc_dir_2

  # Revert recursively in wc 2
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', '-R', G2)
  svntest.actions.run_and_verify_status(wc_dir_2, expected_status)
  svntest.actions.verify_disk(wc_dir_2, expected_disk)

def revert_add_over_not_present_dir(sbox):
  "reverting an add over not present directory"

  sbox.build()
  wc_dir = sbox.wc_dir

  main.run_svn(None, 'rm', os.path.join(wc_dir, 'A/C'))
  main.run_svn(None, 'ci', wc_dir, '-m', 'Deleted dir')

  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.remove('A/C')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  main.run_svn(None, 'mkdir', os.path.join(wc_dir, 'A/C'))

  # This failed in some WC-NG intermediate format (r927318-r958992).
  main.run_svn(None, 'revert', os.path.join(wc_dir, 'A/C'))

  svntest.actions.run_and_verify_status(wc_dir, expected_status)


def revert_added_tree(sbox):
  "revert an added tree fails"

  sbox.build()
  wc_dir = sbox.wc_dir
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'mkdir', sbox.ospath('X'), sbox.ospath('X/Y'))
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'X'   : Item(status='A ', wc_rev=0),
    'X/Y' : Item(status='A ', wc_rev=0),
    })
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Revert is non-recursive and fails, status is unchanged
  expected_error = '.*Try \'svn revert --depth infinity\'.*'
  svntest.actions.run_and_verify_svn(None, None, expected_error,
                                     'revert', sbox.ospath('X'))
  svntest.actions.run_and_verify_status(wc_dir, expected_status)


@Issue(3834)
def revert_child_of_copy(sbox):
  "revert a child of a copied directory"

  sbox.build()
  wc_dir = sbox.wc_dir
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'cp',
                                     sbox.ospath('A/B/E'),
                                     sbox.ospath('A/B/E2'))


  svntest.main.file_append(sbox.ospath('A/B/E2/beta'), 'extra text\n')
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'A/B/E2'       : Item(status='A ', copied='+', wc_rev='-'),
    'A/B/E2/alpha' : Item(status='  ', copied='+', wc_rev='-'),
    'A/B/E2/beta'  : Item(status='M ', copied='+', wc_rev='-'),
    })
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # First revert removes text change, child is still copied
  expected_output = ["Reverted '%s'\n" % sbox.ospath('A/B/E2/beta')]
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', sbox.ospath('A/B/E2/beta'))
  expected_status.tweak('A/B/E2/beta', status='  ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Second revert of child does nothing, child is still copied
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'revert', sbox.ospath('A/B/E2/beta'))
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

@Issue(3783)
def revert_non_recusive_after_delete(sbox):
  "non-recursive revert after delete"

  sbox.build(read_only=True)
  wc_dir = sbox.wc_dir

  svntest.actions.run_and_verify_svn(None, None, [], 'rm', sbox.ospath('A/B'))
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/B', 'A/B/E', 'A/B/E/alpha', 'A/B/E/beta', 'A/B/F',
                        'A/B/lambda', status='D ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # This appears to work but gets the op-depth wrong
  expected_output = ["Reverted '%s'\n" % sbox.ospath('A/B')]
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', sbox.ospath('A/B'))
  expected_status.tweak('A/B', status='  ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  svntest.actions.run_and_verify_svn(None, None, [],
                                     'mkdir', sbox.ospath('A/B/E'))
  expected_status.tweak('A/B/E', status='R ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Since the op-depth was wrong A/B/E erroneously remains deleted
  expected_output = ["Reverted '%s'\n" % sbox.ospath('A/B/E')]
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', sbox.ospath('A/B/E'))
  expected_status.tweak('A/B/E', status='  ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

def revert_permissions_only(sbox):
  "permission-only reverts"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Helpers pinched/adapted from lock_tests.py.  Put them somewhere common?
  def check_writability(path, writable):
    bits = stat.S_IWGRP | stat.S_IWOTH | stat.S_IWRITE
    mode = os.stat(path)[0]
    if bool(mode & bits) != writable:
      raise svntest.Failure("path '%s' is unexpectedly %s (mode %o)"
                            % (path, ["writable", "read-only"][writable], mode))

  def is_writable(path):
    "Raise if PATH is not writable."
    check_writability(path, True)

  def is_readonly(path):
    "Raise if PATH is not readonly."
    check_writability(path, False)

  def check_executability(path, executable):
    bits = stat.S_IXGRP | stat.S_IXOTH | stat.S_IEXEC
    mode = os.stat(path)[0]
    if bool(mode & bits) != executable:
      raise svntest.Failure("path '%s' is unexpectedly %s (mode %o)"
                            % (path,
                               ["executable", "non-executable"][executable],
                               mode))

  def is_executable(path):
    "Raise if PATH is not executable."
    check_executability(path, True)

  def is_non_executable(path):
    "Raise if PATH is executable."
    check_executability(path, False)


  os.chmod(sbox.ospath('A/B/E/alpha'), 0444);  # read-only
  is_readonly(sbox.ospath('A/B/E/alpha'))
  expected_output = ["Reverted '%s'\n" % sbox.ospath('A/B/E/alpha')]
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', sbox.ospath('A/B/E/alpha'))
  is_writable(sbox.ospath('A/B/E/alpha'))

  if svntest.main.is_posix_os():
    os.chmod(sbox.ospath('A/B/E/beta'), 0777);   # executable
    is_executable(sbox.ospath('A/B/E/beta'))
    expected_output = ["Reverted '%s'\n" % sbox.ospath('A/B/E/beta')]
    svntest.actions.run_and_verify_svn(None, expected_output, [],
                                       'revert', sbox.ospath('A/B/E/beta'))
    is_non_executable(sbox.ospath('A/B/E/beta'))

  svntest.actions.run_and_verify_svn(None, None, [],
                                     'propset', 'svn:needs-lock', '1',
                                     sbox.ospath('A/B/E/alpha'))
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'propset', 'svn:executable', '1',
                                     sbox.ospath('A/B/E/beta'))

  expected_output = svntest.wc.State(wc_dir, {
    'A/B/E/alpha': Item(verb='Sending'),
    'A/B/E/beta':  Item(verb='Sending'),
    })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/B/E/alpha', wc_rev='2')
  expected_status.tweak('A/B/E/beta',  wc_rev='2')
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

  os.chmod(sbox.ospath('A/B/E/alpha'), 0666);  # not read-only
  is_writable(sbox.ospath('A/B/E/alpha'))
  expected_output = ["Reverted '%s'\n" % sbox.ospath('A/B/E/alpha')]
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', sbox.ospath('A/B/E/alpha'))
  is_readonly(sbox.ospath('A/B/E/alpha'))

  if svntest.main.is_posix_os():
    os.chmod(sbox.ospath('A/B/E/beta'), 0666);   # not executable
    is_non_executable(sbox.ospath('A/B/E/beta'))
    expected_output = ["Reverted '%s'\n" % sbox.ospath('A/B/E/beta')]
    svntest.actions.run_and_verify_svn(None, expected_output, [],
                                       'revert', sbox.ospath('A/B/E/beta'))
    is_executable(sbox.ospath('A/B/E/beta'))

@XFail()
@Issue(3851)
def revert_copy_depth_files(sbox):
  "revert a copy with depth=files"

  sbox.build(read_only=True)
  wc_dir = sbox.wc_dir

  svntest.actions.run_and_verify_svn(None, None, [],
                                     'copy',
                                     sbox.ospath('A/B/E'),
                                     sbox.ospath('A/B/E2'))

  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'A/B/E2'       : Item(status='A ', copied='+', wc_rev='-'),
    'A/B/E2/alpha' : Item(status='  ', copied='+', wc_rev='-'),
    'A/B/E2/beta'  : Item(status='  ', copied='+', wc_rev='-'),
    })
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  expected_output = svntest.verify.UnorderedOutput([
    "Reverted '%s'\n" % sbox.ospath(path) for path in ['A/B/E2',
                                                       'A/B/E2/alpha',
                                                       'A/B/E2/beta']])

  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', '--depth', 'files',
                                     sbox.ospath('A/B/E2'))

  expected_status.remove('A/B/E2', 'A/B/E2/alpha', 'A/B/E2/beta')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

@XFail()
@Issue(3851)
def revert_nested_add_depth_immediates(sbox):
  "revert a nested add with depth=immediates"

  sbox.build(read_only=True)
  wc_dir = sbox.wc_dir

  svntest.actions.run_and_verify_svn(None, None, [],
                                     'mkdir', '--parents', sbox.ospath('A/X/Y'))

  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'A/X'       : Item(status='A ', wc_rev='0'),
    'A/X/Y'     : Item(status='A ', wc_rev='0'),
    })
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  expected_output = svntest.verify.UnorderedOutput([
    "Reverted '%s'\n" % sbox.ospath(path) for path in ['A/X', 'A/X/Y']])

  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', '--depth', 'immediates',
                                     sbox.ospath('A/X'))

  expected_status.remove('A/X', 'A/X/Y')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

def create_superflous_actual_node(sbox):
  "create a superfluous actual node"

  sbox.build()
  wc_dir = sbox.wc_dir

  svntest.main.file_append(sbox.ospath('A/B/E/alpha'), 'their text\n')
  sbox.simple_commit()
  sbox.simple_update()

  # Create a NODES row with op-depth>0
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'copy', '-r', '1',
                                     sbox.repo_url + '/A/B/E/alpha',
                                     sbox.ospath('alpha'))

  # Merge to create an ACTUAL with a conflict
  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.add({
    'alpha' : Item(status='A ', copied='+', wc_rev='-'),
    })
  svntest.actions.run_and_verify_status(wc_dir, expected_status)
  svntest.main.file_append(sbox.ospath('alpha'), 'my text\n')
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'merge', '--accept', 'postpone',
                                     '^/A/B/E/alpha', sbox.ospath('alpha'))
  expected_status.tweak('alpha', status='CM', entry_status='A ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Clear merge property and remove conflict files
  sbox.simple_propdel('svn:mergeinfo', 'alpha')
  os.remove(sbox.ospath('alpha.merge-left.r1'))
  os.remove(sbox.ospath('alpha.merge-right.r2'))
  os.remove(sbox.ospath('alpha.working'))

  expected_status.tweak('alpha', status='A ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

@Issue(3859)
def revert_empty_actual(sbox):
  "revert with superfluous actual node"

  create_superflous_actual_node(sbox)
  wc_dir = sbox.wc_dir

  # Non-recursive code path works
  svntest.actions.run_and_verify_svn(None,
                                     ["Reverted '%s'\n" % sbox.ospath('alpha')],
                                     [],
                                     'revert', sbox.ospath('alpha'))

  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

@Issue(3859)
def revert_empty_actual_recursive(sbox):
  "recusive revert with superfluous actual node"

  create_superflous_actual_node(sbox)
  wc_dir = sbox.wc_dir

  # Recursive code path fails, the superfluous actual node suppresses the
  # notification
  svntest.actions.run_and_verify_svn(None,
                                     ["Reverted '%s'\n" % sbox.ospath('alpha')],
                                     [],
                                     'revert', '-R', sbox.ospath('alpha'))

  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

@Issue(3879)
def revert_tree_conflicts_with_replacements(sbox):
  "revert tree conflicts with replacements"

  sbox.build()
  wc_dir = sbox.wc_dir
  wc = sbox.ospath

  # Use case 1: local replace, incoming replace
  # A/mu
  # A/D/H --> A/D/H/chi, A/D/H/{loc,inc}_psi

  # Use case 2: local edit, incoming replace
  # A/D/gamma
  # A/D/G --> A/D/G/pi, A/D/G/inc_rho

  # Use case 3: local replace, incoming edit
  # A/B/lambda
  # A/B/E --> A/B/E/alpha, A/B/E/loc_beta

  # Case 1: incoming replacements
  sbox.simple_rm('A/mu', 'A/D/H')
  file_write(wc('A/mu'), "A fresh file.\n")
  os.mkdir(wc('A/D/H'))
  file_write(wc('A/D/H/chi'), "A fresh file.\n")
  file_write(wc('A/D/H/inc_psi'), "A fresh file.\n")
  sbox.simple_add('A/mu', 'A/D/H')

  # Case 2: incoming replacements
  sbox.simple_rm('A/D/gamma', 'A/D/G')
  file_write(wc('A/D/gamma'), "A fresh file.\n")
  os.mkdir(wc('A/D/G'))
  file_write(wc('A/D/G/pi'), "A fresh file.\n")
  file_write(wc('A/D/G/inc_rho'), "A fresh file.\n")
  sbox.simple_add('A/D/gamma','A/D/G')

  # Case 3: incoming edits
  file_append(wc('A/B/lambda'), "Incoming!\n")
  file_write(wc('A/B/E/alpha'), "Incoming!.\n")

  # Commit and roll back to r1.
  sbox.simple_commit()
  run_svn(None, 'up', wc_dir, '-r1', '-q')

  # Case 1: local replacements
  sbox.simple_rm('A/mu', 'A/D/H')
  file_write(wc('A/mu'), "A fresh file.\n")
  os.mkdir(wc('A/D/H'))
  file_write(wc('A/D/H/chi'), "A fresh local file.\n")
  file_write(wc('A/D/H/loc_psi'), "A fresh local file.\n")
  sbox.simple_add('A/mu', 'A/D/H')

  # Case 2: local edits
  file_append(wc('A/D/gamma'), "Local change.\n")
  file_append(wc('A/D/G/pi'), "Local change.\n")

  # Case 3: local replacements
  sbox.simple_rm('A/B/lambda', 'A/B/E')
  file_write(wc('A/B/lambda'), "A fresh local file.\n")
  os.mkdir(wc('A/B/E'))
  file_write(wc('A/B/E/alpha'), "A fresh local file.\n")
  file_write(wc('A/B/E/loc_beta'), "A fresh local file.\n")
  sbox.simple_add('A/B/lambda', 'A/B/E')

  # Update and check tree conflict status.
  run_svn(None, 'up', wc_dir)
  expected_status = svntest.wc.State(wc_dir, {
    ''                : Item(status='  ', wc_rev=2),
    'A'               : Item(status='  ', wc_rev=2),
    'A/B'             : Item(status='  ', wc_rev=2),
    'A/B/E'           : Item(status='R ', wc_rev=2, treeconflict='C'),
    'A/B/E/alpha'     : Item(status='A ', wc_rev='-'),
    'A/B/E/beta'      : Item(status='D ', wc_rev=2),
    'A/B/E/loc_beta'  : Item(status='A ', wc_rev='-'),
    'A/B/F'           : Item(status='  ', wc_rev=2),
    'A/B/lambda'      : Item(status='R ', wc_rev=2, treeconflict='C'),
    'A/C'             : Item(status='  ', wc_rev=2),
    'A/D'             : Item(status='  ', wc_rev=2),
    'A/D/G'           : Item(status='R ', wc_rev='-', copied='+',
                             treeconflict='C'),
    'A/D/G/inc_rho'   : Item(status='D ', wc_rev=2),
    'A/D/G/pi'        : Item(status='M ', wc_rev='-', copied='+'),
    'A/D/G/rho'       : Item(status='  ', wc_rev='-', copied='+'),
    'A/D/G/tau'       : Item(status='  ', wc_rev='-', copied='+'),
    'A/D/H'           : Item(status='R ', wc_rev=2, treeconflict='C'),
    'A/D/H/chi'       : Item(status='A ', wc_rev='-'),
    'A/D/H/inc_psi'   : Item(status='D ', wc_rev=2),
    'A/D/H/loc_psi'   : Item(status='A ', wc_rev='-'),
    'A/D/gamma'       : Item(status='R ', wc_rev='-', copied='+',
                             treeconflict='C'),
    'A/mu'            : Item(status='R ', wc_rev=2, treeconflict='C'),
    'iota'            : Item(status='  ', wc_rev=2),
    })
  svntest.actions.run_and_verify_unquiet_status(wc_dir, expected_status)

  def cd_and_status_u(dir_target):
    was_cwd = os.getcwd()
    os.chdir(os.path.abspath(wc(dir_target)))
    run_svn(None, 'status', '-u')
    os.chdir(was_cwd)

  cd_and_status_u('A')
  cd_and_status_u('A/D')

  # Until r1102143, the following 'status -u' commands failed with "svn:
  # E165004: Two top-level reports with no target".
  cd_and_status_u('A/D/G')
  cd_and_status_u('A/D/H')

  # Revert everything (i.e., accept "theirs-full").
  svntest.actions.run_and_verify_revert([
    wc('A/B/E'),
    wc('A/B/E/alpha'),   # incoming
    wc('A/B/E/beta'),
    wc('A/B/E/loc_beta'),
    wc('A/B/lambda'),
    wc('A/D/G'),
    wc('A/D/G/pi'),
    wc('A/D/G/inc_rho'), # incoming
    wc('A/D/G/rho'),
    wc('A/D/G/tau'),
    wc('A/D/H'),
    wc('A/D/H/chi'),
    wc('A/D/H/inc_psi'), # incoming
    wc('A/D/H/loc_psi'),
    wc('A/D/gamma'),
    wc('A/mu'),
    wc('A/B/E/alpha'),
    ], '-R', wc_dir)

  # Remove a few unversioned files that revert left behind.
  os.remove(wc('A/B/E/loc_beta'))
  os.remove(wc('A/D/H/loc_psi'))

  # The update operation should have put all incoming items in place.
  expected_status = svntest.wc.State(wc_dir, {
    ''                : Item(status='  ', wc_rev=2),
    'A'               : Item(status='  ', wc_rev=2),
    'A/B'             : Item(status='  ', wc_rev=2),
    'A/B/E'           : Item(status='  ', wc_rev=2),
    'A/B/E/alpha'     : Item(status='  ', wc_rev=2),
    'A/B/E/beta'      : Item(status='  ', wc_rev=2),
    'A/B/F'           : Item(status='  ', wc_rev=2),
    'A/B/lambda'      : Item(status='  ', wc_rev=2),
    'A/C'             : Item(status='  ', wc_rev=2),
    'A/D'             : Item(status='  ', wc_rev=2),
    'A/D/G'           : Item(status='  ', wc_rev=2),
    'A/D/G/inc_rho'   : Item(status='  ', wc_rev=2),
    'A/D/G/pi'        : Item(status='  ', wc_rev=2),
    'A/D/H'           : Item(status='  ', wc_rev=2),
    'A/D/H/chi'       : Item(status='  ', wc_rev=2),
    'A/D/H/inc_psi'   : Item(status='  ', wc_rev=2),
    'A/D/gamma'       : Item(status='  ', wc_rev=2),
    'A/mu'            : Item(status='  ', wc_rev=2),
    'iota'            : Item(status='  ', wc_rev=2),
    })
  svntest.actions.run_and_verify_unquiet_status(wc_dir, expected_status)

def create_no_text_change_conflict(sbox):
  "create conflict with no text change"

  sbox.build()
  wc_dir = sbox.wc_dir

  shutil.copyfile(sbox.ospath('A/B/E/alpha'), sbox.ospath('A/B/E/alpha-copy'))
  svntest.main.file_append(sbox.ospath('A/B/E/alpha'), 'their text\n')
  sbox.simple_commit()
  sbox.simple_update()

  # Update to create a conflict
  svntest.main.file_append(sbox.ospath('A/B/E/alpha'), 'my text\n')
  svntest.actions.run_and_verify_svn(None, None, [],
                                     'up', '-r1', '--accept', 'postpone',
                                     wc_dir)
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/B/E/alpha', status='C ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Reset the text with the file still marked as a conflict
  os.remove(sbox.ospath('A/B/E/alpha'))
  shutil.move(sbox.ospath('A/B/E/alpha-copy'), sbox.ospath('A/B/E/alpha'))

@Issue(3859)
def revert_no_text_change_conflict(sbox):
  "revert conflict with no text change"

  create_no_text_change_conflict(sbox)
  wc_dir = sbox.wc_dir

  svntest.actions.run_and_verify_svn(None,
                                     ["Reverted '%s'\n"
                                      % sbox.ospath('A/B/E/alpha')],
                                     [],
                                     'revert', sbox.ospath('A/B/E/alpha'))

  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

@Issue(3859)
def revert_no_text_change_conflict_recursive(sbox):
  "revert -R conflict with no text change"

  create_no_text_change_conflict(sbox)
  wc_dir = sbox.wc_dir

  svntest.actions.run_and_verify_svn(None,
                                     ["Reverted '%s'\n"
                                      % sbox.ospath('A/B/E/alpha')],
                                     [],
                                     'revert', '-R', wc_dir)

  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

@Issue(3938)
def revert_with_unversioned_targets(sbox):
  "revert with unversioned targets"

  sbox.build()
  wc_dir = sbox.wc_dir

  chi_path = sbox.ospath('A/D/H/chi')
  delta_path = sbox.ospath('A/D/H/delta')
  psi_path = sbox.ospath('A/D/H/psi')

  chi_contents = "modified chi\n"
  delta_contents = "This is the unversioned file 'delta'.\n"
  psi_contents = "modified psi\n"

  # touch delta
  open(delta_path, 'w').write(delta_contents)

  # modify chi psi
  open(chi_path, 'w').write(chi_contents)
  open(psi_path, 'w').write(psi_contents)

  # revert
  expected_output = svntest.verify.UnorderedOutput([
    "Reverted '%s'\n" % sbox.ospath('A/D/H/chi'),
    "Skipped '%s'\n" % sbox.ospath('A/D/H/delta'),
    "Reverted '%s'\n" % sbox.ospath('A/D/H/psi'),
  ])
  svntest.actions.run_and_verify_svn(None, expected_output, [],
                                     'revert', chi_path, delta_path, psi_path)

  # verify status
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'A/D/H/delta': Item(status='? '),
    })
  svntest.actions.run_and_verify_unquiet_status(wc_dir, expected_status)

  # verify disk
  expected_disk = svntest.main.greek_state.copy()
  expected_disk.add({
    'A/D/H/delta': Item(delta_contents),
  })
  actual_disk = svntest.tree.build_tree_from_wc(wc_dir, 1)
  svntest.tree.compare_trees("disk", actual_disk, expected_disk.old_tree())

########################################################################
# Run the tests


# list all tests here, starting with None:
test_list = [ None,
              revert_from_wc_root,
              revert_reexpand_keyword,
              revert_replaced_file_without_props,
              revert_moved_file,
              revert_wc_to_wc_replace_with_props,
              revert_file_merge_replace_with_history,
              revert_repos_to_wc_replace_with_props,
              revert_after_second_replace,
              revert_after_manual_conflict_resolution__text,
              revert_after_manual_conflict_resolution__prop,
              revert_propset__dir,
              revert_propset__file,
              revert_propdel__dir,
              revert_propdel__file,
              revert_replaced_with_history_file_1,
              status_of_missing_dir_after_revert,
              status_of_missing_dir_after_revert_replaced_with_history_dir,
              revert_replaced_with_history_file_2,
              revert_tree_conflicts_in_updated_files,
              revert_add_over_not_present_dir,
              revert_added_tree,
              revert_child_of_copy,
              revert_non_recusive_after_delete,
              revert_permissions_only,
              revert_copy_depth_files,
              revert_nested_add_depth_immediates,
              revert_empty_actual,
              revert_tree_conflicts_with_replacements,
              revert_empty_actual_recursive,
              revert_no_text_change_conflict,
              revert_no_text_change_conflict_recursive,
              revert_with_unversioned_targets,
             ]

if __name__ == '__main__':
  svntest.main.run_tests(test_list)
  # NOTREACHED


### End of file.