Skip to content
Permalink
Browse files

lock binary files... (manually tested)

Remember that true locking is not possible in a DVCS; see
doc/locking.mkd for details and limitations of what is offered here.
  • Loading branch information
sitaramc committed May 27, 2012
1 parent d623388 commit 06d3398fb0ef36c835287b73b2794209ecc1eb49
Showing with 313 additions and 0 deletions.
  1. +153 −0 doc/locking.mkd
  2. +36 −0 src/VREF/lock
  3. +124 −0 src/commands/lock
@@ -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. (<font color="gray">If you think it's not a big
deal, you have never manually merged independent changes to an ODT or
something!</font>)

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)

@@ -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
@@ -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 <repo> <file> # lock a file
ssh git@host lock -u <repo> <file> # unlock a file
ssh git@host lock --break <repo> <file> # break someone else's lock
ssh git@host lock -ls <repo> # 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 );
}

0 comments on commit 06d3398

Please sign in to comment.
You can’t perform that action at this time.