Merge branch 'je/hooks'
authorJunio C Hamano <gitster@pobox.com>
Mon, 24 Sep 2007 05:51:03 +0000 (22:51 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 24 Sep 2007 05:51:03 +0000 (22:51 -0700)
* je/hooks:
Added example hook script to save/restore permissions/ownership.
Add post-merge hook, related documentation, and tests.

Documentation/hooks.txt
contrib/hooks/setgitperms.perl [new file with mode: 0644]
git-merge.sh
t/t5402-post-merge-hook.sh [new file with mode: 0755]
index c39edc57c4452091e2f313cb8d5cfa9d51a4b27b..58b954759610ca272b72a05db0cd14e2868ff301 100644 (file)
@@ -87,6 +87,19 @@ parameter, and is invoked after a commit is made.
 This hook is meant primarily for notification, and cannot affect
 the outcome of `git-commit`.
 
+post-merge
+-----------
+
+This hook is invoked by `git-merge`, which happens when a `git pull`
+is done on a local repository.  The hook takes a single parameter, a status
+flag specifying whether or not the merge being done was a squash merge.
+This hook cannot affect the outcome of `git-merge`.
+
+This hook can be used in conjunction with a corresponding pre-commit hook to
+save and restore any form of metadata associated with the working tree
+(eg: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl
+for an example of how to do this.
+
 [[pre-receive]]
 pre-receive
 -----------
diff --git a/contrib/hooks/setgitperms.perl b/contrib/hooks/setgitperms.perl
new file mode 100644 (file)
index 0000000..5e3b89d
--- /dev/null
@@ -0,0 +1,213 @@
+#!/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
+#     . 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` hook with the following lines:
+#      #!/bin/sh
+#     . 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;
+    }
+}
index cde09d4d602811b610e0d744f4e8ede6f9fb0a39..6c513dcbdf44036b0207c276e765a87eceb7aa77 100755 (executable)
@@ -97,6 +97,19 @@ finish () {
                fi
                ;;
        esac
+
+       # Run a post-merge hook
+        if test -x "$GIT_DIR"/hooks/post-merge
+        then
+           case "$squash" in
+           t)
+                "$GIT_DIR"/hooks/post-merge 1
+               ;;
+           '')
+                "$GIT_DIR"/hooks/post-merge 0
+               ;;
+           esac
+        fi
 }
 
 merge_name () {
diff --git a/t/t5402-post-merge-hook.sh b/t/t5402-post-merge-hook.sh
new file mode 100755 (executable)
index 0000000..1c4b0b3
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Josh England
+#
+
+test_description='Test the post-merge hook.'
+. ./test-lib.sh
+
+test_expect_success setup '
+       echo Data for commit0. >a &&
+       git update-index --add a &&
+       tree0=$(git write-tree) &&
+       commit0=$(echo setup | git commit-tree $tree0) &&
+       echo Changed data for commit1. >a &&
+       git update-index a &&
+       tree1=$(git write-tree) &&
+       commit1=$(echo modify | git commit-tree $tree1 -p $commit0) &&
+        git update-ref refs/heads/master $commit0 &&
+       git-clone ./. clone1 &&
+       GIT_DIR=clone1/.git git update-index --add a &&
+       git-clone ./. clone2 &&
+       GIT_DIR=clone2/.git git update-index --add a
+'
+
+for clone in 1 2; do
+    cat >clone${clone}/.git/hooks/post-merge <<'EOF'
+#!/bin/sh
+echo $@ >> $GIT_DIR/post-merge.args
+EOF
+    chmod u+x clone${clone}/.git/hooks/post-merge
+done
+
+test_expect_failure 'post-merge does not run for up-to-date ' '
+        GIT_DIR=clone1/.git git merge $commit0 &&
+       test -e clone1/.git/post-merge.args
+'
+
+test_expect_success 'post-merge runs as expected ' '
+        GIT_DIR=clone1/.git git merge $commit1 &&
+       test -e clone1/.git/post-merge.args
+'
+
+test_expect_success 'post-merge from normal merge receives the right argument ' '
+        grep 0 clone1/.git/post-merge.args
+'
+
+test_expect_success 'post-merge from squash merge runs as expected ' '
+        GIT_DIR=clone2/.git git merge --squash $commit1 &&
+       test -e clone2/.git/post-merge.args
+'
+
+test_expect_success 'post-merge from squash merge receives the right argument ' '
+        grep 1 clone2/.git/post-merge.args
+'
+
+test_done