diff --git a/doc/locking.mkd b/doc/locking.mkd
new file mode 100644
index 0000000..f693ed5
--- /dev/null
+++ b/doc/locking.mkd
@@ -0,0 +1,153 @@
+# locking binary files
+
+Locking is useful to make sure that binary files (office docs, images, ...)
+don't get into a merge state. (If you think it's not a big
+deal, you have never manually merged independent changes to an ODT or
+something!)
+
+When git is used in a truly distributed fashion, locking is impossible.
+However, in most corporate setups, there is a single central server acting as
+the canonical source of truth and collaboration point for all developers. In
+this situation it should be possible to at least prevent commits from being
+pushed that contains changes to files locked by someone else.
+
+The two "lock" programs (one a command that a user uses, and one a VREF that
+the admin adds to a repo's access rules) together achieve this.
+
+----
+
+[[TOC]]
+
+## problem description
+
+Our users are alice, bob, and carol. Our repo is foo. It has some "odt"
+files in the "doc/" directory. We want to make sure these odt files never get
+into a "merge" situation.
+
+## admin/setup
+
+First, someone with shell access to the server must add 'lock' to the
+"COMMANDS" list in the rc file.
+
+Next, the gitolite.conf file should have something like this:
+
+ repo foo
+ <...other rules...>
+ - VREF/lock = @all
+
+However, see below for the difference between "RW" and "RW+" from the point of
+view of this feature and adjust permissions accordingly.
+
+## user view
+
+Here's a summary:
+
+ * Any user with "W" permissions to any branch in the repo can "lock" any
+ file. Once locked, no other user can push changes to that file, *in any
+ branch*, until it is unlocked.
+ * Any user with "+" permissions to any branch in the repo can "break" a lock
+ held by someone else if needed.
+
+For best results, everyone on the team should:
+
+ * Run 'git pull' or eqvt, then lock the binary file(s) before editing them.
+ * Finish the editing task as quickly as possible, then commit, push, and
+ unlock the file(s) so others are not needlessly blocked.
+ * Understand that breaking a lock require additional, (out of band)
+ communication. It is upto the team's policies what that entails.
+
+## detailed example
+
+Alice declares her intent to work on "d1.odt":
+
+ $ git pull
+ $ ssh git@host lock -l foo doc/d1.odt
+
+Similarly Bob starts on "d2.odt"
+
+ $ git pull
+ $ ssh git@host lock -l foo doc/d2.odt
+
+Carol makes some changes to d2.odt (**without attempting to lock the file or
+checking to see if it is already locked**) and pushes:
+
+ $ ooffice doc/d2.odt
+ $ git add doc/d2.odt
+ $ git commit -m 'added footnotes to d2 in klingon'
+ $ git push
+ <...normal push progress output...>
+ remote: FATAL: W VREF/lock testing carol DENIED by VREF/lock
+ remote: 'doc/d2.odt' locked by 'bob'
+ remote: error: hook declined to update refs/heads/master
+ To u2:testing
+ ! [remote rejected] master -> master (hook declined)
+ error: failed to push some refs to 'carol:foo'
+
+Carol backs out her changes, but saves them away for a "manual merge" later.
+
+ git reset HEAD^
+ git stash save 'klingon changes to d2.odt saved for possible manual merge later'
+
+Note that this still represents wasted work in some sense, because Carol would
+have to somehow re-apply the same changes to the new version of d2.odt after
+pulling it down. **This is because she did not lock the file before making
+changes on her local repo. Educating users in doing this is important if this
+scheme is to help you.**
+
+She now decides to work on "d1.odt". However, she has learned her lesson and
+decides to follow the protocol described above:
+
+ $ git pull
+ $ ssh git@host lock -l foo doc/d1.odt
+ FATAL: 'doc/d1.odt' locked by 'alice' since Sun May 27 17:59:59 2012
+
+Oh damn; can't work on that either.
+
+Carol now decides to see what else there may be. Instead of checking each
+file to see if she can lock it, she starts with a list of what is already
+locked:
+
+ $ ssh git@host lock -ls foo
+
+ # locks held:
+
+ alice doc/d1.odt (Sun May 27 17:59:59 2012)
+ bob doc/d2.odt (Sun May 27 18:00:06 2012)
+
+ # locks broken:
+
+Aha, looks like only d1 and d2 are locked. She picks d3.odt to work on. This
+time, she starts by locking it:
+
+ $ ssh git@host lock -l foo doc/d3.odt
+ $ ooffice doc/d3.odt
+ <...etc...>
+
+Meanwhile, in a parallel universe where d3.odt doesn't exist, and Alice has
+gone on vacation while keeping d1.odt locked, Carol breaks the lock. Carol
+can do this because she has RW+ permissions for the repository itself.
+
+However, protocol in this team requires that she get email approval from the
+team lead before doing this and that Alice be in CC in those emails, so she
+does that first, and *then* she breaks the lock:
+
+ $ git pull
+ $ ssh git@host lock --break foo doc/d1.odt
+
+She then locks d1.odt for herself:
+
+ $ ssh git@host lock -l foo doc/d1.odt
+
+When Alice comes back, she can tell who broke her lock and when:
+
+ $ ssh git@host lock -ls foo
+
+ # locks held:
+
+ carol doc/d1.odt (Sun May 27 18:17:29 2012)
+ bob doc/d2.odt (Sun May 27 18:00:06 2012)
+
+ # locks broken:
+
+ carol doc/d1.odt (Sun May 27 18:17:03 2012) (locked by alice at Sun May 27 17:59:59 2012)
+
diff --git a/src/VREF/lock b/src/VREF/lock
new file mode 100755
index 0000000..e07d8d5
--- /dev/null
+++ b/src/VREF/lock
@@ -0,0 +1,36 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use lib $ENV{GL_LIBDIR};
+use Gitolite::Common;
+
+# gitolite VREF to lock and unlock (binary) files. Requires companion command
+# 'lock' to be enabled; see doc/locking.mkd for details.
+
+# ----------------------------------------------------------------------
+
+# see gitolite docs for what the first 7 arguments mean
+
+die "not meant to be run manually" unless $ARGV[6];
+
+my $ff = "$ENV{GL_REPO_BASE}/$ENV{GL_REPO}.git/gl-locks";
+exit 0 unless -f $ff;
+
+our %locks;
+my $t = slurp($ff);
+eval $t;
+_die "do '$ff' failed with '$@', contact your administrator" if $@;
+
+my ( $oldtree, $newtree, $refex ) = @ARGV[ 3, 4, 6 ];
+
+for my $file (`git diff --name-only $oldtree $newtree` ) {
+ chomp($file);
+
+ if ($locks{$file} and $locks{$file}{USER} ne $ENV{GL_USER}) {
+ print "$refex '$file' locked by '$locks{$file}{USER}'";
+ last;
+ }
+}
+
+exit 0
diff --git a/src/commands/lock b/src/commands/lock
new file mode 100755
index 0000000..f95af6c
--- /dev/null
+++ b/src/commands/lock
@@ -0,0 +1,124 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use Getopt::Long;
+
+use lib $ENV{GL_LIBDIR};
+use Gitolite::Rc;
+use Gitolite::Common;
+use Gitolite::Conf::Load;
+
+# gitolite command to lock and unlock (binary) files and deal with locks.
+
+=for usage
+Usage: ssh git@host lock -l # lock a file
+ ssh git@host lock -u # unlock a file
+ ssh git@host lock --break # break someone else's lock
+ ssh git@host lock -ls # list locked files for repo
+
+See doc/locking.mkd for other details.
+=cut
+
+usage() if not @ARGV or $ARGV[0] eq '-h';
+$ENV{GL_USER} or _die "GL_USER not set";
+
+my $op = '';
+$op = 'lock' if $ARGV[0] eq '-l';
+$op = 'unlock' if $ARGV[0] eq '-u';
+$op = 'break' if $ARGV[0] eq '--break';
+$op = 'list' if $ARGV[0] eq '-ls';
+usage() if not $op;
+shift;
+
+my $repo = shift;
+_die "You are not authorised" if access( $repo, $ENV{GL_USER}, 'W', 'any' ) =~ /DENIED/;
+_die "You are not authorised" if $op eq 'break' and access( $repo, $ENV{GL_USER}, '+', 'any' ) =~ /DENIED/;
+
+my $file = shift || '';
+usage() if $op ne 'list' and not $file;
+
+_chdir( $ENV{GL_REPO_BASE} );
+_chdir("$repo.git");
+
+my $ff = "gl-locks";
+
+if ( $op eq 'lock' ) {
+ f_lock( $repo, $file );
+} elsif ( $op eq 'unlock' ) {
+ f_unlock( $repo, $file );
+} elsif ( $op eq 'break' ) {
+ f_break( $repo, $file );
+} elsif ( $op eq 'list' ) {
+ f_list($repo);
+}
+
+# ----------------------------------------------------------------------
+# everything below assumes we have already chdir'd to "$repo.git". Also, $ff
+# is used as a global.
+
+sub f_lock {
+ my ( $repo, $file ) = @_;
+
+ my %locks = get_locks();
+ _die "'$file' locked by '$locks{$file}{USER}' since " . localtime( $locks{$file}{TIME} ) if $locks{$file}{USER};
+ $locks{$file}{USER} = $ENV{GL_USER};
+ $locks{$file}{TIME} = time;
+ put_locks(%locks);
+}
+
+sub f_unlock {
+ my ( $repo, $file ) = @_;
+
+ my %locks = get_locks();
+ _die "'$file' not locked by '$ENV{GL_USER}'" if ( $locks{$file} || '' ) ne $ENV{GL_USER};
+ delete $locks{$file};
+ put_locks(%locks);
+}
+
+sub f_break {
+ my ( $repo, $file ) = @_;
+
+ my %locks = get_locks();
+ _die "'$file' was not locked" unless $locks{$file};
+ push @{ $locks{BREAKS} }, time . " $ENV{GL_USER} $locks{$file}{USER} $locks{$file}{TIME} $file";
+ delete $locks{$file};
+ put_locks(%locks);
+}
+
+sub f_list {
+ my $repo = shift;
+
+ my %locks = get_locks();
+ print "\n# locks held:\n\n";
+ map { print "$locks{$_}{USER}\t$_\t(" . scalar(localtime($locks{$_}{TIME})) . ")\n" } grep { $_ ne 'BREAKS' } sort keys %locks;
+ print "\n# locks broken:\n\n";
+ for my $b ( @{ $locks{BREAKS} } ) {
+ my ( $when, $who, $whose, $how_old, $what ) = split ' ', $b;
+ print "$who\t$what\t(" . scalar( localtime($when) ) . ")\t(locked by $whose at " . scalar( localtime($how_old) ) . ")\n";
+ }
+}
+
+sub get_locks {
+ if ( -f $ff ) {
+ our %locks;
+
+ my $t = slurp($ff);
+ eval $t;
+ _die "do '$ff' failed with '$@', contact your administrator" if $@;
+
+ return %locks;
+ }
+ return ();
+}
+
+sub put_locks {
+ my %locks = @_;
+
+ use Data::Dumper;
+ $Data::Dumper::Indent = 1;
+ $Data::Dumper::Sortkeys = 1;
+
+ my $dumped_data = Data::Dumper->Dump( [ \%locks ], [qw(*locks)] );
+ _print( $ff, $dumped_data );
+}