contrib / hooks / setgitperms.perlon commit Merge branch 'ss/svnimport' (3a22917)
   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}