[svn.haxx.se] · SVN Dev · SVN Users · SVN Org · TSVN Dev · TSVN Users · Subclipse Dev · Subclipse Users · this month's index

[PATCH] Pre-commit hook that stops case insensitive collisions (rewrite).

From: Martin Tomes <lists_at_tomes.org>
Date: 2005-07-14 11:55:43 CEST

This is a replacement for the script check-case-insensitive.pl which I
wrote some time ago. This is my first ever Python program to please be
gentle with me:-)

   + This should be faster than the Perl version.
   + It uses the Python language bindings.
   + It handles accented characters properly.
   + It won't miss any conflicts. (The Perl version can)

I have tested it on Linux and tried it briefly on a Windows server.

kfogel: I have commit access for check-case-insensitive.pl, does that
mean I can commit this if the response is favourable?

[[[
Pre-commit hook that stops case insensitive collisions.

This script can be called from a pre-commit hook on either Windows or a
Unix like operating system. It implements the checks required to ensure
that the repository acts in a way which is compatible with a case
insensitive file system.

When a file is added this script checks the file tree in the repository
for files which would be the same name on a case insensitive file system
and rejects the commit if there is a match.

    * tools/hook-scripts/check-case-insensitive.py
]]]

-- 
Martin Tomes
echo 'martin at tomes x org x uk'\
  | sed -e 's/ x /\./g' -e 's/ at /@/'
The Subversion Wiki is at http://www.subversionary.org/

#!/usr/bin/python
# ====================================================================
# Copyright (c) 2000-2005 Collab Net. All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals. For exact contribution history, see the revision
# history and logs, available at http://subversion.tigris.org/.
# ====================================================================

# This script can be called from a pre-commit hook on either Windows or a Unix
# like operating system. It implements the checks required to ensure that the
# repository acts in a way which is compatible with a case preserving but
# case insensitive file system.
#
# When a file is added this script checks the file tree in the repository for
# files which would be the same name on a case insensitive file system and
# rejects the commit.
#
# On a Unix system put this script in the hooks directory and add this to the
# pre-commit script:
#
# $REPOS/hooks/check-case-insensitive.py "$REPOS" "$TXN" || exit 1
#
# On a windows machine add this to pre-commit.bat:
#
# python <path-to-script>\check-case-insensitive.py %1 %2
# if errorlevel 1 goto :ERROR
# exit 0
# :ERROR
# echo Error found in commit 1>&2
# exit 1
#
# Make sure the python bindigs are installed and working on Windows. The zip
# file can be downloaded from the subversion site. The bindings depend on
# dll's shipped as part of the subversion binaries, if the script cannot load
# the _fs dll it is because it cannot find the other Subversion dll's.
#
# If you have any problems with this script feel free to contact
# Martin Tomes <martin@tomes.org.uk>

import sys

# Set this to point to your install of the Subversion languange bindings
# for Python:
#SVNLIB_DIR = r"C:/win32apps/svnpy/svn-win32-1.2.0/python/"
SVNLIB_DIR = r"/usr/local/lib/svn-python/"

if SVNLIB_DIR:
  sys.path.insert(0, SVNLIB_DIR)

import os.path
import string
from svn import fs, core, repos, delta

# Set this True for debug output.
debug = False
# An existat of 0 means all is well, 1 means there are name conflicts.
exitstat = 0

# This is stolen from the svnlook.py example. All that is not needed has been
# stripped out and it returns data rather than printing it.
class SVNLook:
  def __init__(self, pool, path, cmd, rev, txn):
    self.pool = pool

    repos_ptr = repos.open(path, pool)
    self.fs_ptr = repos.fs(repos_ptr)

    if txn:
      self.txn_ptr = fs.open_txn(self.fs_ptr, txn, pool)
    else:
      self.txn_ptr = None
      if rev is None:
        rev = fs.youngest_rev(self.fs_ptr, pool)
    self.rev = rev

  def cmd_changed(self):
    return self._print_tree(ChangedEditor, pass_root=1)

  def cmd_tree(self, rootpath):
    return self._print_tree(Editor, rootpath, base_rev=0)

  def _print_tree(self, e_factory, rootpath='', base_rev=None, pass_root=0):
    # It no longer prints, it returns the editor made by e_factory which
    # contains the tree in a list.
    if base_rev is None:
      # a specific base rev was not provided. use the transaction base,
      # or the previous revision
      if self.txn_ptr:
        base_rev = fs.txn_base_revision(self.txn_ptr)
      else:
        base_rev = self.rev - 1

    # get the current root
    if self.txn_ptr:
      root = fs.txn_root(self.txn_ptr, self.pool)
    else:
      root = fs.revision_root(self.fs_ptr, self.rev, self.pool)

    # the base of the comparison
    base_root = fs.revision_root(self.fs_ptr, base_rev, self.pool)

    if pass_root:
      editor = e_factory(root, base_root)
    else:
      editor = e_factory()

    # construct the editor for printing these things out
    e_ptr, e_baton = delta.make_editor(editor, self.pool)

    # compute the delta, printing as we go
    def authz_cb(root, path, pool):
      return 1
    repos.dir_delta(base_root, '', '', root, rootpath,
                    e_ptr, e_baton, authz_cb, 0, 1, 0, 0, self.pool)
    return editor

class ChangedEditor(delta.Editor):
  def __init__(self, root, base_root):
    self.root = root
    self.base_root = base_root
    self.addeddir = [];
    self.added = [];
    self.deleted = [];

  def open_root(self, base_revision, dir_pool):
    return [ 1, '' ]

  def delete_entry(self, path, revision, parent_baton, pool):
    ### need more logic to detect 'replace'
    if fs.is_dir(self.base_root, '/' + path, pool):
      self.deleted.append(path.decode('utf-8') + u'/')
    else:
      self.deleted.append(path.decode('utf-8'))

  def add_directory(self, path, parent_baton,
                    copyfrom_path, copyfrom_revision, dir_pool):
    self.addeddir.append(path.decode('utf-8'))
    return [ 0, path ]

  def open_directory(self, path, parent_baton, base_revision, dir_pool):
    return [ 1, path ]

  def change_dir_prop(self, dir_baton, name, value, pool):
    if dir_baton[0]:
      # the directory hasn't been printed yet. do it.
      #print '_U ' + dir_baton[1] + '/'
      dir_baton[0] = 0

  def add_file(self, path, parent_baton,
               copyfrom_path, copyfrom_revision, file_pool):
    self.added.append(path.decode('utf-8'))
    return [ '_', ' ', None ]

  def open_file(self, path, parent_baton, base_revision, file_pool):
    return [ '_', ' ', path ]

  def apply_textdelta(self, file_baton, base_checksum):
    file_baton[0] = 'U'

    # no handler
    return None

  def change_file_prop(self, file_baton, name, value, pool):
    file_baton[1] = 'U'

class Editor(delta.Editor):
  def __init__(self, root=None, base_root=None):
    self.root = root
    self.paths = {}
    # base_root ignored

  def add_directory(self, path, *args):
    lpath = string.lower(path.decode("utf-8"))
    if self.paths.has_key(lpath):
      self.paths[lpath] += 1
    else:
      self.paths[lpath] = 1

  # we cheat. one method implementation for two entry points.
  open_directory = add_directory

  def add_file(self, path, *args):
    lpath = string.lower(path.decode("utf-8"))
    if self.paths.has_key(lpath):
      self.paths[lpath] += 1
    else:
      self.paths[lpath] = 1
    #print >> sys.stderr, path

  # we cheat. one method implementation for two entry points.
  open_file = add_file

  def _get_id(self, path, pool):
    if self.root:
      id = fs.node_id(self.root, path, pool)
      return ' <%s>' % fs.unparse_id(id, pool)
    return ''

class CheckCase:
  """Check for case conflicts"""
  def __init__(self, pool, path, txn):
    self.pool = pool;
    repos_ptr = repos.open(path, pool)
    self.fs_ptr = repos.fs(repos_ptr)

    self.look = SVNLook(self.pool, path, 'changed', None, txn)

    # Get the list of files and directories which have been added.
    changed = self.look.cmd_changed()
    if debug:
      for item in changed.added + changed.addeddir:
        print >> sys.stderr, 'Adding: ' + item.encode('utf-8')
    if self.numadded(changed) != 0:
      # Find the part of the file tree which they live in.
      changedroot = self.findroot(changed)
      if debug:
        print >> sys.stderr, 'Changedroot is ' + changedroot.encode('utf-8')
      # Get that part of the file tree.
      tree = self.look.cmd_tree(changedroot)
  
      if debug:
        print >> sys.stderr, 'File tree:'
        for path in tree.paths.keys():
          print >> sys.stderr, ' [%d] %s len %d' % (tree.paths[path], path.encode('utf-8'), len(path))
  
      # If a member of the paths hash has a count of more than one there is a
      # case conflict.
      for path in tree.paths.keys():
        if tree.paths[path] > 1:
          # Find out if this is one of the files being added, if not ignore it.
          addedfile = self.showfile(path, changedroot, changed)
          if addedfile <> '':
            print >> sys.stderr, "Case conflict: " + addedfile.encode('utf-8')
            globals()["exitstat"] = 1

  def numadded(self, changed):
    return len(changed.added + changed.addeddir)

  def findroot(self, changed):
    """Find the part of the file tree which contains added files"""
    if debug:
      print >> sys.stderr, 'findroot'
    same = True
    pathpos = 0
    added = changed.added + changed.addeddir
    if len(added) == 0:
      return ''
    firstone = added[0].split('/')
    while same and (pathpos < len(firstone)):
      dir = firstone[pathpos]
      if debug:
        print >> sys.stderr, ' Path %d %s dir %s' % (pathpos, added[0].encode('utf-8'), dir.encode('utf-8'))
      for item in added[1:]:
        if debug:
          print >> sys.stderr, ' Path ' + item.encode('utf-8')
        if pathpos >= len(item.split('/')):
          if debug:
            print >> sys.stderr, ' Shorter'
          same = False
        else:
          dir2 = item.split('/')[pathpos]
          if dir != dir2:
            if debug:
              print >> sys.stderr, ' %s != %s' % (dir, dir2)
            same = False
      pathpos += 1
      if pathpos > 10:
        same = False
    return '/'.join(firstone[:pathpos-1])

  def showfile(self, path, changedroot, changed):
    """Find the path which conflicts"""
    if changedroot == '':
      changedpath = path
    else:
      changedpath = changedroot + '/' + path
    for added in changed.added:
      if (string.lower(added) == string.lower(changedpath)):
        return added
    for added in changed.addeddir:
      if (string.lower(added) == string.lower(changedpath)):
        return added
    return ''

if __name__ == "__main__":
  # Check for sane usage.
  if len(sys.argv) != 3:
    sys.stderr.write("Usage: REPOS TXN\n"
                     % (os.path.basename(sys.argv[0])))
    sys.exit(1)

  core.run_app(CheckCase, os.path.normpath(sys.argv[1]), sys.argv[2])
  sys.exit(exitstat)

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@subversion.tigris.org
For additional commands, e-mail: dev-help@subversion.tigris.org
Received on Thu Jul 14 11:56:32 2005

This is an archived mail posted to the Subversion Dev mailing list.