blob: 2770a1b1d205ee6b6edaec291a7dce3fc417027f [file] [log] [blame]
#!/usr/bin/perl
#
# Copyright (c) 2006 Josh England
#
# This script can be used to save/restore full permissions and ownership data
# within a git working tree.
#
# To save permissions/ownership data, place this script in your .git/hooks
# directory and enable a `pre-commit` hook with the following lines:
# #!/bin/sh
# SUBDIRECTORY_OK=1 . git-sh-setup
# $GIT_DIR/hooks/setgitperms.perl -r
#
# To restore permissions/ownership data, place this script in your .git/hooks
# directory and enable a `post-merge` and `post-checkout` hook with the
# following lines:
# #!/bin/sh
# SUBDIRECTORY_OK=1 . git-sh-setup
# $GIT_DIR/hooks/setgitperms.perl -w
#
use strict;
use Getopt::Long;
use File::Find;
use File::Basename;
my $usage =
"usage: setgitperms.perl [OPTION]... <--read|--write>
This program uses a file `.gitmeta` to store/restore permissions and uid/gid
info for all files/dirs tracked by git in the repository.
---------------------------------Read Mode-------------------------------------
-r, --read Reads perms/etc from working dir into a .gitmeta file
-s, --stdout Output to stdout instead of .gitmeta
-d, --diff Show unified diff of perms file (XOR with --stdout)
---------------------------------Write Mode------------------------------------
-w, --write Modify perms/etc in working dir to match the .gitmeta file
-v, --verbose Be verbose
\n";
my ($stdout, $showdiff, $verbose, $read_mode, $write_mode);
if ((@ARGV < 0) || !GetOptions(
"stdout", \$stdout,
"diff", \$showdiff,
"read", \$read_mode,
"write", \$write_mode,
"verbose", \$verbose,
)) { die $usage; }
die $usage unless ($read_mode xor $write_mode);
my $topdir = `git rev-parse --show-cdup` or die "\n"; chomp $topdir;
my $gitdir = $topdir . '.git';
my $gitmeta = $topdir . '.gitmeta';
if ($write_mode) {
# Update the working dir permissions/ownership based on data from .gitmeta
open (IN, "<$gitmeta") or die "Could not open $gitmeta for reading: $!\n";
while (defined ($_ = <IN>)) {
chomp;
if (/^(.*) mode=(\S+)\s+uid=(\d+)\s+gid=(\d+)/) {
# Compare recorded perms to actual perms in the working dir
my ($path, $mode, $uid, $gid) = ($1, $2, $3, $4);
my $fullpath = $topdir . $path;
my (undef,undef,$wmode,undef,$wuid,$wgid) = lstat($fullpath);
$wmode = sprintf "%04o", $wmode & 07777;
if ($mode ne $wmode) {
$verbose && print "Updating permissions on $path: old=$wmode, new=$mode\n";
chmod oct($mode), $fullpath;
}
if ($uid != $wuid || $gid != $wgid) {
if ($verbose) {
# Print out user/group names instead of uid/gid
my $pwname = getpwuid($uid);
my $grpname = getgrgid($gid);
my $wpwname = getpwuid($wuid);
my $wgrpname = getgrgid($wgid);
$pwname = $uid if !defined $pwname;
$grpname = $gid if !defined $grpname;
$wpwname = $wuid if !defined $wpwname;
$wgrpname = $wgid if !defined $wgrpname;
print "Updating uid/gid on $path: old=$wpwname/$wgrpname, new=$pwname/$grpname\n";
}
chown $uid, $gid, $fullpath;
}
}
else {
warn "Invalid input format in $gitmeta:\n\t$_\n";
}
}
close IN;
}
elsif ($read_mode) {
# Handle merge conflicts in the .gitperms file
if (-e "$gitdir/MERGE_MSG") {
if (`grep ====== $gitmeta`) {
# Conflict not resolved -- abort the commit
print "PERMISSIONS/OWNERSHIP CONFLICT\n";
print " Resolve the conflict in the $gitmeta file and then run\n";
print " `.git/hooks/setgitperms.perl --write` to reconcile.\n";
exit 1;
}
elsif (`grep $gitmeta $gitdir/MERGE_MSG`) {
# A conflict in .gitmeta has been manually resolved. Verify that
# the working dir perms matches the current .gitmeta perms for
# each file/dir that conflicted.
# This is here because a `setgitperms.perl --write` was not
# performed due to a merge conflict, so permissions/ownership
# may not be consistent with the manually merged .gitmeta file.
my @conflict_diff = `git show \$(cat $gitdir/MERGE_HEAD)`;
my @conflict_files;
my $metadiff = 0;
# Build a list of files that conflicted from the .gitmeta diff
foreach my $line (@conflict_diff) {
if ($line =~ m|^diff --git a/$gitmeta b/$gitmeta|) {
$metadiff = 1;
}
elsif ($line =~ /^diff --git/) {
$metadiff = 0;
}
elsif ($metadiff && $line =~ /^\+(.*) mode=/) {
push @conflict_files, $1;
}
}
# Verify that each conflict file now has permissions consistent
# with the .gitmeta file
foreach my $file (@conflict_files) {
my $absfile = $topdir . $file;
my $gm_entry = `grep "^$file mode=" $gitmeta`;
if ($gm_entry =~ /mode=(\d+) uid=(\d+) gid=(\d+)/) {
my ($gm_mode, $gm_uid, $gm_gid) = ($1, $2, $3);
my (undef,undef,$mode,undef,$uid,$gid) = lstat("$absfile");
$mode = sprintf("%04o", $mode & 07777);
if (($gm_mode ne $mode) || ($gm_uid != $uid)
|| ($gm_gid != $gid)) {
print "PERMISSIONS/OWNERSHIP CONFLICT\n";
print " Mismatch found for file: $file\n";
print " Run `.git/hooks/setgitperms.perl --write` to reconcile.\n";
exit 1;
}
}
else {
print "Warning! Permissions/ownership no longer being tracked for file: $file\n";
}
}
}
}
# No merge conflicts -- write out perms/ownership data to .gitmeta file
unless ($stdout) {
open (OUT, ">$gitmeta.tmp") or die "Could not open $gitmeta.tmp for writing: $!\n";
}
my @files = `git ls-files`;
my %dirs;
foreach my $path (@files) {
chomp $path;
# We have to manually add stats for parent directories
my $parent = dirname($path);
while (!exists $dirs{$parent}) {
$dirs{$parent} = 1;
next if $parent eq '.';
printstats($parent);
$parent = dirname($parent);
}
# Now the git-tracked file
printstats($path);
}
# diff the temporary metadata file to see if anything has changed
# If no metadata has changed, don't overwrite the real file
# This is just so `git commit -a` doesn't try to commit a bogus update
unless ($stdout) {
if (! -e $gitmeta) {
rename "$gitmeta.tmp", $gitmeta;
}
else {
my $diff = `diff -U 0 $gitmeta $gitmeta.tmp`;
if ($diff ne '') {
rename "$gitmeta.tmp", $gitmeta;
}
else {
unlink "$gitmeta.tmp";
}
if ($showdiff) {
print $diff;
}
}
close OUT;
}
# Make sure the .gitmeta file is tracked
system("git add $gitmeta");
}
sub printstats {
my $path = $_[0];
$path =~ s/@/\@/g;
my (undef,undef,$mode,undef,$uid,$gid) = lstat($path);
$path =~ s/%/\%/g;
if ($stdout) {
print $path;
printf " mode=%04o uid=$uid gid=$gid\n", $mode & 07777;
}
else {
print OUT $path;
printf OUT " mode=%04o uid=$uid gid=$gid\n", $mode & 07777;
}
}