1#!/usr/local/bin/perl 2# 3# Copyright (c) 2006 Josh England 4# 5# This script can be used to save/restore full permissions and ownership data 6# within a git working tree. 7# 8# To save permissions/ownership data, place this script in your .git/hooks 9# directory and enable a `pre-commit` hook with the following lines: 10# #!/bin/sh 11# SUBDIRECTORY_OK=1 . git-sh-setup 12# $GIT_DIR/hooks/setgitperms.perl -r 13# 14# To restore permissions/ownership data, place this script in your .git/hooks 15# directory and enable a `post-merge` and `post-checkout` hook with the 16# following lines: 17# #!/bin/sh 18# SUBDIRECTORY_OK=1 . git-sh-setup 19# $GIT_DIR/hooks/setgitperms.perl -w 20# 21use strict; 22use Getopt::Long; 23use File::Find; 24use File::Basename; 25 26my $usage = 27"usage: setgitperms.perl [OPTION]... <--read|--write> 28This program uses a file `.gitmeta` to store/restore permissions and uid/gid 29info for all files/dirs tracked by git in the repository. 30 31---------------------------------Read Mode------------------------------------- 32-r, --read Reads perms/etc from working dir into a .gitmeta file 33-s, --stdout Output to stdout instead of .gitmeta 34-d, --diff Show unified diff of perms file (XOR with --stdout) 35 36---------------------------------Write Mode------------------------------------ 37-w, --write Modify perms/etc in working dir to match the .gitmeta file 38-v, --verbose Be verbose 39 40\n"; 41 42my ($stdout, $showdiff, $verbose, $read_mode, $write_mode); 43 44if ((@ARGV < 0) || !GetOptions( 45 "stdout", \$stdout, 46 "diff", \$showdiff, 47 "read", \$read_mode, 48 "write", \$write_mode, 49 "verbose", \$verbose, 50 )) { die $usage; } 51die $usage unless ($read_mode xor $write_mode); 52 53my $topdir = `git rev-parse --show-cdup` or die "\n"; chomp $topdir; 54my $gitdir = $topdir . '.git'; 55my $gitmeta = $topdir . '.gitmeta'; 56 57if ($write_mode) { 58 # Update the working dir permissions/ownership based on data from .gitmeta 59 open (IN, "<$gitmeta") or die "Could not open $gitmeta for reading: $!\n"; 60 while (defined ($_ = <IN>)) { 61 chomp; 62 if (/^(.*) mode=(\S+)\s+uid=(\d+)\s+gid=(\d+)/) { 63 # Compare recorded perms to actual perms in the working dir 64 my ($path, $mode, $uid, $gid) = ($1, $2, $3, $4); 65 my $fullpath = $topdir . $path; 66 my (undef,undef,$wmode,undef,$wuid,$wgid) = lstat($fullpath); 67 $wmode = sprintf "%04o", $wmode & 07777; 68 if ($mode ne $wmode) { 69 $verbose && print "Updating permissions on $path: old=$wmode, new=$mode\n"; 70 chmod oct($mode), $fullpath; 71 } 72 if ($uid != $wuid || $gid != $wgid) { 73 if ($verbose) { 74 # Print out user/group names instead of uid/gid 75 my $pwname = getpwuid($uid); 76 my $grpname = getgrgid($gid); 77 my $wpwname = getpwuid($wuid); 78 my $wgrpname = getgrgid($wgid); 79 $pwname = $uid if !defined $pwname; 80 $grpname = $gid if !defined $grpname; 81 $wpwname = $wuid if !defined $wpwname; 82 $wgrpname = $wgid if !defined $wgrpname; 83 84 print "Updating uid/gid on $path: old=$wpwname/$wgrpname, new=$pwname/$grpname\n"; 85 } 86 chown $uid, $gid, $fullpath; 87 } 88 } 89 else { 90 warn "Invalid input format in $gitmeta:\n\t$_\n"; 91 } 92 } 93 close IN; 94} 95elsif ($read_mode) { 96 # Handle merge conflicts in the .gitperms file 97 if (-e "$gitdir/MERGE_MSG") { 98 if (`grep ====== $gitmeta`) { 99 # Conflict not resolved -- abort the commit 100 print "PERMISSIONS/OWNERSHIP CONFLICT\n"; 101 print " Resolve the conflict in the $gitmeta file and then run\n"; 102 print " `.git/hooks/setgitperms.perl --write` to reconcile.\n"; 103 exit 1; 104 } 105 elsif (`grep $gitmeta $gitdir/MERGE_MSG`) { 106 # A conflict in .gitmeta has been manually resolved. Verify that 107 # the working dir perms matches the current .gitmeta perms for 108 # each file/dir that conflicted. 109 # This is here because a `setgitperms.perl --write` was not 110 # performed due to a merge conflict, so permissions/ownership 111 # may not be consistent with the manually merged .gitmeta file. 112 my @conflict_diff = `git show \$(cat $gitdir/MERGE_HEAD)`; 113 my @conflict_files; 114 my $metadiff = 0; 115 116 # Build a list of files that conflicted from the .gitmeta diff 117 foreach my $line (@conflict_diff) { 118 if ($line =~ m|^diff --git a/$gitmeta b/$gitmeta|) { 119 $metadiff = 1; 120 } 121 elsif ($line =~ /^diff --git/) { 122 $metadiff = 0; 123 } 124 elsif ($metadiff && $line =~ /^\+(.*) mode=/) { 125 push @conflict_files, $1; 126 } 127 } 128 129 # Verify that each conflict file now has permissions consistent 130 # with the .gitmeta file 131 foreach my $file (@conflict_files) { 132 my $absfile = $topdir . $file; 133 my $gm_entry = `grep "^$file mode=" $gitmeta`; 134 if ($gm_entry =~ /mode=(\d+) uid=(\d+) gid=(\d+)/) { 135 my ($gm_mode, $gm_uid, $gm_gid) = ($1, $2, $3); 136 my (undef,undef,$mode,undef,$uid,$gid) = lstat("$absfile"); 137 $mode = sprintf("%04o", $mode & 07777); 138 if (($gm_mode ne $mode) || ($gm_uid != $uid) 139 || ($gm_gid != $gid)) { 140 print "PERMISSIONS/OWNERSHIP CONFLICT\n"; 141 print " Mismatch found for file: $file\n"; 142 print " Run `.git/hooks/setgitperms.perl --write` to reconcile.\n"; 143 exit 1; 144 } 145 } 146 else { 147 print "Warning! Permissions/ownership no longer being tracked for file: $file\n"; 148 } 149 } 150 } 151 } 152 153 # No merge conflicts -- write out perms/ownership data to .gitmeta file 154 unless ($stdout) { 155 open (OUT, ">$gitmeta.tmp") or die "Could not open $gitmeta.tmp for writing: $!\n"; 156 } 157 158 my @files = `git ls-files`; 159 my %dirs; 160 161 foreach my $path (@files) { 162 chomp $path; 163 # We have to manually add stats for parent directories 164 my $parent = dirname($path); 165 while (!exists $dirs{$parent}) { 166 $dirs{$parent} = 1; 167 next if $parent eq '.'; 168 printstats($parent); 169 $parent = dirname($parent); 170 } 171 # Now the git-tracked file 172 printstats($path); 173 } 174 175 # diff the temporary metadata file to see if anything has changed 176 # If no metadata has changed, don't overwrite the real file 177 # This is just so `git commit -a` doesn't try to commit a bogus update 178 unless ($stdout) { 179 if (! -e $gitmeta) { 180 rename "$gitmeta.tmp", $gitmeta; 181 } 182 else { 183 my $diff = `diff -U 0 $gitmeta $gitmeta.tmp`; 184 if ($diff ne '') { 185 rename "$gitmeta.tmp", $gitmeta; 186 } 187 else { 188 unlink "$gitmeta.tmp"; 189 } 190 if ($showdiff) { 191 print $diff; 192 } 193 } 194 close OUT; 195 } 196 # Make sure the .gitmeta file is tracked 197 system("git add $gitmeta"); 198} 199 200 201sub printstats { 202 my $path = $_[0]; 203 $path =~ s/@/\@/g; 204 my (undef,undef,$mode,undef,$uid,$gid) = lstat($path); 205 $path =~ s/%/\%/g; 206 if ($stdout) { 207 print $path; 208 printf " mode=%04o uid=$uid gid=$gid\n", $mode & 07777; 209 } 210 else { 211 print OUT $path; 212 printf OUT " mode=%04o uid=$uid gid=$gid\n", $mode & 07777; 213 } 214} 215