SVN Directory Lock
From Noah.org
Often it is useful to lock an entire directory tree to be sure that it remains consistent during testing or build deployment. SVN does not allow directories to be locked. You can lock individual files, but not an entire directory tree. Fortunately, it's easy to add this feature using a simple pre-commit hook script on the server.
The following pre-commit hook script will allow advisory locks. That is, a lock will prevent a commit, but any user may set or clear any lock. A user must deliberately clear and commit lock before further commits to the tree are allowed. This is intended to prevent accidental commits, and to track who modified a lock and why.
Someone familiar with other version control systems might ask why we don't simply tag or branch the trunk in order to test or build a release. The answer is because Subversion makes this very expensive in terms of time and storage space. A branch or tag doubles the amount of storage required for the entire directory tree. It doubles this every single time you branch or tag. So a directory lock is a cheap alternative to ensure that a source tree remains consistent during a given period of time. Nobody said Subversion was a very good version control system.
This script requires that pexpect be installed. Pexpect is a standard Debian/Ubuntu/RedHat package. This script does not use the pysvn library. The reason for this is because I got frustrated that the stupid library would not install on Red Hat Enterprise 4. I could get neither the RPM nor the source to install. It was far easier to use just Pexpect to script the command-line svn tools, which is after all what scripts are for.
Contents |
How to Use
To create a lock you just create and commit a lock property on a directory. After you commit the lock property no further commits will be allowed to the locked directory tree. The only exception is for any commit that deletes the lock itself. This is necessary to allow a directory to be unlocked.
Create a lock
To set a lock on a directory tree simply create a property on a directory called lock. Then you must commit the property to make it take effect.
svn propset lock TRUE trunk/project_a svn commit trunk/project_a
The lock script does not care what value the lock property is set to. The script only checks if the lock property exists or not. The example above set the lock property value to TRUE, but the value could have been a descriptive text message giving the reason for the lock. For example,
svn propset lock 'Locked for bug hunting. Ask the build engineer if you have questions.' trunk/project_a svn commit trunk/project_a
Others can then see the value of the lock for a description of why the lock was set.
$ svn propget lock trunk/project_a Locked for bug hunting. Ask the build engineer if you have questions.
Of course, you can and should also describe the reason for the lock when you commit the lock property when you set the lock.
Delete a lock
To remove a lock simply delete the lock property and then commit:
svn propdel lock trunk/project_a svn commit trunk/project_a
Repository Installation
- To install put the svn_dir_lock.py script in your server hooks directory. In this example, I use /home/svn/repository/hooks/.
- Add the following code to the pre-commit shell script. Note that you must update the LOCK_SCRIPT_PATH to point to the script your server hooks directory.
LOCK_SCRIPT_PATH="/home/svn/repository/hooks/svn_dir_lock.py"
SVNLOOK=/usr/bin/svnlook
REPOS="$1"
TXN="$2"
if ! python ${LOCK_SCRIPT_PATH} -v "$TXN" "$REPOS" >&2; then
echo "ERROR: Commit failed. Delete locks before you commit." >&2
exit 1
fi
svn_dir_lock.py Source
#!/usr/bin/env python
"""This detects if a commit transaction is trying to commit files under
a directory with a lock property.
Pass the transaction_id and repository as arguments. This returns 0 if there
are no locks or a positive number representing the number of locks. If a '-v'
flag is passed this will also print out the lock and the file that is blocked
by the lock.
Normally this would be called by a "pre-commit" shell script in a repository
hooks directory. Add the following code you "pre-commit":
SVNLOOK=/usr/bin/svnlook
REPOS="$1"
TXN="$2"
python /home/svn/repository/hooks/svn_dir_lock.py -v "$TXN" "$REPOS" >&2
EXIT_STATUS=$?
if [ $EXIT_STATUS -ne 0 ]; then
echo "Delete locks before you commit." >&2
exit 1
fi
This requires that Pexpect be installed. See http://pexpect.sourceforge.net/
Version 1.0
Noah Spurrier 2007
$Id: svn_dir_lock.py 9 2007-07-28 20:36:56Z root $
"""
import sys, os, traceback
import re
import getopt
from pexpect import run, spawn
def exit_with_usage():
print globals()['__doc__']
os._exit(1)
def parse_args (options='', long_options=[]):
try:
optlist, args = getopt.getopt(sys.argv[1:], options+'h?', long_options+['help','h','?'])
except Exception, e:
print str(e)
exit_with_usage()
options = dict(optlist)
if [elem for elem in options if elem in ['-h','--h','-?','--?','--help']]:
exit_with_usage()
return (options, args)
def get_dirs_prop_changed (transaction_id, repository):
"""This returns a list of directories in the transaction with changed properties.
"""
dirs_raw = run ('svnlook dirs-changed -t %(transaction_id)s %(repository)s' % locals())
dirs_list = []
for line in dirs_raw.splitlines():
line = line.strip()
m = re.search("^.U\s*([/.\w]*)", line)
if m is not None:
dirs_list.append(m.groups(0)[0])
return dirs_list
def get_items_changed (transaction_id, repository):
"""This returns a list of files in the transaction.
"""
files_raw = run ('svnlook changed -t %(transaction_id)s %(repository)s' % locals())
files_list = []
for line in files_raw.splitlines():
line = line.strip()
m = re.search("^\S*\s*([/.\w]*)", line)
if m is not None:
files_list.append(m.groups(0)[0])
return files_list
def get_lock_prop_state_transaction (transaction_id, repository, item):
state_raw = run ('svnlook propget -t %(transaction_id)s %(repository)s lock %(item)s' % locals())
if 'TRUE' in state_raw.upper():
return True
else:
return False
def get_lock_prop_state_pre (repository, item):
state_raw = run ('svnlook propget %(repository)s lock %(item)s' % locals())
if 'TRUE' in state_raw.upper():
return True
else:
return False
def sub_dirs (path):
"""This returns all possible subpaths for a given path.
>>> print sub_dirs ('a')
None
>>> print sub_dirs ('a/')
['/a/']
>>> print sub_dirs ('a/b')
['/a/']
>>> print sub_dirs ('a/b/')
['/a/', '/a/b/']
>>> print sub_dirs ('a/b/c')
['/a/', '/a/b/']
>>> print sub_dirs ('a/b/c/')
['/a/', '/a/b/', '/a/b/c/']
"""
splits = path.split('/')
if len(splits) < 2:
return None
splits = splits[:-1]
dirs = ['']
for p in splits:
dirs.append (dirs[-1]+p+'/')
dirs=dirs[1:]
return dirs
def get_existing_locks (repository, items):
locks=[]
for i in items:
subdirs = sub_dirs(i)
for s in subdirs:
if s in locks:
continue
if get_lock_prop_state_pre(repository, s) == True:
locks.append(s)
return locks
def remove_newly_cleared_locks (repository, transaction_id, items, locks):
for l in locks:
if l in items:
if get_lock_prop_state_transaction(repository, transaction_id, l) == False:
locks.remove(l)
return locks
def under_lock (locks, items):
locked_items = []
for i in items:
for l in locks:
if l in i:
locked_items.append((i,l))
return locked_items
def main ():
(options, args) = parse_args('v')
# if args<=0:
# exit_with_usage()
if '-v' in options:
verbose_flag = True
else:
verbose_flag = False
transaction_id = args[0]
repository = args[1]
#dirs_changed = get_dirs_prop_changed(transaction_id, repository)
items_changed = get_items_changed(transaction_id, repository)
# for each item in the transaction see if it has a lock prop.
# locks=[]
# for f in items_changed:
# print f
# if get_lock_prop_state_transaction(transaction_id, repository, f) == True:
# locks.append(f)
# a. adding new lock and files under lock --> allow
# b. removing existing lock -> allow
# c. adding files under old lock -> disallow
locks = get_existing_locks(repository, items_changed)
locks = remove_newly_cleared_locks(repository, transaction_id, items_changed, locks)
locked_items = under_lock(locks, items_changed)
if verbose_flag:
for li in locked_items:
print "LOCKED: " + li[0]
print " BY: " + li[1]
print " # To remove lock use 'svn propdel lock "+li[1]+"'."
print " # To check lock use 'svn propget lock "+li[1]+"'."
return len(locked_items)
if __name__ == '__main__':
try:
exit_status = main()
if exit_status is None:
sys.exit(0)
sys.exit(int(exit_status))
except SystemExit, e:
raise e
except Exception, e:
print 'ERROR, UNEXPECTED EXCEPTION'
print str(e)
traceback.print_exc()
os._exit(1)