diff options
Diffstat (limited to 'contrib')
43 files changed, 5224 insertions, 1288 deletions
diff --git a/contrib/ciabot/INSTALL b/contrib/ciabot/INSTALL deleted file mode 100644 index 7222961d35..0000000000 --- a/contrib/ciabot/INSTALL +++ /dev/null @@ -1,54 +0,0 @@ -= Installation instructions = - -Two scripts are included. The Python one (ciabot.py) is faster and -more capable; the shell one (ciabot.sh) is a fallback in case Python -gives your git hosting site indigestion. (I know of no such sites.) - -It is no longer necessary to modify the script in order to put it -in place; in fact, this is now discouraged. It is entirely -configurable with the following git config variables: - -ciabot.project = name of the project -ciabot.repo = name of the project repo for gitweb/cgit purposes -ciabot.xmlrpc = if true, ship notifications via XML-RPC -ciabot.revformat = format in which the revision is shown - -The revformat variable may have the following values -raw -> full hex ID of commit -short -> first 12 chars of hex ID -describe -> describe relative to last tag, falling back to short - -ciabot.project defaults to the directory name of the repository toplevel. -ciabot.repo defaults to ciabot.project lowercased. -ciabot.xmlrpc defaults to True -ciabot.revformat defaults to 'describe'. - -This means that in the normal case you need not do any configuration at all, -however setting ciabot.project will allow the hook to run slightly faster. - -Once you've set these variables, try your script with -n to see the -notification message dumped to stdout and verify that it looks sane. - -To live-test these scripts, your project needs to have been registered with -the CIA site. Here are the steps: - -1. Open an IRC window on irc://freenode/commits or your registered - project IRC channel. - -2. Run ciabot.py and/or ciabot.sh from any directory under git - control. - -You should see a notification on the channel for your most recent commit. - -After verifying correct function, install one of these scripts either -in a post-commit hook or in an update hook. - -In post-commit, run it without arguments. It will query for -current HEAD and the latest commit ID to get the information it -needs. - -In update, call it with a refname followed by a list of commits: -You want to reverse the order git rev-list emits because it lists -from most recent to oldest. - -/path/to/ciabot.py ${refname} $(git rev-list ${oldhead}..${newhead} | tac) diff --git a/contrib/ciabot/README b/contrib/ciabot/README deleted file mode 100644 index 2dfe1f91f5..0000000000 --- a/contrib/ciabot/README +++ /dev/null @@ -1,11 +0,0 @@ -These are hook scripts for the CIA notification service at <http://cia.vc/> - -They are maintained by Eric S. Raymond <esr@thyrsus.com>. There is an -upstream resource page for them at <http://www.catb.org/esr/ciabot/>, -but they are unlikely to change rapidly. - -You probably want the Python version; it's faster, more capable, and -better documented. The shell version is maintained only as a fallback -for use on hosting sites that don't permit Python hook scripts. - -See the file INSTALL for installation instructions. diff --git a/contrib/ciabot/ciabot.py b/contrib/ciabot/ciabot.py deleted file mode 100755 index 36b5665ff8..0000000000 --- a/contrib/ciabot/ciabot.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010 Eric S. Raymond <esr@thyrsus.com> -# Distributed under BSD terms. -# -# This script contains porcelain and porcelain byproducts. -# It's Python because the Python standard libraries avoid portability/security -# issues raised by callouts in the ancestral Perl and sh scripts. It should -# be compatible back to Python 2.1.5 -# -# usage: ciabot.py [-V] [-n] [-p projectname] [refname [commits...]] -# -# This script is meant to be run either in a post-commit hook or in an -# update hook. Try it with -n to see the notification mail dumped to -# stdout and verify that it looks sane. With -V it dumps its version -# and exits. -# -# In post-commit, run it without arguments. It will query for -# current HEAD and the latest commit ID to get the information it -# needs. -# -# In update, call it with a refname followed by a list of commits: -# You want to reverse the order git rev-list emits because it lists -# from most recent to oldest. -# -# /path/to/ciabot.py ${refname} $(git rev-list ${oldhead}..${newhead} | tac) -# -# Configuration variables affecting this script: -# -# ciabot.project = name of the project -# ciabot.repo = name of the project repo for gitweb/cgit purposes -# ciabot.xmlrpc = if true (default), ship notifications via XML-RPC -# ciabot.revformat = format in which the revision is shown -# -# ciabot.project defaults to the directory name of the repository toplevel. -# ciabot.repo defaults to ciabot.project lowercased. -# -# This means that in the normal case you need not do any configuration at all, -# but setting the project name will speed it up slightly. -# -# The revformat variable may have the following values -# raw -> full hex ID of commit -# short -> first 12 chars of hex ID -# describe = -> describe relative to last tag, falling back to short -# The default is 'describe'. -# -# Note: the CIA project now says only XML-RPC is reliable, so -# we default to that. -# - -import sys -if sys.hexversion < 0x02000000: - # The limiter is the xml.sax module - sys.stderr.write("ciabot.py: requires Python 2.0.0 or later.\n") - sys.exit(1) - -import os, commands, socket, urllib -from xml.sax.saxutils import escape - -# Changeset URL prefix for your repo: when the commit ID is appended -# to this, it should point at a CGI that will display the commit -# through gitweb or something similar. The defaults will probably -# work if you have a typical gitweb/cgit setup. -# -#urlprefix="http://%(host)s/cgi-bin/gitweb.cgi?p=%(repo)s;a=commit;h=" -urlprefix="http://%(host)s/cgi-bin/cgit.cgi/%(repo)s/commit/?id=" - -# The service used to turn your gitwebbish URL into a tinyurl so it -# will take up less space on the IRC notification line. -tinyifier = "http://tinyurl.com/api-create.php?url=" - -# The template used to generate the XML messages to CIA. You can make -# visible changes to the IRC-bot notification lines by hacking this. -# The default will produce a notfication line that looks like this: -# -# ${project}: ${author} ${repo}:${branch} * ${rev} ${files}: ${logmsg} ${url} -# -# By omitting $files you can collapse the files part to a single slash. -xml = '''\ -<message> - <generator> - <name>CIA Python client for Git</name> - <version>%(version)s</version> - <url>%(generator)s</url> - </generator> - <source> - <project>%(project)s</project> - <branch>%(repo)s:%(branch)s</branch> - </source> - <timestamp>%(ts)s</timestamp> - <body> - <commit> - <author>%(author)s</author> - <revision>%(rev)s</revision> - <files> - %(files)s - </files> - <log>%(logmsg)s %(url)s</log> - <url>%(url)s</url> - </commit> - </body> -</message> -''' - -# -# No user-serviceable parts below this line: -# - -# Where to ship e-mail notifications. -toaddr = "cia@cia.vc" - -# Identify the generator script. -# Should only change when the script itself gets a new home and maintainer. -generator = "http://www.catb.org/~esr/ciabot.py" -version = "3.6" - -def do(command): - return commands.getstatusoutput(command)[1] - -def report(refname, merged, xmlrpc=True): - "Generate a commit notification to be reported to CIA" - - # Try to tinyfy a reference to a web view for this commit. - try: - url = open(urllib.urlretrieve(tinyifier + urlprefix + merged)[0]).read() - except: - url = urlprefix + merged - - branch = os.path.basename(refname) - - # Compute a description for the revision - if revformat == 'raw': - rev = merged - elif revformat == 'short': - rev = '' - else: # revformat == 'describe' - rev = do("git describe %s 2>/dev/null" % merged) - if not rev: - rev = merged[:12] - - # Extract the meta-information for the commit - files=do("git diff-tree -r --name-only '"+ merged +"' | sed -e '1d' -e 's-.*-<file>&</file>-'") - metainfo = do("git log -1 '--pretty=format:%an <%ae>%n%at%n%s' " + merged) - (author, ts, logmsg) = metainfo.split("\n") - logmsg = escape(logmsg) - - # This discards the part of the author's address after @. - # Might be be nice to ship the full email address, if not - # for spammers' address harvesters - getting this wrong - # would make the freenode #commits channel into harvester heaven. - author = escape(author.replace("<", "").split("@")[0].split()[-1]) - - # This ignores the timezone. Not clear what to do with it... - ts = ts.strip().split()[0] - - context = locals() - context.update(globals()) - - out = xml % context - mail = '''\ -Message-ID: <%(merged)s.%(author)s@%(project)s> -From: %(fromaddr)s -To: %(toaddr)s -Content-type: text/xml -Subject: DeliverXML - -%(out)s''' % locals() - - if xmlrpc: - return out - else: - return mail - -if __name__ == "__main__": - import getopt - - # Get all config variables - revformat = do("git config --get ciabot.revformat") - project = do("git config --get ciabot.project") - repo = do("git config --get ciabot.repo") - xmlrpc = do("git config --get ciabot.xmlrpc") - xmlrpc = not (xmlrpc and xmlrpc == "false") - - host = socket.getfqdn() - fromaddr = "CIABOT-NOREPLY@" + host - - try: - (options, arguments) = getopt.getopt(sys.argv[1:], "np:xV") - except getopt.GetoptError, msg: - print "ciabot.py: " + str(msg) - raise SystemExit, 1 - - notify = True - for (switch, val) in options: - if switch == '-p': - project = val - elif switch == '-n': - notify = False - elif switch == '-x': - xmlrpc = True - elif switch == '-V': - print "ciabot.py: version", version - sys.exit(0) - - # The project variable defaults to the name of the repository toplevel. - if not project: - here = os.getcwd() - while True: - if os.path.exists(os.path.join(here, ".git")): - project = os.path.basename(here) - break - elif here == '/': - sys.stderr.write("ciabot.py: no .git below root!\n") - sys.exit(1) - here = os.path.dirname(here) - - if not repo: - repo = project.lower() - - urlprefix = urlprefix % globals() - - # The script wants a reference to head followed by the list of - # commit ID to report about. - if len(arguments) == 0: - refname = do("git symbolic-ref HEAD 2>/dev/null") - merges = [do("git rev-parse HEAD")] - else: - refname = arguments[0] - merges = arguments[1:] - - if notify: - if xmlrpc: - import xmlrpclib - server = xmlrpclib.Server('http://cia.vc/RPC2'); - else: - import smtplib - server = smtplib.SMTP('localhost') - - for merged in merges: - message = report(refname, merged, xmlrpc) - if not notify: - print message - elif xmlrpc: - try: - # RPC server is flaky, this can fail due to timeout. - server.hub.deliver(message) - except socket.error, e: - sys.stderr.write("%s\n" % e) - else: - server.sendmail(fromaddr, [toaddr], message) - - if notify: - if not xmlrpc: - server.quit() - -#End diff --git a/contrib/ciabot/ciabot.sh b/contrib/ciabot/ciabot.sh deleted file mode 100755 index 3fbbc534ae..0000000000 --- a/contrib/ciabot/ciabot.sh +++ /dev/null @@ -1,233 +0,0 @@ -#!/bin/sh -# Distributed under the terms of the GNU General Public License v2 -# Copyright (c) 2006 Fernando J. Pereda <ferdy@gentoo.org> -# Copyright (c) 2008 Natanael Copa <natanael.copa@gmail.com> -# Copyright (c) 2010 Eric S. Raymond <esr@thyrsus.com> -# Assistance and review by Petr Baudis, author of ciabot.pl, -# is gratefully acknowledged. -# -# This is a version 3.x of ciabot.sh; use -V to find the exact -# version. Versions 1 and 2 were shipped in 2006 and 2008 and are not -# version-stamped. The version 2 maintainer has passed the baton. -# -# Note: This script should be considered obsolete. -# There is a faster, better-documented rewrite in Python: find it as ciabot.py -# Use this only if your hosting site forbids Python hooks. -# It requires: git(1), hostname(1), cut(1), sendmail(1), and wget(1). -# -# Originally based on Git ciabot.pl by Petr Baudis. -# This script contains porcelain and porcelain byproducts. -# -# usage: ciabot.sh [-V] [-n] [-p projectname] [refname commit] -# -# This script is meant to be run either in a post-commit hook or in an -# update hook. Try it with -n to see the notification mail dumped to -# stdout and verify that it looks sane. With -V it dumps its version -# and exits. -# -# In post-commit, run it without arguments. It will query for -# current HEAD and the latest commit ID to get the information it -# needs. -# -# In update, you have to call it once per merged commit: -# -# refname=$1 -# oldhead=$2 -# newhead=$3 -# for merged in $(git rev-list ${oldhead}..${newhead} | tac) ; do -# /path/to/ciabot.sh ${refname} ${merged} -# done -# -# The reason for the tac call is that git rev-list emits commits from -# most recent to least - better to ship notifactions from oldest to newest. -# -# Configuration variables affecting this script: -# -# ciabot.project = name of the project -# ciabot.repo = name of the project repo for gitweb/cgit purposes -# ciabot.revformat = format in which the revision is shown -# -# ciabot.project defaults to the directory name of the repository toplevel. -# ciabot.repo defaults to ciabot.project lowercased. -# -# This means that in the normal case you need not do any configuration at all, -# but setting the project name will speed it up slightly. -# -# The revformat variable may have the following values -# raw -> full hex ID of commit -# short -> first 12 chars of hex ID -# describe = -> describe relative to last tag, falling back to short -# The default is 'describe'. -# -# Note: the shell ancestors of this script used mail, not XML-RPC, in -# order to avoid stalling until timeout when the CIA XML-RPC server is -# down. It is unknown whether this is still an issue in 2010, but -# XML-RPC would be annoying to do from sh in any case. (XML-RPC does -# have the advantage that it guarantees notification of multiple commits -# shpped from an update in their actual order.) -# - -# The project as known to CIA. You can set this with a -p option, -# or let it default to the directory name of the repo toplevel. -project=$(git config --get ciabot.project) - -if [ -z $project ] -then - here=`pwd`; - while :; do - if [ -d $here/.git ] - then - project=`basename $here` - break - elif [ $here = '/' ] - then - echo "ciabot.sh: no .git below root!" - exit 1 - fi - here=`dirname $here` - done -fi - -# Name of the repo for gitweb/cgit purposes -repo=$(git config --get ciabot.repo) -[ -z $repo] && repo=$(echo "${project}" | tr '[A-Z]' '[a-z]') - -# What revision format do we want in the summary? -revformat=$(git config --get ciabot.revformat) - -# Fully qualified domain name of the repo host. You can hardwire this -# to make the script faster. The -f option works under Linux and FreeBSD, -# but not OpenBSD and NetBSD. But under OpenBSD and NetBSD, -# hostname without options gives the FQDN. -if hostname -f >/dev/null 2>&1 -then - hostname=`hostname -f` -else - hostname=`hostname` -fi - -# Changeset URL prefix for your repo: when the commit ID is appended -# to this, it should point at a CGI that will display the commit -# through gitweb or something similar. The defaults will probably -# work if you have a typical gitweb/cgit setup. -#urlprefix="http://${host}/cgi-bin/gitweb.cgi?p=${repo};a=commit;h=" -urlprefix="http://${host}/cgi-bin/cgit.cgi/${repo}/commit/?id=" - -# -# You probably will not need to change the following: -# - -# Identify the script. The 'generator' variable should change only -# when the script itself gets a new home and maintainer. -generator="http://www.catb.org/~esr/ciabot/ciabot.sh" -version=3.5 - -# Addresses for the e-mail -from="CIABOT-NOREPLY@${hostname}" -to="cia@cia.vc" - -# SMTP client to use - may need to edit the absolute pathname for your system -sendmail="sendmail -t -f ${from}" - -# -# No user-serviceable parts below this line: -# - -# Should include all places sendmail is likely to lurk. -PATH="$PATH:/usr/sbin/" - -mode=mailit -while getopts pnV opt -do - case $opt in - p) project=$2; shift ; shift ;; - n) mode=dumpit; shift ;; - V) echo "ciabot.sh: version $version"; exit 0; shift ;; - esac -done - -# Cough and die if user has not specified a project -if [ -z "$project" ] -then - echo "ciabot.sh: no project specified, bailing out." >&2 - exit 1 -fi - -if [ $# -eq 0 ] ; then - refname=$(git symbolic-ref HEAD 2>/dev/null) - merged=$(git rev-parse HEAD) -else - refname=$1 - merged=$2 -fi - -# This tries to turn your gitwebbish URL into a tinyurl so it will take up -# less space on the IRC notification line. Some repo sites (I'm looking at -# you, berlios.de!) forbid wget calls for security reasons. On these, -# the code will fall back to the full un-tinyfied URL. -longurl=${urlprefix}${merged} -url=$(wget -O - -q http://tinyurl.com/api-create.php?url=${longurl} 2>/dev/null) -if [ -z "$url" ]; then - url="${longurl}" -fi - -refname=${refname##refs/heads/} - -case $revformat in -raw) rev=$merged ;; -short) rev='' ;; -*) rev=$(git describe ${merged} 2>/dev/null) ;; -esac -[ -z ${rev} ] && rev=$(echo "$merged" | cut -c 1-12) - -# We discard the part of the author's address after @. -# Might be nice to ship the full email address, if not -# for spammers' address harvesters - getting this wrong -# would make the freenode #commits channel into harvester heaven. -author=$(git log -1 '--pretty=format:%an <%ae>' $merged) -author=$(echo "$author" | sed -n -e '/^.*<\([^@]*\).*$/s--\1-p') - -logmessage=$(git log -1 '--pretty=format:%s' $merged) -ts=$(git log -1 '--pretty=format:%at' $merged) -files=$(git diff-tree -r --name-only ${merged} | sed -e '1d' -e 's-.*-<file>&</file>-') - -out=" -<message> - <generator> - <name>CIA Shell client for Git</name> - <version>${version}</version> - <url>${generator}</url> - </generator> - <source> - <project>${project}</project> - <branch>$repo:${refname}</branch> - </source> - <timestamp>${ts}</timestamp> - <body> - <commit> - <author>${author}</author> - <revision>${rev}</revision> - <files> - ${files} - </files> - <log>${logmessage} ${url}</log> - <url>${url}</url> - </commit> - </body> -</message>" - -if [ "$mode" = "dumpit" ] -then - sendmail=cat -fi - -${sendmail} << EOM -Message-ID: <${merged}.${author}@${project}> -From: ${from} -To: ${to} -Content-type: text/xml -Subject: DeliverXML -${out} -EOM - -# vim: set tw=70 : diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 6c3bafeea5..dba3c15700 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -33,8 +33,6 @@ esac # returns location of .git repo __gitdir () { - # Note: this function is duplicated in git-prompt.sh - # When updating it, make sure you update the other one to match. if [ -z "${1-}" ]; then if [ -n "${__git_dir-}" ]; then echo "$__git_dir" @@ -650,6 +648,7 @@ __git_list_porcelain_commands () cat-file) : plumbing;; check-attr) : plumbing;; check-ignore) : plumbing;; + check-mailmap) : plumbing;; check-ref-format) : plumbing;; checkout-index) : plumbing;; commit-tree) : plumbing;; @@ -902,7 +901,7 @@ _git_add () esac # XXX should we check for --update and --all options ? - __git_complete_index_file "--others --modified" + __git_complete_index_file "--others --modified --directory --no-empty-directory" } _git_archive () @@ -1064,7 +1063,7 @@ _git_clean () esac # XXX should we check for -x option ? - __git_complete_index_file "--others" + __git_complete_index_file "--others --directory" } _git_clone () @@ -1189,7 +1188,7 @@ _git_diff () __git_complete_revlist_file } -__git_mergetools_common="diffuse ecmerge emerge kdiff3 meld opendiff +__git_mergetools_common="diffuse diffmerge ecmerge emerge kdiff3 meld opendiff tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3 codecompare " @@ -1959,7 +1958,6 @@ _git_config () core.fileMode core.fsyncobjectfiles core.gitProxy - core.ignoreCygwinFSTricks core.ignoreStat core.ignorecase core.logAllRefUpdates @@ -2492,9 +2490,10 @@ __git_main () i="${words[c]}" case "$i" in --git-dir=*) __git_dir="${i#--git-dir=}" ;; + --git-dir) ((c++)) ; __git_dir="${words[c]}" ;; --bare) __git_dir="." ;; --help) command="help"; break ;; - -c) c=$((++c)) ;; + -c|--work-tree|--namespace) ((c++)) ;; -*) ;; *) command="$i"; break ;; esac @@ -2512,6 +2511,7 @@ __git_main () --exec-path --exec-path= --html-path + --man-path --info-path --work-tree= --namespace= @@ -2580,7 +2580,7 @@ if [[ -n ${ZSH_VERSION-} ]]; then --*=*|*.) ;; *) c="$c " ;; esac - array+=("$c") + array[${#array[@]}+1]="$c" done compset -P '*[=:]' compadd -Q -S '' -p "${2-}" -a -- array && _ret=0 diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh index 07a6218d10..7b732d2aeb 100644 --- a/contrib/completion/git-prompt.sh +++ b/contrib/completion/git-prompt.sh @@ -3,7 +3,7 @@ # Copyright (C) 2006,2007 Shawn O. Pearce <spearce@spearce.org> # Distributed under the GNU General Public License, version 2.0. # -# This script allows you to see the current branch in your prompt. +# This script allows you to see repository status in your prompt. # # To enable: # @@ -13,24 +13,27 @@ # 3a) Change your PS1 to call __git_ps1 as # command-substitution: # Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' -# ZSH: PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ ' +# ZSH: setopt PROMPT_SUBST ; PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ ' # the optional argument will be used as format string. -# 3b) Alternatively, if you are using bash, __git_ps1 can be -# used for PROMPT_COMMAND with two parameters, <pre> and -# <post>, which are strings you would put in $PS1 before -# and after the status string generated by the git-prompt -# machinery. e.g. +# 3b) Alternatively, for a slightly faster prompt, __git_ps1 can +# be used for PROMPT_COMMAND in Bash or for precmd() in Zsh +# with two parameters, <pre> and <post>, which are strings +# you would put in $PS1 before and after the status string +# generated by the git-prompt machinery. e.g. # Bash: PROMPT_COMMAND='__git_ps1 "\u@\h:\w" "\\\$ "' +# will show username, at-sign, host, colon, cwd, then +# various status string, followed by dollar and SP, as +# your prompt. # ZSH: precmd () { __git_ps1 "%n" ":%~$ " "|%s" } -# will show username, at-sign, host, colon, cwd, then -# various status string, followed by dollar and SP, as -# your prompt. +# will show username, pipe, then various status string, +# followed by colon, cwd, dollar and SP, as your prompt. # Optionally, you can supply a third argument with a printf # format string to finetune the output of the branch status # -# The argument to __git_ps1 will be displayed only if you are currently -# in a git repository. The %s token will be the name of the current -# branch. +# The repository status will be displayed only if you are currently in a +# git repository. The %s token is the placeholder for the shown status. +# +# The prompt status always includes the current branch name. # # In addition, if you set GIT_PS1_SHOWDIRTYSTATE to a nonempty value, # unstaged (*) and staged (+) changes will be shown next to the branch @@ -57,6 +60,7 @@ # of values: # # verbose show number of commits ahead/behind (+/-) upstream +# name if verbose, then also show the upstream abbrev name # legacy don't use the '--count' option available in recent # versions of git-rev-list # git always compare HEAD to @{upstream} @@ -78,31 +82,12 @@ # # If you would like a colored hint about the current dirty state, set # GIT_PS1_SHOWCOLORHINTS to a nonempty value. The colors are based on -# the colored output of "git status -sb". +# the colored output of "git status -sb" and are available only when +# using __git_ps1 for PROMPT_COMMAND or precmd. -# __gitdir accepts 0 or 1 arguments (i.e., location) -# returns location of .git repo -__gitdir () -{ - # Note: this function is duplicated in git-completion.bash - # When updating it, make sure you update the other one to match. - if [ -z "${1-}" ]; then - if [ -n "${__git_dir-}" ]; then - echo "$__git_dir" - elif [ -n "${GIT_DIR-}" ]; then - test -d "${GIT_DIR-}" || return 1 - echo "$GIT_DIR" - elif [ -d .git ]; then - echo .git - else - git rev-parse --git-dir 2>/dev/null - fi - elif [ -d "$1/.git" ]; then - echo "$1/.git" - else - echo "$1" - fi -} +# check whether printf supports -v +__git_printf_supports_v= +printf -v __git_printf_supports_v -- '%s' yes >/dev/null 2>&1 # stores the divergence from upstream in $p # used by GIT_PS1_SHOWUPSTREAM @@ -110,7 +95,7 @@ __git_ps1_show_upstream () { local key value local svn_remote svn_url_pattern count n - local upstream=git legacy="" verbose="" + local upstream=git legacy="" verbose="" name="" svn_remote=() # get some config options from git-config @@ -126,7 +111,7 @@ __git_ps1_show_upstream () ;; svn-remote.*.url) svn_remote[$((${#svn_remote[@]} + 1))]="$value" - svn_url_pattern+="\\|$value" + svn_url_pattern="$svn_url_pattern\\|$value" upstream=svn+git # default upstream is SVN if available, else git ;; esac @@ -138,6 +123,7 @@ __git_ps1_show_upstream () git|svn) upstream="$option" ;; verbose) verbose=1 ;; legacy) legacy=1 ;; + name) name=1 ;; esac done @@ -220,13 +206,16 @@ __git_ps1_show_upstream () *) # diverged from upstream p=" u+${count#* }-${count% *}" ;; esac + if [[ -n "$count" && -n "$name" ]]; then + p="$p $(git rev-parse --abbrev-ref "$upstream" 2>/dev/null)" + fi fi } # Helper function that is meant to be called from __git_ps1. It -# builds up a gitstring injecting color codes into the appropriate -# places. +# injects color codes into the appropriate gitstring variables used +# to build a gitstring. __git_ps1_colorize_gitstring () { if [[ -n ${ZSH_VERSION-} ]]; then @@ -234,74 +223,40 @@ __git_ps1_colorize_gitstring () local c_green='%F{green}' local c_lblue='%F{blue}' local c_clear='%f' - local bad_color=$c_red - local ok_color=$c_green - local branch_color="$c_clear" - local flags_color="$c_lblue" - local branchstring="$c${b##refs/heads/}" - - if [ $detached = no ]; then - branch_color="$ok_color" - else - branch_color="$bad_color" - fi - - gitstring="$branch_color$branchstring$c_clear" - - if [ -n "$w$i$s$u$r$p" ]; then - gitstring="$gitstring$z" - fi - if [ "$w" = "*" ]; then - gitstring="$gitstring$bad_color$w" - fi - if [ -n "$i" ]; then - gitstring="$gitstring$ok_color$i" - fi - if [ -n "$s" ]; then - gitstring="$gitstring$flags_color$s" - fi - if [ -n "$u" ]; then - gitstring="$gitstring$bad_color$u" - fi - gitstring="$gitstring$c_clear$r$p" - return + else + # Using \[ and \] around colors is necessary to prevent + # issues with command line editing/browsing/completion! + local c_red='\[\e[31m\]' + local c_green='\[\e[32m\]' + local c_lblue='\[\e[1;34m\]' + local c_clear='\[\e[0m\]' fi - local c_red='\e[31m' - local c_green='\e[32m' - local c_lblue='\e[1;34m' - local c_clear='\e[0m' local bad_color=$c_red local ok_color=$c_green - local branch_color="$c_clear" local flags_color="$c_lblue" - local branchstring="$c${b##refs/heads/}" + local branch_color="" if [ $detached = no ]; then branch_color="$ok_color" else branch_color="$bad_color" fi + c="$branch_color$c" - # Setting gitstring directly with \[ and \] around colors - # is necessary to prevent wrapping issues! - gitstring="\[$branch_color\]$branchstring\[$c_clear\]" - - if [ -n "$w$i$s$u$r$p" ]; then - gitstring="$gitstring$z" - fi + z="$c_clear$z" if [ "$w" = "*" ]; then - gitstring="$gitstring\[$bad_color\]$w" + w="$bad_color$w" fi if [ -n "$i" ]; then - gitstring="$gitstring\[$ok_color\]$i" + i="$ok_color$i" fi if [ -n "$s" ]; then - gitstring="$gitstring\[$flags_color\]$s" + s="$flags_color$s" fi if [ -n "$u" ]; then - gitstring="$gitstring\[$bad_color\]$u" + u="$bad_color$u" fi - gitstring="$gitstring\[$c_clear\]$r$p" + r="$c_clear$r" } # __git_ps1 accepts 0 or 1 arguments (i.e., format string) @@ -335,50 +290,83 @@ __git_ps1 () ;; esac - local g="$(__gitdir)" - if [ -z "$g" ]; then + local repo_info rev_parse_exit_code + repo_info="$(git rev-parse --git-dir --is-inside-git-dir \ + --is-bare-repository --is-inside-work-tree \ + --short HEAD 2>/dev/null)" + rev_parse_exit_code="$?" + + if [ -z "$repo_info" ]; then if [ $pcmode = yes ]; then #In PC mode PS1 always needs to be set PS1="$ps1pc_start$ps1pc_end" fi + return + fi + + local short_sha + if [ "$rev_parse_exit_code" = "0" ]; then + short_sha="${repo_info##*$'\n'}" + repo_info="${repo_info%$'\n'*}" + fi + local inside_worktree="${repo_info##*$'\n'}" + repo_info="${repo_info%$'\n'*}" + local bare_repo="${repo_info##*$'\n'}" + repo_info="${repo_info%$'\n'*}" + local inside_gitdir="${repo_info##*$'\n'}" + local g="${repo_info%$'\n'*}" + + local r="" + local b="" + local step="" + local total="" + if [ -d "$g/rebase-merge" ]; then + read b 2>/dev/null <"$g/rebase-merge/head-name" + read step 2>/dev/null <"$g/rebase-merge/msgnum" + read total 2>/dev/null <"$g/rebase-merge/end" + if [ -f "$g/rebase-merge/interactive" ]; then + r="|REBASE-i" + else + r="|REBASE-m" + fi else - local r="" - local b="" - local step="" - local total="" - if [ -d "$g/rebase-merge" ]; then - b="$(cat "$g/rebase-merge/head-name" 2>/dev/null)" - step=$(cat "$g/rebase-merge/msgnum" 2>/dev/null) - total=$(cat "$g/rebase-merge/end" 2>/dev/null) - if [ -f "$g/rebase-merge/interactive" ]; then - r="|REBASE-i" + if [ -d "$g/rebase-apply" ]; then + read step 2>/dev/null <"$g/rebase-apply/next" + read total 2>/dev/null <"$g/rebase-apply/last" + if [ -f "$g/rebase-apply/rebasing" ]; then + read b 2>/dev/null <"$g/rebase-apply/head-name" + r="|REBASE" + elif [ -f "$g/rebase-apply/applying" ]; then + r="|AM" else - r="|REBASE-m" + r="|AM/REBASE" fi + elif [ -f "$g/MERGE_HEAD" ]; then + r="|MERGING" + elif [ -f "$g/CHERRY_PICK_HEAD" ]; then + r="|CHERRY-PICKING" + elif [ -f "$g/REVERT_HEAD" ]; then + r="|REVERTING" + elif [ -f "$g/BISECT_LOG" ]; then + r="|BISECTING" + fi + + if [ -n "$b" ]; then + : + elif [ -h "$g/HEAD" ]; then + # symlink symbolic ref + b="$(git symbolic-ref HEAD 2>/dev/null)" else - if [ -d "$g/rebase-apply" ]; then - step=$(cat "$g/rebase-apply/next" 2>/dev/null) - total=$(cat "$g/rebase-apply/last" 2>/dev/null) - if [ -f "$g/rebase-apply/rebasing" ]; then - b="$(cat "$g/rebase-apply/head-name" 2>/dev/null)" - r="|REBASE" - elif [ -f "$g/rebase-apply/applying" ]; then - r="|AM" - else - r="|AM/REBASE" + local head="" + if ! read head 2>/dev/null <"$g/HEAD"; then + if [ $pcmode = yes ]; then + PS1="$ps1pc_start$ps1pc_end" fi - elif [ -f "$g/MERGE_HEAD" ]; then - r="|MERGING" - elif [ -f "$g/CHERRY_PICK_HEAD" ]; then - r="|CHERRY-PICKING" - elif [ -f "$g/REVERT_HEAD" ]; then - r="|REVERTING" - elif [ -f "$g/BISECT_LOG" ]; then - r="|BISECTING" + return fi - - test -n "$b" || - b="$(git symbolic-ref HEAD 2>/dev/null)" || { + # is it a symbolic ref? + b="${head#ref: }" + if [ "$head" = "$b" ]; then detached=yes b="$( case "${GIT_PS1_DESCRIBE_STYLE-}" in @@ -392,70 +380,75 @@ __git_ps1 () git describe --tags --exact-match HEAD ;; esac 2>/dev/null)" || - b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." || - b="unknown" + b="$short_sha..." b="($b)" - } + fi fi + fi - if [ -n "$step" ] && [ -n "$total" ]; then - r="$r $step/$total" - fi + if [ -n "$step" ] && [ -n "$total" ]; then + r="$r $step/$total" + fi - local w="" - local i="" - local s="" - local u="" - local c="" - local p="" + local w="" + local i="" + local s="" + local u="" + local c="" + local p="" - if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then - if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then - c="BARE:" + if [ "true" = "$inside_gitdir" ]; then + if [ "true" = "$bare_repo" ]; then + c="BARE:" + else + b="GIT_DIR!" + fi + elif [ "true" = "$inside_worktree" ]; then + if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ] && + [ "$(git config --bool bash.showDirtyState)" != "false" ] + then + git diff --no-ext-diff --quiet --exit-code || w="*" + if [ -n "$short_sha" ]; then + git diff-index --cached --quiet HEAD -- || i="+" else - b="GIT_DIR!" - fi - elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then - if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ] && - [ "$(git config --bool bash.showDirtyState)" != "false" ] - then - git diff --no-ext-diff --quiet --exit-code || w="*" - if git rev-parse --quiet --verify HEAD >/dev/null; then - git diff-index --cached --quiet HEAD -- || i="+" - else - i="#" - fi - fi - if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then - git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$" + i="#" fi + fi + if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ] && + [ -r "$g/refs/stash" ]; then + s="$" + fi - if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] && - [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] && - [ -n "$(git ls-files --others --exclude-standard)" ] - then - u="%${ZSH_VERSION+%}" - fi + if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] && + [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] && + git ls-files --others --exclude-standard --error-unmatch -- '*' >/dev/null 2>/dev/null + then + u="%${ZSH_VERSION+%}" + fi - if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then - __git_ps1_show_upstream - fi + if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then + __git_ps1_show_upstream fi + fi - local z="${GIT_PS1_STATESEPARATOR-" "}" - local f="$w$i$s$u" - if [ $pcmode = yes ]; then - local gitstring= - if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then - __git_ps1_colorize_gitstring - else - gitstring="$c${b##refs/heads/}${f:+$z$f}$r$p" - fi + local z="${GIT_PS1_STATESEPARATOR-" "}" + + # NO color option unless in PROMPT_COMMAND mode + if [ $pcmode = yes ] && [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then + __git_ps1_colorize_gitstring + fi + + local f="$w$i$s$u" + local gitstring="$c${b##refs/heads/}${f:+$z$f}$r$p" + + if [ $pcmode = yes ]; then + if [ "${__git_printf_supports_v-}" != yes ]; then gitstring=$(printf -- "$printf_format" "$gitstring") - PS1="$ps1pc_start$gitstring$ps1pc_end" else - # NO color option unless in PROMPT_COMMAND mode - printf -- "$printf_format" "$c${b##refs/heads/}${f:+$z$f}$r$p" + printf -v gitstring -- "$printf_format" "$gitstring" fi + PS1="$ps1pc_start$gitstring$ps1pc_end" + else + printf -- "$printf_format" "$gitstring" fi } diff --git a/contrib/contacts/git-contacts b/contrib/contacts/git-contacts new file mode 100755 index 0000000000..428cc1a9a1 --- /dev/null +++ b/contrib/contacts/git-contacts @@ -0,0 +1,205 @@ +#!/usr/bin/perl + +# List people who might be interested in a patch. Useful as the argument to +# git-send-email --cc-cmd option, and in other situations. +# +# Usage: git contacts <file | rev-list option> ... + +use strict; +use warnings; +use IPC::Open2; + +my $since = '5-years-ago'; +my $min_percent = 10; +my $labels_rx = qr/Signed-off-by|Reviewed-by|Acked-by|Cc/i; +my %seen; + +sub format_contact { + my ($name, $email) = @_; + return "$name <$email>"; +} + +sub parse_commit { + my ($commit, $data) = @_; + my $contacts = $commit->{contacts}; + my $inbody = 0; + for (split(/^/m, $data)) { + if (not $inbody) { + if (/^author ([^<>]+) <(\S+)> .+$/) { + $contacts->{format_contact($1, $2)} = 1; + } elsif (/^$/) { + $inbody = 1; + } + } elsif (/^$labels_rx:\s+([^<>]+)\s+<(\S+?)>$/o) { + $contacts->{format_contact($1, $2)} = 1; + } + } +} + +sub import_commits { + my ($commits) = @_; + return unless %$commits; + my $pid = open2 my $reader, my $writer, qw(git cat-file --batch); + for my $id (keys(%$commits)) { + print $writer "$id\n"; + my $line = <$reader>; + if ($line =~ /^([0-9a-f]{40}) commit (\d+)/) { + my ($cid, $len) = ($1, $2); + die "expected $id but got $cid\n" unless $id eq $cid; + my $data; + # cat-file emits newline after data, so read len+1 + read $reader, $data, $len + 1; + parse_commit($commits->{$id}, $data); + } + } + close $reader; + close $writer; + waitpid($pid, 0); + die "git-cat-file error: $?\n" if $?; +} + +sub get_blame { + my ($commits, $source, $from, $ranges) = @_; + return unless @$ranges; + open my $f, '-|', + qw(git blame --porcelain -C), + map({"-L$_->[0],+$_->[1]"} @$ranges), + '--since', $since, "$from^", '--', $source or die; + while (<$f>) { + if (/^([0-9a-f]{40}) \d+ \d+ \d+$/) { + my $id = $1; + $commits->{$id} = { id => $id, contacts => {} } + unless $seen{$id}; + $seen{$id} = 1; + } + } + close $f; +} + +sub blame_sources { + my ($sources, $commits) = @_; + for my $s (keys %$sources) { + for my $id (keys %{$sources->{$s}}) { + get_blame($commits, $s, $id, $sources->{$s}{$id}); + } + } +} + +sub scan_patches { + my ($sources, $id, $f) = @_; + my $source; + while (<$f>) { + if (/^From ([0-9a-f]{40}) Mon Sep 17 00:00:00 2001$/) { + $id = $1; + $seen{$id} = 1; + } + next unless $id; + if (m{^--- (?:a/(.+)|/dev/null)$}) { + $source = $1; + } elsif (/^--- /) { + die "Cannot parse hunk source: $_\n"; + } elsif (/^@@ -(\d+)(?:,(\d+))?/ && $source) { + my $len = defined($2) ? $2 : 1; + push @{$sources->{$source}{$id}}, [$1, $len] if $len; + } + } +} + +sub scan_patch_file { + my ($commits, $file) = @_; + open my $f, '<', $file or die "read failure: $file: $!\n"; + scan_patches($commits, undef, $f); + close $f; +} + +sub parse_rev_args { + my @args = @_; + open my $f, '-|', + qw(git rev-parse --revs-only --default HEAD --symbolic), @args + or die; + my @revs; + while (<$f>) { + chomp; + push @revs, $_; + } + close $f; + return @revs if scalar(@revs) != 1; + return "^$revs[0]", 'HEAD' unless $revs[0] =~ /^-/; + return $revs[0], 'HEAD'; +} + +sub scan_rev_args { + my ($commits, $args) = @_; + my @revs = parse_rev_args(@$args); + open my $f, '-|', qw(git rev-list --reverse), @revs or die; + while (<$f>) { + chomp; + my $id = $_; + $seen{$id} = 1; + open my $g, '-|', qw(git show -C --oneline), $id or die; + scan_patches($commits, $id, $g); + close $g; + } + close $f; +} + +sub mailmap_contacts { + my ($contacts) = @_; + my %mapped; + my $pid = open2 my $reader, my $writer, qw(git check-mailmap --stdin); + for my $contact (keys(%$contacts)) { + print $writer "$contact\n"; + my $canonical = <$reader>; + chomp $canonical; + $mapped{$canonical} += $contacts->{$contact}; + } + close $reader; + close $writer; + waitpid($pid, 0); + die "git-check-mailmap error: $?\n" if $?; + return \%mapped; +} + +if (!@ARGV) { + die "No input revisions or patch files\n"; +} + +my (@files, @rev_args); +for (@ARGV) { + if (-e) { + push @files, $_; + } else { + push @rev_args, $_; + } +} + +my %sources; +for (@files) { + scan_patch_file(\%sources, $_); +} +if (@rev_args) { + scan_rev_args(\%sources, \@rev_args) +} + +my $toplevel = `git rev-parse --show-toplevel`; +chomp $toplevel; +chdir($toplevel) or die "chdir failure: $toplevel: $!\n"; + +my %commits; +blame_sources(\%sources, \%commits); +import_commits(\%commits); + +my $contacts = {}; +for my $commit (values %commits) { + for my $contact (keys %{$commit->{contacts}}) { + $contacts->{$contact}++; + } +} +$contacts = mailmap_contacts($contacts); + +my $ncommits = scalar(keys %commits); +for my $contact (keys %$contacts) { + my $percent = $contacts->{$contact} * 100 / $ncommits; + next if $percent < $min_percent; + print "$contact\n"; +} diff --git a/contrib/contacts/git-contacts.txt b/contrib/contacts/git-contacts.txt new file mode 100644 index 0000000000..dd914d1261 --- /dev/null +++ b/contrib/contacts/git-contacts.txt @@ -0,0 +1,94 @@ +git-contacts(1) +=============== + +NAME +---- +git-contacts - List people who might be interested in a set of changes + + +SYNOPSIS +-------- +[verse] +'git contacts' (<patch>|<range>|<rev>)... + + +DESCRIPTION +----------- + +Given a set of changes, specified as patch files or revisions, determine people +who might be interested in those changes. This is done by consulting the +history of each patch or revision hunk to find people mentioned by commits +which touched the lines of files under consideration. + +Input consists of one or more patch files or revision arguments. A revision +argument can be a range or a single `<rev>` which is interpreted as +`<rev>..HEAD`, thus the same revision arguments are accepted as for +linkgit:git-format-patch[1]. Patch files and revision arguments can be combined +in the same invocation. + +This command can be useful for determining the list of people with whom to +discuss proposed changes, or for finding the list of recipients to Cc: when +submitting a patch series via `git send-email`. For the latter case, `git +contacts` can be used as the argument to `git send-email`'s `--cc-cmd` option. + + +DISCUSSION +---------- + +`git blame` is invoked for each hunk in a patch file or revision. For each +commit mentioned by `git blame`, the commit message is consulted for people who +authored, reviewed, signed, acknowledged, or were Cc:'d. Once the list of +participants is known, each person's relevance is computed by considering how +many commits mentioned that person compared with the total number of commits +under consideration. The final output consists only of participants who exceed +a minimum threshold of participation. + + +OUTPUT +------ + +For each person of interest, a single line is output, terminated by a newline. +If the person's name is known, ``Name $$<user@host>$$'' is printed; otherwise +only ``$$<user@host>$$'' is printed. + + +EXAMPLES +-------- + +* Consult patch files: ++ +------------ +$ git contacts feature/*.patch +------------ + +* Revision range: ++ +------------ +$ git contacts R1..R2 +------------ + +* From a single revision to `HEAD`: ++ +------------ +$ git contacts origin +------------ + +* Helper for `git send-email`: ++ +------------ +$ git send-email --cc-cmd='git contacts' feature/*.patch +------------ + + +LIMITATIONS +----------- + +Several conditions controlling a person's significance are currently +hard-coded, such as minimum participation level (10%), blame date-limiting (5 +years), and `-C` level for detecting moved and copied lines (a single `-C`). In +the future, these conditions may become configurable. + + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/contrib/credential/gnome-keyring/Makefile b/contrib/credential/gnome-keyring/Makefile index e6561d8db6..c3c7c98aa1 100644 --- a/contrib/credential/gnome-keyring/Makefile +++ b/contrib/credential/gnome-keyring/Makefile @@ -8,8 +8,8 @@ CFLAGS = -g -O2 -Wall -include ../../../config.mak.autogen -include ../../../config.mak -INCS:=$(shell pkg-config --cflags gnome-keyring-1) -LIBS:=$(shell pkg-config --libs gnome-keyring-1) +INCS:=$(shell pkg-config --cflags gnome-keyring-1 glib-2.0) +LIBS:=$(shell pkg-config --libs gnome-keyring-1 glib-2.0) SRCS:=$(MAIN).c OBJS:=$(SRCS:.c=.o) diff --git a/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c b/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c index f2cdefee60..635c96bc56 100644 --- a/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c +++ b/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c @@ -25,133 +25,150 @@ #include <stdio.h> #include <string.h> -#include <stdarg.h> #include <stdlib.h> -#include <errno.h> +#include <glib.h> #include <gnome-keyring.h> -/* - * This credential struct and API is simplified from git's credential.{h,c} - */ -struct credential -{ - char *protocol; - char *host; - unsigned short port; - char *path; - char *username; - char *password; -}; +#ifdef GNOME_KEYRING_DEFAULT -#define CREDENTIAL_INIT \ - { NULL,NULL,0,NULL,NULL,NULL } + /* Modern gnome-keyring */ -void credential_init(struct credential *c); -void credential_clear(struct credential *c); -int credential_read(struct credential *c); -void credential_write(const struct credential *c); +#include <gnome-keyring-memory.h> -typedef int (*credential_op_cb)(struct credential*); +#else -struct credential_operation -{ - char *name; - credential_op_cb op; -}; + /* + * Support ancient gnome-keyring, circ. RHEL 5.X. + * GNOME_KEYRING_DEFAULT seems to have been introduced with Gnome 2.22, + * and the other features roughly around Gnome 2.20, 6 months before. + * Ubuntu 8.04 used Gnome 2.22 (I think). Not sure any distro used 2.20. + * So the existence/non-existence of GNOME_KEYRING_DEFAULT seems like + * a decent thing to use as an indicator. + */ -#define CREDENTIAL_OP_END \ - { NULL,NULL } +#define GNOME_KEYRING_DEFAULT NULL /* - * Table with operation callbacks is defined in concrete - * credential helper implementation and contains entries - * like { "get", function_to_get_credential } terminated - * by CREDENTIAL_OP_END. + * ancient gnome-keyring returns DENIED when an entry is not found. + * Setting NO_MATCH to DENIED will prevent us from reporting DENIED + * errors during get and erase operations, but we will still report + * DENIED errors during a store. */ -struct credential_operation const credential_helper_ops[]; +#define GNOME_KEYRING_RESULT_NO_MATCH GNOME_KEYRING_RESULT_DENIED -/* ---------------- common helper functions ----------------- */ +#define gnome_keyring_memory_alloc g_malloc +#define gnome_keyring_memory_free gnome_keyring_free_password +#define gnome_keyring_memory_strdup g_strdup -static inline void free_password(char *password) +static const char* gnome_keyring_result_to_message(GnomeKeyringResult result) { - char *c = password; - if (!password) - return; - - while (*c) *c++ = '\0'; - free(password); + switch (result) { + case GNOME_KEYRING_RESULT_OK: + return "OK"; + case GNOME_KEYRING_RESULT_DENIED: + return "Denied"; + case GNOME_KEYRING_RESULT_NO_KEYRING_DAEMON: + return "No Keyring Daemon"; + case GNOME_KEYRING_RESULT_ALREADY_UNLOCKED: + return "Already UnLocked"; + case GNOME_KEYRING_RESULT_NO_SUCH_KEYRING: + return "No Such Keyring"; + case GNOME_KEYRING_RESULT_BAD_ARGUMENTS: + return "Bad Arguments"; + case GNOME_KEYRING_RESULT_IO_ERROR: + return "IO Error"; + case GNOME_KEYRING_RESULT_CANCELLED: + return "Cancelled"; + case GNOME_KEYRING_RESULT_ALREADY_EXISTS: + return "Already Exists"; + default: + return "Unknown Error"; + } } -static inline void warning(const char *fmt, ...) +/* + * Support really ancient gnome-keyring, circ. RHEL 4.X. + * Just a guess for the Glib version. Glib 2.8 was roughly Gnome 2.12 ? + * Which was released with gnome-keyring 0.4.3 ?? + */ +#if GLIB_MAJOR_VERSION == 2 && GLIB_MINOR_VERSION < 8 + +static void gnome_keyring_done_cb(GnomeKeyringResult result, gpointer user_data) { - va_list ap; + gpointer *data = (gpointer*) user_data; + int *done = (int*) data[0]; + GnomeKeyringResult *r = (GnomeKeyringResult*) data[1]; - va_start(ap, fmt); - fprintf(stderr, "warning: "); - vfprintf(stderr, fmt, ap); - fprintf(stderr, "\n" ); - va_end(ap); + *r = result; + *done = 1; } -static inline void error(const char *fmt, ...) +static void wait_for_request_completion(int *done) { - va_list ap; - - va_start(ap, fmt); - fprintf(stderr, "error: "); - vfprintf(stderr, fmt, ap); - fprintf(stderr, "\n" ); - va_end(ap); + GMainContext *mc = g_main_context_default(); + while (!*done) + g_main_context_iteration(mc, TRUE); } -static inline void die(const char *fmt, ...) +static GnomeKeyringResult gnome_keyring_item_delete_sync(const char *keyring, guint32 id) { - va_list ap; + int done = 0; + GnomeKeyringResult result; + gpointer data[] = { &done, &result }; + + gnome_keyring_item_delete(keyring, id, gnome_keyring_done_cb, data, + NULL); + + wait_for_request_completion(&done); - va_start(ap,fmt); - error(fmt, ap); - va_end(ap); - exit(EXIT_FAILURE); + return result; } -static inline void die_errno(int err) +#endif +#endif + +/* + * This credential struct and API is simplified from git's credential.{h,c} + */ +struct credential { - error("%s", strerror(err)); - exit(EXIT_FAILURE); -} + char *protocol; + char *host; + unsigned short port; + char *path; + char *username; + char *password; +}; -static inline char *xstrdup(const char *str) +#define CREDENTIAL_INIT \ + { NULL,NULL,0,NULL,NULL,NULL } + +typedef int (*credential_op_cb)(struct credential*); + +struct credential_operation { - char *ret = strdup(str); - if (!ret) - die_errno(errno); + char *name; + credential_op_cb op; +}; - return ret; -} +#define CREDENTIAL_OP_END \ + { NULL,NULL } /* ----------------- GNOME Keyring functions ----------------- */ /* create a special keyring option string, if path is given */ static char* keyring_object(struct credential *c) { - char* object = NULL; - if (!c->path) - return object; - - object = (char*) malloc(strlen(c->host)+strlen(c->path)+8); - if(!object) - die_errno(errno); + return NULL; - if(c->port) - sprintf(object,"%s:%hd/%s",c->host,c->port,c->path); - else - sprintf(object,"%s/%s",c->host,c->path); + if (c->port) + return g_strdup_printf("%s:%hd/%s", c->host, c->port, c->path); - return object; + return g_strdup_printf("%s/%s", c->host, c->path); } -int keyring_get(struct credential *c) +static int keyring_get(struct credential *c) { char* object = NULL; GList *entries; @@ -173,7 +190,7 @@ int keyring_get(struct credential *c) c->port, &entries); - free(object); + g_free(object); if (result == GNOME_KEYRING_RESULT_NO_MATCH) return EXIT_SUCCESS; @@ -182,18 +199,18 @@ int keyring_get(struct credential *c) return EXIT_SUCCESS; if (result != GNOME_KEYRING_RESULT_OK) { - error("%s",gnome_keyring_result_to_message(result)); + g_critical("%s", gnome_keyring_result_to_message(result)); return EXIT_FAILURE; } /* pick the first one from the list */ password_data = (GnomeKeyringNetworkPasswordData *) entries->data; - free_password(c->password); - c->password = xstrdup(password_data->password); + gnome_keyring_memory_free(c->password); + c->password = gnome_keyring_memory_strdup(password_data->password); if (!c->username) - c->username = xstrdup(password_data->user); + c->username = g_strdup(password_data->user); gnome_keyring_network_password_list_free(entries); @@ -201,10 +218,11 @@ int keyring_get(struct credential *c) } -int keyring_store(struct credential *c) +static int keyring_store(struct credential *c) { guint32 item_id; char *object = NULL; + GnomeKeyringResult result; /* * Sanity check that what we are storing is actually sensible. @@ -219,7 +237,7 @@ int keyring_store(struct credential *c) object = keyring_object(c); - gnome_keyring_set_network_password_sync( + result = gnome_keyring_set_network_password_sync( GNOME_KEYRING_DEFAULT, c->username, NULL /* domain */, @@ -231,11 +249,18 @@ int keyring_store(struct credential *c) c->password, &item_id); - free(object); + g_free(object); + + if (result != GNOME_KEYRING_RESULT_OK && + result != GNOME_KEYRING_RESULT_CANCELLED) { + g_critical("%s", gnome_keyring_result_to_message(result)); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; } -int keyring_erase(struct credential *c) +static int keyring_erase(struct credential *c) { char *object = NULL; GList *entries; @@ -265,7 +290,7 @@ int keyring_erase(struct credential *c) c->port, &entries); - free(object); + g_free(object); if (result == GNOME_KEYRING_RESULT_NO_MATCH) return EXIT_SUCCESS; @@ -275,7 +300,7 @@ int keyring_erase(struct credential *c) if (result != GNOME_KEYRING_RESULT_OK) { - error("%s",gnome_keyring_result_to_message(result)); + g_critical("%s", gnome_keyring_result_to_message(result)); return EXIT_FAILURE; } @@ -289,7 +314,7 @@ int keyring_erase(struct credential *c) if (result != GNOME_KEYRING_RESULT_OK) { - error("%s",gnome_keyring_result_to_message(result)); + g_critical("%s", gnome_keyring_result_to_message(result)); return EXIT_FAILURE; } @@ -300,7 +325,7 @@ int keyring_erase(struct credential *c) * Table with helper operation callbacks, used by generic * credential helper main function. */ -struct credential_operation const credential_helper_ops[] = +static struct credential_operation const credential_helper_ops[] = { { "get", keyring_get }, { "store", keyring_store }, @@ -310,66 +335,69 @@ struct credential_operation const credential_helper_ops[] = /* ------------------ credential functions ------------------ */ -void credential_init(struct credential *c) +static void credential_init(struct credential *c) { memset(c, 0, sizeof(*c)); } -void credential_clear(struct credential *c) +static void credential_clear(struct credential *c) { - free(c->protocol); - free(c->host); - free(c->path); - free(c->username); - free_password(c->password); + g_free(c->protocol); + g_free(c->host); + g_free(c->path); + g_free(c->username); + gnome_keyring_memory_free(c->password); credential_init(c); } -int credential_read(struct credential *c) +static int credential_read(struct credential *c) { - char buf[1024]; - ssize_t line_len = 0; - char *key = buf; + char *buf; + size_t line_len; + char *key; char *value; - while (fgets(buf, sizeof(buf), stdin)) + key = buf = gnome_keyring_memory_alloc(1024); + + while (fgets(buf, 1024, stdin)) { line_len = strlen(buf); - if(buf[line_len-1]=='\n') + if (line_len && buf[line_len-1] == '\n') buf[--line_len]='\0'; - if(!line_len) + if (!line_len) break; value = strchr(buf,'='); - if(!value) { - warning("invalid credential line: %s", key); + if (!value) { + g_warning("invalid credential line: %s", key); + gnome_keyring_memory_free(buf); return -1; } *value++ = '\0'; if (!strcmp(key, "protocol")) { - free(c->protocol); - c->protocol = xstrdup(value); + g_free(c->protocol); + c->protocol = g_strdup(value); } else if (!strcmp(key, "host")) { - free(c->host); - c->host = xstrdup(value); + g_free(c->host); + c->host = g_strdup(value); value = strrchr(c->host,':'); if (value) { *value++ = '\0'; c->port = atoi(value); } } else if (!strcmp(key, "path")) { - free(c->path); - c->path = xstrdup(value); + g_free(c->path); + c->path = g_strdup(value); } else if (!strcmp(key, "username")) { - free(c->username); - c->username = xstrdup(value); + g_free(c->username); + c->username = g_strdup(value); } else if (!strcmp(key, "password")) { - free_password(c->password); - c->password = xstrdup(value); + gnome_keyring_memory_free(c->password); + c->password = gnome_keyring_memory_strdup(value); while (*value) *value++ = '\0'; } /* @@ -378,17 +406,20 @@ int credential_read(struct credential *c) * learn new lines, and the helpers are updated to match. */ } + + gnome_keyring_memory_free(buf); + return 0; } -void credential_write_item(FILE *fp, const char *key, const char *value) +static void credential_write_item(FILE *fp, const char *key, const char *value) { if (!value) return; fprintf(fp, "%s=%s\n", key, value); } -void credential_write(const struct credential *c) +static void credential_write(const struct credential *c) { /* only write username/password, if set */ credential_write_item(stdout, "username", c->username); @@ -402,9 +433,9 @@ static void usage(const char *name) basename = (basename) ? basename + 1 : name; fprintf(stderr, "usage: %s <", basename); - while(try_op->name) { + while (try_op->name) { fprintf(stderr,"%s",(try_op++)->name); - if(try_op->name) + if (try_op->name) fprintf(stderr,"%s","|"); } fprintf(stderr,"%s",">\n"); @@ -419,19 +450,21 @@ int main(int argc, char *argv[]) if (!argv[1]) { usage(argv[0]); - goto out; + exit(EXIT_FAILURE); } + g_set_application_name("Git Credential Helper"); + /* lookup operation callback */ - while(try_op->name && strcmp(argv[1], try_op->name)) + while (try_op->name && strcmp(argv[1], try_op->name)) try_op++; /* unsupported operation given -- ignore silently */ - if(!try_op->name || !try_op->op) + if (!try_op->name || !try_op->op) goto out; ret = credential_read(&cred); - if(ret) + if (ret) goto out; /* perform credential operation */ diff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc index 6c51c43885..1571a7b269 100755 --- a/contrib/credential/netrc/git-credential-netrc +++ b/contrib/credential/netrc/git-credential-netrc @@ -369,7 +369,9 @@ sub find_netrc_entry { { my $entry_text = join ', ', map { "$_=$entry->{$_}" } keys %$entry; foreach my $check (sort keys %$query) { - if (defined $query->{$check}) { + if (!defined $entry->{$check}) { + log_debug("OK: entry has no $check token, so any value satisfies check $check"); + } elsif (defined $query->{$check}) { log_debug("compare %s [%s] to [%s] (entry: %s)", $check, $entry->{$check}, diff --git a/contrib/examples/git-log.sh b/contrib/examples/git-log.sh new file mode 100755 index 0000000000..c2ea71cf14 --- /dev/null +++ b/contrib/examples/git-log.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# + +USAGE='[--max-count=<n>] [<since>..<limit>] [--pretty=<format>] [git-rev-list options]' +SUBDIRECTORY_OK='Yes' +. git-sh-setup + +revs=$(git-rev-parse --revs-only --no-flags --default HEAD "$@") || exit +[ "$revs" ] || { + die "No HEAD ref" +} +git-rev-list --pretty $(git-rev-parse --default HEAD "$@") | +LESS=-S ${PAGER:-less} diff --git a/contrib/examples/git-merge.sh b/contrib/examples/git-merge.sh index 7b922c3948..a5e42a9f01 100755 --- a/contrib/examples/git-merge.sh +++ b/contrib/examples/git-merge.sh @@ -263,7 +263,7 @@ fi # This could be traditional "merge <msg> HEAD <commit>..." and the # way we can tell it is to see if the second token is HEAD, but some -# people might have misused the interface and used a committish that +# people might have misused the interface and used a commit-ish that # is the same as HEAD there instead. Traditional format never would # have "-m" so it is an additional safety measure to check for it. diff --git a/contrib/examples/git-repack.sh b/contrib/examples/git-repack.sh new file mode 100755 index 0000000000..757933174e --- /dev/null +++ b/contrib/examples/git-repack.sh @@ -0,0 +1,194 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# + +OPTIONS_KEEPDASHDASH= +OPTIONS_SPEC="\ +git repack [options] +-- +a pack everything in a single pack +A same as -a, and turn unreachable objects loose +d remove redundant packs, and run git-prune-packed +f pass --no-reuse-delta to git-pack-objects +F pass --no-reuse-object to git-pack-objects +n do not run git-update-server-info +q,quiet be quiet +l pass --local to git-pack-objects +unpack-unreachable= with -A, do not loosen objects older than this + Packing constraints +window= size of the window used for delta compression +window-memory= same as the above, but limit memory size instead of entries count +depth= limits the maximum delta depth +max-pack-size= maximum size of each packfile +" +SUBDIRECTORY_OK='Yes' +. git-sh-setup + +no_update_info= all_into_one= remove_redundant= unpack_unreachable= +local= no_reuse= extra= +while test $# != 0 +do + case "$1" in + -n) no_update_info=t ;; + -a) all_into_one=t ;; + -A) all_into_one=t + unpack_unreachable=--unpack-unreachable ;; + --unpack-unreachable) + unpack_unreachable="--unpack-unreachable=$2"; shift ;; + -d) remove_redundant=t ;; + -q) GIT_QUIET=t ;; + -f) no_reuse=--no-reuse-delta ;; + -F) no_reuse=--no-reuse-object ;; + -l) local=--local ;; + --max-pack-size|--window|--window-memory|--depth) + extra="$extra $1=$2"; shift ;; + --) shift; break;; + *) usage ;; + esac + shift +done + +case "`git config --bool repack.usedeltabaseoffset || echo true`" in +true) + extra="$extra --delta-base-offset" ;; +esac + +PACKDIR="$GIT_OBJECT_DIRECTORY/pack" +PACKTMP="$PACKDIR/.tmp-$$-pack" +rm -f "$PACKTMP"-* +trap 'rm -f "$PACKTMP"-*' 0 1 2 3 15 + +# There will be more repacking strategies to come... +case ",$all_into_one," in +,,) + args='--unpacked --incremental' + ;; +,t,) + args= existing= + if [ -d "$PACKDIR" ]; then + for e in `cd "$PACKDIR" && find . -type f -name '*.pack' \ + | sed -e 's/^\.\///' -e 's/\.pack$//'` + do + if [ -e "$PACKDIR/$e.keep" ]; then + : keep + else + existing="$existing $e" + fi + done + if test -n "$existing" -a -n "$unpack_unreachable" -a \ + -n "$remove_redundant" + then + # This may have arbitrary user arguments, so we + # have to protect it against whitespace splitting + # when it gets run as "pack-objects $args" later. + # Fortunately, we know it's an approxidate, so we + # can just use dots instead. + args="$args $(echo "$unpack_unreachable" | tr ' ' .)" + fi + fi + ;; +esac + +mkdir -p "$PACKDIR" || exit + +args="$args $local ${GIT_QUIET:+-q} $no_reuse$extra" +names=$(git pack-objects --keep-true-parents --honor-pack-keep --non-empty --all --reflog $args </dev/null "$PACKTMP") || + exit 1 +if [ -z "$names" ]; then + say Nothing new to pack. +fi + +# Ok we have prepared all new packfiles. + +# First see if there are packs of the same name and if so +# if we can move them out of the way (this can happen if we +# repacked immediately after packing fully. +rollback= +failed= +for name in $names +do + for sfx in pack idx + do + file=pack-$name.$sfx + test -f "$PACKDIR/$file" || continue + rm -f "$PACKDIR/old-$file" && + mv "$PACKDIR/$file" "$PACKDIR/old-$file" || { + failed=t + break + } + rollback="$rollback $file" + done + test -z "$failed" || break +done + +# If renaming failed for any of them, roll the ones we have +# already renamed back to their original names. +if test -n "$failed" +then + rollback_failure= + for file in $rollback + do + mv "$PACKDIR/old-$file" "$PACKDIR/$file" || + rollback_failure="$rollback_failure $file" + done + if test -n "$rollback_failure" + then + echo >&2 "WARNING: Some packs in use have been renamed by" + echo >&2 "WARNING: prefixing old- to their name, in order to" + echo >&2 "WARNING: replace them with the new version of the" + echo >&2 "WARNING: file. But the operation failed, and" + echo >&2 "WARNING: attempt to rename them back to their" + echo >&2 "WARNING: original names also failed." + echo >&2 "WARNING: Please rename them in $PACKDIR manually:" + for file in $rollback_failure + do + echo >&2 "WARNING: old-$file -> $file" + done + fi + exit 1 +fi + +# Now the ones with the same name are out of the way... +fullbases= +for name in $names +do + fullbases="$fullbases pack-$name" + chmod a-w "$PACKTMP-$name.pack" + chmod a-w "$PACKTMP-$name.idx" + mv -f "$PACKTMP-$name.pack" "$PACKDIR/pack-$name.pack" && + mv -f "$PACKTMP-$name.idx" "$PACKDIR/pack-$name.idx" || + exit +done + +# Remove the "old-" files +for name in $names +do + rm -f "$PACKDIR/old-pack-$name.idx" + rm -f "$PACKDIR/old-pack-$name.pack" +done + +# End of pack replacement. + +if test "$remove_redundant" = t +then + # We know $existing are all redundant. + if [ -n "$existing" ] + then + ( cd "$PACKDIR" && + for e in $existing + do + case " $fullbases " in + *" $e "*) ;; + *) rm -f "$e.pack" "$e.idx" "$e.keep" ;; + esac + done + ) + fi + git prune-packed ${GIT_QUIET:+-q} +fi + +case "$no_update_info" in +t) : ;; +*) git update-server-info ;; +esac diff --git a/contrib/examples/git-whatchanged.sh b/contrib/examples/git-whatchanged.sh new file mode 100755 index 0000000000..1fb9feb348 --- /dev/null +++ b/contrib/examples/git-whatchanged.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +USAGE='[-p] [--max-count=<n>] [<since>..<limit>] [--pretty=<format>] [-m] [git-diff-tree options] [git-rev-list options]' +SUBDIRECTORY_OK='Yes' +. git-sh-setup + +diff_tree_flags=$(git-rev-parse --sq --no-revs --flags "$@") || exit +case "$0" in +*whatchanged) + count= + test -z "$diff_tree_flags" && + diff_tree_flags=$(git-repo-config --get whatchanged.difftree) + diff_tree_default_flags='-c -M --abbrev' ;; +*show) + count=-n1 + test -z "$diff_tree_flags" && + diff_tree_flags=$(git-repo-config --get show.difftree) + diff_tree_default_flags='--cc --always' ;; +esac +test -z "$diff_tree_flags" && + diff_tree_flags="$diff_tree_default_flags" + +rev_list_args=$(git-rev-parse --sq --default HEAD --revs-only "$@") && +diff_tree_args=$(git-rev-parse --sq --no-revs --no-flags "$@") && + +eval "git-rev-list $count $rev_list_args" | +eval "git-diff-tree --stdin --pretty -r $diff_tree_flags $diff_tree_args" | +LESS="$LESS -S" ${PAGER:-less} diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview index 4c99dfb903..4e23c650fe 100755 --- a/contrib/gitview/gitview +++ b/contrib/gitview/gitview @@ -1205,7 +1205,7 @@ class GitView(object): #The first parent always continue on the same line try: - # check we alreay have the value + # check we already have the value tmp_node_pos = self.nodepos[commit.parent_sha1[0]] except KeyError: self.colours[commit.parent_sha1[0]] = colour diff --git a/contrib/hg-to-git/hg-to-git.py b/contrib/hg-to-git/hg-to-git.py index 232625a7b7..60dec86d37 100755 --- a/contrib/hg-to-git/hg-to-git.py +++ b/contrib/hg-to-git/hg-to-git.py @@ -225,7 +225,7 @@ for cset in range(int(tip) + 1): os.system('git ls-files -x .hg --deleted | git update-index --remove --stdin') # commit - os.system(getgitenv(user, date) + 'git commit --allow-empty -a -F %s' % filecomment) + os.system(getgitenv(user, date) + 'git commit --allow-empty --allow-empty-message -a -F %s' % filecomment) os.unlink(filecomment) # tag diff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README new file mode 100644 index 0000000000..9904396710 --- /dev/null +++ b/contrib/hooks/multimail/README @@ -0,0 +1,486 @@ + git-multimail + ============= + +git-multimail is a tool for sending notification emails on pushes to a +Git repository. It includes a Python module called git_multimail.py, +which can either be used as a hook script directly or can be imported +as a Python module into another script. + +git-multimail is derived from the Git project's old +contrib/hooks/post-receive-email, and is mostly compatible with that +script. See README.migrate-from-post-receive-email for details about +the differences and for how to migrate from post-receive-email to +git-multimail. + +git-multimail, like the rest of the Git project, is licensed under +GPLv2 (see the COPYING file for details). + +Please note: although, as a convenience, git-multimail may be +distributed along with the main Git project, development of +git-multimail takes place in its own, separate project. See section +"Getting involved" below for more information. + + +By default, for each push received by the repository, git-multimail: + +1. Outputs one email summarizing each reference that was changed. + These "reference change" (called "refchange" below) emails describe + the nature of the change (e.g., was the reference created, deleted, + fast-forwarded, etc.) and include a one-line summary of each commit + that was added to the reference. + +2. Outputs one email for each new commit that was introduced by the + reference change. These "commit" emails include a list of the + files changed by the commit, followed by the diffs of files + modified by the commit. The commit emails are threaded to the + corresponding reference change email via "In-Reply-To". This style + (similar to the "git format-patch" style used on the Git mailing + list) makes it easy to scan through the emails, jump to patches + that need further attention, and write comments about specific + commits. Commits are handled in reverse topological order (i.e., + parents shown before children). For example, + + [git] branch master updated + + [git] 01/08: doc: fix xref link from api docs to manual pages + + [git] 02/08: api-credentials.txt: show the big picture first + + [git] 03/08: api-credentials.txt: mention credential.helper explicitly + + [git] 04/08: api-credentials.txt: add "see also" section + + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&' + + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix' + + [git] 07/08: Merge branch 'mm/api-credentials-doc' + + [git] 08/08: Git 1.7.11-rc2 + + Each commit appears in exactly one commit email, the first time + that it is pushed to the repository. If a commit is later merged + into another branch, then a one-line summary of the commit is + included in the reference change email (as usual), but no + additional commit email is generated. + + By default, reference change emails have their "Reply-To" field set + to the person who pushed the change, and commit emails have their + "Reply-To" field set to the author of the commit. + +3. Output one "announce" mail for each new annotated tag, including + information about the tag and optionally a shortlog describing the + changes since the previous tag. Such emails might be useful if you + use annotated tags to mark releases of your project. + + +Requirements +------------ + +* Python 2.x, version 2.4 or later. No non-standard Python modules + are required. git-multimail does *not* currently work with Python + 3.x. + + The example scripts invoke Python using the following shebang line + (following PEP 394 [1]): + + #! /usr/bin/env python2 + + If your system's Python2 interpreter is not in your PATH or is not + called "python2", you can change the lines accordingly. Or you can + invoke the Python interpreter explicitly, for example via a tiny + shell script like + + #! /bin/sh + /usr/local/bin/python /path/to/git_multimail.py "$@" + +* The "git" command must be in your PATH. git-multimail is known to + work with Git versions back to 1.7.1. (Earlier versions have not + been tested; if you do so, please report your results.) + +* To send emails using the default configuration, a standard sendmail + program must be located at '/usr/sbin/sendmail' and configured + correctly to send emails. If this is not the case, see the + multimailhook.mailer configuration variable below for how to + configure git-multimail to send emails via an SMTP server. + + +Invocation +---------- + +git_multimail.py is designed to be used as a "post-receive" hook in a +Git repository (see githooks(5)). Link or copy it to +$GIT_DIR/hooks/post-receive within the repository for which email +notifications are desired. Usually it should be installed on the +central repository for a project, to which all commits are eventually +pushed. + +For use on pre-v1.5.1 Git servers, git_multimail.py can also work as +an "update" hook, taking its arguments on the command line. To use +this script in this manner, link or copy it to $GIT_DIR/hooks/update. +Please note that the script is not completely reliable in this mode +[2]. + +Alternatively, git_multimail.py can be imported as a Python module +into your own Python post-receive script. This method is a bit more +work, but allows the behavior of the hook to be customized using +arbitrary Python code. For example, you can use a custom environment +(perhaps inheriting from GenericEnvironment or GitoliteEnvironment) to + +* change how the user who did the push is determined + +* read users' email addresses from an LDAP server or from a database + +* decide which users should be notified about which commits based on + the contents of the commits (e.g., for users who want to be notified + only about changes affecting particular files or subdirectories) + +Or you can change how emails are sent by writing your own Mailer +class. The "post-receive" script in this directory demonstrates how +to use git_multimail.py as a Python module. (If you make interesting +changes of this type, please consider sharing them with the +community.) + + +Configuration +------------- + +By default, git-multimail mostly takes its configuration from the +following "git config" settings: + +multimailhook.environment + + This describes the general environment of the repository. + Currently supported values: + + "generic" -- the username of the pusher is read from $USER and the + repository name is derived from the repository's path. + + "gitolite" -- the username of the pusher is read from $GL_USER and + the repository name from $GL_REPO. + + If neither of these environments is suitable for your setup, then + you can implement a Python class that inherits from Environment + and instantiate it via a script that looks like the example + post-receive script. + + The environment value can be specified on the command line using + the --environment option. If it is not specified on the command + line or by multimailhook.environment, then it defaults to + "gitolite" if the environment contains variables $GL_USER and + $GL_REPO; otherwise "generic". + +multimailhook.repoName + + A short name of this Git repository, to be used in various places + in the notification email text. The default is to use $GL_REPO + for gitolite repositories, or otherwise to derive this value from + the repository path name. + +multimailhook.mailinglist + + The list of email addresses to which notification emails should be + sent, as RFC 2822 email addresses separated by commas. This + configuration option can be multivalued. Leave it unset or set it + to the empty string to not send emails by default. The next few + settings can be used to configure specific address lists for + specific types of notification email. + +multimailhook.refchangeList + + The list of email addresses to which summary emails about + reference changes should be sent, as RFC 2822 email addresses + separated by commas. This configuration option can be + multivalued. The default is the value in + multimailhook.mailinglist. Set this value to the empty string to + prevent reference change emails from being sent. + +multimailhook.announceList + + The list of email addresses to which emails about new annotated + tags should be sent, as RFC 2822 email addresses separated by + commas. This configuration option can be multivalued. The + default is the value in multimailhook.refchangelist or + multimailhook.mailinglist. Set this value to the empty string to + prevent annotated tag announcement emails from being sent. + +multimailhook.commitList + + The list of email addresses to which emails about individual new + commits should be sent, as RFC 2822 email addresses separated by + commas. This configuration option can be multivalued. The + default is the value in multimailhook.mailinglist. Set this value + to the empty string to prevent notification emails about + individual commits from being sent. + +multimailhook.announceShortlog + + If this option is set to true, then emails about changes to + annotated tags include a shortlog of changes since the previous + tag. This can be useful if the annotated tags represent releases; + then the shortlog will be a kind of rough summary of what has + happened since the last release. But if your tagging policy is + not so straightforward, then the shortlog might be confusing + rather than useful. Default is false. + +multimailhook.refchangeShowLog + + If this option is set to true, then summary emails about reference + changes will include a detailed log of the added commits in + addition to the one line summary. The log is generated by running + "git log" with the options specified in multimailhook.logOpts. + Default is false. + +multimailhook.mailer + + This option changes the way emails are sent. Accepted values are: + + - sendmail (the default): use the command /usr/sbin/sendmail or + /usr/lib/sendmail (or sendmailCommand, if configured). This + mode can be further customized via the following options: + + multimailhook.sendmailCommand + + The command used by mailer "sendmail" to send emails. Shell + quoting is allowed in the value of this setting, but remember that + Git requires double-quotes to be escaped; e.g., + + git config multimailhook.sendmailcommand '/usr/sbin/sendmail -t -F \"Git Repo\"' + + Default is '/usr/sbin/sendmail -t' or '/usr/lib/sendmail + -t' (depending on which file is present and executable). + + multimailhook.envelopeSender + + If set then pass this value to sendmail via the -f option to set + the envelope sender address. + + - smtp: use Python's smtplib. This is useful when the sendmail + command is not available on the system. This mode can be + further customized via the following options: + + multimailhook.smtpServer + + The name of the SMTP server to connect to. The value can + also include a colon and a port number; e.g., + "mail.example.com:25". Default is 'localhost' using port + 25. + + multimailhook.envelopeSender + + The sender address to be passed to the SMTP server. If + unset, then the value of multimailhook.from is used. + +multimailhook.from + + If set then use this value in the From: field of generated emails. + If unset, then use the repository's user configuration (user.name + and user.email). If user.email is also unset, then use + multimailhook.envelopeSender. + +multimailhook.administrator + + The name and/or email address of the administrator of the Git + repository; used in FOOTER_TEMPLATE. Default is + multimailhook.envelopesender if it is set; otherwise a generic + string is used. + +multimailhook.emailPrefix + + All emails have this string prepended to their subjects, to aid + email filtering (though filtering based on the X-Git-* email + headers is probably more robust). Default is the short name of + the repository in square brackets; e.g., "[myrepo]". + +multimailhook.emailMaxLines + + The maximum number of lines that should be included in the body of + a generated email. 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. + +multimailhook.emailMaxLineLength + + The maximum length of a line in the email body. Lines longer than + this limit are truncated to this length with a trailing " [...]" + added to indicate the missing text. The default is 500, because + (a) diffs with longer lines are probably from binary files, for + which a diff is useless, and (b) even if a text file has such long + lines, the diffs are probably unreadable anyway. To disable line + truncation, set this option to 0. + +multimailhook.maxCommitEmails + + The maximum number of commit emails to send for a given change. + When the number of patches is larger that this value, only the + summary refchange email is sent. This can avoid accidental + mailbombing, for example on an initial push. To disable commit + emails limit, set this option to 0. The default is 500. + +multimailhook.emailStrictUTF8 + + If this boolean option is set to "true", then the main part of the + email body is forced to be valid UTF-8. Any characters that are + not valid UTF-8 are converted to the Unicode replacement + character, U+FFFD. The default is "true". + +multimailhook.diffOpts + + Options passed to "git diff-tree" when generating the summary + information for ReferenceChange emails. 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. Shell quoting is allowed; see multimailhook.logOpts for + details. + +multimailhook.logOpts + + Options passed to "git log" to generate additional info for + reference change emails (used only if refchangeShowLog is set). + For example, adding --graph will show the graph of revisions, -p + will show the complete diff, etc. The default is empty. + + Shell quoting is allowed; for example, a log format that contains + spaces can be specified using something like: + + git config multimailhook.logopts '--pretty=format:"%h %aN <%aE>%n%s%n%n%b%n"' + + If you want to set this by editing your configuration file + directly, remember that Git requires double-quotes to be escaped + (see git-config(1) for more information): + + [multimailhook] + logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\" + +multimailhook.emailDomain + + Domain name appended to the username of the person doing the push + to convert it into an email address (via "%s@%s" % (username, + emaildomain)). More complicated schemes can be implemented by + overriding Environment and overriding its get_pusher_email() + method. + +multimailhook.replyTo +multimailhook.replyToCommit +multimailhook.replyToRefchange + + Addresses to use in the Reply-To: field for commit emails + (replyToCommit) and refchange emails (replyToRefchange). + multimailhook.replyTo is used as default when replyToCommit or + replyToRefchange is not set. The value for these variables can be + either: + + - An email address, which will be used directly. + + - The value "pusher", in which case the pusher's address (if + available) will be used. This is the default for refchange + emails. + + - The value "author" (meaningful only for replyToCommit), in which + case the commit author's address will be used. This is the + default for commit emails. + + - The value "none", in which case the Reply-To: field will be + omitted. + + +Email filtering aids +-------------------- + +All emails include extra headers to enable fine tuned filtering and +give information for debugging. All emails include the headers +"X-Git-Repo", "X-Git-Refname", and "X-Git-Reftype". ReferenceChange +emails also include headers "X-Git-Oldrev" and "X-Git-Newrev"; +Revision emails also include header "X-Git-Rev". + + +Customizing email contents +-------------------------- + +git-multimail mostly generates emails by expanding templates. The +templates can be customized. To avoid the need to edit +git_multimail.py directly, the preferred way to change the templates +is to write a separate Python script that imports git_multimail.py as +a module, then replaces the templates in place. See the provided +post-receive script for an example of how this is done. + + +Customizing git-multimail for your environment +---------------------------------------------- + +git-multimail is mostly customized via an "environment" that describes +the local environment in which Git is running. Two types of +environment are built in: + +* GenericEnvironment: a stand-alone Git repository. + +* GitoliteEnvironment: a Git repository that is managed by gitolite + [3]. For such repositories, the identity of the pusher is read from + environment variable $GL_USER, and the name of the repository is + read from $GL_REPO (if it is not overridden by + multimailhook.reponame). + +By default, git-multimail assumes GitoliteEnvironment if $GL_USER and +$GL_REPO are set, and otherwise assumes GenericEnvironment. +Alternatively, you can choose one of these two environments explicitly +by setting a "multimailhook.environment" config setting (which can +have the value "generic" or "gitolite") or by passing an --environment +option to the script. + +If you need to customize the script in ways that are not supported by +the existing environments, you can define your own environment class +class using arbitrary Python code. To do so, you need to import +git_multimail.py as a Python module, as demonstrated by the example +post-receive script. Then implement your environment class; it should +usually inherit from one of the existing Environment classes and +possibly one or more of the EnvironmentMixin classes. Then set the +"environment" variable to an instance of your own environment class +and pass it to run_as_post_receive_hook(). + +The standard environment classes, GenericEnvironment and +GitoliteEnvironment, are in fact themselves put together out of a +number of mixin classes, each of which handles one aspect of the +customization. For the finest control over your configuration, you +can specify exactly which mixin classes your own environment class +should inherit from, and override individual methods (or even add your +own mixin classes) to implement entirely new behaviors. If you +implement any mixins that might be useful to other people, please +consider sharing them with the community! + + +Getting involved +---------------- + +git-multimail is an open-source project, built by volunteers. We +would welcome your help! + +The current maintainer is Michael Haggerty <mhagger@alum.mit.edu>. + +General discussion of git-multimail takes place on the main Git +mailing list, + + git@vger.kernel.org + +Please CC emails regarding git-multimail to me so that I don't +overlook them. + +The git-multimail project itself is currently hosted on GitHub: + + https://github.com/mhagger/git-multimail + +We use the GitHub issue tracker to keep track of bugs and feature +requests, and GitHub pull requests to exchange patches (though, if you +prefer, you can send patches via the Git mailing list with cc to me). + +Please note that although a copy of git-multimail will probably be +distributed in the "contrib" section of the main Git project, +development takes place in the separate git-multimail repository on +GitHub! (Whenever enough changes to git-multimail have accumulated, a +new code-drop of git-multimail will be submitted for inclusion in the +Git project.) + + +Footnotes +--------- + +[1] http://www.python.org/dev/peps/pep-0394/ + +[2] Because of the way information is passed to update hooks, the + script's method of determining whether a commit has already been + seen does not work when it is used as an "update" script. In + particular, no notification email will be generated for a new + commit that is added to multiple references in the same push. + +[3] https://github.com/sitaramc/gitolite diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git new file mode 100644 index 0000000000..9c2e66a69a --- /dev/null +++ b/contrib/hooks/multimail/README.Git @@ -0,0 +1,15 @@ +This copy of git-multimail is distributed as part of the "contrib" +section of the Git project as a convenience to Git users. +git-multimail is developed as an independent project at the following +website: + + https://github.com/mhagger/git-multimail + +The version in this directory was obtained from the upstream project +on 2013-07-14 and consists of the "git-multimail" subdirectory from +revision + + 1a5cb09c698a74d15a715a86b09ead5f56bf4b06 + +Please see the README file in this directory for information about how +to report bugs or contribute to git-multimail. diff --git a/contrib/hooks/multimail/README.migrate-from-post-receive-email b/contrib/hooks/multimail/README.migrate-from-post-receive-email new file mode 100644 index 0000000000..1e6a976699 --- /dev/null +++ b/contrib/hooks/multimail/README.migrate-from-post-receive-email @@ -0,0 +1,145 @@ +git-multimail is close to, but not exactly, a plug-in replacement for +the old Git project script contrib/hooks/post-receive-email. This +document describes the differences and explains how to configure +git-multimail to get behavior closest to that of post-receive-email. + +If you are in a hurry +===================== + +A script called migrate-mailhook-config is included with +git-multimail. If you run this script within a Git repository that is +configured to use post-receive-email, it will convert the +configuration settings into the approximate equivalent settings for +git-multimail. For more information, run + + migrate-mailhook-config --help + + +Configuration differences +========================= + +* The names of the config options for git-multimail are in namespace + "multimailhook.*" instead of "hooks.*". (Editorial comment: + post-receive-email should never have used such a generic top-level + namespace.) + +* In emails about new annotated tags, post-receive-email includes a + shortlog of all changes since the previous annotated tag. To get + this behavior with git-multimail, you need to set + multimailhook.announceshortlog to true: + + git config multimailhook.announceshortlog true + +* multimailhook.commitlist -- This is a new configuration variable. + Recipients listed here will receive a separate email for each new + commit. However, if this variable is *not* set, it defaults to the + value of multimailhook.mailinglist. Therefore, if you *don't* want + the members of multimailhook.mailinglist to receive one email per + commit, then set this value to the empty string: + + git config multimailhook.commitlist '' + +* multimailhook.emailprefix -- If this value is not set, then the + subjects of generated emails are prefixed with the short name of the + repository enclosed in square brackets; e.g., "[myrepo]". + post-receive-email defaults to prefix "[SCM]" if this option is not + set. So if you were using the old default and want to retain it + (for example, to avoid having to change your email filters), set + this variable explicitly to the old value: + + git config multimailhook.emailprefix "[SCM]" + +* The "multimailhook.showrev" configuration option is not supported. + Its main use is obsoleted by the one-email-per-commit feature of + git-multimail. + + +Other differences +================= + +This section describes other differences in the behavior of +git-multimail vs. post-receive-email. For full details, please refer +to the main README file: + +* One email per commit. For each reference change, the script first + outputs one email summarizing the reference change (including + one-line summaries of the new commits), then it outputs a separate + email for each new commit that was introduced, including patches. + These one-email-per-commit emails go to the addresses listed in + multimailhook.commitlist. post-receive-email sends only one email + for each *reference* that is changed, no matter how many commits + were added to the reference. + +* Better algorithm for detecting new commits. post-receive-email + processes one reference change at a time, which causes it to fail to + describe new commits that were included in multiple branches. For + example, if a single push adds the "*" commits in the diagram below, + then post-receive-email would never include the details of the two + commits that are common to "master" and "branch" in its + notifications. + + o---o---o---*---*---* <-- master + \ + *---* <-- branch + + git-multimail analyzes all reference modifications to determine + which commits were not present before the change, therefore avoiding + that error. + +* In reference change emails, git-multimail tells which commits have + been added to the reference vs. are entirely new to the repository, + and which commits that have been omitted from the reference + vs. entirely discarded from the repository. + +* The environment in which Git is running can be configured via an + "Environment" abstraction. + +* Built-in support for Gitolite-managed repositories. + +* Instead of using full SHA1 object names in emails, git-multimail + mostly uses abbreviated SHA1s, plus one-line log message summaries + where appropriate. + +* In the schematic diagrams that explain non-fast-forward commits, + git-multimail shows the names of the branches involved. + +* The emails generated by git-multimail include the name of the Git + repository that was modified; this is convenient for recipients who + are monitoring multiple repositories. + +* git-multimail allows the email "From" addresses to be configured. + +* The recipients lists (multimailhook.mailinglist, + multimailhook.refchangelist, multimailhook.announcelist, and + multimailhook.commitlist) can be comma-separated values and/or + multivalued settings in the config file; e.g., + + [multimailhook] + mailinglist = mr.brown@example.com, mr.black@example.com + announcelist = Him <him@example.com> + announcelist = Jim <jim@example.com> + announcelist = pop@example.com + + This might make it easier to maintain short recipients lists without + requiring full-fledged mailing list software. + +* By default, git-multimail sets email "Reply-To" headers to reply to + the pusher (for reference updates) and to the author (for commit + notifications). By default, the pusher's email address is + constructed by appending "multimailhook.emaildomain" to the pusher's + username. + +* The generated emails contain a configurable footer. By default, it + lists the name of the administrator who should be contacted to + unsubscribe from notification emails. + +* New option multimailhook.emailmaxlinelength to limit the length of + lines in the main part of the email body. The default limit is 500 + characters. + +* New option multimailhook.emailstrictutf8 to ensure that the main + part of the email body is valid UTF-8. Invalid characters are + turned into the Unicode replacement character, U+FFFD. By default + this option is turned on. + +* Written in Python. Easier to add new features. diff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py new file mode 100755 index 0000000000..81c6a51706 --- /dev/null +++ b/contrib/hooks/multimail/git_multimail.py @@ -0,0 +1,2393 @@ +#! /usr/bin/env python2 + +# Copyright (c) 2012,2013 Michael Haggerty +# Derived from contrib/hooks/post-receive-email, which is +# Copyright (c) 2007 Andy Parkins +# and also includes contributions by other authors. +# +# This file is part of git-multimail. +# +# git-multimail is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License version +# 2 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# <http://www.gnu.org/licenses/>. + +"""Generate notification emails for pushes to a git repository. + +This hook sends emails describing changes introduced by pushes to a +git repository. For each reference that was changed, it emits one +ReferenceChange email summarizing how the reference was changed, +followed by one Revision email for each new commit that was introduced +by the reference change. + +Each commit is announced in exactly one Revision email. If the same +commit is merged into another branch in the same or a later push, then +the ReferenceChange email will list the commit's SHA1 and its one-line +summary, but no new Revision email will be generated. + +This script is designed to be used as a "post-receive" hook in a git +repository (see githooks(5)). It can also be used as an "update" +script, but this usage is not completely reliable and is deprecated. + +To help with debugging, this script accepts a --stdout option, which +causes the emails to be written to standard output rather than sent +using sendmail. + +See the accompanying README file for the complete documentation. + +""" + +import sys +import os +import re +import bisect +import subprocess +import shlex +import optparse +import smtplib + +try: + from email.utils import make_msgid + from email.utils import getaddresses + from email.utils import formataddr + from email.header import Header +except ImportError: + # Prior to Python 2.5, the email module used different names: + from email.Utils import make_msgid + from email.Utils import getaddresses + from email.Utils import formataddr + from email.Header import Header + + +DEBUG = False + +ZEROS = '0' * 40 +LOGBEGIN = '- Log -----------------------------------------------------------------\n' +LOGEND = '-----------------------------------------------------------------------\n' + + +# It is assumed in many places that the encoding is uniformly UTF-8, +# so changing these constants is unsupported. But define them here +# anyway, to make it easier to find (at least most of) the places +# where the encoding is important. +(ENCODING, CHARSET) = ('UTF-8', 'utf-8') + + +REF_CREATED_SUBJECT_TEMPLATE = ( + '%(emailprefix)s%(refname_type)s %(short_refname)s created' + ' (now %(newrev_short)s)' + ) +REF_UPDATED_SUBJECT_TEMPLATE = ( + '%(emailprefix)s%(refname_type)s %(short_refname)s updated' + ' (%(oldrev_short)s -> %(newrev_short)s)' + ) +REF_DELETED_SUBJECT_TEMPLATE = ( + '%(emailprefix)s%(refname_type)s %(short_refname)s deleted' + ' (was %(oldrev_short)s)' + ) + +REFCHANGE_HEADER_TEMPLATE = """\ +To: %(recipients)s +Subject: %(subject)s +MIME-Version: 1.0 +Content-Type: text/plain; charset=%(charset)s +Content-Transfer-Encoding: 8bit +Message-ID: %(msgid)s +From: %(fromaddr)s +Reply-To: %(reply_to)s +X-Git-Repo: %(repo_shortname)s +X-Git-Refname: %(refname)s +X-Git-Reftype: %(refname_type)s +X-Git-Oldrev: %(oldrev)s +X-Git-Newrev: %(newrev)s +Auto-Submitted: auto-generated +""" + +REFCHANGE_INTRO_TEMPLATE = """\ +This is an automated email from the git hooks/post-receive script. + +%(pusher)s pushed a change to %(refname_type)s %(short_refname)s +in repository %(repo_shortname)s. + +""" + + +FOOTER_TEMPLATE = """\ + +-- \n\ +To stop receiving notification emails like this one, please contact +%(administrator)s. +""" + + +REWIND_ONLY_TEMPLATE = """\ +This update removed existing revisions from the reference, leaving the +reference pointing at a previous point in the repository history. + + * -- * -- N %(refname)s (%(newrev_short)s) + \\ + O -- O -- O (%(oldrev_short)s) + +Any revisions marked "omits" are not gone; other references still +refer to them. Any revisions marked "discards" are gone forever. +""" + + +NON_FF_TEMPLATE = """\ +This update added new revisions after undoing existing revisions. +That is to say, some revisions that were in the old version of the +%(refname_type)s are not in the new version. This situation occurs +when a user --force pushes a change and generates a repository +containing something like this: + + * -- * -- B -- O -- O -- O (%(oldrev_short)s) + \\ + N -- N -- N %(refname)s (%(newrev_short)s) + +You should already have received notification emails for all of the O +revisions, and so the following emails describe only the N revisions +from the common base, B. + +Any revisions marked "omits" are not gone; other references still +refer to them. Any revisions marked "discards" are gone forever. +""" + + +NO_NEW_REVISIONS_TEMPLATE = """\ +No new revisions were added by this update. +""" + + +DISCARDED_REVISIONS_TEMPLATE = """\ +This change permanently discards the following revisions: +""" + + +NO_DISCARDED_REVISIONS_TEMPLATE = """\ +The revisions that were on this %(refname_type)s are still contained in +other references; therefore, this change does not discard any commits +from the repository. +""" + + +NEW_REVISIONS_TEMPLATE = """\ +The %(tot)s revisions listed above as "new" are entirely new to this +repository and will be described in separate emails. The revisions +listed as "adds" were already present in the repository and have only +been added to this reference. + +""" + + +TAG_CREATED_TEMPLATE = """\ + at %(newrev_short)-9s (%(newrev_type)s) +""" + + +TAG_UPDATED_TEMPLATE = """\ +*** WARNING: tag %(short_refname)s was modified! *** + + from %(oldrev_short)-9s (%(oldrev_type)s) + to %(newrev_short)-9s (%(newrev_type)s) +""" + + +TAG_DELETED_TEMPLATE = """\ +*** WARNING: tag %(short_refname)s was deleted! *** + +""" + + +# The template used in summary tables. It looks best if this uses the +# same alignment as TAG_CREATED_TEMPLATE and TAG_UPDATED_TEMPLATE. +BRIEF_SUMMARY_TEMPLATE = """\ +%(action)10s %(rev_short)-9s %(text)s +""" + + +NON_COMMIT_UPDATE_TEMPLATE = """\ +This is an unusual reference change because the reference did not +refer to a commit either before or after the change. We do not know +how to provide full information about this reference change. +""" + + +REVISION_HEADER_TEMPLATE = """\ +To: %(recipients)s +Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s +MIME-Version: 1.0 +Content-Type: text/plain; charset=%(charset)s +Content-Transfer-Encoding: 8bit +From: %(fromaddr)s +Reply-To: %(reply_to)s +In-Reply-To: %(reply_to_msgid)s +References: %(reply_to_msgid)s +X-Git-Repo: %(repo_shortname)s +X-Git-Refname: %(refname)s +X-Git-Reftype: %(refname_type)s +X-Git-Rev: %(rev)s +Auto-Submitted: auto-generated +""" + +REVISION_INTRO_TEMPLATE = """\ +This is an automated email from the git hooks/post-receive script. + +%(pusher)s pushed a commit to %(refname_type)s %(short_refname)s +in repository %(repo_shortname)s. + +""" + + +REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE + + +class CommandError(Exception): + def __init__(self, cmd, retcode): + self.cmd = cmd + self.retcode = retcode + Exception.__init__( + self, + 'Command "%s" failed with retcode %s' % (' '.join(cmd), retcode,) + ) + + +class ConfigurationException(Exception): + pass + + +def read_git_output(args, input=None, keepends=False, **kw): + """Read the output of a Git command.""" + + return read_output( + ['git', '-c', 'i18n.logoutputencoding=%s' % (ENCODING,)] + args, + input=input, keepends=keepends, **kw + ) + + +def read_output(cmd, input=None, keepends=False, **kw): + if input: + stdin = subprocess.PIPE + else: + stdin = None + p = subprocess.Popen( + cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw + ) + (out, err) = p.communicate(input) + retcode = p.wait() + if retcode: + raise CommandError(cmd, retcode) + if not keepends: + out = out.rstrip('\n\r') + return out + + +def read_git_lines(args, keepends=False, **kw): + """Return the lines output by Git command. + + Return as single lines, with newlines stripped off.""" + + return read_git_output(args, keepends=True, **kw).splitlines(keepends) + + +class Config(object): + def __init__(self, section, git_config=None): + """Represent a section of the git configuration. + + If git_config is specified, it is passed to "git config" in + the GIT_CONFIG environment variable, meaning that "git config" + will read the specified path rather than the Git default + config paths.""" + + self.section = section + if git_config: + self.env = os.environ.copy() + self.env['GIT_CONFIG'] = git_config + else: + self.env = None + + @staticmethod + def _split(s): + """Split NUL-terminated values.""" + + words = s.split('\0') + assert words[-1] == '' + return words[:-1] + + def get(self, name, default=None): + try: + values = self._split(read_git_output( + ['config', '--get', '--null', '%s.%s' % (self.section, name)], + env=self.env, keepends=True, + )) + assert len(values) == 1 + return values[0] + except CommandError: + return default + + def get_bool(self, name, default=None): + try: + value = read_git_output( + ['config', '--get', '--bool', '%s.%s' % (self.section, name)], + env=self.env, + ) + except CommandError: + return default + return value == 'true' + + def get_all(self, name, default=None): + """Read a (possibly multivalued) setting from the configuration. + + Return the result as a list of values, or default if the name + is unset.""" + + try: + return self._split(read_git_output( + ['config', '--get-all', '--null', '%s.%s' % (self.section, name)], + env=self.env, keepends=True, + )) + except CommandError, e: + if e.retcode == 1: + # "the section or key is invalid"; i.e., there is no + # value for the specified key. + return default + else: + raise + + def get_recipients(self, name, default=None): + """Read a recipients list from the configuration. + + Return the result as a comma-separated list of email + addresses, or default if the option is unset. If the setting + has multiple values, concatenate them with comma separators.""" + + lines = self.get_all(name, default=None) + if lines is None: + return default + return ', '.join(line.strip() for line in lines) + + def set(self, name, value): + read_git_output( + ['config', '%s.%s' % (self.section, name), value], + env=self.env, + ) + + def add(self, name, value): + read_git_output( + ['config', '--add', '%s.%s' % (self.section, name), value], + env=self.env, + ) + + def has_key(self, name): + return self.get_all(name, default=None) is not None + + def unset_all(self, name): + try: + read_git_output( + ['config', '--unset-all', '%s.%s' % (self.section, name)], + env=self.env, + ) + except CommandError, e: + if e.retcode == 5: + # The name doesn't exist, which is what we wanted anyway... + pass + else: + raise + + def set_recipients(self, name, value): + self.unset_all(name) + for pair in getaddresses([value]): + self.add(name, formataddr(pair)) + + +def generate_summaries(*log_args): + """Generate a brief summary for each revision requested. + + log_args are strings that will be passed directly to "git log" as + revision selectors. Iterate over (sha1_short, subject) for each + commit specified by log_args (subject is the first line of the + commit message as a string without EOLs).""" + + cmd = [ + 'log', '--abbrev', '--format=%h %s', + ] + list(log_args) + ['--'] + for line in read_git_lines(cmd): + yield tuple(line.split(' ', 1)) + + +def limit_lines(lines, max_lines): + for (index, line) in enumerate(lines): + if index < max_lines: + yield line + + if index >= max_lines: + yield '... %d lines suppressed ...\n' % (index + 1 - max_lines,) + + +def limit_linelength(lines, max_linelength): + for line in lines: + # Don't forget that lines always include a trailing newline. + if len(line) > max_linelength + 1: + line = line[:max_linelength - 7] + ' [...]\n' + yield line + + +class CommitSet(object): + """A (constant) set of object names. + + The set should be initialized with full SHA1 object names. The + __contains__() method returns True iff its argument is an + abbreviation of any the names in the set.""" + + def __init__(self, names): + self._names = sorted(names) + + def __len__(self): + return len(self._names) + + def __contains__(self, sha1_abbrev): + """Return True iff this set contains sha1_abbrev (which might be abbreviated).""" + + i = bisect.bisect_left(self._names, sha1_abbrev) + return i < len(self) and self._names[i].startswith(sha1_abbrev) + + +class GitObject(object): + def __init__(self, sha1, type=None): + if sha1 == ZEROS: + self.sha1 = self.type = self.commit_sha1 = None + else: + self.sha1 = sha1 + self.type = type or read_git_output(['cat-file', '-t', self.sha1]) + + if self.type == 'commit': + self.commit_sha1 = self.sha1 + elif self.type == 'tag': + try: + self.commit_sha1 = read_git_output( + ['rev-parse', '--verify', '%s^0' % (self.sha1,)] + ) + except CommandError: + # Cannot deref tag to determine commit_sha1 + self.commit_sha1 = None + else: + self.commit_sha1 = None + + self.short = read_git_output(['rev-parse', '--short', sha1]) + + def get_summary(self): + """Return (sha1_short, subject) for this commit.""" + + if not self.sha1: + raise ValueError('Empty commit has no summary') + + return iter(generate_summaries('--no-walk', self.sha1)).next() + + def __eq__(self, other): + return isinstance(other, GitObject) and self.sha1 == other.sha1 + + def __hash__(self): + return hash(self.sha1) + + def __nonzero__(self): + return bool(self.sha1) + + def __str__(self): + return self.sha1 or ZEROS + + +class Change(object): + """A Change that has been made to the Git repository. + + Abstract class from which both Revisions and ReferenceChanges are + derived. A Change knows how to generate a notification email + describing itself.""" + + def __init__(self, environment): + self.environment = environment + self._values = None + + def _compute_values(self): + """Return a dictionary {keyword : expansion} for this Change. + + Derived classes overload this method to add more entries to + the return value. This method is used internally by + get_values(). The return value should always be a new + dictionary.""" + + return self.environment.get_values() + + def get_values(self, **extra_values): + """Return a dictionary {keyword : expansion} for this Change. + + Return a dictionary mapping keywords to the values that they + should be expanded to for this Change (used when interpolating + template strings). If any keyword arguments are supplied, add + those to the return value as well. The return value is always + a new dictionary.""" + + if self._values is None: + self._values = self._compute_values() + + values = self._values.copy() + if extra_values: + values.update(extra_values) + return values + + def expand(self, template, **extra_values): + """Expand template. + + Expand the template (which should be a string) using string + interpolation of the values for this Change. If any keyword + arguments are provided, also include those in the keywords + available for interpolation.""" + + return template % self.get_values(**extra_values) + + def expand_lines(self, template, **extra_values): + """Break template into lines and expand each line.""" + + values = self.get_values(**extra_values) + for line in template.splitlines(True): + yield line % values + + def expand_header_lines(self, template, **extra_values): + """Break template into lines and expand each line as an RFC 2822 header. + + Encode values and split up lines that are too long. Silently + skip lines that contain references to unknown variables.""" + + values = self.get_values(**extra_values) + for line in template.splitlines(): + (name, value) = line.split(':', 1) + + try: + value = value % values + except KeyError, e: + if DEBUG: + sys.stderr.write( + 'Warning: unknown variable %r in the following line; line skipped:\n' + ' %s\n' + % (e.args[0], line,) + ) + else: + try: + h = Header(value, header_name=name) + except UnicodeDecodeError: + h = Header(value, header_name=name, charset=CHARSET, errors='replace') + for splitline in ('%s: %s\n' % (name, h.encode(),)).splitlines(True): + yield splitline + + def generate_email_header(self): + """Generate the RFC 2822 email headers for this Change, a line at a time. + + The output should not include the trailing blank line.""" + + raise NotImplementedError() + + def generate_email_intro(self): + """Generate the email intro for this Change, a line at a time. + + The output will be used as the standard boilerplate at the top + of the email body.""" + + raise NotImplementedError() + + def generate_email_body(self): + """Generate the main part of the email body, a line at a time. + + The text in the body might be truncated after a specified + number of lines (see multimailhook.emailmaxlines).""" + + raise NotImplementedError() + + def generate_email_footer(self): + """Generate the footer of the email, a line at a time. + + The footer is always included, irrespective of + multimailhook.emailmaxlines.""" + + raise NotImplementedError() + + def generate_email(self, push, body_filter=None): + """Generate an email describing this change. + + Iterate over the lines (including the header lines) of an + email describing this change. If body_filter is not None, + then use it to filter the lines that are intended for the + email body.""" + + for line in self.generate_email_header(): + yield line + yield '\n' + for line in self.generate_email_intro(): + yield line + + body = self.generate_email_body(push) + if body_filter is not None: + body = body_filter(body) + for line in body: + yield line + + for line in self.generate_email_footer(): + yield line + + +class Revision(Change): + """A Change consisting of a single git commit.""" + + def __init__(self, reference_change, rev, num, tot): + Change.__init__(self, reference_change.environment) + self.reference_change = reference_change + self.rev = rev + self.change_type = self.reference_change.change_type + self.refname = self.reference_change.refname + self.num = num + self.tot = tot + self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1]) + self.recipients = self.environment.get_revision_recipients(self) + + def _compute_values(self): + values = Change._compute_values(self) + + oneline = read_git_output( + ['log', '--format=%s', '--no-walk', self.rev.sha1] + ) + + values['rev'] = self.rev.sha1 + values['rev_short'] = self.rev.short + values['change_type'] = self.change_type + values['refname'] = self.refname + values['short_refname'] = self.reference_change.short_refname + values['refname_type'] = self.reference_change.refname_type + values['reply_to_msgid'] = self.reference_change.msgid + values['num'] = self.num + values['tot'] = self.tot + values['recipients'] = self.recipients + values['oneline'] = oneline + values['author'] = self.author + + reply_to = self.environment.get_reply_to_commit(self) + if reply_to: + values['reply_to'] = reply_to + + return values + + def generate_email_header(self): + for line in self.expand_header_lines(REVISION_HEADER_TEMPLATE): + yield line + + def generate_email_intro(self): + for line in self.expand_lines(REVISION_INTRO_TEMPLATE): + yield line + + def generate_email_body(self, push): + """Show this revision.""" + + return read_git_lines( + [ + 'log', '-C', + '--stat', '-p', '--cc', + '-1', self.rev.sha1, + ], + keepends=True, + ) + + def generate_email_footer(self): + return self.expand_lines(REVISION_FOOTER_TEMPLATE) + + +class ReferenceChange(Change): + """A Change to a Git reference. + + An abstract class representing a create, update, or delete of a + Git reference. Derived classes handle specific types of reference + (e.g., tags vs. branches). These classes generate the main + reference change email summarizing the reference change and + whether it caused any any commits to be added or removed. + + ReferenceChange objects are usually created using the static + create() method, which has the logic to decide which derived class + to instantiate.""" + + REF_RE = re.compile(r'^refs\/(?P<area>[^\/]+)\/(?P<shortname>.*)$') + + @staticmethod + def create(environment, oldrev, newrev, refname): + """Return a ReferenceChange object representing the change. + + Return an object that represents the type of change that is being + made. oldrev and newrev should be SHA1s or ZEROS.""" + + old = GitObject(oldrev) + new = GitObject(newrev) + rev = new or old + + # The revision type tells us what type the commit is, combined with + # the location of the ref we can decide between + # - working branch + # - tracking branch + # - unannotated tag + # - annotated tag + m = ReferenceChange.REF_RE.match(refname) + if m: + area = m.group('area') + short_refname = m.group('shortname') + else: + area = '' + short_refname = refname + + if rev.type == 'tag': + # Annotated tag: + klass = AnnotatedTagChange + elif rev.type == 'commit': + if area == 'tags': + # Non-annotated tag: + klass = NonAnnotatedTagChange + elif area == 'heads': + # Branch: + klass = BranchChange + elif area == 'remotes': + # Tracking branch: + sys.stderr.write( + '*** Push-update of tracking branch %r\n' + '*** - incomplete email generated.\n' + % (refname,) + ) + klass = OtherReferenceChange + else: + # Some other reference namespace: + sys.stderr.write( + '*** Push-update of strange reference %r\n' + '*** - incomplete email generated.\n' + % (refname,) + ) + klass = OtherReferenceChange + else: + # Anything else (is there anything else?) + sys.stderr.write( + '*** Unknown type of update to %r (%s)\n' + '*** - incomplete email generated.\n' + % (refname, rev.type,) + ) + klass = OtherReferenceChange + + return klass( + environment, + refname=refname, short_refname=short_refname, + old=old, new=new, rev=rev, + ) + + def __init__(self, environment, refname, short_refname, old, new, rev): + Change.__init__(self, environment) + self.change_type = { + (False, True) : 'create', + (True, True) : 'update', + (True, False) : 'delete', + }[bool(old), bool(new)] + self.refname = refname + self.short_refname = short_refname + self.old = old + self.new = new + self.rev = rev + self.msgid = make_msgid() + self.diffopts = environment.diffopts + self.logopts = environment.logopts + self.showlog = environment.refchange_showlog + + def _compute_values(self): + values = Change._compute_values(self) + + values['change_type'] = self.change_type + values['refname_type'] = self.refname_type + values['refname'] = self.refname + values['short_refname'] = self.short_refname + values['msgid'] = self.msgid + values['recipients'] = self.recipients + values['oldrev'] = str(self.old) + values['oldrev_short'] = self.old.short + values['newrev'] = str(self.new) + values['newrev_short'] = self.new.short + + if self.old: + values['oldrev_type'] = self.old.type + if self.new: + values['newrev_type'] = self.new.type + + reply_to = self.environment.get_reply_to_refchange(self) + if reply_to: + values['reply_to'] = reply_to + + return values + + def get_subject(self): + template = { + 'create' : REF_CREATED_SUBJECT_TEMPLATE, + 'update' : REF_UPDATED_SUBJECT_TEMPLATE, + 'delete' : REF_DELETED_SUBJECT_TEMPLATE, + }[self.change_type] + return self.expand(template) + + def generate_email_header(self): + for line in self.expand_header_lines( + REFCHANGE_HEADER_TEMPLATE, subject=self.get_subject(), + ): + yield line + + def generate_email_intro(self): + for line in self.expand_lines(REFCHANGE_INTRO_TEMPLATE): + yield line + + def generate_email_body(self, push): + """Call the appropriate body-generation routine. + + Call one of generate_create_summary() / + generate_update_summary() / generate_delete_summary().""" + + change_summary = { + 'create' : self.generate_create_summary, + 'delete' : self.generate_delete_summary, + 'update' : self.generate_update_summary, + }[self.change_type](push) + for line in change_summary: + yield line + + for line in self.generate_revision_change_summary(push): + yield line + + def generate_email_footer(self): + return self.expand_lines(FOOTER_TEMPLATE) + + def generate_revision_change_log(self, new_commits_list): + if self.showlog: + yield '\n' + yield 'Detailed log of new commits:\n\n' + for line in read_git_lines( + ['log', '--no-walk'] + + self.logopts + + new_commits_list + + ['--'], + keepends=True, + ): + yield line + + def generate_revision_change_summary(self, push): + """Generate a summary of the revisions added/removed by this change.""" + + if self.new.commit_sha1 and not self.old.commit_sha1: + # A new reference was created. List the new revisions + # brought by the new reference (i.e., those revisions that + # were not in the repository before this reference + # change). + sha1s = list(push.get_new_commits(self)) + sha1s.reverse() + tot = len(sha1s) + new_revisions = [ + Revision(self, GitObject(sha1), num=i+1, tot=tot) + for (i, sha1) in enumerate(sha1s) + ] + + if new_revisions: + yield self.expand('This %(refname_type)s includes the following new commits:\n') + yield '\n' + for r in new_revisions: + (sha1, subject) = r.rev.get_summary() + yield r.expand( + BRIEF_SUMMARY_TEMPLATE, action='new', text=subject, + ) + yield '\n' + for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot): + yield line + for line in self.generate_revision_change_log([r.rev.sha1 for r in new_revisions]): + yield line + else: + for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE): + yield line + + elif self.new.commit_sha1 and self.old.commit_sha1: + # A reference was changed to point at a different commit. + # List the revisions that were removed and/or added *from + # that reference* by this reference change, along with a + # diff between the trees for its old and new values. + + # List of the revisions that were added to the branch by + # this update. Note this list can include revisions that + # have already had notification emails; we want such + # revisions in the summary even though we will not send + # new notification emails for them. + adds = list(generate_summaries( + '--topo-order', '--reverse', '%s..%s' + % (self.old.commit_sha1, self.new.commit_sha1,) + )) + + # List of the revisions that were removed from the branch + # by this update. This will be empty except for + # non-fast-forward updates. + discards = list(generate_summaries( + '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,) + )) + + if adds: + new_commits_list = push.get_new_commits(self) + else: + new_commits_list = [] + new_commits = CommitSet(new_commits_list) + + if discards: + discarded_commits = CommitSet(push.get_discarded_commits(self)) + else: + discarded_commits = CommitSet([]) + + if discards and adds: + for (sha1, subject) in discards: + if sha1 in discarded_commits: + action = 'discards' + else: + action = 'omits' + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action=action, + rev_short=sha1, text=subject, + ) + for (sha1, subject) in adds: + if sha1 in new_commits: + action = 'new' + else: + action = 'adds' + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action=action, + rev_short=sha1, text=subject, + ) + yield '\n' + for line in self.expand_lines(NON_FF_TEMPLATE): + yield line + + elif discards: + for (sha1, subject) in discards: + if sha1 in discarded_commits: + action = 'discards' + else: + action = 'omits' + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action=action, + rev_short=sha1, text=subject, + ) + yield '\n' + for line in self.expand_lines(REWIND_ONLY_TEMPLATE): + yield line + + elif adds: + (sha1, subject) = self.old.get_summary() + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action='from', + rev_short=sha1, text=subject, + ) + for (sha1, subject) in adds: + if sha1 in new_commits: + action = 'new' + else: + action = 'adds' + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action=action, + rev_short=sha1, text=subject, + ) + + yield '\n' + + if new_commits: + for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=len(new_commits)): + yield line + for line in self.generate_revision_change_log(new_commits_list): + yield line + else: + for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE): + yield line + + # 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. + yield '\n' + yield 'Summary of changes:\n' + for line in read_git_lines( + ['diff-tree'] + + self.diffopts + + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)], + keepends=True, + ): + yield line + + elif self.old.commit_sha1 and not self.new.commit_sha1: + # A reference was deleted. List the revisions that were + # removed from the repository by this reference change. + + sha1s = list(push.get_discarded_commits(self)) + tot = len(sha1s) + discarded_revisions = [ + Revision(self, GitObject(sha1), num=i+1, tot=tot) + for (i, sha1) in enumerate(sha1s) + ] + + if discarded_revisions: + for line in self.expand_lines(DISCARDED_REVISIONS_TEMPLATE): + yield line + yield '\n' + for r in discarded_revisions: + (sha1, subject) = r.rev.get_summary() + yield r.expand( + BRIEF_SUMMARY_TEMPLATE, action='discards', text=subject, + ) + else: + for line in self.expand_lines(NO_DISCARDED_REVISIONS_TEMPLATE): + yield line + + elif not self.old.commit_sha1 and not self.new.commit_sha1: + for line in self.expand_lines(NON_COMMIT_UPDATE_TEMPLATE): + yield line + + def generate_create_summary(self, push): + """Called for the creation of a reference.""" + + # This is a new reference and so oldrev is not valid + (sha1, subject) = self.new.get_summary() + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action='at', + rev_short=sha1, text=subject, + ) + yield '\n' + + def generate_update_summary(self, push): + """Called for the change of a pre-existing branch.""" + + return iter([]) + + def generate_delete_summary(self, push): + """Called for the deletion of any type of reference.""" + + (sha1, subject) = self.old.get_summary() + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action='was', + rev_short=sha1, text=subject, + ) + yield '\n' + + +class BranchChange(ReferenceChange): + refname_type = 'branch' + + def __init__(self, environment, refname, short_refname, old, new, rev): + ReferenceChange.__init__( + self, environment, + refname=refname, short_refname=short_refname, + old=old, new=new, rev=rev, + ) + self.recipients = environment.get_refchange_recipients(self) + + +class AnnotatedTagChange(ReferenceChange): + refname_type = 'annotated tag' + + def __init__(self, environment, refname, short_refname, old, new, rev): + ReferenceChange.__init__( + self, environment, + refname=refname, short_refname=short_refname, + old=old, new=new, rev=rev, + ) + self.recipients = environment.get_announce_recipients(self) + self.show_shortlog = environment.announce_show_shortlog + + ANNOTATED_TAG_FORMAT = ( + '%(*objectname)\n' + '%(*objecttype)\n' + '%(taggername)\n' + '%(taggerdate)' + ) + + def describe_tag(self, push): + """Describe the new value of an annotated tag.""" + + # Use git for-each-ref to pull out the individual fields from + # the tag + [tagobject, tagtype, tagger, tagged] = read_git_lines( + ['for-each-ref', '--format=%s' % (self.ANNOTATED_TAG_FORMAT,), self.refname], + ) + + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action='tagging', + rev_short=tagobject, text='(%s)' % (tagtype,), + ) + if tagtype == '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 + try: + prevtag = read_git_output(['describe', '--abbrev=0', '%s^' % (self.new,)]) + except CommandError: + prevtag = None + if prevtag: + yield ' replaces %s\n' % (prevtag,) + else: + prevtag = None + yield ' length %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),) + + yield ' tagged by %s\n' % (tagger,) + yield ' on %s\n' % (tagged,) + yield '\n' + + # Show the content of the tag message; this might contain a + # change log or release notes so is worth displaying. + yield LOGBEGIN + contents = list(read_git_lines(['cat-file', 'tag', self.new.sha1], keepends=True)) + contents = contents[contents.index('\n') + 1:] + if contents and contents[-1][-1:] != '\n': + contents.append('\n') + for line in contents: + yield line + + if self.show_shortlog and tagtype == 'commit': + # Only commit tags make sense to have rev-list operations + # performed on them + yield '\n' + if prevtag: + # Show changes since the previous release + revlist = read_git_output( + ['rev-list', '--pretty=short', '%s..%s' % (prevtag, self.new,)], + keepends=True, + ) + else: + # No previous tag, show all the changes since time + # began + revlist = read_git_output( + ['rev-list', '--pretty=short', '%s' % (self.new,)], + keepends=True, + ) + for line in read_git_lines(['shortlog'], input=revlist, keepends=True): + yield line + + yield LOGEND + yield '\n' + + def generate_create_summary(self, push): + """Called for the creation of an annotated tag.""" + + for line in self.expand_lines(TAG_CREATED_TEMPLATE): + yield line + + for line in self.describe_tag(push): + yield line + + def generate_update_summary(self, push): + """Called for the update of an annotated tag. + + This is probably a rare event and may not even be allowed.""" + + for line in self.expand_lines(TAG_UPDATED_TEMPLATE): + yield line + + for line in self.describe_tag(push): + yield line + + def generate_delete_summary(self, push): + """Called when a non-annotated reference is updated.""" + + for line in self.expand_lines(TAG_DELETED_TEMPLATE): + yield line + + yield self.expand(' tag was %(oldrev_short)s\n') + yield '\n' + + +class NonAnnotatedTagChange(ReferenceChange): + refname_type = 'tag' + + def __init__(self, environment, refname, short_refname, old, new, rev): + ReferenceChange.__init__( + self, environment, + refname=refname, short_refname=short_refname, + old=old, new=new, rev=rev, + ) + self.recipients = environment.get_refchange_recipients(self) + + def generate_create_summary(self, push): + """Called for the creation of an annotated tag.""" + + for line in self.expand_lines(TAG_CREATED_TEMPLATE): + yield line + + def generate_update_summary(self, push): + """Called when a non-annotated reference is updated.""" + + for line in self.expand_lines(TAG_UPDATED_TEMPLATE): + yield line + + def generate_delete_summary(self, push): + """Called when a non-annotated reference is updated.""" + + for line in self.expand_lines(TAG_DELETED_TEMPLATE): + yield line + + for line in ReferenceChange.generate_delete_summary(self, push): + yield line + + +class OtherReferenceChange(ReferenceChange): + refname_type = 'reference' + + def __init__(self, environment, refname, short_refname, old, new, rev): + # We use the full refname as short_refname, because otherwise + # the full name of the reference would not be obvious from the + # text of the email. + ReferenceChange.__init__( + self, environment, + refname=refname, short_refname=refname, + old=old, new=new, rev=rev, + ) + self.recipients = environment.get_refchange_recipients(self) + + +class Mailer(object): + """An object that can send emails.""" + + def send(self, lines, to_addrs): + """Send an email consisting of lines. + + lines must be an iterable over the lines constituting the + header and body of the email. to_addrs is a list of recipient + addresses (can be needed even if lines already contains a + "To:" field). It can be either a string (comma-separated list + of email addresses) or a Python list of individual email + addresses. + + """ + + raise NotImplementedError() + + +class SendMailer(Mailer): + """Send emails using 'sendmail -t'.""" + + SENDMAIL_CANDIDATES = [ + '/usr/sbin/sendmail', + '/usr/lib/sendmail', + ] + + @staticmethod + def find_sendmail(): + for path in SendMailer.SENDMAIL_CANDIDATES: + if os.access(path, os.X_OK): + return path + else: + raise ConfigurationException( + 'No sendmail executable found. ' + 'Try setting multimailhook.sendmailCommand.' + ) + + def __init__(self, command=None, envelopesender=None): + """Construct a SendMailer instance. + + command should be the command and arguments used to invoke + sendmail, as a list of strings. If an envelopesender is + provided, it will also be passed to the command, via '-f + envelopesender'.""" + + if command: + self.command = command[:] + else: + self.command = [self.find_sendmail(), '-t'] + + if envelopesender: + self.command.extend(['-f', envelopesender]) + + def send(self, lines, to_addrs): + try: + p = subprocess.Popen(self.command, stdin=subprocess.PIPE) + except OSError, e: + sys.stderr.write( + '*** Cannot execute command: %s\n' % ' '.join(self.command) + + '*** %s\n' % str(e) + + '*** Try setting multimailhook.mailer to "smtp"\n' + '*** to send emails without using the sendmail command.\n' + ) + sys.exit(1) + try: + p.stdin.writelines(lines) + except: + sys.stderr.write( + '*** Error while generating commit email\n' + '*** - mail sending aborted.\n' + ) + p.terminate() + raise + else: + p.stdin.close() + retcode = p.wait() + if retcode: + raise CommandError(self.command, retcode) + + +class SMTPMailer(Mailer): + """Send emails using Python's smtplib.""" + + def __init__(self, envelopesender, smtpserver): + if not envelopesender: + sys.stderr.write( + 'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n' + 'please set either multimailhook.envelopeSender or user.email\n' + ) + sys.exit(1) + self.envelopesender = envelopesender + self.smtpserver = smtpserver + try: + self.smtp = smtplib.SMTP(self.smtpserver) + except Exception, e: + sys.stderr.write('*** Error establishing SMTP connection to %s***\n' % self.smtpserver) + sys.stderr.write('*** %s\n' % str(e)) + sys.exit(1) + + def __del__(self): + self.smtp.quit() + + def send(self, lines, to_addrs): + try: + msg = ''.join(lines) + # turn comma-separated list into Python list if needed. + if isinstance(to_addrs, basestring): + to_addrs = [email for (name, email) in getaddresses([to_addrs])] + self.smtp.sendmail(self.envelopesender, to_addrs, msg) + except Exception, e: + sys.stderr.write('*** Error sending email***\n') + sys.stderr.write('*** %s\n' % str(e)) + self.smtp.quit() + sys.exit(1) + + +class OutputMailer(Mailer): + """Write emails to an output stream, bracketed by lines of '=' characters. + + This is intended for debugging purposes.""" + + SEPARATOR = '=' * 75 + '\n' + + def __init__(self, f): + self.f = f + + def send(self, lines, to_addrs): + self.f.write(self.SEPARATOR) + self.f.writelines(lines) + self.f.write(self.SEPARATOR) + + +def get_git_dir(): + """Determine GIT_DIR. + + Determine GIT_DIR either from the GIT_DIR environment variable or + from the working directory, using Git's usual rules.""" + + try: + return read_git_output(['rev-parse', '--git-dir']) + except CommandError: + sys.stderr.write('fatal: git_multimail: not in a git directory\n') + sys.exit(1) + + +class Environment(object): + """Describes the environment in which the push is occurring. + + An Environment object encapsulates information about the local + environment. For example, it knows how to determine: + + * the name of the repository to which the push occurred + + * what user did the push + + * what users want to be informed about various types of changes. + + An Environment object is expected to have the following methods: + + get_repo_shortname() + + Return a short name for the repository, for display + purposes. + + get_repo_path() + + Return the absolute path to the Git repository. + + get_emailprefix() + + Return a string that will be prefixed to every email's + subject. + + get_pusher() + + Return the username of the person who pushed the changes. + This value is used in the email body to indicate who + pushed the change. + + get_pusher_email() (may return None) + + Return the email address of the person who pushed the + changes. The value should be a single RFC 2822 email + address as a string; e.g., "Joe User <user@example.com>" + if available, otherwise "user@example.com". If set, the + value is used as the Reply-To address for refchange + emails. If it is impossible to determine the pusher's + email, this attribute should be set to None (in which case + no Reply-To header will be output). + + get_sender() + + Return the address to be used as the 'From' email address + in the email envelope. + + get_fromaddr() + + Return the 'From' email address used in the email 'From:' + headers. (May be a full RFC 2822 email address like 'Joe + User <user@example.com>'.) + + get_administrator() + + Return the name and/or email of the repository + administrator. This value is used in the footer as the + person to whom requests to be removed from the + notification list should be sent. Ideally, it should + include a valid email address. + + get_reply_to_refchange() + get_reply_to_commit() + + Return the address to use in the email "Reply-To" header, + as a string. These can be an RFC 2822 email address, or + None to omit the "Reply-To" header. + get_reply_to_refchange() is used for refchange emails; + get_reply_to_commit() is used for individual commit + emails. + + They should also define the following attributes: + + announce_show_shortlog (bool) + + True iff announce emails should include a shortlog. + + refchange_showlog (bool) + + True iff refchanges emails should include a detailed log. + + diffopts (list of strings) + + The options that should be passed to 'git diff' for the + summary email. The value should be a list of strings + representing words to be passed to the command. + + logopts (list of strings) + + Analogous to diffopts, but contains options passed to + 'git log' when generating the detailed log for a set of + commits (see refchange_showlog) + + """ + + REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$') + + def __init__(self, osenv=None): + self.osenv = osenv or os.environ + self.announce_show_shortlog = False + self.maxcommitemails = 500 + self.diffopts = ['--stat', '--summary', '--find-copies-harder'] + self.logopts = [] + self.refchange_showlog = False + + self.COMPUTED_KEYS = [ + 'administrator', + 'charset', + 'emailprefix', + 'fromaddr', + 'pusher', + 'pusher_email', + 'repo_path', + 'repo_shortname', + 'sender', + ] + + self._values = None + + def get_repo_shortname(self): + """Use the last part of the repo path, with ".git" stripped off if present.""" + + basename = os.path.basename(os.path.abspath(self.get_repo_path())) + m = self.REPO_NAME_RE.match(basename) + if m: + return m.group('name') + else: + return basename + + def get_pusher(self): + raise NotImplementedError() + + def get_pusher_email(self): + return None + + def get_administrator(self): + return 'the administrator of this repository' + + def get_emailprefix(self): + return '' + + def get_repo_path(self): + if read_git_output(['rev-parse', '--is-bare-repository']) == 'true': + path = get_git_dir() + else: + path = read_git_output(['rev-parse', '--show-toplevel']) + return os.path.abspath(path) + + def get_charset(self): + return CHARSET + + def get_values(self): + """Return a dictionary {keyword : expansion} for this Environment. + + This method is called by Change._compute_values(). The keys + in the returned dictionary are available to be used in any of + the templates. The dictionary is created by calling + self.get_NAME() for each of the attributes named in + COMPUTED_KEYS and recording those that do not return None. + The return value is always a new dictionary.""" + + if self._values is None: + values = {} + + for key in self.COMPUTED_KEYS: + value = getattr(self, 'get_%s' % (key,))() + if value is not None: + values[key] = value + + self._values = values + + return self._values.copy() + + def get_refchange_recipients(self, refchange): + """Return the recipients for notifications about refchange. + + Return the list of email addresses to which notifications + about the specified ReferenceChange should be sent.""" + + raise NotImplementedError() + + def get_announce_recipients(self, annotated_tag_change): + """Return the recipients for notifications about annotated_tag_change. + + Return the list of email addresses to which notifications + about the specified AnnotatedTagChange should be sent.""" + + raise NotImplementedError() + + def get_reply_to_refchange(self, refchange): + return self.get_pusher_email() + + def get_revision_recipients(self, revision): + """Return the recipients for messages about revision. + + Return the list of email addresses to which notifications + about the specified Revision should be sent. This method + could be overridden, for example, to take into account the + contents of the revision when deciding whom to notify about + it. For example, there could be a scheme for users to express + interest in particular files or subdirectories, and only + receive notification emails for revisions that affecting those + files.""" + + raise NotImplementedError() + + def get_reply_to_commit(self, revision): + return revision.author + + def filter_body(self, lines): + """Filter the lines intended for an email body. + + lines is an iterable over the lines that would go into the + email body. Filter it (e.g., limit the number of lines, the + line length, character set, etc.), returning another iterable. + See FilterLinesEnvironmentMixin and MaxlinesEnvironmentMixin + for classes implementing this functionality.""" + + return lines + + +class ConfigEnvironmentMixin(Environment): + """A mixin that sets self.config to its constructor's config argument. + + This class's constructor consumes the "config" argument. + + Mixins that need to inspect the config should inherit from this + class (1) to make sure that "config" is still in the constructor + arguments with its own constructor runs and/or (2) to be sure that + self.config is set after construction.""" + + def __init__(self, config, **kw): + super(ConfigEnvironmentMixin, self).__init__(**kw) + self.config = config + + +class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): + """An Environment that reads most of its information from "git config".""" + + def __init__(self, config, **kw): + super(ConfigOptionsEnvironmentMixin, self).__init__( + config=config, **kw + ) + + self.announce_show_shortlog = config.get_bool( + 'announceshortlog', default=self.announce_show_shortlog + ) + + self.refchange_showlog = config.get_bool( + 'refchangeshowlog', default=self.refchange_showlog + ) + + maxcommitemails = config.get('maxcommitemails') + if maxcommitemails is not None: + try: + self.maxcommitemails = int(maxcommitemails) + except ValueError: + sys.stderr.write( + '*** Malformed value for multimailhook.maxCommitEmails: %s\n' % maxcommitemails + + '*** Expected a number. Ignoring.\n' + ) + + diffopts = config.get('diffopts') + if diffopts is not None: + self.diffopts = shlex.split(diffopts) + + logopts = config.get('logopts') + if logopts is not None: + self.logopts = shlex.split(logopts) + + reply_to = config.get('replyTo') + self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to) + if ( + self.__reply_to_refchange is not None + and self.__reply_to_refchange.lower() == 'author' + ): + raise ConfigurationException( + '"author" is not an allowed setting for replyToRefchange' + ) + self.__reply_to_commit = config.get('replyToCommit', default=reply_to) + + def get_administrator(self): + return ( + self.config.get('administrator') + or self.get_sender() + or super(ConfigOptionsEnvironmentMixin, self).get_administrator() + ) + + def get_repo_shortname(self): + return ( + self.config.get('reponame') + or super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname() + ) + + def get_emailprefix(self): + emailprefix = self.config.get('emailprefix') + if emailprefix and emailprefix.strip(): + return emailprefix.strip() + ' ' + else: + return '[%s] ' % (self.get_repo_shortname(),) + + def get_sender(self): + return self.config.get('envelopesender') + + def get_fromaddr(self): + fromaddr = self.config.get('from') + if fromaddr: + return fromaddr + else: + config = Config('user') + fromname = config.get('name', default='') + fromemail = config.get('email', default='') + if fromemail: + return formataddr([fromname, fromemail]) + else: + return self.get_sender() + + def get_reply_to_refchange(self, refchange): + if self.__reply_to_refchange is None: + return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_refchange(refchange) + elif self.__reply_to_refchange.lower() == 'pusher': + return self.get_pusher_email() + elif self.__reply_to_refchange.lower() == 'none': + return None + else: + return self.__reply_to_refchange + + def get_reply_to_commit(self, revision): + if self.__reply_to_commit is None: + return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision) + elif self.__reply_to_commit.lower() == 'author': + return revision.get_author() + elif self.__reply_to_commit.lower() == 'pusher': + return self.get_pusher_email() + elif self.__reply_to_commit.lower() == 'none': + return None + else: + return self.__reply_to_commit + + +class FilterLinesEnvironmentMixin(Environment): + """Handle encoding and maximum line length of body lines. + + emailmaxlinelength (int or None) + + The maximum length of any single line in the email body. + Longer lines are truncated at that length with ' [...]' + appended. + + strict_utf8 (bool) + + If this field is set to True, then the email body text is + expected to be UTF-8. Any invalid characters are + converted to U+FFFD, the Unicode replacement character + (encoded as UTF-8, of course). + + """ + + def __init__(self, strict_utf8=True, emailmaxlinelength=500, **kw): + super(FilterLinesEnvironmentMixin, self).__init__(**kw) + self.__strict_utf8 = strict_utf8 + self.__emailmaxlinelength = emailmaxlinelength + + def filter_body(self, lines): + lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines) + if self.__strict_utf8: + lines = (line.decode(ENCODING, 'replace') for line in lines) + # Limit the line length in Unicode-space to avoid + # splitting characters: + if self.__emailmaxlinelength: + lines = limit_linelength(lines, self.__emailmaxlinelength) + lines = (line.encode(ENCODING, 'replace') for line in lines) + elif self.__emailmaxlinelength: + lines = limit_linelength(lines, self.__emailmaxlinelength) + + return lines + + +class ConfigFilterLinesEnvironmentMixin( + ConfigEnvironmentMixin, + FilterLinesEnvironmentMixin, + ): + """Handle encoding and maximum line length based on config.""" + + def __init__(self, config, **kw): + strict_utf8 = config.get_bool('emailstrictutf8', default=None) + if strict_utf8 is not None: + kw['strict_utf8'] = strict_utf8 + + emailmaxlinelength = config.get('emailmaxlinelength') + if emailmaxlinelength is not None: + kw['emailmaxlinelength'] = int(emailmaxlinelength) + + super(ConfigFilterLinesEnvironmentMixin, self).__init__( + config=config, **kw + ) + + +class MaxlinesEnvironmentMixin(Environment): + """Limit the email body to a specified number of lines.""" + + def __init__(self, emailmaxlines, **kw): + super(MaxlinesEnvironmentMixin, self).__init__(**kw) + self.__emailmaxlines = emailmaxlines + + def filter_body(self, lines): + lines = super(MaxlinesEnvironmentMixin, self).filter_body(lines) + if self.__emailmaxlines: + lines = limit_lines(lines, self.__emailmaxlines) + return lines + + +class ConfigMaxlinesEnvironmentMixin( + ConfigEnvironmentMixin, + MaxlinesEnvironmentMixin, + ): + """Limit the email body to the number of lines specified in config.""" + + def __init__(self, config, **kw): + emailmaxlines = int(config.get('emailmaxlines', default='0')) + super(ConfigMaxlinesEnvironmentMixin, self).__init__( + config=config, + emailmaxlines=emailmaxlines, + **kw + ) + + +class PusherDomainEnvironmentMixin(ConfigEnvironmentMixin): + """Deduce pusher_email from pusher by appending an emaildomain.""" + + def __init__(self, **kw): + super(PusherDomainEnvironmentMixin, self).__init__(**kw) + self.__emaildomain = self.config.get('emaildomain') + + def get_pusher_email(self): + if self.__emaildomain: + # Derive the pusher's full email address in the default way: + return '%s@%s' % (self.get_pusher(), self.__emaildomain) + else: + return super(PusherDomainEnvironmentMixin, self).get_pusher_email() + + +class StaticRecipientsEnvironmentMixin(Environment): + """Set recipients statically based on constructor parameters.""" + + def __init__( + self, + refchange_recipients, announce_recipients, revision_recipients, + **kw + ): + super(StaticRecipientsEnvironmentMixin, self).__init__(**kw) + + # The recipients for various types of notification emails, as + # RFC 2822 email addresses separated by commas (or the empty + # string if no recipients are configured). Although there is + # a mechanism to choose the recipient lists based on on the + # actual *contents* of the change being reported, we only + # choose based on the *type* of the change. Therefore we can + # compute them once and for all: + self.__refchange_recipients = refchange_recipients + self.__announce_recipients = announce_recipients + self.__revision_recipients = revision_recipients + + def get_refchange_recipients(self, refchange): + return self.__refchange_recipients + + def get_announce_recipients(self, annotated_tag_change): + return self.__announce_recipients + + def get_revision_recipients(self, revision): + return self.__revision_recipients + + +class ConfigRecipientsEnvironmentMixin( + ConfigEnvironmentMixin, + StaticRecipientsEnvironmentMixin + ): + """Determine recipients statically based on config.""" + + def __init__(self, config, **kw): + super(ConfigRecipientsEnvironmentMixin, self).__init__( + config=config, + refchange_recipients=self._get_recipients( + config, 'refchangelist', 'mailinglist', + ), + announce_recipients=self._get_recipients( + config, 'announcelist', 'refchangelist', 'mailinglist', + ), + revision_recipients=self._get_recipients( + config, 'commitlist', 'mailinglist', + ), + **kw + ) + + def _get_recipients(self, config, *names): + """Return the recipients for a particular type of message. + + Return the list of email addresses to which a particular type + of notification email should be sent, by looking at the config + value for "multimailhook.$name" for each of names. Use the + value from the first name that is configured. The return + value is a (possibly empty) string containing RFC 2822 email + addresses separated by commas. If no configuration could be + found, raise a ConfigurationException.""" + + for name in names: + retval = config.get_recipients(name) + if retval is not None: + return retval + if len(names) == 1: + hint = 'Please set "%s.%s"' % (config.section, name) + else: + hint = ( + 'Please set one of the following:\n "%s"' + % ('"\n "'.join('%s.%s' % (config.section, name) for name in names)) + ) + + raise ConfigurationException( + 'The list of recipients for %s is not configured.\n%s' % (names[0], hint) + ) + + +class ProjectdescEnvironmentMixin(Environment): + """Make a "projectdesc" value available for templates. + + By default, it is set to the first line of $GIT_DIR/description + (if that file is present and appears to be set meaningfully).""" + + def __init__(self, **kw): + super(ProjectdescEnvironmentMixin, self).__init__(**kw) + self.COMPUTED_KEYS += ['projectdesc'] + + def get_projectdesc(self): + """Return a one-line descripition of the project.""" + + git_dir = get_git_dir() + try: + projectdesc = open(os.path.join(git_dir, 'description')).readline().strip() + if projectdesc and not projectdesc.startswith('Unnamed repository'): + return projectdesc + except IOError: + pass + + return 'UNNAMED PROJECT' + + +class GenericEnvironmentMixin(Environment): + def get_pusher(self): + return self.osenv.get('USER', 'unknown user') + + +class GenericEnvironment( + ProjectdescEnvironmentMixin, + ConfigMaxlinesEnvironmentMixin, + ConfigFilterLinesEnvironmentMixin, + ConfigRecipientsEnvironmentMixin, + PusherDomainEnvironmentMixin, + ConfigOptionsEnvironmentMixin, + GenericEnvironmentMixin, + Environment, + ): + pass + + +class GitoliteEnvironmentMixin(Environment): + def get_repo_shortname(self): + # The gitolite environment variable $GL_REPO is a pretty good + # repo_shortname (though it's probably not as good as a value + # the user might have explicitly put in his config). + return ( + self.osenv.get('GL_REPO', None) + or super(GitoliteEnvironmentMixin, self).get_repo_shortname() + ) + + def get_pusher(self): + return self.osenv.get('GL_USER', 'unknown user') + + +class GitoliteEnvironment( + ProjectdescEnvironmentMixin, + ConfigMaxlinesEnvironmentMixin, + ConfigFilterLinesEnvironmentMixin, + ConfigRecipientsEnvironmentMixin, + PusherDomainEnvironmentMixin, + ConfigOptionsEnvironmentMixin, + GitoliteEnvironmentMixin, + Environment, + ): + pass + + +class Push(object): + """Represent an entire push (i.e., a group of ReferenceChanges). + + It is easy to figure out what commits were added to a *branch* by + a Reference change: + + git rev-list change.old..change.new + + or removed from a *branch*: + + git rev-list change.new..change.old + + But it is not quite so trivial to determine which entirely new + commits were added to the *repository* by a push and which old + commits were discarded by a push. A big part of the job of this + class is to figure out these things, and to make sure that new + commits are only detailed once even if they were added to multiple + references. + + The first step is to determine the "other" references--those + unaffected by the current push. They are computed by + Push._compute_other_ref_sha1s() by listing all references then + removing any affected by this push. + + The commits contained in the repository before this push were + + git rev-list other1 other2 other3 ... change1.old change2.old ... + + Where "changeN.old" is the old value of one of the references + affected by this push. + + The commits contained in the repository after this push are + + git rev-list other1 other2 other3 ... change1.new change2.new ... + + The commits added by this push are the difference between these + two sets, which can be written + + git rev-list \ + ^other1 ^other2 ... \ + ^change1.old ^change2.old ... \ + change1.new change2.new ... + + The commits removed by this push can be computed by + + git rev-list \ + ^other1 ^other2 ... \ + ^change1.new ^change2.new ... \ + change1.old change2.old ... + + The last point is that it is possible that other pushes are + occurring simultaneously to this one, so reference values can + change at any time. It is impossible to eliminate all race + conditions, but we reduce the window of time during which problems + can occur by translating reference names to SHA1s as soon as + possible and working with SHA1s thereafter (because SHA1s are + immutable).""" + + # A map {(changeclass, changetype) : integer} specifying the order + # that reference changes will be processed if multiple reference + # changes are included in a single push. The order is significant + # mostly because new commit notifications are threaded together + # with the first reference change that includes the commit. The + # following order thus causes commits to be grouped with branch + # changes (as opposed to tag changes) if possible. + SORT_ORDER = dict( + (value, i) for (i, value) in enumerate([ + (BranchChange, 'update'), + (BranchChange, 'create'), + (AnnotatedTagChange, 'update'), + (AnnotatedTagChange, 'create'), + (NonAnnotatedTagChange, 'update'), + (NonAnnotatedTagChange, 'create'), + (BranchChange, 'delete'), + (AnnotatedTagChange, 'delete'), + (NonAnnotatedTagChange, 'delete'), + (OtherReferenceChange, 'update'), + (OtherReferenceChange, 'create'), + (OtherReferenceChange, 'delete'), + ]) + ) + + def __init__(self, changes): + self.changes = sorted(changes, key=self._sort_key) + + # The SHA-1s of commits referred to by references unaffected + # by this push: + other_ref_sha1s = self._compute_other_ref_sha1s() + + self._old_rev_exclusion_spec = self._compute_rev_exclusion_spec( + other_ref_sha1s.union( + change.old.sha1 + for change in self.changes + if change.old.type in ['commit', 'tag'] + ) + ) + self._new_rev_exclusion_spec = self._compute_rev_exclusion_spec( + other_ref_sha1s.union( + change.new.sha1 + for change in self.changes + if change.new.type in ['commit', 'tag'] + ) + ) + + @classmethod + def _sort_key(klass, change): + return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,) + + def _compute_other_ref_sha1s(self): + """Return the GitObjects referred to by references unaffected by this push.""" + + # The refnames being changed by this push: + updated_refs = set( + change.refname + for change in self.changes + ) + + # The SHA-1s of commits referred to by all references in this + # repository *except* updated_refs: + sha1s = set() + fmt = ( + '%(objectname) %(objecttype) %(refname)\n' + '%(*objectname) %(*objecttype) %(refname)' + ) + for line in read_git_lines(['for-each-ref', '--format=%s' % (fmt,)]): + (sha1, type, name) = line.split(' ', 2) + if sha1 and type == 'commit' and name not in updated_refs: + sha1s.add(sha1) + + return sha1s + + def _compute_rev_exclusion_spec(self, sha1s): + """Return an exclusion specification for 'git rev-list'. + + git_objects is an iterable over GitObject instances. Return a + string that can be passed to the standard input of 'git + rev-list --stdin' to exclude all of the commits referred to by + git_objects.""" + + return ''.join( + ['^%s\n' % (sha1,) for sha1 in sorted(sha1s)] + ) + + def get_new_commits(self, reference_change=None): + """Return a list of commits added by this push. + + Return a list of the object names of commits that were added + by the part of this push represented by reference_change. If + reference_change is None, then return a list of *all* commits + added by this push.""" + + if not reference_change: + new_revs = sorted( + change.new.sha1 + for change in self.changes + if change.new + ) + elif not reference_change.new.commit_sha1: + return [] + else: + new_revs = [reference_change.new.commit_sha1] + + cmd = ['rev-list', '--stdin'] + new_revs + return read_git_lines(cmd, input=self._old_rev_exclusion_spec) + + def get_discarded_commits(self, reference_change): + """Return a list of commits discarded by this push. + + Return a list of the object names of commits that were + entirely discarded from the repository by the part of this + push represented by reference_change.""" + + if not reference_change.old.commit_sha1: + return [] + else: + old_revs = [reference_change.old.commit_sha1] + + cmd = ['rev-list', '--stdin'] + old_revs + return read_git_lines(cmd, input=self._new_rev_exclusion_spec) + + def send_emails(self, mailer, body_filter=None): + """Use send all of the notification emails needed for this push. + + Use send all of the notification emails (including reference + change emails and commit emails) needed for this push. Send + the emails using mailer. If body_filter is not None, then use + it to filter the lines that are intended for the email + body.""" + + # The sha1s of commits that were introduced by this push. + # They will be removed from this set as they are processed, to + # guarantee that one (and only one) email is generated for + # each new commit. + unhandled_sha1s = set(self.get_new_commits()) + for change in self.changes: + # Check if we've got anyone to send to + if not change.recipients: + sys.stderr.write( + '*** no recipients configured so no email will be sent\n' + '*** for %r update %s->%s\n' + % (change.refname, change.old.sha1, change.new.sha1,) + ) + else: + sys.stderr.write('Sending notification emails to: %s\n' % (change.recipients,)) + mailer.send(change.generate_email(self, body_filter), change.recipients) + + sha1s = [] + for sha1 in reversed(list(self.get_new_commits(change))): + if sha1 in unhandled_sha1s: + sha1s.append(sha1) + unhandled_sha1s.remove(sha1) + + max_emails = change.environment.maxcommitemails + if max_emails and len(sha1s) > max_emails: + sys.stderr.write( + '*** Too many new commits (%d), not sending commit emails.\n' % len(sha1s) + + '*** Try setting multimailhook.maxCommitEmails to a greater value\n' + + '*** Currently, multimailhook.maxCommitEmails=%d\n' % max_emails + ) + return + + for (num, sha1) in enumerate(sha1s): + rev = Revision(change, GitObject(sha1), num=num+1, tot=len(sha1s)) + if rev.recipients: + mailer.send(rev.generate_email(self, body_filter), rev.recipients) + + # Consistency check: + if unhandled_sha1s: + sys.stderr.write( + 'ERROR: No emails were sent for the following new commits:\n' + ' %s\n' + % ('\n '.join(sorted(unhandled_sha1s)),) + ) + + +def run_as_post_receive_hook(environment, mailer): + changes = [] + for line in sys.stdin: + (oldrev, newrev, refname) = line.strip().split(' ', 2) + changes.append( + ReferenceChange.create(environment, oldrev, newrev, refname) + ) + push = Push(changes) + push.send_emails(mailer, body_filter=environment.filter_body) + + +def run_as_update_hook(environment, mailer, refname, oldrev, newrev): + changes = [ + ReferenceChange.create( + environment, + read_git_output(['rev-parse', '--verify', oldrev]), + read_git_output(['rev-parse', '--verify', newrev]), + refname, + ), + ] + push = Push(changes) + push.send_emails(mailer, body_filter=environment.filter_body) + + +def choose_mailer(config, environment): + mailer = config.get('mailer', default='sendmail') + + if mailer == 'smtp': + smtpserver = config.get('smtpserver', default='localhost') + mailer = SMTPMailer( + envelopesender=(environment.get_sender() or environment.get_fromaddr()), + smtpserver=smtpserver, + ) + elif mailer == 'sendmail': + command = config.get('sendmailcommand') + if command: + command = shlex.split(command) + mailer = SendMailer(command=command, envelopesender=environment.get_sender()) + else: + sys.stderr.write( + 'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer + + 'please use one of "smtp" or "sendmail".\n' + ) + sys.exit(1) + return mailer + + +KNOWN_ENVIRONMENTS = { + 'generic' : GenericEnvironmentMixin, + 'gitolite' : GitoliteEnvironmentMixin, + } + + +def choose_environment(config, osenv=None, env=None, recipients=None): + if not osenv: + osenv = os.environ + + environment_mixins = [ + ProjectdescEnvironmentMixin, + ConfigMaxlinesEnvironmentMixin, + ConfigFilterLinesEnvironmentMixin, + PusherDomainEnvironmentMixin, + ConfigOptionsEnvironmentMixin, + ] + environment_kw = { + 'osenv' : osenv, + 'config' : config, + } + + if not env: + env = config.get('environment') + + if not env: + if 'GL_USER' in osenv and 'GL_REPO' in osenv: + env = 'gitolite' + else: + env = 'generic' + + environment_mixins.append(KNOWN_ENVIRONMENTS[env]) + + if recipients: + environment_mixins.insert(0, StaticRecipientsEnvironmentMixin) + environment_kw['refchange_recipients'] = recipients + environment_kw['announce_recipients'] = recipients + environment_kw['revision_recipients'] = recipients + else: + environment_mixins.insert(0, ConfigRecipientsEnvironmentMixin) + + environment_klass = type( + 'EffectiveEnvironment', + tuple(environment_mixins) + (Environment,), + {}, + ) + return environment_klass(**environment_kw) + + +def main(args): + parser = optparse.OptionParser( + description=__doc__, + usage='%prog [OPTIONS]\n or: %prog [OPTIONS] REFNAME OLDREV NEWREV', + ) + + parser.add_option( + '--environment', '--env', action='store', type='choice', + choices=['generic', 'gitolite'], default=None, + help=( + 'Choose type of environment is in use. Default is taken from ' + 'multimailhook.environment if set; otherwise "generic".' + ), + ) + parser.add_option( + '--stdout', action='store_true', default=False, + help='Output emails to stdout rather than sending them.', + ) + parser.add_option( + '--recipients', action='store', default=None, + help='Set list of email recipients for all types of emails.', + ) + parser.add_option( + '--show-env', action='store_true', default=False, + help=( + 'Write to stderr the values determined for the environment ' + '(intended for debugging purposes).' + ), + ) + + (options, args) = parser.parse_args(args) + + config = Config('multimailhook') + + try: + environment = choose_environment( + config, osenv=os.environ, + env=options.environment, + recipients=options.recipients, + ) + + if options.show_env: + sys.stderr.write('Environment values:\n') + for (k,v) in sorted(environment.get_values().items()): + sys.stderr.write(' %s : %r\n' % (k,v)) + sys.stderr.write('\n') + + if options.stdout: + mailer = OutputMailer(sys.stdout) + else: + mailer = choose_mailer(config, environment) + + # Dual mode: if arguments were specified on the command line, run + # like an update hook; otherwise, run as a post-receive hook. + if args: + if len(args) != 3: + parser.error('Need zero or three non-option arguments') + (refname, oldrev, newrev) = args + run_as_update_hook(environment, mailer, refname, oldrev, newrev) + else: + run_as_post_receive_hook(environment, mailer) + except ConfigurationException, e: + sys.exit(str(e)) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config new file mode 100755 index 0000000000..04eeaac413 --- /dev/null +++ b/contrib/hooks/multimail/migrate-mailhook-config @@ -0,0 +1,269 @@ +#! /usr/bin/env python2 + +"""Migrate a post-receive-email configuration to be usable with git_multimail.py. + +See README.migrate-from-post-receive-email for more information. + +""" + +import sys +import optparse + +from git_multimail import CommandError +from git_multimail import Config +from git_multimail import read_output + + +OLD_NAMES = [ + 'mailinglist', + 'announcelist', + 'envelopesender', + 'emailprefix', + 'showrev', + 'emailmaxlines', + 'diffopts', + ] + +NEW_NAMES = [ + 'environment', + 'reponame', + 'mailinglist', + 'refchangelist', + 'commitlist', + 'announcelist', + 'announceshortlog', + 'envelopesender', + 'administrator', + 'emailprefix', + 'emailmaxlines', + 'diffopts', + 'emaildomain', + ] + + +INFO = """\ + +SUCCESS! + +Your post-receive-email configuration has been converted to +git-multimail format. Please see README and +README.migrate-from-post-receive-email to learn about other +git-multimail configuration possibilities. + +For example, git-multimail has the following new options with no +equivalent in post-receive-email. You might want to read about them +to see if they would be useful in your situation: + +""" + + +def _check_old_config_exists(old): + """Check that at least one old configuration value is set.""" + + for name in OLD_NAMES: + if old.has_key(name): + return True + + return False + + +def _check_new_config_clear(new): + """Check that none of the new configuration names are set.""" + + retval = True + for name in NEW_NAMES: + if new.has_key(name): + if retval: + sys.stderr.write('INFO: The following configuration values already exist:\n\n') + sys.stderr.write(' "%s.%s"\n' % (new.section, name)) + retval = False + + return retval + + +def erase_values(config, names): + for name in names: + if config.has_key(name): + try: + sys.stderr.write('...unsetting "%s.%s"\n' % (config.section, name)) + config.unset_all(name) + except CommandError: + sys.stderr.write( + '\nWARNING: could not unset "%s.%s". ' + 'Perhaps it is not set at the --local level?\n\n' + % (config.section, name) + ) + + +def is_section_empty(section, local): + """Return True iff the specified configuration section is empty. + + Iff local is True, use the --local option when invoking 'git + config'.""" + + if local: + local_option = ['--local'] + else: + local_option = [] + + try: + read_output( + ['git', 'config'] + + local_option + + ['--get-regexp', '^%s\.' % (section,)] + ) + except CommandError, e: + if e.retcode == 1: + # This means that no settings were found. + return True + else: + raise + else: + return False + + +def remove_section_if_empty(section): + """If the specified configuration section is empty, delete it.""" + + try: + empty = is_section_empty(section, local=True) + except CommandError: + # Older versions of git do not support the --local option, so + # if the first attempt fails, try without --local. + try: + empty = is_section_empty(section, local=False) + except CommandError: + sys.stderr.write( + '\nINFO: If configuration section "%s.*" is empty, you might want ' + 'to delete it.\n\n' + % (section,) + ) + return + + if empty: + sys.stderr.write('...removing section "%s.*"\n' % (section,)) + read_output(['git', 'config', '--remove-section', section]) + else: + sys.stderr.write( + '\nINFO: Configuration section "%s.*" still has contents. ' + 'It will not be deleted.\n\n' + % (section,) + ) + + +def migrate_config(strict=False, retain=False, overwrite=False): + old = Config('hooks') + new = Config('multimailhook') + if not _check_old_config_exists(old): + sys.exit( + 'Your repository has no post-receive-email configuration. ' + 'Nothing to do.' + ) + if not _check_new_config_clear(new): + if overwrite: + sys.stderr.write('\nWARNING: Erasing the above values...\n\n') + erase_values(new, NEW_NAMES) + else: + sys.exit( + '\nERROR: Refusing to overwrite existing values. Use the --overwrite\n' + 'option to continue anyway.' + ) + + name = 'showrev' + if old.has_key(name): + msg = 'git-multimail does not support "%s.%s"' % (old.section, name,) + if strict: + sys.exit( + 'ERROR: %s.\n' + 'Please unset that value then try again, or run without --strict.' + % (msg,) + ) + else: + sys.stderr.write('\nWARNING: %s (ignoring).\n\n' % (msg,)) + + for name in ['mailinglist', 'announcelist']: + if old.has_key(name): + sys.stderr.write( + '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name) + ) + new.set_recipients(name, old.get_recipients(name)) + + if strict: + sys.stderr.write( + '...setting "%s.commitlist" to the empty string\n' % (new.section,) + ) + new.set_recipients('commitlist', '') + sys.stderr.write( + '...setting "%s.announceshortlog" to "true"\n' % (new.section,) + ) + new.set('announceshortlog', 'true') + + for name in ['envelopesender', 'emailmaxlines', 'diffopts']: + if old.has_key(name): + sys.stderr.write( + '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name) + ) + new.set(name, old.get(name)) + + name = 'emailprefix' + if old.has_key(name): + sys.stderr.write( + '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name) + ) + new.set(name, old.get(name)) + elif strict: + sys.stderr.write( + '...setting "%s.%s" to "[SCM]" to preserve old subject lines\n' + % (new.section, name) + ) + new.set(name, '[SCM]') + + if not retain: + erase_values(old, OLD_NAMES) + remove_section_if_empty(old.section) + + sys.stderr.write(INFO) + for name in NEW_NAMES: + if name not in OLD_NAMES: + sys.stderr.write(' "%s.%s"\n' % (new.section, name,)) + sys.stderr.write('\n') + + +def main(args): + parser = optparse.OptionParser( + description=__doc__, + usage='%prog [OPTIONS]', + ) + + parser.add_option( + '--strict', action='store_true', default=False, + help=( + 'Slavishly configure git-multimail as closely as possible to ' + 'the post-receive-email configuration. Default is to turn ' + 'on some new features that have no equivalent in post-receive-email.' + ), + ) + parser.add_option( + '--retain', action='store_true', default=False, + help=( + 'Retain the post-receive-email configuration values. ' + 'Default is to delete them after the new values are set.' + ), + ) + parser.add_option( + '--overwrite', action='store_true', default=False, + help=( + 'Overwrite any existing git-multimail configuration settings. ' + 'Default is to abort if such settings already exist.' + ), + ) + + (options, args) = parser.parse_args(args) + + if args: + parser.error('Unexpected arguments: %s' % (' '.join(args),)) + + migrate_config(strict=options.strict, retain=options.retain, overwrite=options.overwrite) + + +main(sys.argv[1:]) diff --git a/contrib/hooks/multimail/post-receive b/contrib/hooks/multimail/post-receive new file mode 100755 index 0000000000..93ebb437d1 --- /dev/null +++ b/contrib/hooks/multimail/post-receive @@ -0,0 +1,90 @@ +#! /usr/bin/env python2 + +"""Example post-receive hook based on git-multimail. + +This script is a simple example of a post-receive hook implemented +using git_multimail.py as a Python module. It is intended to be +customized before use; see the comments in the script to help you get +started. + +It is possible to use git_multimail.py itself as a post-receive or +update hook, configured via git config settings and/or command-line +parameters. But for more flexibility, it can also be imported as a +Python module by a custom post-receive script as done here. The +latter has the following advantages: + +* The tool's behavior can be customized using arbitrary Python code, + without having to edit git_multimail.py. + +* Configuration settings can be read from other sources; for example, + user names and email addresses could be read from LDAP or from a + database. Or the settings can even be hardcoded in the importing + Python script, if this is preferred. + +This script is a very basic example of how to use git_multimail.py as +a module. The comments below explain some of the points at which the +script's behavior could be changed or customized. + +""" + +import sys +import os + +# If necessary, add the path to the directory containing +# git_multimail.py to the Python path as follows. (This is not +# necessary if git_multimail.py is in the same directory as this +# script): + +#LIBDIR = 'path/to/directory/containing/module' +#sys.path.insert(0, LIBDIR) + +import git_multimail + + +# It is possible to modify the output templates here; e.g.: + +#git_multimail.FOOTER_TEMPLATE = """\ +# +#-- \n\ +#This email was generated by the wonderful git-multimail tool. +#""" + + +# Specify which "git config" section contains the configuration for +# git-multimail: +config = git_multimail.Config('multimailhook') + + +# Select the type of environment: +environment = git_multimail.GenericEnvironment(config=config) +#environment = git_multimail.GitoliteEnvironment(config=config) + + +# Choose the method of sending emails based on the git config: +mailer = git_multimail.choose_mailer(config, environment) + +# Alternatively, you may hardcode the mailer using code like one of +# the following: + +# Use "/usr/sbin/sendmail -t" to send emails. The envelopesender +# argument is optional: +#mailer = git_multimail.SendMailer( +# command=['/usr/sbin/sendmail', '-t'], +# envelopesender='git-repo@example.com', +# ) + +# Use Python's smtplib to send emails. Both arguments are required. +#mailer = git_multimail.SMTPMailer( +# envelopesender='git-repo@example.com', +# # The smtpserver argument can also include a port number; e.g., +# # smtpserver='mail.example.com:25' +# smtpserver='mail.example.com', +# ) + +# OutputMailer is intended only for testing; it writes the emails to +# the specified file stream. +#mailer = git_multimail.OutputMailer(sys.stdout) + + +# Read changes from stdin and send notification emails: +git_multimail.run_as_post_receive_hook(environment, mailer) diff --git a/contrib/hooks/post-receive-email b/contrib/hooks/post-receive-email index 0e5b72d7f1..8ee410f843 100755 --- a/contrib/hooks/post-receive-email +++ b/contrib/hooks/post-receive-email @@ -2,10 +2,19 @@ # # 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. +# +# NOTE: This script is no longer under active development. There +# is another script, git-multimail, which is more capable and +# configurable and is largely backwards-compatible with this script; +# please see "contrib/hooks/multimail/". For instructions on how to +# migrate from post-receive-email to git-multimail, please see +# "README.migrate-from-post-receive-email" in that directory. +# +# 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 @@ -233,6 +242,9 @@ generate_email_header() cat <<-EOF To: $recipients Subject: ${emailprefix}$projectdesc $refname_type $short_refname ${change_type}d. $describe + MIME-Version: 1.0 + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 8bit X-Git-Refname: $refname X-Git-Reftype: $refname_type X-Git-Oldrev: $oldrev @@ -462,7 +474,7 @@ generate_delete_branch_email() echo " was $oldrev" echo "" echo $LOGBEGIN - git show -s --pretty=oneline $oldrev + git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev echo $LOGEND } @@ -538,11 +550,11 @@ generate_atag_email() # performed on them if [ -n "$prevtag" ]; then # Show changes since the previous release - git rev-list --pretty=short "$prevtag..$newrev" | git shortlog + git shortlog "$prevtag..$newrev" else # No previous tag, show all the changes since time # began - git rev-list --pretty=short $newrev | git shortlog + git shortlog $newrev fi ;; *) @@ -562,7 +574,7 @@ generate_delete_atag_email() echo " was $oldrev" echo "" echo $LOGBEGIN - git show -s --pretty=oneline $oldrev + git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev echo $LOGEND } @@ -608,7 +620,7 @@ generate_general_email() echo "" if [ "$newrev_type" = "commit" ]; then echo $LOGBEGIN - git show --no-color --root -s --pretty=medium $newrev + git diff-tree -s --always --encoding=UTF-8 --pretty=medium $newrev echo $LOGEND else # What can we do here? The tag marks an object that is not @@ -627,7 +639,7 @@ generate_delete_general_email() echo " was $oldrev" echo "" echo $LOGBEGIN - git show -s --pretty=oneline $oldrev + git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev echo $LOGEND } diff --git a/contrib/mw-to-git/.gitignore b/contrib/mw-to-git/.gitignore index b9196555e5..ae545b013d 100644 --- a/contrib/mw-to-git/.gitignore +++ b/contrib/mw-to-git/.gitignore @@ -1 +1,2 @@ git-remote-mediawiki +git-mw diff --git a/contrib/mw-to-git/Git/Mediawiki.pm b/contrib/mw-to-git/Git/Mediawiki.pm index 805f42a49e..d13c4dfa7d 100644 --- a/contrib/mw-to-git/Git/Mediawiki.pm +++ b/contrib/mw-to-git/Git/Mediawiki.pm @@ -18,7 +18,83 @@ require Exporter; @EXPORT = (); # Methods which can be called as standalone functions as well: -@EXPORT_OK = (); +@EXPORT_OK = qw(clean_filename smudge_filename connect_maybe + EMPTY HTTP_CODE_OK HTTP_CODE_PAGE_NOT_FOUND); +} + +# Mediawiki filenames can contain forward slashes. This variable decides by which pattern they should be replaced +use constant SLASH_REPLACEMENT => '%2F'; + +# Used to test for empty strings +use constant EMPTY => q{}; + +# HTTP codes +use constant HTTP_CODE_OK => 200; +use constant HTTP_CODE_PAGE_NOT_FOUND => 404; + +sub clean_filename { + my $filename = shift; + $filename =~ s{@{[SLASH_REPLACEMENT]}}{/}g; + # [, ], |, {, and } are forbidden by MediaWiki, even URL-encoded. + # Do a variant of URL-encoding, i.e. looks like URL-encoding, + # but with _ added to prevent MediaWiki from thinking this is + # an actual special character. + $filename =~ s/[\[\]\{\}\|]/sprintf("_%%_%x", ord($&))/ge; + # If we use the uri escape before + # we should unescape here, before anything + + return $filename; +} + +sub smudge_filename { + my $filename = shift; + $filename =~ s{/}{@{[SLASH_REPLACEMENT]}}g; + $filename =~ s/ /_/g; + # Decode forbidden characters encoded in clean_filename + $filename =~ s/_%_([0-9a-fA-F][0-9a-fA-F])/sprintf('%c', hex($1))/ge; + return $filename; +} + +sub connect_maybe { + my $wiki = shift; + if ($wiki) { + return $wiki; + } + + my $remote_name = shift; + my $remote_url = shift; + my ($wiki_login, $wiki_password, $wiki_domain); + + $wiki_login = Git::config("remote.${remote_name}.mwLogin"); + $wiki_password = Git::config("remote.${remote_name}.mwPassword"); + $wiki_domain = Git::config("remote.${remote_name}.mwDomain"); + + $wiki = MediaWiki::API->new; + $wiki->{config}->{api_url} = "${remote_url}/api.php"; + if ($wiki_login) { + my %credential = ( + 'url' => $remote_url, + 'username' => $wiki_login, + 'password' => $wiki_password + ); + Git::credential(\%credential); + my $request = {lgname => $credential{username}, + lgpassword => $credential{password}, + lgdomain => $wiki_domain}; + if ($wiki->login($request)) { + Git::credential(\%credential, 'approve'); + print {*STDERR} qq(Logged in mediawiki user "$credential{username}".\n); + } else { + print {*STDERR} qq(Failed to log in mediawiki user "$credential{username}" on ${remote_url}\n); + print {*STDERR} ' (error ' . + $wiki->{error}->{code} . ': ' . + $wiki->{error}->{details} . ")\n"; + Git::credential(\%credential, 'reject'); + exit 1; + } + } + + return $wiki; } 1; # Famous last words diff --git a/contrib/mw-to-git/Makefile b/contrib/mw-to-git/Makefile index 37b68e2571..a4b6f7a2cd 100644 --- a/contrib/mw-to-git/Makefile +++ b/contrib/mw-to-git/Makefile @@ -2,12 +2,19 @@ # Copyright (C) 2013 # Matthieu Moy <Matthieu.Moy@imag.fr> # +# To build and test: +# +# make +# bin-wrapper/git mw preview Some_page.mw +# bin-wrapper/git clone mediawiki::http://example.com/wiki/ +# # To install, run Git's toplevel 'make install' then run: # # make install GIT_MEDIAWIKI_PM=Git/Mediawiki.pm SCRIPT_PERL=git-remote-mediawiki.perl +SCRIPT_PERL+=git-mw.perl GIT_ROOT_DIR=../.. HERE=contrib/mw-to-git/ @@ -21,22 +28,30 @@ INSTLIBDIR_SQ = $(subst ','\'',$(INSTLIBDIR)) all: build +test: all + $(MAKE) -C t + +check: perlcritic test + install_pm: $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(INSTLIBDIR_SQ)/Git' $(INSTALL) -m 644 $(GIT_MEDIAWIKI_PM) \ '$(DESTDIR_SQ)$(INSTLIBDIR_SQ)/$(GIT_MEDIAWIKI_PM)' build: - $(MAKE) -C $(GIT_ROOT_DIR) SCRIPT_PERL=$(SCRIPT_PERL_FULL) \ + $(MAKE) -C $(GIT_ROOT_DIR) SCRIPT_PERL="$(SCRIPT_PERL_FULL)" \ build-perl-script install: install_pm - $(MAKE) -C $(GIT_ROOT_DIR) SCRIPT_PERL=$(SCRIPT_PERL_FULL) \ + $(MAKE) -C $(GIT_ROOT_DIR) SCRIPT_PERL="$(SCRIPT_PERL_FULL)" \ install-perl-script clean: - $(MAKE) -C $(GIT_ROOT_DIR) SCRIPT_PERL=$(SCRIPT_PERL_FULL) \ + $(MAKE) -C $(GIT_ROOT_DIR) SCRIPT_PERL="$(SCRIPT_PERL_FULL)" \ clean-perl-script perlcritic: - perlcritic -2 *.perl + perlcritic -5 $(SCRIPT_PERL) + -perlcritic -2 $(SCRIPT_PERL) + +.PHONY: all test check install_pm install clean perlcritic diff --git a/contrib/mw-to-git/bin-wrapper/git b/contrib/mw-to-git/bin-wrapper/git new file mode 100755 index 0000000000..6663ae57e8 --- /dev/null +++ b/contrib/mw-to-git/bin-wrapper/git @@ -0,0 +1,14 @@ +#!/bin/sh + +# git executable wrapper script for Git-Mediawiki to run tests without +# installing all the scripts and perl packages. + +GIT_ROOT_DIR=../../.. +GIT_EXEC_PATH=$(cd "$(dirname "$0")" && cd ${GIT_ROOT_DIR} && pwd) + +GITPERLLIB="$GIT_EXEC_PATH"'/contrib/mw-to-git'"${GITPERLLIB:+:$GITPERLLIB}" +PATH="$GIT_EXEC_PATH"'/contrib/mw-to-git:'"$PATH" + +export GITPERLLIB PATH + +exec "${GIT_EXEC_PATH}/bin-wrappers/git" "$@" diff --git a/contrib/mw-to-git/git-mw.perl b/contrib/mw-to-git/git-mw.perl new file mode 100755 index 0000000000..28df3ee321 --- /dev/null +++ b/contrib/mw-to-git/git-mw.perl @@ -0,0 +1,368 @@ +#!/usr/bin/perl + +# Copyright (C) 2013 +# Benoit Person <benoit.person@ensimag.imag.fr> +# Celestin Matte <celestin.matte@ensimag.imag.fr> +# License: GPL v2 or later + +# Set of tools for git repo with a mediawiki remote. +# Documentation & bugtracker: https://github.com/moy/Git-Mediawiki/ + +use strict; +use warnings; + +use Getopt::Long; +use URI::URL qw(url); +use LWP::UserAgent; +use HTML::TreeBuilder; + +use Git; +use MediaWiki::API; +use Git::Mediawiki qw(clean_filename connect_maybe + EMPTY HTTP_CODE_PAGE_NOT_FOUND); + +# By default, use UTF-8 to communicate with Git and the user +binmode STDERR, ':encoding(UTF-8)'; +binmode STDOUT, ':encoding(UTF-8)'; + +# Global parameters +my $verbose = 0; +sub v_print { + if ($verbose) { + return print {*STDERR} @_; + } + return; +} + +# Preview parameters +my $file_name = EMPTY; +my $remote_name = EMPTY; +my $preview_file_name = EMPTY; +my $autoload = 0; +sub file { + $file_name = shift; + return $file_name; +} + +my %commands = ( + 'help' => + [\&help, {}, \&help], + 'preview' => + [\&preview, { + '<>' => \&file, + 'output|o=s' => \$preview_file_name, + 'remote|r=s' => \$remote_name, + 'autoload|a' => \$autoload + }, \&preview_help] +); + +# Search for sub-command +my $cmd = $commands{'help'}; +for (0..@ARGV-1) { + if (defined $commands{$ARGV[$_]}) { + $cmd = $commands{$ARGV[$_]}; + splice @ARGV, $_, 1; + last; + } +}; +GetOptions( %{$cmd->[1]}, + 'help|h' => \&{$cmd->[2]}, + 'verbose|v' => \$verbose); + +# Launch command +&{$cmd->[0]}; + +############################# Preview Functions ################################ + +sub preview_help { + print {*STDOUT} <<'END'; +USAGE: git mw preview [--remote|-r <remote name>] [--autoload|-a] + [--output|-o <output filename>] [--verbose|-v] + <blob> | <filename> + +DESCRIPTION: +Preview is an utiliy to preview local content of a mediawiki repo as if it was +pushed on the remote. + +For that, preview searches for the remote name of the current branch's +upstream if --remote is not set. If that remote is not found or if it +is not a mediawiki, it lists all mediawiki remotes configured and asks +you to replay your command with the --remote option set properly. + +Then, it searches for a file named 'filename'. If it's not found in +the current dir, it will assume it's a blob. + +The content retrieved in the file (or in the blob) will then be parsed +by the remote mediawiki and combined with a template retrieved from +the mediawiki. + +Finally, preview will save the HTML result in a file. and autoload it +in your default web browser if the option --autoload is present. + +OPTIONS: + -r <remote name>, --remote <remote name> + If the remote is a mediawiki, the template and the parse engine + used for the preview will be those of that remote. + If not, a list of valid remotes will be shown. + + -a, --autoload + Try to load the HTML output in a new tab (or new window) of your + default web browser. + + -o <output filename>, --output <output filename> + Change the HTML output filename. Default filename is based on the + input filename with its extension replaced by '.html'. + + -v, --verbose + Show more information on what's going on under the hood. +END + exit; +} + +sub preview { + my $wiki; + my ($remote_url, $wiki_page_name); + my ($new_content, $template); + my $file_content; + + if ($file_name eq EMPTY) { + die "Missing file argument, see `git mw help`\n"; + } + + v_print("### Selecting remote\n"); + if ($remote_name eq EMPTY) { + $remote_name = find_upstream_remote_name(); + if ($remote_name) { + $remote_url = mediawiki_remote_url_maybe($remote_name); + } + + if (! $remote_url) { + my @valid_remotes = find_mediawiki_remotes(); + + if ($#valid_remotes == 0) { + print {*STDERR} "No mediawiki remote in this repo. \n"; + exit 1; + } else { + my $remotes_list = join("\n\t", @valid_remotes); + print {*STDERR} <<"MESSAGE"; +There are multiple mediawiki remotes, which of: + ${remotes_list} +do you want ? Use the -r option to specify the remote. +MESSAGE + } + + exit 1; + } + } else { + if (!is_valid_remote($remote_name)) { + die "${remote_name} is not a remote\n"; + } + + $remote_url = mediawiki_remote_url_maybe($remote_name); + if (! $remote_url) { + die "${remote_name} is not a mediawiki remote\n"; + } + } + v_print("selected remote:\n\tname: ${remote_name}\n\turl: ${remote_url}\n"); + + $wiki = connect_maybe($wiki, $remote_name, $remote_url); + + # Read file content + if (! -e $file_name) { + $file_content = git_cmd_try { + Git::command('cat-file', 'blob', $file_name); } + "%s failed w/ code %d"; + + if ($file_name =~ /(.+):(.+)/) { + $file_name = $2; + } + } else { + open my $read_fh, "<", $file_name + or die "could not open ${file_name}: $!\n"; + $file_content = do { local $/ = undef; <$read_fh> }; + close $read_fh + or die "unable to close: $!\n"; + } + + v_print("### Retrieving template\n"); + ($wiki_page_name = clean_filename($file_name)) =~ s/\.[^.]+$//; + $template = get_template($remote_url, $wiki_page_name); + + v_print("### Parsing local content\n"); + $new_content = $wiki->api({ + action => 'parse', + text => $file_content, + title => $wiki_page_name + }, { + skip_encoding => 1 + }) or die "No response from remote mediawiki\n"; + $new_content = $new_content->{'parse'}->{'text'}->{'*'}; + + v_print("### Merging contents\n"); + if ($preview_file_name eq EMPTY) { + ($preview_file_name = $file_name) =~ s/\.[^.]+$/.html/; + } + open(my $save_fh, '>:encoding(UTF-8)', $preview_file_name) + or die "Could not open: $!\n"; + print {$save_fh} merge_contents($template, $new_content, $remote_url); + close($save_fh) + or die "Could not close: $!\n"; + + v_print("### Results\n"); + if ($autoload) { + v_print("Launching browser w/ file: ${preview_file_name}"); + system('git', 'web--browse', $preview_file_name); + } else { + print {*STDERR} "Preview file saved as: ${preview_file_name}\n"; + } + + exit; +} + +# uses global scope variable: $remote_name +sub merge_contents { + my $template = shift; + my $content = shift; + my $remote_url = shift; + my ($content_tree, $html_tree, $mw_content_text); + my $template_content_id = 'bodyContent'; + + $html_tree = HTML::TreeBuilder->new; + $html_tree->parse($template); + + $content_tree = HTML::TreeBuilder->new; + $content_tree->parse($content); + + $template_content_id = Git::config("remote.${remote_name}.mwIDcontent") + || $template_content_id; + v_print("Using '${template_content_id}' as the content ID\n"); + + $mw_content_text = $html_tree->look_down('id', $template_content_id); + if (!defined $mw_content_text) { + print {*STDERR} <<"CONFIG"; +Could not combine the new content with the template. You might want to +configure `mediawiki.IDContent` in your config: + git config --add remote.${remote_name}.mwIDcontent <id> +and re-run the command afterward. +CONFIG + exit 1; + } + $mw_content_text->delete_content(); + $mw_content_text->push_content($content_tree); + + make_links_absolute($html_tree, $remote_url); + + return $html_tree->as_HTML; +} + +sub make_links_absolute { + my $html_tree = shift; + my $remote_url = shift; + for (@{ $html_tree->extract_links() }) { + my ($link, $element, $attr) = @{ $_ }; + my $url = url($link)->canonical; + if ($url !~ /#/) { + $element->attr($attr, URI->new_abs($url, $remote_url)); + } + } + return $html_tree; +} + +sub is_valid_remote { + my $remote = shift; + my @remotes = git_cmd_try { + Git::command('remote') } + "%s failed w/ code %d"; + my $found_remote = 0; + foreach my $remote (@remotes) { + if ($remote eq $remote) { + $found_remote = 1; + last; + } + } + return $found_remote; +} + +sub find_mediawiki_remotes { + my @remotes = git_cmd_try { + Git::command('remote'); } + "%s failed w/ code %d"; + my $remote_url; + my @valid_remotes = (); + foreach my $remote (@remotes) { + $remote_url = mediawiki_remote_url_maybe($remote); + if ($remote_url) { + push(@valid_remotes, $remote); + } + } + return @valid_remotes; +} + +sub find_upstream_remote_name { + my $current_branch = git_cmd_try { + Git::command_oneline('symbolic-ref', '--short', 'HEAD') } + "%s failed w/ code %d"; + return Git::config("branch.${current_branch}.remote"); +} + +sub mediawiki_remote_url_maybe { + my $remote = shift; + + # Find remote url + my $remote_url = Git::config("remote.${remote}.url"); + if ($remote_url =~ s/mediawiki::(.*)/$1/) { + return url($remote_url)->canonical; + } + + return; +} + +sub get_template { + my $url = shift; + my $page_name = shift; + my ($req, $res, $code, $url_after); + + $req = LWP::UserAgent->new; + if ($verbose) { + $req->show_progress(1); + } + + $res = $req->get("${url}/index.php?title=${page_name}"); + if (!$res->is_success) { + $code = $res->code; + $url_after = $res->request()->uri(); # resolve all redirections + if ($code == HTTP_CODE_PAGE_NOT_FOUND) { + if ($verbose) { + print {*STDERR} <<"WARNING"; +Warning: Failed to retrieve '$page_name'. Create it on the mediawiki if you want +all the links to work properly. +Trying to use the mediawiki homepage as a fallback template ... +WARNING + } + + # LWP automatically redirects GET request + $res = $req->get("${url}/index.php"); + if (!$res->is_success) { + $url_after = $res->request()->uri(); # resolve all redirections + die "Failed to get homepage @ ${url_after} w/ code ${code}\n"; + } + } else { + die "Failed to get '${page_name}' @ ${url_after} w/ code ${code}\n"; + } + } + + return $res->decoded_content; +} + +############################## Help Functions ################################## + +sub help { + print {*STDOUT} <<'END'; +usage: git mw <command> <args> + +git mw commands are: + help Display help information about git mw + preview Parse and render local file into HTML +END + exit; +} diff --git a/contrib/mw-to-git/git-remote-mediawiki.perl b/contrib/mw-to-git/git-remote-mediawiki.perl index 71baf8ace8..3f8d993afa 100755 --- a/contrib/mw-to-git/git-remote-mediawiki.perl +++ b/contrib/mw-to-git/git-remote-mediawiki.perl @@ -14,6 +14,8 @@ use strict; use MediaWiki::API; use Git; +use Git::Mediawiki qw(clean_filename smudge_filename connect_maybe + EMPTY HTTP_CODE_OK); use DateTime::Format::ISO8601; use warnings; @@ -23,9 +25,6 @@ binmode STDOUT, ':encoding(UTF-8)'; use URI::Escape; -# Mediawiki filenames can contain forward slashes. This variable decides by which pattern they should be replaced -use constant SLASH_REPLACEMENT => '%2F'; - # It's not always possible to delete pages (may require some # privileges). Deleted pages are replaced with this content. use constant DELETED_CONTENT => "[[Category:Deleted]]\n"; @@ -40,8 +39,6 @@ use constant NULL_SHA1 => '0000000000000000000000000000000000000000'; # Used on Git's side to reflect empty edit messages on the wiki use constant EMPTY_MESSAGE => '*Empty MediaWiki Message*'; -use constant EMPTY => q{}; - # Number of pages taken into account at once in submodule get_mw_page_list use constant SLICE_SIZE => 50; @@ -50,8 +47,6 @@ use constant SLICE_SIZE => 50; # the number of links to be returned (500 links max). use constant BATCH_SIZE => 10; -use constant HTTP_CODE_OK => 200; - if (@ARGV != 2) { exit_error_usage(); } @@ -199,37 +194,6 @@ sub parse_command { # MediaWiki API instance, created lazily. my $mediawiki; -sub mw_connect_maybe { - if ($mediawiki) { - return; - } - $mediawiki = MediaWiki::API->new; - $mediawiki->{config}->{api_url} = "${url}/api.php"; - if ($wiki_login) { - my %credential = ( - 'url' => $url, - 'username' => $wiki_login, - 'password' => $wiki_passwd - ); - Git::credential(\%credential); - my $request = {lgname => $credential{username}, - lgpassword => $credential{password}, - lgdomain => $wiki_domain}; - if ($mediawiki->login($request)) { - Git::credential(\%credential, 'approve'); - print {*STDERR} qq(Logged in mediawiki user "$credential{username}".\n); - } else { - print {*STDERR} qq(Failed to log in mediawiki user "$credential{username}" on ${url}\n); - print {*STDERR} ' (error ' . - $mediawiki->{error}->{code} . ': ' . - $mediawiki->{error}->{details} . ")\n"; - Git::credential(\%credential, 'reject'); - exit 1; - } - } - return; -} - sub fatal_mw_error { my $action = shift; print STDERR "fatal: could not $action.\n"; @@ -339,7 +303,7 @@ sub get_mw_first_pages { # Get the list of pages to be fetched according to configuration. sub get_mw_pages { - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); print {*STDERR} "Listing pages on remote wiki...\n"; @@ -529,7 +493,7 @@ sub get_last_local_revision { # avoid a loop onto all tracked pages. This is useful for the fetch-by-rev # option. sub get_last_global_remote_rev { - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); my $query = { action => 'query', @@ -545,7 +509,7 @@ sub get_last_global_remote_rev { # Get the last remote revision concerning the tracked pages and the tracked # categories. sub get_last_remote_revision { - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); my %pages_hash = get_mw_pages(); my @pages = values(%pages_hash); @@ -601,29 +565,6 @@ sub mediawiki_smudge { return "${string}\n"; } -sub mediawiki_clean_filename { - my $filename = shift; - $filename =~ s{@{[SLASH_REPLACEMENT]}}{/}g; - # [, ], |, {, and } are forbidden by MediaWiki, even URL-encoded. - # Do a variant of URL-encoding, i.e. looks like URL-encoding, - # but with _ added to prevent MediaWiki from thinking this is - # an actual special character. - $filename =~ s/[\[\]\{\}\|]/sprintf("_%%_%x", ord($&))/ge; - # If we use the uri escape before - # we should unescape here, before anything - - return $filename; -} - -sub mediawiki_smudge_filename { - my $filename = shift; - $filename =~ s{/}{@{[SLASH_REPLACEMENT]}}g; - $filename =~ s/ /_/g; - # Decode forbidden characters encoded in mediawiki_clean_filename - $filename =~ s/_%_([0-9a-fA-F][0-9a-fA-F])/sprintf('%c', hex($1))/ge; - return $filename; -} - sub literal_data { my ($content) = @_; print {*STDOUT} 'data ', bytes::length($content), "\n", $content; @@ -635,9 +576,9 @@ sub literal_data_raw { my ($content) = @_; # Avoid confusion between size in bytes and in characters utf8::downgrade($content); - binmode {*STDOUT}, ':raw'; + binmode STDOUT, ':raw'; print {*STDOUT} 'data ', bytes::length($content), "\n", $content; - binmode {*STDOUT}, ':encoding(UTF-8)'; + binmode STDOUT, ':encoding(UTF-8)'; return; } @@ -649,6 +590,9 @@ sub mw_capabilities { print {*STDOUT} "import\n"; print {*STDOUT} "list\n"; print {*STDOUT} "push\n"; + if ($dumb_push) { + print {*STDOUT} "no-private-update\n"; + } print {*STDOUT} "\n"; return; } @@ -681,6 +625,9 @@ sub fetch_mw_revisions_for_page { rvstartid => $fetch_from, rvlimit => 500, pageids => $id, + + # Let MediaWiki know that we support the latest API. + continue => '', }; my $revnum = 0; @@ -696,8 +643,15 @@ sub fetch_mw_revisions_for_page { push(@page_revs, $page_rev_ids); $revnum++; } - last if (!$result->{'query-continue'}); - $query->{rvstartid} = $result->{'query-continue'}->{revisions}->{rvstartid}; + + if ($result->{'query-continue'}) { # For legacy APIs + $query->{rvstartid} = $result->{'query-continue'}->{revisions}->{rvstartid}; + } elsif ($result->{continue}) { # For newer APIs + $query->{rvstartid} = $result->{continue}->{rvcontinue}; + $query->{continue} = $result->{continue}->{continue}; + } else { + last; + } } if ($shallow_import && @page_revs) { print {*STDERR} " Found 1 revision (shallow import).\n"; @@ -831,7 +785,7 @@ sub mw_import_ref { return; } - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); print {*STDERR} "Searching revisions...\n"; my $last_local = get_last_local_revision(); @@ -945,7 +899,7 @@ sub mw_import_revids { my %commit; $commit{author} = $rev->{user} || 'Anonymous'; $commit{comment} = $rev->{comment} || EMPTY_MESSAGE; - $commit{title} = mediawiki_smudge_filename($page_title); + $commit{title} = smudge_filename($page_title); $commit{mw_revision} = $rev->{revid}; $commit{content} = mediawiki_smudge($rev->{'*'}); @@ -1006,7 +960,7 @@ sub mw_upload_file { } # Deleting and uploading a file requires a priviledged user if ($file_deleted) { - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); my $query = { action => 'delete', title => $path, @@ -1022,7 +976,7 @@ sub mw_upload_file { # Don't let perl try to interpret file content as UTF-8 => use "raw" my $content = run_git("cat-file blob ${new_sha1}", 'raw'); if ($content ne EMPTY) { - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); $mediawiki->{config}->{upload_url} = "${url}/index.php/Special:Upload"; $mediawiki->edit({ @@ -1070,7 +1024,7 @@ sub mw_push_file { my $old_sha1 = $diff_info_split[2]; my $page_created = ($old_sha1 eq NULL_SHA1); my $page_deleted = ($new_sha1 eq NULL_SHA1); - $complete_file_name = mediawiki_clean_filename($complete_file_name); + $complete_file_name = clean_filename($complete_file_name); my ($title, $extension) = $complete_file_name =~ /^(.*)\.([^\.]*)$/; if (!defined($extension)) { @@ -1093,7 +1047,7 @@ sub mw_push_file { $file_content = run_git("cat-file blob ${new_sha1}"); } - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); my $result = $mediawiki->edit( { action => 'edit', @@ -1270,7 +1224,6 @@ sub mw_push_revision { } if (!$dumb_push) { run_git(qq(notes --ref=${remotename}/mediawiki add -f -m "mediawiki_revision: ${mw_revision}" ${sha1_commit})); - run_git(qq(update-ref -m "Git-MediaWiki push" refs/mediawiki/${remotename}/master ${sha1_commit} ${sha1_child})); } } @@ -1279,7 +1232,7 @@ sub mw_push_revision { } sub get_allowed_file_extensions { - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); my $query = { action => 'query', @@ -1303,7 +1256,7 @@ my %cached_mw_namespace_id; # Return MediaWiki id for a canonical namespace name. # Ex.: "File", "Project". sub get_mw_namespace_id { - mw_connect_maybe(); + $mediawiki = connect_maybe($mediawiki, $remotename, $url); my $name = shift; if (!exists $namespace_id{$name}) { @@ -1362,7 +1315,7 @@ sub get_mw_namespace_id { # Store "notANameSpace" as special value for inexisting namespaces my $store_id = ($id || 'notANameSpace'); - # Store explicitely requested namespaces on disk + # Store explicitly requested namespaces on disk if (!exists $cached_mw_namespace_id{$name}) { run_git(qq(config --add remote.${remotename}.namespaceCache "${name}:${store_id}")); $cached_mw_namespace_id{$name} = 1; diff --git a/contrib/mw-to-git/t/t9365-continuing-queries.sh b/contrib/mw-to-git/t/t9365-continuing-queries.sh new file mode 100755 index 0000000000..27e267f532 --- /dev/null +++ b/contrib/mw-to-git/t/t9365-continuing-queries.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +test_description='Test the Git Mediawiki remote helper: queries w/ more than 500 results' + +. ./test-gitmw-lib.sh +. $TEST_DIRECTORY/test-lib.sh + +test_check_precond + +test_expect_success 'creating page w/ >500 revisions' ' + wiki_reset && + for i in `test_seq 501` + do + echo "creating revision $i" && + wiki_editpage foo "revision $i<br/>" true + done +' + +test_expect_success 'cloning page w/ >500 revisions' ' + git clone mediawiki::'"$WIKI_URL"' mw_dir +' + +test_done diff --git a/contrib/mw-to-git/t/test-gitmw-lib.sh b/contrib/mw-to-git/t/test-gitmw-lib.sh index bb76cee379..3372b2af34 100755 --- a/contrib/mw-to-git/t/test-gitmw-lib.sh +++ b/contrib/mw-to-git/t/test-gitmw-lib.sh @@ -62,12 +62,8 @@ test_check_precond () { test_done fi - if [ ! -f "$GIT_BUILD_DIR"/git-remote-mediawiki ]; - then - echo "No remote mediawiki for git found. Copying it in git" - echo "cp $GIT_BUILD_DIR/contrib/mw-to-git/git-remote-mediawiki $GIT_BUILD_DIR/" - ln -s "$GIT_BUILD_DIR"/contrib/mw-to-git/git-remote-mediawiki "$GIT_BUILD_DIR" - fi + GIT_EXEC_PATH=$(cd "$(dirname "$0")" && cd "../.." && pwd) + PATH="$GIT_EXEC_PATH"'/bin-wrapper:'"$PATH" if [ ! -d "$WIKI_DIR_INST/$WIKI_DIR_NAME" ]; then @@ -95,7 +91,7 @@ test_diff_directories () { # Check that <dir> contains exactly <N> files test_contains_N_files () { if test `ls -- "$1" | wc -l` -ne "$2"; then - echo "directory $1 sould contain $2 files" + echo "directory $1 should contain $2 files" echo "it contains these files:" ls "$1" false diff --git a/contrib/mw-to-git/t/test.config b/contrib/mw-to-git/t/test.config index 4cfebe9c69..5ba0684162 100644 --- a/contrib/mw-to-git/t/test.config +++ b/contrib/mw-to-git/t/test.config @@ -12,7 +12,7 @@ SERVER_ADDR=localhost TMP=/tmp DB_FILE=wikidb.sqlite -# If LIGHTTPD is not set to true, the script will use the defaut +# If LIGHTTPD is not set to true, the script will use the default # web server running in WIKI_DIR_INST. WIKI_DIR_INST=/var/www diff --git a/contrib/remote-helpers/git-remote-bzr b/contrib/remote-helpers/git-remote-bzr index c3a3cac77b..054161ae21 100755 --- a/contrib/remote-helpers/git-remote-bzr +++ b/contrib/remote-helpers/git-remote-bzr @@ -13,8 +13,11 @@ # or # % git clone bzr::lp:myrepo # -# If you want to specify which branches you want track (per repo): -# git config remote-bzr.branches 'trunk, devel, test' +# If you want to specify which branches you want to track (per repo): +# % git config remote.origin.bzr-branches 'trunk, devel, test' +# +# Where 'origin' is the name of the repository you want to specify the +# branches. # import sys @@ -168,17 +171,16 @@ class Parser: if not m: return None _, name, email, date, tz = m.groups() + name = name.decode('utf-8') committer = '%s <%s>' % (name, email) tz = int(tz) tz = ((tz / 100) * 3600) + ((tz % 100) * 60) return (committer, int(date), tz) def rev_to_mark(rev): - global marks return marks.from_rev(rev) def mark_to_rev(mark): - global marks return marks.to_rev(mark) def fixup_user(user): @@ -233,8 +235,6 @@ def get_filechanges(cur, prev): return modified, removed def export_files(tree, files): - global marks, filenodes - final = [] for path, fid in files.iteritems(): kind = tree.kind(fid) @@ -276,8 +276,6 @@ def export_files(tree, files): return final def export_branch(repo, name): - global prefix - ref = '%s/heads/%s' % (prefix, name) tip = marks.get_tip(name) @@ -378,16 +376,12 @@ def export_branch(repo, name): marks.set_tip(name, revid) def export_tag(repo, name): - global tags, prefix - ref = '%s/tags/%s' % (prefix, name) print "reset %s" % ref print "from :%u" % rev_to_mark(tags[name]) print def do_import(parser): - global dirname - repo = parser.repo path = os.path.join(dirname, 'marks-git') @@ -413,8 +407,6 @@ def do_import(parser): sys.stdout.flush() def parse_blob(parser): - global blob_marks - parser.next() mark = parser.get_mark() parser.next() @@ -425,8 +417,6 @@ def parse_blob(parser): class CustomTree(): def __init__(self, branch, revid, parents, files): - global files_cache - self.updates = {} self.branch = branch @@ -484,7 +474,7 @@ class CustomTree(): add_entry(fid, dirname, 'directory') return fid - def add_entry(fid, path, kind, mode = None): + def add_entry(fid, path, kind, mode=None): dirname, basename = os.path.split(path) parent_fid = get_parent(dirname, basename) @@ -505,7 +495,7 @@ class CustomTree(): self.files[path] = [change[0], None] changes.append(change) - def update_entry(fid, path, kind, mode = None): + def update_entry(fid, path, kind, mode=None): dirname, basename = os.path.split(path) parent_fid = get_parent(dirname, basename) @@ -583,9 +573,6 @@ def c_style_unescape(string): return string def parse_commit(parser): - global marks, blob_marks, parsed_refs - global mode - parents = [] ref = parser[1] @@ -657,8 +644,6 @@ def parse_commit(parser): marks.new_mark(revid, commit_mark) def parse_reset(parser): - global parsed_refs - ref = parser[1] parser.next() @@ -674,8 +659,6 @@ def parse_reset(parser): parsed_refs[ref] = mark_to_rev(from_mark) def do_export(parser): - global parsed_refs, dirname - parser.next() for line in parser.each_block('done'): @@ -699,7 +682,8 @@ def do_export(parser): branch.generate_revision_history(revid, marks.get_tip(name)) if name in peers: - peer = bzrlib.branch.Branch.open(peers[name]) + peer = bzrlib.branch.Branch.open(peers[name], + possible_transports=transports) try: peer.bzrdir.push_branch(branch, revision_id=revid) except bzrlib.errors.DivergedBranches: @@ -724,8 +708,6 @@ def do_export(parser): print def do_capabilities(parser): - global dirname - print "import" print "export" print "refspec refs/heads/*:%s/heads/*" % prefix @@ -743,8 +725,6 @@ def ref_is_valid(name): return not True in [c in name for c in '~^: \\'] def do_list(parser): - global tags - master_branch = None for name in branches: @@ -770,24 +750,24 @@ def do_list(parser): def clone(path, remote_branch): try: - bdir = bzrlib.bzrdir.BzrDir.create(path) + bdir = bzrlib.bzrdir.BzrDir.create(path, possible_transports=transports) except bzrlib.errors.AlreadyControlDirError: - bdir = bzrlib.bzrdir.BzrDir.open(path) + bdir = bzrlib.bzrdir.BzrDir.open(path, possible_transports=transports) repo = bdir.find_repository() repo.fetch(remote_branch.repository) return remote_branch.sprout(bdir, repository=repo) def get_remote_branch(name): - global dirname, branches - - remote_branch = bzrlib.branch.Branch.open(branches[name]) + remote_branch = bzrlib.branch.Branch.open(branches[name], + possible_transports=transports) if isinstance(remote_branch.user_transport, bzrlib.transport.local.LocalTransport): return remote_branch branch_path = os.path.join(dirname, 'clone', name) try: - branch = bzrlib.branch.Branch.open(branch_path) + branch = bzrlib.branch.Branch.open(branch_path, + possible_transports=transports) except bzrlib.errors.NotBranchError: # clone branch = clone(branch_path, remote_branch) @@ -821,17 +801,17 @@ def find_branches(repo): yield name, branch.base def get_repo(url, alias): - global dirname, peer, branches - normal_url = bzrlib.urlutils.normalize_url(url) - origin = bzrlib.bzrdir.BzrDir.open(url) + origin = bzrlib.bzrdir.BzrDir.open(url, possible_transports=transports) is_local = isinstance(origin.transport, bzrlib.transport.local.LocalTransport) shared_path = os.path.join(gitdir, 'bzr') try: - shared_dir = bzrlib.bzrdir.BzrDir.open(shared_path) + shared_dir = bzrlib.bzrdir.BzrDir.open(shared_path, + possible_transports=transports) except bzrlib.errors.NotBranchError: - shared_dir = bzrlib.bzrdir.BzrDir.create(shared_path) + shared_dir = bzrlib.bzrdir.BzrDir.create(shared_path, + possible_transports=transports) try: shared_repo = shared_dir.open_repository() except bzrlib.errors.NoRepositoryPresent: @@ -844,16 +824,21 @@ def get_repo(url, alias): else: # check and remove old organization try: - bdir = bzrlib.bzrdir.BzrDir.open(clone_path) + bdir = bzrlib.bzrdir.BzrDir.open(clone_path, + possible_transports=transports) bdir.destroy_repository() except bzrlib.errors.NotBranchError: pass except bzrlib.errors.NoRepositoryPresent: pass - wanted = get_config('remote-bzr.branches').rstrip().split(', ') + wanted = get_config('remote.%s.bzr-branches' % alias).rstrip().split(', ') # stupid python wanted = [e for e in wanted if e] + if not wanted: + wanted = get_config('remote-bzr.branches').rstrip().split(', ') + # stupid python + wanted = [e for e in wanted if e] if not wanted: try: @@ -897,6 +882,7 @@ def main(args): global files_cache global is_tmp global branches, peers + global transports alias = args[1] url = args[2] @@ -909,6 +895,7 @@ def main(args): marks = None branches = {} peers = {} + transports = [] if alias[5:] == url: is_tmp = True diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index 0194c67fb1..c6026b9bed 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -23,8 +23,12 @@ import subprocess import urllib import atexit import urlparse, hashlib +import time as ptime # +# If you want to see Mercurial revisions as Git commit notes: +# git config core.notesRef refs/notes/hg +# # If you are not in hg-git-compat mode and want to disable the tracking of # named branches: # git config --global remote-hg.track-branches false @@ -126,6 +130,7 @@ class Marks: self.rev_marks = {} self.last_mark = 0 self.version = 0 + self.last_note = 0 def load(self): if not os.path.exists(self.path): @@ -137,6 +142,7 @@ class Marks: self.marks = tmp['marks'] self.last_mark = tmp['last-mark'] self.version = tmp.get('version', 1) + self.last_note = tmp.get('last-note', 0) for rev, mark in self.marks.iteritems(): self.rev_marks[mark] = rev @@ -150,7 +156,7 @@ class Marks: self.version = 2 def dict(self): - return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark, 'version' : self.version } + return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark, 'version' : self.version, 'last-note' : self.last_note } def store(self): json.dump(self.dict(), open(self.path, 'w')) @@ -227,8 +233,6 @@ class Parser: return sys.stdin.read(size) def get_author(self): - global bad_mail - ex = None m = RAW_AUTHOR_RE.match(self.line) if not m: @@ -261,8 +265,6 @@ def fix_file_path(path): return os.path.relpath(path, '/') def export_files(files): - global marks, filenodes - final = [] for f in files: fid = node.hex(f.filenode()) @@ -344,8 +346,6 @@ def fixup_user_hg(user): return (name, mail) def fixup_user(user): - global mode, bad_mail - if mode == 'git': name, mail = fixup_user_git(user) else: @@ -374,7 +374,7 @@ def updatebookmarks(repo, peer): bookmarks.write(repo) def get_repo(url, alias): - global dirname, peer + global peer myui = ui.ui() myui.setconfig('ui', 'interactive', 'off') @@ -391,11 +391,24 @@ def get_repo(url, alias): os.makedirs(dirname) else: shared_path = os.path.join(gitdir, 'hg') - if not os.path.exists(shared_path): - try: - hg.clone(myui, {}, url, shared_path, update=False, pull=True) - except: - die('Repository error') + + # check and upgrade old organization + hg_path = os.path.join(shared_path, '.hg') + if os.path.exists(shared_path) and not os.path.exists(hg_path): + repos = os.listdir(shared_path) + for x in repos: + local_hg = os.path.join(shared_path, x, 'clone', '.hg') + if not os.path.exists(local_hg): + continue + if not os.path.exists(hg_path): + shutil.move(local_hg, hg_path) + shutil.rmtree(os.path.join(shared_path, x, 'clone')) + + # setup shared repo (if not there) + try: + hg.peer(myui, {}, shared_path, create=True) + except error.RepoError: + pass if not os.path.exists(dirname): os.makedirs(dirname) @@ -416,16 +429,12 @@ def get_repo(url, alias): return repo def rev_to_mark(rev): - global marks return marks.from_rev(rev.hex()) def mark_to_rev(mark): - global marks return marks.to_rev(mark) def export_ref(repo, name, kind, head): - global prefix, marks, mode - ename = '%s/%s' % (kind, name) try: tip = marks.get_tip(ename) @@ -522,6 +531,31 @@ def export_ref(repo, name, kind, head): print "from :%u" % rev_to_mark(head) print + pending_revs = set(revs) - notes + if pending_revs: + note_mark = marks.next_mark() + ref = "refs/notes/hg" + + print "commit %s" % ref + print "mark :%d" % (note_mark) + print "committer remote-hg <> %s" % (ptime.strftime('%s %z')) + desc = "Notes for %s\n" % (name) + print "data %d" % (len(desc)) + print desc + if marks.last_note: + print "from :%u" % marks.last_note + + for rev in pending_revs: + notes.add(rev) + c = repo[rev] + print "N inline :%u" % rev_to_mark(c) + msg = c.hex() + print "data %d" % (len(msg)) + print msg + print + + marks.last_note = note_mark + marks.set_tip(ename, head.hex()) def export_tag(repo, tag): @@ -537,12 +571,9 @@ def export_branch(repo, branch): export_ref(repo, branch, 'branches', head) def export_head(repo): - global g_head export_ref(repo, g_head[0], 'bookmarks', g_head[1]) def do_capabilities(parser): - global prefix, dirname - print "import" print "export" print "refspec refs/heads/branches/*:%s/branches/*" % prefix @@ -562,8 +593,6 @@ def branch_tip(branch): return branches[branch][-1] def get_branch_tip(repo, branch): - global branches - heads = branches.get(hgref(branch), None) if not heads: return None @@ -576,7 +605,7 @@ def get_branch_tip(repo, branch): return heads[0] def list_head(repo, cur): - global g_head, bmarks, fake_bmark + global g_head, fake_bmark if 'default' not in branches: # empty repo @@ -592,8 +621,6 @@ def list_head(repo, cur): g_head = (head, node) def do_list(parser): - global branches, bmarks, track_branches - repo = parser.repo for bmark, node in bookmarks.listbookmarks(repo).iteritems(): bmarks[bmark] = repo[node] @@ -661,8 +688,6 @@ def do_import(parser): print 'done' def parse_blob(parser): - global blob_marks - parser.next() mark = parser.get_mark() parser.next() @@ -678,10 +703,12 @@ def get_merge_files(repo, p1, p2, files): f = { 'ctx' : repo[p1][e] } files[e] = f -def parse_commit(parser): - global marks, blob_marks, parsed_refs - global mode +def c_style_unescape(string): + if string[0] == string[-1] == '"': + return string.decode('string-escape')[1:-1] + return string +def parse_commit(parser): from_mark = merge_mark = None ref = parser[1] @@ -720,6 +747,7 @@ def parse_commit(parser): f = { 'deleted' : True } else: die('Unknown file command: %s' % line) + path = c_style_unescape(path) files[path] = f # only export the commits if we are on an internal proxy repo @@ -799,8 +827,6 @@ def parse_commit(parser): marks.new_mark(node, commit_mark) def parse_reset(parser): - global parsed_refs - ref = parser[1] parser.next() # ugh @@ -993,8 +1019,6 @@ def check_tip(ref, kind, name, heads): return tip in heads def do_export(parser): - global parsed_refs, bmarks, peer - p_bmarks = [] p_revs = {} @@ -1066,7 +1090,7 @@ def do_export(parser): author, msg = parsed_tags.get(tag, (None, None)) if mode == 'git': if not msg: - msg = 'Added tag %s for changeset %s' % (tag, node[:12]); + msg = 'Added tag %s for changeset %s' % (tag, node[:12]) tagnode, branch = write_tag(parser.repo, tag, node, msg, author) p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch) else: @@ -1124,7 +1148,7 @@ def do_option(parser): def fix_path(alias, repo, orig_url): url = urlparse.urlparse(orig_url, 'file') - if url.scheme != 'file' or os.path.isabs(url.path): + if url.scheme != 'file' or os.path.isabs(os.path.expanduser(url.path)): return abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url) cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url] @@ -1139,6 +1163,7 @@ def main(args): global filenodes global fake_bmark, hg_version global dry_run + global notes, alias alias = args[1] url = args[2] @@ -1178,6 +1203,7 @@ def main(args): except: hg_version = None dry_run = False + notes = set() repo = get_repo(url, alias) prefix = 'refs/hg/%s' % alias diff --git a/contrib/remote-helpers/test-bzr.sh b/contrib/remote-helpers/test-bzr.sh index dce281f911..5c50251783 100755 --- a/contrib/remote-helpers/test-bzr.sh +++ b/contrib/remote-helpers/test-bzr.sh @@ -7,19 +7,21 @@ test_description='Test remote-bzr' . ./test-lib.sh -if ! test_have_prereq PYTHON; then +if ! test_have_prereq PYTHON +then skip_all='skipping remote-bzr tests; python not available' test_done fi -if ! python -c 'import bzrlib'; then +if ! python -c 'import bzrlib' +then skip_all='skipping remote-bzr tests; bzr not available' test_done fi check () { - echo $3 > expected && - git --git-dir=$1/.git log --format='%s' -1 $2 > actual + echo $3 >expected && + git --git-dir=$1/.git log --format='%s' -1 $2 >actual test_cmp expected actual } @@ -29,7 +31,7 @@ test_expect_success 'cloning' ' ( bzr init bzrrepo && cd bzrrepo && - echo one > content && + echo one >content && bzr add content && bzr commit -m one ) && @@ -41,7 +43,7 @@ test_expect_success 'cloning' ' test_expect_success 'pulling' ' ( cd bzrrepo && - echo two > content && + echo two >content && bzr commit -m two ) && @@ -53,13 +55,13 @@ test_expect_success 'pulling' ' test_expect_success 'pushing' ' ( cd gitrepo && - echo three > content && + echo three >content && git commit -a -m three && git push ) && - echo three > expected && - cat bzrrepo/content > actual && + echo three >expected && + cat bzrrepo/content >actual && test_cmp expected actual ' @@ -67,16 +69,16 @@ test_expect_success 'roundtrip' ' ( cd gitrepo && git pull && - git log --format="%s" -1 origin/master > actual + git log --format="%s" -1 origin/master >actual ) && - echo three > expected && + echo three >expected && test_cmp expected actual && (cd gitrepo && git push && git pull) && ( cd bzrrepo && - echo four > content && + echo four >content && bzr commit -m four ) && @@ -86,19 +88,19 @@ test_expect_success 'roundtrip' ' ( cd gitrepo && - echo five > content && + echo five >content && git commit -a -m five && git push && git pull ) && (cd bzrrepo && bzr revert) && - echo five > expected && - cat bzrrepo/content > actual && + echo five >expected && + cat bzrrepo/content >actual && test_cmp expected actual ' -cat > expected <<EOF +cat >expected <<\EOF 100644 blob 54f9d6da5c91d556e6b54340b1327573073030af content 100755 blob 68769579c3eaadbe555379b9c3538e6628bae1eb executable 120000 blob 6b584e8ece562ebffc15d38808cd6b98fc3d97ea link @@ -107,7 +109,7 @@ EOF test_expect_success 'special modes' ' ( cd bzrrepo && - echo exec > executable + echo exec >executable chmod +x executable && bzr add executable bzr commit -m exec && @@ -122,21 +124,21 @@ test_expect_success 'special modes' ' ( cd gitrepo && git pull - git ls-tree HEAD > ../actual + git ls-tree HEAD >../actual ) && test_cmp expected actual && ( cd gitrepo && - git cat-file -p HEAD:link > ../actual + git cat-file -p HEAD:link >../actual ) && - printf content > expected && + printf content >expected && test_cmp expected actual ' -cat > expected <<EOF +cat >expected <<\EOF 100644 blob 54f9d6da5c91d556e6b54340b1327573073030af content 100755 blob 68769579c3eaadbe555379b9c3538e6628bae1eb executable 120000 blob 6b584e8ece562ebffc15d38808cd6b98fc3d97ea link @@ -147,8 +149,8 @@ test_expect_success 'moving directory' ' ( cd bzrrepo && mkdir movedir && - echo one > movedir/one && - echo two > movedir/two && + echo one >movedir/one && + echo two >movedir/two && bzr add movedir && bzr commit -m movedir && bzr mv movedir movedir-new && @@ -158,7 +160,7 @@ test_expect_success 'moving directory' ' ( cd gitrepo && git pull && - git ls-tree HEAD > ../actual + git ls-tree HEAD >../actual ) && test_cmp expected actual @@ -167,7 +169,7 @@ test_expect_success 'moving directory' ' test_expect_success 'different authors' ' ( cd bzrrepo && - echo john >> content && + echo john >>content && bzr commit -m john \ --author "Jane Rey <jrey@example.com>" \ --author "John Doe <jdoe@example.com>" @@ -176,10 +178,10 @@ test_expect_success 'different authors' ' ( cd gitrepo && git pull && - git show --format="%an <%ae>, %cn <%ce>" --quiet > ../actual + git show --format="%an <%ae>, %cn <%ce>" --quiet >../actual ) && - echo "Jane Rey <jrey@example.com>, A U Thor <author@example.com>" > expected && + echo "Jane Rey <jrey@example.com>, A U Thor <author@example.com>" >expected && test_cmp expected actual ' @@ -196,12 +198,12 @@ test_expect_success 'fetch utf-8 filenames' ' bzr init bzrrepo && cd bzrrepo && - echo test >> "ærø" && + echo test >>"ærø" && bzr add "ærø" && - echo test >> "ø~?" && + echo test >>"ø~?" && bzr add "ø~?" && bzr commit -m add-utf-8 && - echo test >> "ærø" && + echo test >>"ærø" && bzr commit -m test-utf-8 && bzr rm "ø~?" && bzr mv "ærø" "ø~?" && @@ -211,9 +213,9 @@ test_expect_success 'fetch utf-8 filenames' ' ( git clone "bzr::bzrrepo" gitrepo && cd gitrepo && - git -c core.quotepath=false ls-files > ../actual + git -c core.quotepath=false ls-files >../actual ) && - echo "ø~?" > expected && + echo "ø~?" >expected && test_cmp expected actual ' @@ -229,7 +231,7 @@ test_expect_success 'push utf-8 filenames' ' bzr init bzrrepo && cd bzrrepo && - echo one >> content && + echo one >>content && bzr add content && bzr commit -m one ) && @@ -238,15 +240,15 @@ test_expect_success 'push utf-8 filenames' ' git clone "bzr::bzrrepo" gitrepo && cd gitrepo && - echo test >> "ærø" && + echo test >>"ærø" && git add "ærø" && git commit -m utf-8 && git push ) && - (cd bzrrepo && bzr ls > ../actual) && - printf "content\nærø\n" > expected && + (cd bzrrepo && bzr ls >../actual) && + printf "content\nærø\n" >expected && test_cmp expected actual ' @@ -256,7 +258,7 @@ test_expect_success 'pushing a merge' ' ( bzr init bzrrepo && cd bzrrepo && - echo one > content && + echo one >content && bzr add content && bzr commit -m one ) && @@ -265,27 +267,27 @@ test_expect_success 'pushing a merge' ' ( cd bzrrepo && - echo two > content && + echo two >content && bzr commit -m two ) && ( cd gitrepo && - echo three > content && + echo three >content && git commit -a -m three && git fetch && git merge origin/master || true && - echo three > content && + echo three >content && git commit -a --no-edit && git push ) && - echo three > expected && - cat bzrrepo/content > actual && + echo three >expected && + cat bzrrepo/content >actual && test_cmp expected actual ' -cat > expected <<EOF +cat >expected <<\EOF origin/HEAD origin/branch origin/trunk @@ -299,7 +301,7 @@ test_expect_success 'proper bzr repo' ' ( bzr init bzrrepo/trunk && cd bzrrepo/trunk && - echo one >> content && + echo one >>content && bzr add content && bzr commit -m one ) && @@ -307,14 +309,14 @@ test_expect_success 'proper bzr repo' ' ( bzr branch bzrrepo/trunk bzrrepo/branch && cd bzrrepo/branch && - echo two >> content && + echo two >>content && bzr commit -m one ) && ( git clone "bzr::bzrrepo" gitrepo && cd gitrepo && - git for-each-ref --format "%(refname:short)" refs/remotes/origin > ../actual + git for-each-ref --format "%(refname:short)" refs/remotes/origin >../actual ) && test_cmp expected actual @@ -327,11 +329,11 @@ test_expect_success 'strip' ' bzr init bzrrepo && cd bzrrepo && - echo one >> content && + echo one >>content && bzr add content && bzr commit -m one && - echo two >> content && + echo two >>content && bzr commit -m two ) && @@ -341,21 +343,51 @@ test_expect_success 'strip' ' cd bzrrepo && bzr uncommit --force && - echo three >> content && + echo three >>content && bzr commit -m three && - echo four >> content && + echo four >>content && bzr commit -m four && - bzr log --line | sed -e "s/^[0-9][0-9]*: //" > ../expected + bzr log --line | sed -e "s/^[0-9][0-9]*: //" >../expected ) && ( cd gitrepo && git fetch && - git log --format="%an %ad %s" --date=short origin/master > ../actual + git log --format="%an %ad %s" --date=short origin/master >../actual ) && test_cmp expected actual ' +test_expect_success 'export utf-8 authors' ' + test_when_finished "rm -rf bzrrepo gitrepo && LC_ALL=C && unset GIT_COMMITTER_NAME" && + + LC_ALL=en_US.UTF-8 + export LC_ALL + + GIT_COMMITTER_NAME="Grégoire" + export GIT_COMMITTER_NAME + + bzr init bzrrepo && + + ( + git init gitrepo && + cd gitrepo && + echo greg >>content && + git add content && + git commit -m one && + git remote add bzr "bzr::../bzrrepo" && + git push bzr + ) && + + ( + cd bzrrepo && + bzr log | grep "^committer: " >../actual + ) && + + echo "committer: Grégoire <committer@example.com>" >expected && + test_cmp expected actual +' + test_done diff --git a/contrib/remote-helpers/test-hg-bidi.sh b/contrib/remote-helpers/test-hg-bidi.sh index f83d67d74f..e24c51daad 100755 --- a/contrib/remote-helpers/test-hg-bidi.sh +++ b/contrib/remote-helpers/test-hg-bidi.sh @@ -10,12 +10,14 @@ test_description='Test bidirectionality of remote-hg' . ./test-lib.sh -if ! test_have_prereq PYTHON; then +if ! test_have_prereq PYTHON +then skip_all='skipping remote-hg tests; python not available' test_done fi -if ! python -c 'import mercurial'; then +if ! python -c 'import mercurial' +then skip_all='skipping remote-hg tests; mercurial not available' test_done fi @@ -43,7 +45,7 @@ hg_push () { git checkout -q -b tmp && git fetch -q "hg::../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' && git checkout -q @{-1} && - git branch -q -D tmp 2> /dev/null || true + git branch -q -D tmp 2>/dev/null || true ) } @@ -62,7 +64,7 @@ setup () { echo "tag = -d \"0 0\"" echo "[extensions]" echo "graphlog =" - ) >> "$HOME"/.hgrc && + ) >>"$HOME"/.hgrc && git config --global remote-hg.hg-git-compat true git config --global remote-hg.track-branches true @@ -81,22 +83,22 @@ test_expect_success 'encoding' ' git init -q gitrepo && cd gitrepo && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add älphà" && GIT_AUTHOR_NAME="tést èncödîng" && export GIT_AUTHOR_NAME && - echo beta > beta && + echo beta >beta && git add beta && git commit -m "add beta" && - echo gamma > gamma && + echo gamma >gamma && git add gamma && git commit -m "add gämmâ" && : TODO git config i18n.commitencoding latin-1 && - echo delta > delta && + echo delta >delta && git add delta && git commit -m "add déltà" ) && @@ -105,8 +107,8 @@ test_expect_success 'encoding' ' git_clone hgrepo gitrepo2 && hg_clone gitrepo2 hgrepo2 && - HGENCODING=utf-8 hg_log hgrepo > expected && - HGENCODING=utf-8 hg_log hgrepo2 > actual && + HGENCODING=utf-8 hg_log hgrepo >expected && + HGENCODING=utf-8 hg_log hgrepo2 >actual && test_cmp expected actual ' @@ -117,14 +119,14 @@ test_expect_success 'file removal' ' ( git init -q gitrepo && cd gitrepo && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add alpha" && - echo beta > beta && + echo beta >beta && git add beta && git commit -m "add beta" mkdir foo && - echo blah > foo/bar && + echo blah >foo/bar && git add foo && git commit -m "add foo" && git rm alpha && @@ -137,8 +139,8 @@ test_expect_success 'file removal' ' git_clone hgrepo gitrepo2 && hg_clone gitrepo2 hgrepo2 && - hg_log hgrepo > expected && - hg_log hgrepo2 > actual && + hg_log hgrepo >expected && + hg_log hgrepo2 >actual && test_cmp expected actual ' @@ -150,12 +152,12 @@ test_expect_success 'git tags' ' git init -q gitrepo && cd gitrepo && git config receive.denyCurrentBranch ignore && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add alpha" && git tag alpha && - echo beta > beta && + echo beta >beta && git add beta && git commit -m "add beta" && git tag -a -m "added tag beta" beta @@ -165,8 +167,8 @@ test_expect_success 'git tags' ' git_clone hgrepo gitrepo2 && hg_clone gitrepo2 hgrepo2 && - hg_log hgrepo > expected && - hg_log hgrepo2 > actual && + hg_log hgrepo >expected && + hg_log hgrepo2 >actual && test_cmp expected actual ' @@ -178,7 +180,7 @@ test_expect_success 'hg branch' ' git init -q gitrepo && cd gitrepo && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -q -m "add alpha" && git checkout -q -b not-master @@ -201,8 +203,8 @@ test_expect_success 'hg branch' ' : Back to the common revision && (cd hgrepo && hg checkout default) && - hg_log hgrepo > expected && - hg_log hgrepo2 > actual && + hg_log hgrepo >expected && + hg_log hgrepo2 >actual && test_cmp expected actual ' @@ -214,7 +216,7 @@ test_expect_success 'hg tags' ' git init -q gitrepo && cd gitrepo && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add alpha" && git checkout -q -b not-master @@ -231,8 +233,8 @@ test_expect_success 'hg tags' ' hg_push hgrepo gitrepo && hg_clone gitrepo hgrepo2 && - hg_log hgrepo > expected && - hg_log hgrepo2 > actual && + hg_log hgrepo >expected && + hg_log hgrepo2 >actual && test_cmp expected actual ' diff --git a/contrib/remote-helpers/test-hg-hg-git.sh b/contrib/remote-helpers/test-hg-hg-git.sh index 2219284382..6dcd95d10f 100755 --- a/contrib/remote-helpers/test-hg-hg-git.sh +++ b/contrib/remote-helpers/test-hg-hg-git.sh @@ -10,17 +10,20 @@ test_description='Test remote-hg output compared to hg-git' . ./test-lib.sh -if ! test_have_prereq PYTHON; then +if ! test_have_prereq PYTHON +then skip_all='skipping remote-hg tests; python not available' test_done fi -if ! python -c 'import mercurial'; then +if ! python -c 'import mercurial' +then skip_all='skipping remote-hg tests; mercurial not available' test_done fi -if ! python -c 'import hggit'; then +if ! python -c 'import hggit' +then skip_all='skipping remote-hg tests; hg-git not available' test_done fi @@ -66,7 +69,7 @@ hg_push_git () { git fetch -q "hg::../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' && git branch -D default && git checkout -q @{-1} && - git branch -q -D tmp 2> /dev/null || true + git branch -q -D tmp 2>/dev/null || true ) } @@ -100,7 +103,7 @@ setup () { echo "hgext.bookmarks =" echo "hggit =" echo "graphlog =" - ) >> "$HOME"/.hgrc && + ) >>"$HOME"/.hgrc && git config --global receive.denycurrentbranch warn git config --global remote-hg.hg-git-compat true git config --global remote-hg.track-branches false @@ -121,7 +124,7 @@ test_expect_success 'executable bit' ' ( git init -q gitrepo && cd gitrepo && - echo alpha > alpha && + echo alpha >alpha && chmod 0644 alpha && git add alpha && git commit -m "add alpha" && @@ -133,17 +136,18 @@ test_expect_success 'executable bit' ' git commit -m "clear executable bit" ) && - for x in hg git; do + for x in hg git + do ( hg_clone_$x gitrepo hgrepo-$x && cd hgrepo-$x && hg_log . && hg manifest -r 1 -v && hg manifest -v - ) > output-$x && + ) >"output-$x" && git_clone_$x hgrepo-$x gitrepo2-$x && - git_log gitrepo2-$x > log-$x + git_log gitrepo2-$x >"log-$x" done && test_cmp output-hg output-git && @@ -156,7 +160,7 @@ test_expect_success 'symlink' ' ( git init -q gitrepo && cd gitrepo && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add alpha" && ln -s alpha beta && @@ -164,16 +168,17 @@ test_expect_success 'symlink' ' git commit -m "add beta" ) && - for x in hg git; do + for x in hg git + do ( hg_clone_$x gitrepo hgrepo-$x && cd hgrepo-$x && hg_log . && hg manifest -v - ) > output-$x && + ) >"output-$x" && git_clone_$x hgrepo-$x gitrepo2-$x && - git_log gitrepo2-$x > log-$x + git_log gitrepo2-$x >"log-$x" done && test_cmp output-hg output-git && @@ -186,28 +191,29 @@ test_expect_success 'merge conflict 1' ' ( hg init hgrepo1 && cd hgrepo1 && - echo A > afile && + echo A >afile && hg add afile && hg ci -m "origin" && - echo B > afile && + echo B >afile && hg ci -m "A->B" && hg up -r0 && - echo C > afile && + echo C >afile && hg ci -m "A->C" && hg merge -r1 && - echo C > afile && + echo C >afile && hg resolve -m afile && hg ci -m "merge to C" ) && - for x in hg git; do + for x in hg git + do git_clone_$x hgrepo1 gitrepo-$x && hg_clone_$x gitrepo-$x hgrepo2-$x && - hg_log hgrepo2-$x > hg-log-$x && - git_log gitrepo-$x > git-log-$x + hg_log hgrepo2-$x >"hg-log-$x" && + git_log gitrepo-$x >"git-log-$x" done && test_cmp hg-log-hg hg-log-git && @@ -220,28 +226,29 @@ test_expect_success 'merge conflict 2' ' ( hg init hgrepo1 && cd hgrepo1 && - echo A > afile && + echo A >afile && hg add afile && hg ci -m "origin" && - echo B > afile && + echo B >afile && hg ci -m "A->B" && hg up -r0 && - echo C > afile && + echo C >afile && hg ci -m "A->C" && hg merge -r1 || true && - echo B > afile && + echo B >afile && hg resolve -m afile && hg ci -m "merge to B" ) && - for x in hg git; do + for x in hg git + do git_clone_$x hgrepo1 gitrepo-$x && hg_clone_$x gitrepo-$x hgrepo2-$x && - hg_log hgrepo2-$x > hg-log-$x && - git_log gitrepo-$x > git-log-$x + hg_log hgrepo2-$x >"hg-log-$x" && + git_log gitrepo-$x >"git-log-$x" done && test_cmp hg-log-hg hg-log-git && @@ -254,29 +261,30 @@ test_expect_success 'converged merge' ' ( hg init hgrepo1 && cd hgrepo1 && - echo A > afile && + echo A >afile && hg add afile && hg ci -m "origin" && - echo B > afile && + echo B >afile && hg ci -m "A->B" && - echo C > afile && + echo C >afile && hg ci -m "B->C" && hg up -r0 && - echo C > afile && + echo C >afile && hg ci -m "A->C" && hg merge -r2 || true && hg ci -m "merge" ) && - for x in hg git; do + for x in hg git + do git_clone_$x hgrepo1 gitrepo-$x && hg_clone_$x gitrepo-$x hgrepo2-$x && - hg_log hgrepo2-$x > hg-log-$x && - git_log gitrepo-$x > git-log-$x + hg_log hgrepo2-$x >"hg-log-$x" && + git_log gitrepo-$x >"git-log-$x" done && test_cmp hg-log-hg hg-log-git && @@ -290,32 +298,33 @@ test_expect_success 'encoding' ' git init -q gitrepo && cd gitrepo && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add älphà" && GIT_AUTHOR_NAME="tést èncödîng" && export GIT_AUTHOR_NAME && - echo beta > beta && + echo beta >beta && git add beta && git commit -m "add beta" && - echo gamma > gamma && + echo gamma >gamma && git add gamma && git commit -m "add gämmâ" && : TODO git config i18n.commitencoding latin-1 && - echo delta > delta && + echo delta >delta && git add delta && git commit -m "add déltà" ) && - for x in hg git; do + for x in hg git + do hg_clone_$x gitrepo hgrepo-$x && git_clone_$x hgrepo-$x gitrepo2-$x && - HGENCODING=utf-8 hg_log hgrepo-$x > hg-log-$x && - git_log gitrepo2-$x > git-log-$x + HGENCODING=utf-8 hg_log hgrepo-$x >"hg-log-$x" && + git_log gitrepo2-$x >"git-log-$x" done && test_cmp hg-log-hg hg-log-git && @@ -328,14 +337,14 @@ test_expect_success 'file removal' ' ( git init -q gitrepo && cd gitrepo && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add alpha" && - echo beta > beta && + echo beta >beta && git add beta && git commit -m "add beta" mkdir foo && - echo blah > foo/bar && + echo blah >foo/bar && git add foo && git commit -m "add foo" && git rm alpha && @@ -344,17 +353,18 @@ test_expect_success 'file removal' ' git commit -m "remove foo/bar" ) && - for x in hg git; do + for x in hg git + do ( hg_clone_$x gitrepo hgrepo-$x && cd hgrepo-$x && hg_log . && hg manifest -r 3 && hg manifest - ) > output-$x && + ) >"output-$x" && git_clone_$x hgrepo-$x gitrepo2-$x && - git_log gitrepo2-$x > log-$x + git_log gitrepo2-$x >"log-$x" done && test_cmp output-hg output-git && @@ -368,20 +378,21 @@ test_expect_success 'git tags' ' git init -q gitrepo && cd gitrepo && git config receive.denyCurrentBranch ignore && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add alpha" && git tag alpha && - echo beta > beta && + echo beta >beta && git add beta && git commit -m "add beta" && git tag -a -m "added tag beta" beta ) && - for x in hg git; do + for x in hg git + do hg_clone_$x gitrepo hgrepo-$x && - hg_log hgrepo-$x > log-$x + hg_log hgrepo-$x >"log-$x" done && test_cmp log-hg log-git @@ -390,12 +401,13 @@ test_expect_success 'git tags' ' test_expect_success 'hg author' ' test_when_finished "rm -rf gitrepo* hgrepo*" && - for x in hg git; do + for x in hg git + do ( git init -q gitrepo-$x && cd gitrepo-$x && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add alpha" && git checkout -q -b not-master @@ -406,38 +418,38 @@ test_expect_success 'hg author' ' cd hgrepo-$x && hg co master && - echo beta > beta && + echo beta >beta && hg add beta && hg commit -u "test" -m "add beta" && - echo gamma >> beta && + echo gamma >>beta && hg commit -u "test <test@example.com> (comment)" -m "modify beta" && - echo gamma > gamma && + echo gamma >gamma && hg add gamma && hg commit -u "<test@example.com>" -m "add gamma" && - echo delta > delta && + echo delta >delta && hg add delta && hg commit -u "name<test@example.com>" -m "add delta" && - echo epsilon > epsilon && + echo epsilon >epsilon && hg add epsilon && hg commit -u "name <test@example.com" -m "add epsilon" && - echo zeta > zeta && + echo zeta >zeta && hg add zeta && hg commit -u " test " -m "add zeta" && - echo eta > eta && + echo eta >eta && hg add eta && hg commit -u "test < test@example.com >" -m "add eta" && - echo theta > theta && + echo theta >theta && hg add theta && hg commit -u "test >test@example.com>" -m "add theta" && - echo iota > iota && + echo iota >iota && hg add iota && hg commit -u "test <test <at> example <dot> com>" -m "add iota" ) && @@ -445,8 +457,8 @@ test_expect_success 'hg author' ' hg_push_$x hgrepo-$x gitrepo-$x && hg_clone_$x gitrepo-$x hgrepo2-$x && - hg_log hgrepo2-$x > hg-log-$x && - git_log gitrepo-$x > git-log-$x + hg_log hgrepo2-$x >"hg-log-$x" && + git_log gitrepo-$x >"git-log-$x" done && test_cmp hg-log-hg hg-log-git && @@ -456,12 +468,13 @@ test_expect_success 'hg author' ' test_expect_success 'hg branch' ' test_when_finished "rm -rf gitrepo* hgrepo*" && - for x in hg git; do + for x in hg git + do ( git init -q gitrepo-$x && cd gitrepo-$x && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -q -m "add alpha" && git checkout -q -b not-master @@ -481,8 +494,8 @@ test_expect_success 'hg branch' ' hg_push_$x hgrepo-$x gitrepo-$x && hg_clone_$x gitrepo-$x hgrepo2-$x && - hg_log hgrepo2-$x > hg-log-$x && - git_log gitrepo-$x > git-log-$x + hg_log hgrepo2-$x >"hg-log-$x" && + git_log gitrepo-$x >"git-log-$x" done && test_cmp hg-log-hg hg-log-git && @@ -492,12 +505,13 @@ test_expect_success 'hg branch' ' test_expect_success 'hg tags' ' test_when_finished "rm -rf gitrepo* hgrepo*" && - for x in hg git; do + for x in hg git + do ( git init -q gitrepo-$x && cd gitrepo-$x && - echo alpha > alpha && + echo alpha >alpha && git add alpha && git commit -m "add alpha" && git checkout -q -b not-master @@ -518,7 +532,7 @@ test_expect_success 'hg tags' ' git --git-dir=gitrepo-$x/.git tag -l && hg_log hgrepo2-$x && cat hgrepo2-$x/.hgtags - ) > output-$x + ) >"output-$x" done && test_cmp output-hg output-git diff --git a/contrib/remote-helpers/test-hg.sh b/contrib/remote-helpers/test-hg.sh index f7ce8aa853..72f745d63f 100755 --- a/contrib/remote-helpers/test-hg.sh +++ b/contrib/remote-helpers/test-hg.sh @@ -10,40 +10,44 @@ test_description='Test remote-hg' . ./test-lib.sh -if ! test_have_prereq PYTHON; then +if ! test_have_prereq PYTHON +then skip_all='skipping remote-hg tests; python not available' test_done fi -if ! python -c 'import mercurial'; then +if ! python -c 'import mercurial' +then skip_all='skipping remote-hg tests; mercurial not available' test_done fi check () { - echo $3 > expected && - git --git-dir=$1/.git log --format='%s' -1 $2 > actual + echo $3 >expected && + git --git-dir=$1/.git log --format='%s' -1 $2 >actual test_cmp expected actual } check_branch () { - if [ -n "$3" ]; then - echo $3 > expected && - hg -R $1 log -r $2 --template '{desc}\n' > actual && + if test -n "$3" + then + echo $3 >expected && + hg -R $1 log -r $2 --template '{desc}\n' >actual && test_cmp expected actual else - hg -R $1 branches > out && + hg -R $1 branches >out && ! grep $2 out fi } check_bookmark () { - if [ -n "$3" ]; then - echo $3 > expected && - hg -R $1 log -r "bookmark('$2')" --template '{desc}\n' > actual && + if test -n "$3" + then + echo $3 >expected && + hg -R $1 log -r "bookmark('$2')" --template '{desc}\n' >actual && test_cmp expected actual else - hg -R $1 bookmarks > out && + hg -R $1 bookmarks >out && ! grep $2 out fi } @@ -52,7 +56,7 @@ check_push () { local expected_ret=$1 ret=0 ref_ret=0 IFS=':' shift - git push origin "$@" 2> error + git push origin "$@" 2>error ret=$? cat error @@ -75,10 +79,10 @@ check_push () { grep "^ [a-f0-9]*\.\.[a-f0-9]* *${branch} -> ${branch}$" error || ref_ret=1 ;; esac - let 'ref_ret' && echo "match for '$branch' failed" && break + test $ref_ret -ne 0 && echo "match for '$branch' failed" && break done - if let 'expected_ret != ret || ref_ret' + if test $expected_ret -ne $ret -o $ref_ret -ne 0 then return 1 fi @@ -92,7 +96,7 @@ setup () { echo "username = H G Wells <wells@example.com>" echo "[extensions]" echo "mq =" - ) >> "$HOME"/.hgrc && + ) >>"$HOME"/.hgrc && GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0230" && GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" && @@ -107,7 +111,7 @@ test_expect_success 'cloning' ' ( hg init hgrepo && cd hgrepo && - echo zero > content && + echo zero >content && hg add content && hg commit -m zero ) && @@ -122,7 +126,7 @@ test_expect_success 'cloning with branches' ' ( cd hgrepo && hg branch next && - echo next > content && + echo next >content && hg commit -m next ) && @@ -137,7 +141,7 @@ test_expect_success 'cloning with bookmarks' ' cd hgrepo && hg checkout default && hg bookmark feature-a && - echo feature-a > content && + echo feature-a >content && hg commit -m feature-a ) && @@ -157,7 +161,7 @@ test_expect_success 'update bookmark' ' git clone "hg::hgrepo" gitrepo && cd gitrepo && git checkout --quiet devel && - echo devel > content && + echo devel >content && git commit -a -m devel && git push --quiet ) && @@ -172,7 +176,7 @@ test_expect_success 'new bookmark' ' git clone "hg::hgrepo" gitrepo && cd gitrepo && git checkout --quiet -b feature-b && - echo feature-b > content && + echo feature-b >content && git commit -a -m feature-b && git push --quiet origin feature-b ) && @@ -184,9 +188,9 @@ test_expect_success 'new bookmark' ' rm -rf hgrepo author_test () { - echo $1 >> content && + echo $1 >>content && hg commit -u "$2" -m "add $1" && - echo "$3" >> ../expected + echo "$3" >>../expected } test_expect_success 'authors' ' @@ -199,7 +203,7 @@ test_expect_success 'authors' ' touch content && hg add content && - > ../expected && + >../expected && author_test alpha "" "H G Wells <wells@example.com>" && author_test beta "test" "test <unknown>" && author_test beta "test <test@example.com> (comment)" "test <test@example.com>" && @@ -214,7 +218,7 @@ test_expect_success 'authors' ' ) && git clone "hg::hgrepo" gitrepo && - git --git-dir=gitrepo/.git log --reverse --format="%an <%ae>" > actual && + git --git-dir=gitrepo/.git log --reverse --format="%an <%ae>" >actual && test_cmp expected actual ' @@ -226,11 +230,11 @@ test_expect_success 'strip' ' hg init hgrepo && cd hgrepo && - echo one >> content && + echo one >>content && hg add content && hg commit -m one && - echo two >> content && + echo two >>content && hg commit -m two ) && @@ -240,20 +244,20 @@ test_expect_success 'strip' ' cd hgrepo && hg strip 1 && - echo three >> content && + echo three >>content && hg commit -m three && - echo four >> content && + echo four >>content && hg commit -m four ) && ( cd gitrepo && git fetch && - git log --format="%s" origin/master > ../actual + git log --format="%s" origin/master >../actual ) && - hg -R hgrepo log --template "{desc}\n" > expected && + hg -R hgrepo log --template "{desc}\n" >expected && test_cmp actual expected ' @@ -263,18 +267,18 @@ test_expect_success 'remote push with master bookmark' ' ( hg init hgrepo && cd hgrepo && - echo zero > content && + echo zero >content && hg add content && hg commit -m zero && hg bookmark master && - echo one > content && + echo one >content && hg commit -m one ) && ( git clone "hg::hgrepo" gitrepo && cd gitrepo && - echo two > content && + echo two >content && git commit -a -m two && git push ) && @@ -282,7 +286,7 @@ test_expect_success 'remote push with master bookmark' ' check_branch hgrepo default two ' -cat > expected <<EOF +cat >expected <<\EOF changeset: 0:6e2126489d3d tag: tip user: A U Thor <author@example.com> @@ -300,13 +304,13 @@ test_expect_success 'remote push from master branch' ' git init gitrepo && cd gitrepo && git remote add origin "hg::../hgrepo" && - echo one > content && + echo one >content && git add content && git commit -a -m one && git push origin master ) && - hg -R hgrepo log > actual && + hg -R hgrepo log >actual && cat actual && test_cmp expected actual && @@ -322,7 +326,7 @@ test_expect_success 'remote cloning' ' ( hg init hgrepo && cd hgrepo && - echo zero > content && + echo zero >content && hg add content && hg commit -m zero ) && @@ -343,7 +347,7 @@ test_expect_success 'remote update bookmark' ' git clone "hg::hgrepo" gitrepo && cd gitrepo && git checkout --quiet devel && - echo devel > content && + echo devel >content && git commit -a -m devel && git push --quiet ) && @@ -358,7 +362,7 @@ test_expect_success 'remote new bookmark' ' git clone "hg::hgrepo" gitrepo && cd gitrepo && git checkout --quiet -b feature-b && - echo feature-b > content && + echo feature-b >content && git commit -a -m feature-b && git push --quiet origin feature-b ) && @@ -374,15 +378,15 @@ test_expect_success 'remote push diverged' ' ( cd hgrepo && hg checkout default && - echo bump > content && + echo bump >content && hg commit -m bump ) && ( cd gitrepo && - echo diverge > content && + echo diverge >content && git commit -a -m diverged && - check_push 1 <<-EOF + check_push 1 <<-\EOF master:non-fast-forward EOF ) && @@ -403,16 +407,16 @@ test_expect_success 'remote update bookmark diverge' ' ( cd hgrepo && - echo "bump bookmark" > content && + echo "bump bookmark" >content && hg commit -m "bump bookmark" ) && ( cd gitrepo && git checkout --quiet diverge && - echo diverge > content && + echo diverge >content && git commit -a -m diverge && - check_push 1 <<-EOF + check_push 1 <<-\EOF diverge:fetch-first EOF ) && @@ -427,7 +431,7 @@ test_expect_success 'remote new bookmark multiple branch head' ' git clone "hg::hgrepo" gitrepo && cd gitrepo && git checkout --quiet -b feature-c HEAD^ && - echo feature-c > content && + echo feature-c >content && git commit -a -m feature-c && git push --quiet origin feature-c ) && @@ -442,20 +446,20 @@ setup_big_push () { ( hg init hgrepo && cd hgrepo && - echo zero > content && + echo zero >content && hg add content && hg commit -m zero && hg bookmark bad_bmark1 && - echo one > content && + echo one >content && hg commit -m one && hg bookmark bad_bmark2 && hg bookmark good_bmark && hg bookmark -i good_bmark && hg -q branch good_branch && - echo "good branch" > content && + echo "good branch" >content && hg commit -m "good branch" && hg -q branch bad_branch && - echo "bad branch" > content && + echo "bad branch" >content && hg commit -m "bad branch" ) && @@ -463,40 +467,40 @@ setup_big_push () { ( cd gitrepo && - echo two > content && + echo two >content && git commit -q -a -m two && git checkout -q good_bmark && - echo three > content && + echo three >content && git commit -q -a -m three && git checkout -q bad_bmark1 && git reset --hard HEAD^ && - echo four > content && + echo four >content && git commit -q -a -m four && git checkout -q bad_bmark2 && git reset --hard HEAD^ && - echo five > content && + echo five >content && git commit -q -a -m five && git checkout -q -b new_bmark master && - echo six > content && + echo six >content && git commit -q -a -m six && git checkout -q branches/good_branch && - echo seven > content && + echo seven >content && git commit -q -a -m seven && - echo eight > content && + echo eight >content && git commit -q -a -m eight && git checkout -q branches/bad_branch && git reset --hard HEAD^ && - echo nine > content && + echo nine >content && git commit -q -a -m nine && git checkout -q -b branches/new_branch master && - echo ten > content && + echo ten >content && git commit -q -a -m ten ) } @@ -509,7 +513,7 @@ test_expect_success 'remote big push' ' ( cd gitrepo && - check_push 1 --all <<-EOF + check_push 1 --all <<-\EOF master good_bmark branches/good_branch @@ -537,17 +541,17 @@ test_expect_success 'remote big push fetch first' ' ( hg init hgrepo && cd hgrepo && - echo zero > content && + echo zero >content && hg add content && hg commit -m zero && hg bookmark bad_bmark && hg bookmark good_bmark && hg bookmark -i good_bmark && hg -q branch good_branch && - echo "good branch" > content && + echo "good branch" >content && hg commit -m "good branch" && hg -q branch bad_branch && - echo "bad branch" > content && + echo "bad branch" >content && hg commit -m "bad branch" ) && @@ -556,39 +560,37 @@ test_expect_success 'remote big push fetch first' ' ( cd hgrepo && hg bookmark -f bad_bmark && - echo update_bmark > content && + echo update_bmark >content && hg commit -m "update bmark" ) && ( cd gitrepo && - echo two > content && + echo two >content && git commit -q -a -m two && git checkout -q good_bmark && - echo three > content && + echo three >content && git commit -q -a -m three && git checkout -q bad_bmark && - echo four > content && + echo four >content && git commit -q -a -m four && git checkout -q branches/bad_branch && - echo five > content && + echo five >content && git commit -q -a -m five && - check_push 1 --all <<-EOF + check_push 1 --all <<-\EOF && master good_bmark - new_bmark:new - new_branch:new bad_bmark:fetch-first branches/bad_branch:festch-first EOF git fetch && - check_push 1 --all <<-EOF + check_push 1 --all <<-\EOF master good_bmark bad_bmark:non-fast-forward @@ -605,7 +607,7 @@ test_expect_failure 'remote big push force' ' ( cd gitrepo && - check_push 0 --force --all <<-EOF + check_push 0 --force --all <<-\EOF master good_bmark branches/good_branch @@ -635,7 +637,7 @@ test_expect_failure 'remote big push dry-run' ' ( cd gitrepo && - check_push 0 --dry-run --all <<-EOF + check_push 1 --dry-run --all <<-\EOF && master good_bmark branches/good_branch @@ -646,7 +648,7 @@ test_expect_failure 'remote big push dry-run' ' branches/bad_branch:non-fast-forward EOF - check_push 0 --dry-run master good_bmark new_bmark branches/good_branch branches/new_branch <<-EOF + check_push 0 --dry-run master good_bmark new_bmark branches/good_branch branches/new_branch <<-\EOF master good_bmark branches/good_branch @@ -671,10 +673,10 @@ test_expect_success 'remote double failed push' ' ( hg init hgrepo && cd hgrepo && - echo zero > content && + echo zero >content && hg add content && hg commit -m zero && - echo one > content && + echo one >content && hg commit -m one ) && @@ -682,7 +684,7 @@ test_expect_success 'remote double failed push' ' git clone "hg::hgrepo" gitrepo && cd gitrepo && git reset --hard HEAD^ && - echo two > content && + echo two >content && git commit -a -m two && test_expect_code 1 git push && test_expect_code 1 git push diff --git a/contrib/subtree/Makefile b/contrib/subtree/Makefile index b50750565f..4030a16898 100644 --- a/contrib/subtree/Makefile +++ b/contrib/subtree/Makefile @@ -21,15 +21,17 @@ GIT_SUBTREE := git-subtree GIT_SUBTREE_DOC := git-subtree.1 GIT_SUBTREE_XML := git-subtree.xml GIT_SUBTREE_TXT := git-subtree.txt +GIT_SUBTREE_HTML := git-subtree.html all: $(GIT_SUBTREE) $(GIT_SUBTREE): $(GIT_SUBTREE_SH) cp $< $@ && chmod +x $@ -doc: $(GIT_SUBTREE_DOC) +doc: $(GIT_SUBTREE_DOC) $(GIT_SUBTREE_HTML) install: $(GIT_SUBTREE) + $(INSTALL) -d -m 755 $(DESTDIR)$(libexecdir) $(INSTALL) -m 755 $(GIT_SUBTREE) $(DESTDIR)$(libexecdir) install-doc: install-man @@ -45,6 +47,10 @@ $(GIT_SUBTREE_XML): $(GIT_SUBTREE_TXT) asciidoc -b docbook -d manpage -f $(ASCIIDOC_CONF) \ -agit_version=$(gitver) $^ +$(GIT_SUBTREE_HTML): $(GIT_SUBTREE_TXT) + asciidoc -b xhtml11 -d manpage -f $(ASCIIDOC_CONF) \ + -agit_version=$(gitver) $^ + test: $(MAKE) -C t/ test diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh index 51ae932e5e..7d7af03274 100755 --- a/contrib/subtree/git-subtree.sh +++ b/contrib/subtree/git-subtree.sh @@ -311,7 +311,7 @@ copy_commit() GIT_COMMITTER_NAME \ GIT_COMMITTER_EMAIL \ GIT_COMMITTER_DATE - (echo -n "$annotate"; cat ) | + (printf "%s" "$annotate"; cat ) | git commit-tree "$2" $3 # reads the rest of stdin ) || die "Can't copy commit $1" } diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt index 7ba853eeda..e0957eee55 100644 --- a/contrib/subtree/git-subtree.txt +++ b/contrib/subtree/git-subtree.txt @@ -270,7 +270,7 @@ git-extensions repository in ~/git-extensions/: name You can omit the --squash flag, but doing so will increase the number -of commits that are incldued in your local repository. +of commits that are included in your local repository. We now have a ~/git-extensions/git-subtree directory containing code from the master branch of git://github.com/apenwarr/git-subtree.git diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh index b0f8536fca..66ce4b07c2 100755 --- a/contrib/subtree/t/t7900-subtree.sh +++ b/contrib/subtree/t/t7900-subtree.sh @@ -182,9 +182,9 @@ test_expect_success 'merge new subproj history into subdir' ' test_expect_success 'Check that prefix argument is required for split' ' echo "You must provide the --prefix option." > expected && test_must_fail git subtree split > actual 2>&1 && - test_debug "echo -n expected: " && + test_debug "printf '"'"'expected: '"'"'" && test_debug "cat expected" && - test_debug "echo -n actual: " && + test_debug "printf '"'"'actual: '"'"'" && test_debug "cat actual" && test_cmp expected actual && rm -f expected actual @@ -193,9 +193,9 @@ test_expect_success 'Check that prefix argument is required for split' ' test_expect_success 'Check that the <prefix> exists for a split' ' echo "'"'"'non-existent-directory'"'"'" does not exist\; use "'"'"'git subtree add'"'"'" > expected && test_must_fail git subtree split --prefix=non-existent-directory > actual 2>&1 && - test_debug "echo -n expected: " && + test_debug "printf '"'"'expected: '"'"'" && test_debug "cat expected" && - test_debug "echo -n actual: " && + test_debug "printf '"'"'actual: '"'"'" && test_debug "cat actual" && test_cmp expected actual # rm -f expected actual |