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