| #!/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; |
| } |
| } |