You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
6.8 KiB
214 lines
6.8 KiB
#!/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; |
|
} |
|
}
|
|
|