| #!/usr/bin/env python |
| |
| # ==================================================================== |
| # Copyright (c) 2006 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/. |
| # ==================================================================== |
| |
| |
| """\ |
| remove-zombie-locks.py - remove zombie locks on deleted files |
| |
| Usage: remove-zombie-locks.py REPOS-PATH <REVISION|all> |
| |
| When REVISION (an interger) is specified this script scans a committed |
| revision for deleted files and checks if a lock exists for any of these |
| files. If any locks exist they are forcibly removed. |
| |
| When "all" is specified this script scans the whole repository for |
| locks on files that don't exist in the HEAD revision, removing any found. |
| |
| This script is a workaround for Subversion issue #2507 |
| https://issues.apache.org/jira/browse/SVN-2507 |
| |
| Examples: |
| |
| As a post-commit Hook script to prevent zombie locks: |
| remove-zombie-locks.py /var/svn/myrepo 6174 |
| |
| To clean a repository with existing zombie locks: |
| remove-zombie-locks.py /var/svn/myrepo all |
| |
| For additional information read the commented notes in this script. |
| """ |
| |
| # |
| # ** FAQ ** |
| # |
| # What is the problem, exactly? |
| # |
| # When you commit a file deletion with the --keep-locks option then |
| # though the file is deleted in the repository, the lock is not. |
| # This is a bug in Subversion. If locks are left on deleted files |
| # then any future attempt to add a file of the same name or |
| # delete/move/rename any parent directory will fail. This is very |
| # difficult for the end-user to fix since there's no easy way to find |
| # out the names of these zombie locks, and it is not simple to remove |
| # them even if you do know the names. |
| # |
| # Is this script 100% safe? |
| # |
| # There is a theoretical and very small chance that before this |
| # script runs another commit adds a file with the same path as one |
| # just deleted in this revision, and then a lock is acquired for it, |
| # resulting in this script unlocking the "wrong" file. In practice it |
| # seems highly improbable and would require very strange performance |
| # characteristics on your svn server. However, to minimize the |
| # window for error it is recommended to run this script first in your |
| # post-commit hook. |
| # |
| # How Do I Start Using This Script? |
| # |
| # 1. Once only, run this script in 'all' mode to start your repo out clean |
| # 2. Call this script from your post-commit hook to keep your repo clean |
| # |
| |
| import os |
| import sys |
| |
| import svn.core |
| import svn.repos |
| import svn.fs |
| |
| assert (svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR) >= (1, 2), \ |
| "Subversion 1.2 or later required but only have " \ |
| + str(svn.core.SVN_VER_MAJOR) + "." + str(svn.core.SVN_VER_MINOR) |
| |
| def usage_and_exit(): |
| sys.stderr.write(__doc__ + "\n") |
| sys.exit(1) |
| |
| class RepositoryZombieLockRemover: |
| """Remove all locks on non-existent files in repository@HEAD""" |
| def __init__(self, repos_path, repos_subpath=""): |
| self.repos_path = repos_path # path to repository on disk |
| self.repos_subpath = repos_subpath # if only cleaning part of the repo |
| |
| # init svn |
| svn.core.apr_initialize() |
| self.pool = svn.core.svn_pool_create(None) |
| self.repos_ptr = svn.repos.open(self.repos_path, self.pool) |
| self.fs_ptr = svn.repos.fs(self.repos_ptr) |
| self.fs_type = svn.fs.type(svn.repos.db_env(self.repos_ptr)) |
| self.rev_root = svn.fs.revision_root(self.fs_ptr, |
| svn.fs.youngest_rev(self.fs_ptr, |
| self.pool), |
| self.pool) |
| |
| def __del__(self): |
| svn.core.svn_pool_destroy(self.pool) |
| svn.core.apr_terminate() |
| |
| def unlock_nonexistent_files(self, lock, callback_pool): |
| """check if the file still exists in HEAD, removing the lock if not""" |
| if svn.fs.svn_fs_check_path(self.rev_root, lock.path, callback_pool) \ |
| == svn.core.svn_node_none: |
| print(lock.path) |
| svn.repos.svn_repos_fs_unlock(self.repos_ptr, lock.path, lock.token, |
| True, callback_pool) |
| |
| def run(self): |
| """iterate over every locked file in repo_path/repo_subpath, |
| calling unlock_nonexistent_files for each""" |
| |
| print("Removing all zombie locks from repository at %s\n" \ |
| "This may take several minutes..." % self.repos_path) |
| |
| # Try to use svn_fs_get_locks2() if it's present, as it's believed |
| # to be problem-free. |
| # |
| # If not, use svn_fs_get_locks(). But note that Subversion's |
| # Berkeley DB implementation of svn_fs_get_locks() in 1.6 and |
| # prior doesn't allow reentry of its BDB-transaction-using APIs |
| # from within BDB-transaction-using APIs (such as |
| # svn_fs_get_locks()). So we have do this in two steps, first |
| # harvest the locks, then checking/removing them. |
| if hasattr(svn.fs, 'svn_fs_get_locks2'): |
| svn.fs.svn_fs_get_locks2(self.fs_ptr, self.repos_subpath, |
| svn.core.svn_depth_infinity, |
| self.unlock_nonexistent_files, self.pool) |
| else: |
| if self.fs_type == svn.fs.SVN_FS_TYPE_BDB: |
| self.locks = [] |
| def bdb_lock_callback(lock, callback_pool): |
| self.locks.append(lock) |
| svn.fs.svn_fs_get_locks(self.fs_ptr, self.repos_subpath, |
| bdb_lock_callback, self.pool) |
| subpool = svn.core.svn_pool_create(self.pool) |
| for lock in self.locks: |
| svn.core.svn_pool_clear(subpool) |
| self.unlock_nonexistent_files(lock, subpool) |
| svn.core.svn_pool_destroy(subpool) |
| else: |
| svn.fs.svn_fs_get_locks(self.fs_ptr, self.repos_subpath, |
| self.unlock_nonexistent_files, self.pool) |
| print("Done.") |
| |
| |
| class RevisionZombieLockRemover: |
| """Remove all locks on files deleted in a revision""" |
| def __init__(self, repos_path, repos_rev): |
| self.repos_path = repos_path # path to repository on disk |
| |
| # init svn |
| svn.core.apr_initialize() |
| self.pool = svn.core.svn_pool_create(None) |
| self.repos_ptr = svn.repos.open(self.repos_path, self.pool) |
| self.fs_ptr = svn.repos.fs(self.repos_ptr) |
| self.rev_root = svn.fs.revision_root(self.fs_ptr, repos_rev, self.pool) |
| |
| def __del__(self): |
| svn.core.svn_pool_destroy(self.pool) |
| svn.core.apr_terminate() |
| |
| def get_deleted_paths(self): |
| """return list of deleted paths in a revision""" |
| deleted_paths = [] |
| for path, change in \ |
| svn.fs.paths_changed(self.rev_root, self.pool).iteritems(): |
| if (change.change_kind == svn.fs.path_change_delete): |
| deleted_paths.append(path) |
| return deleted_paths |
| |
| def run(self): |
| """remove any existing locks on files that are deleted in this revision""" |
| deleted_paths = self.get_deleted_paths() |
| subpool = svn.core.svn_pool_create(self.pool) |
| for path in deleted_paths: |
| svn.core.svn_pool_clear(subpool) |
| lock = svn.fs.svn_fs_get_lock(self.fs_ptr, path, subpool) |
| if lock: |
| svn.repos.svn_repos_fs_unlock(self.repos_ptr, path, |
| lock.token, True, subpool) |
| svn.core.svn_pool_destroy(subpool) |
| |
| |
| def main(): |
| if len(sys.argv) < 3: |
| usage_and_exit() |
| repos_path = os.path.abspath(sys.argv[1]) |
| if sys.argv[2].lower() == "all": |
| remover = RepositoryZombieLockRemover(repos_path, "") |
| else: |
| try: |
| repos_rev = int(sys.argv[2]) |
| except ValueError: |
| usage_and_exit() |
| remover = RevisionZombieLockRemover(repos_path, repos_rev) |
| |
| remover.run() |
| |
| sys.exit(0) |
| |
| |
| if __name__ == "__main__": |
| main() |
| |