config: move a few helper functions up
[git.git] / contrib / hooks / setgitperms.perl
blob2770a1b1d205ee6b6edaec291a7dce3fc417027f
1 #!/usr/bin/perl
3 # Copyright (c) 2006 Josh England
5 # This script can be used to save/restore full permissions and ownership data
6 # within a git working tree.
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
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
21 use strict;
22 use Getopt::Long;
23 use File::Find;
24 use File::Basename;
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.
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)
36 ---------------------------------Write Mode------------------------------------
37 -w, --write Modify perms/etc in working dir to match the .gitmeta file
38 -v, --verbose Be verbose
40 \n";
42 my ($stdout, $showdiff, $verbose, $read_mode, $write_mode);
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);
53 my $topdir = `git rev-parse --show-cdup` or die "\n"; chomp $topdir;
54 my $gitdir = $topdir . '.git';
55 my $gitmeta = $topdir . '.gitmeta';
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;
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;
84 print "Updating uid/gid on $path: old=$wpwname/$wgrpname, new=$pwname/$grpname\n";
86 chown $uid, $gid, $fullpath;
89 else {
90 warn "Invalid input format in $gitmeta:\n\t$_\n";
93 close IN;
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;
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;
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;
121 elsif ($line =~ /^diff --git/) {
122 $metadiff = 0;
124 elsif ($metadiff && $line =~ /^\+(.*) mode=/) {
125 push @conflict_files, $1;
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;
146 else {
147 print "Warning! Permissions/ownership no longer being tracked for file: $file\n";
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";
158 my @files = `git ls-files`;
159 my %dirs;
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);
171 # Now the git-tracked file
172 printstats($path);
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;
182 else {
183 my $diff = `diff -U 0 $gitmeta $gitmeta.tmp`;
184 if ($diff ne '') {
185 rename "$gitmeta.tmp", $gitmeta;
187 else {
188 unlink "$gitmeta.tmp";
190 if ($showdiff) {
191 print $diff;
194 close OUT;
196 # Make sure the .gitmeta file is tracked
197 system("git add $gitmeta");
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;
210 else {
211 print OUT $path;
212 printf OUT " mode=%04o uid=$uid gid=$gid\n", $mode & 07777;