#!/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 ($_ = )) { 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; } }