1#!/usr/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$usageunless($read_modexor$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 59open(IN,"<$gitmeta")or die"Could not open$gitmetafor reading:$!\n"; 60while(defined($_= <IN>)) { 61chomp; 62if(/^(.*) mode=(\S+)\s+uid=(\d+)\s+gid=(\d+)/) { 63# Compare recorded perms to actual perms in the working dir 64my($path,$mode,$uid,$gid) = ($1,$2,$3,$4); 65my$fullpath=$topdir.$path; 66my(undef,undef,$wmode,undef,$wuid,$wgid) =lstat($fullpath); 67$wmode=sprintf"%04o",$wmode&07777; 68if($modene$wmode) { 69$verbose&&print"Updating permissions on$path: old=$wmode, new=$mode\n"; 70chmod oct($mode),$fullpath; 71} 72if($uid!=$wuid||$gid!=$wgid) { 73if($verbose) { 74# Print out user/group names instead of uid/gid 75my$pwname=getpwuid($uid); 76my$grpname=getgrgid($gid); 77my$wpwname=getpwuid($wuid); 78my$wgrpname=getgrgid($wgid); 79$pwname=$uidif!defined$pwname; 80$grpname=$gidif!defined$grpname; 81$wpwname=$wuidif!defined$wpwname; 82$wgrpname=$wgidif!defined$wgrpname; 83 84print"Updating uid/gid on$path: old=$wpwname/$wgrpname, new=$pwname/$grpname\n"; 85} 86chown$uid,$gid,$fullpath; 87} 88} 89else{ 90warn"Invalid input format in$gitmeta:\n\t$_\n"; 91} 92} 93close IN; 94} 95elsif($read_mode) { 96# Handle merge conflicts in the .gitperms file 97if(-e "$gitdir/MERGE_MSG") { 98if(`grep ======$gitmeta`) { 99 # Conflict not resolved -- abort the commit 100 print "PERMISSIONS/OWNERSHIP CONFLICT\n"; 101 print " Resolve the conflict in the$gitmetafile 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. 112my@conflict_diff=`git show \$(cat$gitdir/MERGE_HEAD)`; 113my@conflict_files; 114my$metadiff=0; 115 116# Build a list of files that conflicted from the .gitmeta diff 117foreachmy$line(@conflict_diff) { 118if($line=~ m|^diff --git a/$gitmeta b/$gitmeta|) { 119$metadiff=1; 120} 121elsif($line=~/^diff --git/) { 122$metadiff=0; 123} 124elsif($metadiff&&$line=~/^\+(.*) mode=/) { 125push@conflict_files,$1; 126} 127} 128 129# Verify that each conflict file now has permissions consistent 130# with the .gitmeta file 131foreachmy$file(@conflict_files) { 132my$absfile=$topdir.$file; 133my$gm_entry=`grep "^$filemode="$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_modene$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$parenteq '.'; 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 ($diffne '') { 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=%04ouid=$uidgid=$gid\n",$mode& 07777; 209 } 210 else { 211 print OUT$path; 212 printf OUT " mode=%04ouid=$uidgid=$gid\n",$mode& 07777; 213 } 214}