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