summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rw-r--r--contrib/coccinelle/.gitignore1
-rw-r--r--contrib/coccinelle/README2
-rw-r--r--contrib/coccinelle/array.cocci26
-rw-r--r--contrib/coccinelle/free.cocci5
-rw-r--r--contrib/coccinelle/object_id.cocci110
-rw-r--r--contrib/coccinelle/strbuf.cocci40
-rw-r--r--contrib/coccinelle/xstrdup_or_null.cocci7
-rw-r--r--contrib/completion/git-completion.bash132
-rw-r--r--contrib/completion/git-prompt.sh6
-rw-r--r--contrib/credential/gnome-keyring/Makefile5
-rw-r--r--contrib/diff-highlight/Makefile5
-rwxr-xr-xcontrib/diff-highlight/diff-highlight19
-rw-r--r--contrib/diff-highlight/t/.gitignore2
-rw-r--r--contrib/diff-highlight/t/Makefile22
-rwxr-xr-xcontrib/diff-highlight/t/t9400-diff-highlight.sh296
-rwxr-xr-xcontrib/fast-import/import-tars.perl31
-rw-r--r--contrib/git-jump/README6
-rwxr-xr-xcontrib/git-jump/git-jump8
-rw-r--r--contrib/hooks/multimail/CHANGES102
-rw-r--r--contrib/hooks/multimail/CONTRIBUTING.rst24
-rw-r--r--contrib/hooks/multimail/README292
-rw-r--r--contrib/hooks/multimail/README.Git4
-rw-r--r--contrib/hooks/multimail/doc/customizing-emails.rst56
-rw-r--r--contrib/hooks/multimail/doc/troubleshooting.rst78
-rwxr-xr-xcontrib/hooks/multimail/git_multimail.py1044
-rwxr-xr-xcontrib/hooks/multimail/post-receive.example6
-rw-r--r--contrib/mw-to-git/.perlcriticrc2
-rwxr-xr-xcontrib/mw-to-git/git-remote-mediawiki.perl2
-rw-r--r--contrib/persistent-https/Makefile6
-rwxr-xr-xcontrib/subtree/git-subtree.sh678
-rwxr-xr-xcontrib/subtree/t/t7900-subtree.sh34
31 files changed, 2316 insertions, 735 deletions
diff --git a/contrib/coccinelle/.gitignore b/contrib/coccinelle/.gitignore
new file mode 100644
index 0000000000..d3f29646dc
--- /dev/null
+++ b/contrib/coccinelle/.gitignore
@@ -0,0 +1 @@
+*.patch*
diff --git a/contrib/coccinelle/README b/contrib/coccinelle/README
new file mode 100644
index 0000000000..9c2f8879c2
--- /dev/null
+++ b/contrib/coccinelle/README
@@ -0,0 +1,2 @@
+This directory provides examples of Coccinelle (http://coccinelle.lip6.fr/)
+semantic patches that might be useful to developers.
diff --git a/contrib/coccinelle/array.cocci b/contrib/coccinelle/array.cocci
new file mode 100644
index 0000000000..2d7f25d99f
--- /dev/null
+++ b/contrib/coccinelle/array.cocci
@@ -0,0 +1,26 @@
+@@
+type T;
+T *dst;
+T *src;
+expression n;
+@@
+- memcpy(dst, src, n * sizeof(*dst));
++ COPY_ARRAY(dst, src, n);
+
+@@
+type T;
+T *dst;
+T *src;
+expression n;
+@@
+- memcpy(dst, src, n * sizeof(*src));
++ COPY_ARRAY(dst, src, n);
+
+@@
+type T;
+T *dst;
+T *src;
+expression n;
+@@
+- memcpy(dst, src, n * sizeof(T));
++ COPY_ARRAY(dst, src, n);
diff --git a/contrib/coccinelle/free.cocci b/contrib/coccinelle/free.cocci
new file mode 100644
index 0000000000..e28213161a
--- /dev/null
+++ b/contrib/coccinelle/free.cocci
@@ -0,0 +1,5 @@
+@@
+expression E;
+@@
+- if (E)
+ free(E);
diff --git a/contrib/coccinelle/object_id.cocci b/contrib/coccinelle/object_id.cocci
new file mode 100644
index 0000000000..09afdbf994
--- /dev/null
+++ b/contrib/coccinelle/object_id.cocci
@@ -0,0 +1,110 @@
+@@
+expression E1;
+@@
+- is_null_sha1(E1.hash)
++ is_null_oid(&E1)
+
+@@
+expression E1;
+@@
+- is_null_sha1(E1->hash)
++ is_null_oid(E1)
+
+@@
+expression E1;
+@@
+- sha1_to_hex(E1.hash)
++ oid_to_hex(&E1)
+
+@@
+identifier f != oid_to_hex;
+expression E1;
+@@
+ f(...) {...
+- sha1_to_hex(E1->hash)
++ oid_to_hex(E1)
+ ...}
+
+@@
+expression E1, E2;
+@@
+- sha1_to_hex_r(E1, E2.hash)
++ oid_to_hex_r(E1, &E2)
+
+@@
+identifier f != oid_to_hex_r;
+expression E1, E2;
+@@
+ f(...) {...
+- sha1_to_hex_r(E1, E2->hash)
++ oid_to_hex_r(E1, E2)
+ ...}
+
+@@
+expression E1;
+@@
+- hashclr(E1.hash)
++ oidclr(&E1)
+
+@@
+identifier f != oidclr;
+expression E1;
+@@
+ f(...) {...
+- hashclr(E1->hash)
++ oidclr(E1)
+ ...}
+
+@@
+expression E1, E2;
+@@
+- hashcmp(E1.hash, E2.hash)
++ oidcmp(&E1, &E2)
+
+@@
+identifier f != oidcmp;
+expression E1, E2;
+@@
+ f(...) {...
+- hashcmp(E1->hash, E2->hash)
++ oidcmp(E1, E2)
+ ...}
+
+@@
+expression E1, E2;
+@@
+- hashcmp(E1->hash, E2.hash)
++ oidcmp(E1, &E2)
+
+@@
+expression E1, E2;
+@@
+- hashcmp(E1.hash, E2->hash)
++ oidcmp(&E1, E2)
+
+@@
+expression E1, E2;
+@@
+- hashcpy(E1.hash, E2.hash)
++ oidcpy(&E1, &E2)
+
+@@
+identifier f != oidcpy;
+expression E1, E2;
+@@
+ f(...) {...
+- hashcpy(E1->hash, E2->hash)
++ oidcpy(E1, E2)
+ ...}
+
+@@
+expression E1, E2;
+@@
+- hashcpy(E1->hash, E2.hash)
++ oidcpy(E1, &E2)
+
+@@
+expression E1, E2;
+@@
+- hashcpy(E1.hash, E2->hash)
++ oidcpy(&E1, E2)
diff --git a/contrib/coccinelle/strbuf.cocci b/contrib/coccinelle/strbuf.cocci
new file mode 100644
index 0000000000..63995f22ff
--- /dev/null
+++ b/contrib/coccinelle/strbuf.cocci
@@ -0,0 +1,40 @@
+@ strbuf_addf_with_format_only @
+expression E;
+constant fmt;
+@@
+ strbuf_addf(E,
+(
+ fmt
+|
+ _(fmt)
+)
+ );
+
+@ script:python @
+fmt << strbuf_addf_with_format_only.fmt;
+@@
+cocci.include_match("%" not in fmt)
+
+@ extends strbuf_addf_with_format_only @
+@@
+- strbuf_addf
++ strbuf_addstr
+ (E,
+(
+ fmt
+|
+ _(fmt)
+)
+ );
+
+@@
+expression E1, E2;
+@@
+- strbuf_addf(E1, "%s", E2);
++ strbuf_addstr(E1, E2);
+
+@@
+expression E1, E2, E3;
+@@
+- strbuf_addstr(E1, find_unique_abbrev(E2, E3));
++ strbuf_add_unique_abbrev(E1, E2, E3);
diff --git a/contrib/coccinelle/xstrdup_or_null.cocci b/contrib/coccinelle/xstrdup_or_null.cocci
new file mode 100644
index 0000000000..3fceef132b
--- /dev/null
+++ b/contrib/coccinelle/xstrdup_or_null.cocci
@@ -0,0 +1,7 @@
+@@
+expression E;
+expression V;
+@@
+- if (E)
+- V = xstrdup(E);
++ V = xstrdup_or_null(E);
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 0e3841d835..21016bf8df 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -338,7 +338,7 @@ __git_tags ()
__git_refs ()
{
local i hash dir="$(__gitdir "${1-}")" track="${2-}"
- local format refs
+ local format refs pfx
if [ -d "$dir" ]; then
case "$cur" in
refs|refs/*)
@@ -347,14 +347,15 @@ __git_refs ()
track=""
;;
*)
+ [[ "$cur" == ^* ]] && pfx="^"
for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD; do
- if [ -e "$dir/$i" ]; then echo $i; fi
+ if [ -e "$dir/$i" ]; then echo $pfx$i; fi
done
format="refname:short"
refs="refs/tags refs/heads refs/remotes"
;;
esac
- git --git-dir="$dir" for-each-ref --format="%($format)" \
+ git --git-dir="$dir" for-each-ref --format="$pfx%($format)" \
$refs
if [ -n "$track" ]; then
# employ the heuristic used by git checkout
@@ -803,6 +804,50 @@ __git_find_on_cmdline ()
done
}
+# Echo the value of an option set on the command line or config
+#
+# $1: short option name
+# $2: long option name including =
+# $3: list of possible values
+# $4: config string (optional)
+#
+# example:
+# result="$(__git_get_option_value "-d" "--do-something=" \
+# "yes no" "core.doSomething")"
+#
+# result is then either empty (no option set) or "yes" or "no"
+#
+# __git_get_option_value requires 3 arguments
+__git_get_option_value ()
+{
+ local c short_opt long_opt val
+ local result= values config_key word
+
+ short_opt="$1"
+ long_opt="$2"
+ values="$3"
+ config_key="$4"
+
+ ((c = $cword - 1))
+ while [ $c -ge 0 ]; do
+ word="${words[c]}"
+ for val in $values; do
+ if [ "$short_opt$val" = "$word" ] ||
+ [ "$long_opt$val" = "$word" ]; then
+ result="$val"
+ break 2
+ fi
+ done
+ ((c--))
+ done
+
+ if [ -n "$config_key" ] && [ -z "$result" ]; then
+ result="$(git --git-dir="$(__gitdir)" config "$config_key")"
+ fi
+
+ echo "$result"
+}
+
__git_has_doubledash ()
{
local c=1
@@ -964,8 +1009,8 @@ _git_branch ()
while [ $c -lt $cword ]; do
i="${words[c]}"
case "$i" in
- -d|-m) only_local_ref="y" ;;
- -r) has_r="y" ;;
+ -d|--delete|-m|--move) only_local_ref="y" ;;
+ -r|--remotes) has_r="y" ;;
esac
((c++))
done
@@ -979,7 +1024,7 @@ _git_branch ()
--color --no-color --verbose --abbrev= --no-abbrev
--track --no-track --contains --merged --no-merged
--set-upstream-to= --edit-description --list
- --unset-upstream
+ --unset-upstream --delete --move --remotes
"
;;
*)
@@ -1092,12 +1137,15 @@ _git_clone ()
--depth
--single-branch
--branch
+ --recurse-submodules
"
return
;;
esac
}
+__git_untracked_file_modes="all no normal"
+
_git_commit ()
{
case "$prev" in
@@ -1119,7 +1167,7 @@ _git_commit ()
return
;;
--untracked-files=*)
- __gitcomp "all no normal" "" "${cur##--untracked-files=}"
+ __gitcomp "$__git_untracked_file_modes" "" "${cur##--untracked-files=}"
return
;;
--*)
@@ -1158,6 +1206,8 @@ _git_describe ()
__git_diff_algorithms="myers minimal patience histogram"
+__git_diff_submodule_formats="log short"
+
__git_diff_common_options="--stat --numstat --shortstat --summary
--patch-with-stat --name-only --name-status --color
--no-color --color-words --no-renames --check
@@ -1173,6 +1223,7 @@ __git_diff_common_options="--stat --numstat --shortstat --summary
--dirstat --dirstat= --dirstat-by-file
--dirstat-by-file= --cumulative
--diff-algorithm=
+ --submodule --submodule=
"
_git_diff ()
@@ -1184,6 +1235,10 @@ _git_diff ()
__gitcomp "$__git_diff_algorithms" "" "${cur##--diff-algorithm=}"
return
;;
+ --submodule=*)
+ __gitcomp "$__git_diff_submodule_formats" "" "${cur##--submodule=}"
+ return
+ ;;
--*)
__gitcomp "--cached --staged --pickaxe-all --pickaxe-regex
--base --ours --theirs --no-index
@@ -1447,6 +1502,14 @@ _git_log ()
__gitcomp "full short no" "" "${cur##--decorate=}"
return
;;
+ --diff-algorithm=*)
+ __gitcomp "$__git_diff_algorithms" "" "${cur##--diff-algorithm=}"
+ return
+ ;;
+ --submodule=*)
+ __gitcomp "$__git_diff_submodule_formats" "" "${cur##--submodule=}"
+ return
+ ;;
--*)
__gitcomp "
$__git_log_common_options
@@ -1780,6 +1843,56 @@ _git_stage ()
_git_add
}
+_git_status ()
+{
+ local complete_opt
+ local untracked_state
+
+ case "$cur" in
+ --ignore-submodules=*)
+ __gitcomp "none untracked dirty all" "" "${cur##--ignore-submodules=}"
+ return
+ ;;
+ --untracked-files=*)
+ __gitcomp "$__git_untracked_file_modes" "" "${cur##--untracked-files=}"
+ return
+ ;;
+ --column=*)
+ __gitcomp "
+ always never auto column row plain dense nodense
+ " "" "${cur##--column=}"
+ return
+ ;;
+ --*)
+ __gitcomp "
+ --short --branch --porcelain --long --verbose
+ --untracked-files= --ignore-submodules= --ignored
+ --column= --no-column
+ "
+ return
+ ;;
+ esac
+
+ untracked_state="$(__git_get_option_value "-u" "--untracked-files=" \
+ "$__git_untracked_file_modes" "status.showUntrackedFiles")"
+
+ case "$untracked_state" in
+ no)
+ # --ignored option does not matter
+ complete_opt=
+ ;;
+ all|normal|*)
+ complete_opt="--cached --directory --no-empty-directory --others"
+
+ if [ -n "$(__git_find_on_cmdline "--ignored")" ]; then
+ complete_opt="$complete_opt --ignored --exclude=*"
+ fi
+ ;;
+ esac
+
+ __git_complete_index_file "$complete_opt"
+}
+
__git_config_get_set_variables ()
{
local prevword word config_file= c=$cword
@@ -2085,6 +2198,7 @@ _git_config ()
format.attach
format.cc
format.coverLetter
+ format.from
format.headers
format.numbered
format.pretty
@@ -2359,6 +2473,10 @@ _git_show ()
__gitcomp "$__git_diff_algorithms" "" "${cur##--diff-algorithm=}"
return
;;
+ --submodule=*)
+ __gitcomp "$__git_diff_submodule_formats" "" "${cur##--submodule=}"
+ return
+ ;;
--*)
__gitcomp "--pretty= --format= --abbrev-commit --oneline
--show-signature
diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh
index 64219e631a..97eacd7832 100644
--- a/contrib/completion/git-prompt.sh
+++ b/contrib/completion/git-prompt.sh
@@ -355,8 +355,8 @@ __git_ps1 ()
# incorrect.)
#
local ps1_expanded=yes
- [ -z "$ZSH_VERSION" ] || [[ -o PROMPT_SUBST ]] || ps1_expanded=no
- [ -z "$BASH_VERSION" ] || shopt -q promptvars || ps1_expanded=no
+ [ -z "${ZSH_VERSION-}" ] || [[ -o PROMPT_SUBST ]] || ps1_expanded=no
+ [ -z "${BASH_VERSION-}" ] || shopt -q promptvars || ps1_expanded=no
local repo_info rev_parse_exit_code
repo_info="$(git rev-parse --git-dir --is-inside-git-dir \
@@ -368,7 +368,7 @@ __git_ps1 ()
return $exit
fi
- local short_sha
+ local short_sha=""
if [ "$rev_parse_exit_code" = "0" ]; then
short_sha="${repo_info##*$'\n'}"
repo_info="${repo_info%$'\n'*}"
diff --git a/contrib/credential/gnome-keyring/Makefile b/contrib/credential/gnome-keyring/Makefile
index c3c7c98aa1..22c19df94b 100644
--- a/contrib/credential/gnome-keyring/Makefile
+++ b/contrib/credential/gnome-keyring/Makefile
@@ -4,12 +4,13 @@ all:: $(MAIN)
CC = gcc
RM = rm -f
CFLAGS = -g -O2 -Wall
+PKG_CONFIG = pkg-config
-include ../../../config.mak.autogen
-include ../../../config.mak
-INCS:=$(shell pkg-config --cflags gnome-keyring-1 glib-2.0)
-LIBS:=$(shell pkg-config --libs gnome-keyring-1 glib-2.0)
+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/diff-highlight/Makefile b/contrib/diff-highlight/Makefile
new file mode 100644
index 0000000000..9018724524
--- /dev/null
+++ b/contrib/diff-highlight/Makefile
@@ -0,0 +1,5 @@
+# nothing to build
+all:
+
+test:
+ $(MAKE) -C t
diff --git a/contrib/diff-highlight/diff-highlight b/contrib/diff-highlight/diff-highlight
index ffefc31a98..81bd8040e3 100755
--- a/contrib/diff-highlight/diff-highlight
+++ b/contrib/diff-highlight/diff-highlight
@@ -21,6 +21,10 @@ my $RESET = "\x1b[m";
my $COLOR = qr/\x1b\[[0-9;]*m/;
my $BORING = qr/$COLOR|\s/;
+# The patch portion of git log -p --graph should only ever have preceding | and
+# not / or \ as merge history only shows up on the commit line.
+my $GRAPH = qr/$COLOR?\|$COLOR?\s+/;
+
my @removed;
my @added;
my $in_hunk;
@@ -32,12 +36,12 @@ $SIG{PIPE} = 'DEFAULT';
while (<>) {
if (!$in_hunk) {
print;
- $in_hunk = /^$COLOR*\@/;
+ $in_hunk = /^$GRAPH*$COLOR*\@\@ /;
}
- elsif (/^$COLOR*-/) {
+ elsif (/^$GRAPH*$COLOR*-/) {
push @removed, $_;
}
- elsif (/^$COLOR*\+/) {
+ elsif (/^$GRAPH*$COLOR*\+/) {
push @added, $_;
}
else {
@@ -46,7 +50,7 @@ while (<>) {
@added = ();
print;
- $in_hunk = /^$COLOR*[\@ ]/;
+ $in_hunk = /^$GRAPH*$COLOR*[\@ ]/;
}
# Most of the time there is enough output to keep things streaming,
@@ -163,6 +167,9 @@ sub highlight_pair {
}
}
+# we split either by $COLOR or by character. This has the side effect of
+# leaving in graph cruft. It works because the graph cruft does not contain "-"
+# or "+"
sub split_line {
local $_ = shift;
return utf8::decode($_) ?
@@ -211,8 +218,8 @@ sub is_pair_interesting {
my $suffix_a = join('', @$a[($sa+1)..$#$a]);
my $suffix_b = join('', @$b[($sb+1)..$#$b]);
- return $prefix_a !~ /^$COLOR*-$BORING*$/ ||
- $prefix_b !~ /^$COLOR*\+$BORING*$/ ||
+ return $prefix_a !~ /^$GRAPH*$COLOR*-$BORING*$/ ||
+ $prefix_b !~ /^$GRAPH*$COLOR*\+$BORING*$/ ||
$suffix_a !~ /^$BORING*$/ ||
$suffix_b !~ /^$BORING*$/;
}
diff --git a/contrib/diff-highlight/t/.gitignore b/contrib/diff-highlight/t/.gitignore
new file mode 100644
index 0000000000..7dcbb232cd
--- /dev/null
+++ b/contrib/diff-highlight/t/.gitignore
@@ -0,0 +1,2 @@
+/trash directory*
+/test-results
diff --git a/contrib/diff-highlight/t/Makefile b/contrib/diff-highlight/t/Makefile
new file mode 100644
index 0000000000..5ff5275496
--- /dev/null
+++ b/contrib/diff-highlight/t/Makefile
@@ -0,0 +1,22 @@
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+# copied from ../../t/Makefile
+SHELL_PATH ?= $(SHELL)
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
+
+all: test
+test: $(T)
+
+.PHONY: help clean all test $(T)
+
+help:
+ @echo 'Run "$(MAKE) test" to launch test scripts'
+ @echo 'Run "$(MAKE) clean" to remove trash folders'
+
+$(T):
+ @echo "*** $@ ***"; '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS)
+
+clean:
+ $(RM) -r 'trash directory'.*
diff --git a/contrib/diff-highlight/t/t9400-diff-highlight.sh b/contrib/diff-highlight/t/t9400-diff-highlight.sh
new file mode 100755
index 0000000000..3b43dbed74
--- /dev/null
+++ b/contrib/diff-highlight/t/t9400-diff-highlight.sh
@@ -0,0 +1,296 @@
+#!/bin/sh
+
+test_description='Test diff-highlight'
+
+CURR_DIR=$(pwd)
+TEST_OUTPUT_DIRECTORY=$(pwd)
+TEST_DIRECTORY="$CURR_DIR"/../../../t
+DIFF_HIGHLIGHT="$CURR_DIR"/../diff-highlight
+
+CW="$(printf "\033[7m")" # white
+CR="$(printf "\033[27m")" # reset
+
+. "$TEST_DIRECTORY"/test-lib.sh
+
+if ! test_have_prereq PERL
+then
+ skip_all='skipping diff-highlight tests; perl not available'
+ test_done
+fi
+
+# dh_test is a test helper function which takes 3 file names as parameters. The
+# first 2 files are used to generate diff and commit output, which is then
+# piped through diff-highlight. The 3rd file should contain the expected output
+# of diff-highlight (minus the diff/commit header, ie. everything after and
+# including the first @@ line).
+dh_test () {
+ a="$1" b="$2" &&
+
+ cat >patch.exp &&
+
+ {
+ cat "$a" >file &&
+ git add file &&
+ git commit -m "Add a file" &&
+
+ cat "$b" >file &&
+ git diff file >diff.raw &&
+ git commit -a -m "Update a file" &&
+ git show >commit.raw
+ } >/dev/null &&
+
+ "$DIFF_HIGHLIGHT" <diff.raw | test_strip_patch_header >diff.act &&
+ "$DIFF_HIGHLIGHT" <commit.raw | test_strip_patch_header >commit.act &&
+ test_cmp patch.exp diff.act &&
+ test_cmp patch.exp commit.act
+}
+
+test_strip_patch_header () {
+ sed -n '/^@@/,$p' $*
+}
+
+# dh_test_setup_history generates a contrived graph such that we have at least
+# 1 nesting (E) and 2 nestings (F).
+#
+# A branch
+# /
+# D---E---F master
+#
+# git log --all --graph
+# * commit
+# | A
+# | * commit
+# | | F
+# | * commit
+# |/
+# | E
+# * commit
+# D
+#
+dh_test_setup_history () {
+ echo "file1" >file1 &&
+ echo "file2" >file2 &&
+ echo "file3" >file3 &&
+
+ cat file1 >file &&
+ git add file &&
+ git commit -m "D" &&
+
+ git checkout -b branch &&
+ cat file2 >file &&
+ git commit -a -m "A" &&
+
+ git checkout master &&
+ cat file2 >file &&
+ git commit -a -m "E" &&
+
+ cat file3 >file &&
+ git commit -a -m "F"
+}
+
+left_trim () {
+ "$PERL_PATH" -pe 's/^\s+//'
+}
+
+trim_graph () {
+ # graphs start with * or |
+ # followed by a space or / or \
+ "$PERL_PATH" -pe 's@^((\*|\|)( |/|\\))+@@'
+}
+
+test_expect_success 'diff-highlight highlights the beginning of a line' '
+ cat >a <<-\EOF &&
+ aaa
+ bbb
+ ccc
+ EOF
+
+ cat >b <<-\EOF &&
+ aaa
+ 0bb
+ ccc
+ EOF
+
+ dh_test a b <<-EOF
+ @@ -1,3 +1,3 @@
+ aaa
+ -${CW}b${CR}bb
+ +${CW}0${CR}bb
+ ccc
+ EOF
+'
+
+test_expect_success 'diff-highlight highlights the end of a line' '
+ cat >a <<-\EOF &&
+ aaa
+ bbb
+ ccc
+ EOF
+
+ cat >b <<-\EOF &&
+ aaa
+ bb0
+ ccc
+ EOF
+
+ dh_test a b <<-EOF
+ @@ -1,3 +1,3 @@
+ aaa
+ -bb${CW}b${CR}
+ +bb${CW}0${CR}
+ ccc
+ EOF
+'
+
+test_expect_success 'diff-highlight highlights the middle of a line' '
+ cat >a <<-\EOF &&
+ aaa
+ bbb
+ ccc
+ EOF
+
+ cat >b <<-\EOF &&
+ aaa
+ b0b
+ ccc
+ EOF
+
+ dh_test a b <<-EOF
+ @@ -1,3 +1,3 @@
+ aaa
+ -b${CW}b${CR}b
+ +b${CW}0${CR}b
+ ccc
+ EOF
+'
+
+test_expect_success 'diff-highlight does not highlight whole line' '
+ cat >a <<-\EOF &&
+ aaa
+ bbb
+ ccc
+ EOF
+
+ cat >b <<-\EOF &&
+ aaa
+ 000
+ ccc
+ EOF
+
+ dh_test a b <<-EOF
+ @@ -1,3 +1,3 @@
+ aaa
+ -bbb
+ +000
+ ccc
+ EOF
+'
+
+test_expect_failure 'diff-highlight highlights mismatched hunk size' '
+ cat >a <<-\EOF &&
+ aaa
+ bbb
+ EOF
+
+ cat >b <<-\EOF &&
+ aaa
+ b0b
+ ccc
+ EOF
+
+ dh_test a b <<-EOF
+ @@ -1,3 +1,3 @@
+ aaa
+ -b${CW}b${CR}b
+ +b${CW}0${CR}b
+ +ccc
+ EOF
+'
+
+# These two code points share the same leading byte in UTF-8 representation;
+# a naive byte-wise diff would highlight only the second byte.
+#
+# - U+00f3 ("o" with acute)
+o_accent=$(printf '\303\263')
+# - U+00f8 ("o" with stroke)
+o_stroke=$(printf '\303\270')
+
+test_expect_success 'diff-highlight treats multibyte utf-8 as a unit' '
+ echo "unic${o_accent}de" >a &&
+ echo "unic${o_stroke}de" >b &&
+ dh_test a b <<-EOF
+ @@ -1 +1 @@
+ -unic${CW}${o_accent}${CR}de
+ +unic${CW}${o_stroke}${CR}de
+ EOF
+'
+
+# Unlike the UTF-8 above, these are combining code points which are meant
+# to modify the character preceding them:
+#
+# - U+0301 (combining acute accent)
+combine_accent=$(printf '\314\201')
+# - U+0302 (combining circumflex)
+combine_circum=$(printf '\314\202')
+
+test_expect_failure 'diff-highlight treats combining code points as a unit' '
+ echo "unico${combine_accent}de" >a &&
+ echo "unico${combine_circum}de" >b &&
+ dh_test a b <<-EOF
+ @@ -1 +1 @@
+ -unic${CW}o${combine_accent}${CR}de
+ +unic${CW}o${combine_circum}${CR}de
+ EOF
+'
+
+test_expect_success 'diff-highlight works with the --graph option' '
+ dh_test_setup_history &&
+
+ # topo-order so that the order of the commits is the same as with --graph
+ # trim graph elements so we can do a diff
+ # trim leading space because our trim_graph is not perfect
+ git log --branches -p --topo-order |
+ "$DIFF_HIGHLIGHT" | left_trim >graph.exp &&
+ git log --branches -p --graph |
+ "$DIFF_HIGHLIGHT" | trim_graph | left_trim >graph.act &&
+ test_cmp graph.exp graph.act
+'
+
+# Most combined diffs won't meet diff-highlight's line-number filter. So we
+# create one here where one side drops a line and the other modifies it. That
+# should result in a diff like:
+#
+# - modified content
+# ++resolved content
+#
+# which naively looks like one side added "+resolved".
+test_expect_success 'diff-highlight ignores combined diffs' '
+ echo "content" >file &&
+ git add file &&
+ git commit -m base &&
+
+ >file &&
+ git commit -am master &&
+
+ git checkout -b other HEAD^ &&
+ echo "modified content" >file &&
+ git commit -am other &&
+
+ test_must_fail git merge master &&
+ echo "resolved content" >file &&
+ git commit -am resolved &&
+
+ cat >expect <<-\EOF &&
+ --- a/file
+ +++ b/file
+ @@@ -1,1 -1,0 +1,1 @@@
+ - modified content
+ ++resolved content
+ EOF
+
+ git show -c | "$DIFF_HIGHLIGHT" >actual.raw &&
+ sed -n "/^---/,\$p" <actual.raw >actual &&
+ test_cmp expect actual
+'
+
+test_done
diff --git a/contrib/fast-import/import-tars.perl b/contrib/fast-import/import-tars.perl
index 95438e1ed4..d60b4315ed 100755
--- a/contrib/fast-import/import-tars.perl
+++ b/contrib/fast-import/import-tars.perl
@@ -96,18 +96,21 @@ foreach my $tar_file (@ARGV)
$mtime = oct $mtime;
next if $typeflag == 5; # directory
- print FI "blob\n", "mark :$next_mark\n";
- if ($typeflag == 2) { # symbolic link
- print FI "data ", length($linkname), "\n", $linkname;
- $mode = 0120000;
- } else {
- print FI "data $size\n";
- while ($size > 0 && read(I, $_, 512) == 512) {
- print FI substr($_, 0, $size);
- $size -= 512;
+ if ($typeflag != 1) { # handle hard links later
+ print FI "blob\n", "mark :$next_mark\n";
+ if ($typeflag == 2) { # symbolic link
+ print FI "data ", length($linkname), "\n",
+ $linkname;
+ $mode = 0120000;
+ } else {
+ print FI "data $size\n";
+ while ($size > 0 && read(I, $_, 512) == 512) {
+ print FI substr($_, 0, $size);
+ $size -= 512;
+ }
}
+ print FI "\n";
}
- print FI "\n";
my $path;
if ($prefix) {
@@ -115,7 +118,13 @@ foreach my $tar_file (@ARGV)
} else {
$path = "$name";
}
- $files{$path} = [$next_mark++, $mode];
+
+ if ($typeflag == 1) { # hard link
+ $linkname = "$prefix/$linkname" if $prefix;
+ $files{$path} = [ $files{$linkname}->[0], $mode ];
+ } else {
+ $files{$path} = [$next_mark++, $mode];
+ }
$author_time = $mtime if $mtime > $author_time;
$path =~ m,^([^/]+)/,;
diff --git a/contrib/git-jump/README b/contrib/git-jump/README
index 1cebc328cb..225e3f0954 100644
--- a/contrib/git-jump/README
+++ b/contrib/git-jump/README
@@ -29,7 +29,7 @@ Obviously this trivial case isn't that interesting; you could just open
`foo.c` yourself. But when you have many changes scattered across a
project, you can use the editor's support to "jump" from point to point.
-Git-jump can generate three types of interesting lists:
+Git-jump can generate four types of interesting lists:
1. The beginning of any diff hunks.
@@ -37,6 +37,8 @@ Git-jump can generate three types of interesting lists:
3. Any grep matches.
+ 4. Any whitespace errors detected by `git diff --check`.
+
Using git-jump
--------------
@@ -83,7 +85,7 @@ complete list of files and line numbers for each match.
Limitations
-----------
-This scripts was written and tested with vim. Given that the quickfix
+This script was written and tested with vim. Given that the quickfix
format is the same as what gcc produces, I expect emacs users have a
similar feature for iterating through the list, but I know nothing about
how to activate it.
diff --git a/contrib/git-jump/git-jump b/contrib/git-jump/git-jump
index dc90cd6379..427f206a45 100755
--- a/contrib/git-jump/git-jump
+++ b/contrib/git-jump/git-jump
@@ -12,6 +12,8 @@ diff: elements are diff hunks. Arguments are given to diff.
merge: elements are merge conflicts. Arguments are ignored.
grep: elements are grep hits. Arguments are given to grep.
+
+ws: elements are whitespace errors. Arguments are given to diff --check.
EOF
}
@@ -25,7 +27,7 @@ mode_diff() {
perl -ne '
if (m{^\+\+\+ (.*)}) { $file = $1; next }
defined($file) or next;
- if (m/^@@ .*\+(\d+)/) { $line = $1; next }
+ if (m/^@@ .*?\+(\d+)/) { $line = $1; next }
defined($line) or next;
if (/^ /) { $line++; next }
if (/^[-+]\s*(.*)/) {
@@ -55,6 +57,10 @@ mode_grep() {
'
}
+mode_ws() {
+ git diff --check "$@"
+}
+
if test $# -lt 1; then
usage >&2
exit 1
diff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES
index bc77e66b85..2076cf972b 100644
--- a/contrib/hooks/multimail/CHANGES
+++ b/contrib/hooks/multimail/CHANGES
@@ -1,3 +1,105 @@
+Release 1.4.0
+=============
+
+New features to troubleshoot a git-multimail installation
+---------------------------------------------------------
+
+* One can now perform a basic check of git-multimail's setup by
+ running the hook with the environment variable
+ GIT_MULTIMAIL_CHECK_SETUP set to a non-empty string. See
+ doc/troubleshooting.rst for details.
+
+* A new log files system was added. See the multimailhook.logFile,
+ multimailhook.errorLogFile and multimailhook.debugLogFile variables.
+
+* git_multimail.py can now be made more verbose using
+ multimailhook.verbose.
+
+* A new option --check-ref-filter is now available to help debugging
+ the refFilter* options.
+
+Formatting emails
+-----------------
+
+* Formatting of emails was made slightly more compact, to reduce the
+ odds of having long subject lines truncated or wrapped in short list
+ of commits.
+
+* multimailhook.emailPrefix may now use the '%(repo_shortname)s'
+ placeholder for the repository's short name.
+
+* A new option multimailhook.subjectMaxLength is available to truncate
+ overly long subject lines.
+
+Bug fixes and minor changes
+---------------------------
+
+* Options refFilterDoSendRegex and refFilterDontSendRegex were
+ essentially broken. They should work now.
+
+* The behavior when both refFilter{Do,Dont}SendRegex and
+ refFilter{Exclusion,Inclusion}Regex are set have been slightly
+ changed. Exclusion/Inclusion is now strictly stronger than
+ DoSend/DontSend.
+
+* The management of precedence when a setting can be computed in
+ multiple ways has been considerably refactored and modified.
+ multimailhook.from and multimailhook.reponame now have precedence
+ over the environment-specific settings ($GL_REPO/$GL_USER for
+ gitolite, --stash-user/repo for Stash, --submitter/--project for
+ Gerrit).
+
+* The coverage of the testsuite has been considerably improved. All
+ configuration variables now appear at least once in the testsuite.
+
+This version was tested with Python 2.6 to 3.5. It also mostly works
+with Python 2.4, but there is one known breakage in the testsuite
+related to non-ascii characters. It was tested with Git
+1.7.10.406.gdc801, 1.8.5.6, 2.1.4, and 2.10.0.rc0.1.g07c9292.
+
+Release 1.3.1 (bugfix-only release)
+===================================
+
+* Generate links to commits in combined emails (it was done only for
+ commit emails in 1.3.0).
+
+* Fix broken links on PyPi.
+
+Release 1.3.0
+=============
+
+* New options multimailhook.htmlInIntro and multimailhook.htmlInFooter
+ now allow using HTML in the introduction and footer of emails (e.g.
+ for a more pleasant formatting or to insert a link to the commit on
+ a web interface).
+
+* A new option multimailhook.commitBrowseURL gives a simpler (and less
+ flexible) way to add a link to a web interface for commit emails
+ than multimailhook.htmlInIntro and multimailhook.htmlInFooter.
+
+* A new public function config.add_config_parameters was added to
+ allow custom hooks to set specific Git configuration variables
+ without modifying the configuration files. See an example in
+ post-receive.example.
+
+* Error handling for SMTP has been improved (we used to print Python
+ backtraces for legitimate errors).
+
+* The SMTP mailer can now check TLS certificates when the newly added
+ configuration variable multimailhook.smtpCACerts.
+
+* Python 3 portability has been improved.
+
+* The documentation's formatting has been improved.
+
+* The testsuite has been improved (we now use pyflakes to check for
+ errors in the code).
+
+This version has been tested with Python 2.4 and 2.6 to 3.5, and Git
+v1.7.10-406-gdc801e7, 2.1.4 and 2.8.1.339.g3ad15fd.
+
+No change since 1.3 RC1.
+
Release 1.2.0
=============
diff --git a/contrib/hooks/multimail/CONTRIBUTING.rst b/contrib/hooks/multimail/CONTRIBUTING.rst
index 09efdb059c..da65570e9b 100644
--- a/contrib/hooks/multimail/CONTRIBUTING.rst
+++ b/contrib/hooks/multimail/CONTRIBUTING.rst
@@ -1,14 +1,16 @@
+Contributing
+============
+
git-multimail is an open-source project, built by volunteers. We would
welcome your help!
-The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>
-and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.
+The current maintainers are Matthieu Moy
+<matthieu.moy@grenoble-inp.fr> and Michael Haggerty
+<mhagger@alum.mit.edu>.
Please note that although a copy of git-multimail is distributed in
the "contrib" section of the main Git project, development takes place
-in a separate git-multimail repository on GitHub:
-
- https://github.com/git-multimail/git-multimail
+in a 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
@@ -21,10 +23,16 @@ to the maintainers). Please sign off your patches as per the `Git
project practice
<https://github.com/git/git/blob/master/Documentation/SubmittingPatches#L234>`__.
-General discussion of git-multimail can take place on the main Git
-mailing list,
+Please vote for issues you would like to be addressed in priority
+(click "add your reaction" and then the "+1" thumbs-up button on the
+GitHub issue).
- git@vger.kernel.org
+General discussion of git-multimail can take place on the main `Git
+mailing list`_.
Please CC emails regarding git-multimail to the maintainers so that we
don't overlook them.
+
+
+.. _`git-multimail repository on GitHub`: https://github.com/git-multimail/git-multimail
+.. _`Git mailing list`: git@vger.kernel.org
diff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README
index 55120685f0..5105373aea 100644
--- a/contrib/hooks/multimail/README
+++ b/contrib/hooks/multimail/README
@@ -1,11 +1,11 @@
-git-multimail (version 1.2.0)
-=============================
+git-multimail version 1.4.0
+===========================
.. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master
:target: https://travis-ci.org/git-multimail/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,
+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.
@@ -93,20 +93,20 @@ Requirements
Invocation
----------
-git_multimail.py is designed to be used as a ``post-receive`` hook in a
+``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
+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]_.
+[1]_.
-Alternatively, git_multimail.py can be imported as a Python module
+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
@@ -122,11 +122,18 @@ arbitrary Python code. For example, you can use a custom environment
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
+to use ``git_multimail.py`` as a Python module. (If you make interesting
changes of this type, please consider sharing them with the
community.)
+Troubleshooting/FAQ
+-------------------
+
+Please read `<doc/troubleshooting.rst>`__ for frequently asked
+questions and common issues with git-multimail.
+
+
Configuration
-------------
@@ -134,28 +141,27 @@ By default, git-multimail mostly takes its configuration from the
following ``git config`` settings:
multimailhook.environment
-
This describes the general environment of the repository. In most
cases, you do not need to specify a value for this variable:
`git-multimail` will autodetect which environment to use.
Currently supported values:
- * generic
-
+ generic
the username of the pusher is read from $USER or $USERNAME and
the repository name is derived from the repository's path.
- * gitolite
+ gitolite
+ Environment to use when ``git-multimail`` is ran as a gitolite_
+ hook.
- the username of the pusher is read from $GL_USER, the repository
+ The username of the pusher is read from $GL_USER, the repository
name is read from $GL_REPO, and the From: header value is
optionally read from gitolite.conf (see multimailhook.from).
For more information about gitolite and git-multimail, read
`<doc/gitolite.rst>`__
- * stash
-
+ stash
Environment to use when ``git-multimail`` is ran as an Atlassian
BitBucket Server (formerly known as Atlassian Stash) hook.
@@ -169,8 +175,7 @@ multimailhook.environment
and repo come from these two command line flags, which must be
specified.
- * gerrit
-
+ gerrit
Environment to use when ``git-multimail`` is ran as a
``ref-updated`` Gerrit hook.
@@ -205,14 +210,12 @@ multimailhook.environment
* If none of the above apply, then ``generic`` is used.
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
@@ -221,7 +224,6 @@ multimailhook.mailingList
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
@@ -231,7 +233,6 @@ multimailhook.refchangeList
multimailhook.mailingList is set.
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
@@ -241,7 +242,6 @@ multimailhook.announceList
even if one of the other values is set.
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
@@ -251,7 +251,6 @@ multimailhook.commitList
multimailhook.mailingList is set.
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;
@@ -261,7 +260,6 @@ multimailhook.announceShortlog
rather than useful. Default is false.
multimailhook.commitEmailFormat
-
The format of email messages for the individual commits, can be "text" or
"html". In the latter case, the emails will include diffs using colorized
HTML instead of plain text used by default. Note that this currently the
@@ -274,8 +272,43 @@ multimailhook.commitEmailFormat
the message starting with ``+++`` or ``---`` colored in red or
green).
-multimailhook.refchangeShowGraph
+ By default, all the message is HTML-escaped. See
+ ``multimailhook.htmlInIntro`` to change this behavior.
+
+multimailhook.commitBrowseURL
+ Used to generate a link to an online repository browser in commit
+ emails. This variable must be a string. Format directives like
+ ``%(<variable>)s`` will be expanded the same way as template
+ strings. In particular, ``%(id)s`` will be replaced by the full
+ Git commit identifier (40-chars hexadecimal).
+ If the string does not contain any format directive, then
+ ``%(id)s`` will be automatically added to the string. If you don't
+ want ``%(id)s`` to be automatically added, use the empty format
+ directive ``%()s`` anywhere in the string.
+
+ For example, a suitable value for the git-multimail project itself
+ would be
+ ``https://github.com/git-multimail/git-multimail/commit/%(id)s``.
+
+multimailhook.htmlInIntro, multimailhook.htmlInFooter
+ When generating an HTML message, git-multimail escapes any HTML
+ sequence by default. This means that if a template contains HTML
+ like ``<a href="foo">link</a>``, the reader will see the HTML
+ source code and not a proper link.
+
+ Set ``multimailhook.htmlInIntro`` to true to allow writing HTML
+ formatting in introduction templates. Similarly, set
+ ``multimailhook.htmlInFooter`` for HTML in the footer.
+
+ Variables expanded in the template are still escaped. For example,
+ if a repository's path contains a ``<``, it will be rendered as
+ such in the message.
+
+ Read `<doc/customizing-emails.rst>`__ for more details and
+ examples.
+
+multimailhook.refchangeShowGraph
If this option is set to true, then summary emails about reference
changes will additionally include:
@@ -287,7 +320,6 @@ multimailhook.refchangeShowGraph
specified in graphOpts. The 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
@@ -295,71 +327,80 @@ multimailhook.refchangeShowLog
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
+ * **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.::
+ 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 -oi -t -F \"Git Repo\"'
+ git config multimailhook.sendmailcommand '/usr/sbin/sendmail -oi -t -F \"Git Repo\"'
- Default is '/usr/sbin/sendmail -oi -t' or
- '/usr/lib/sendmail -oi -t' (depending on which file is
- present and executable).
+ Default is '/usr/sbin/sendmail -oi -t' or
+ '/usr/lib/sendmail -oi -t' (depending on which file is
+ present and executable).
- * multimailhook.envelopeSender
+ multimailhook.envelopeSender
+ If set then pass this value to sendmail via the -f option to set
+ the envelope sender address.
- 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
+ * **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.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.smtpUser
- * multimailhook.smtpPass
-
- Server username and password. Required if smtpEncryption is 'ssl'.
- Note that the username and password currently need to be
- set cleartext in the configuration file, which is not
- recommended. If you need to use this option, be sure your
- configuration file is read-only.
-
- * multimailhook.envelopeSender
+ multimailhook.smtpUser, multimailhook.smtpPass
+ Server username and password. Required if smtpEncryption is 'ssl'.
+ Note that the username and password currently need to be
+ set cleartext in the configuration file, which is not
+ recommended. If you need to use this option, be sure your
+ configuration file is read-only.
+ multimailhook.envelopeSender
The sender address to be passed to the SMTP server. If
unset, then the value of multimailhook.from is used.
- * multimailhook.smtpServerTimeout
-
+ multimailhook.smtpServerTimeout
Timeout in seconds.
- * multimailhook.smtpEncryption
-
- Set the security type. Allowed values: none, ssl, tls.
- Default=none.
-
- * multimailhook.smtpServerDebugLevel
-
+ multimailhook.smtpEncryption
+ Set the security type. Allowed values: ``none``, ``ssl``, ``tls`` (starttls).
+ Default is ``none``.
+
+ multimailhook.smtpCACerts
+ Set the path to a list of trusted CA certificate to verify the
+ server certificate, only supported when ``smtpEncryption`` is
+ ``tls``. If unset or empty, the server certificate is not
+ verified. If it targets a file containing a list of trusted CA
+ certificates (PEM format) these CAs will be used to verify the
+ server certificate. For debian, you can set
+ ``/etc/ssl/certs/ca-certificates.crt`` for using the system
+ trusted CAs. For self-signed server, you can add your server
+ certificate to the system store::
+
+ cd /usr/local/share/ca-certificates/
+ openssl s_client -starttls smtp \
+ -connect mail.example.net:587 -showcerts \
+ </dev/null 2>/dev/null \
+ | openssl x509 -outform PEM >mail.example.net.crt
+ update-ca-certificates
+
+ and used the updated ``/etc/ssl/certs/ca-certificates.crt``. Or
+ directly use your ``/path/to/mail.example.net.crt``. Default is
+ unset.
+
+ multimailhook.smtpServerDebugLevel
Integer number. Set to greater than 0 to activate debugging.
-multimailhook.from
-multimailhook.fromCommit
-multimailhook.fromRefchange
-
+multimailhook.from, multimailhook.fromCommit, multimailhook.fromRefchange
If set, use this value in the From: field of generated emails.
``fromCommit`` is used for commit emails, ``fromRefchange`` is
used for refchange emails, and ``from`` is used as fall-back in
@@ -372,7 +413,7 @@ multimailhook.fromRefchange
- The value ``pusher``, in which case the pusher's address (if
available) will be used.
- - The value ``author`` (meaningful only for replyToCommit), in which
+ - The value ``author`` (meaningful only for ``fromCommit``), in which
case the commit author's address will be used.
If config values are unset, the value of the From: header is
@@ -396,39 +437,47 @@ multimailhook.fromRefchange
3. Use the value of 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]``. Set this
- value to the empty string to suppress the email prefix.
+ value to the empty string to suppress the email prefix. You may
+ use the placeholder ``%(repo_shortname)s`` for the short name of
+ the repository.
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 `` [...]``
+ 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
+multimailhook.subjectMaxLength
+ The maximum length of the subject line (i.e. the ``oneline`` field
+ in templates, not including the prefix). Lines longer than this
+ limit are truncated to this length with a trailing ``[...]`` added
+ to indicate the missing text. This option The default is to use
+ ``multimailhook.emailMaxLineLength``. This option avoids sending
+ emails with overly long subject lines, but should not be needed if
+ the commit messages follow the Git convention (one short subject
+ line, then a blank line, then the message body). 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
@@ -436,23 +485,23 @@ multimailhook.maxCommitEmails
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
+ This option is ineffective with Python 3, where non-UTF-8
+ characters are unconditionally replaced.
+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
+ output. Shell quoting is allowed; see ``multimailhook.logOpts`` for
details.
multimailhook.graphOpts
-
Options passed to ``git log --graph`` when generating graphs for the
reference change summary emails (used only if refchangeShowGraph
is true). The default is '--oneline --decorate'.
@@ -460,7 +509,6 @@ multimailhook.graphOpts
Shell quoting is allowed; see 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 -p will show each commit's complete diff. The
@@ -479,7 +527,6 @@ multimailhook.logOpts
logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\"
multimailhook.commitLogOpts
-
Options passed to ``git log`` to generate additional info for
revision change emails. For example, adding --ignore-all-spaces
will suppress whitespace changes. The default options are ``-C
@@ -487,26 +534,21 @@ multimailhook.commitLogOpts
multimailhook.logOpts for details.
multimailhook.dateSubstitute
-
String to use as a substitute for ``Date:`` in the output of ``git
- log`` while formatting commit messages. This is usefull to avoid
+ log`` while formatting commit messages. This is useful to avoid
emitting a line that can be interpreted by mailers as the start of
a cited message (Zimbra webmail in particular). Defaults to
- ``CommitDate: ``. Set to an empty string or ``none`` to deactivate
+ ``CommitDate:``. Set to an empty string or ``none`` to deactivate
the behavior.
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
-
+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
@@ -519,36 +561,30 @@ multimailhook.replyToRefchange
commit emails.
multimailhook.quiet
-
Do not output the list of email recipients from the hook
multimailhook.stdout
-
For debugging, send emails to stdout rather than to the
mailer. Equivalent to the --stdout command line option
multimailhook.scanCommitForCc
-
If this option is set to true, than recipients from lines in commit body
that starts with ``CC:`` will be added to CC list.
Default: false
multimailhook.combineWhenSingleCommit
-
If this option is set to true and a single new commit is pushed to
a branch, combine the summary and commit email messages into a
single email.
Default: true
-multimailhook.refFilterInclusionRegex
-multimailhook.refFilterExclusionRegex
-multimailhook.refFilterDoSendRegex
-multimailhook.refFilterDontSendRegex
-
+multimailhook.refFilterInclusionRegex, multimailhook.refFilterExclusionRegex, multimailhook.refFilterDoSendRegex, multimailhook.refFilterDontSendRegex
**Warning:** these options are experimental. They should work, but
the user-interface is not stable yet (in particular, the option
names may change). If you want to participate in stabilizing the
feature, please contact the maintainers and/or send pull-requests.
+ If you are happy with the current shape of the feature, please
+ report it too.
Regular expressions that can be used to limit refs for which email
updates will be sent. It is an error to specify both an inclusion
@@ -598,6 +634,32 @@ multimailhook.refFilterDontSendRegex
[multimailhook]
refFilterExclusionRegex = ^refs/tags/|^refs/heads/master$
+ ``refFilterInclusionRegex`` and ``refFilterExclusionRegex`` are
+ strictly stronger than ``refFilterDoSendRegex`` and
+ ``refFilterDontSendRegex``. In other words, adding a ref to a
+ DoSend/DontSend regex has no effect if it is already excluded by a
+ Exclusion/Inclusion regex.
+
+multimailhook.logFile, multimailhook.errorLogFile, multimailhook.debugLogFile
+
+ When set, these variable designate path to files where
+ git-multimail will log some messages. Normal messages and error
+ messages are sent to ``logFile``, and error messages are also sent
+ to ``errorLogFile``. Debug messages and all other messages are
+ sent to ``debugLogFile``. The recommended way is to set only one
+ of these variables, but it is also possible to set several of them
+ (part of the information is then duplicated in several log files,
+ for example errors are duplicated to all log files).
+
+ Relative path are relative to the Git repository where the push is
+ done.
+
+multimailhook.verbose
+
+ Verbosity level of git-multimail on its standard output. By
+ default, show only error and info messages. If set to true, show
+ also debug messages.
+
Email filtering aids
--------------------
@@ -613,8 +675,8 @@ 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
+``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.
@@ -626,14 +688,16 @@ 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.
+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, the name of the repository is read
- from $GL_REPO (if it is not overridden by multimailhook.reponame),
- and the From: header value is optionally read from gitolite.conf
- (see multimailhook.from).
+GitoliteEnvironment
+ a Git repository that is managed by gitolite_. For such
+ repositories, the identity of the pusher is read from
+ environment variable $GL_USER, the name of the repository is read
+ from $GL_REPO (if it is not overridden by multimailhook.reponame),
+ and the From: header value is optionally read from gitolite.conf
+ (see multimailhook.from).
By default, git-multimail assumes GitoliteEnvironment if $GL_USER and
$GL_REPO are set, and otherwise assumes GenericEnvironment.
@@ -645,7 +709,7 @@ 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
+``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
@@ -673,9 +737,7 @@ contribute to git-multimail.
Footnotes
---------
-.. [1] http://www.python.org/dev/peps/pep-0394/
-
-.. [2] Because of the way information is passed to update hooks, the
+.. [1] 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
@@ -683,4 +745,4 @@ Footnotes
push. A workaround is to use --force-send to force sending the
emails.
-.. [3] https://github.com/sitaramc/gitolite
+.. _gitolite: https://github.com/sitaramc/gitolite
diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git
index 300a2a4d2d..161b0230a0 100644
--- a/contrib/hooks/multimail/README.Git
+++ b/contrib/hooks/multimail/README.Git
@@ -6,10 +6,10 @@ website:
https://github.com/git-multimail/git-multimail
The version in this directory was obtained from the upstream project
-on October 11 2015 and consists of the "git-multimail" subdirectory from
+on August 17 2016 and consists of the "git-multimail" subdirectory from
revision
- c0791b9ef5821a746fc3475c25765e640452eaae refs/tags/1.2.0
+ 07b1cb6bfd7be156c62e1afa17cae13b850a869f refs/tags/1.4.0
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/doc/customizing-emails.rst b/contrib/hooks/multimail/doc/customizing-emails.rst
new file mode 100644
index 0000000000..3f5b67f768
--- /dev/null
+++ b/contrib/hooks/multimail/doc/customizing-emails.rst
@@ -0,0 +1,56 @@
+Customizing the content and formatting of emails
+================================================
+
+Overloading template strings
+----------------------------
+
+The content of emails is generated based on template strings defined
+in ``git_multimail.py``. You can customize these template strings
+without changing the script itself, by defining a Python wrapper
+around it. The python wrapper should ``import git_multimail`` and then
+override the ``git_multimail.*`` strings like this::
+
+ import sys # needed for sys.argv
+
+ # Import and customize git_multimail:
+ import git_multimail
+ git_multimail.REVISION_INTRO_TEMPLATE = """..."""
+ git_multimail.COMBINED_INTRO_TEMPLATE = git_multimail.REVISION_INTRO_TEMPLATE
+
+ # start git_multimail itself:
+ git_multimail.main(sys.argv[1:])
+
+The template strings can use any value already used in the existing
+templates (read the source code).
+
+Using HTML in template strings
+------------------------------
+
+If ``multimailhook.commitEmailFormat`` is set to HTML, then
+git-multimail will generate HTML emails for commit notifications. The
+log and diff will be formatted automatically by git-multimail. By
+default, any HTML special character in the templates will be escaped.
+
+To use HTML formatting in the introduction of the email, set
+``multimailhook.htmlInIntro`` to ``true``. Then, the template can
+contain any HTML tags, that will be sent as-is in the email. For
+example, to add some formatting and a link to the online commit, use
+a format like::
+
+ git_multimail.REVISION_INTRO_TEMPLATE = """\
+ <span style="color:#808080">This is an automated email from the git hooks/post-receive script.</span><br /><br />
+
+ <strong>%(pusher)s</strong> pushed a commit to %(refname_type)s %(short_refname)s
+ in repository %(repo_shortname)s.<br />
+
+ <a href="https://github.com/git-multimail/git-multimail/commit/%(newrev)s">View on GitHub</a>.
+ """
+
+Note that the values expanded from ``%(variable)s`` in the format
+strings will still be escaped.
+
+For a less flexible but easier to set up way to add a link to commit
+emails, see ``multimailhook.commitBrowseURL``.
+
+Similarly, one can set ``multimailhook.htmlInFooter`` and override any
+of the ``*_FOOTER*`` template strings.
diff --git a/contrib/hooks/multimail/doc/troubleshooting.rst b/contrib/hooks/multimail/doc/troubleshooting.rst
new file mode 100644
index 0000000000..651b509ee6
--- /dev/null
+++ b/contrib/hooks/multimail/doc/troubleshooting.rst
@@ -0,0 +1,78 @@
+Troubleshooting issues with git-multimail: a FAQ
+================================================
+
+How to check that git-multimail is properly set up?
+---------------------------------------------------
+
+Since version 1.4.0, git-multimail allows a simple self-checking of
+its configuration: run it with the environment variable
+``GIT_MULTIMAIL_CHECK_SETUP`` set to a non-empty string. You should
+get something like this::
+
+ $ GIT_MULTIMAIL_CHECK_SETUP=true /home/moy/dev/git-multimail/git-multimail/git_multimail.py
+ Environment values:
+ administrator : 'the administrator of this repository'
+ charset : 'utf-8'
+ emailprefix : '[git-multimail] '
+ fqdn : 'anie'
+ projectdesc : 'UNNAMED PROJECT'
+ pusher : 'moy'
+ repo_path : '/home/moy/dev/git-multimail'
+ repo_shortname : 'git-multimail'
+
+ Now, checking that git-multimail's standard input is properly set ...
+ Please type some text and then press Return
+ foo
+ You have just entered:
+ foo
+ git-multimail seems properly set up.
+
+If you forgot to set an important variable, you may get instead::
+
+ $ GIT_MULTIMAIL_CHECK_SETUP=true /home/moy/dev/git-multimail/git-multimail/git_multimail.py
+ No email recipients configured!
+
+Do not set ``$GIT_MULTIMAIL_CHECK_SETUP`` other than for testing your
+configuration: it would disable the hook completely.
+
+Git is not using the right address in the From/To/Reply-To field
+----------------------------------------------------------------
+
+First, make sure that git-multimail actually uses what you think it is
+using. A lot happens to your email (especially when posting to a
+mailing-list) between the time `git_multimail.py` sends it and the
+time it reaches your inbox.
+
+A simple test (to do on a test repository, do not use in production as
+it would disable email sending): change your post-receive hook to call
+`git_multimail.py` with the `--stdout` option, and try to push to the
+repository. You should see something like::
+
+ Counting objects: 3, done.
+ Writing objects: 100% (3/3), 263 bytes | 0 bytes/s, done.
+ Total 3 (delta 0), reused 0 (delta 0)
+ remote: Sending notification emails to: foo.bar@example.com
+ remote: ===========================================================================
+ remote: Date: Mon, 25 Apr 2016 18:39:59 +0200
+ remote: To: foo.bar@example.com
+ remote: Subject: [git] branch master updated: foo
+ remote: MIME-Version: 1.0
+ remote: Content-Type: text/plain; charset=utf-8
+ remote: Content-Transfer-Encoding: 8bit
+ remote: Message-ID: <20160425163959.2311.20498@anie>
+ remote: From: Auth Or <Foo.Bar@example.com>
+ remote: Reply-To: Auth Or <Foo.Bar@example.com>
+ remote: X-Git-Host: example
+ ...
+ remote: --
+ remote: To stop receiving notification emails like this one, please contact
+ remote: the administrator of this repository.
+ remote: ===========================================================================
+ To /path/to/repo
+ 6278f04..e173f20 master -> master
+
+Note: this does not include the sender (Return-Path: header), as it is
+not part of the message content but passed to the mailer. Some mailer
+show the ``Sender:`` field instead of the ``From:`` field (for
+example, Zimbra Webmail shows ``From: <sender-field> on behalf of
+<from-field>``).
diff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py
index 0180dba431..c7f86403cf 100755
--- a/contrib/hooks/multimail/git_multimail.py
+++ b/contrib/hooks/multimail/git_multimail.py
@@ -1,8 +1,8 @@
#! /usr/bin/env python
-__version__ = '1.2.0'
+__version__ = '1.4.0'
-# Copyright (c) 2015 Matthieu Moy and others
+# Copyright (c) 2015-2016 Matthieu Moy and others
# Copyright (c) 2012-2014 Michael Haggerty and others
# Derived from contrib/hooks/post-receive-email, which is
# Copyright (c) 2007 Andy Parkins
@@ -56,7 +56,13 @@ import socket
import subprocess
import shlex
import optparse
+import logging
import smtplib
+try:
+ import ssl
+except ImportError:
+ # Python < 2.6 do not have ssl, but that's OK if we don't use it.
+ pass
import time
import cgi
@@ -75,11 +81,14 @@ def is_ascii(s):
if PYTHON3:
+ def is_string(s):
+ return isinstance(s, str)
+
def str_to_bytes(s):
return s.encode(ENCODING)
- def bytes_to_str(s):
- return s.decode(ENCODING)
+ def bytes_to_str(s, errors='strict'):
+ return s.decode(ENCODING, errors)
unicode = str
@@ -90,16 +99,34 @@ if PYTHON3:
f.buffer.write(msg.encode(sys.getdefaultencoding()))
except UnicodeEncodeError:
f.buffer.write(msg.encode(ENCODING))
+
+ def read_line(f):
+ # Try reading with the default encoding. If it fails,
+ # try UTF-8.
+ out = f.buffer.readline()
+ try:
+ return out.decode(sys.getdefaultencoding())
+ except UnicodeEncodeError:
+ return out.decode(ENCODING)
else:
+ def is_string(s):
+ try:
+ return isinstance(s, basestring)
+ except NameError: # Silence Pyflakes warning
+ raise
+
def str_to_bytes(s):
return s
- def bytes_to_str(s):
+ def bytes_to_str(s, errors='strict'):
return s
def write_str(f, msg):
f.write(msg)
+ def read_line(f):
+ return f.readline()
+
def next(it):
return it.next()
@@ -199,8 +226,8 @@ reference pointing at a previous point in the repository history.
\\
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.
+Any revisions marked "omit" are not gone; other references still
+refer to them. Any revisions marked "discard" are gone forever.
"""
@@ -219,8 +246,8 @@ 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.
+Any revisions marked "omit" are not gone; other references still
+refer to them. Any revisions marked "discard" are gone forever.
"""
@@ -244,22 +271,22 @@ 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
+listed as "add" were already present in the repository and have only
been added to this reference.
"""
TAG_CREATED_TEMPLATE = """\
- at %(newrev_short)-9s (%(newrev_type)s)
+ at %(newrev_short)-8s (%(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)
+ from %(oldrev_short)-8s (%(oldrev_type)s)
+ to %(newrev_short)-8s (%(newrev_type)s)
"""
@@ -272,7 +299,7 @@ TAG_DELETED_TEMPLATE = """\
# 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
+%(action)8s %(rev_short)-8s %(text)s
"""
@@ -313,6 +340,16 @@ in repository %(repo_shortname)s.
"""
+LINK_TEXT_TEMPLATE = """\
+View the commit online:
+%(browse_url)s
+
+"""
+
+LINK_HTML_TEMPLATE = """\
+<p><a href="%(browse_url)s">View the commit online</a>.</p>
+"""
+
REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE
@@ -410,11 +447,16 @@ def read_output(cmd, input=None, keepends=False, **kw):
input = str_to_bytes(input)
else:
stdin = None
+ errors = 'strict'
+ if 'errors' in kw:
+ errors = kw['errors']
+ del kw['errors']
p = subprocess.Popen(
- cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
+ tuple(str_to_bytes(w) for w in cmd),
+ stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
)
(out, err) = p.communicate(input)
- out = bytes_to_str(out)
+ out = bytes_to_str(out, errors=errors)
retcode = p.wait()
if retcode:
raise CommandError(cmd, retcode)
@@ -532,6 +574,28 @@ class Config(object):
assert words[-1] == ''
return words[:-1]
+ @staticmethod
+ def add_config_parameters(c):
+ """Add configuration parameters to Git.
+
+ c is either an str or a list of str, each element being of the
+ form 'var=val' or 'var', with the same syntax and meaning as
+ the argument of 'git -c var=val'.
+ """
+ if isinstance(c, str):
+ c = (c,)
+ parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '')
+ if parameters:
+ parameters += ' '
+ # git expects GIT_CONFIG_PARAMETERS to be of the form
+ # "'name1=value1' 'name2=value2' 'name3=value3'"
+ # including everything inside the double quotes (but not the double
+ # quotes themselves). Spacing is critical. Also, if a value contains
+ # a literal single quote that quote must be represented using the
+ # four character sequence: '\''
+ parameters += ' '.join("'" + x.replace("'", "'\\''") + "'" for x in c)
+ os.environ['GIT_CONFIG_PARAMETERS'] = parameters
+
def get(self, name, default=None):
try:
values = self._split(read_git_output(
@@ -745,6 +809,12 @@ class Change(object):
values['multimail_version'] = get_version()
return values
+ # Aliases usable in template strings. Tuple of pairs (destination,
+ # source).
+ VALUES_ALIAS = (
+ ("id", "newrev"),
+ )
+
def get_values(self, **extra_values):
"""Return a dictionary {keyword: expansion} for this Change.
@@ -760,6 +830,9 @@ class Change(object):
values = self._values.copy()
if extra_values:
values.update(extra_values)
+
+ for alias, val in self.VALUES_ALIAS:
+ values[alias] = values[val]
return values
def expand(self, template, **extra_values):
@@ -772,10 +845,14 @@ class Change(object):
return template % self.get_values(**extra_values)
- def expand_lines(self, template, **extra_values):
+ def expand_lines(self, template, html_escape_val=False, **extra_values):
"""Break template into lines and expand each line."""
values = self.get_values(**extra_values)
+ if html_escape_val:
+ for k in values:
+ if is_string(values[k]):
+ values[k] = cgi.escape(values[k], True)
for line in template.splitlines(True):
yield line % values
@@ -787,9 +864,10 @@ class Change(object):
values = self.get_values(**extra_values)
if self._contains_html_diff:
- values['contenttype'] = 'html'
+ self._content_type = 'html'
else:
- values['contenttype'] = 'plain'
+ self._content_type = 'plain'
+ values['contenttype'] = self._content_type
for line in template.splitlines():
(name, value) = line.split(': ', 1)
@@ -819,7 +897,11 @@ class Change(object):
raise NotImplementedError()
- def generate_email_intro(self):
+ def generate_browse_link(self, base_url):
+ """Generate a link to an online repository browser."""
+ return iter(())
+
+ def generate_email_intro(self, html_escape_val=False):
"""Generate the email intro for this Change, a line at a time.
The output will be used as the standard boilerplate at the top
@@ -835,7 +917,7 @@ class Change(object):
raise NotImplementedError()
- def generate_email_footer(self):
+ def generate_email_footer(self, html_escape_val):
"""Generate the footer of the email, a line at a time.
The footer is always included, irrespective of
@@ -876,9 +958,18 @@ class Change(object):
for line in self.generate_email_header(**extra_header_values):
yield line
yield '\n'
- for line in self._wrap_for_html(self.generate_email_intro()):
+ html_escape_val = (self.environment.html_in_intro and
+ self._contains_html_diff)
+ intro = self.generate_email_intro(html_escape_val)
+ if not self.environment.html_in_intro:
+ intro = self._wrap_for_html(intro)
+ for line in intro:
yield line
+ if self.environment.commitBrowseURL:
+ for line in self.generate_browse_link(self.environment.commitBrowseURL):
+ yield line
+
body = self.generate_email_body(push)
if body_filter is not None:
body = body_filter(body)
@@ -939,11 +1030,17 @@ class Change(object):
yield line
if self._contains_html_diff:
yield '</pre>'
-
- for line in self._wrap_for_html(self.generate_email_footer()):
+ html_escape_val = (self.environment.html_in_footer and
+ self._contains_html_diff)
+ footer = self.generate_email_footer(html_escape_val)
+ if not self.environment.html_in_footer:
+ footer = self._wrap_for_html(footer)
+ for line in footer:
yield line
- def get_alt_fromaddr(self):
+ def get_specific_fromaddr(self):
+ """For kinds of Changes which specify it, return the kind-specific
+ From address to use."""
return None
@@ -968,7 +1065,7 @@ class Revision(Change):
self.cc_recipients = ', '.join(to.strip() for to in self._cc_recipients())
if self.cc_recipients:
self.environment.log_msg(
- 'Add %s to CC for %s\n' % (self.cc_recipients, self.rev.sha1))
+ 'Add %s to CC for %s' % (self.cc_recipients, self.rev.sha1))
def _cc_recipients(self):
cc_recipients = []
@@ -988,10 +1085,15 @@ class Revision(Change):
['log', '--format=%s', '--no-walk', self.rev.sha1]
)
+ max_subject_length = self.environment.get_max_subject_length()
+ if max_subject_length > 0 and len(oneline) > max_subject_length:
+ oneline = oneline[:max_subject_length - 6] + ' [...]'
+
values['rev'] = self.rev.sha1
values['rev_short'] = self.rev.short
values['change_type'] = self.change_type
values['refname'] = self.refname
+ values['newrev'] = self.rev.sha1
values['short_refname'] = self.reference_change.short_refname
values['refname_type'] = self.reference_change.refname_type
values['reply_to_msgid'] = self.reference_change.msgid
@@ -1015,8 +1117,26 @@ class Revision(Change):
):
yield line
- def generate_email_intro(self):
- for line in self.expand_lines(REVISION_INTRO_TEMPLATE):
+ def generate_browse_link(self, base_url):
+ if '%(' not in base_url:
+ base_url += '%(id)s'
+ url = "".join(self.expand_lines(base_url))
+ if self._content_type == 'html':
+ for line in self.expand_lines(LINK_HTML_TEMPLATE,
+ html_escape_val=True,
+ browse_url=url):
+ yield line
+ elif self._content_type == 'plain':
+ for line in self.expand_lines(LINK_TEXT_TEMPLATE,
+ html_escape_val=False,
+ browse_url=url):
+ yield line
+ else:
+ raise NotImplementedError("Content-type %s unsupported. Please report it as a bug.")
+
+ def generate_email_intro(self, html_escape_val=False):
+ for line in self.expand_lines(REVISION_INTRO_TEMPLATE,
+ html_escape_val=html_escape_val):
yield line
def generate_email_body(self, push):
@@ -1025,20 +1145,21 @@ class Revision(Change):
for line in read_git_lines(
['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],
keepends=True,
- ):
+ errors='replace'):
if line.startswith('Date: ') and self.environment.date_substitute:
yield self.environment.date_substitute + line[len('Date: '):]
else:
yield line
- def generate_email_footer(self):
- return self.expand_lines(REVISION_FOOTER_TEMPLATE)
+ def generate_email_footer(self, html_escape_val):
+ return self.expand_lines(REVISION_FOOTER_TEMPLATE,
+ html_escape_val=html_escape_val)
def generate_email(self, push, body_filter=None, extra_header_values={}):
self._contains_diff()
return Change.generate_email(self, push, body_filter, extra_header_values)
- def get_alt_fromaddr(self):
+ def get_specific_fromaddr(self):
return self.environment.from_commit
@@ -1096,7 +1217,7 @@ class ReferenceChange(Change):
# Tracking branch:
environment.log_warning(
'*** Push-update of tracking branch %r\n'
- '*** - incomplete email generated.\n'
+ '*** - incomplete email generated.'
% (refname,)
)
klass = OtherReferenceChange
@@ -1104,7 +1225,7 @@ class ReferenceChange(Change):
# Some other reference namespace:
environment.log_warning(
'*** Push-update of strange reference %r\n'
- '*** - incomplete email generated.\n'
+ '*** - incomplete email generated.'
% (refname,)
)
klass = OtherReferenceChange
@@ -1112,7 +1233,7 @@ class ReferenceChange(Change):
# Anything else (is there anything else?)
environment.log_warning(
'*** Unknown type of update to %r (%s)\n'
- '*** - incomplete email generated.\n'
+ '*** - incomplete email generated.'
% (refname, rev.type,)
)
klass = OtherReferenceChange
@@ -1217,8 +1338,9 @@ class ReferenceChange(Change):
):
yield line
- def generate_email_intro(self):
- for line in self.expand_lines(self.intro_template):
+ def generate_email_intro(self, html_escape_val=False):
+ for line in self.expand_lines(self.intro_template,
+ html_escape_val=html_escape_val):
yield line
def generate_email_body(self, push):
@@ -1238,8 +1360,9 @@ class ReferenceChange(Change):
for line in self.generate_revision_change_summary(push):
yield line
- def generate_email_footer(self):
- return self.expand_lines(self.footer_template)
+ def generate_email_footer(self, html_escape_val):
+ return self.expand_lines(self.footer_template,
+ html_escape_val=html_escape_val)
def generate_revision_change_graph(self, push):
if self.showgraph:
@@ -1347,9 +1470,9 @@ class ReferenceChange(Change):
if discards and adds:
for (sha1, subject) in discards:
if sha1 in discarded_commits:
- action = 'discards'
+ action = 'discard'
else:
- action = 'omits'
+ action = 'omit'
yield self.expand(
BRIEF_SUMMARY_TEMPLATE, action=action,
rev_short=sha1, text=subject,
@@ -1358,7 +1481,7 @@ class ReferenceChange(Change):
if sha1 in new_commits:
action = 'new'
else:
- action = 'adds'
+ action = 'add'
yield self.expand(
BRIEF_SUMMARY_TEMPLATE, action=action,
rev_short=sha1, text=subject,
@@ -1370,9 +1493,9 @@ class ReferenceChange(Change):
elif discards:
for (sha1, subject) in discards:
if sha1 in discarded_commits:
- action = 'discards'
+ action = 'discard'
else:
- action = 'omits'
+ action = 'omit'
yield self.expand(
BRIEF_SUMMARY_TEMPLATE, action=action,
rev_short=sha1, text=subject,
@@ -1391,7 +1514,7 @@ class ReferenceChange(Change):
if sha1 in new_commits:
action = 'new'
else:
- action = 'adds'
+ action = 'add'
yield self.expand(
BRIEF_SUMMARY_TEMPLATE, action=action,
rev_short=sha1, text=subject,
@@ -1444,7 +1567,7 @@ class ReferenceChange(Change):
for r in discarded_revisions:
(sha1, subject) = r.rev.get_summary()
yield r.expand(
- BRIEF_SUMMARY_TEMPLATE, action='discards', text=subject,
+ BRIEF_SUMMARY_TEMPLATE, action='discard', text=subject,
)
for line in self.generate_revision_change_graph(push):
yield line
@@ -1482,7 +1605,7 @@ class ReferenceChange(Change):
)
yield '\n'
- def get_alt_fromaddr(self):
+ def get_specific_fromaddr(self):
return self.environment.from_refchange
@@ -1605,6 +1728,14 @@ class BranchChange(ReferenceChange):
self.header_template = COMBINED_HEADER_TEMPLATE
self.intro_template = COMBINED_INTRO_TEMPLATE
self.footer_template = COMBINED_FOOTER_TEMPLATE
+
+ def revision_gen_link(base_url):
+ # revision is used only to generate the body, and
+ # _content_type is set while generating headers. Get it
+ # from the BranchChange object.
+ revision._content_type = self._content_type
+ return revision.generate_browse_link(base_url)
+ self.generate_browse_link = revision_gen_link
for line in self.generate_email(push, body_filter, values):
yield line
@@ -1684,13 +1815,13 @@ class AnnotatedTagChange(ReferenceChange):
except CommandError:
prevtag = None
if prevtag:
- yield ' replaces %s\n' % (prevtag,)
+ yield ' replaces %s\n' % (prevtag,)
else:
prevtag = None
- yield ' length %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),)
+ yield ' length %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),)
- yield ' tagged by %s\n' % (tagger,)
- yield ' on %s\n' % (tagged,)
+ yield ' by %s\n' % (tagger,)
+ yield ' on %s\n' % (tagged,)
yield '\n'
# Show the content of the tag message; this might contain a
@@ -1807,6 +1938,9 @@ class OtherReferenceChange(ReferenceChange):
class Mailer(object):
"""An object that can send emails."""
+ def __init__(self, environment):
+ self.environment = environment
+
def send(self, lines, to_addrs):
"""Send an email consisting of lines.
@@ -1841,14 +1975,14 @@ class SendMailer(Mailer):
'Try setting multimailhook.sendmailCommand.'
)
- def __init__(self, command=None, envelopesender=None):
+ def __init__(self, environment, 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'."""
-
+ super(SendMailer, self).__init__(environment)
if command:
self.command = command[:]
else:
@@ -1861,7 +1995,7 @@ class SendMailer(Mailer):
try:
p = subprocess.Popen(self.command, stdin=subprocess.PIPE)
except OSError:
- sys.stderr.write(
+ self.environment.get_logger().error(
'*** Cannot execute command: %s\n' % ' '.join(self.command) +
'*** %s\n' % sys.exc_info()[1] +
'*** Try setting multimailhook.mailer to "smtp"\n' +
@@ -1872,15 +2006,16 @@ class SendMailer(Mailer):
lines = (str_to_bytes(line) for line in lines)
p.stdin.writelines(lines)
except Exception:
- sys.stderr.write(
+ self.environment.get_logger().error(
'*** Error while generating commit email\n'
'*** - mail sending aborted.\n'
)
- try:
+ if hasattr(p, 'terminate'):
# subprocess.terminate() is not available in Python 2.4
p.terminate()
- except AttributeError:
- pass
+ else:
+ import signal
+ os.kill(p.pid, signal.SIGTERM)
raise
else:
p.stdin.close()
@@ -1892,13 +2027,16 @@ class SendMailer(Mailer):
class SMTPMailer(Mailer):
"""Send emails using Python's smtplib."""
- def __init__(self, envelopesender, smtpserver,
+ def __init__(self, environment,
+ envelopesender, smtpserver,
smtpservertimeout=10.0, smtpserverdebuglevel=0,
smtpencryption='none',
smtpuser='', smtppass='',
+ smtpcacerts=''
):
+ super(SMTPMailer, self).__init__(environment)
if not envelopesender:
- sys.stderr.write(
+ self.environment.get_logger().error(
'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n'
'please set either multimailhook.envelopeSender or user.email\n'
)
@@ -1915,6 +2053,7 @@ class SMTPMailer(Mailer):
self.security = smtpencryption
self.username = smtpuser
self.password = smtppass
+ self.smtpcacerts = smtpcacerts
try:
def call(klass, server, timeout):
try:
@@ -1925,13 +2064,56 @@ class SMTPMailer(Mailer):
if self.security == 'none':
self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout)
elif self.security == 'ssl':
+ if self.smtpcacerts:
+ raise smtplib.SMTPException(
+ "Checking certificate is not supported for ssl, prefer starttls"
+ )
self.smtp = call(smtplib.SMTP_SSL, self.smtpserver, timeout=self.smtpservertimeout)
elif self.security == 'tls':
+ if 'ssl' not in sys.modules:
+ self.environment.get_logger().error(
+ '*** Your Python version does not have the ssl library installed\n'
+ '*** smtpEncryption=tls is not available.\n'
+ '*** Either upgrade Python to 2.6 or later\n'
+ ' or use git_multimail.py version 1.2.\n')
if ':' not in self.smtpserver:
self.smtpserver += ':587' # default port for TLS
self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout)
+ # start: ehlo + starttls
+ # equivalent to
+ # self.smtp.ehlo()
+ # self.smtp.starttls()
+ # with acces to the ssl layer
self.smtp.ehlo()
- self.smtp.starttls()
+ if not self.smtp.has_extn("starttls"):
+ raise smtplib.SMTPException("STARTTLS extension not supported by server")
+ resp, reply = self.smtp.docmd("STARTTLS")
+ if resp != 220:
+ raise smtplib.SMTPException("Wrong answer to the STARTTLS command")
+ if self.smtpcacerts:
+ self.smtp.sock = ssl.wrap_socket(
+ self.smtp.sock,
+ ca_certs=self.smtpcacerts,
+ cert_reqs=ssl.CERT_REQUIRED
+ )
+ else:
+ self.smtp.sock = ssl.wrap_socket(
+ self.smtp.sock,
+ cert_reqs=ssl.CERT_NONE
+ )
+ self.environment.get_logger().error(
+ '*** Warning, the server certificat is not verified (smtp) ***\n'
+ '*** set the option smtpCACerts ***\n'
+ )
+ if not hasattr(self.smtp.sock, "read"):
+ # using httplib.FakeSocket with Python 2.5.x or earlier
+ self.smtp.sock.read = self.smtp.sock.recv
+ self.smtp.file = smtplib.SSLFakeFile(self.smtp.sock)
+ self.smtp.helo_resp = None
+ self.smtp.ehlo_resp = None
+ self.smtp.esmtp_features = {}
+ self.smtp.does_esmtp = 0
+ # end: ehlo + starttls
self.smtp.ehlo()
else:
sys.stdout.write('*** Error: Control reached an invalid option. ***')
@@ -1942,15 +2124,16 @@ class SMTPMailer(Mailer):
% self.smtpserverdebuglevel)
self.smtp.set_debuglevel(self.smtpserverdebuglevel)
except Exception:
- sys.stderr.write(
+ self.environment.get_logger().error(
'*** Error establishing SMTP connection to %s ***\n'
- % self.smtpserver)
- sys.stderr.write('*** %s\n' % sys.exc_info()[1])
+ '*** %s\n'
+ % (self.smtpserver, sys.exc_info()[1]))
sys.exit(1)
def __del__(self):
if hasattr(self, 'smtp'):
self.smtp.quit()
+ del self.smtp
def send(self, lines, to_addrs):
try:
@@ -1958,13 +2141,26 @@ class SMTPMailer(Mailer):
self.smtp.login(self.username, self.password)
msg = ''.join(lines)
# turn comma-separated list into Python list if needed.
- if isinstance(to_addrs, basestring):
+ if is_string(to_addrs):
to_addrs = [email for (name, email) in getaddresses([to_addrs])]
self.smtp.sendmail(self.envelopesender, to_addrs, msg)
- except Exception:
- sys.stderr.write('*** Error sending email ***\n')
- sys.stderr.write('*** %s\n' % sys.exc_info()[1])
- self.smtp.quit()
+ except smtplib.SMTPResponseException:
+ err = sys.exc_info()[1]
+ self.environment.get_logger().error(
+ '*** Error sending email ***\n'
+ '*** Error %d: %s\n'
+ % (err.smtp_code, bytes_to_str(err.smtp_error)))
+ try:
+ smtp = self.smtp
+ # delete the field before quit() so that in case of
+ # error, self.smtp is deleted anyway.
+ del self.smtp
+ smtp.quit()
+ except:
+ self.environment.get_logger().error(
+ '*** Error closing the SMTP connection ***\n'
+ '*** Exiting anyway ... ***\n'
+ '*** %s\n' % sys.exc_info()[1])
sys.exit(1)
@@ -2086,6 +2282,11 @@ class Environment(object):
to send and when computing what commits are considered new
to the repository. Default is "^refs/notes/".
+ get_max_subject_length()
+
+ Return an int giving the maximal length for the subject
+ (git log --oneline).
+
They should also define the following attributes:
announce_show_shortlog (bool)
@@ -2097,6 +2298,14 @@ class Environment(object):
If "html", generate commit emails in HTML instead of plain text
used by default.
+ html_in_intro (bool)
+ html_in_footer (bool)
+
+ When generating HTML emails, the introduction (respectively,
+ the footer) will be HTML-escaped iff html_in_intro (respectively,
+ the footer) is true. When false, only the values used to expand
+ the template are escaped.
+
refchange_showgraph (bool)
True iff refchanges emails should include a detailed graph.
@@ -2152,6 +2361,15 @@ class Environment(object):
multimailhook.fromRefchange and multimailhook.fromCommit
by ConfigEnvironmentMixin.
+ log_file, error_log_file, debug_log_file (string)
+
+ Name of a file to which logs should be sent.
+
+ verbose (int)
+
+ How verbose the system should be.
+ - 0 (default): show info, errors, ...
+ - 1 : show basic debug info
"""
REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
@@ -2160,6 +2378,9 @@ class Environment(object):
self.osenv = osenv or os.environ
self.announce_show_shortlog = False
self.commit_email_format = "text"
+ self.html_in_intro = False
+ self.html_in_footer = False
+ self.commitBrowseURL = None
self.maxcommitemails = 500
self.diffopts = ['--stat', '--summary', '--find-copies-harder']
self.graphopts = ['--oneline', '--decorate']
@@ -2171,6 +2392,7 @@ class Environment(object):
self.quiet = False
self.stdout = False
self.combine_when_single_commit = True
+ self.logger = None
self.COMPUTED_KEYS = [
'administrator',
@@ -2185,6 +2407,12 @@ class Environment(object):
self._values = None
+ def get_logger(self):
+ """Get (possibly creates) the logger associated to this environment."""
+ if self.logger is None:
+ self.logger = Logger(self)
+ return self.logger
+
def get_repo_shortname(self):
"""Use the last part of the repo path, with ".git" stripped off if present."""
@@ -2236,7 +2464,7 @@ class Environment(object):
The return value is always a new dictionary."""
if self._values is None:
- values = {}
+ values = {'': ''} # %()s expands to the empty string.
for key in self.COMPUTED_KEYS:
value = getattr(self, 'get_%s' % (key,))()
@@ -2292,6 +2520,11 @@ class Environment(object):
# which we simply do not have right now.
return "^refs/notes/"
+ def get_max_subject_length(self):
+ """Return the maximal subject line (git log --oneline) length.
+ Longer subject lines will be truncated."""
+ raise NotImplementedError()
+
def filter_body(self, lines):
"""Filter the lines intended for an email body.
@@ -2307,19 +2540,22 @@ class Environment(object):
"""Write the string msg on a log file or on stderr.
Sends the text to stderr by default, override to change the behavior."""
- write_str(sys.stderr, msg)
+ self.get_logger().info(msg)
def log_warning(self, msg):
"""Write the string msg on a log file or on stderr.
Sends the text to stderr by default, override to change the behavior."""
- write_str(sys.stderr, msg)
+ self.get_logger().warning(msg)
def log_error(self, msg):
"""Write the string msg on a log file or on stderr.
Sends the text to stderr by default, override to change the behavior."""
- write_str(sys.stderr, msg)
+ self.get_logger().error(msg)
+
+ def check(self):
+ pass
class ConfigEnvironmentMixin(Environment):
@@ -2375,6 +2611,16 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
else:
self.commit_email_format = commit_email_format
+ html_in_intro = config.get_bool('htmlInIntro')
+ if html_in_intro is not None:
+ self.html_in_intro = html_in_intro
+
+ html_in_footer = config.get_bool('htmlInFooter')
+ if html_in_footer is not None:
+ self.html_in_footer = html_in_footer
+
+ self.commitBrowseURL = config.get('commitBrowseURL')
+
maxcommitemails = config.get('maxcommitemails')
if maxcommitemails is not None:
try:
@@ -2415,7 +2661,6 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
['author'])
self.__reply_to_commit = config.get('replyToCommit', default=reply_to)
- from_addr = self.config.get('from')
self.from_refchange = config.get('fromRefchange')
self.forbid_field_values('fromRefchange',
self.from_refchange,
@@ -2429,6 +2674,14 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
if combine is not None:
self.combine_when_single_commit = combine
+ self.log_file = config.get('logFile', default=None)
+ self.error_log_file = config.get('errorLogFile', default=None)
+ self.debug_log_file = config.get('debugLogFile', default=None)
+ if config.get_bool('Verbose', default=False):
+ self.verbose = 1
+ else:
+ self.verbose = 0
+
def get_administrator(self):
return (
self.config.get('administrator') or
@@ -2447,11 +2700,21 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
if emailprefix is not None:
emailprefix = emailprefix.strip()
if emailprefix:
- return emailprefix + ' '
- else:
- return ''
+ emailprefix += ' '
else:
- return '[%s] ' % (self.get_repo_shortname(),)
+ emailprefix = '[%(repo_shortname)s] '
+ short_name = self.get_repo_shortname()
+ try:
+ return emailprefix % {'repo_shortname': short_name}
+ except:
+ self.get_logger().error(
+ '*** Invalid multimailhook.emailPrefix: %s\n' % emailprefix +
+ '*** %s\n' % sys.exc_info()[1] +
+ "*** Only the '%(repo_shortname)s' placeholder is allowed\n"
+ )
+ raise ConfigurationException(
+ '"%s" is not an allowed setting for emailPrefix' % emailprefix
+ )
def get_sender(self):
return self.config.get('envelopesender')
@@ -2472,9 +2735,9 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
def get_fromaddr(self, change=None):
fromaddr = self.config.get('from')
if change:
- alt_fromaddr = change.get_alt_fromaddr()
- if alt_fromaddr:
- fromaddr = alt_fromaddr
+ specific_fromaddr = change.get_specific_fromaddr()
+ if specific_fromaddr:
+ fromaddr = specific_fromaddr
if fromaddr:
fromaddr = self.process_addr(fromaddr, change)
if fromaddr:
@@ -2500,7 +2763,7 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
class FilterLinesEnvironmentMixin(Environment):
"""Handle encoding and maximum line length of body lines.
- emailmaxlinelength (int or None)
+ email_max_line_length (int or None)
The maximum length of any single line in the email body.
Longer lines are truncated at that length with ' [...]'
@@ -2515,10 +2778,13 @@ class FilterLinesEnvironmentMixin(Environment):
"""
- def __init__(self, strict_utf8=True, emailmaxlinelength=500, **kw):
+ def __init__(self, strict_utf8=True,
+ email_max_line_length=500, max_subject_length=500,
+ **kw):
super(FilterLinesEnvironmentMixin, self).__init__(**kw)
self.__strict_utf8 = strict_utf8
- self.__emailmaxlinelength = emailmaxlinelength
+ self.__email_max_line_length = email_max_line_length
+ self.__max_subject_length = max_subject_length
def filter_body(self, lines):
lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines)
@@ -2527,15 +2793,18 @@ class FilterLinesEnvironmentMixin(Environment):
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)
+ if self.__email_max_line_length > 0:
+ lines = limit_linelength(lines, self.__email_max_line_length)
if not PYTHON3:
lines = (line.encode(ENCODING, 'replace') for line in lines)
- elif self.__emailmaxlinelength:
- lines = limit_linelength(lines, self.__emailmaxlinelength)
+ elif self.__email_max_line_length:
+ lines = limit_linelength(lines, self.__email_max_line_length)
return lines
+ def get_max_subject_length(self):
+ return self.__max_subject_length
+
class ConfigFilterLinesEnvironmentMixin(
ConfigEnvironmentMixin,
@@ -2548,9 +2817,13 @@ class ConfigFilterLinesEnvironmentMixin(
if strict_utf8 is not None:
kw['strict_utf8'] = strict_utf8
- emailmaxlinelength = config.get('emailmaxlinelength')
- if emailmaxlinelength is not None:
- kw['emailmaxlinelength'] = int(emailmaxlinelength)
+ email_max_line_length = config.get('emailmaxlinelength')
+ if email_max_line_length is not None:
+ kw['email_max_line_length'] = int(email_max_line_length)
+
+ max_subject_length = config.get('subjectMaxLength', default=email_max_line_length)
+ if max_subject_length is not None:
+ kw['max_subject_length'] = int(max_subject_length)
super(ConfigFilterLinesEnvironmentMixin, self).__init__(
config=config, **kw
@@ -2566,7 +2839,7 @@ class MaxlinesEnvironmentMixin(Environment):
def filter_body(self, lines):
lines = super(MaxlinesEnvironmentMixin, self).filter_body(lines)
- if self.__emailmaxlines:
+ if self.__emailmaxlines > 0:
lines = limit_lines(lines, self.__emailmaxlines)
return lines
@@ -2659,25 +2932,64 @@ class StaticRecipientsEnvironmentMixin(Environment):
# 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:
- if not (refchange_recipients or
- announce_recipients or
- revision_recipients or
- scancommitforcc):
- raise ConfigurationException('No email recipients configured!')
self.__refchange_recipients = refchange_recipients
self.__announce_recipients = announce_recipients
self.__revision_recipients = revision_recipients
+ def check(self):
+ if not (self.get_refchange_recipients(None) or
+ self.get_announce_recipients(None) or
+ self.get_revision_recipients(None) or
+ self.get_scancommitforcc()):
+ raise ConfigurationException('No email recipients configured!')
+ super(StaticRecipientsEnvironmentMixin, self).check()
+
def get_refchange_recipients(self, refchange):
+ if self.__refchange_recipients is None:
+ return super(StaticRecipientsEnvironmentMixin,
+ self).get_refchange_recipients(refchange)
return self.__refchange_recipients
def get_announce_recipients(self, annotated_tag_change):
+ if self.__announce_recipients is None:
+ return super(StaticRecipientsEnvironmentMixin,
+ self).get_refchange_recipients(annotated_tag_change)
return self.__announce_recipients
def get_revision_recipients(self, revision):
+ if self.__revision_recipients is None:
+ return super(StaticRecipientsEnvironmentMixin,
+ self).get_refchange_recipients(revision)
return self.__revision_recipients
+class CLIRecipientsEnvironmentMixin(Environment):
+ """Mixin storing recipients information comming from the
+ command-line."""
+
+ def __init__(self, cli_recipients=None, **kw):
+ super(CLIRecipientsEnvironmentMixin, self).__init__(**kw)
+ self.__cli_recipients = cli_recipients
+
+ def get_refchange_recipients(self, refchange):
+ if self.__cli_recipients is None:
+ return super(CLIRecipientsEnvironmentMixin,
+ self).get_refchange_recipients(refchange)
+ return self.__cli_recipients
+
+ def get_announce_recipients(self, annotated_tag_change):
+ if self.__cli_recipients is None:
+ return super(CLIRecipientsEnvironmentMixin,
+ self).get_announce_recipients(annotated_tag_change)
+ return self.__cli_recipients
+
+ def get_revision_recipients(self, revision):
+ if self.__cli_recipients is None:
+ return super(CLIRecipientsEnvironmentMixin,
+ self).get_revision_recipients(revision)
+ return self.__cli_recipients
+
+
class ConfigRecipientsEnvironmentMixin(
ConfigEnvironmentMixin,
StaticRecipientsEnvironmentMixin
@@ -2751,24 +3063,20 @@ class StaticRefFilterEnvironmentMixin(Environment):
if ref_filter_do_send_regex and ref_filter_dont_send_regex:
raise ConfigurationException(
"Cannot specify both a ref doSend and dontSend regex.")
- if ref_filter_do_send_regex or ref_filter_dont_send_regex:
- self.__is_do_send_filter = bool(ref_filter_do_send_regex)
- if ref_filter_incl_regex:
- ref_filter_send_regex = ref_filter_incl_regex
- elif ref_filter_excl_regex:
- ref_filter_send_regex = ref_filter_excl_regex
- else:
- ref_filter_send_regex = '.*'
- self.__is_do_send_filter = True
- try:
- self.__send_compiled_regex = re.compile(ref_filter_send_regex)
- except Exception:
- raise ConfigurationException(
- 'Invalid Ref Filter Regex "%s": %s' %
- (ref_filter_send_regex, sys.exc_info()[1]))
+ self.__is_do_send_filter = bool(ref_filter_do_send_regex)
+ if ref_filter_do_send_regex:
+ ref_filter_send_regex = ref_filter_do_send_regex
+ elif ref_filter_dont_send_regex:
+ ref_filter_send_regex = ref_filter_dont_send_regex
else:
- self.__send_compiled_regex = self.__compiled_regex
- self.__is_do_send_filter = self.__is_inclusion_filter
+ ref_filter_send_regex = '.*'
+ self.__is_do_send_filter = True
+ try:
+ self.__send_compiled_regex = re.compile(ref_filter_send_regex)
+ except Exception:
+ raise ConfigurationException(
+ 'Invalid Ref Filter Regex "%s": %s' %
+ (ref_filter_send_regex, sys.exc_info()[1]))
def get_ref_filter_regex(self, send_filter=False):
if send_filter:
@@ -2839,34 +3147,21 @@ class GenericEnvironmentMixin(Environment):
return self.osenv.get('USER', self.osenv.get('USERNAME', 'unknown user'))
-class GenericEnvironment(
- ProjectdescEnvironmentMixin,
- ConfigMaxlinesEnvironmentMixin,
- ComputeFQDNEnvironmentMixin,
- ConfigFilterLinesEnvironmentMixin,
- ConfigRecipientsEnvironmentMixin,
- ConfigRefFilterEnvironmentMixin,
- PusherDomainEnvironmentMixin,
- ConfigOptionsEnvironmentMixin,
- GenericEnvironmentMixin,
- Environment,
- ):
- pass
+class GitoliteEnvironmentHighPrecMixin(Environment):
+ def get_pusher(self):
+ return self.osenv.get('GL_USER', 'unknown user')
-class GitoliteEnvironmentMixin(Environment):
+class GitoliteEnvironmentLowPrecMixin(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()
+ super(GitoliteEnvironmentLowPrecMixin, self).get_repo_shortname()
)
- def get_pusher(self):
- return self.osenv.get('GL_USER', 'unknown user')
-
def get_fromaddr(self, change=None):
GL_USER = self.osenv.get('GL_USER')
if GL_USER is not None:
@@ -2904,7 +3199,7 @@ class GitoliteEnvironmentMixin(Environment):
return m.group(1)
finally:
f.close()
- return super(GitoliteEnvironmentMixin, self).get_fromaddr(change)
+ return super(GitoliteEnvironmentLowPrecMixin, self).get_fromaddr(change)
class IncrementalDateTime(object):
@@ -2925,67 +3220,43 @@ class IncrementalDateTime(object):
return formatted
-class GitoliteEnvironment(
- ProjectdescEnvironmentMixin,
- ConfigMaxlinesEnvironmentMixin,
- ComputeFQDNEnvironmentMixin,
- ConfigFilterLinesEnvironmentMixin,
- ConfigRecipientsEnvironmentMixin,
- ConfigRefFilterEnvironmentMixin,
- PusherDomainEnvironmentMixin,
- ConfigOptionsEnvironmentMixin,
- GitoliteEnvironmentMixin,
- Environment,
- ):
- pass
-
-
-class StashEnvironmentMixin(Environment):
+class StashEnvironmentHighPrecMixin(Environment):
def __init__(self, user=None, repo=None, **kw):
- super(StashEnvironmentMixin, self).__init__(**kw)
+ super(StashEnvironmentHighPrecMixin,
+ self).__init__(user=user, repo=repo, **kw)
self.__user = user
self.__repo = repo
- def get_repo_shortname(self):
- return self.__repo
-
def get_pusher(self):
return re.match('(.*?)\s*<', self.__user).group(1)
def get_pusher_email(self):
return self.__user
- def get_fromaddr(self, change=None):
- return self.__user
+class StashEnvironmentLowPrecMixin(Environment):
+ def __init__(self, user=None, repo=None, **kw):
+ super(StashEnvironmentLowPrecMixin, self).__init__(**kw)
+ self.__repo = repo
+ self.__user = user
-class StashEnvironment(
- StashEnvironmentMixin,
- ProjectdescEnvironmentMixin,
- ConfigMaxlinesEnvironmentMixin,
- ComputeFQDNEnvironmentMixin,
- ConfigFilterLinesEnvironmentMixin,
- ConfigRecipientsEnvironmentMixin,
- ConfigRefFilterEnvironmentMixin,
- PusherDomainEnvironmentMixin,
- ConfigOptionsEnvironmentMixin,
- Environment,
- ):
- pass
+ def get_repo_shortname(self):
+ return self.__repo
+
+ def get_fromaddr(self, change=None):
+ return self.__user
-class GerritEnvironmentMixin(Environment):
+class GerritEnvironmentHighPrecMixin(Environment):
def __init__(self, project=None, submitter=None, update_method=None, **kw):
- super(GerritEnvironmentMixin, self).__init__(**kw)
+ super(GerritEnvironmentHighPrecMixin,
+ self).__init__(submitter=submitter, project=project, **kw)
self.__project = project
self.__submitter = submitter
self.__update_method = update_method
"Make an 'update_method' value available for templates."
self.COMPUTED_KEYS += ['update_method']
- def get_repo_shortname(self):
- return self.__project
-
def get_pusher(self):
if self.__submitter:
if self.__submitter.find('<') != -1:
@@ -3008,16 +3279,10 @@ class GerritEnvironmentMixin(Environment):
if self.__submitter:
return self.__submitter
else:
- return super(GerritEnvironmentMixin, self).get_pusher_email()
-
- def get_fromaddr(self, change=None):
- if self.__submitter and self.__submitter.find('<') != -1:
- return self.__submitter
- else:
- return super(GerritEnvironmentMixin, self).get_fromaddr(change)
+ return super(GerritEnvironmentHighPrecMixin, self).get_pusher_email()
def get_default_ref_ignore_regex(self):
- default = super(GerritEnvironmentMixin, self).get_default_ref_ignore_regex()
+ default = super(GerritEnvironmentHighPrecMixin, self).get_default_ref_ignore_regex()
return default + '|^refs/changes/|^refs/cache-automerge/|^refs/meta/'
def get_revision_recipients(self, revision):
@@ -3030,25 +3295,26 @@ class GerritEnvironmentMixin(Environment):
if committer == 'Gerrit Code Review':
return []
else:
- return super(GerritEnvironmentMixin, self).get_revision_recipients(revision)
+ return super(GerritEnvironmentHighPrecMixin, self).get_revision_recipients(revision)
def get_update_method(self):
return self.__update_method
-class GerritEnvironment(
- GerritEnvironmentMixin,
- ProjectdescEnvironmentMixin,
- ConfigMaxlinesEnvironmentMixin,
- ComputeFQDNEnvironmentMixin,
- ConfigFilterLinesEnvironmentMixin,
- ConfigRecipientsEnvironmentMixin,
- ConfigRefFilterEnvironmentMixin,
- PusherDomainEnvironmentMixin,
- ConfigOptionsEnvironmentMixin,
- Environment,
- ):
- pass
+class GerritEnvironmentLowPrecMixin(Environment):
+ def __init__(self, project=None, submitter=None, **kw):
+ super(GerritEnvironmentLowPrecMixin, self).__init__(**kw)
+ self.__project = project
+ self.__submitter = submitter
+
+ def get_repo_shortname(self):
+ return self.__project
+
+ def get_fromaddr(self, change=None):
+ if self.__submitter and self.__submitter.find('<') != -1:
+ return self.__submitter
+ else:
+ return super(GerritEnvironmentLowPrecMixin, self).get_fromaddr(change)
class Push(object):
@@ -3314,13 +3580,13 @@ class Push(object):
if not change.recipients:
change.environment.log_warning(
'*** no recipients configured so no email will be sent\n'
- '*** for %r update %s->%s\n'
+ '*** for %r update %s->%s'
% (change.refname, change.old.sha1, change.new.sha1,)
)
else:
if not change.environment.quiet:
change.environment.log_msg(
- 'Sending notification emails to: %s\n' % (change.recipients,))
+ 'Sending notification emails to: %s' % (change.recipients,))
extra_values = {'send_date': next(send_date)}
rev = change.send_single_combined_email(sha1s)
@@ -3343,14 +3609,14 @@ class Push(object):
change.environment.log_warning(
'*** 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
+ '*** Currently, multimailhook.maxCommitEmails=%d' % max_emails
)
return
for (num, sha1) in enumerate(sha1s):
rev = Revision(change, GitObject(sha1), num=num + 1, tot=len(sha1s))
if not rev.recipients and rev.cc_recipients:
- change.environment.log_msg('*** Replacing Cc: with To:\n')
+ change.environment.log_msg('*** Replacing Cc: with To:')
rev.recipients = rev.cc_recipients
rev.cc_recipients = None
if rev.recipients:
@@ -3364,7 +3630,7 @@ class Push(object):
if unhandled_sha1s:
change.environment.log_error(
'ERROR: No emails were sent for the following new commits:\n'
- ' %s\n'
+ ' %s'
% ('\n '.join(sorted(unhandled_sha1s)),)
)
@@ -3378,24 +3644,41 @@ def include_ref(refname, ref_filter_regex, is_inclusion_filter):
def run_as_post_receive_hook(environment, mailer):
- ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
+ environment.check()
+ send_filter_regex, send_is_inclusion_filter = environment.get_ref_filter_regex(True)
+ ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(False)
changes = []
- for line in sys.stdin:
+ while True:
+ line = read_line(sys.stdin)
+ if line == '':
+ break
(oldrev, newrev, refname) = line.strip().split(' ', 2)
+ environment.get_logger().debug(
+ "run_as_post_receive_hook: oldrev=%s, newrev=%s, refname=%s" %
+ (oldrev, newrev, refname))
+
if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
continue
+ if not include_ref(refname, send_filter_regex, send_is_inclusion_filter):
+ continue
changes.append(
ReferenceChange.create(environment, oldrev, newrev, refname)
)
if changes:
push = Push(environment, changes)
push.send_emails(mailer, body_filter=environment.filter_body)
+ if hasattr(mailer, '__del__'):
+ mailer.__del__()
def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
- ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
+ environment.check()
+ send_filter_regex, send_is_inclusion_filter = environment.get_ref_filter_regex(True)
+ ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(False)
if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
return
+ if not include_ref(refname, send_filter_regex, send_is_inclusion_filter):
+ return
changes = [
ReferenceChange.create(
environment,
@@ -3406,6 +3689,77 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=
]
push = Push(environment, changes, force_send)
push.send_emails(mailer, body_filter=environment.filter_body)
+ if hasattr(mailer, '__del__'):
+ mailer.__del__()
+
+
+def check_ref_filter(environment):
+ send_filter_regex, send_is_inclusion = environment.get_ref_filter_regex(True)
+ ref_filter_regex, ref_is_inclusion = environment.get_ref_filter_regex(False)
+
+ def inc_exc_lusion(b):
+ if b:
+ return 'inclusion'
+ else:
+ return 'exclusion'
+
+ if send_filter_regex:
+ sys.stdout.write("DoSend/DontSend filter regex (" +
+ (inc_exc_lusion(send_is_inclusion)) +
+ '): ' + send_filter_regex.pattern +
+ '\n')
+ if send_filter_regex:
+ sys.stdout.write("Include/Exclude filter regex (" +
+ (inc_exc_lusion(ref_is_inclusion)) +
+ '): ' + ref_filter_regex.pattern +
+ '\n')
+ sys.stdout.write(os.linesep)
+
+ sys.stdout.write(
+ "Refs marked as EXCLUDE are excluded by either refFilterInclusionRegex\n"
+ "or refFilterExclusionRegex. No emails will be sent for commits included\n"
+ "in these refs.\n"
+ "Refs marked as DONT-SEND are excluded by either refFilterDoSendRegex or\n"
+ "refFilterDontSendRegex, but not by either refFilterInclusionRegex or\n"
+ "refFilterExclusionRegex. Emails will be sent for commits included in these\n"
+ "refs only when the commit reaches a ref which isn't excluded.\n"
+ "Refs marked as DO-SEND are not excluded by any filter. Emails will\n"
+ "be sent normally for commits included in these refs.\n")
+
+ sys.stdout.write(os.linesep)
+
+ for refname in read_git_lines(['for-each-ref', '--format', '%(refname)']):
+ sys.stdout.write(refname)
+ if not include_ref(refname, ref_filter_regex, ref_is_inclusion):
+ sys.stdout.write(' EXCLUDE')
+ elif not include_ref(refname, send_filter_regex, send_is_inclusion):
+ sys.stdout.write(' DONT-SEND')
+ else:
+ sys.stdout.write(' DO-SEND')
+
+ sys.stdout.write(os.linesep)
+
+
+def show_env(environment, out):
+ out.write('Environment values:\n')
+ for (k, v) in sorted(environment.get_values().items()):
+ if k: # Don't show the {'' : ''} pair.
+ out.write(' %s : %r\n' % (k, v))
+ out.write('\n')
+ # Flush to avoid interleaving with further log output
+ out.flush()
+
+
+def check_setup(environment):
+ environment.check()
+ show_env(environment, sys.stdout)
+ sys.stdout.write("Now, checking that git-multimail's standard input "
+ "is properly set ..." + os.linesep)
+ sys.stdout.write("Please type some text and then press Return" + os.linesep)
+ stdin = sys.stdin.readline()
+ sys.stdout.write("You have just entered:" + os.linesep)
+ sys.stdout.write(stdin)
+ sys.stdout.write("git-multimail seems properly set up." + os.linesep)
def choose_mailer(config, environment):
@@ -3418,55 +3772,56 @@ def choose_mailer(config, environment):
smtpencryption = config.get('smtpencryption', default='none')
smtpuser = config.get('smtpuser', default='')
smtppass = config.get('smtppass', default='')
+ smtpcacerts = config.get('smtpcacerts', default='')
mailer = SMTPMailer(
+ environment,
envelopesender=(environment.get_sender() or environment.get_fromaddr()),
smtpserver=smtpserver, smtpservertimeout=smtpservertimeout,
smtpserverdebuglevel=smtpserverdebuglevel,
smtpencryption=smtpencryption,
smtpuser=smtpuser,
smtppass=smtppass,
+ smtpcacerts=smtpcacerts
)
elif mailer == 'sendmail':
command = config.get('sendmailcommand')
if command:
command = shlex.split(command)
- mailer = SendMailer(command=command, envelopesender=environment.get_sender())
+ mailer = SendMailer(environment,
+ command=command, envelopesender=environment.get_sender())
else:
environment.log_error(
'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer +
- 'please use one of "smtp" or "sendmail".\n'
+ 'please use one of "smtp" or "sendmail".'
)
sys.exit(1)
return mailer
KNOWN_ENVIRONMENTS = {
- 'generic': GenericEnvironmentMixin,
- 'gitolite': GitoliteEnvironmentMixin,
- 'stash': StashEnvironmentMixin,
- 'gerrit': GerritEnvironmentMixin,
+ 'generic': {'highprec': GenericEnvironmentMixin},
+ 'gitolite': {'highprec': GitoliteEnvironmentHighPrecMixin,
+ 'lowprec': GitoliteEnvironmentLowPrecMixin},
+ 'stash': {'highprec': StashEnvironmentHighPrecMixin,
+ 'lowprec': StashEnvironmentLowPrecMixin},
+ 'gerrit': {'highprec': GerritEnvironmentHighPrecMixin,
+ 'lowprec': GerritEnvironmentLowPrecMixin},
}
def choose_environment(config, osenv=None, env=None, recipients=None,
hook_info=None):
+ env_name = choose_environment_name(config, env, osenv)
+ environment_klass = build_environment_klass(env_name)
+ env = build_environment(environment_klass, env_name, config,
+ osenv, recipients, hook_info)
+ return env
+
+
+def choose_environment_name(config, env, osenv):
if not osenv:
osenv = os.environ
- environment_mixins = [
- ConfigRefFilterEnvironmentMixin,
- ProjectdescEnvironmentMixin,
- ConfigMaxlinesEnvironmentMixin,
- ComputeFQDNEnvironmentMixin,
- ConfigFilterLinesEnvironmentMixin,
- PusherDomainEnvironmentMixin,
- ConfigOptionsEnvironmentMixin,
- ]
- environment_kw = {
- 'osenv': osenv,
- 'config': config,
- }
-
if not env:
env = config.get('environment')
@@ -3475,8 +3830,58 @@ def choose_environment(config, osenv=None, env=None, recipients=None,
env = 'gitolite'
else:
env = 'generic'
+ return env
+
+
+COMMON_ENVIRONMENT_MIXINS = [
+ ConfigRecipientsEnvironmentMixin,
+ CLIRecipientsEnvironmentMixin,
+ ConfigRefFilterEnvironmentMixin,
+ ProjectdescEnvironmentMixin,
+ ConfigMaxlinesEnvironmentMixin,
+ ComputeFQDNEnvironmentMixin,
+ ConfigFilterLinesEnvironmentMixin,
+ PusherDomainEnvironmentMixin,
+ ConfigOptionsEnvironmentMixin,
+ ]
+
+
+def build_environment_klass(env_name):
+ if 'class' in KNOWN_ENVIRONMENTS[env_name]:
+ return KNOWN_ENVIRONMENTS[env_name]['class']
+
+ environment_mixins = []
+ known_env = KNOWN_ENVIRONMENTS[env_name]
+ if 'highprec' in known_env:
+ high_prec_mixin = known_env['highprec']
+ environment_mixins.append(high_prec_mixin)
+ environment_mixins = environment_mixins + COMMON_ENVIRONMENT_MIXINS
+ if 'lowprec' in known_env:
+ low_prec_mixin = known_env['lowprec']
+ environment_mixins.append(low_prec_mixin)
+ environment_mixins.append(Environment)
+ klass_name = env_name.capitalize() + 'Environement'
+ environment_klass = type(
+ klass_name,
+ tuple(environment_mixins),
+ {},
+ )
+ KNOWN_ENVIRONMENTS[env_name]['class'] = environment_klass
+ return environment_klass
+
- environment_mixins.insert(0, KNOWN_ENVIRONMENTS[env])
+GerritEnvironment = build_environment_klass('gerrit')
+StashEnvironment = build_environment_klass('stash')
+GitoliteEnvironment = build_environment_klass('gitolite')
+GenericEnvironment = build_environment_klass('generic')
+
+
+def build_environment(environment_klass, env, config,
+ osenv, recipients, hook_info):
+ environment_kw = {
+ 'osenv': osenv,
+ 'config': config,
+ }
if env == 'stash':
environment_kw['user'] = hook_info['stash_user']
@@ -3486,20 +3891,8 @@ def choose_environment(config, osenv=None, env=None, recipients=None,
environment_kw['submitter'] = hook_info['submitter']
environment_kw['update_method'] = hook_info['update_method']
- if recipients:
- environment_mixins.insert(0, StaticRecipientsEnvironmentMixin)
- environment_kw['refchange_recipients'] = recipients
- environment_kw['announce_recipients'] = recipients
- environment_kw['revision_recipients'] = recipients
- environment_kw['scancommitforcc'] = config.get('scancommitforcc')
- else:
- environment_mixins.insert(0, ConfigRecipientsEnvironmentMixin)
+ environment_kw['cli_recipients'] = recipients
- environment_klass = type(
- 'EffectiveEnvironment',
- tuple(environment_mixins) + (Environment,),
- {},
- )
return environment_klass(**environment_kw)
@@ -3520,7 +3913,8 @@ def get_version():
return __version__
-def compute_gerrit_options(options, args, required_gerrit_options):
+def compute_gerrit_options(options, args, required_gerrit_options,
+ raw_refname):
if None in required_gerrit_options:
raise SystemExit("Error: Specify all of --oldrev, --newrev, --refname, "
"and --project; or none of them.")
@@ -3537,24 +3931,11 @@ def compute_gerrit_options(options, args, required_gerrit_options):
# Gerrit oddly omits 'refs/heads/' in the refname when calling
# ref-updated hook; put it back.
git_dir = get_git_dir()
- if (not os.path.exists(os.path.join(git_dir, options.refname)) and
+ if (not os.path.exists(os.path.join(git_dir, raw_refname)) and
os.path.exists(os.path.join(git_dir, 'refs', 'heads',
- options.refname))):
+ raw_refname))):
options.refname = 'refs/heads/' + options.refname
- # Convert each string option unicode for Python3.
- if PYTHON3:
- opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname',
- 'project', 'submitter', 'stash-user', 'stash-repo']
- for opt in opts:
- if not hasattr(options, opt):
- continue
- obj = getattr(options, opt)
- if obj:
- enc = obj.encode('utf-8', 'surrogateescape')
- dec = enc.decode('utf-8', 'replace')
- setattr(options, opt, dec)
-
# New revisions can appear in a gerrit repository either due to someone
# pushing directly (in which case options.submitter will be set), or they
# can press "Submit this patchset" in the web UI for some CR (in which
@@ -3594,6 +3975,20 @@ def compute_gerrit_options(options, args, required_gerrit_options):
def check_hook_specific_args(options, args):
+ raw_refname = options.refname
+ # Convert each string option unicode for Python3.
+ if PYTHON3:
+ opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname',
+ 'project', 'submitter', 'stash_user', 'stash_repo']
+ for opt in opts:
+ if not hasattr(options, opt):
+ continue
+ obj = getattr(options, opt)
+ if obj:
+ enc = obj.encode('utf-8', 'surrogateescape')
+ dec = enc.decode('utf-8', 'replace')
+ setattr(options, opt, dec)
+
# First check for stash arguments
if (options.stash_user is None) != (options.stash_repo is None):
raise SystemExit("Error: Specify both of --stash-user and "
@@ -3607,12 +4002,78 @@ def check_hook_specific_args(options, args):
required_gerrit_options = (options.oldrev, options.newrev, options.refname,
options.project)
if required_gerrit_options != (None,) * 4:
- return compute_gerrit_options(options, args, required_gerrit_options)
+ return compute_gerrit_options(options, args, required_gerrit_options,
+ raw_refname)
# No special options in use, just return what we started with
return options, args, {}
+class Logger(object):
+ def parse_verbose(self, verbose):
+ if verbose > 0:
+ return logging.DEBUG
+ else:
+ return logging.INFO
+
+ def create_log_file(self, environment, name, path, verbosity):
+ log_file = logging.getLogger(name)
+ file_handler = logging.FileHandler(path)
+ log_fmt = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s")
+ file_handler.setFormatter(log_fmt)
+ log_file.addHandler(file_handler)
+ log_file.setLevel(verbosity)
+ return log_file
+
+ def __init__(self, environment):
+ self.environment = environment
+ self.loggers = []
+ stderr_log = logging.getLogger('git_multimail.stderr')
+
+ class EncodedStderr(object):
+ def write(self, x):
+ write_str(sys.stderr, x)
+
+ def flush(self):
+ sys.stderr.flush()
+
+ stderr_handler = logging.StreamHandler(EncodedStderr())
+ stderr_log.addHandler(stderr_handler)
+ stderr_log.setLevel(self.parse_verbose(environment.verbose))
+ self.loggers.append(stderr_log)
+
+ if environment.debug_log_file is not None:
+ debug_log_file = self.create_log_file(
+ environment, 'git_multimail.debug', environment.debug_log_file, logging.DEBUG)
+ self.loggers.append(debug_log_file)
+
+ if environment.log_file is not None:
+ log_file = self.create_log_file(
+ environment, 'git_multimail.file', environment.log_file, logging.INFO)
+ self.loggers.append(log_file)
+
+ if environment.error_log_file is not None:
+ error_log_file = self.create_log_file(
+ environment, 'git_multimail.error', environment.error_log_file, logging.ERROR)
+ self.loggers.append(error_log_file)
+
+ def info(self, msg):
+ for l in self.loggers:
+ l.info(msg)
+
+ def debug(self, msg):
+ for l in self.loggers:
+ l.debug(msg)
+
+ def warning(self, msg):
+ for l in self.loggers:
+ l.warning(msg)
+
+ def error(self, msg):
+ for l in self.loggers:
+ l.error(msg)
+
+
def main(args):
parser = optparse.OptionParser(
description=__doc__,
@@ -3639,7 +4100,7 @@ def main(args):
'--show-env', action='store_true', default=False,
help=(
'Write to stderr the values determined for the environment '
- '(intended for debugging purposes).'
+ '(intended for debugging purposes), then proceed normally.'
),
)
parser.add_option(
@@ -3664,6 +4125,22 @@ def main(args):
"Display git-multimail's version"
),
)
+
+ parser.add_option(
+ '--python-version', action='store_true', default=False,
+ help=(
+ "Display the version of Python used by git-multimail"
+ ),
+ )
+
+ parser.add_option(
+ '--check-ref-filter', action='store_true', default=False,
+ help=(
+ 'List refs and show information on how git-multimail '
+ 'will process them.'
+ )
+ )
+
# The following options permit this script to be run as a gerrit
# ref-updated hook. See e.g.
# code.google.com/p/gerrit/source/browse/Documentation/config-hooks.txt
@@ -3690,21 +4167,16 @@ def main(args):
sys.stdout.write('git-multimail version ' + get_version() + '\n')
return
+ if options.python_version:
+ sys.stdout.write('Python version ' + sys.version + '\n')
+ return
+
if options.c:
- parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '')
- if parameters:
- parameters += ' '
- # git expects GIT_CONFIG_PARAMETERS to be of the form
- # "'name1=value1' 'name2=value2' 'name3=value3'"
- # including everything inside the double quotes (but not the double
- # quotes themselves). Spacing is critical. Also, if a value contains
- # a literal single quote that quote must be represented using the
- # four character sequence: '\''
- parameters += ' '.join("'" + x.replace("'", "'\\''") + "'" for x in options.c)
- os.environ['GIT_CONFIG_PARAMETERS'] = parameters
+ Config.add_config_parameters(options.c)
config = Config('multimailhook')
+ environment = None
try:
environment = choose_environment(
config, osenv=os.environ,
@@ -3714,38 +4186,52 @@ def main(args):
)
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')
+ show_env(environment, sys.stderr)
if options.stdout or environment.stdout:
mailer = OutputMailer(sys.stdout)
else:
mailer = choose_mailer(config, environment)
+ must_check_setup = os.environ.get('GIT_MULTIMAIL_CHECK_SETUP')
+ if must_check_setup == '':
+ must_check_setup = False
+ if options.check_ref_filter:
+ check_ref_filter(environment)
+ elif must_check_setup:
+ check_setup(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:
+ elif args:
if len(args) != 3:
parser.error('Need zero or three non-option arguments')
(refname, oldrev, newrev) = args
+ environment.get_logger().debug(
+ "run_as_update_hook: refname=%s, oldrev=%s, newrev=%s, force_send=%s" %
+ (refname, oldrev, newrev, options.force_send))
run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send)
else:
run_as_post_receive_hook(environment, mailer)
except ConfigurationException:
sys.exit(sys.exc_info()[1])
+ except SystemExit:
+ raise
except Exception:
t, e, tb = sys.exc_info()
import traceback
- sys.stdout.write('\n')
- sys.stdout.write('Exception \'' + t.__name__ +
- '\' raised. Please report this as a bug to\n')
- sys.stdout.write('https://github.com/git-multimail/git-multimail/issues\n')
- sys.stdout.write('with the information below:\n\n')
- sys.stdout.write('git-multimail version ' + get_version() + '\n')
- sys.stdout.write('Python version ' + sys.version + '\n')
- traceback.print_exc(file=sys.stdout)
+ sys.stderr.write('\n') # Avoid mixing message with previous output
+ msg = (
+ 'Exception \'' + t.__name__ +
+ '\' raised. Please report this as a bug to\n'
+ 'https://github.com/git-multimail/git-multimail/issues\n'
+ 'with the information below:\n\n'
+ 'git-multimail version ' + get_version() + '\n'
+ 'Python version ' + sys.version + '\n' +
+ traceback.format_exc())
+ try:
+ environment.get_logger().error(msg)
+ except:
+ sys.stderr.write(msg)
sys.exit(1)
if __name__ == '__main__':
diff --git a/contrib/hooks/multimail/post-receive.example b/contrib/hooks/multimail/post-receive.example
index 9975df7107..1ea113d274 100755
--- a/contrib/hooks/multimail/post-receive.example
+++ b/contrib/hooks/multimail/post-receive.example
@@ -55,6 +55,12 @@ import git_multimail
# git-multimail:
config = git_multimail.Config('multimailhook')
+# Set some Git configuration variables. Equivalent to passing var=val
+# to "git -c var=val" each time git is called, or to adding the
+# configuration in .git/config (must come before instanciating the
+# environment) :
+#git_multimail.Config.add_config_parameters('multimailhook.commitEmailFormat=html')
+#git_multimail.Config.add_config_parameters(('user.name=foo', 'user.email=foo@example.com'))
# Select the type of environment:
try:
diff --git a/contrib/mw-to-git/.perlcriticrc b/contrib/mw-to-git/.perlcriticrc
index 5a9955d757..158958d363 100644
--- a/contrib/mw-to-git/.perlcriticrc
+++ b/contrib/mw-to-git/.perlcriticrc
@@ -19,7 +19,7 @@
[InputOutput::RequireCheckedSyscalls]
functions = open say close
-# This rules demands to add a dependancy for the Readonly module. This is not
+# This rule demands to add a dependency for the Readonly module. This is not
# wished.
[-ValuesAndExpressions::ProhibitConstantPragma]
diff --git a/contrib/mw-to-git/git-remote-mediawiki.perl b/contrib/mw-to-git/git-remote-mediawiki.perl
index 8dd74a9a40..41e74fba1e 100755
--- a/contrib/mw-to-git/git-remote-mediawiki.perl
+++ b/contrib/mw-to-git/git-remote-mediawiki.perl
@@ -963,7 +963,7 @@ sub mw_upload_file {
print {*STDERR} "Check the configuration of file uploads in your mediawiki.\n";
return $newrevid;
}
- # Deleting and uploading a file requires a priviledged user
+ # Deleting and uploading a file requires a privileged user
if ($file_deleted) {
$mediawiki = connect_maybe($mediawiki, $remotename, $url);
my $query = {
diff --git a/contrib/persistent-https/Makefile b/contrib/persistent-https/Makefile
index 92baa3beee..52b84ba3d4 100644
--- a/contrib/persistent-https/Makefile
+++ b/contrib/persistent-https/Makefile
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-BUILD_LABEL=$(shell date +"%s")
+BUILD_LABEL=$(shell cut -d" " -f3 ../../GIT-VERSION-FILE)
TAR_OUT=$(shell go env GOOS)_$(shell go env GOARCH).tar.gz
all: git-remote-persistent-https git-remote-persistent-https--proxy \
@@ -25,8 +25,10 @@ git-remote-persistent-http: git-remote-persistent-https
ln -f -s git-remote-persistent-https git-remote-persistent-http
git-remote-persistent-https:
+ case $$(go version) in \
+ "go version go"1.[0-5].*) EQ=" " ;; *) EQ="=" ;; esac && \
go build -o git-remote-persistent-https \
- -ldflags "-X main._BUILD_EMBED_LABEL $(BUILD_LABEL)"
+ -ldflags "-X main._BUILD_EMBED_LABEL$${EQ}$(BUILD_LABEL)"
clean:
rm -f git-remote-persistent-http* *.tar.gz
diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh
index 7a39b30ad0..dec085a235 100755
--- a/contrib/subtree/git-subtree.sh
+++ b/contrib/subtree/git-subtree.sh
@@ -4,8 +4,9 @@
#
# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
#
-if [ $# -eq 0 ]; then
- set -- -h
+if test $# -eq 0
+then
+ set -- -h
fi
OPTS_SPEC="\
git subtree add --prefix=<prefix> <commit>
@@ -48,89 +49,144 @@ squash=
message=
prefix=
-debug()
-{
- if [ -n "$debug" ]; then
+debug () {
+ if test -n "$debug"
+ then
printf "%s\n" "$*" >&2
fi
}
-say()
-{
- if [ -z "$quiet" ]; then
+say () {
+ if test -z "$quiet"
+ then
printf "%s\n" "$*" >&2
fi
}
-progress()
-{
- if [ -z "$quiet" ]; then
+progress () {
+ if test -z "$quiet"
+ then
printf "%s\r" "$*" >&2
fi
}
-assert()
-{
- if "$@"; then
- :
- else
+assert () {
+ if ! "$@"
+ then
die "assertion failed: " "$@"
fi
}
-#echo "Options: $*"
-
-while [ $# -gt 0 ]; do
+while test $# -gt 0
+do
opt="$1"
shift
+
case "$opt" in
- -q) quiet=1 ;;
- -d) debug=1 ;;
- --annotate) annotate="$1"; shift ;;
- --no-annotate) annotate= ;;
- -b) branch="$1"; shift ;;
- -P) prefix="${1%/}"; shift ;;
- -m) message="$1"; shift ;;
- --no-prefix) prefix= ;;
- --onto) onto="$1"; shift ;;
- --no-onto) onto= ;;
- --rejoin) rejoin=1 ;;
- --no-rejoin) rejoin= ;;
- --ignore-joins) ignore_joins=1 ;;
- --no-ignore-joins) ignore_joins= ;;
- --squash) squash=1 ;;
- --no-squash) squash= ;;
- --) break ;;
- *) die "Unexpected option: $opt" ;;
+ -q)
+ quiet=1
+ ;;
+ -d)
+ debug=1
+ ;;
+ --annotate)
+ annotate="$1"
+ shift
+ ;;
+ --no-annotate)
+ annotate=
+ ;;
+ -b)
+ branch="$1"
+ shift
+ ;;
+ -P)
+ prefix="${1%/}"
+ shift
+ ;;
+ -m)
+ message="$1"
+ shift
+ ;;
+ --no-prefix)
+ prefix=
+ ;;
+ --onto)
+ onto="$1"
+ shift
+ ;;
+ --no-onto)
+ onto=
+ ;;
+ --rejoin)
+ rejoin=1
+ ;;
+ --no-rejoin)
+ rejoin=
+ ;;
+ --ignore-joins)
+ ignore_joins=1
+ ;;
+ --no-ignore-joins)
+ ignore_joins=
+ ;;
+ --squash)
+ squash=1
+ ;;
+ --no-squash)
+ squash=
+ ;;
+ --)
+ break
+ ;;
+ *)
+ die "Unexpected option: $opt"
+ ;;
esac
done
command="$1"
shift
+
case "$command" in
- add|merge|pull) default= ;;
- split|push) default="--default HEAD" ;;
- *) die "Unknown command '$command'" ;;
+add|merge|pull)
+ default=
+ ;;
+split|push)
+ default="--default HEAD"
+ ;;
+*)
+ die "Unknown command '$command'"
+ ;;
esac
-if [ -z "$prefix" ]; then
+if test -z "$prefix"
+then
die "You must provide the --prefix option."
fi
case "$command" in
- add) [ -e "$prefix" ] &&
- die "prefix '$prefix' already exists." ;;
- *) [ -e "$prefix" ] ||
- die "'$prefix' does not exist; use 'git subtree add'" ;;
+add)
+ test -e "$prefix" &&
+ die "prefix '$prefix' already exists."
+ ;;
+*)
+ test -e "$prefix" ||
+ die "'$prefix' does not exist; use 'git subtree add'"
+ ;;
esac
dir="$(dirname "$prefix/.")"
-if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
+if test "$command" != "pull" &&
+ test "$command" != "add" &&
+ test "$command" != "push"
+then
revs=$(git rev-parse $default --revs-only "$@") || exit $?
- dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
- if [ -n "$dirs" ]; then
+ dirs=$(git rev-parse --no-revs --no-flags "$@") || exit $?
+ if test -n "$dirs"
+ then
die "Error: Use --prefix instead of bare filenames."
fi
fi
@@ -142,78 +198,82 @@ debug "dir: {$dir}"
debug "opts: {$*}"
debug
-cache_setup()
-{
+cache_setup () {
cachedir="$GIT_DIR/subtree-cache/$$"
- rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
- mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
- mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree"
+ rm -rf "$cachedir" ||
+ die "Can't delete old cachedir: $cachedir"
+ mkdir -p "$cachedir" ||
+ die "Can't create new cachedir: $cachedir"
+ mkdir -p "$cachedir/notree" ||
+ die "Can't create new cachedir: $cachedir/notree"
debug "Using cachedir: $cachedir" >&2
}
-cache_get()
-{
- for oldrev in $*; do
- if [ -r "$cachedir/$oldrev" ]; then
+cache_get () {
+ for oldrev in "$@"
+ do
+ if test -r "$cachedir/$oldrev"
+ then
read newrev <"$cachedir/$oldrev"
echo $newrev
fi
done
}
-cache_miss()
-{
- for oldrev in $*; do
- if [ ! -r "$cachedir/$oldrev" ]; then
+cache_miss () {
+ for oldrev in "$@"
+ do
+ if ! test -r "$cachedir/$oldrev"
+ then
echo $oldrev
fi
done
}
-check_parents()
-{
- missed=$(cache_miss $*)
- for miss in $missed; do
- if [ ! -r "$cachedir/notree/$miss" ]; then
+check_parents () {
+ missed=$(cache_miss "$@")
+ for miss in $missed
+ do
+ if ! test -r "$cachedir/notree/$miss"
+ then
debug " incorrect order: $miss"
fi
done
}
-set_notree()
-{
+set_notree () {
echo "1" > "$cachedir/notree/$1"
}
-cache_set()
-{
+cache_set () {
oldrev="$1"
newrev="$2"
- if [ "$oldrev" != "latest_old" \
- -a "$oldrev" != "latest_new" \
- -a -e "$cachedir/$oldrev" ]; then
+ if test "$oldrev" != "latest_old" &&
+ test "$oldrev" != "latest_new" &&
+ test -e "$cachedir/$oldrev"
+ then
die "cache for $oldrev already exists!"
fi
echo "$newrev" >"$cachedir/$oldrev"
}
-rev_exists()
-{
- if git rev-parse "$1" >/dev/null 2>&1; then
+rev_exists () {
+ if git rev-parse "$1" >/dev/null 2>&1
+ then
return 0
else
return 1
fi
}
-rev_is_descendant_of_branch()
-{
+rev_is_descendant_of_branch () {
newrev="$1"
branch="$2"
- branch_hash=$(git rev-parse $branch)
- match=$(git rev-list -1 $branch_hash ^$newrev)
+ branch_hash=$(git rev-parse "$branch")
+ match=$(git rev-list -1 "$branch_hash" "^$newrev")
- if [ -z "$match" ]; then
+ if test -z "$match"
+ then
return 0
else
return 1
@@ -223,15 +283,14 @@ rev_is_descendant_of_branch()
# if a commit doesn't have a parent, this might not work. But we only want
# to remove the parent from the rev-list, and since it doesn't exist, it won't
# be there anyway, so do nothing in that case.
-try_remove_previous()
-{
- if rev_exists "$1^"; then
+try_remove_previous () {
+ if rev_exists "$1^"
+ then
echo "^$1^"
fi
}
-find_latest_squash()
-{
+find_latest_squash () {
debug "Looking for latest squash ($dir)..."
dir="$1"
sq=
@@ -239,37 +298,43 @@ find_latest_squash()
sub=
git log --grep="^git-subtree-dir: $dir/*\$" \
--pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
- while read a b junk; do
+ while read a b junk
+ do
debug "$a $b $junk"
debug "{{$sq/$main/$sub}}"
case "$a" in
- START) sq="$b" ;;
- git-subtree-mainline:) main="$b" ;;
- git-subtree-split:)
- sub="$(git rev-parse "$b^0")" ||
- die "could not rev-parse split hash $b from commit $sq"
- ;;
- END)
- if [ -n "$sub" ]; then
- if [ -n "$main" ]; then
- # a rejoin commit?
- # Pretend its sub was a squash.
- sq="$sub"
- fi
- debug "Squash found: $sq $sub"
- echo "$sq" "$sub"
- break
+ START)
+ sq="$b"
+ ;;
+ git-subtree-mainline:)
+ main="$b"
+ ;;
+ git-subtree-split:)
+ sub="$(git rev-parse "$b^0")" ||
+ die "could not rev-parse split hash $b from commit $sq"
+ ;;
+ END)
+ if test -n "$sub"
+ then
+ if test -n "$main"
+ then
+ # a rejoin commit?
+ # Pretend its sub was a squash.
+ sq="$sub"
fi
- sq=
- main=
- sub=
- ;;
+ debug "Squash found: $sq $sub"
+ echo "$sq" "$sub"
+ break
+ fi
+ sq=
+ main=
+ sub=
+ ;;
esac
done
}
-find_existing_splits()
-{
+find_existing_splits () {
debug "Looking for prior splits..."
dir="$1"
revs="$2"
@@ -277,37 +342,43 @@ find_existing_splits()
sub=
git log --grep="^git-subtree-dir: $dir/*\$" \
--pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
- while read a b junk; do
+ while read a b junk
+ do
case "$a" in
- START) sq="$b" ;;
- git-subtree-mainline:) main="$b" ;;
- git-subtree-split:)
- sub="$(git rev-parse "$b^0")" ||
- die "could not rev-parse split hash $b from commit $sq"
- ;;
- END)
- debug " Main is: '$main'"
- if [ -z "$main" -a -n "$sub" ]; then
- # squash commits refer to a subtree
- debug " Squash: $sq from $sub"
- cache_set "$sq" "$sub"
- fi
- if [ -n "$main" -a -n "$sub" ]; then
- debug " Prior: $main -> $sub"
- cache_set $main $sub
- cache_set $sub $sub
- try_remove_previous "$main"
- try_remove_previous "$sub"
- fi
- main=
- sub=
- ;;
+ START)
+ sq="$b"
+ ;;
+ git-subtree-mainline:)
+ main="$b"
+ ;;
+ git-subtree-split:)
+ sub="$(git rev-parse "$b^0")" ||
+ die "could not rev-parse split hash $b from commit $sq"
+ ;;
+ END)
+ debug " Main is: '$main'"
+ if test -z "$main" -a -n "$sub"
+ then
+ # squash commits refer to a subtree
+ debug " Squash: $sq from $sub"
+ cache_set "$sq" "$sub"
+ fi
+ if test -n "$main" -a -n "$sub"
+ then
+ debug " Prior: $main -> $sub"
+ cache_set $main $sub
+ cache_set $sub $sub
+ try_remove_previous "$main"
+ try_remove_previous "$sub"
+ fi
+ main=
+ sub=
+ ;;
esac
done
}
-copy_commit()
-{
+copy_commit () {
# We're going to set some environment vars here, so
# do it in a subshell to get rid of them safely later
debug copy_commit "{$1}" "{$2}" "{$3}"
@@ -325,66 +396,69 @@ copy_commit()
GIT_COMMITTER_NAME \
GIT_COMMITTER_EMAIL \
GIT_COMMITTER_DATE
- (printf "%s" "$annotate"; cat ) |
+ (
+ printf "%s" "$annotate"
+ cat
+ ) |
git commit-tree "$2" $3 # reads the rest of stdin
) || die "Can't copy commit $1"
}
-add_msg()
-{
+add_msg () {
dir="$1"
latest_old="$2"
latest_new="$3"
- if [ -n "$message" ]; then
+ if test -n "$message"
+ then
commit_message="$message"
else
commit_message="Add '$dir/' from commit '$latest_new'"
fi
cat <<-EOF
$commit_message
-
+
git-subtree-dir: $dir
git-subtree-mainline: $latest_old
git-subtree-split: $latest_new
EOF
}
-add_squashed_msg()
-{
- if [ -n "$message" ]; then
+add_squashed_msg () {
+ if test -n "$message"
+ then
echo "$message"
else
echo "Merge commit '$1' as '$2'"
fi
}
-rejoin_msg()
-{
+rejoin_msg () {
dir="$1"
latest_old="$2"
latest_new="$3"
- if [ -n "$message" ]; then
+ if test -n "$message"
+ then
commit_message="$message"
else
commit_message="Split '$dir/' into commit '$latest_new'"
fi
cat <<-EOF
$commit_message
-
+
git-subtree-dir: $dir
git-subtree-mainline: $latest_old
git-subtree-split: $latest_new
EOF
}
-squash_msg()
-{
+squash_msg () {
dir="$1"
oldsub="$2"
newsub="$3"
newsub_short=$(git rev-parse --short "$newsub")
-
- if [ -n "$oldsub" ]; then
+
+ if test -n "$oldsub"
+ then
oldsub_short=$(git rev-parse --short "$oldsub")
echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
echo
@@ -393,41 +467,41 @@ squash_msg()
else
echo "Squashed '$dir/' content from commit $newsub_short"
fi
-
+
echo
echo "git-subtree-dir: $dir"
echo "git-subtree-split: $newsub"
}
-toptree_for_commit()
-{
+toptree_for_commit () {
commit="$1"
git log -1 --pretty=format:'%T' "$commit" -- || exit $?
}
-subtree_for_commit()
-{
+subtree_for_commit () {
commit="$1"
dir="$2"
git ls-tree "$commit" -- "$dir" |
- while read mode type tree name; do
- assert [ "$name" = "$dir" ]
- assert [ "$type" = "tree" -o "$type" = "commit" ]
- [ "$type" = "commit" ] && continue # ignore submodules
+ while read mode type tree name
+ do
+ assert test "$name" = "$dir"
+ assert test "$type" = "tree" -o "$type" = "commit"
+ test "$type" = "commit" && continue # ignore submodules
echo $tree
break
done
}
-tree_changed()
-{
+tree_changed () {
tree=$1
shift
- if [ $# -ne 1 ]; then
+ if test $# -ne 1
+ then
return 0 # weird parents, consider it changed
else
ptree=$(toptree_for_commit $1)
- if [ "$ptree" != "$tree" ]; then
+ if test "$ptree" != "$tree"
+ then
return 0 # changed
else
return 1 # not changed
@@ -435,118 +509,127 @@ tree_changed()
fi
}
-new_squash_commit()
-{
+new_squash_commit () {
old="$1"
oldsub="$2"
newsub="$3"
tree=$(toptree_for_commit $newsub) || exit $?
- if [ -n "$old" ]; then
- squash_msg "$dir" "$oldsub" "$newsub" |
- git commit-tree "$tree" -p "$old" || exit $?
+ if test -n "$old"
+ then
+ squash_msg "$dir" "$oldsub" "$newsub" |
+ git commit-tree "$tree" -p "$old" || exit $?
else
squash_msg "$dir" "" "$newsub" |
- git commit-tree "$tree" || exit $?
+ git commit-tree "$tree" || exit $?
fi
}
-copy_or_skip()
-{
+copy_or_skip () {
rev="$1"
tree="$2"
newparents="$3"
- assert [ -n "$tree" ]
+ assert test -n "$tree"
identical=
nonidentical=
p=
gotparents=
- for parent in $newparents; do
+ for parent in $newparents
+ do
ptree=$(toptree_for_commit $parent) || exit $?
- [ -z "$ptree" ] && continue
- if [ "$ptree" = "$tree" ]; then
+ test -z "$ptree" && continue
+ if test "$ptree" = "$tree"
+ then
# an identical parent could be used in place of this rev.
identical="$parent"
else
nonidentical="$parent"
fi
-
+
# sometimes both old parents map to the same newparent;
# eliminate duplicates
is_new=1
- for gp in $gotparents; do
- if [ "$gp" = "$parent" ]; then
+ for gp in $gotparents
+ do
+ if test "$gp" = "$parent"
+ then
is_new=
break
fi
done
- if [ -n "$is_new" ]; then
+ if test -n "$is_new"
+ then
gotparents="$gotparents $parent"
p="$p -p $parent"
fi
done
copycommit=
- if [ -n "$identical" ] && [ -n "$nonidentical" ]; then
+ if test -n "$identical" && test -n "$nonidentical"
+ then
extras=$(git rev-list --count $identical..$nonidentical)
- if [ "$extras" -ne 0 ]; then
+ if test "$extras" -ne 0
+ then
# we need to preserve history along the other branch
copycommit=1
fi
fi
- if [ -n "$identical" ] && [ -z "$copycommit" ]; then
+ if test -n "$identical" && test -z "$copycommit"
+ then
echo $identical
else
- copy_commit $rev $tree "$p" || exit $?
+ copy_commit "$rev" "$tree" "$p" || exit $?
fi
}
-ensure_clean()
-{
- if ! git diff-index HEAD --exit-code --quiet 2>&1; then
+ensure_clean () {
+ if ! git diff-index HEAD --exit-code --quiet 2>&1
+ then
die "Working tree has modifications. Cannot add."
fi
- if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
+ if ! git diff-index --cached HEAD --exit-code --quiet 2>&1
+ then
die "Index has modifications. Cannot add."
fi
}
-ensure_valid_ref_format()
-{
+ensure_valid_ref_format () {
git check-ref-format "refs/heads/$1" ||
- die "'$1' does not look like a ref"
+ die "'$1' does not look like a ref"
}
-cmd_add()
-{
- if [ -e "$dir" ]; then
+cmd_add () {
+ if test -e "$dir"
+ then
die "'$dir' already exists. Cannot add."
fi
ensure_clean
-
- if [ $# -eq 1 ]; then
- git rev-parse -q --verify "$1^{commit}" >/dev/null ||
- die "'$1' does not refer to a commit"
-
- "cmd_add_commit" "$@"
- elif [ $# -eq 2 ]; then
- # Technically we could accept a refspec here but we're
- # just going to turn around and add FETCH_HEAD under the
- # specified directory. Allowing a refspec might be
- # misleading because we won't do anything with any other
- # branches fetched via the refspec.
- ensure_valid_ref_format "$2"
-
- "cmd_add_repository" "$@"
+
+ if test $# -eq 1
+ then
+ git rev-parse -q --verify "$1^{commit}" >/dev/null ||
+ die "'$1' does not refer to a commit"
+
+ cmd_add_commit "$@"
+
+ elif test $# -eq 2
+ then
+ # Technically we could accept a refspec here but we're
+ # just going to turn around and add FETCH_HEAD under the
+ # specified directory. Allowing a refspec might be
+ # misleading because we won't do anything with any other
+ # branches fetched via the refspec.
+ ensure_valid_ref_format "$2"
+
+ cmd_add_repository "$@"
else
- say "error: parameters were '$@'"
- die "Provide either a commit or a repository and commit."
+ say "error: parameters were '$@'"
+ die "Provide either a commit or a repository and commit."
fi
}
-cmd_add_repository()
-{
+cmd_add_repository () {
echo "git fetch" "$@"
repository=$1
refspec=$2
@@ -556,60 +639,63 @@ cmd_add_repository()
cmd_add_commit "$@"
}
-cmd_add_commit()
-{
+cmd_add_commit () {
revs=$(git rev-parse $default --revs-only "$@") || exit $?
set -- $revs
rev="$1"
-
+
debug "Adding $dir as '$rev'..."
git read-tree --prefix="$dir" $rev || exit $?
git checkout -- "$dir" || exit $?
tree=$(git write-tree) || exit $?
-
+
headrev=$(git rev-parse HEAD) || exit $?
- if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
+ if test -n "$headrev" && test "$headrev" != "$rev"
+ then
headp="-p $headrev"
else
headp=
fi
-
- if [ -n "$squash" ]; then
+
+ if test -n "$squash"
+ then
rev=$(new_squash_commit "" "" "$rev") || exit $?
commit=$(add_squashed_msg "$rev" "$dir" |
- git commit-tree $tree $headp -p "$rev") || exit $?
+ git commit-tree "$tree" $headp -p "$rev") || exit $?
else
revp=$(peel_committish "$rev") &&
- commit=$(add_msg "$dir" "$headrev" "$rev" |
- git commit-tree $tree $headp -p "$revp") || exit $?
+ commit=$(add_msg "$dir" $headrev "$rev" |
+ git commit-tree "$tree" $headp -p "$revp") || exit $?
fi
git reset "$commit" || exit $?
-
+
say "Added dir '$dir'"
}
-cmd_split()
-{
+cmd_split () {
debug "Splitting $dir..."
cache_setup || exit $?
-
- if [ -n "$onto" ]; then
+
+ if test -n "$onto"
+ then
debug "Reading history for --onto=$onto..."
git rev-list $onto |
- while read rev; do
+ while read rev
+ do
# the 'onto' history is already just the subdir, so
# any parent we find there can be used verbatim
debug " cache: $rev"
- cache_set $rev $rev
+ cache_set "$rev" "$rev"
done
fi
-
- if [ -n "$ignore_joins" ]; then
+
+ if test -n "$ignore_joins"
+ then
unrevs=
else
unrevs="$(find_existing_splits "$dir" "$revs")"
fi
-
+
# We can't restrict rev-list to only $dir here, because some of our
# parents have the $dir contents the root, and those won't match.
# (and rev-list --follow doesn't seem to solve this)
@@ -618,12 +704,14 @@ cmd_split()
revcount=0
createcount=0
eval "$grl" |
- while read rev parents; do
+ while read rev parents
+ do
revcount=$(($revcount + 1))
progress "$revcount/$revmax ($createcount)"
debug "Processing commit: $rev"
- exists=$(cache_get $rev)
- if [ -n "$exists" ]; then
+ exists=$(cache_get "$rev")
+ if test -n "$exists"
+ then
debug " prior: $exists"
continue
fi
@@ -631,76 +719,89 @@ cmd_split()
debug " parents: $parents"
newparents=$(cache_get $parents)
debug " newparents: $newparents"
-
- tree=$(subtree_for_commit $rev "$dir")
+
+ tree=$(subtree_for_commit "$rev" "$dir")
debug " tree is: $tree"
check_parents $parents
-
+
# ugly. is there no better way to tell if this is a subtree
# vs. a mainline commit? Does it matter?
- if [ -z $tree ]; then
- set_notree $rev
- if [ -n "$newparents" ]; then
- cache_set $rev $rev
+ if test -z "$tree"
+ then
+ set_notree "$rev"
+ if test -n "$newparents"
+ then
+ cache_set "$rev" "$rev"
fi
continue
fi
newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
debug " newrev is: $newrev"
- cache_set $rev $newrev
- cache_set latest_new $newrev
- cache_set latest_old $rev
+ cache_set "$rev" "$newrev"
+ cache_set latest_new "$newrev"
+ cache_set latest_old "$rev"
done || exit $?
+
latest_new=$(cache_get latest_new)
- if [ -z "$latest_new" ]; then
+ if test -z "$latest_new"
+ then
die "No new revisions were found"
fi
-
- if [ -n "$rejoin" ]; then
+
+ if test -n "$rejoin"
+ then
debug "Merging split branch into HEAD..."
latest_old=$(cache_get latest_old)
git merge -s ours \
- -m "$(rejoin_msg "$dir" $latest_old $latest_new)" \
- $latest_new >&2 || exit $?
- fi
- if [ -n "$branch" ]; then
- if rev_exists "refs/heads/$branch"; then
- if ! rev_is_descendant_of_branch $latest_new $branch; then
+ --allow-unrelated-histories \
+ -m "$(rejoin_msg "$dir" "$latest_old" "$latest_new")" \
+ "$latest_new" >&2 || exit $?
+ fi
+ if test -n "$branch"
+ then
+ if rev_exists "refs/heads/$branch"
+ then
+ if ! rev_is_descendant_of_branch "$latest_new" "$branch"
+ then
die "Branch '$branch' is not an ancestor of commit '$latest_new'."
fi
action='Updated'
else
action='Created'
fi
- git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
+ git update-ref -m 'subtree split' \
+ "refs/heads/$branch" "$latest_new" || exit $?
say "$action branch '$branch'"
fi
- echo $latest_new
+ echo "$latest_new"
exit 0
}
-cmd_merge()
-{
+cmd_merge () {
revs=$(git rev-parse $default --revs-only "$@") || exit $?
ensure_clean
-
+
set -- $revs
- if [ $# -ne 1 ]; then
+ if test $# -ne 1
+ then
die "You must provide exactly one revision. Got: '$revs'"
fi
rev="$1"
-
- if [ -n "$squash" ]; then
+
+ if test -n "$squash"
+ then
first_split="$(find_latest_squash "$dir")"
- if [ -z "$first_split" ]; then
+ if test -z "$first_split"
+ then
die "Can't squash-merge: '$dir' was never added."
fi
set $first_split
old=$1
sub=$2
- if [ "$sub" = "$rev" ]; then
+ if test "$sub" = "$rev"
+ then
say "Subtree is already at commit $rev."
exit 0
fi
@@ -710,25 +811,29 @@ cmd_merge()
fi
version=$(git version)
- if [ "$version" \< "git version 1.7" ]; then
- if [ -n "$message" ]; then
- git merge -s subtree --message="$message" $rev
+ if test "$version" \< "git version 1.7"
+ then
+ if test -n "$message"
+ then
+ git merge -s subtree --message="$message" "$rev"
else
- git merge -s subtree $rev
+ git merge -s subtree "$rev"
fi
else
- if [ -n "$message" ]; then
- git merge -Xsubtree="$prefix" --message="$message" $rev
+ if test -n "$message"
+ then
+ git merge -Xsubtree="$prefix" \
+ --message="$message" "$rev"
else
git merge -Xsubtree="$prefix" $rev
fi
fi
}
-cmd_pull()
-{
- if [ $# -ne 2 ]; then
- die "You must provide <repository> <ref>"
+cmd_pull () {
+ if test $# -ne 2
+ then
+ die "You must provide <repository> <ref>"
fi
ensure_clean
ensure_valid_ref_format "$2"
@@ -738,20 +843,21 @@ cmd_pull()
cmd_merge "$@"
}
-cmd_push()
-{
- if [ $# -ne 2 ]; then
- die "You must provide <repository> <ref>"
+cmd_push () {
+ if test $# -ne 2
+ then
+ die "You must provide <repository> <ref>"
fi
ensure_valid_ref_format "$2"
- if [ -e "$dir" ]; then
- repository=$1
- refspec=$2
- echo "git push using: " $repository $refspec
- localrev=$(git subtree split --prefix="$prefix") || die
- git push "$repository" $localrev:refs/heads/$refspec
+ if test -e "$dir"
+ then
+ repository=$1
+ refspec=$2
+ echo "git push using: " "$repository" "$refspec"
+ localrev=$(git subtree split --prefix="$prefix") || die
+ git push "$repository" "$localrev":"refs/heads/$refspec"
else
- die "'$dir' must already exist. Try 'git subtree add'."
+ die "'$dir' must already exist. Try 'git subtree add'."
fi
}
diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh
index 3bf96a9bb6..3c87ebaf57 100755
--- a/contrib/subtree/t/t7900-subtree.sh
+++ b/contrib/subtree/t/t7900-subtree.sh
@@ -16,16 +16,16 @@ export TEST_DIRECTORY
subtree_test_create_repo()
{
- test_create_repo "$1"
+ test_create_repo "$1" &&
(
- cd $1
+ cd "$1" &&
git config log.date relative
)
}
create()
{
- echo "$1" >"$1"
+ echo "$1" >"$1" &&
git add "$1"
}
@@ -71,12 +71,12 @@ join_commits()
}
test_create_commit() (
- repo=$1
- commit=$2
- cd "$repo"
- mkdir -p $(dirname "$commit") \
+ repo=$1 &&
+ commit=$2 &&
+ cd "$repo" &&
+ mkdir -p "$(dirname "$commit")" \
|| error "Could not create directory for commit"
- echo "$commit" >"$commit"
+ echo "$commit" >"$commit" &&
git add "$commit" || error "Could not add commit"
git commit -m "$commit" || error "Could not commit"
)
@@ -347,6 +347,22 @@ test_expect_success 'split sub dir/ with --rejoin' '
'
next_test
+test_expect_success 'split sub dir/ with --rejoin from scratch' '
+ subtree_test_create_repo "$subtree_test_count" &&
+ test_create_commit "$subtree_test_count" main1 &&
+ (
+ cd "$subtree_test_count" &&
+ mkdir "sub dir" &&
+ echo file >"sub dir"/file &&
+ git add "sub dir/file" &&
+ git commit -m"sub dir file" &&
+ split_hash=$(git subtree split --prefix="sub dir" --rejoin) &&
+ git subtree split --prefix="sub dir" --rejoin &&
+ check_equal "$(last_commit_message)" "Split '\''sub dir/'\'' into commit '\''$split_hash'\''"
+ )
+ '
+
+next_test
test_expect_success 'split sub dir/ with --rejoin and --message' '
subtree_test_create_repo "$subtree_test_count" &&
subtree_test_create_repo "$subtree_test_count/sub proj" &&
@@ -932,7 +948,7 @@ test_expect_success 'split a new subtree without --onto option' '
# also test that we still can split out an entirely new subtree
# if the parent of the first commit in the tree is not empty,
- # then the new subtree has accidently been attached to something
+ # then the new subtree has accidentally been attached to something
git subtree split --prefix="sub dir2" --branch subproj2-br &&
check_equal "$(git log --pretty=format:%P -1 subproj2-br)" ""
)