#!/usr/bin/env python
#
#  switch_tests.py:  testing `svn switch'.
#
#  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 shutil, re, os

# Our testing module
import svntest
from svntest import verify, actions, main

# (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

from svntest.main import SVN_PROP_MERGEINFO, server_has_mergeinfo
from externals_tests import change_external
from switch_tests import do_routine_switching

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

def relocate_deleted_missing_copied(sbox):
  "relocate with deleted, missing and copied entries"
  sbox.build()
  wc_dir = sbox.wc_dir

  # Delete A/mu to create a deleted entry for mu in A/.svn/entries
  mu_path = os.path.join(wc_dir, 'A', 'mu')
  svntest.actions.run_and_verify_svn(None, None, [], 'rm', mu_path)
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.remove('A/mu')
  expected_output = svntest.wc.State(wc_dir, {
    'A/mu' : Item(verb='Deleting'),
    })
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

  # Remove A/B/F to create a missing entry
  svntest.main.safe_rmtree(os.path.join(wc_dir, 'A', 'B', 'F'))

  # Copy A/D to A/D2
  D_path = os.path.join(wc_dir, 'A', 'D')
  D2_path = os.path.join(wc_dir, 'A', 'D2')
  svntest.actions.run_and_verify_svn(None, None, [], 'copy',
                                     D_path, D2_path)
  # Delete within the copy
  D2G_path = os.path.join(wc_dir, 'A', 'D2', 'G')
  svntest.actions.run_and_verify_svn(None, None, [], 'rm', D2G_path)

  expected_status.add({
    'A/D2'         : Item(status='A ', wc_rev='-', copied='+'),
    'A/D2/gamma'   : Item(status='  ', wc_rev='-', copied='+'),
    'A/D2/G'       : Item(status='D ', wc_rev='-', copied='+'),
    'A/D2/G/pi'    : Item(status='D ', wc_rev='-', copied='+'),
    'A/D2/G/rho'   : Item(status='D ', wc_rev='-', copied='+'),
    'A/D2/G/tau'   : Item(status='D ', wc_rev='-', copied='+'),
    'A/D2/H'       : Item(status='  ', wc_rev='-', copied='+'),
    'A/D2/H/chi'   : Item(status='  ', wc_rev='-', copied='+'),
    'A/D2/H/omega' : Item(status='  ', wc_rev='-', copied='+'),
    'A/D2/H/psi'   : Item(status='  ', wc_rev='-', copied='+'),
    })
  expected_status.tweak('A/B/F', status='! ', wc_rev='1')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # Relocate
  repo_dir = sbox.repo_dir
  repo_url = sbox.repo_url
  other_repo_dir, other_repo_url = sbox.add_repo_path('other')
  svntest.main.copy_repos(repo_dir, other_repo_dir, 2, 0)
  svntest.main.safe_rmtree(repo_dir, 1)
  svntest.actions.run_and_verify_svn(None, None, [], 'switch', '--relocate',
                                     repo_url, other_repo_url, wc_dir)

  # Deleted and missing entries should be preserved, so update should
  # show only A/B/F being reinstated
  expected_output = svntest.wc.State(wc_dir, {
        'A/B/F' : Item(verb='Restored'),
  })

  expected_disk = svntest.main.greek_state.copy()
  expected_disk.remove('A/mu')
  expected_disk.add({
    'A/D2'       : Item(),
    'A/D2/gamma'   : Item("This is the file 'gamma'.\n"),
    'A/D2/H'       : Item(),
    'A/D2/H/chi'   : Item("This is the file 'chi'.\n"),
    'A/D2/H/omega' : Item("This is the file 'omega'.\n"),
    'A/D2/H/psi'   : Item("This is the file 'psi'.\n"),
    })

  expected_status.add({
    'A/B/F'       : Item(status='  ', wc_rev='2'),
    })
  expected_status.tweak(wc_rev=2)
  expected_status.tweak('A/D2', 'A/D2/gamma',
                        'A/D2/H', 'A/D2/H/chi', 'A/D2/H/omega', 'A/D2/H/psi',
                        wc_rev='-')
  expected_status.tweak('A/D2/G', 'A/D2/G/pi', 'A/D2/G/rho', 'A/D2/G/tau',
                        copied='+', wc_rev='-')
  svntest.actions.run_and_verify_update(wc_dir,
                                        expected_output,
                                        expected_disk,
                                        expected_status)

  # Commit to verify that copyfrom URLs have been relocated
  expected_output = svntest.wc.State(wc_dir, {
    'A/D2'       : Item(verb='Adding'),
    'A/D2/G'     : Item(verb='Deleting'),
    })
  expected_status.tweak('A/D2', 'A/D2/gamma',
                        'A/D2/H', 'A/D2/H/chi', 'A/D2/H/omega', 'A/D2/H/psi',
                        status='  ', wc_rev='3', copied=None)
  expected_status.remove('A/D2/G', 'A/D2/G/pi', 'A/D2/G/rho', 'A/D2/G/tau')
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output, expected_status,
                                        None, wc_dir)

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

@Issue(2380)
def relocate_beyond_repos_root(sbox):
  "relocate with prefixes longer than repo root"
  sbox.build(read_only=True, create_wc=False)

  wc_backup = sbox.add_wc_path('backup')

  wc_dir = sbox.wc_dir
  repo_dir = sbox.repo_dir
  repo_url = sbox.repo_url
  other_repo_dir, other_repo_url = sbox.add_repo_path('other')
  A_url = repo_url + "/A"
  A_wc_dir = wc_dir
  other_A_url = other_repo_url + "/A"
  other_B_url = other_repo_url + "/B"

  svntest.main.safe_rmtree(wc_dir, 1)
  svntest.actions.run_and_verify_svn(None, None, [], 'checkout',
                                     repo_url + '/A', wc_dir)

  svntest.main.copy_repos(repo_dir, other_repo_dir, 1, 0)

  # A relocate that changes the repo path part of the URL shouldn't work.
  # This tests for issue #2380.
  svntest.actions.run_and_verify_svn(None, None,
                                     ".*Invalid relocation destination.*",
                                     'relocate',
                                     A_url, other_B_url, A_wc_dir)

  # Another way of trying to change the fs path, leading to an invalid
  # repository root.
  svntest.actions.run_and_verify_svn(None, None,
                                     ".*is not the root.*",
                                     'relocate',
                                     repo_url, other_B_url, A_wc_dir)

  svntest.actions.run_and_verify_svn(None, None, [],
                                     'relocate',
                                     A_url, other_A_url, A_wc_dir)

  # Check that we can contact the repository, meaning that the
  # relocate actually changed the URI.  Escape the expected URI to
  # avoid problems from any regex meta-characters it may contain
  # (e.g. '+').
  expected_infos = [
      { 'URL'                : re.escape(other_A_url) + '$',
        'Path'               : '.+',
        'Repository UUID'    : '.+',
        'Revision'           : '.+',
        'Node Kind'          : '.+',
        'Last Changed Date'  : '.+' },
    ]
  svntest.actions.run_and_verify_info(expected_infos, A_wc_dir, '-rHEAD')

#----------------------------------------------------------------------
# Issue 2578.
def relocate_and_propset(sbox):
  "out of date propset should fail after a relocate"

  # Create virgin repos and working copy
  svntest.main.safe_rmtree(sbox.repo_dir, 1)
  svntest.main.create_repos(sbox.repo_dir)

  wc_dir = sbox.wc_dir
  repo_dir = sbox.repo_dir
  repo_url = sbox.repo_url

  # import the greek tree
  svntest.main.greek_state.write_to_disk(svntest.main.greek_dump_dir)
  exit_code, output, errput = svntest.main.run_svn(
    None, 'import', '-m', 'Log message for revision 1.',
    svntest.main.greek_dump_dir, sbox.repo_url)

  # checkout
  svntest.main.safe_rmtree(wc_dir, 1)
  svntest.actions.run_and_verify_svn(None,
                                     None, [],
                                     'checkout',
                                     repo_url, wc_dir)

  # Relocate
  other_repo_dir, other_repo_url = sbox.add_repo_path('other')
  svntest.main.copy_repos(repo_dir, other_repo_dir, 1, 0)
  svntest.main.safe_rmtree(repo_dir, 1)
  svntest.actions.run_and_verify_svn(None, None, [], 'relocate',
                                     repo_url, other_repo_url, wc_dir)

  # Remove gamma from the working copy.
  D_path = os.path.join(wc_dir, 'A', 'D')
  gamma_path = os.path.join(wc_dir, 'A', 'D', 'gamma')
  svntest.main.run_svn(None, 'rm', gamma_path)

  # Create expected commit output.
  expected_output = svntest.wc.State(wc_dir, {
    'A/D/gamma' : Item(verb='Deleting'),
    })

  # After committing, status should show no sign of gamma.
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.remove('A/D/gamma')

  # Commit the deletion of gamma and verify.
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        None, wc_dir)

  # Now gamma should be marked as `deleted' under the hood, at
  # revision 2.  Meanwhile, A/D is still lagging at revision 1.

  # Make a propchange on A/D
  svntest.main.run_svn(None, 'ps', 'foo', 'bar', D_path)

  # Commit and *expect* a repository Merge failure:
  svntest.actions.run_and_verify_commit(wc_dir,
                                        None,
                                        None,
                                        "[Oo]ut.of.date",
                                        wc_dir)

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

def single_file_relocate(sbox):
  "relocate a single file"

  # Create virgin repos and working copy
  svntest.main.safe_rmtree(sbox.repo_dir, 1)
  svntest.main.create_repos(sbox.repo_dir)

  wc_dir = sbox.wc_dir
  iota_path = os.path.join(sbox.wc_dir, 'iota')
  repo_dir = sbox.repo_dir
  repo_url = sbox.repo_url
  iota_url = repo_url + '/iota'

  # import the greek tree
  svntest.main.greek_state.write_to_disk(svntest.main.greek_dump_dir)
  exit_code, output, errput = svntest.main.run_svn(
    None, 'import', '-m', 'Log message for revision 1.',
    svntest.main.greek_dump_dir, sbox.repo_url)

  # checkout
  svntest.main.safe_rmtree(wc_dir, 1)
  svntest.actions.run_and_verify_svn(None,
                                     None, [],
                                     'checkout',
                                     repo_url, wc_dir)

  # Relocate
  other_repo_dir, other_repo_url = sbox.add_repo_path('other')
  other_iota_url = other_repo_url + '/iota'
  svntest.main.copy_repos(repo_dir, other_repo_dir, 1, 0)
  svntest.main.safe_rmtree(repo_dir, 1)
  svntest.actions.run_and_verify_svn(None, None,
                                     ".*Cannot relocate.*",
                                     'relocate',
                                     iota_url, other_iota_url, iota_path)

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

def relocate_with_switched_children(sbox):
  "relocate a directory with switched children"
  sbox.build()
  wc_dir = sbox.wc_dir

  # Setup (and verify) some switched things
  do_routine_switching(sbox.wc_dir, sbox.repo_url, False)

  # Relocate
  repo_dir = sbox.repo_dir
  repo_url = sbox.repo_url
  other_repo_dir, other_repo_url = sbox.add_repo_path('other')
  svntest.main.copy_repos(repo_dir, other_repo_dir, 1, 0)
  svntest.main.safe_rmtree(repo_dir, 1)

  # Do the switch and check the results in three ways.
  svntest.actions.run_and_verify_svn(None, None, [], 'relocate',
                                     repo_url, other_repo_url, wc_dir)

  # Attempt to commit changes and examine results
  expected_output = svntest.wc.State(wc_dir, { })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/B', 'iota',
                        switched='S')
  expected_status.remove('A/B/E', 'A/B/F', 'A/B/E/alpha', 'A/B/E/beta',
                         'A/B/lambda')
  expected_status.add({
    'A/B/pi'       : Item(status='  ', wc_rev='1'),
    'A/B/rho'      : Item(status='  ', wc_rev='1'),
    'A/B/tau'      : Item(status='  ', wc_rev='1'),
    })

  # This won't actually do a commit, because nothing should be modified.
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output, expected_status,
                                        None, wc_dir)

  # Check the URLs of various nodes.
  info_output = {
        wc_dir:                                '.*.other$',
        os.path.join(wc_dir, 'iota'):          '.*.other/A/D/gamma$',
        os.path.join(wc_dir, 'A', 'B'):        '.*.other/A/D/G$',
        os.path.join(wc_dir, 'A', 'B', 'pi'):  '.*.other/A/D/G/pi$',
    }

  for path, pattern in info_output.items():
    expected_info = { 'URL' : pattern }
    svntest.actions.run_and_verify_info([expected_info], path)

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


### regression test for issue #3597
@Issue(3597)
def relocate_with_relative_externals(sbox):
  "relocate a directory containing relative externals"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Add a relative external.
  change_external(os.path.join(wc_dir, 'A', 'B'),
                  "^/A/D/G G-ext\n../D/H H-ext", commit=True)
  svntest.actions.run_and_verify_svn(None, None, [], 'update', wc_dir)

  # Move our repository to another location.
  repo_dir = sbox.repo_dir
  repo_url = sbox.repo_url
  other_repo_dir, other_repo_url = sbox.add_repo_path('other')
  svntest.main.copy_repos(repo_dir, other_repo_dir, 2, 0)
  svntest.main.safe_rmtree(repo_dir, 1)

  # Now relocate our working copy.
  svntest.actions.run_and_verify_svn(None, None, [], 'relocate',
                                     repo_url, other_repo_url, wc_dir)

  # Check the URLs of the externals -- were they updated to point to the
  # .other repository URL?
  svntest.actions.run_and_verify_info([{ 'URL' : '.*.other/A/D/G$' }],
                                      os.path.join(wc_dir, 'A', 'B', 'G-ext'))
  svntest.actions.run_and_verify_info([{ 'URL' : '.*.other/A/D/H$' }],
                                      os.path.join(wc_dir, 'A', 'B', 'H-ext'))

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

# list all tests here, starting with None:
test_list = [ None,
              relocate_deleted_missing_copied,
              relocate_beyond_repos_root,
              relocate_and_propset,
              single_file_relocate,
              relocate_with_switched_children,
              relocate_with_relative_externals,
              ]

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


### End of file.
