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