3 # ====================================================================
4 # Copyright (c) 2006 CollabNet. All rights reserved.
6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms
8 # are also available at http://subversion.tigris.org/license-1.html.
9 # If newer versions of this license are posted there, you may use a
10 # newer version instead, at your option.
12 # This software consists of voluntary contributions made by many
13 # individuals. For exact contribution history, see the revision
14 # history and logs, available at http://subversion.tigris.org/.
15 # ====================================================================
19 remove-zombie-locks.py - remove zombie locks on deleted files
21 Usage: remove-zombie-locks.py REPOS-PATH <REVISION|all>
23 When REVISION (an interger) is specified this script scans a commited
24 revision for deleted files and checks if a lock exists for any of these
25 files. If any locks exist they are forcibly removed.
27 When "all" is specified this script scans the whole repository for
28 locks on files that don't exist in the HEAD revision, removing any found.
30 This script is a workaround for Subversion issue #2507
31 http://subversion.tigris.org/issues/show_bug.cgi?id=2507
35 As a post-commit Hook script to prevent zombie locks:
36 remove-zombie-locks.py /var/svn/myrepo 6174
38 To clean a repository with existing zombie locks:
39 remove-zombie-locks.py /var/svn/myrepo all
41 For additional information read the commented notes in this script.
47 # What is the problem, exactly?
49 # When you commit a file deletion with the --keep-locks option then
50 # though the file is deleted in the repository, the lock is not.
51 # This is a bug in Subversion. If locks are left on deleted files
52 # then any future attempt to add a file of the same name or
53 # delete/move/rename any parent directory will fail. This is very
54 # difficult for the end-user to fix since there's no easy way to find
55 # out the names of these zombie locks, and it is not simple to remove
56 # them even if you do know the names.
58 # Is this script 100% safe?
60 # There is a theoretical and very small chance that before this
61 # script runs another commit adds a file with the same path as one
62 # just deleted in this revision, and then a lock is aquired for it,
63 # resulting in this script unlocking the "wrong" file. In practice it
64 # seems highly improbable and would require very strange performance
65 # characteristics on your svn server. However, to minimize the
66 # window for error it is recommended to run this script first in your
69 # How Do I Start Using This Script?
71 # 1. Once only, run this script in 'all' mode to start your repo out clean
72 # 2. Call this script from your post-commit hook to keep your repo clean
82 assert (svn
.core
.SVN_VER_MAJOR
, svn
.core
.SVN_VER_MINOR
) >= (1, 2), \
83 "Subversion 1.2 or later required but only have " \
84 + str(svn
.core
.SVN_VER_MAJOR
) + "." + str(svn
.core
.SVN_VER_MINOR
)
87 print >> sys
.stderr
, __doc__
90 class RepositoryZombieLockRemover
:
91 """Remove all locks on non-existant files in repository@HEAD"""
92 def __init__(self
, repos_path
, repos_subpath
=""):
93 self
.repos_path
= repos_path
# path to repository on disk
94 self
.repos_subpath
= repos_subpath
# if only cleaning part of the repo
97 svn
.core
.apr_initialize()
98 self
.pool
= svn
.core
.svn_pool_create(None)
99 self
.repos_ptr
= svn
.repos
.open(self
.repos_path
, self
.pool
)
100 self
.fs_ptr
= svn
.repos
.fs(self
.repos_ptr
)
101 self
.rev_root
= svn
.fs
.revision_root(self
.fs_ptr
,
102 svn
.fs
.youngest_rev(self
.fs_ptr
,
107 svn
.core
.svn_pool_destroy(self
.pool
)
108 svn
.core
.apr_terminate()
110 def unlock_nonexistant_files(self
, lock
, callback_pool
):
111 """check if the file still exists in HEAD, removing the lock if not"""
112 if svn
.fs
.svn_fs_check_path(self
.rev_root
, lock
.path
, callback_pool
) \
113 == svn
.core
.svn_node_none
:
115 svn
.repos
.svn_repos_fs_unlock(self
.repos_ptr
, lock
.path
, lock
.token
,
119 """iterate over every locked file in repo_path/repo_subpath,
120 calling unlock_nonexistant_files for each"""
122 print "Removing all zombie locks from repository at %s\n" \
123 "This may take several minutes..." % self
.repos_path
124 svn
.fs
.svn_fs_get_locks(self
.fs_ptr
, self
.repos_subpath
,
125 self
.unlock_nonexistant_files
, self
.pool
)
129 class RevisionZombieLockRemover
:
130 """Remove all locks on files deleted in a revision"""
131 def __init__(self
, repos_path
, repos_rev
):
132 self
.repos_path
= repos_path
# path to repository on disk
135 svn
.core
.apr_initialize()
136 self
.pool
= svn
.core
.svn_pool_create(None)
137 self
.repos_ptr
= svn
.repos
.open(self
.repos_path
, self
.pool
)
138 self
.fs_ptr
= svn
.repos
.fs(self
.repos_ptr
)
139 self
.rev_root
= svn
.fs
.revision_root(self
.fs_ptr
, repos_rev
, self
.pool
)
142 svn
.core
.svn_pool_destroy(self
.pool
)
143 svn
.core
.apr_terminate()
145 def get_deleted_paths(self
):
146 """return list of deleted paths in a revision"""
148 for path
, change
in \
149 svn
.fs
.paths_changed(self
.rev_root
, self
.pool
).iteritems():
150 if (change
.change_kind
== svn
.fs
.path_change_delete
):
151 deleted_paths
.append(path
)
155 """remove any existing locks on files that are deleted in this revision"""
156 deleted_paths
= self
.get_deleted_paths()
157 subpool
= svn
.core
.svn_pool_create(self
.pool
)
158 for path
in deleted_paths
:
159 svn
.core
.svn_pool_clear(subpool
)
160 lock
= svn
.fs
.svn_fs_get_lock(self
.fs_ptr
, path
, subpool
)
162 svn
.repos
.svn_repos_fs_unlock(self
.repos_ptr
, path
,
163 lock
.token
, True, subpool
)
164 svn
.core
.svn_pool_destroy(subpool
)
168 if len(sys
.argv
) < 3:
170 repos_path
= os
.path
.abspath(sys
.argv
[1])
171 if sys
.argv
[2].lower() == "all":
172 remover
= RepositoryZombieLockRemover(repos_path
, "")
175 repos_rev
= int(sys
.argv
[2])
178 remover
= RevisionZombieLockRemover(repos_path
, repos_rev
)
185 if __name__
== "__main__":