| #!/usr/bin/env python3 | |
| # Licensed under the same terms as Subversion. | |
| # A pre-commit hook to detect case-insensitive filename clashes. | |
| # | |
| # What this script does: | |
| # - Detects new paths that 'clash' with existing, or other new, paths. | |
| # - Ignores existings paths that already 'clash' | |
| # - Exits with an error code, and a diagnostic on stderr, if 'clashes' | |
| # are detected. | |
| # | |
| # How it does it: | |
| # - Get a list of changed paths. | |
| # - From that list extract the new paths that represent adds or replaces. | |
| # - For each new path: | |
| # - Split the path into a directory and a name. | |
| # - Get the names of all the entries in the version of the directory | |
| # within the txn. | |
| # - Compare the canonical new name with each canonical entry name. | |
| # - If the canonical names match and the pristine names do not match | |
| # then we have a 'clash'. | |
| # | |
| # Notes: | |
| # - All the paths from the Subversion filesystem bindings are encoded | |
| # in UTF-8 and the separator is '/' on all OS's. | |
| # - The canonical form determines what constitutes a 'clash', at present | |
| # a simple 'lower case' is used. That's probably not identical to the | |
| # behaviour of Windows or OSX, but it might be good enough. | |
| # - Hooks get invoked with an empty environment so this script explicitly | |
| # sets a locale; make sure it is a sensible value. | |
| # - If used with Apache the 'clash' diagnostic must be ASCII irrespective | |
| # of the locale, see the 'Force' comment near the end of the script for | |
| # one way to achieve this. | |
| # | |
| # How to call it: | |
| # | |
| # On a Unix system put this script in the hooks directory and add this to | |
| # the pre-commit script: | |
| # | |
| # $REPOS/hooks/case-insensitive.py "$REPOS" "$TXN" || exit 1 | |
| # | |
| # On a windows machine add this to pre-commit.bat: | |
| # | |
| # python <path-to-script>\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 bindings 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. | |
| # | |
| # $HeadURL$ | |
| # $LastChangedRevision$ | |
| # $LastChangedDate$ | |
| # $LastChangedBy$ | |
| import sys, locale | |
| sys.path.append('/usr/local/subversion/lib/svn-python') | |
| from svn import repos, fs | |
| locale.setlocale(locale.LC_ALL, 'en_GB') | |
| def canonicalize(path): | |
| return path.lower() | |
| def get_new_paths(txn_root): | |
| new_paths = [] | |
| for path, change in fs.paths_changed(txn_root).items(): | |
| if (change.change_kind == fs.path_change_add | |
| or change.change_kind == fs.path_change_replace): | |
| new_paths.append(path.decode('utf-8')) | |
| return new_paths | |
| def split_path(path): | |
| slash = path.rindex('/') | |
| if (slash == 0): | |
| return '/', path[1:] | |
| return path[:slash], path[slash+1:] | |
| def join_path(dir, name): | |
| if (dir == '/'): | |
| return '/' + name | |
| return dir + '/' + name | |
| def ensure_names(path, names, txn_root): | |
| if (path not in names): | |
| names[path] = [] | |
| for name, dirent in fs.dir_entries(txn_root, path).items(): | |
| name = name.decode('utf-8') | |
| names[path].append([canonicalize(name), name]) | |
| names = {} # map of: key - path, value - list of two element lists of names | |
| clashes = {} # map of: key - path, value - map of: key - path, value - dummy | |
| native = locale.getlocale()[1] | |
| if not native: native = 'ascii' | |
| repos_handle = repos.open(sys.argv[1].encode('utf-8')) | |
| fs_handle = repos.fs(repos_handle) | |
| txn_handle = fs.open_txn(fs_handle, sys.argv[2].encode('utf-8')) | |
| txn_root = fs.txn_root(txn_handle) | |
| new_paths = get_new_paths(txn_root) | |
| for path in new_paths: | |
| dir, name = split_path(path) | |
| canonical = canonicalize(name) | |
| ensure_names(dir, names, txn_root) | |
| for name_pair in names[dir]: | |
| if (name_pair[0] == canonical and name_pair[1] != name): | |
| canonical_path = join_path(dir, canonical) | |
| if (canonical_path not in clashes): | |
| clashes[canonical_path] = {} | |
| clashes[canonical_path][join_path(dir, name)] = True | |
| clashes[canonical_path][join_path(dir, name_pair[1])] = True | |
| if (clashes): | |
| # native = 'ascii' # Force ASCII output for Apache | |
| for canonical_path in clashes.keys(): | |
| sys.stderr.write('Clash:') | |
| for path in clashes[canonical_path].keys(): | |
| sys.stderr.write(' \'' + path + '\'') | |
| sys.stderr.write('\n') | |
| sys.exit(1) |