Merge branch 'jc/combine-diff-callback'
[git] / contrib / hooks / setgitperms.perl
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 #
21 use strict;
22 use Getopt::Long;
23 use File::Find;
24 use File::Basename;
25
26 my $usage =
27 "Usage: setgitperms.perl [OPTION]... <--read|--write>
28 This program uses a file `.gitmeta` to store/restore permissions and uid/gid
29 info 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
42 my ($stdout, $showdiff, $verbose, $read_mode, $write_mode);
43
44 if ((@ARGV < 0) || !GetOptions(
45                                "stdout",         \$stdout,
46                                "diff",           \$showdiff,
47                                "read",           \$read_mode,
48                                "write",          \$write_mode,
49                                "verbose",        \$verbose,
50                               )) { die $usage; }
51 die $usage unless ($read_mode xor $write_mode);
52
53 my $topdir = `git rev-parse --show-cdup` or die "\n"; chomp $topdir;
54 my $gitdir = $topdir . '.git';
55 my $gitmeta = $topdir . '.gitmeta';
56
57 if ($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 }
95 elsif ($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
201 sub 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 }