From 06d3398fb0ef36c835287b73b2794209ecc1eb49 Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Sun, 27 May 2012 09:50:50 +0530 Subject: [PATCH] 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. --- doc/locking.mkd | 153 ++++++++++++++++++++++++++++++++++++++++++++++ src/VREF/lock | 36 +++++++++++ src/commands/lock | 124 +++++++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+) create mode 100644 doc/locking.mkd create mode 100755 src/VREF/lock create mode 100755 src/commands/lock 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 ); +}