#!/usr/bin/env python
#
#  changelist_tests.py:  testing changelist uses.
#
#  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 string, sys, os, re

# Our testing module
import svntest

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


######################################################################
# Utilities


def mod_all_files(wc_dir, new_text):
  """Walk over working copy WC_DIR, appending NEW_TEXT to all the
  files in that tree (but not inside the .svn areas of that tree)."""

  dot_svn = svntest.main.get_admin_name()
  for dirpath, dirs, files in os.walk(wc_dir):
    if dot_svn in dirs:
      dirs.remove(dot_svn)
    for name in files:
      svntest.main.file_append(os.path.join(dirpath, name), new_text)

def changelist_all_files(wc_dir, name_func):
  """Walk over working copy WC_DIR, adding versioned files to
  changelists named by invoking NAME_FUNC(full-path-of-file) and
  noting its string return value (or None, if we wish to remove the
  file from a changelist)."""

  dot_svn = svntest.main.get_admin_name()
  for dirpath, dirs, files in os.walk(wc_dir):
    if dot_svn in dirs:
      dirs.remove(dot_svn)
    for name in files:
        full_path = os.path.join(dirpath, name)
        clname = name_func(full_path)
        if not clname:
          svntest.main.run_svn(None, "changelist", "--remove", full_path)
        else:
          svntest.main.run_svn(None, "changelist", clname, full_path)

def select_paths(target_path, depth, changelists, name_func):
  """Return the subset of paths found on disk at TARGET_PATH, to a depth
     of DEPTH, that match CHANGELISTS.
     NAME_FUNC, rather than the working copy, determines what
     changelist each path is associated with.
     Returned paths are relative to the CWD.

     ### Only the paths of files are returned.
  """
  dot_svn = svntest.main.get_admin_name()
  for dirpath, dirs, files in os.walk(target_path):
    # prepare to return paths relative to WC_DIR
    if dot_svn in dirs:
      dirs.remove(dot_svn)
    if not changelists:  # When changelists support dirs, add: "or name_func(name) in changelists"
      yield os.path.normpath(dirpath)
    if depth == 'empty':
      dirs[:] = []  # process no subdirs
      continue      # nor files
    for name in files:
      if not changelists or name_func(name) in changelists:
        yield os.path.normpath(os.path.join(dirpath, name))
    if depth == 'files':
      dirs[:] = []  # process no subdirs
    if depth == 'immediates':
      depth = 'empty'  # process subdirs, but no files nor dirs in them

def clname_from_lastchar_cb(full_path):
  """Callback for changelist_all_files() that returns a changelist
  name matching the last character in the file's name.  For example,
  after running this on a greek tree where every file has some text
  modification, 'svn status' shows:

    --- Changelist 'a':
    M      A/B/lambda
    M      A/B/E/alpha
    M      A/B/E/beta
    M      A/D/gamma
    M      A/D/H/omega
    M      iota

    --- Changelist 'u':
    M      A/mu
    M      A/D/G/tau

    --- Changelist 'i':
    M      A/D/G/pi
    M      A/D/H/chi
    M      A/D/H/psi

    --- Changelist 'o':
    M      A/D/G/rho
    """
  return full_path[-1]


# Regular expressions for 'svn changelist' output.
_re_cl_rem_pattern = r"^D \[(.*)\] (.*)"
_re_cl_skip = re.compile(r"Skipped '(.*)'")
_re_cl_add  = re.compile(r"^A \[(.*)\] (.*)")
_re_cl_rem  = re.compile(_re_cl_rem_pattern)

def verify_changelist_output(output, expected_adds=None,
                             expected_removals=None,
                             expected_skips=None):
  """Compare lines of OUTPUT from 'svn changelist' against
  EXPECTED_ADDS (a dictionary mapping paths to changelist names),
  EXPECTED_REMOVALS (a dictionary mapping paths to ... whatever), and
  EXPECTED_SKIPS (a dictionary mapping paths to ... whatever).

  EXPECTED_SKIPS is ignored if None."""

  num_expected = 0
  if expected_adds:
    num_expected += len(expected_adds)
  if expected_removals:
    num_expected += len(expected_removals)
  if expected_skips:
    num_expected += len(expected_skips)

  if not expected_skips:
    output = [line for line in output if (not _re_cl_skip.match(line))]

  for line in output:
    line = line.rstrip()
    match = _re_cl_rem.match(line)
    if match \
       and expected_removals \
       and match.group(2) in expected_removals:
        continue
    elif match:
      raise svntest.Failure("Unexpected changelist removal line: " + line)
    match = _re_cl_add.match(line)
    if match \
       and expected_adds \
       and expected_adds.get(match.group(2)) == match.group(1):
        continue
    elif match:
      raise svntest.Failure("Unexpected changelist add line: " + line)
    match = _re_cl_skip.match(line)
    if match \
       and expected_skips \
       and match.group(2) in expected_skips:
        continue
    elif match:
      raise svntest.Failure("Unexpected changelist skip line: " + line)
    raise svntest.Failure("Unexpected line: " + line)

  if len(output) != num_expected:
    raise svntest.Failure("Unexpected number of 'svn changelist' output " +
                          "lines (%d vs %d)" % (len(output), num_expected))

def verify_pget_output(output, expected_props):
  """Compare lines of OUTPUT from 'svn propget' against EXPECTED_PROPS
  (a dictionary mapping paths to property values)."""

  _re_pget = re.compile('^(.*) - (.*)$')
  actual_props = {}
  for line in output:
    try:
      path, prop = line.rstrip().split(' - ')
    except:
      raise svntest.Failure("Unexpected output line: " + line)
    actual_props[path] = prop
  if expected_props != actual_props:
    raise svntest.Failure("Got unexpected property results\n"
                          "\tExpected: %s\n"
                          "\tActual: %s" % (str(expected_props),
                                            str(actual_props)))


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


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

def add_remove_changelists(sbox):
  "add and remove files from changelists"

  sbox.build()
  wc_dir = sbox.wc_dir

  ### 'Skip' notifications

  def expected_skips_under(*greek_path):
    "return a dict mapping Greek-tree directories below GREEK_PATH to None"

    expected_skips = {}
    for path in expected_skips_all:
      if path.startswith(os.path.join(wc_dir, *greek_path)):
        expected_skips[path] = None

    return expected_skips

  def all_parents(expected_adds):
    """return a dict mapping Greek-tree directories above directories in
       EXPECTED_ADDS to None"""

    expected_skips = {}
    for path in expected_adds.keys():
      if not os.path.isdir(path):
        path = os.path.dirname(path)

      while path != wc_dir:
        expected_skips[path] = None
        path = os.path.dirname(path)

    expected_skips[wc_dir] = None
    return expected_skips

  # all dirs in the Greek tree
  expected_skips_all = dict([(x, None) for x in [
    sbox.ospath(''),
    sbox.ospath('A'),
    sbox.ospath('A/B'),
    sbox.ospath('A/B/E'),
    sbox.ospath('A/B/F'),
    sbox.ospath('A/C'),
    sbox.ospath('A/D'),
    sbox.ospath('A/D/G'),
    sbox.ospath('A/D/H'),
    ]])

  expected_skips_wc_dir = { wc_dir : None }

  ### First, we play with just adding to changelists ###

  # svn changelist foo WC_DIR
  exit_code, output, errput = svntest.main.run_svn(None, "changelist", "foo",
                                                   wc_dir)
  verify_changelist_output(output) # nothing expected

  # svn changelist foo WC_DIR --depth files
  exit_code, output, errput = svntest.main.run_svn(None, "changelist", "foo",
                                                   "--depth", "files",
                                                   wc_dir)
  expected_adds = {
    os.path.join(wc_dir, 'iota') : 'foo',
    }
  verify_changelist_output(output, expected_adds)

  # svn changelist foo WC_DIR --depth infinity
  exit_code, output, errput = svntest.main.run_svn(None, "changelist", "foo",
                                                   "--depth", "infinity",
                                                   wc_dir)
  expected_adds = {
      sbox.ospath('A/B/E/alpha') : 'foo',
      sbox.ospath('A/B/E/beta') : 'foo',
      sbox.ospath('A/B/lambda') : 'foo',
      sbox.ospath('A/D/G/pi') : 'foo',
      sbox.ospath('A/D/G/rho') : 'foo',
      sbox.ospath('A/D/G/tau') : 'foo',
      sbox.ospath('A/D/H/chi') : 'foo',
      sbox.ospath('A/D/H/omega') : 'foo',
      sbox.ospath('A/D/H/psi') : 'foo',
      sbox.ospath('A/D/gamma') : 'foo',
      sbox.ospath('A/mu') : 'foo',
    }
  verify_changelist_output(output, expected_adds)

  ### Now, change some changelists ###

  # svn changelist bar WC_DIR/A/D --depth infinity
  exit_code, output, errput = svntest.main.run_svn(".*", "changelist", "bar",
                                                   "--depth", "infinity",
                                                   sbox.ospath('A/D'))
  expected_adds = {
      sbox.ospath('A/D/G/pi') : 'bar',
      sbox.ospath('A/D/G/rho') : 'bar',
      sbox.ospath('A/D/G/tau') : 'bar',
      sbox.ospath('A/D/H/chi') : 'bar',
      sbox.ospath('A/D/H/omega') : 'bar',
      sbox.ospath('A/D/H/psi') : 'bar',
      sbox.ospath('A/D/gamma') : 'bar',
    }
  expected_removals = expected_adds
  verify_changelist_output(output, expected_adds, expected_removals)

  # svn changelist baz WC_DIR/A/D/H --depth infinity
  exit_code, output, errput = svntest.main.run_svn(".*", "changelist", "baz",
                                                   "--depth", "infinity",
                                                   sbox.ospath('A/D/H'))
  expected_adds = {
      sbox.ospath('A/D/H/chi') : 'baz',
      sbox.ospath('A/D/H/omega') : 'baz',
      sbox.ospath('A/D/H/psi') : 'baz',
    }
  expected_removals = expected_adds
  verify_changelist_output(output, expected_adds, expected_removals)

  ### Now, let's selectively rename some changelists ###

  # svn changelist foo-rename WC_DIR --depth infinity --changelist foo
  exit_code, output, errput = svntest.main.run_svn(".*", "changelist",
                                                   "foo-rename",
                                                   "--depth", "infinity",
                                                   "--changelist", "foo",
                                                   wc_dir)
  expected_adds = {
      sbox.ospath('A/B/E/alpha') : 'foo-rename',
      sbox.ospath('A/B/E/beta') : 'foo-rename',
      sbox.ospath('A/B/lambda') : 'foo-rename',
      sbox.ospath('A/mu') : 'foo-rename',
      sbox.ospath('iota') : 'foo-rename',
    }
  expected_removals = expected_adds
  verify_changelist_output(output, expected_adds, expected_removals)

  # svn changelist bar WC_DIR --depth infinity
  #     --changelist foo-rename --changelist baz
  exit_code, output, errput = svntest.main.run_svn(
    ".*", "changelist", "bar", "--depth", "infinity",
    "--changelist", "foo-rename", "--changelist", "baz", wc_dir)

  expected_adds = {
      sbox.ospath('A/B/E/alpha') : 'bar',
      sbox.ospath('A/B/E/beta') : 'bar',
      sbox.ospath('A/B/lambda') : 'bar',
      sbox.ospath('A/D/H/chi') : 'bar',
      sbox.ospath('A/D/H/omega') : 'bar',
      sbox.ospath('A/D/H/psi') : 'bar',
      sbox.ospath('A/mu') : 'bar',
      sbox.ospath('iota') : 'bar',
    }
  expected_removals = expected_adds
  verify_changelist_output(output, expected_adds, expected_removals)

  ### Okay.  Time to remove some stuff from changelists now. ###

  # svn changelist --remove WC_DIR
  exit_code, output, errput = svntest.main.run_svn(None, "changelist",
                                                   "--remove", wc_dir)
  verify_changelist_output(output) # nothing expected

  # svn changelist --remove WC_DIR --depth files
  exit_code, output, errput = svntest.main.run_svn(None, "changelist",
                                                   "--remove",
                                                   "--depth", "files",
                                                   wc_dir)
  expected_removals = {
    os.path.join(wc_dir, 'iota') : None,
    }
  verify_changelist_output(output, None, expected_removals)

  # svn changelist --remove WC_DIR --depth infinity
  exit_code, output, errput = svntest.main.run_svn(None, "changelist",
                                                   "--remove",
                                                   "--depth", "infinity",
                                                   wc_dir)
  expected_removals = {
      sbox.ospath('A/B/E/alpha') : None,
      sbox.ospath('A/B/E/beta') : None,
      sbox.ospath('A/B/lambda') : None,
      sbox.ospath('A/D/G/pi') : None,
      sbox.ospath('A/D/G/rho') : None,
      sbox.ospath('A/D/G/tau') : None,
      sbox.ospath('A/D/H/chi') : None,
      sbox.ospath('A/D/H/omega') : None,
      sbox.ospath('A/D/H/psi') : None,
      sbox.ospath('A/D/gamma') : None,
      sbox.ospath('A/mu') : None,
    }
  verify_changelist_output(output, None, expected_removals)

  ### Add files to changelists based on the last character in their names ###

  changelist_all_files(wc_dir, clname_from_lastchar_cb)

  ### Now, do selective changelist removal ###

  # svn changelist --remove WC_DIR --depth infinity --changelist a
  exit_code, output, errput = svntest.main.run_svn(None, "changelist",
                                                   "--remove",
                                                   "--depth", "infinity",
                                                   "--changelist", "a",
                                                   wc_dir)
  expected_removals = {
      sbox.ospath('A/B/E/alpha') : None,
      sbox.ospath('A/B/E/beta') : None,
      sbox.ospath('A/B/lambda') : None,
      sbox.ospath('A/D/H/omega') : None,
      sbox.ospath('A/D/gamma') : None,
      sbox.ospath('iota') : None,
    }
  verify_changelist_output(output, None, expected_removals)

  # svn changelist --remove WC_DIR --depth infinity
  #     --changelist i --changelist o
  exit_code, output, errput = svntest.main.run_svn(None, "changelist",
                                                   "--remove",
                                                   "--depth", "infinity",
                                                   "--changelist", "i",
                                                   "--changelist", "o",
                                                   wc_dir)
  expected_removals = {
      sbox.ospath('A/D/G/pi') : None,
      sbox.ospath('A/D/G/rho') : None,
      sbox.ospath('A/D/H/chi') : None,
      sbox.ospath('A/D/H/psi') : None,
    }
  verify_changelist_output(output, None, expected_removals)

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

def commit_one_changelist(sbox):
  "commit with single --changelist"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Add a line of text to all the versioned files in the tree.
  mod_all_files(wc_dir, "New text.\n")

  # Add files to changelists based on the last character in their names.
  changelist_all_files(wc_dir, clname_from_lastchar_cb)

  # Now, test a commit that uses a single changelist filter (--changelist a).
  expected_output = svntest.wc.State(wc_dir, {
    'A/B/lambda' : Item(verb='Sending'),
    'A/B/E/alpha' : Item(verb='Sending'),
    'A/B/E/beta' : Item(verb='Sending'),
    'A/D/gamma' : Item(verb='Sending'),
    'A/D/H/omega' : Item(verb='Sending'),
    'iota' : Item(verb='Sending'),
    })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/mu', 'A/D/G/tau', 'A/D/G/pi', 'A/D/H/chi',
                        'A/D/H/psi', 'A/D/G/rho', wc_rev=1, status='M ')
  expected_status.tweak('iota', 'A/B/lambda', 'A/B/E/alpha', 'A/B/E/beta',
                        'A/D/gamma', 'A/D/H/omega', wc_rev=2, status='  ')
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        [],
                                        wc_dir,
                                        "--changelist",
                                        "a")

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

def commit_multiple_changelists(sbox):
  "commit with multiple --changelist's"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Add a line of text to all the versioned files in the tree.
  mod_all_files(wc_dir, "New text.\n")

  # Add files to changelists based on the last character in their names.
  changelist_all_files(wc_dir, clname_from_lastchar_cb)

  # Now, test a commit that uses multiple changelist filters
  # (--changelist=a --changelist=i).
  expected_output = svntest.wc.State(wc_dir, {
    'A/B/lambda' : Item(verb='Sending'),
    'A/B/E/alpha' : Item(verb='Sending'),
    'A/B/E/beta' : Item(verb='Sending'),
    'A/D/gamma' : Item(verb='Sending'),
    'A/D/H/omega' : Item(verb='Sending'),
    'iota' : Item(verb='Sending'),
    'A/D/G/pi' : Item(verb='Sending'),
    'A/D/H/chi' : Item(verb='Sending'),
    'A/D/H/psi' : Item(verb='Sending'),
    })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/mu', 'A/D/G/tau', 'A/D/G/rho',
                        wc_rev=1, status='M ')
  expected_status.tweak('iota', 'A/B/lambda', 'A/B/E/alpha', 'A/B/E/beta',
                        'A/D/gamma', 'A/D/H/omega', 'A/D/G/pi', 'A/D/H/chi',
                        'A/D/H/psi', wc_rev=2, status='  ')
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        [],
                                        wc_dir,
                                        "--changelist", "a",
                                        "--changelist", "i")

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

def info_with_changelists(sbox):
  "info --changelist"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Add files to changelists based on the last character in their names.
  changelist_all_files(wc_dir, clname_from_lastchar_cb)

  # Now, test various combinations of changelist specification and depths.
  for clname in [['a'], ['i'], ['a', 'i']]:
    for depth in [None, 'files', 'infinity']:

      # Figure out what we expect to see in our info output.
      expected_paths = []
      if 'a' in clname:
        if depth == 'infinity':
          expected_paths.append('A/B/lambda')
          expected_paths.append('A/B/E/alpha')
          expected_paths.append('A/B/E/beta')
          expected_paths.append('A/D/gamma')
          expected_paths.append('A/D/H/omega')
        if depth == 'files' or depth == 'infinity':
          expected_paths.append('iota')
      if 'i' in clname:
        if depth == 'infinity':
          expected_paths.append('A/D/G/pi')
          expected_paths.append('A/D/H/chi')
          expected_paths.append('A/D/H/psi')
      expected_paths = sorted([os.path.join(wc_dir, x.replace('/', os.sep)) for x in expected_paths])

      # Build the command line.
      args = ['info', wc_dir]
      for cl in clname:
        args.append('--changelist')
        args.append(cl)
      if depth:
        args.append('--depth')
        args.append(depth)

      # Run 'svn info ...'
      exit_code, output, errput = svntest.main.run_svn(None, *args)

      # Filter the output for lines that begin with 'Path:', and
      # reduce even those lines to just the actual path.
      paths = sorted([x[6:].rstrip() for x in output if x[:6] == 'Path: '])

      # And, compare!
      if (paths != expected_paths):
        raise svntest.Failure("Expected paths (%s) and actual paths (%s) "
                              "don't gel" % (str(expected_paths), str(paths)))

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

@XFail()
@Issue(4826)
def diff_with_changelists(sbox):
  "diff --changelist (wc-wc and repos-wc)"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Add a line of text to all the versioned files in the tree.
  mod_all_files(wc_dir, "New text.\n")

  # Also make a property modification on each directory.
  svntest.main.run_svn(None, 'propset', 'p', 'v', '-R', wc_dir)

  # Add files to changelists based on the last character in their names.
  changelist_all_files(wc_dir, clname_from_lastchar_cb)

  # Now, test various combinations of changelist specification and depths.
  for is_repos_wc in [0, 1]:
    for clname in [['a'], ['a', 'i'], []]:
      for depth in ['empty', 'files', 'immediates', 'infinity', None]:
       for subdir in ['.', 'A', 'A/D']:

        # Figure out what we expect to see in our diff output.
        expected_paths = sorted(select_paths(sbox.ospath(subdir), depth, clname, clname_from_lastchar_cb))

        # Build the command line.
        args = ['diff']
        for cl in clname:
          args.append('--changelist')
          args.append(cl)
        if depth:
          args.append('--depth')
          args.append(depth)
        if is_repos_wc:
          args.append('--old')
          args.append(sbox.repo_url + '/' + subdir)
          args.append('--new')
          args.append(os.path.join(wc_dir, subdir))
        else:
          args.append(os.path.join(wc_dir, subdir))

        # Run 'svn diff ...'
        exit_code, output, errput = svntest.main.run_svn(None, *args)

        # Filter the output for lines that begin with 'Index:', and
        # reduce even those lines to just the actual path.
        paths = sorted([x[7:].rstrip() for x in output if x[:7] == 'Index: '])

        # Diff output on Win32 uses '/' path separators.
        if sys.platform == 'win32':
          paths = [x.replace('/', os.sep) for x in paths]

        # And, compare!
        if (paths != expected_paths):
          raise svntest.Failure("Expected paths (%s) and actual paths (%s) "
                                "don't gel"
                                % (str(expected_paths), str(paths)))

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

def propmods_with_changelists(sbox):
  "propset/del/get/list --changelist"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Add files to changelists based on the last character in their names.
  changelist_all_files(wc_dir, clname_from_lastchar_cb)

  # Set property 'name'='value' on all working copy items.
  svntest.main.run_svn(None, "pset", "--depth", "infinity",
                       "name", "value", wc_dir)
  expected_disk = svntest.main.greek_state.copy()
  expected_disk.add({'' : Item(props={ 'name' : 'value' })})
  expected_disk.tweak('A', 'A/B', 'A/B/E', 'A/B/E/alpha', 'A/B/E/beta',
                      'A/B/F', 'A/B/lambda', 'A/C', 'A/D', 'A/D/G',
                      'A/D/G/pi', 'A/D/G/rho', 'A/D/G/tau', 'A/D/H',
                      'A/D/H/chi', 'A/D/H/omega', 'A/D/H/psi', 'A/D/gamma',
                      'A/mu', 'iota', props={ 'name' : 'value' })

  svntest.actions.verify_disk(wc_dir, expected_disk, True)

  # Proplist the 'i' changelist
  exit_code, output, errput = svntest.main.run_svn(None, "proplist", "--depth",
                                                   "infinity", "--changelist",
                                                   "i", wc_dir)
  ### Really simple sanity check on the output of 'proplist'.  If we've got
  ### a proper proplist content checker anywhere, we should probably use it
  ### instead.
  if len(output) != 6:
    raise svntest.Failure

  # Remove the 'name' property from files in the 'o' and 'i' changelists.
  svntest.main.run_svn(None, "pdel", "--depth", "infinity",
                       "name", "--changelist", "o", "--changelist", "i",
                       wc_dir)
  expected_disk.tweak('A/D/G/pi', 'A/D/G/rho', 'A/D/H/chi', 'A/D/H/psi',
                      props={})
  svntest.actions.verify_disk(wc_dir, expected_disk, True)

  # Add 'foo'='bar' property on all files under A/B to depth files and
  # in changelist 'a'.
  svntest.main.run_svn(None, "pset", "--depth", "files",
                       "foo", "bar", "--changelist", "a",
                       os.path.join(wc_dir, 'A', 'B'))
  expected_disk.tweak('A/B/lambda', props={ 'name' : 'value',
                                            'foo'  : 'bar' })
  svntest.actions.verify_disk(wc_dir, expected_disk, True)

  # Add 'bloo'='blarg' property to all files in changelist 'a'.
  svntest.main.run_svn(None, "pset", "--depth", "infinity",
                       "bloo", "blarg", "--changelist", "a",
                       wc_dir)
  expected_disk.tweak('A/B/lambda', props={ 'name' : 'value',
                                            'foo'  : 'bar',
                                            'bloo' : 'blarg' })
  expected_disk.tweak('A/B/E/alpha', 'A/B/E/beta', 'A/D/H/omega', 'A/D/gamma',
                      'iota', props={ 'name' : 'value',
                                      'bloo' : 'blarg' })
  svntest.actions.verify_disk(wc_dir, expected_disk, True)

  # Propget 'name' in files in changelists 'a' and 'i' to depth files.
  exit_code, output, errput = svntest.main.run_svn(None, "pget",
                                                   "--depth", "files", "name",
                                                   "--changelist", "a",
                                                   "--changelist", "i",
                                                   wc_dir)
  verify_pget_output(output, {
    os.path.join(wc_dir, 'iota') : 'value',
    })

  # Propget 'name' in files in changelists 'a' and 'i' to depth infinity.
  exit_code, output, errput = svntest.main.run_svn(None, "pget",
                                                   "--depth", "infinity",
                                                   "name",
                                                   "--changelist", "a",
                                                   "--changelist", "i",
                                                   wc_dir)
  verify_pget_output(output, {
    os.path.join(wc_dir, 'A', 'D', 'gamma')      : 'value',
    os.path.join(wc_dir, 'A', 'B', 'E', 'alpha') : 'value',
    os.path.join(wc_dir, 'iota')                 : 'value',
    os.path.join(wc_dir, 'A', 'B', 'E', 'beta')  : 'value',
    os.path.join(wc_dir, 'A', 'B', 'lambda')     : 'value',
    os.path.join(wc_dir, 'A', 'D', 'H', 'omega') : 'value',
    })


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

def revert_with_changelists(sbox):
  "revert --changelist"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Add files to changelists based on the last character in their names.
  changelist_all_files(wc_dir, clname_from_lastchar_cb)

  # Add a line of text to all the versioned files in the tree.
  mod_all_files(wc_dir, "Please, oh please, revert me!\n")
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  expected_status.tweak('A/B/lambda', 'A/B/E/alpha', 'A/B/E/beta',
                        'A/D/gamma', 'A/D/H/omega', 'iota', 'A/mu',
                        'A/D/G/tau', 'A/D/G/pi', 'A/D/H/chi',
                        'A/D/H/psi', 'A/D/G/rho', status='M ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # 'svn revert --changelist a WC_DIR' (without depth, no change expected)
  svntest.main.run_svn(None, "revert", "--changelist", "a", wc_dir)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # 'svn revert --changelist o --depth files WC_DIR WC_DIR/A/B' (no change)
  svntest.main.run_svn(None, "revert", "--depth", "files",
                       "--changelist", "o",
                       wc_dir, os.path.join(wc_dir, 'A', 'B'))
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # 'svn revert --changelist a --depth files WC_DIR WC_DIR/A/B'
  # (iota, lambda reverted)
  svntest.main.run_svn(None, "revert", "--depth", "files",
                       "--changelist", "a",
                       wc_dir, os.path.join(wc_dir, 'A', 'B'))
  expected_status.tweak('iota', 'A/B/lambda', status='  ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # 'svn revert --changelist a --changelist i --depth infinity WC_DIR'
  # (alpha, beta, gamma, omega, pi, chi, psi reverted)
  svntest.main.run_svn(None, "revert", "--depth", "infinity",
                       "--changelist", "a", "--changelist", "i",
                       wc_dir)
  expected_status.tweak('A/B/E/alpha', 'A/B/E/beta', 'A/D/gamma',
                        'A/D/H/omega', 'A/D/G/pi', 'A/D/H/chi',
                        'A/D/H/psi', status='  ')
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # 'svn revert --depth infinity WC_DIR' (back to pristine-ness)
  svntest.main.run_svn(None, "revert", "--depth", "infinity",
                       wc_dir)
  expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
  svntest.actions.run_and_verify_status(wc_dir, expected_status)

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

def update_with_changelists(sbox):
  "update --changelist"

  sbox.build()
  wc_dir = sbox.wc_dir

  # Add a line of text to all the versioned files in the tree, commit, update.
  mod_all_files(wc_dir, "Added line.\n")
  svntest.main.run_svn(None, "commit", "-m", "logmsg", wc_dir)
  svntest.main.run_svn(None, "update", wc_dir)

  # Add files to changelists based on the last character in their names.
  changelist_all_files(wc_dir, clname_from_lastchar_cb)

  ### Backdate only the files in the 'a' and 'i' changelists at depth
  ### files under WC_DIR and WC_DIR/A/B.

  # We expect update to only touch lambda and iota.
  expected_output = svntest.wc.State(wc_dir, {
    'A/B/lambda' : Item(status='U '),
    'iota' : Item(status='U '),
    })

  # Disk state should have all the files except iota and lambda
  # carrying new text.
  expected_disk = svntest.main.greek_state.copy()
  expected_disk.tweak('A/B/E/alpha',
                      contents="This is the file 'alpha'.\nAdded line.\n")
  expected_disk.tweak('A/B/E/beta',
                      contents="This is the file 'beta'.\nAdded line.\n")
  expected_disk.tweak('A/D/gamma',
                      contents="This is the file 'gamma'.\nAdded line.\n")
  expected_disk.tweak('A/D/H/omega',
                      contents="This is the file 'omega'.\nAdded line.\n")
  expected_disk.tweak('A/mu',
                      contents="This is the file 'mu'.\nAdded line.\n")
  expected_disk.tweak('A/D/G/tau',
                      contents="This is the file 'tau'.\nAdded line.\n")
  expected_disk.tweak('A/D/G/pi',
                      contents="This is the file 'pi'.\nAdded line.\n")
  expected_disk.tweak('A/D/H/chi',
                      contents="This is the file 'chi'.\nAdded line.\n")
  expected_disk.tweak('A/D/H/psi',
                      contents="This is the file 'psi'.\nAdded line.\n")
  expected_disk.tweak('A/D/G/rho',
                      contents="This is the file 'rho'.\nAdded line.\n")

  # Status is clean, but with iota and lambda at r1 and all else at r2.
  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.tweak('iota', 'A/B/lambda', wc_rev=1)

  # Update.
  svntest.actions.run_and_verify_update(wc_dir,
                                        expected_output,
                                        expected_disk,
                                        expected_status,
                                        [], True,
                                        "-r", "1",
                                        "--changelist", "a",
                                        "--changelist", "i",
                                        "--depth", "files",
                                        wc_dir,
                                        os.path.join(wc_dir, 'A', 'B'))

  ### Backdate to depth infinity all changelists "a", "i", and "o" now.

  # We expect update to only touch all the files ending in 'a', 'i',
  # and 'o' (except lambda and iota which were previously updated).
  expected_output = svntest.wc.State(wc_dir, {
    'A/D/G/pi' : Item(status='U '),
    'A/D/H/chi' : Item(status='U '),
    'A/D/H/psi' : Item(status='U '),
    'A/D/G/rho' : Item(status='U '),
    'A/B/E/alpha' : Item(status='U '),
    'A/B/E/beta' : Item(status='U '),
    'A/D/gamma' : Item(status='U '),
    'A/D/H/omega' : Item(status='U '),
    })

  # Disk state should have only tau and mu carrying new text.
  expected_disk = svntest.main.greek_state.copy()
  expected_disk.tweak('A/mu',
                      contents="This is the file 'mu'.\nAdded line.\n")
  expected_disk.tweak('A/D/G/tau',
                      contents="This is the file 'tau'.\nAdded line.\n")

  # Status is clean, but with iota and lambda at r1 and all else at r2.
  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.tweak('iota', 'A/B/lambda', 'A/D/G/pi', 'A/D/H/chi',
                        'A/D/H/psi', 'A/D/G/rho', 'A/B/E/alpha',
                        'A/B/E/beta', 'A/D/gamma', 'A/D/H/omega', wc_rev=1)

  # Update.
  svntest.actions.run_and_verify_update(wc_dir,
                                        expected_output,
                                        expected_disk,
                                        expected_status,
                                        [], True,
                                        "-r", "1",
                                        "--changelist", "a",
                                        "--changelist", "i",
                                        "--changelist", "o",
                                        "--depth", "infinity",
                                        wc_dir)

def tree_conflicts_and_changelists_on_commit1(sbox):
  "tree conflicts, changelists and commit"
  svntest.actions.build_greek_tree_conflicts(sbox)
  wc_dir = sbox.wc_dir

  iota = os.path.join(wc_dir, "iota")
  rho = os.path.join(wc_dir, "A", "D", "G", "rho")

  # This file will ultimately be committed
  svntest.main.file_append(iota, "More stuff in iota")

  # Verify that the commit is blocked when we include a tree-conflicted
  # item.
  svntest.main.run_svn(None, "changelist", "list", iota, rho)

  expected_error = ("svn: E155015: Aborting commit: '.*" + re.escape(rho)
                    + "' remains in .*conflict")

  svntest.actions.run_and_verify_commit(wc_dir,
                                        None, None,
                                        expected_error,
                                        wc_dir,
                                        "--changelist",
                                        "list")

  # Now, test if we can commit iota without those tree-conflicts
  # getting in the way.
  svntest.main.run_svn(None, "changelist", "--remove", rho)

  expected_output = svntest.wc.State(wc_dir, {
    'iota' : Item(verb='Sending'),
    })
  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.tweak('A/D/G/pi', status='D ', treeconflict='C')
  expected_status.tweak('A/D/G/tau', status='! ', treeconflict='C',
                        wc_rev=None)
  expected_status.tweak('A/D/G/rho', status='A ', copied='+',
                        treeconflict='C', wc_rev='-')
  expected_status.tweak('iota', wc_rev=3, status='  ')
  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        [],
                                        wc_dir,
                                        "--changelist",
                                        "list")


def tree_conflicts_and_changelists_on_commit2(sbox):
  "more tree conflicts, changelists and commit"

  sbox.build()
  wc_dir = sbox.wc_dir

  iota = os.path.join(wc_dir, "iota")
  A = os.path.join(wc_dir, "A",)
  C = os.path.join(A, "C")

  # Make a tree-conflict on A/C:
  # Remove it, warp back, add a prop, update.
  svntest.main.run_svn(None, 'delete', C)

  expected_output = svntest.verify.RegexOutput(
                                     "Deleting.*" + re.escape(C),
                                     False)
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'commit', '-m', 'delete A/C', C)

  expected_output = svntest.verify.RegexOutput(
                                     "A.*" + re.escape(C), False)
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'update', C, "-r1")

  expected_output = svntest.verify.RegexOutput(
                                     ".*'propname' set on '"
                                     + re.escape(C) + "'", False)
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'propset', 'propname', 'propval', C)

  expected_output = svntest.verify.RegexOutput(
                                     "   C " + re.escape(C), False)
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'update', wc_dir)


  expected_status = svntest.actions.get_virginal_state(wc_dir, 2)
  expected_status.tweak('A/C', status='A ', copied='+',
                        treeconflict='C', wc_rev='-')

  svntest.actions.run_and_verify_status(wc_dir, expected_status)

  # So far so good. We have a tree-conflict on an absent dir A/C.

  # Verify that the current situation does not commit.
  expected_error = "svn: E155015: Aborting commit:.* remains in .*conflict";

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

  # Now try to commit with a changelist, not letting the
  # tree-conflict get in the way.
  svntest.main.file_append(iota, "More stuff in iota")
  svntest.main.run_svn(None, "changelist", "list", iota)

  expected_output = svntest.wc.State(wc_dir, {
    'iota' : Item(verb='Sending'),
    })

  expected_status.tweak('iota', wc_rev=3, status='  ')

  svntest.actions.run_and_verify_commit(wc_dir,
                                        expected_output,
                                        expected_status,
                                        [],
                                        wc_dir,
                                        "--changelist",
                                        "list")


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

def move_keeps_changelist(sbox):
  "'svn mv' of existing keeps the changelist"

  sbox.build(read_only = True)
  wc_dir = sbox.wc_dir
  iota_path  = os.path.join(wc_dir, 'iota')
  iota2_path = iota_path + '2'

  # 'svn mv' of existing file should *copy* the changelist to the new place
  svntest.main.run_svn(None, "changelist", 'foo', iota_path)
  svntest.main.run_svn(None, "rename", iota_path, iota2_path)
  expected_infos = [
    {
      'Name' : 'iota',
      'Schedule' : 'delete',
      'Changelist' : 'foo',
    },
    {
      'Name' : 'iota2',
      'Schedule' : 'add',
      'Changelist' : 'foo',  # this line fails the test
    },
  ]
  svntest.actions.run_and_verify_info(expected_infos, iota_path, iota2_path)

def move_added_keeps_changelist(sbox):
  "'svn mv' of added keeps the changelist"
  sbox.build(read_only = True)
  wc_dir = sbox.wc_dir
  repo_url = sbox.repo_url

  kappa_path  = os.path.join(wc_dir, 'kappa')
  kappa2_path = kappa_path + '2'

  # add 'kappa' (do not commit!)
  svntest.main.file_write(kappa_path, "This is the file 'kappa'.\n")
  svntest.main.run_svn(None, 'add', kappa_path)

  # 'svn mv' of added file should *move* the changelist to the new place
  svntest.main.run_svn(None, "changelist", 'foo', kappa_path)
  svntest.main.run_svn(None, "rename", kappa_path, kappa2_path)

  # kappa not under version control
  svntest.actions.run_and_verify_svnversion(kappa_path, repo_url,
                                            [], ".*doesn't exist.*")
  # kappa2 in a changelist
  expected_infos = [
    {
      'Name' : 'kappa2',
      'Schedule' : 'add',
      'Changelist' : 'foo',  # this line fails the test
    },
  ]
  svntest.actions.run_and_verify_info(expected_infos, kappa2_path)

@Issue(3820)
def change_to_dir(sbox):
  "change file in changelist to dir"

  sbox.build()

  # No changelist initially
  expected_infos = [{'Name' : 'mu', 'Changelist' : None}]
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu visible in changelist
  svntest.actions.run_and_verify_svn(None, [],
                                     'changelist', 'qq', sbox.ospath('A/mu'))
  expected_infos = [{'Name' : 'mu', 'Changelist' : 'qq'}]
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu still visible after delete
  svntest.actions.run_and_verify_svn(None, [], 'rm', sbox.ospath('A/mu'))
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu removed from changelist after replace with directory
  svntest.actions.run_and_verify_svn('^A|' + _re_cl_rem_pattern, [],
                                     'mkdir', sbox.ospath('A/mu'))
  expected_infos = [{'Changelist' : None}] # No Name for directories?
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  svntest.main.run_svn(None, "commit", "-m", "r2: replace A/mu: file->dir",
                       sbox.ospath('A'))
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  svntest.main.run_svn(None, "update", "-r", "1", sbox.ospath('A'))
  expected_infos = [{'Name' : 'mu', 'Changelist' : None}]
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu visible in changelist
  svntest.actions.run_and_verify_svn(None, [],
                                     'changelist', 'qq', sbox.ospath('A/mu'))
  expected_infos = [{'Name' : 'mu', 'Changelist' : 'qq'}]
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu removed from changelist after replace with dir via merge
  svntest.main.run_svn(None, "merge", "-c", "2", sbox.ospath('A'),
                       sbox.ospath('A'))
  expected_infos = [{'Changelist' : None}] # No Name for directories?
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))


@Issue(3822)
def revert_deleted_in_changelist(sbox):
  "revert a deleted file in a changelist"

  sbox.build(read_only = True)

  # No changelist initially
  expected_infos = [{'Name' : 'mu', 'Changelist' : None}]
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu visible in changelist
  svntest.actions.run_and_verify_svn(None, [],
                                     'changelist', 'qq', sbox.ospath('A/mu'))
  expected_infos = [{'Name' : 'mu', 'Changelist' : 'qq'}]
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu still visible after delete
  svntest.actions.run_and_verify_svn(None, [], 'rm', sbox.ospath('A/mu'))
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu still visible after revert
  svntest.actions.run_and_verify_svn(None, [],
                                     'revert', sbox.ospath('A/mu'))
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu still visible after parent delete
  svntest.actions.run_and_verify_svn(None, [], 'rm', sbox.ospath('A'))
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

  # A/mu still visible after revert
  svntest.actions.run_and_verify_svn(None, [],
                                     'revert', '-R', sbox.ospath('A'))
  svntest.actions.run_and_verify_info(expected_infos, sbox.ospath('A/mu'))

def add_remove_non_existent_target(sbox):
  "add and remove non-existent target to changelist"

  sbox.build(read_only = True)
  wc_dir = sbox.wc_dir
  bogus_path = os.path.join(wc_dir, 'A', 'bogus')

  expected_err = "svn: warning: W155010: The node '" + \
                 re.escape(os.path.abspath(bogus_path)) + \
                 "' was not found"

  svntest.actions.run_and_verify_svn(None, expected_err,
                                     'changelist', 'testlist',
                                     bogus_path)

  svntest.actions.run_and_verify_svn(None, expected_err,
                                     'changelist', bogus_path,
                                      '--remove')

def add_remove_unversioned_target(sbox):
  "add and remove unversioned target to changelist"

  sbox.build(read_only = True)
  unversioned = sbox.ospath('unversioned')
  svntest.main.file_write(unversioned, "dummy contents", 'w+')

  expected_err = "svn: warning: W155010: The node '" + \
                 re.escape(os.path.abspath(unversioned)) + \
                 "' was not found"

  svntest.actions.run_and_verify_svn(None, expected_err,
                                     'changelist', 'testlist',
                                     unversioned)

  svntest.actions.run_and_verify_svn(None, expected_err,
                                     'changelist', unversioned,
                                      '--remove')

@Issue(3985)
def readd_after_revert(sbox):
  "add new file to changelist, revert and readd"
  sbox.build(read_only = True)

  dummy = sbox.ospath('dummy')
  svntest.main.file_write(dummy, "dummy contents")

  sbox.simple_add('dummy')
  svntest.actions.run_and_verify_svn(None, [],
                                     'changelist', 'testlist',
                                     dummy)

  sbox.simple_revert('dummy')

  svntest.main.file_write(dummy, "dummy contents")

  svntest.actions.run_and_verify_svn(None, [],
                                     'add', dummy)

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

# A wc-wc diff returned no results if changelists were specified and the
# diff target dir was not the WC root.
@Issue(4822)
def diff_with_changelists_subdir(sbox):
  "diff --changelist (wc-wc) in subdir of WC"

  sbox.build()
  wc_dir = sbox.wc_dir

  expected_paths = sbox.ospaths(['A/D/gamma'])
  subdir = 'A/D'
  clname = 'a'

  for path in expected_paths:
    svntest.main.file_append(path, "New text.\n")
  svntest.main.run_svn(None, "changelist", clname, *expected_paths)

  # Run 'svn diff ...'
  exit_code, output, errput = svntest.main.run_svn(None,
                                'diff', '--changelist', clname,
                                sbox.ospath(subdir))

  # Filter the output for lines that begin with 'Index:', and
  # reduce even those lines to just the actual path.
  paths = sorted([x[7:].rstrip() for x in output if x[:7] == 'Index: '])

  # Diff output on Win32 uses '/' path separators.
  if sys.platform == 'win32':
    paths = [x.replace('/', os.sep) for x in paths]

  # And, compare!
  if (paths != expected_paths):
    raise svntest.Failure("Expected paths (%s) and actual paths (%s) "
                          "don't gel"
                          % (str(expected_paths), str(paths)))


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

# list all tests here, starting with None:
test_list = [ None,
              add_remove_changelists,
              commit_one_changelist,
              commit_multiple_changelists,
              info_with_changelists,
              diff_with_changelists,
              propmods_with_changelists,
              revert_with_changelists,
              update_with_changelists,
              tree_conflicts_and_changelists_on_commit1,
              tree_conflicts_and_changelists_on_commit2,
              move_keeps_changelist,
              move_added_keeps_changelist,
              change_to_dir,
              revert_deleted_in_changelist,
              add_remove_non_existent_target,
              add_remove_unversioned_target,
              readd_after_revert,
              diff_with_changelists_subdir,
             ]

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


### End of file.
