diff options
Diffstat (limited to 'contrib/hooks')
-rwxr-xr-x[-rw-r--r--] | contrib/hooks/post-receive-email | 422 | ||||
-rw-r--r-- | contrib/hooks/pre-auto-gc-battery | 43 | ||||
-rw-r--r-- | contrib/hooks/setgitperms.perl | 214 | ||||
-rw-r--r-- | contrib/hooks/update-paranoid | 16 |
4 files changed, 540 insertions, 155 deletions
diff --git a/contrib/hooks/post-receive-email b/contrib/hooks/post-receive-email index c589a39a0c..01af9df15e 100644..100755 --- a/contrib/hooks/post-receive-email +++ b/contrib/hooks/post-receive-email @@ -2,24 +2,33 @@ # # Copyright (c) 2007 Andy Parkins # -# An example hook script to mail out commit update information. This hook sends emails -# listing new revisions to the repository introduced by the change being reported. The -# rule is that (for branch updates) each commit will appear on one email and one email -# only. +# An example hook script to mail out commit update information. This hook +# sends emails listing new revisions to the repository introduced by the +# change being reported. The rule is that (for branch updates) each commit +# will appear on one email and one email only. # -# This hook is stored in the contrib/hooks directory. Your distribution will have put -# this somewhere standard. You should make this script executable then link to it in -# the repository you would like to use it in. For example, on debian the hook is stored -# in /usr/share/doc/git-core/contrib/hooks/post-receive-email: +# This hook is stored in the contrib/hooks directory. Your distribution +# will have put this somewhere standard. You should make this script +# executable then link to it in the repository you would like to use it in. +# For example, on debian the hook is stored in +# /usr/share/git-core/contrib/hooks/post-receive-email: # # chmod a+x post-receive-email # cd /path/to/your/repository.git -# ln -sf /usr/share/doc/git-core/contrib/hooks/post-receive-email hooks/post-receive +# ln -sf /usr/share/git-core/contrib/hooks/post-receive-email hooks/post-receive # -# This hook script assumes it is enabled on the central repository of a project, with -# all users pushing only to it and not between each other. It will still work if you -# don't operate in that style, but it would become possible for the email to be from -# someone other than the person doing the push. +# This hook script assumes it is enabled on the central repository of a +# project, with all users pushing only to it and not between each other. It +# will still work if you don't operate in that style, but it would become +# possible for the email to be from someone other than the person doing the +# push. +# +# To help with debugging and use on pre-v1.5.1 git servers, this script will +# also obey the interface of hooks/update, taking its arguments on the +# command line. Unfortunately, hooks/update is called once for each ref. +# To avoid firing one email per ref, this script just prints its output to +# the screen when used in this mode. The output can then be redirected if +# wanted. # # Config # ------ @@ -28,15 +37,37 @@ # emails for every ref update. # hooks.announcelist # This is the list that all pushes of annotated tags will go to. Leave it -# blank to default to the mailinglist field. The announce emails lists the -# short log summary of the changes since the last annotated tag. -# hook.envelopesender -# If set then the -f option is passed to sendmail to allow the envelope sender -# address to be set +# blank to default to the mailinglist field. The announce emails lists +# the short log summary of the changes since the last annotated tag. +# hooks.envelopesender +# If set then the -f option is passed to sendmail to allow the envelope +# sender address to be set +# hooks.emailprefix +# All emails have their subjects prefixed with this prefix, or "[SCM]" +# if emailprefix is unset, to aid filtering +# hooks.showrev +# The shell command used to format each revision in the email, with +# "%s" replaced with the commit id. Defaults to "git rev-list -1 +# --pretty %s", displaying the commit id, author, date and log +# message. To list full patches separated by a blank line, you +# could set this to "git show -C %s; echo". +# To list a gitweb/cgit URL *and* a full patch for each change set, use this: +# "t=%s; printf 'http://.../?id=%%s' \$t; echo;echo; git show -C \$t; echo" +# Be careful if "..." contains things that will be expanded by shell "eval" +# or printf. +# hooks.emailmaxlines +# The maximum number of lines that should be included in the generated +# email body. If not specified, there is no limit. +# Lines beyond the limit are suppressed and counted, and a final +# line is added indicating the number of suppressed lines. +# hooks.diffopts +# Alternate options for the git diff-tree invocation that shows changes. +# Default is "--stat --summary --find-copies-harder". Add -p to those +# options to include a unified diff of changes in addition to the usual +# summary output. # # Notes # ----- -# All emails have their subjects prefixed with "[SCM]" to aid filtering. # All emails include the headers "X-Git-Refname", "X-Git-Oldrev", # "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and # give information for debugging. @@ -45,19 +76,10 @@ # ---------------------------- Functions # -# Top level email generation function. This decides what type of update -# this is and calls the appropriate body-generation routine after outputting -# the common header +# Function to prepare for email generation. This decides what type +# of update this is and whether an email should even be generated. # -# Note this function doesn't actually generate any email output, that is taken -# care of by the functions it calls: -# - generate_email_header -# - generate_create_XXXX_email -# - generate_update_XXXX_email -# - generate_delete_XXXX_email -# - generate_email_footer -# -generate_email() +prep_for_email() { # --- Arguments oldrev=$(git rev-parse $1) @@ -126,28 +148,55 @@ generate_email() short_refname=${refname##refs/remotes/} echo >&2 "*** Push-update of tracking branch, $refname" echo >&2 "*** - no email generated." - exit 0 + return 1 ;; *) # Anything else (is there anything else?) echo >&2 "*** Unknown type of update to $refname ($rev_type)" echo >&2 "*** - no email generated" - exit 1 + return 1 ;; esac # Check if we've got anyone to send to if [ -z "$recipients" ]; then - echo >&2 "*** hooks.recipients is not set so no email will be sent" + case "$refname_type" in + "annotated tag") + config_name="hooks.announcelist" + ;; + *) + config_name="hooks.mailinglist" + ;; + esac + echo >&2 "*** $config_name is not set so no email will be sent" echo >&2 "*** for $refname update $oldrev->$newrev" - exit 0 + return 1 fi + return 0 +} + +# +# Top level email generation function. This calls the appropriate +# body-generation routine after outputting the common header. +# +# Note this function doesn't actually generate any email output, that is +# taken care of by the functions it calls: +# - generate_email_header +# - generate_create_XXXX_email +# - generate_update_XXXX_email +# - generate_delete_XXXX_email +# - generate_email_footer +# +# Note also that this function cannot 'exit' from the script; when this +# function is running (in hook script mode), the send_mail() function +# is already executing in another process, connected via a pipe, and +# if this function exits without, whatever has been generated to that +# point will be sent as an email... even if nothing has been generated. +# +generate_email() +{ # Email parameters - # The committer will be obtained from the latest existing rev; so - # for a deletion it will be the oldrev, for the others, then newrev - committer=$(git show --pretty=full -s $rev | sed -ne "s/^Commit: //p" | - sed -ne 's/\(.*\) </"\1" </p') # The email subject will contain the best description of the ref # that we can build from the parameters describe=$(git describe $rev 2>/dev/null) @@ -167,7 +216,12 @@ generate_email() fn_name=atag ;; esac - generate_${change_type}_${fn_name}_email + + if [ -z "$maxlines" ]; then + generate_${change_type}_${fn_name}_email + else + generate_${change_type}_${fn_name}_email | limit_lines $maxlines + fi generate_email_footer } @@ -177,9 +231,8 @@ generate_email_header() # --- Email (all stdout will be the email) # Generate header cat <<-EOF - From: $committer To: $recipients - Subject: ${EMAILPREFIX}$projectdesc $refname_type, $short_refname, ${change_type}d. $describe + Subject: ${emailprefix}$projectdesc $refname_type $short_refname ${change_type}d. $describe X-Git-Refname: $refname X-Git-Reftype: $refname_type X-Git-Oldrev: $oldrev @@ -195,11 +248,12 @@ generate_email_header() generate_email_footer() { + SPACE=" " cat <<-EOF hooks/post-receive - -- + --${SPACE} $projectdesc EOF } @@ -216,12 +270,7 @@ generate_create_branch_email() echo "" echo $LOGBEGIN - # This shows all log entries that are not already covered by - # another ref - i.e. commits that are now accessible from this - # ref that were previously not accessible (see generate_update_branch_email - # for the explanation of this command) - git rev-parse --not --branches | grep -v $(git rev-parse $refname) | - git rev-list --pretty --stdin $newrev + show_new_revisions echo $LOGEND } @@ -240,35 +289,37 @@ generate_update_branch_email() # In this case we want to issue an email containing only revisions # 3, 4, and N. Given (almost) by # - # git-rev-list N ^O --not --all + # git rev-list N ^O --not --all # # The reason for the "almost", is that the "--not --all" will take # precedence over the "N", and effectively will translate to # - # git-rev-list N ^O ^X ^N + # git rev-list N ^O ^X ^N # - # So, we need to build up the list more carefully. git-rev-parse will - # generate a list of revs that may be fed into git-rev-list. We can get - # it to make the "--not --all" part and then filter out the "^N" with: + # So, we need to build up the list more carefully. git rev-parse + # will generate a list of revs that may be fed into git rev-list. + # We can get it to make the "--not --all" part and then filter out + # the "^N" with: # - # git-rev-parse --not --all | grep -v N + # git rev-parse --not --all | grep -v N # - # Then, using the --stdin switch to git-rev-list we have effectively + # Then, using the --stdin switch to git rev-list we have effectively # manufactured # - # git-rev-list N ^O ^X + # git rev-list N ^O ^X # # This leaves a problem when someone else updates the repository - # while this script is running. Their new value of the ref we're working - # on would be included in the "--not --all" output; and as our $newrev - # would be an ancestor of that commit, it would exclude all of our - # commits. What we really want is to exclude the current value of - # $refname from the --not list, rather than N itself. So: + # while this script is running. Their new value of the ref we're + # working on would be included in the "--not --all" output; and as + # our $newrev would be an ancestor of that commit, it would exclude + # all of our commits. What we really want is to exclude the current + # value of $refname from the --not list, rather than N itself. So: # - # git-rev-parse --not --all | grep -v $(git-rev-parse $refname) + # git rev-parse --not --all | grep -v $(git rev-parse $refname) # - # Get's us to something pretty safe (apart from the small time between - # refname being read, and git-rev-parse running - for that, I give up) + # Get's us to something pretty safe (apart from the small time + # between refname being read, and git rev-parse running - for that, + # I give up) # # # Next problem, consider this: @@ -276,18 +327,18 @@ generate_update_branch_email() # \ # * --- X --- * --- N ($newrev) # - # That is to say, there is no guarantee that oldrev is a strict subset of - # newrev (it would have required a --force, but that's allowed). So, we - # can't simply say rev-list $oldrev..$newrev. Instead we find the common - # base of the two revs and list from there. + # That is to say, there is no guarantee that oldrev is a strict + # subset of newrev (it would have required a --force, but that's + # allowed). So, we can't simply say rev-list $oldrev..$newrev. + # Instead we find the common base of the two revs and list from + # there. # - # As above, we need to take into account the presence of X; if another - # branch is already in the repository and points at some of the revisions - # that we are about to output - we don't want them. The solution is as - # before: git-rev-parse output filtered. + # As above, we need to take into account the presence of X; if + # another branch is already in the repository and points at some of + # the revisions that we are about to output - we don't want them. + # The solution is as before: git rev-parse output filtered. # - # Finally, tags: - # 1 --- 2 --- O --- T --- 3 --- 4 --- N + # Finally, tags: 1 --- 2 --- O --- T --- 3 --- 4 --- N # # Tags pushed into the repository generate nice shortlog emails that # summarise the commits between them and the previous tag. However, @@ -295,13 +346,14 @@ generate_update_branch_email() # for a branch update. Therefore we still want to output revisions # that have been output on a tag email. # - # Luckily, git-rev-parse includes just the tool. Instead of using "--all" - # we use "--branches"; this has the added benefit that "remotes/" will - # be ignored as well. - - # List all of the revisions that were removed by this update, in a fast forward - # update, this list will be empty, because rev-list O ^N is empty. For a non - # fast forward, O ^N is the list of removed revisions + # Luckily, git rev-parse includes just the tool. Instead of using + # "--all" we use "--branches"; this has the added benefit that + # "remotes/" will be ignored as well. + + # List all of the revisions that were removed by this update, in a + # fast-forward update, this list will be empty, because rev-list O + # ^N is empty. For a non-fast-forward, O ^N is the list of removed + # revisions fast_forward="" rev="" for rev in $(git rev-list $newrev..$oldrev) @@ -314,32 +366,33 @@ generate_update_branch_email() fi # List all the revisions from baserev to newrev in a kind of - # "table-of-contents"; note this list can include revisions that have - # already had notification emails and is present to show the full detail - # of the change from rolling back the old revision to the base revision and - # then forward to the new revision + # "table-of-contents"; note this list can include revisions that + # have already had notification emails and is present to show the + # full detail of the change from rolling back the old revision to + # the base revision and then forward to the new revision for rev in $(git rev-list $oldrev..$newrev) do revtype=$(git cat-file -t "$rev") echo " via $rev ($revtype)" done - if [ -z "$fastforward" ]; then + if [ "$fast_forward" ]; then echo " from $oldrev ($oldrev_type)" else - # 1. Existing revisions were removed. In this case newrev is a - # subset of oldrev - this is the reverse of a fast-forward, - # a rewind - # 2. New revisions were added on top of an old revision, this is - # a rewind and addition. + # 1. Existing revisions were removed. In this case newrev + # is a subset of oldrev - this is the reverse of a + # fast-forward, a rewind + # 2. New revisions were added on top of an old revision, + # this is a rewind and addition. - # (1) certainly happened, (2) possibly. When (2) hasn't happened, - # we set a flag to indicate that no log printout is required. + # (1) certainly happened, (2) possibly. When (2) hasn't + # happened, we set a flag to indicate that no log printout + # is required. echo "" - # Find the common ancestor of the old and new revisions and compare - # it with newrev + # Find the common ancestor of the old and new revisions and + # compare it with newrev baserev=$(git merge-base $oldrev $newrev) rewind_only="" if [ "$baserev" = "$newrev" ]; then @@ -377,27 +430,27 @@ generate_update_branch_email() echo "" echo $LOGBEGIN - git rev-parse --not --branches | grep -v $(git rev-parse $refname) | - git rev-list --pretty --stdin $oldrev..$newrev + show_new_revisions - # XXX: Need a way of detecting whether git rev-list actually outputted - # anything, so that we can issue a "no new revisions added by this - # update" message + # XXX: Need a way of detecting whether git rev-list actually + # outputted anything, so that we can issue a "no new + # revisions added by this update" message echo $LOGEND else echo "No new revisions were added by this update." fi - # The diffstat is shown from the old revision to the new revision. This - # is to show the truth of what happened in this change. There's no point - # showing the stat from the base to the new revision because the base - # is effectively a random revision at this point - the user will be - # interested in what this revision changed - including the undoing of - # previous revisions in the case of non-fast forward updates. + # The diffstat is shown from the old revision to the new revision. + # This is to show the truth of what happened in this change. + # There's no point showing the stat from the base to the new + # revision because the base is effectively a random revision at this + # point - the user will be interested in what this revision changed + # - including the undoing of previous revisions in the case of + # non-fast-forward updates. echo "" echo "Summary of changes:" - git diff-tree --stat --summary --find-copies-harder $oldrev..$newrev + git diff-tree $diffopts $oldrev..$newrev } # @@ -407,7 +460,7 @@ generate_delete_branch_email() { echo " was $oldrev" echo "" - echo $LOGEND + echo $LOGBEGIN git show -s --pretty=oneline $oldrev echo $LOGEND } @@ -441,7 +494,8 @@ generate_update_atag_email() # generate_atag_email() { - # Use git-for-each-ref to pull out the individual fields from the tag + # Use git for-each-ref to pull out the individual fields from the + # tag eval $(git for-each-ref --shell --format=' tagobject=%(*objectname) tagtype=%(*objecttype) @@ -452,8 +506,10 @@ generate_atag_email() echo " tagging $tagobject ($tagtype)" case "$tagtype" in commit) + # If the tagged object is a commit, then we assume this is a - # release, and so we calculate which tag this tag is replacing + # release, and so we calculate which tag this tag is + # replacing prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null) if [ -n "$prevtag" ]; then @@ -470,25 +526,27 @@ generate_atag_email() echo "" echo $LOGBEGIN - # Show the content of the tag message; this might contain a change log - # or release notes so is worth displaying. + # Show the content of the tag message; this might contain a change + # log or release notes so is worth displaying. git cat-file tag $newrev | sed -e '1,/^$/d' echo "" case "$tagtype" in commit) - # Only commit tags make sense to have rev-list operations performed - # on them + # Only commit tags make sense to have rev-list operations + # performed on them if [ -n "$prevtag" ]; then # Show changes since the previous release git rev-list --pretty=short "$prevtag..$newrev" | git shortlog else - # No previous tag, show all the changes since time began + # No previous tag, show all the changes since time + # began git rev-list --pretty=short $newrev | git shortlog fi ;; *) - # XXX: Is there anything useful we can do for non-commit objects? + # XXX: Is there anything useful we can do for non-commit + # objects? ;; esac @@ -502,7 +560,7 @@ generate_delete_atag_email() { echo " was $oldrev" echo "" - echo $LOGEND + echo $LOGBEGIN git show -s --pretty=oneline $oldrev echo $LOGEND } @@ -537,24 +595,25 @@ generate_update_general_email() # generate_general_email() { - # Unannotated tags are more about marking a point than releasing a version; - # therefore we don't do the shortlog summary that we do for annotated tags - # above - we simply show that the point has been marked, and print the log - # message for the marked point for reference purposes + # Unannotated tags are more about marking a point than releasing a + # version; therefore we don't do the shortlog summary that we do for + # annotated tags above - we simply show that the point has been + # marked, and print the log message for the marked point for + # reference purposes # - # Note this section also catches any other reference type (although there - # aren't any) and deals with them in the same way. + # Note this section also catches any other reference type (although + # there aren't any) and deals with them in the same way. echo "" if [ "$newrev_type" = "commit" ]; then echo $LOGBEGIN - git show --no-color --root -s $newrev + git show --no-color --root -s --pretty=medium $newrev echo $LOGEND else - # What can we do here? The tag marks an object that is not a commit, - # so there is no log for us to display. It's probably not wise to - # output git-cat-file as it could be a binary blob. We'll just say how - # big it is + # What can we do here? The tag marks an object that is not + # a commit, so there is no log for us to display. It's + # probably not wise to output git cat-file as it could be a + # binary blob. We'll just say how big it is echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long." fi } @@ -566,15 +625,82 @@ generate_delete_general_email() { echo " was $oldrev" echo "" - echo $LOGEND + echo $LOGBEGIN git show -s --pretty=oneline $oldrev echo $LOGEND } + +# --------------- Miscellaneous utilities + +# +# Show new revisions as the user would like to see them in the email. +# +show_new_revisions() +{ + # This shows all log entries that are not already covered by + # another ref - i.e. commits that are now accessible from this + # ref that were previously not accessible + # (see generate_update_branch_email for the explanation of this + # command) + + # Revision range passed to rev-list differs for new vs. updated + # branches. + if [ "$change_type" = create ] + then + # Show all revisions exclusive to this (new) branch. + revspec=$newrev + else + # Branch update; show revisions not part of $oldrev. + revspec=$oldrev..$newrev + fi + + other_branches=$(git for-each-ref --format='%(refname)' refs/heads/ | + grep -F -v $refname) + git rev-parse --not $other_branches | + if [ -z "$custom_showrev" ] + then + git rev-list --pretty --stdin $revspec + else + git rev-list --stdin $revspec | + while read onerev + do + eval $(printf "$custom_showrev" $onerev) + done + fi +} + + +limit_lines() +{ + lines=0 + skipped=0 + while IFS="" read -r line; do + lines=$((lines + 1)) + if [ $lines -gt $1 ]; then + skipped=$((skipped + 1)) + else + printf "%s\n" "$line" + fi + done + if [ $skipped -ne 0 ]; then + echo "... $skipped lines suppressed ..." + fi +} + + +send_mail() +{ + if [ -n "$envelopesender" ]; then + /usr/sbin/sendmail -t -f "$envelopesender" + else + /usr/sbin/sendmail -t + fi +} + # ---------------------------- main() # --- Constants -EMAILPREFIX="[SCM] " LOGBEGIN="- Log -----------------------------------------------------------------" LOGEND="-----------------------------------------------------------------------" @@ -587,33 +713,35 @@ if [ -z "$GIT_DIR" ]; then exit 1 fi -projectdesc=$(sed -ne '1p' "$GIT_DIR/description") -# Check if the description is unchanged from it's default, and shorten it to a -# more manageable length if it is +projectdesc=$(sed -ne '1p' "$GIT_DIR/description" 2>/dev/null) +# Check if the description is unchanged from it's default, and shorten it to +# a more manageable length if it is if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null then projectdesc="UNNAMED PROJECT" fi -recipients=$(git repo-config hooks.mailinglist) -announcerecipients=$(git repo-config hooks.announcelist) -envelopesender=$(git-repo-config hooks.envelopesender) +recipients=$(git config hooks.mailinglist) +announcerecipients=$(git config hooks.announcelist) +envelopesender=$(git config hooks.envelopesender) +emailprefix=$(git config hooks.emailprefix || echo '[SCM] ') +custom_showrev=$(git config hooks.showrev) +maxlines=$(git config hooks.emailmaxlines) +diffopts=$(git config hooks.diffopts) +: ${diffopts:="--stat --summary --find-copies-harder"} # --- Main loop -# Allow dual mode: run from the command line just like the update hook, or if -# no arguments are given then run as a hook script +# Allow dual mode: run from the command line just like the update hook, or +# if no arguments are given then run as a hook script if [ -n "$1" -a -n "$2" -a -n "$3" ]; then # Output to the terminal in command line mode - if someone wanted to - # resend an email; they could redirect the output to sendmail themselves - PAGER= generate_email $2 $3 $1 + # resend an email; they could redirect the output to sendmail + # themselves + prep_for_email $2 $3 $1 && PAGER= generate_email else - if [ -n "$envelopesender" ]; then - envelopesender="-f '$envelopesender'" - fi - while read oldrev newrev refname do - generate_email $oldrev $newrev $refname | - /usr/sbin/sendmail -t $envelopesender + prep_for_email $oldrev $newrev $refname || continue + generate_email $maxlines | send_mail done fi diff --git a/contrib/hooks/pre-auto-gc-battery b/contrib/hooks/pre-auto-gc-battery new file mode 100644 index 0000000000..1f914c94aa --- /dev/null +++ b/contrib/hooks/pre-auto-gc-battery @@ -0,0 +1,43 @@ +#!/bin/sh +# +# An example hook script to verify if you are on battery, in case you +# are running Linux or OS X. Called by git-gc --auto with no arguments. +# The hook should exit with non-zero status after issuing an appropriate +# message if it wants to stop the auto repacking. +# +# This hook is stored in the contrib/hooks directory. Your distribution +# may have put this somewhere else. If you want to use this hook, you +# should make this script executable then link to it in the repository +# you would like to use it in. +# +# For example, if the hook is stored in +# /usr/share/git-core/contrib/hooks/pre-auto-gc-battery: +# +# chmod a+x pre-auto-gc-battery +# cd /path/to/your/repository.git +# ln -sf /usr/share/git-core/contrib/hooks/pre-auto-gc-battery \ +# hooks/pre-auto-gc + +if test -x /sbin/on_ac_power && /sbin/on_ac_power +then + exit 0 +elif test "$(cat /sys/class/power_supply/AC/online 2>/dev/null)" = 1 +then + exit 0 +elif grep -q 'on-line' /proc/acpi/ac_adapter/AC/state 2>/dev/null +then + exit 0 +elif grep -q '0x01$' /proc/apm 2>/dev/null +then + exit 0 +elif grep -q "AC Power \+: 1" /proc/pmu/info 2>/dev/null +then + exit 0 +elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt | + grep -q "Currently drawing from 'AC Power'" +then + exit 0 +fi + +echo "Auto packing deferred; not on AC" +exit 1 diff --git a/contrib/hooks/setgitperms.perl b/contrib/hooks/setgitperms.perl new file mode 100644 index 0000000000..a577ad095f --- /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; + } +} diff --git a/contrib/hooks/update-paranoid b/contrib/hooks/update-paranoid index 068fa37083..d18b317b2f 100644 --- a/contrib/hooks/update-paranoid +++ b/contrib/hooks/update-paranoid @@ -136,6 +136,7 @@ sub parse_config ($$$$) { local $ENV{GIT_DIR} = shift; my $br = shift; my $fn = shift; + return unless git_value('rev-list','--max-count=1',$br,'--',$fn); info "Loading $br:$fn"; open(I,'-|','git','cat-file','blob',"$br:$fn"); my $section = ''; @@ -225,14 +226,12 @@ sub load_diff ($) { local $/ = "\0"; my %this_diff; if ($base =~ /^0{40}$/) { - open(T,'-|','git','ls-tree', - '-r','--name-only','-z', - $new) or return undef; - while (<T>) { - chop; - $this_diff{$_} = 'A'; - } - close T or return undef; + # Don't load the diff at all; we are making the + # branch and have no base to compare to in this + # case. A file level ACL makes no sense in this + # context. Having an empty diff will allow the + # branch creation. + # } else { open(T,'-|','git','diff-tree', '-r','--name-status','-z', @@ -260,6 +259,7 @@ deny "Refusing funny ref $ref" unless $ref =~ s,^refs/,,; deny "Bad old value $old" unless $old =~ /^[a-z0-9]{40}$/; deny "Bad new value $new" unless $new =~ /^[a-z0-9]{40}$/; deny "Cannot determine who you are." unless $this_user; +grant "No change requested." if $old eq $new; $repository_name = File::Spec->rel2abs($git_dir); $repository_name =~ m,/([^/]+)(?:\.git|/\.git)$,; |