#!/usr/bin/env python
#
#  mergetrees.py:  routines that create merge scenarios
#
#  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, sys, re, os
import time

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

from prop_tests import binary_mime_type_on_text_file_warning

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

from svntest.main import SVN_PROP_MERGEINFO

def expected_merge_output(rev_ranges, additional_lines=[], foreign=False,
                          elides=False, two_url=False, target=None,
                          text_conflicts=0, prop_conflicts=0, tree_conflicts=0,
                          text_resolved=0, prop_resolved=0, tree_resolved=0,
                          skipped_paths=0):
  """Generate an (inefficient) regex representing the expected merge
  output and mergeinfo notifications from REV_RANGES and ADDITIONAL_LINES.

  REV_RANGES is a list of revision ranges for which mergeinfo is being
  recorded.  Each range is of the form [start, end] (where both START and
  END are inclusive, unlike in '-rX:Y') or the form [single_rev] (which is
  like '-c SINGLE_REV').  If REV_RANGES is None then only the standard
  notification for a 3-way merge is expected.

  ADDITIONAL_LINES is a list of strings to match the other lines of output;
  these are basically regular expressions except that backslashes will be
  escaped herein.  If ADDITIONAL_LINES is a single string, it is interpreted
  the same as a list containing that string.

  If ELIDES is true, add to the regex an expression representing elision
  notification.  If TWO_URL is true, tweak the regex to expect the
  appropriate mergeinfo notification for a 3-way merge.

  TARGET is the local path to the target, as it should appear in
  notifications; if None, it is not checked.

  TEXT_CONFLICTS, PROP_CONFLICTS, TREE_CONFLICTS and SKIPPED_PATHS specify
  the number of each kind of conflict to expect.
  """

  if rev_ranges is None:
    lines = [main.merge_notify_line(None, None, False, foreign)]
  else:
    lines = []
    for rng in rev_ranges:
      start_rev = rng[0]
      if len(rng) > 1:
        end_rev = rng[1]
      else:
        end_rev = None
      lines += [main.merge_notify_line(start_rev, end_rev,
                                               True, foreign, target)]
      lines += [main.mergeinfo_notify_line(start_rev, end_rev, target)]

  if (elides):
    lines += ["--- Eliding mergeinfo from .*\n"]

  if (two_url):
    lines += ["--- Recording mergeinfo for merge between repository URLs .*\n"]

  # Address "The Backslash Plague"
  #
  # If ADDITIONAL_LINES are present there are possibly paths in it with
  # multiple components and on Windows these components are separated with
  # '\'.  These need to be escaped properly in the regexp for the match to
  # work correctly.  See http://aspn.activestate.com/ASPN/docs/ActivePython
  # /2.2/howto/regex/regex.html#SECTION000420000000000000000.
  if isinstance(additional_lines, str):
    additional_lines = [additional_lines]
  if sys.platform == 'win32':
    additional_lines = [line.replace("\\", "\\\\") for line in additional_lines]
  lines += additional_lines

  lines += main.summary_of_conflicts(
             text_conflicts, prop_conflicts, tree_conflicts,
             text_resolved, prop_resolved, tree_resolved,
             skipped_paths,
             as_regex=True)

  return "|".join(lines)

def check_mergeinfo_recursively(root_path, subpaths_mergeinfo):
  """Check that the mergeinfo properties on and under ROOT_PATH are those in
     SUBPATHS_MERGEINFO, a {path: mergeinfo-prop-val} dictionary."""
  expected = verify.UnorderedOutput(
    [path + ' - ' + subpaths_mergeinfo[path] + '\n'
     for path in subpaths_mergeinfo])
  actions.run_and_verify_svn(expected, [],
                                     'propget', '-R', SVN_PROP_MERGEINFO,
                                     root_path)

######################################################################
#----------------------------------------------------------------------
def set_up_dir_replace(sbox):
  """Set up the working copy for directory replace tests, creating
  directory 'A/B/F/foo' with files 'new file' and 'new file2' within
  it (r2), and merging 'foo' onto 'C' (r3), then deleting 'A/B/F/foo'
  (r4)."""

  sbox.build()
  wc_dir = sbox.wc_dir

  C_path = sbox.ospath('A/C')
  F_path = sbox.ospath('A/B/F')
  F_url = sbox.repo_url + '/A/B/F'

  foo_path = os.path.join(F_path, 'foo')
  new_file = os.path.join(foo_path, "new file")
  new_file2 = os.path.join(foo_path, "new file 2")

  # Make directory foo in F, and add some files within it.
  actions.run_and_verify_svn(None, [], 'mkdir', foo_path)
  main.file_append(new_file, "Initial text in new file.\n")
  main.file_append(new_file2, "Initial text in new file 2.\n")
  main.run_svn(None, "add", new_file)
  main.run_svn(None, "add", new_file2)

  # Commit all the new content, creating r2.
  expected_output = wc.State(wc_dir, {
    'A/B/F/foo'            : Item(verb='Adding'),
    'A/B/F/foo/new file'   : Item(verb='Adding'),
    'A/B/F/foo/new file 2' : Item(verb='Adding'),
    })
  expected_status = actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'A/B/F/foo'             : Item(status='  ', wc_rev=2),
    'A/B/F/foo/new file'    : Item(status='  ', wc_rev=2),
    'A/B/F/foo/new file 2'  : Item(status='  ', wc_rev=2),
    })
  actions.run_and_verify_commit(wc_dir, expected_output, expected_status)

  # Merge foo onto C
  expected_output = wc.State(C_path, {
    'foo' : Item(status='A '),
    'foo/new file'   : Item(status='A '),
    'foo/new file 2' : Item(status='A '),
    })
  expected_mergeinfo_output = wc.State(C_path, {
    '' : Item(status=' U'),
    })
  expected_elision_output = wc.State(C_path, {
    })
  expected_disk = wc.State('', {
    ''               : Item(props={SVN_PROP_MERGEINFO : '/A/B/F:2'}),
    'foo' : Item(),
    'foo/new file'   : Item("Initial text in new file.\n"),
    'foo/new file 2' : Item("Initial text in new file 2.\n"),
    })
  expected_status = wc.State(C_path, {
    ''    : Item(status=' M', wc_rev=1),
    'foo' : Item(status='A ', wc_rev='-', copied='+'),
    'foo/new file'   : Item(status='  ', wc_rev='-', copied='+'),
    'foo/new file 2' : Item(status='  ', wc_rev='-', copied='+'),
    })
  expected_skip = wc.State(C_path, { })
  actions.run_and_verify_merge(C_path, '1', '2', F_url, None,
                                       expected_output,
                                       expected_mergeinfo_output,
                                       expected_elision_output,
                                       expected_disk,
                                       expected_status,
                                       expected_skip,
                                       check_props=True)
  # Commit merge of foo onto C, creating r3.
  expected_output = wc.State(wc_dir, {
    'A/C'        : Item(verb='Sending'),
    'A/C/foo'    : Item(verb='Adding'),
    })
  expected_status = actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'A/B/F/foo'  : Item(status='  ', wc_rev=2),
    'A/C'        : Item(status='  ', wc_rev=3),
    'A/B/F/foo/new file'      : Item(status='  ', wc_rev=2),
    'A/B/F/foo/new file 2'    : Item(status='  ', wc_rev=2),
    'A/C/foo'    : Item(status='  ', wc_rev=3),
    'A/C/foo/new file'      : Item(status='  ', wc_rev=3),
    'A/C/foo/new file 2'    : Item(status='  ', wc_rev=3),

    })
  actions.run_and_verify_commit(wc_dir, expected_output, expected_status)

  # Delete foo on F, creating r4.
  actions.run_and_verify_svn(None, [], 'rm', foo_path)
  expected_output = wc.State(wc_dir, {
    'A/B/F/foo'   : Item(verb='Deleting'),
    })
  expected_status = actions.get_virginal_state(wc_dir, 1)
  expected_status.add({
    'A/C'         : Item(status='  ', wc_rev=3),
    'A/C/foo'     : Item(status='  ', wc_rev=3),
    'A/C/foo/new file'      : Item(status='  ', wc_rev=3),
    'A/C/foo/new file 2'    : Item(status='  ', wc_rev=3),
    })
  actions.run_and_verify_commit(wc_dir, expected_output, expected_status)

#----------------------------------------------------------------------
def set_up_branch(sbox, branch_only = False, nbr_of_branches = 1):
  '''Starting with standard greek tree, copy 'A' NBR_OF_BRANCHES times
  to A_COPY, A_COPY_2, A_COPY_3, and so on.  Then, unless BRANCH_ONLY is
  true, make four modifications (setting file contents to "New content")
  under A:
    r(2 + NBR_OF_BRANCHES) - A/D/H/psi
    r(3 + NBR_OF_BRANCHES) - A/D/G/rho
    r(4 + NBR_OF_BRANCHES) - A/B/E/beta
    r(5 + NBR_OF_BRANCHES) - A/D/H/omega
  Return (expected_disk, expected_status).'''

  # With the default parameters, the branching looks like this:
  #
  #   A         -1-----3-4-5-6--
  #                \
  #   A_COPY        2-----------

  wc_dir = sbox.wc_dir

  expected_status = actions.get_virginal_state(wc_dir, 1)
  expected_disk = main.greek_state.copy()

  def path_join(head, tail):
    if not head: return tail
    if not tail: return head
    return head + '/' + tail

  def greek_file_item(path):
    if path[-1:].islower():
      basename = re.sub('.*/', '', path)
      return Item("This is the file '" + basename + "'.\n")
    return Item()

  A_paths = [
    "",
    "B",
    "B/lambda",
    "B/E",
    "B/E/alpha",
    "B/E/beta",
    "B/F",
    "mu",
    "C",
    "D",
    "D/gamma",
    "D/G",
    "D/G/pi",
    "D/G/rho",
    "D/G/tau",
    "D/H",
    "D/H/chi",
    "D/H/omega",
    "D/H/psi",
    ]
  def copy_A(dest_name, rev):
    expected = verify.UnorderedOutput(
      [ "A         " + sbox.ospath(path_join(dest_name, p)) + "\n"
        for p in A_paths ])
    expected_status.add(
      { path_join(dest_name, p) : Item(status='  ', wc_rev=rev)
        for p in A_paths })
    expected_disk.add(
      { path_join(dest_name, p) : greek_file_item(p)
        for p in A_paths })

    # Make a branch A_COPY to merge into.
    actions.run_and_verify_svn(expected, [], 'copy',
                                       sbox.repo_url + "/A",
                                       os.path.join(wc_dir,
                                                    dest_name))

    expected_output = wc.State(wc_dir, {dest_name : Item(verb='Adding')})
    actions.run_and_verify_commit(wc_dir, expected_output, expected_status)
  for i in range(nbr_of_branches):
    if i == 0:
      copy_A('A_COPY', i + 2)
    else:
      copy_A('A_COPY_' + str(i + 1), i + 2)

  if branch_only:
    return expected_disk, expected_status

  # Make some changes under A which we'll later merge under A_COPY:

  # r(nbr_of_branches + 2) - modify and commit A/D/H/psi
  main.file_write(sbox.ospath('A/D/H/psi'),
                          "New content")
  expected_output = wc.State(wc_dir, {'A/D/H/psi' : Item(verb='Sending')})
  expected_status.tweak('A/D/H/psi', wc_rev=nbr_of_branches + 2)
  actions.run_and_verify_commit(wc_dir, expected_output, expected_status)
  expected_disk.tweak('A/D/H/psi', contents="New content")

  # r(nbr_of_branches + 3) - modify and commit A/D/G/rho
  main.file_write(sbox.ospath('A/D/G/rho'),
                          "New content")
  expected_output = wc.State(wc_dir, {'A/D/G/rho' : Item(verb='Sending')})
  expected_status.tweak('A/D/G/rho', wc_rev=nbr_of_branches + 3)
  actions.run_and_verify_commit(wc_dir, expected_output, expected_status)
  expected_disk.tweak('A/D/G/rho', contents="New content")

  # r(nbr_of_branches + 4) - modify and commit A/B/E/beta
  main.file_write(sbox.ospath('A/B/E/beta'),
                          "New content")
  expected_output = wc.State(wc_dir, {'A/B/E/beta' : Item(verb='Sending')})
  expected_status.tweak('A/B/E/beta', wc_rev=nbr_of_branches + 4)
  actions.run_and_verify_commit(wc_dir, expected_output, expected_status)
  expected_disk.tweak('A/B/E/beta', contents="New content")

  # r(nbr_of_branches + 5) - modify and commit A/D/H/omega
  main.file_write(sbox.ospath('A/D/H/omega'),
                          "New content")
  expected_output = wc.State(wc_dir, {'A/D/H/omega' : Item(verb='Sending')})
  expected_status.tweak('A/D/H/omega', wc_rev=nbr_of_branches + 5)
  actions.run_and_verify_commit(wc_dir, expected_output, expected_status)
  expected_disk.tweak('A/D/H/omega', contents="New content")

  return expected_disk, expected_status

#----------------------------------------------------------------------
# Helper functions. These take local paths using '/' separators.

def local_path(path):
  "Convert a path from '/' separators to the local style."
  return os.sep.join(path.split('/'))

def svn_mkfile(path):
  "Make and add a file with some default content, and keyword expansion."
  path = local_path(path)
  dirname, filename = os.path.split(path)
  main.file_write(path, "This is the file '" + filename + "'.\n" +
                                "Last changed in '$Revision$'.\n")
  actions.run_and_verify_svn(None, [], 'add', path)
  actions.run_and_verify_svn(None, [], 'propset',
                                     'svn:keywords', 'Revision', path)

def svn_modfile(path):
  "Make text and property mods to a WC file."
  path = local_path(path)
  main.file_append(path, "An extra line.\n")
  actions.run_and_verify_svn(None, [], 'propset',
                                     'newprop', 'v', path)

def svn_copy(s_rev, path1, path2):
  "Copy a WC path locally."
  path1 = local_path(path1)
  path2 = local_path(path2)
  actions.run_and_verify_svn(None, [], 'copy', '--parents',
                                     '-r', s_rev, path1, path2)

def svn_merge(rev_range, source, target, lines=None, elides=[],
              text_conflicts=0, prop_conflicts=0, tree_conflicts=0,
              text_resolved=0, prop_resolved=0, tree_resolved=0,
              args=[]):
  """Merge a single change from path SOURCE to path TARGET and verify the
  output and that there is no error.  (The changes made are not verified.)

  REV_RANGE is either a number (to cherry-pick that specific change) or a
  two-element list [X,Y] to pick the revision range '-r(X-1):Y'.

  LINES is a list of regular expressions to match other lines of output; if
  LINES is 'None' then match all normal (non-conflicting) merges.

  ELIDES is a list of paths on which mergeinfo elision should be reported.

  TEXT_CONFLICTS, PROP_CONFLICTS and TREE_CONFLICTS specify the number of
  each kind of conflict to expect.

  ARGS are additional arguments passed to svn merge.
  """

  source = local_path(source)
  target = local_path(target)
  elides = [local_path(p) for p in elides]
  if isinstance(rev_range, int):
    mi_rev_range = [rev_range]
    rev_arg = '-c' + str(rev_range)
  else:
    mi_rev_range = rev_range
    rev_arg = '-r' + str(rev_range[0] - 1) + ':' + str(rev_range[1])
  if lines is None:
    lines = ["(A |D |[UG] | [UG]|[UG][UG])   " + target + ".*\n"]
  else:
    # Expect mergeinfo on the target; caller must supply matches for any
    # subtree mergeinfo paths.
    lines.append(" [UG]   " + target + "\n")
  exp_out = expected_merge_output([mi_rev_range], lines, target=target,
                                  elides=elides,
                                  text_conflicts=text_conflicts,
                                  prop_conflicts=prop_conflicts,
                                  tree_conflicts=tree_conflicts,
                                  text_resolved=text_resolved,
                                  prop_resolved=prop_resolved,
                                  tree_resolved=tree_resolved)
  actions.run_and_verify_svn(exp_out, [],
                                     'merge', rev_arg, source, target,
                                     '--accept=postpone', *args)

#----------------------------------------------------------------------
# Setup helper for issue #4056 and issue #4057 tests.
def noninheritable_mergeinfo_test_set_up(sbox):
  '''Starting with standard greek tree, copy 'A' to 'branch' in r2 and
  then made a file edit to A/B/lambda in r3.
  Return (expected_output, expected_mergeinfo_output, expected_elision_output,
          expected_status, expected_disk, expected_skip) for a merge of
  r3 from ^/A/B to branch/B.'''

  sbox.build()
  wc_dir = sbox.wc_dir

  lambda_path   = sbox.ospath('A/B/lambda')
  B_branch_path = sbox.ospath('branch/B')

  # r2 - Branch ^/A to ^/branch.
  main.run_svn(None, 'copy', sbox.repo_url + '/A',
                       sbox.repo_url + '/branch', '-m', 'make a branch')

  # r3 - Make an edit to A/B/lambda.
  main.file_write(lambda_path, "trunk edit.\n")
  main.run_svn(None, 'commit', '-m', 'file edit', wc_dir)
  main.run_svn(None, 'up', wc_dir)

  expected_output = wc.State(B_branch_path, {
    'lambda' : Item(status='U '),
    })
  expected_mergeinfo_output = wc.State(B_branch_path, {
    ''       : Item(status=' U'),
    'lambda' : Item(status=' U'),
    })
  expected_elision_output = wc.State(B_branch_path, {
    'lambda' : Item(status=' U'),
    })
  expected_status = wc.State(B_branch_path, {
    ''        : Item(status=' M'),
    'lambda'  : Item(status='M '),
    'E'       : Item(status='  '),
    'E/alpha' : Item(status='  '),
    'E/beta'  : Item(status='  '),
    'F'       : Item(status='  '),
    })
  expected_status.tweak(wc_rev='3')
  expected_disk = wc.State('', {
    ''          : Item(props={SVN_PROP_MERGEINFO : '/A/B:3'}),
    'lambda'  : Item("trunk edit.\n"),
    'E'       : Item(),
    'E/alpha' : Item("This is the file 'alpha'.\n"),
    'E/beta'  : Item("This is the file 'beta'.\n"),
    'F'       : Item(),
    })
  expected_skip = wc.State(B_branch_path, {})

  return expected_output, expected_mergeinfo_output, expected_elision_output, \
    expected_status, expected_disk, expected_skip

