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