about summary refs log tree commit diff
path: root/contrib/hooks/setgitperms.perl
diff options
context:
space:
mode:
authorVincent Ambo <Vincent Ambo>2020-01-11T23·36+0000
committerVincent Ambo <Vincent Ambo>2020-01-11T23·36+0000
commit1b593e1ea4d2af0f6444d9a7788d5d99abd6fde5 (patch)
treee3accb9beed5c4c1b5a05c99db71ab2841f0ed04 /contrib/hooks/setgitperms.perl
Squashed 'third_party/git/' content from commit cb71568594
git-subtree-dir: third_party/git
git-subtree-split: cb715685942260375e1eb8153b0768a376e4ece7
Diffstat (limited to 'contrib/hooks/setgitperms.perl')
-rwxr-xr-xcontrib/hooks/setgitperms.perl214
1 files changed, 214 insertions, 0 deletions
diff --git a/contrib/hooks/setgitperms.perl b/contrib/hooks/setgitperms.perl
new file mode 100755
index 000000000000..2770a1b1d205
--- /dev/null
+++ b/contrib/hooks/setgitperms.perl
@@ -0,0 +1,214 @@
+#!/usr/bin/perl
+#
+# Copyright (c) 2006 Josh England
+#
+# This script can be used to save/restore full permissions and ownership data
+# within a git working tree.
+#
+# To save permissions/ownership data, place this script in your .git/hooks
+# directory and enable a `pre-commit` hook with the following lines:
+#      #!/bin/sh
+#     SUBDIRECTORY_OK=1 . git-sh-setup
+#     $GIT_DIR/hooks/setgitperms.perl -r
+#
+# To restore permissions/ownership data, place this script in your .git/hooks
+# directory and enable a `post-merge` and `post-checkout` hook with the
+# following lines:
+#      #!/bin/sh
+#     SUBDIRECTORY_OK=1 . git-sh-setup
+#     $GIT_DIR/hooks/setgitperms.perl -w
+#
+use strict;
+use Getopt::Long;
+use File::Find;
+use File::Basename;
+
+my $usage =
+"usage: setgitperms.perl [OPTION]... <--read|--write>
+This program uses a file `.gitmeta` to store/restore permissions and uid/gid
+info for all files/dirs tracked by git in the repository.
+
+---------------------------------Read Mode-------------------------------------
+-r,  --read         Reads perms/etc from working dir into a .gitmeta file
+-s,  --stdout       Output to stdout instead of .gitmeta
+-d,  --diff         Show unified diff of perms file (XOR with --stdout)
+
+---------------------------------Write Mode------------------------------------
+-w,  --write        Modify perms/etc in working dir to match the .gitmeta file
+-v,  --verbose      Be verbose
+
+\n";
+
+my ($stdout, $showdiff, $verbose, $read_mode, $write_mode);
+
+if ((@ARGV < 0) || !GetOptions(
+			       "stdout",         \$stdout,
+			       "diff",           \$showdiff,
+			       "read",           \$read_mode,
+			       "write",          \$write_mode,
+			       "verbose",        \$verbose,
+			      )) { die $usage; }
+die $usage unless ($read_mode xor $write_mode);
+
+my $topdir = `git rev-parse --show-cdup` or die "\n"; chomp $topdir;
+my $gitdir = $topdir . '.git';
+my $gitmeta = $topdir . '.gitmeta';
+
+if ($write_mode) {
+    # Update the working dir permissions/ownership based on data from .gitmeta
+    open (IN, "<$gitmeta") or die "Could not open $gitmeta for reading: $!\n";
+    while (defined ($_ = <IN>)) {
+	chomp;
+	if (/^(.*)  mode=(\S+)\s+uid=(\d+)\s+gid=(\d+)/) {
+	    # Compare recorded perms to actual perms in the working dir
+	    my ($path, $mode, $uid, $gid) = ($1, $2, $3, $4);
+	    my $fullpath = $topdir . $path;
+	    my (undef,undef,$wmode,undef,$wuid,$wgid) = lstat($fullpath);
+	    $wmode = sprintf "%04o", $wmode & 07777;
+	    if ($mode ne $wmode) {
+		$verbose && print "Updating permissions on $path: old=$wmode, new=$mode\n";
+		chmod oct($mode), $fullpath;
+	    }
+	    if ($uid != $wuid || $gid != $wgid) {
+		if ($verbose) {
+		    # Print out user/group names instead of uid/gid
+		    my $pwname  = getpwuid($uid);
+		    my $grpname  = getgrgid($gid);
+		    my $wpwname  = getpwuid($wuid);
+		    my $wgrpname  = getgrgid($wgid);
+		    $pwname = $uid if !defined $pwname;
+		    $grpname = $gid if !defined $grpname;
+		    $wpwname = $wuid if !defined $wpwname;
+		    $wgrpname = $wgid if !defined $wgrpname;
+
+		    print "Updating uid/gid on $path: old=$wpwname/$wgrpname, new=$pwname/$grpname\n";
+		}
+		chown $uid, $gid, $fullpath;
+	    }
+	}
+	else {
+	    warn "Invalid input format in $gitmeta:\n\t$_\n";
+	}
+    }
+    close IN;
+}
+elsif ($read_mode) {
+    # Handle merge conflicts in the .gitperms file
+    if (-e "$gitdir/MERGE_MSG") {
+	if (`grep ====== $gitmeta`) {
+	    # Conflict not resolved -- abort the commit
+	    print "PERMISSIONS/OWNERSHIP CONFLICT\n";
+	    print "    Resolve the conflict in the $gitmeta file and then run\n";
+	    print "    `.git/hooks/setgitperms.perl --write` to reconcile.\n";
+	    exit 1;
+	}
+	elsif (`grep $gitmeta $gitdir/MERGE_MSG`) {
+	    # A conflict in .gitmeta has been manually resolved. Verify that
+	    # the working dir perms matches the current .gitmeta perms for
+	    # each file/dir that conflicted.
+	    # This is here because a `setgitperms.perl --write` was not
+	    # performed due to a merge conflict, so permissions/ownership
+	    # may not be consistent with the manually merged .gitmeta file.
+	    my @conflict_diff = `git show \$(cat $gitdir/MERGE_HEAD)`;
+	    my @conflict_files;
+	    my $metadiff = 0;
+
+	    # Build a list of files that conflicted from the .gitmeta diff
+	    foreach my $line (@conflict_diff) {
+		if ($line =~ m|^diff --git a/$gitmeta b/$gitmeta|) {
+		    $metadiff = 1;
+		}
+		elsif ($line =~ /^diff --git/) {
+		    $metadiff = 0;
+		}
+		elsif ($metadiff && $line =~ /^\+(.*)  mode=/) {
+		    push @conflict_files, $1;
+		}
+	    }
+
+	    # Verify that each conflict file now has permissions consistent
+	    # with the .gitmeta file
+	    foreach my $file (@conflict_files) {
+		my $absfile = $topdir . $file;
+		my $gm_entry = `grep "^$file  mode=" $gitmeta`;
+		if ($gm_entry =~ /mode=(\d+)  uid=(\d+)  gid=(\d+)/) {
+		    my ($gm_mode, $gm_uid, $gm_gid) = ($1, $2, $3);
+		    my (undef,undef,$mode,undef,$uid,$gid) = lstat("$absfile");
+		    $mode = sprintf("%04o", $mode & 07777);
+		    if (($gm_mode ne $mode) || ($gm_uid != $uid)
+			|| ($gm_gid != $gid)) {
+			print "PERMISSIONS/OWNERSHIP CONFLICT\n";
+			print "    Mismatch found for file: $file\n";
+			print "    Run `.git/hooks/setgitperms.perl --write` to reconcile.\n";
+			exit 1;
+		    }
+		}
+		else {
+		    print "Warning! Permissions/ownership no longer being tracked for file: $file\n";
+		}
+	    }
+	}
+    }
+
+    # No merge conflicts -- write out perms/ownership data to .gitmeta file
+    unless ($stdout) {
+	open (OUT, ">$gitmeta.tmp") or die "Could not open $gitmeta.tmp for writing: $!\n";
+    }
+
+    my @files = `git ls-files`;
+    my %dirs;
+
+    foreach my $path (@files) {
+	chomp $path;
+	# We have to manually add stats for parent directories
+	my $parent = dirname($path);
+	while (!exists $dirs{$parent}) {
+	    $dirs{$parent} = 1;
+	    next if $parent eq '.';
+	    printstats($parent);
+	    $parent = dirname($parent);
+	}
+	# Now the git-tracked file
+	printstats($path);
+    }
+
+    # diff the temporary metadata file to see if anything has changed
+    # If no metadata has changed, don't overwrite the real file
+    # This is just so `git commit -a` doesn't try to commit a bogus update
+    unless ($stdout) {
+	if (! -e $gitmeta) {
+	    rename "$gitmeta.tmp", $gitmeta;
+	}
+	else {
+	    my $diff = `diff -U 0 $gitmeta $gitmeta.tmp`;
+	    if ($diff ne '') {
+		rename "$gitmeta.tmp", $gitmeta;
+	    }
+	    else {
+		unlink "$gitmeta.tmp";
+	    }
+	    if ($showdiff) {
+		print $diff;
+	    }
+	}
+	close OUT;
+    }
+    # Make sure the .gitmeta file is tracked
+    system("git add $gitmeta");
+}
+
+
+sub printstats {
+    my $path = $_[0];
+    $path =~ s/@/\@/g;
+    my (undef,undef,$mode,undef,$uid,$gid) = lstat($path);
+    $path =~ s/%/\%/g;
+    if ($stdout) {
+	print $path;
+	printf "  mode=%04o  uid=$uid  gid=$gid\n", $mode & 07777;
+    }
+    else {
+	print OUT $path;
+	printf OUT "  mode=%04o  uid=$uid  gid=$gid\n", $mode & 07777;
+    }
+}