#!/usr/bin/python # ==================================================================== # Copyright (c) 2000-2005 CollabNet. 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/. # ==================================================================== # $HeadURL: http://svn.collab.net/repos/svn/branches/1.4.x/contrib/hook-scripts/check-case-insensitive.py $ # $LastChangedDate: 2006-02-04 00:36:42 +0000 (Sat, 04 Feb 2006) $ # $LastChangedBy: sunny256 $ # $LastChangedRevision: 18331 $ # 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 \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 at tomes dot org dot uk import sys # Set this to point to your install of the Subversion languange bindings # for Python: #SVNLIB_DIR = r"C:/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.encode('utf-8'), 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)