diff options
Diffstat (limited to 'contrib')
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)" "" ) |