summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rw-r--r--contrib/completion/git-completion.bash158
-rw-r--r--contrib/completion/git-completion.tcsh2
-rw-r--r--contrib/completion/git-completion.zsh2
-rw-r--r--contrib/completion/git-prompt.sh45
-rw-r--r--contrib/contacts/.gitignore3
-rw-r--r--contrib/contacts/Makefile71
-rwxr-xr-xcontrib/convert-grafts-to-replace-refs.sh28
-rw-r--r--contrib/convert-objects/git-convert-objects.txt2
-rw-r--r--contrib/credential/wincred/Makefile16
-rw-r--r--contrib/credential/wincred/git-credential-wincred.c25
-rw-r--r--contrib/diff-highlight/README41
-rwxr-xr-xcontrib/diff-highlight/diff-highlight75
-rw-r--r--contrib/diffall/README31
-rwxr-xr-xcontrib/diffall/git-diffall257
-rw-r--r--contrib/examples/builtin-fetch--tool.c3
-rwxr-xr-xcontrib/examples/git-clone.sh2
-rwxr-xr-xcontrib/examples/git-commit.sh4
-rwxr-xr-xcontrib/examples/git-merge.sh4
-rwxr-xr-xcontrib/examples/git-repack.sh4
-rwxr-xr-xcontrib/examples/git-resolve.sh2
-rw-r--r--contrib/examples/git-svnimport.txt2
-rw-r--r--contrib/gitview/gitview.txt2
-rw-r--r--contrib/hooks/multimail/CHANGES53
-rw-r--r--contrib/hooks/multimail/README320
-rw-r--r--contrib/hooks/multimail/README.Git6
-rwxr-xr-xcontrib/hooks/multimail/git_multimail.py973
-rwxr-xr-xcontrib/hooks/multimail/migrate-mailhook-config18
-rwxr-xr-xcontrib/hooks/multimail/post-receive.example (renamed from contrib/hooks/multimail/post-receive)27
-rwxr-xr-xcontrib/hooks/pre-auto-gc-battery2
-rwxr-xr-xcontrib/mw-to-git/git-remote-mediawiki.perl7
-rwxr-xr-xcontrib/mw-to-git/t/install-wiki.sh10
-rwxr-xr-xcontrib/mw-to-git/t/t9363-mw-to-git-export-import.sh19
-rwxr-xr-xcontrib/mw-to-git/t/t9365-continuing-queries.sh2
-rwxr-xr-xcontrib/mw-to-git/t/test-gitmw-lib.sh8
-rw-r--r--contrib/subtree/.gitignore3
-rw-r--r--contrib/subtree/Makefile58
-rwxr-xr-xcontrib/subtree/git-subtree.sh20
-rw-r--r--contrib/subtree/git-subtree.txt196
-rwxr-xr-xcontrib/subtree/t/t7900-subtree.sh308
-rw-r--r--contrib/svn-fe/Makefile60
-rwxr-xr-xcontrib/svn-fe/svnrdump_sim.py93
-rwxr-xr-xcontrib/thunderbird-patch-inline/appp.sh14
-rw-r--r--contrib/vim/README22
-rwxr-xr-xcontrib/workdir/git-new-workdir53
44 files changed, 1925 insertions, 1126 deletions
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 019026efcb..c97c648d7e 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -16,11 +16,17 @@
#
# To use these routines:
#
-# 1) Copy this file to somewhere (e.g. ~/.git-completion.sh).
+# 1) Copy this file to somewhere (e.g. ~/.git-completion.bash).
# 2) Add the following line to your .bashrc/.zshrc:
-# source ~/.git-completion.sh
+# source ~/.git-completion.bash
# 3) Consider changing your PS1 to also show the current branch,
# see git-prompt.sh for details.
+#
+# If you use complex aliases of form '!f() { ... }; f', you can use the null
+# command ':' as the first command in the function body to declare the desired
+# completion style. For example '!f() { : git commit ; ... }; f' will
+# tell the completion to use commit completion. This also works with aliases
+# of form "!sh -c '...'". For example, "!sh -c ': git commit ; ... '".
case "$COMP_WORDBREAKS" in
*:*) : great ;;
@@ -180,7 +186,7 @@ fi
__gitcompappend ()
{
- local i=${#COMPREPLY[@]}
+ local x i=${#COMPREPLY[@]}
for x in $1; do
if [[ "$x" == "$3"* ]]; then
COMPREPLY[i++]="$2$x$4"
@@ -275,16 +281,12 @@ __gitcomp_file ()
# argument, and using the options specified in the second argument.
__git_ls_files_helper ()
{
- (
- test -n "${CDPATH+set}" && unset CDPATH
- cd "$1"
- if [ "$2" == "--committable" ]; then
- git diff-index --name-only --relative HEAD
- else
- # NOTE: $2 is not quoted in order to support multiple options
- git ls-files --exclude-standard $2
- fi
- ) 2>/dev/null
+ if [ "$2" == "--committable" ]; then
+ git -C "$1" diff-index --name-only --relative HEAD
+ else
+ # NOTE: $2 is not quoted in order to support multiple options
+ git -C "$1" ls-files --exclude-standard $2
+ fi 2>/dev/null
}
@@ -382,7 +384,8 @@ __git_refs ()
;;
*)
echo "HEAD"
- git for-each-ref --format="%(refname:short)" -- "refs/remotes/$dir/" | sed -e "s#^$dir/##"
+ git for-each-ref --format="%(refname:short)" -- \
+ "refs/remotes/$dir/" 2>/dev/null | sed -e "s#^$dir/##"
;;
esac
}
@@ -408,12 +411,9 @@ __git_refs_remotes ()
__git_remotes ()
{
- local i IFS=$'\n' d="$(__gitdir)"
+ local d="$(__gitdir)"
test -d "$d/remotes" && ls -1 "$d/remotes"
- for i in $(git --git-dir="$d" config --get-regexp 'remote\..*\.url' 2>/dev/null); do
- i="${i#remote.}"
- echo "${i/.url*/}"
- done
+ git --git-dir="$d" remote
}
__git_list_merge_strategies ()
@@ -516,7 +516,7 @@ __git_complete_index_file ()
;;
esac
- __gitcomp_file "$(__git_index_files "$1" "$pfx")" "$pfx" "$cur_"
+ __gitcomp_file "$(__git_index_files "$1" ${pfx:+"$pfx"})" "$pfx" "$cur_"
}
__git_complete_file ()
@@ -665,8 +665,8 @@ __git_list_porcelain_commands ()
checkout-index) : plumbing;;
commit-tree) : plumbing;;
count-objects) : infrequent;;
- credential-cache) : credentials helper;;
- credential-store) : credentials helper;;
+ credential) : credentials;;
+ credential-*) : credentials helper;;
cvsexportcommit) : export;;
cvsimport) : import;;
cvsserver) : daemon;;
@@ -735,35 +735,29 @@ __git_list_porcelain_commands ()
__git_porcelain_commands=
__git_compute_porcelain_commands ()
{
- __git_compute_all_commands
test -n "$__git_porcelain_commands" ||
__git_porcelain_commands=$(__git_list_porcelain_commands)
}
-__git_pretty_aliases ()
+# Lists all set config variables starting with the given section prefix,
+# with the prefix removed.
+__git_get_config_variables ()
{
- local i IFS=$'\n'
- for i in $(git --git-dir="$(__gitdir)" config --get-regexp "pretty\..*" 2>/dev/null); do
- case "$i" in
- pretty.*)
- i="${i#pretty.}"
- echo "${i/ */}"
- ;;
- esac
+ local section="$1" i IFS=$'\n'
+ for i in $(git --git-dir="$(__gitdir)" config --get-regexp "^$section\..*" 2>/dev/null); do
+ i="${i#$section.}"
+ echo "${i/ */}"
done
}
+__git_pretty_aliases ()
+{
+ __git_get_config_variables "pretty"
+}
+
__git_aliases ()
{
- local i IFS=$'\n'
- for i in $(git --git-dir="$(__gitdir)" config --get-regexp "alias\..*" 2>/dev/null); do
- case "$i" in
- alias.*)
- i="${i#alias.}"
- echo "${i/ */}"
- ;;
- esac
- done
+ __git_get_config_variables "alias"
}
# __git_aliased_command requires 1 argument
@@ -781,6 +775,10 @@ __git_aliased_command ()
-*) : option ;;
*=*) : setting env ;;
git) : git itself ;;
+ \(\)) : skip parens of shell function definition ;;
+ {) : skip start of shell helper function ;;
+ :) : skip null command ;;
+ \'*) : skip opening quote after sh -c ;;
*)
echo "$word"
return
@@ -973,7 +971,7 @@ _git_branch ()
case "$cur" in
--set-upstream-to=*)
- __gitcomp "$(__git_refs)" "" "${cur##--set-upstream-to=}"
+ __gitcomp_nl "$(__git_refs)" "" "${cur##--set-upstream-to=}"
;;
--*)
__gitcomp "
@@ -1041,7 +1039,7 @@ _git_checkout ()
_git_cherry ()
{
- __gitcomp "$(__git_refs)"
+ __gitcomp_nl "$(__git_refs)"
}
_git_cherry_pick ()
@@ -1110,7 +1108,7 @@ _git_commit ()
case "$cur" in
--cleanup=*)
- __gitcomp "default strip verbatim whitespace
+ __gitcomp "default scissors strip verbatim whitespace
" "" "${cur##--cleanup=}"
return
;;
@@ -1165,8 +1163,8 @@ __git_diff_common_options="--stat --numstat --shortstat --summary
--full-index --binary --abbrev --diff-filter=
--find-copies-harder
--text --ignore-space-at-eol --ignore-space-change
- --ignore-all-space --exit-code --quiet --ext-diff
- --no-ext-diff
+ --ignore-all-space --ignore-blank-lines --exit-code
+ --quiet --ext-diff --no-ext-diff
--no-prefix --src-prefix= --dst-prefix=
--inter-hunk-context=
--patience --histogram --minimal
@@ -1197,7 +1195,7 @@ _git_diff ()
}
__git_mergetools_common="diffuse diffmerge ecmerge emerge kdiff3 meld opendiff
- tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3 codecompare
+ tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc codecompare
"
_git_difftool ()
@@ -1298,7 +1296,7 @@ _git_gitk ()
}
__git_match_ctag() {
- awk "/^${1////\\/}/ { print \$1 }" "$2"
+ awk "/^${1//\//\\/}/ { print \$1 }" "$2"
}
_git_grep ()
@@ -1418,7 +1416,7 @@ __git_log_gitk_options="
# Options that go well for log and shortlog (not gitk)
__git_log_shortlog_options="
--author= --committer= --grep=
- --all-match
+ --all-match --invert-grep
"
__git_log_pretty_formats="oneline short medium full fuller email raw format:"
@@ -1444,7 +1442,7 @@ _git_log ()
return
;;
--decorate=*)
- __gitcomp "long short" "" "${cur##--decorate=}"
+ __gitcomp "full short no" "" "${cur##--decorate=}"
return
;;
--*)
@@ -1457,6 +1455,7 @@ _git_log ()
--abbrev-commit --abbrev=
--relative-date --date=
--pretty= --format= --oneline
+ --show-signature
--cherry-pick
--graph
--decorate --decorate=
@@ -1611,12 +1610,33 @@ _git_pull ()
__git_push_recurse_submodules="check on-demand"
+__git_complete_force_with_lease ()
+{
+ local cur_=$1
+
+ case "$cur_" in
+ --*=)
+ ;;
+ *:*)
+ __gitcomp_nl "$(__git_refs)" "" "${cur_#*:}"
+ ;;
+ *)
+ __gitcomp_nl "$(__git_refs)" "" "$cur_"
+ ;;
+ esac
+}
+
_git_push ()
{
case "$prev" in
--repo)
__gitcomp_nl "$(__git_remotes)"
return
+ ;;
+ --recurse-submodules)
+ __gitcomp "$__git_push_recurse_submodules"
+ return
+ ;;
esac
case "$cur" in
--repo=*)
@@ -1627,11 +1647,16 @@ _git_push ()
__gitcomp "$__git_push_recurse_submodules" "" "${cur##--recurse-submodules=}"
return
;;
+ --force-with-lease=*)
+ __git_complete_force_with_lease "${cur##--force-with-lease=}"
+ return
+ ;;
--*)
__gitcomp "
--all --mirror --tags --dry-run --force --verbose
+ --quiet --prune --delete --follow-tags
--receive-pack= --repo= --set-upstream
- --recurse-submodules=
+ --force-with-lease --force-with-lease= --recurse-submodules=
"
return
;;
@@ -1659,6 +1684,7 @@ _git_rebase ()
--committer-date-is-author-date --ignore-date
--ignore-whitespace --whitespace=
--autosquash --fork-point --no-fork-point
+ --autostash
"
return
@@ -1841,6 +1867,10 @@ _git_config ()
__gitcomp "$__git_send_email_suppresscc_options"
return
;;
+ sendemail.transferencoding)
+ __gitcomp "7bit 8bit quoted-printable base64"
+ return
+ ;;
--get|--get-all|--unset|--unset-all)
__gitcomp_nl "$(__git_config_get_set_variables)"
return
@@ -1975,6 +2005,7 @@ _git_config ()
color.status.changed
color.status.header
color.status.nobranch
+ color.status.unmerged
color.status.untracked
color.status.updated
color.ui
@@ -2086,6 +2117,7 @@ _git_config ()
http.noEPSV
http.postBuffer
http.proxy
+ http.sslCipherList
http.sslCAInfo
http.sslCAPath
http.sslCert
@@ -2149,6 +2181,7 @@ _git_config ()
pull.octopus
pull.twohead
push.default
+ push.followTags
rebase.autosquash
rebase.stat
receive.autogc
@@ -2222,12 +2255,7 @@ _git_remote ()
__git_complete_remote_or_refspec
;;
update)
- local i c='' IFS=$'\n'
- for i in $(git --git-dir="$(__gitdir)" config --get-regexp "remotes\..*" 2>/dev/null); do
- i="${i#remotes.}"
- c="$c ${i/ */}"
- done
- __gitcomp "$c"
+ __gitcomp "$(__git_get_config_variables "remotes")"
;;
*)
;;
@@ -2254,6 +2282,11 @@ _git_reset ()
_git_revert ()
{
+ local dir="$(__gitdir)"
+ if [ -f "$dir"/REVERT_HEAD ]; then
+ __gitcomp "--continue --quit --abort"
+ return
+ fi
case "$cur" in
--*)
__gitcomp "--edit --mainline --no-edit --no-commit --signoff"
@@ -2308,6 +2341,7 @@ _git_show ()
;;
--*)
__gitcomp "--pretty= --format= --abbrev-commit --oneline
+ --show-signature
$__git_diff_common_options
"
return
@@ -2513,6 +2547,16 @@ _git_tag ()
__gitcomp_nl "$(__git_refs)"
;;
esac
+
+ case "$cur" in
+ --*)
+ __gitcomp "
+ --list --delete --verify --annotate --message --file
+ --sign --cleanup --local-user --force --column --sort
+ --contains --points-at
+ "
+ ;;
+ esac
}
_git_whatchanged ()
diff --git a/contrib/completion/git-completion.tcsh b/contrib/completion/git-completion.tcsh
index 6104a42a23..4a790d8f4e 100644
--- a/contrib/completion/git-completion.tcsh
+++ b/contrib/completion/git-completion.tcsh
@@ -41,7 +41,7 @@ if ( ! -e ${__git_tcsh_completion_original_script} ) then
exit
endif
-cat << EOF > ${__git_tcsh_completion_script}
+cat << EOF >! ${__git_tcsh_completion_script}
#!bash
#
# This script is GENERATED and will be overwritten automatically.
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
index 9f6f0fa558..e25541308a 100644
--- a/contrib/completion/git-completion.zsh
+++ b/contrib/completion/git-completion.zsh
@@ -9,7 +9,7 @@
#
# If your script is somewhere else, you can configure it on your ~/.zshrc:
#
-# zstyle ':completion:*:*:git:*' script ~/.git-completion.sh
+# zstyle ':completion:*:*:git:*' script ~/.git-completion.zsh
#
# The recommended way to install this script is to copy to '~/.zsh/_git', and
# then add the following to your ~/.zshrc file:
diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh
index 9d684b10a6..366f0bc1e9 100644
--- a/contrib/completion/git-prompt.sh
+++ b/contrib/completion/git-prompt.sh
@@ -66,6 +66,10 @@
# git always compare HEAD to @{upstream}
# svn always compare HEAD to your SVN upstream
#
+# You can change the separator between the branch name and the above
+# state symbols by setting GIT_PS1_STATESEPARATOR. The default separator
+# is SP.
+#
# By default, __git_ps1 will compare HEAD to your SVN upstream if it can
# find one, or @{upstream} otherwise. Once you have set
# GIT_PS1_SHOWUPSTREAM, you can override it on a per-repository basis by
@@ -84,6 +88,11 @@
# GIT_PS1_SHOWCOLORHINTS to a nonempty value. The colors are based on
# the colored output of "git status -sb" and are available only when
# using __git_ps1 for PROMPT_COMMAND or precmd.
+#
+# If you would like __git_ps1 to do nothing in the case when the current
+# directory is set up to be ignored by git, then set
+# GIT_PS1_HIDE_IF_PWD_IGNORED to a nonempty value. Override this on the
+# repository level by setting bash.hideIfPwdIgnored to "false".
# check whether printf supports -v
__git_printf_supports_v=
@@ -270,7 +279,7 @@ __git_ps1_colorize_gitstring ()
__git_eread ()
{
- f="$1"
+ local f="$1"
shift
test -r "$f" && read "$@" <"$f"
}
@@ -288,6 +297,8 @@ __git_eread ()
# In this mode you can request colored hints using GIT_PS1_SHOWCOLORHINTS=true
__git_ps1 ()
{
+ # preserve exit status
+ local exit=$?
local pcmode=no
local detached=no
local ps1pc_start='\u@\h:\w '
@@ -299,10 +310,14 @@ __git_ps1 ()
ps1pc_start="$1"
ps1pc_end="$2"
printf_format="${3:-$printf_format}"
+ # set PS1 to a plain prompt so that we can
+ # simply return early if the prompt should not
+ # be decorated
+ PS1="$ps1pc_start$ps1pc_end"
;;
0|1) printf_format="${1:-$printf_format}"
;;
- *) return
+ *) return $exit
;;
esac
@@ -350,11 +365,7 @@ __git_ps1 ()
rev_parse_exit_code="$?"
if [ -z "$repo_info" ]; then
- if [ $pcmode = yes ]; then
- #In PC mode PS1 always needs to be set
- PS1="$ps1pc_start$ps1pc_end"
- fi
- return
+ return $exit
fi
local short_sha
@@ -369,6 +380,14 @@ __git_ps1 ()
local inside_gitdir="${repo_info##*$'\n'}"
local g="${repo_info%$'\n'*}"
+ if [ "true" = "$inside_worktree" ] &&
+ [ -n "${GIT_PS1_HIDE_IF_PWD_IGNORED-}" ] &&
+ [ "$(git config --bool bash.hideIfPwdIgnored)" != "false" ] &&
+ git check-ignore -q .
+ then
+ return $exit
+ fi
+
local r=""
local b=""
local step=""
@@ -412,10 +431,7 @@ __git_ps1 ()
else
local head=""
if ! __git_eread "$g/HEAD" head; then
- if [ $pcmode = yes ]; then
- PS1="$ps1pc_start$ps1pc_end"
- fi
- return
+ return $exit
fi
# is it a symbolic ref?
b="${head#ref: }"
@@ -468,13 +484,14 @@ __git_ps1 ()
fi
fi
if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ] &&
- [ -r "$g/refs/stash" ]; then
+ git rev-parse --verify --quiet refs/stash >/dev/null
+ then
s="$"
fi
if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] &&
[ "$(git config --bool bash.showUntrackedFiles)" != "false" ] &&
- git ls-files --others --exclude-standard --error-unmatch -- '*' >/dev/null 2>/dev/null
+ git ls-files --others --exclude-standard --error-unmatch -- ':/*' >/dev/null 2>/dev/null
then
u="%${ZSH_VERSION+%}"
fi
@@ -510,4 +527,6 @@ __git_ps1 ()
else
printf -- "$printf_format" "$gitstring"
fi
+
+ return $exit
}
diff --git a/contrib/contacts/.gitignore b/contrib/contacts/.gitignore
new file mode 100644
index 0000000000..f385ee643c
--- /dev/null
+++ b/contrib/contacts/.gitignore
@@ -0,0 +1,3 @@
+git-contacts.1
+git-contacts.html
+git-contacts.xml
diff --git a/contrib/contacts/Makefile b/contrib/contacts/Makefile
new file mode 100644
index 0000000000..a2990f0dcb
--- /dev/null
+++ b/contrib/contacts/Makefile
@@ -0,0 +1,71 @@
+# The default target of this Makefile is...
+all::
+
+-include ../../config.mak.autogen
+-include ../../config.mak
+
+prefix ?= /usr/local
+gitexecdir ?= $(prefix)/libexec/git-core
+mandir ?= $(prefix)/share/man
+man1dir ?= $(mandir)/man1
+htmldir ?= $(prefix)/share/doc/git-doc
+
+../../GIT-VERSION-FILE: FORCE
+ $(MAKE) -C ../../ GIT-VERSION-FILE
+
+-include ../../GIT-VERSION-FILE
+
+# this should be set to a 'standard' bsd-type install program
+INSTALL ?= install
+RM ?= rm -f
+
+ASCIIDOC = asciidoc
+XMLTO = xmlto
+
+ifndef SHELL_PATH
+ SHELL_PATH = /bin/sh
+endif
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+
+ASCIIDOC_CONF = ../../Documentation/asciidoc.conf
+MANPAGE_XSL = ../../Documentation/manpage-normal.xsl
+
+GIT_CONTACTS := git-contacts
+
+GIT_CONTACTS_DOC := git-contacts.1
+GIT_CONTACTS_XML := git-contacts.xml
+GIT_CONTACTS_TXT := git-contacts.txt
+GIT_CONTACTS_HTML := git-contacts.html
+
+doc: $(GIT_CONTACTS_DOC) $(GIT_CONTACTS_HTML)
+
+install: $(GIT_CONTACTS)
+ $(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir)
+ $(INSTALL) -m 755 $(GIT_CONTACTS) $(DESTDIR)$(gitexecdir)
+
+install-doc: install-man install-html
+
+install-man: $(GIT_CONTACTS_DOC)
+ $(INSTALL) -d -m 755 $(DESTDIR)$(man1dir)
+ $(INSTALL) -m 644 $^ $(DESTDIR)$(man1dir)
+
+install-html: $(GIT_CONTACTS_HTML)
+ $(INSTALL) -d -m 755 $(DESTDIR)$(htmldir)
+ $(INSTALL) -m 644 $^ $(DESTDIR)$(htmldir)
+
+$(GIT_CONTACTS_DOC): $(GIT_CONTACTS_XML)
+ $(XMLTO) -m $(MANPAGE_XSL) man $^
+
+$(GIT_CONTACTS_XML): $(GIT_CONTACTS_TXT)
+ $(ASCIIDOC) -b docbook -d manpage -f $(ASCIIDOC_CONF) \
+ -agit_version=$(GIT_VERSION) $^
+
+$(GIT_CONTACTS_HTML): $(GIT_CONTACTS_TXT)
+ $(ASCIIDOC) -b xhtml11 -d manpage -f $(ASCIIDOC_CONF) \
+ -agit_version=$(GIT_VERSION) $^
+
+clean:
+ $(RM) $(GIT_CONTACTS)
+ $(RM) *.xml *.html *.1
+
+.PHONY: FORCE
diff --git a/contrib/convert-grafts-to-replace-refs.sh b/contrib/convert-grafts-to-replace-refs.sh
new file mode 100755
index 0000000000..0cbc917b8c
--- /dev/null
+++ b/contrib/convert-grafts-to-replace-refs.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+# You should execute this script in the repository where you
+# want to convert grafts to replace refs.
+
+GRAFTS_FILE="${GIT_DIR:-.git}/info/grafts"
+
+. $(git --exec-path)/git-sh-setup
+
+test -f "$GRAFTS_FILE" || die "Could not find graft file: '$GRAFTS_FILE'"
+
+grep '^[^# ]' "$GRAFTS_FILE" |
+while read definition
+do
+ if test -n "$definition"
+ then
+ echo "Converting: $definition"
+ git replace --graft $definition ||
+ die "Conversion failed for: $definition"
+ fi
+done
+
+mv "$GRAFTS_FILE" "$GRAFTS_FILE.bak" ||
+ die "Could not rename '$GRAFTS_FILE' to '$GRAFTS_FILE.bak'"
+
+echo "Success!"
+echo "All the grafts in '$GRAFTS_FILE' have been converted to replace refs!"
+echo "The grafts file '$GRAFTS_FILE' has been renamed: '$GRAFTS_FILE.bak'"
diff --git a/contrib/convert-objects/git-convert-objects.txt b/contrib/convert-objects/git-convert-objects.txt
index 0565d83fc4..f871880cfb 100644
--- a/contrib/convert-objects/git-convert-objects.txt
+++ b/contrib/convert-objects/git-convert-objects.txt
@@ -26,4 +26,4 @@ Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel
GIT
---
-Part of the gitlink:git[7] suite
+Part of the linkgit:git[7] suite
diff --git a/contrib/credential/wincred/Makefile b/contrib/credential/wincred/Makefile
index bad45ca47a..6e992c0866 100644
--- a/contrib/credential/wincred/Makefile
+++ b/contrib/credential/wincred/Makefile
@@ -1,14 +1,22 @@
all: git-credential-wincred.exe
-CC = gcc
-RM = rm -f
-CFLAGS = -O2 -Wall
-
-include ../../../config.mak.autogen
-include ../../../config.mak
+CC ?= gcc
+RM ?= rm -f
+CFLAGS ?= -O2 -Wall
+
+prefix ?= /usr/local
+libexecdir ?= $(prefix)/libexec/git-core
+
+INSTALL ?= install
+
git-credential-wincred.exe : git-credential-wincred.c
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
+install: git-credential-wincred.exe
+ $(INSTALL) -m 755 $^ $(libexecdir)
+
clean:
$(RM) git-credential-wincred.exe
diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c
index a1d38f035b..006134043a 100644
--- a/contrib/credential/wincred/git-credential-wincred.c
+++ b/contrib/credential/wincred/git-credential-wincred.c
@@ -111,14 +111,23 @@ static void write_item(const char *what, LPCWSTR wbuf, int wlen)
* Match an (optional) expected string and a delimiter in the target string,
* consuming the matched text by updating the target pointer.
*/
-static int match_part(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
+
+static LPCWSTR wcsstr_last(LPCWSTR str, LPCWSTR find)
+{
+ LPCWSTR res = NULL, pos;
+ for (pos = wcsstr(str, find); pos; pos = wcsstr(pos + 1, find))
+ res = pos;
+ return res;
+}
+
+static int match_part_with_last(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim, int last)
{
LPCWSTR delim_pos, start = *ptarget;
int len;
/* find start of delimiter (or end-of-string if delim is empty) */
if (*delim)
- delim_pos = wcsstr(start, delim);
+ delim_pos = last ? wcsstr_last(start, delim) : wcsstr(start, delim);
else
delim_pos = start + wcslen(start);
@@ -138,6 +147,16 @@ static int match_part(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
return !want || (!wcsncmp(want, start, len) && !want[len]);
}
+static int match_part(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
+{
+ return match_part_with_last(ptarget, want, delim, 0);
+}
+
+static int match_part_last(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
+{
+ return match_part_with_last(ptarget, want, delim, 1);
+}
+
static int match_cred(const CREDENTIALW *cred)
{
LPCWSTR target = cred->TargetName;
@@ -146,7 +165,7 @@ static int match_cred(const CREDENTIALW *cred)
return match_part(&target, L"git", L":") &&
match_part(&target, protocol, L"://") &&
- match_part(&target, wusername, L"@") &&
+ match_part_last(&target, wusername, L"@") &&
match_part(&target, host, L"/") &&
match_part(&target, path, L"");
}
diff --git a/contrib/diff-highlight/README b/contrib/diff-highlight/README
index 502e03b305..836b97a730 100644
--- a/contrib/diff-highlight/README
+++ b/contrib/diff-highlight/README
@@ -58,6 +58,47 @@ following in your git configuration:
diff = diff-highlight | less
---------------------------------------------
+
+Color Config
+------------
+
+You can configure the highlight colors and attributes using git's
+config. The colors for "old" and "new" lines can be specified
+independently. There are two "modes" of configuration:
+
+ 1. You can specify a "highlight" color and a matching "reset" color.
+ This will retain any existing colors in the diff, and apply the
+ "highlight" and "reset" colors before and after the highlighted
+ portion.
+
+ 2. You can specify a "normal" color and a "highlight" color. In this
+ case, existing colors are dropped from that line. The non-highlighted
+ bits of the line get the "normal" color, and the highlights get the
+ "highlight" color.
+
+If no "new" colors are specified, they default to the "old" colors. If
+no "old" colors are specified, the default is to reverse the foreground
+and background for highlighted portions.
+
+Examples:
+
+---------------------------------------------
+# Underline highlighted portions
+[color "diff-highlight"]
+oldHighlight = ul
+oldReset = noul
+---------------------------------------------
+
+---------------------------------------------
+# Varying background intensities
+[color "diff-highlight"]
+oldNormal = "black #f8cbcb"
+oldHighlight = "black #ffaaaa"
+newNormal = "black #cbeecb"
+newHighlight = "black #aaffaa"
+---------------------------------------------
+
+
Bugs
----
diff --git a/contrib/diff-highlight/diff-highlight b/contrib/diff-highlight/diff-highlight
index c4404d49c9..ffefc31a98 100755
--- a/contrib/diff-highlight/diff-highlight
+++ b/contrib/diff-highlight/diff-highlight
@@ -1,12 +1,23 @@
#!/usr/bin/perl
+use 5.008;
use warnings FATAL => 'all';
use strict;
# Highlight by reversing foreground and background. You could do
# other things like bold or underline if you prefer.
-my $HIGHLIGHT = "\x1b[7m";
-my $UNHIGHLIGHT = "\x1b[27m";
+my @OLD_HIGHLIGHT = (
+ color_config('color.diff-highlight.oldnormal'),
+ color_config('color.diff-highlight.oldhighlight', "\x1b[7m"),
+ color_config('color.diff-highlight.oldreset', "\x1b[27m")
+);
+my @NEW_HIGHLIGHT = (
+ color_config('color.diff-highlight.newnormal', $OLD_HIGHLIGHT[0]),
+ color_config('color.diff-highlight.newhighlight', $OLD_HIGHLIGHT[1]),
+ color_config('color.diff-highlight.newreset', $OLD_HIGHLIGHT[2])
+);
+
+my $RESET = "\x1b[m";
my $COLOR = qr/\x1b\[[0-9;]*m/;
my $BORING = qr/$COLOR|\s/;
@@ -14,6 +25,10 @@ my @removed;
my @added;
my $in_hunk;
+# Some scripts may not realize that SIGPIPE is being ignored when launching the
+# pager--for instance scripts written in Python.
+$SIG{PIPE} = 'DEFAULT';
+
while (<>) {
if (!$in_hunk) {
print;
@@ -53,6 +68,17 @@ show_hunk(\@removed, \@added);
exit 0;
+# Ideally we would feed the default as a human-readable color to
+# git-config as the fallback value. But diff-highlight does
+# not otherwise depend on git at all, and there are reports
+# of it being used in other settings. Let's handle our own
+# fallback, which means we will work even if git can't be run.
+sub color_config {
+ my ($key, $default) = @_;
+ my $s = `git config --get-color $key 2>/dev/null`;
+ return length($s) ? $s : $default;
+}
+
sub show_hunk {
my ($a, $b) = @_;
@@ -128,8 +154,8 @@ sub highlight_pair {
}
if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) {
- return highlight_line(\@a, $pa, $sa),
- highlight_line(\@b, $pb, $sb);
+ return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT),
+ highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT);
}
else {
return join('', @a),
@@ -139,20 +165,39 @@ sub highlight_pair {
sub split_line {
local $_ = shift;
- return map { /$COLOR/ ? $_ : (split //) }
- split /($COLOR*)/;
+ return utf8::decode($_) ?
+ map { utf8::encode($_); $_ }
+ map { /$COLOR/ ? $_ : (split //) }
+ split /($COLOR+)/ :
+ map { /$COLOR/ ? $_ : (split //) }
+ split /($COLOR+)/;
}
sub highlight_line {
- my ($line, $prefix, $suffix) = @_;
-
- return join('',
- @{$line}[0..($prefix-1)],
- $HIGHLIGHT,
- @{$line}[$prefix..$suffix],
- $UNHIGHLIGHT,
- @{$line}[($suffix+1)..$#$line]
- );
+ my ($line, $prefix, $suffix, $theme) = @_;
+
+ my $start = join('', @{$line}[0..($prefix-1)]);
+ my $mid = join('', @{$line}[$prefix..$suffix]);
+ my $end = join('', @{$line}[($suffix+1)..$#$line]);
+
+ # If we have a "normal" color specified, then take over the whole line.
+ # Otherwise, we try to just manipulate the highlighted bits.
+ if (defined $theme->[0]) {
+ s/$COLOR//g for ($start, $mid, $end);
+ chomp $end;
+ return join('',
+ $theme->[0], $start, $RESET,
+ $theme->[1], $mid, $RESET,
+ $theme->[0], $end, $RESET,
+ "\n"
+ );
+ } else {
+ return join('',
+ $start,
+ $theme->[1], $mid, $theme->[2],
+ $end
+ );
+ }
}
# Pairs are interesting to highlight only if we are going to end up
diff --git a/contrib/diffall/README b/contrib/diffall/README
deleted file mode 100644
index 507f17dcd6..0000000000
--- a/contrib/diffall/README
+++ /dev/null
@@ -1,31 +0,0 @@
-The git-diffall script provides a directory based diff mechanism
-for git.
-
-To determine what diff viewer is used, the script requires either
-the 'diff.tool' or 'merge.tool' configuration option to be set.
-
-This script is compatible with most common forms used to specify a
-range of revisions to diff:
-
- 1. git diffall: shows diff between working tree and staged changes
- 2. git diffall --cached [<commit>]: shows diff between staged
- changes and HEAD (or other named commit)
- 3. git diffall <commit>: shows diff between working tree and named
- commit
- 4. git diffall <commit> <commit>: show diff between two named commits
- 5. git diffall <commit>..<commit>: same as above
- 6. git diffall <commit>...<commit>: show the changes on the branch
- containing and up to the second, starting at a common ancestor
- of both <commit>
-
-Note: all forms take an optional path limiter [-- <path>*]
-
-The '--extcmd=<command>' option allows the user to specify a custom
-command for viewing diffs. When given, configured defaults are
-ignored and the script runs $command $LOCAL $REMOTE. Additionally,
-$BASE is set in the environment.
-
-This script is based on an example provided by Thomas Rast on the
-Git list [1]:
-
-[1] http://thread.gmane.org/gmane.comp.version-control.git/124807
diff --git a/contrib/diffall/git-diffall b/contrib/diffall/git-diffall
deleted file mode 100755
index 84f2b654d7..0000000000
--- a/contrib/diffall/git-diffall
+++ /dev/null
@@ -1,257 +0,0 @@
-#!/bin/sh
-# Copyright 2010 - 2012, Tim Henigan <tim.henigan@gmail.com>
-#
-# Perform a directory diff between commits in the repository using
-# the external diff or merge tool specified in the user's config.
-
-USAGE='[--cached] [--copy-back] [-x|--extcmd=<command>] <commit>{0,2} [-- <path>*]
-
- --cached Compare to the index rather than the working tree.
-
- --copy-back Copy files back to the working tree when the diff
- tool exits (in case they were modified by the
- user). This option is only valid if the diff
- compared with the working tree.
-
- -x=<command>
- --extcmd=<command> Specify a custom command for viewing diffs.
- git-diffall ignores the configured defaults and
- runs $command $LOCAL $REMOTE when this option is
- specified. Additionally, $BASE is set in the
- environment.
-'
-
-SUBDIRECTORY_OK=1
-. "$(git --exec-path)/git-sh-setup"
-
-TOOL_MODE=diff
-. "$(git --exec-path)/git-mergetool--lib"
-
-merge_tool="$(get_merge_tool)"
-if test -z "$merge_tool"
-then
- echo "Error: Either the 'diff.tool' or 'merge.tool' option must be set."
- usage
-fi
-
-start_dir=$(pwd)
-
-# All the file paths returned by the diff command are relative to the root
-# of the working copy. So if the script is called from a subdirectory, it
-# must switch to the root of working copy before trying to use those paths.
-cdup=$(git rev-parse --show-cdup) &&
-cd "$cdup" || {
- echo >&2 "Cannot chdir to $cdup, the toplevel of the working tree"
- exit 1
-}
-
-# set up temp dir
-tmp=$(perl -e 'use File::Temp qw(tempdir);
- $t=tempdir("/tmp/git-diffall.XXXXX") or exit(1);
- print $t') || exit 1
-trap 'rm -rf "$tmp"' EXIT
-
-left=
-right=
-paths=
-dashdash_seen=
-compare_staged=
-merge_base=
-left_dir=
-right_dir=
-diff_tool=
-copy_back=
-
-while test $# != 0
-do
- case "$1" in
- -h|--h|--he|--hel|--help)
- usage
- ;;
- --cached)
- compare_staged=1
- ;;
- --copy-back)
- copy_back=1
- ;;
- -x|--e|--ex|--ext|--extc|--extcm|--extcmd)
- if test $# = 1
- then
- echo You must specify the tool for use with --extcmd
- usage
- else
- diff_tool=$2
- shift
- fi
- ;;
- --)
- dashdash_seen=1
- ;;
- -*)
- echo Invalid option: "$1"
- usage
- ;;
- *)
- # could be commit, commit range or path limiter
- case "$1" in
- *...*)
- left=${1%...*}
- right=${1#*...}
- merge_base=1
- ;;
- *..*)
- left=${1%..*}
- right=${1#*..}
- ;;
- *)
- if test -n "$dashdash_seen"
- then
- paths="$paths$1 "
- elif test -z "$left"
- then
- left=$1
- elif test -z "$right"
- then
- right=$1
- else
- paths="$paths$1 "
- fi
- ;;
- esac
- ;;
- esac
- shift
-done
-
-# Determine the set of files which changed
-if test -n "$left" && test -n "$right"
-then
- left_dir="cmt-$(git rev-parse --short $left)"
- right_dir="cmt-$(git rev-parse --short $right)"
-
- if test -n "$compare_staged"
- then
- usage
- elif test -n "$merge_base"
- then
- git diff --name-only "$left"..."$right" -- $paths >"$tmp/filelist"
- else
- git diff --name-only "$left" "$right" -- $paths >"$tmp/filelist"
- fi
-elif test -n "$left"
-then
- left_dir="cmt-$(git rev-parse --short $left)"
-
- if test -n "$compare_staged"
- then
- right_dir="staged"
- git diff --name-only --cached "$left" -- $paths >"$tmp/filelist"
- else
- right_dir="working_tree"
- git diff --name-only "$left" -- $paths >"$tmp/filelist"
- fi
-else
- left_dir="HEAD"
-
- if test -n "$compare_staged"
- then
- right_dir="staged"
- git diff --name-only --cached -- $paths >"$tmp/filelist"
- else
- right_dir="working_tree"
- git diff --name-only -- $paths >"$tmp/filelist"
- fi
-fi
-
-# Exit immediately if there are no diffs
-if test ! -s "$tmp/filelist"
-then
- exit 0
-fi
-
-if test -n "$copy_back" && test "$right_dir" != "working_tree"
-then
- echo "--copy-back is only valid when diff includes the working tree."
- exit 1
-fi
-
-# Create the named tmp directories that will hold the files to be compared
-mkdir -p "$tmp/$left_dir" "$tmp/$right_dir"
-
-# Populate the tmp/right_dir directory with the files to be compared
-while read name
-do
- if test -n "$right"
- then
- ls_list=$(git ls-tree $right "$name")
- if test -n "$ls_list"
- then
- mkdir -p "$tmp/$right_dir/$(dirname "$name")"
- git show "$right":"$name" >"$tmp/$right_dir/$name" || true
- fi
- elif test -n "$compare_staged"
- then
- ls_list=$(git ls-files -- "$name")
- if test -n "$ls_list"
- then
- mkdir -p "$tmp/$right_dir/$(dirname "$name")"
- git show :"$name" >"$tmp/$right_dir/$name"
- fi
- else
- if test -e "$name"
- then
- mkdir -p "$tmp/$right_dir/$(dirname "$name")"
- cp "$name" "$tmp/$right_dir/$name"
- fi
- fi
-done < "$tmp/filelist"
-
-# Populate the tmp/left_dir directory with the files to be compared
-while read name
-do
- if test -n "$left"
- then
- ls_list=$(git ls-tree $left "$name")
- if test -n "$ls_list"
- then
- mkdir -p "$tmp/$left_dir/$(dirname "$name")"
- git show "$left":"$name" >"$tmp/$left_dir/$name" || true
- fi
- else
- if test -n "$compare_staged"
- then
- ls_list=$(git ls-tree HEAD "$name")
- if test -n "$ls_list"
- then
- mkdir -p "$tmp/$left_dir/$(dirname "$name")"
- git show HEAD:"$name" >"$tmp/$left_dir/$name"
- fi
- else
- mkdir -p "$tmp/$left_dir/$(dirname "$name")"
- git show :"$name" >"$tmp/$left_dir/$name"
- fi
- fi
-done < "$tmp/filelist"
-
-LOCAL="$tmp/$left_dir"
-REMOTE="$tmp/$right_dir"
-
-if test -n "$diff_tool"
-then
- export BASE
- eval $diff_tool '"$LOCAL"' '"$REMOTE"'
-else
- run_merge_tool "$merge_tool" false
-fi
-
-# Copy files back to the working dir, if requested
-if test -n "$copy_back" && test "$right_dir" = "working_tree"
-then
- cd "$start_dir"
- git_top_dir=$(git rev-parse --show-toplevel)
- find "$tmp/$right_dir" -type f |
- while read file
- do
- cp "$file" "$git_top_dir/${file#$tmp/$right_dir/}"
- done
-fi
diff --git a/contrib/examples/builtin-fetch--tool.c b/contrib/examples/builtin-fetch--tool.c
index 8bc8c7533a..ee1916641e 100644
--- a/contrib/examples/builtin-fetch--tool.c
+++ b/contrib/examples/builtin-fetch--tool.c
@@ -31,7 +31,8 @@ static int update_ref_env(const char *action,
rla = "(reflog update)";
if (snprintf(msg, sizeof(msg), "%s: %s", rla, action) >= sizeof(msg))
warning("reflog message too long: %.*s...", 50, msg);
- return update_ref(msg, refname, sha1, oldval, 0, QUIET_ON_ERR);
+ return update_ref(msg, refname, sha1, oldval, 0,
+ UPDATE_REFS_QUIET_ON_ERR);
}
static int update_local_ref(const char *name,
diff --git a/contrib/examples/git-clone.sh b/contrib/examples/git-clone.sh
index b4c9376a2c..08cf246bbb 100755
--- a/contrib/examples/git-clone.sh
+++ b/contrib/examples/git-clone.sh
@@ -516,7 +516,7 @@ then
case "$no_checkout" in
'')
- test "z$quiet" = z -a "z$no_progress" = z && v=-v || v=
+ test "z$quiet" = z && test "z$no_progress" = z && v=-v || v=
git read-tree -m -u $v HEAD HEAD
esac
fi
diff --git a/contrib/examples/git-commit.sh b/contrib/examples/git-commit.sh
index 5cafe2eb77..934505bab9 100755
--- a/contrib/examples/git-commit.sh
+++ b/contrib/examples/git-commit.sh
@@ -51,7 +51,7 @@ run_status () {
export GIT_INDEX_FILE
fi
- if test "$status_only" = "t" -o "$use_status_color" = "t"; then
+ if test "$status_only" = "t" || test "$use_status_color" = "t"; then
color=
else
color=--nocolor
@@ -296,7 +296,7 @@ t,,,[1-9]*)
die "No paths with -i does not make sense." ;;
esac
-if test ! -z "$templatefile" -a -z "$log_given"
+if test ! -z "$templatefile" && test -z "$log_given"
then
if test ! -f "$templatefile"
then
diff --git a/contrib/examples/git-merge.sh b/contrib/examples/git-merge.sh
index 7e40f40c78..52f2aafb9d 100755
--- a/contrib/examples/git-merge.sh
+++ b/contrib/examples/git-merge.sh
@@ -161,7 +161,7 @@ merge_name () {
return
fi
fi
- if test "$remote" = "FETCH_HEAD" -a -r "$GIT_DIR/FETCH_HEAD"
+ if test "$remote" = "FETCH_HEAD" && test -r "$GIT_DIR/FETCH_HEAD"
then
sed -e 's/ not-for-merge / /' -e 1q \
"$GIT_DIR/FETCH_HEAD"
@@ -527,7 +527,7 @@ do
git diff-files --name-only
git ls-files --unmerged
} | wc -l`
- if test $best_cnt -le 0 -o $cnt -le $best_cnt
+ if test $best_cnt -le 0 || test $cnt -le $best_cnt
then
best_strategy=$strategy
best_cnt=$cnt
diff --git a/contrib/examples/git-repack.sh b/contrib/examples/git-repack.sh
index f312405a25..96e3fed326 100755
--- a/contrib/examples/git-repack.sh
+++ b/contrib/examples/git-repack.sh
@@ -76,8 +76,8 @@ case ",$all_into_one," in
existing="$existing $e"
fi
done
- if test -n "$existing" -a -n "$unpack_unreachable" -a \
- -n "$remove_redundant"
+ if test -n "$existing" && test -n "$unpack_unreachable" && \
+ test -n "$remove_redundant"
then
# This may have arbitrary user arguments, so we
# have to protect it against whitespace splitting
diff --git a/contrib/examples/git-resolve.sh b/contrib/examples/git-resolve.sh
index 48d0fc971f..70fdc27b72 100755
--- a/contrib/examples/git-resolve.sh
+++ b/contrib/examples/git-resolve.sh
@@ -76,7 +76,7 @@ case "$common" in
2>/dev/null || continue
# Count the paths that are unmerged.
cnt=$(GIT_INDEX_FILE=$G git ls-files --unmerged | wc -l)
- if test $best_cnt -le 0 -o $cnt -le $best_cnt
+ if test $best_cnt -le 0 || test $cnt -le $best_cnt
then
best=$c
best_cnt=$cnt
diff --git a/contrib/examples/git-svnimport.txt b/contrib/examples/git-svnimport.txt
index 3bb871e42f..3f0a9c33b5 100644
--- a/contrib/examples/git-svnimport.txt
+++ b/contrib/examples/git-svnimport.txt
@@ -176,4 +176,4 @@ Documentation by Matthias Urlichs <smurf@smurf.noris.de>.
GIT
---
-Part of the gitlink:git[7] suite
+Part of the linkgit:git[7] suite
diff --git a/contrib/gitview/gitview.txt b/contrib/gitview/gitview.txt
index 9e12f97842..7b5f9002b9 100644
--- a/contrib/gitview/gitview.txt
+++ b/contrib/gitview/gitview.txt
@@ -28,7 +28,7 @@ OPTIONS
<args>::
- All the valid option for gitlink:git-rev-list[1].
+ All the valid option for linkgit:git-rev-list[1].
Key Bindings
------------
diff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES
index 3603d56c26..6bb12306b8 100644
--- a/contrib/hooks/multimail/CHANGES
+++ b/contrib/hooks/multimail/CHANGES
@@ -1,3 +1,56 @@
+Release 1.1.1 (bugfix-only release)
+===================================
+
+* The SMTP mailer was not working with Python 2.4.
+
+Release 1.1.0
+=============
+
+* When a single commit is pushed, omit the reference changed email.
+ Set multimailhook.combineWhenSingleCommit to false to disable this
+ new feature.
+
+* In gitolite environments, the pusher's email address can be used as
+ the From address by creating a specially formatted comment block in
+ gitolite.conf (see multimailhook.from in README).
+
+* Support for SMTP authentication and SSL/TLS encryption was added,
+ see smtpUser, smtpPass, smtpEncryption in README.
+
+* A new option scanCommitForCc was added to allow git-multimail to
+ search the commit message for 'Cc: ...' lines, and add the
+ corresponding emails in Cc.
+
+* If $USER is not set, use the variable $USERNAME. This is needed on
+ Windows platform to recognize the pusher.
+
+* The emailPrefix variable can now be set to an empty string to remove
+ the prefix.
+
+* A short tutorial was added in doc/gitolite.rst to set up
+ git-multimail with gitolite.
+
+* The post-receive file was renamed to post-receive.example. It has
+ always been an example (the standard way to call git-multimail is to
+ call git_multimail.py), but it was unclear to many users.
+
+* A new refchangeShowGraph option was added to make it possible to
+ include both a graph and a log in the summary emails. The options
+ to control the graph formatting can be set via the new graphOpts
+ option.
+
+* New option --force-send was added to disable new commit detection
+ for update hook. One use-case is to run git_multimail.py after
+ running "git fetch" to send emails about commits that have just been
+ fetched (the detection of new commits was unreliable in this mode).
+
+* The testing infrastructure was considerably improved (continuous
+ integration with travis-ci, automatic check of PEP8 and RST syntax,
+ many improvements to the test scripts).
+
+This version has been tested with Python 2.4 to 2.7, and Git 1.7.1 to
+2.4.
+
Release 1.0.0
=============
diff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README
index 477d65fed3..e552c90c45 100644
--- a/contrib/hooks/multimail/README
+++ b/contrib/hooks/multimail/README
@@ -1,5 +1,8 @@
- git-multimail
- =============
+git-multimail Version 1.1.1
+===========================
+
+.. 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,
@@ -38,17 +41,17 @@ By default, for each push received by the repository, git-multimail:
list) makes it easy to scan through the emails, jump to patches
that need further attention, and write comments about specific
commits. Commits are handled in reverse topological order (i.e.,
- parents shown before children). For example,
-
- [git] branch master updated
- + [git] 01/08: doc: fix xref link from api docs to manual pages
- + [git] 02/08: api-credentials.txt: show the big picture first
- + [git] 03/08: api-credentials.txt: mention credential.helper explicitly
- + [git] 04/08: api-credentials.txt: add "see also" section
- + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&'
- + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix'
- + [git] 07/08: Merge branch 'mm/api-credentials-doc'
- + [git] 08/08: Git 1.7.11-rc2
+ parents shown before children). For example::
+
+ [git] branch master updated
+ + [git] 01/08: doc: fix xref link from api docs to manual pages
+ + [git] 02/08: api-credentials.txt: show the big picture first
+ + [git] 03/08: api-credentials.txt: mention credential.helper explicitly
+ + [git] 04/08: api-credentials.txt: add "see also" section
+ + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&'
+ + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix'
+ + [git] 07/08: Merge branch 'mm/api-credentials-doc'
+ + [git] 08/08: Git 1.7.11-rc2
Each commit appears in exactly one commit email, the first time
that it is pushed to the repository. If a commit is later merged
@@ -74,19 +77,19 @@ Requirements
3.x.
The example scripts invoke Python using the following shebang line
- (following PEP 394 [1]):
+ (following PEP 394 [1]_)::
#! /usr/bin/env python2
If your system's Python2 interpreter is not in your PATH or is not
- called "python2", you can change the lines accordingly. Or you can
+ called ``python2``, you can change the lines accordingly. Or you can
invoke the Python interpreter explicitly, for example via a tiny
- shell script like
+ shell script like::
#! /bin/sh
/usr/local/bin/python /path/to/git_multimail.py "$@"
-* The "git" command must be in your PATH. git-multimail is known to
+* The ``git`` command must be in your PATH. git-multimail is known to
work with Git versions back to 1.7.1. (Earlier versions have not
been tested; if you do so, please report your results.)
@@ -101,7 +104,7 @@ 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
@@ -109,10 +112,10 @@ central repository for a project, to which all commits are eventually
pushed.
For use on pre-v1.5.1 Git servers, git_multimail.py can also work as
-an "update" hook, taking its arguments on the command line. To use
+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].
+[2]_.
Alternatively, git_multimail.py can be imported as a Python module
into your own Python post-receive script. This method is a bit more
@@ -129,7 +132,7 @@ arbitrary Python code. For example, you can use a custom environment
only about changes affecting particular files or subdirectories)
Or you can change how emails are sent by writing your own Mailer
-class. The "post-receive" script in this directory demonstrates how
+class. The ``post-receive`` script in this directory demonstrates how
to use git_multimail.py as a Python module. (If you make interesting
changes of this type, please consider sharing them with the
community.)
@@ -139,18 +142,26 @@ Configuration
-------------
By default, git-multimail mostly takes its configuration from the
-following "git config" settings:
+following ``git config`` settings:
multimailhook.environment
This describes the general environment of the repository.
Currently supported values:
- "generic" -- the username of the pusher is read from $USER and the
- repository name is derived from the repository's path.
+ * 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" -- the username of the pusher is read from $GL_USER and
- the repository name from $GL_REPO.
+ 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
If neither of these environments is suitable for your setup, then
you can implement a Python class that inherits from Environment
@@ -160,8 +171,8 @@ multimailhook.environment
The environment value can be specified on the command line using
the --environment option. If it is not specified on the command
line or by multimailhook.environment, then it defaults to
- "gitolite" if the environment contains variables $GL_USER and
- $GL_REPO; otherwise "generic".
+ ``gitolite`` if the environment contains variables $GL_USER and
+ $GL_REPO; otherwise ``generic``.
multimailhook.repoName
@@ -219,61 +230,109 @@ multimailhook.announceShortlog
not so straightforward, then the shortlog might be confusing
rather than useful. Default is false.
+multimailhook.refchangeShowGraph
+
+ If this option is set to true, then summary emails about reference
+ changes will additionally include:
+
+ * a graph of the added commits (if any)
+
+ * a graph of the discarded commits (if any)
+
+ The log is generated by running ``git log --graph`` with the options
+ 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
- "git log" with the options specified in multimailhook.logOpts.
+ ``git log`` with the options specified in multimailhook.logOpts.
Default is false.
multimailhook.mailer
This option changes the way emails are sent. Accepted values are:
- - sendmail (the default): use the command /usr/sbin/sendmail or
- /usr/lib/sendmail (or sendmailCommand, if configured). This
+ - 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
+ * 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.,
+ 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\"'
- 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
command is not available on the system. This mode can be
further customized via the following options:
- multimailhook.smtpServer
+ * 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.
+ The name of the SMTP server to connect to. The value can
+ also include a colon and a port number; e.g.,
+ ``mail.example.com:25``. Default is 'localhost' using port 25.
- multimailhook.envelopeSender
+ * multimailhook.smtpUser
+ * multimailhook.smtpPass
- The sender address to be passed to the SMTP server. If
- unset, then the value of multimailhook.from is used.
+ 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
+
+ Timeout in seconds.
+
+ * multimailhook.smtpEncryption
+
+ Set the security type. Allowed values: none, ssl.
+ Default=none.
+
+ * multimailhook.smtpServerDebugLevel
+
+ Integer number. Set to greater than 0 to activate debugging.
multimailhook.from
- If set then use this value in the From: field of generated emails.
- If unset, then use the repository's user configuration (user.name
- and user.email). If user.email is also unset, then use
- multimailhook.envelopeSender.
+ If set, use this value in the From: field of generated emails. If
+ unset, the value of the From: header is determined as follows:
+
+ 1. (gitolite environment only) Parse gitolite.conf, looking for a
+ block of comments that looks like this::
+
+ # BEGIN USER EMAILS
+ # username Firstname Lastname <email@example.com>
+ # END USER EMAILS
+
+ If that block exists, and there is a line between the BEGIN
+ USER EMAILS and END USER EMAILS lines where the first field
+ matches the gitolite username ($GL_USER), use the rest of the
+ line for the From: header.
+
+ 2. If the user.email configuration setting is set, use its value
+ (and the value of user.name, if set).
+
+ 3. Use the value of multimailhook.envelopeSender.
multimailhook.administrator
@@ -287,7 +346,8 @@ 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]".
+ the repository in square brackets; e.g., ``[myrepo]``. Set this
+ value to the empty string to suppress the email prefix.
multimailhook.emailMaxLines
@@ -299,7 +359,7 @@ multimailhook.emailMaxLines
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
@@ -316,54 +376,62 @@ multimailhook.maxCommitEmails
multimailhook.emailStrictUTF8
- If this boolean option is set to "true", then the main part of the
+ 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".
+ character, U+FFFD. The default is `true`.
multimailhook.diffOpts
- Options passed to "git diff-tree" when generating the summary
- information for ReferenceChange emails. Default is "--stat
- --summary --find-copies-harder". Add -p to those options to
+ Options passed to ``git diff-tree`` when generating the summary
+ information for ReferenceChange emails. Default is ``--stat
+ --summary --find-copies-harder``. Add -p to those options to
include a unified diff of changes in addition to the usual summary
output. Shell quoting is allowed; see multimailhook.logOpts for
details.
+multimailhook.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'.
+
+ Shell quoting is allowed; see logOpts for details.
+
multimailhook.logOpts
- Options passed to "git log" to generate additional info for
+ Options passed to ``git log`` to generate additional info for
reference change emails (used only if refchangeShowLog is set).
- For example, adding --graph will show the graph of revisions, -p
- will show the complete diff, etc. The default is empty.
+ For example, adding -p will show each commit's complete diff. The
+ default is empty.
Shell quoting is allowed; for example, a log format that contains
- spaces can be specified using something like:
+ spaces can be specified using something like::
git config multimailhook.logopts '--pretty=format:"%h %aN <%aE>%n%s%n%n%b%n"'
If you want to set this by editing your configuration file
directly, remember that Git requires double-quotes to be escaped
- (see git-config(1) for more information):
+ (see git-config(1) for more information)::
[multimailhook]
logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\"
multimailhook.commitLogOpts
- Options passed to "git log" to generate additional info for
+ 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
- --stat -p --cc". Shell quoting is allowed; see
+ will suppress whitespace changes. The default options are ``-C
+ --stat -p --cc``. Shell quoting is allowed; see
multimailhook.logOpts for details.
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.
+ 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
@@ -377,26 +445,48 @@ multimailhook.replyToRefchange
- An email address, which will be used directly.
- - The value "pusher", in which case the pusher's address (if
+ - The value `pusher`, in which case the pusher's address (if
available) will be used. This is the default for refchange
emails.
- - The value "author" (meaningful only for replyToCommit), in which
+ - The value `author` (meaningful only for replyToCommit), in which
case the commit author's address will be used. This is the
default for commit emails.
- - The value "none", in which case the Reply-To: field will be
+ - The value `none`, in which case the Reply-To: field will be
omitted.
+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
+
Email filtering aids
--------------------
All emails include extra headers to enable fine tuned filtering and
give information for debugging. All emails include the headers
-"X-Git-Host", "X-Git-Repo", "X-Git-Refname", and "X-Git-Reftype".
-ReferenceChange emails also include headers "X-Git-Oldrev" and "X-Git-Newrev";
-Revision emails also include header "X-Git-Rev".
+``X-Git-Host``, ``X-Git-Repo``, ``X-Git-Refname``, and ``X-Git-Reftype``.
+ReferenceChange emails also include headers ``X-Git-Oldrev`` and ``X-Git-Newrev``;
+Revision emails also include header ``X-Git-Rev``.
Customizing email contents
@@ -420,16 +510,17 @@ environment are built in:
* GenericEnvironment: a stand-alone Git repository.
* GitoliteEnvironment: a Git repository that is managed by gitolite
- [3]. For such repositories, the identity of the pusher is read from
- environment variable $GL_USER, and the name of the repository is
- read from $GL_REPO (if it is not overridden by
- multimailhook.reponame).
+ [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).
By default, git-multimail assumes GitoliteEnvironment if $GL_USER and
$GL_REPO are set, and otherwise assumes GenericEnvironment.
Alternatively, you can choose one of these two environments explicitly
-by setting a "multimailhook.environment" config setting (which can
-have the value "generic" or "gitolite") or by passing an --environment
+by setting a ``multimailhook.environment`` config setting (which can
+have the value `generic` or `gitolite`) or by passing an --environment
option to the script.
If you need to customize the script in ways that are not supported by
@@ -439,8 +530,8 @@ git_multimail.py as a Python module, as demonstrated by the example
post-receive script. Then implement your environment class; it should
usually inherit from one of the existing Environment classes and
possibly one or more of the EnvironmentMixin classes. Then set the
-"environment" variable to an instance of your own environment class
-and pass it to run_as_post_receive_hook().
+``environment`` variable to an instance of your own environment class
+and pass it to ``run_as_post_receive_hook()``.
The standard environment classes, GenericEnvironment and
GitoliteEnvironment, are in fact themselves put together out of a
@@ -456,45 +547,48 @@ consider sharing them with the community!
Getting involved
----------------
-git-multimail is an open-source project, built by volunteers. We
-would welcome your help!
+git-multimail is an open-source project, built by volunteers. We would
+welcome your help!
-The current maintainer is Michael Haggerty <mhagger@alum.mit.edu>.
+The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>
+and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.
-General discussion of git-multimail takes place on the main Git
-mailing list,
+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:
- git@vger.kernel.org
+ https://github.com/git-multimail/git-multimail
-Please CC emails regarding git-multimail to me so that I don't
-overlook them.
+Whenever enough changes to git-multimail have accumulated, a new
+code-drop of git-multimail will be submitted for inclusion in the Git
+project.
-The git-multimail project itself is currently hosted on GitHub:
+We use the GitHub issue tracker to keep track of bugs and feature
+requests, and we use GitHub pull requests to exchange patches (though,
+if you prefer, you can send patches via the Git mailing list with CC
+to the maintainers). Please sign off your patches as per the Git
+project practice.
- https://github.com/mhagger/git-multimail
+General discussion of git-multimail can take place on the main Git
+mailing list,
-We use the GitHub issue tracker to keep track of bugs and feature
-requests, and GitHub pull requests to exchange patches (though, if you
-prefer, you can send patches via the Git mailing list with cc to me).
-Please sign off your patches as per the Git project practice.
+ git@vger.kernel.org
-Please note that although a copy of git-multimail will probably be
-distributed in the "contrib" section of the main Git project,
-development takes place in the separate git-multimail repository on
-GitHub! (Whenever enough changes to git-multimail have accumulated, a
-new code-drop of git-multimail will be submitted for inclusion in the
-Git project.)
+Please CC emails regarding git-multimail to the maintainers so that we
+don't overlook them.
Footnotes
---------
-[1] http://www.python.org/dev/peps/pep-0394/
+.. [1] http://www.python.org/dev/peps/pep-0394/
-[2] Because of the way information is passed to update hooks, the
- script's method of determining whether a commit has already been
- seen does not work when it is used as an "update" script. In
- particular, no notification email will be generated for a new
- commit that is added to multiple references in the same push.
+.. [2] Because of the way information is passed to update hooks, the
+ script's method of determining whether a commit has already
+ been seen does not work when it is used as an ``update`` script.
+ In particular, no notification email will be generated for a
+ new commit that is added to multiple references in the same
+ push. A workaround is to use --force-send to force sending the
+ emails.
-[3] https://github.com/sitaramc/gitolite
+.. [3] https://github.com/sitaramc/gitolite
diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git
index 129b771410..f5d59a8d31 100644
--- a/contrib/hooks/multimail/README.Git
+++ b/contrib/hooks/multimail/README.Git
@@ -3,13 +3,13 @@ section of the Git project as a convenience to Git users.
git-multimail is developed as an independent project at the following
website:
- https://github.com/mhagger/git-multimail
+ https://github.com/git-multimail/git-multimail
The version in this directory was obtained from the upstream project
-on 2014-04-07 and consists of the "git-multimail" subdirectory from
+on July 03 2015 and consists of the "git-multimail" subdirectory from
revision
- 1b32653bafc4f902535b9fc1cd9cae911325b870
+ 6d6c9eb62a054143322cfaecde3949189c065b46 refs/tags/1.1.1
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/git_multimail.py b/contrib/hooks/multimail/git_multimail.py
index 8b58ed6444..c06ce7a515 100755
--- a/contrib/hooks/multimail/git_multimail.py
+++ b/contrib/hooks/multimail/git_multimail.py
@@ -1,5 +1,6 @@
#! /usr/bin/env python2
+# Copyright (c) 2015 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
@@ -99,6 +100,10 @@ REF_DELETED_SUBJECT_TEMPLATE = (
' (was %(oldrev_short)s)'
)
+COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE = (
+ '%(emailprefix)s%(refname_type)s %(short_refname)s updated: %(oneline)s'
+ )
+
REFCHANGE_HEADER_TEMPLATE = """\
Date: %(send_date)s
To: %(recipients)s
@@ -230,6 +235,7 @@ how to provide full information about this reference change.
REVISION_HEADER_TEMPLATE = """\
Date: %(send_date)s
To: %(recipients)s
+Cc: %(cc_recipients)s
Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s
MIME-Version: 1.0
Content-Type: text/plain; charset=%(charset)s
@@ -258,6 +264,38 @@ in repository %(repo_shortname)s.
REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE
+# Combined, meaning refchange+revision email (for single-commit additions)
+COMBINED_HEADER_TEMPLATE = """\
+Date: %(send_date)s
+To: %(recipients)s
+Subject: %(subject)s
+MIME-Version: 1.0
+Content-Type: text/plain; charset=%(charset)s
+Content-Transfer-Encoding: 8bit
+Message-ID: %(msgid)s
+From: %(fromaddr)s
+Reply-To: %(reply_to)s
+X-Git-Host: %(fqdn)s
+X-Git-Repo: %(repo_shortname)s
+X-Git-Refname: %(refname)s
+X-Git-Reftype: %(refname_type)s
+X-Git-Oldrev: %(oldrev)s
+X-Git-Newrev: %(newrev)s
+X-Git-Rev: %(rev)s
+Auto-Submitted: auto-generated
+"""
+
+COMBINED_INTRO_TEMPLATE = """\
+This is an automated email from the git hooks/post-receive script.
+
+%(pusher)s pushed a commit to %(refname_type)s %(short_refname)s
+in repository %(repo_shortname)s.
+
+"""
+
+COMBINED_FOOTER_TEMPLATE = FOOTER_TEMPLATE
+
+
class CommandError(Exception):
def __init__(self, cmd, retcode):
self.cmd = cmd
@@ -336,6 +374,47 @@ def read_git_lines(args, keepends=False, **kw):
return read_git_output(args, keepends=True, **kw).splitlines(keepends)
+def git_rev_list_ish(cmd, spec, args=None, **kw):
+ """Common functionality for invoking a 'git rev-list'-like command.
+
+ Parameters:
+ * cmd is the Git command to run, e.g., 'rev-list' or 'log'.
+ * spec is a list of revision arguments to pass to the named
+ command. If None, this function returns an empty list.
+ * args is a list of extra arguments passed to the named command.
+ * All other keyword arguments (if any) are passed to the
+ underlying read_git_lines() function.
+
+ Return the output of the Git command in the form of a list, one
+ entry per output line.
+ """
+ if spec is None:
+ return []
+ if args is None:
+ args = []
+ args = [cmd, '--stdin'] + args
+ spec_stdin = ''.join(s + '\n' for s in spec)
+ return read_git_lines(args, input=spec_stdin, **kw)
+
+
+def git_rev_list(spec, **kw):
+ """Run 'git rev-list' with the given list of revision arguments.
+
+ See git_rev_list_ish() for parameter and return value
+ documentation.
+ """
+ return git_rev_list_ish('rev-list', spec, **kw)
+
+
+def git_log(spec, **kw):
+ """Run 'git log' with the given list of revision arguments.
+
+ See git_rev_list_ish() for parameter and return value
+ documentation.
+ """
+ return git_rev_list_ish('log', spec, **kw)
+
+
def header_encode(text, header_name=None):
"""Encode and line-wrap the value of an email header field."""
@@ -388,9 +467,9 @@ class Config(object):
def get(self, name, default=None):
try:
values = self._split(read_git_output(
- ['config', '--get', '--null', '%s.%s' % (self.section, name)],
- env=self.env, keepends=True,
- ))
+ ['config', '--get', '--null', '%s.%s' % (self.section, name)],
+ env=self.env, keepends=True,
+ ))
assert len(values) == 1
return values[0]
except CommandError:
@@ -449,9 +528,14 @@ class Config(object):
env=self.env,
)
- def has_key(self, name):
+ def __contains__(self, name):
return self.get_all(name, default=None) is not None
+ # We don't use this method anymore internally, but keep it here in
+ # case somebody is calling it from their own code:
+ def has_key(self, name):
+ return name in self
+
def unset_all(self, name):
try:
read_git_output(
@@ -579,7 +663,7 @@ class Change(object):
self._values = None
def _compute_values(self):
- """Return a dictionary {keyword : expansion} for this Change.
+ """Return a dictionary {keyword: expansion} for this Change.
Derived classes overload this method to add more entries to
the return value. This method is used internally by
@@ -589,7 +673,7 @@ class Change(object):
return self.environment.get_values()
def get_values(self, **extra_values):
- """Return a dictionary {keyword : expansion} for this Change.
+ """Return a dictionary {keyword: expansion} for this Change.
Return a dictionary mapping keywords to the values that they
should be expanded to for this Change (used when interpolating
@@ -636,7 +720,7 @@ class Change(object):
value = value % values
except KeyError, e:
if DEBUG:
- sys.stderr.write(
+ self.environment.log_warning(
'Warning: unknown variable %r in the following line; line skipped:\n'
' %s\n'
% (e.args[0], line,)
@@ -711,6 +795,8 @@ class Change(object):
class Revision(Change):
"""A Change consisting of a single git commit."""
+ CC_RE = re.compile(r'^\s*C[Cc]:\s*(?P<to>[^#]+@[^\s#]*)\s*(#.*)?$')
+
def __init__(self, reference_change, rev, num, tot):
Change.__init__(self, reference_change.environment)
self.reference_change = reference_change
@@ -722,6 +808,24 @@ class Revision(Change):
self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1])
self.recipients = self.environment.get_revision_recipients(self)
+ self.cc_recipients = ''
+ if self.environment.get_scancommitforcc():
+ 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))
+
+ def _cc_recipients(self):
+ cc_recipients = []
+ message = read_git_output(['log', '--no-walk', '--format=%b', self.rev.sha1])
+ lines = message.strip().split('\n')
+ for line in lines:
+ m = re.match(self.CC_RE, line)
+ if m:
+ cc_recipients.append(m.group('to'))
+
+ return cc_recipients
+
def _compute_values(self):
values = Change._compute_values(self)
@@ -739,6 +843,8 @@ class Revision(Change):
values['num'] = self.num
values['tot'] = self.tot
values['recipients'] = self.recipients
+ if self.cc_recipients:
+ values['cc_recipients'] = self.cc_recipients
values['oneline'] = oneline
values['author'] = self.author
@@ -750,8 +856,8 @@ class Revision(Change):
def generate_email_header(self, **extra_values):
for line in self.expand_header_lines(
- REVISION_HEADER_TEMPLATE, **extra_values
- ):
+ REVISION_HEADER_TEMPLATE, **extra_values
+ ):
yield line
def generate_email_intro(self):
@@ -822,26 +928,26 @@ class ReferenceChange(Change):
klass = BranchChange
elif area == 'remotes':
# Tracking branch:
- sys.stderr.write(
+ environment.log_warning(
'*** Push-update of tracking branch %r\n'
'*** - incomplete email generated.\n'
- % (refname,)
+ % (refname,)
)
klass = OtherReferenceChange
else:
# Some other reference namespace:
- sys.stderr.write(
+ environment.log_warning(
'*** Push-update of strange reference %r\n'
'*** - incomplete email generated.\n'
- % (refname,)
+ % (refname,)
)
klass = OtherReferenceChange
else:
# Anything else (is there anything else?)
- sys.stderr.write(
+ environment.log_warning(
'*** Unknown type of update to %r (%s)\n'
'*** - incomplete email generated.\n'
- % (refname, rev.type,)
+ % (refname, rev.type,)
)
klass = OtherReferenceChange
@@ -854,9 +960,9 @@ class ReferenceChange(Change):
def __init__(self, environment, refname, short_refname, old, new, rev):
Change.__init__(self, environment)
self.change_type = {
- (False, True) : 'create',
- (True, True) : 'update',
- (True, False) : 'delete',
+ (False, True): 'create',
+ (True, True): 'update',
+ (True, False): 'delete',
}[bool(old), bool(new)]
self.refname = refname
self.short_refname = short_refname
@@ -865,10 +971,16 @@ class ReferenceChange(Change):
self.rev = rev
self.msgid = make_msgid()
self.diffopts = environment.diffopts
+ self.graphopts = environment.graphopts
self.logopts = environment.logopts
self.commitlogopts = environment.commitlogopts
+ self.showgraph = environment.refchange_showgraph
self.showlog = environment.refchange_showlog
+ self.header_template = REFCHANGE_HEADER_TEMPLATE
+ self.intro_template = REFCHANGE_INTRO_TEMPLATE
+ self.footer_template = FOOTER_TEMPLATE
+
def _compute_values(self):
values = Change._compute_values(self)
@@ -894,11 +1006,39 @@ class ReferenceChange(Change):
return values
+ def send_single_combined_email(self, known_added_sha1s):
+ """Determine if a combined refchange/revision email should be sent
+
+ If there is only a single new (non-merge) commit added by a
+ change, it is useful to combine the ReferenceChange and
+ Revision emails into one. In such a case, return the single
+ revision; otherwise, return None.
+
+ This method is overridden in BranchChange."""
+
+ return None
+
+ def generate_combined_email(self, push, revision, body_filter=None, extra_header_values={}):
+ """Generate an email describing this change AND specified revision.
+
+ Iterate over the lines (including the header lines) of an
+ email describing this change. If body_filter is not None,
+ then use it to filter the lines that are intended for the
+ email body.
+
+ The extra_header_values field is received as a dict and not as
+ **kwargs, to allow passing other keyword arguments in the
+ future (e.g. passing extra values to generate_email_intro()
+
+ This method is overridden in BranchChange."""
+
+ raise NotImplementedError
+
def get_subject(self):
template = {
- 'create' : REF_CREATED_SUBJECT_TEMPLATE,
- 'update' : REF_UPDATED_SUBJECT_TEMPLATE,
- 'delete' : REF_DELETED_SUBJECT_TEMPLATE,
+ 'create': REF_CREATED_SUBJECT_TEMPLATE,
+ 'update': REF_UPDATED_SUBJECT_TEMPLATE,
+ 'delete': REF_DELETED_SUBJECT_TEMPLATE,
}[self.change_type]
return self.expand(template)
@@ -907,12 +1047,12 @@ class ReferenceChange(Change):
extra_values['subject'] = self.get_subject()
for line in self.expand_header_lines(
- REFCHANGE_HEADER_TEMPLATE, **extra_values
- ):
+ self.header_template, **extra_values
+ ):
yield line
def generate_email_intro(self):
- for line in self.expand_lines(REFCHANGE_INTRO_TEMPLATE):
+ for line in self.expand_lines(self.intro_template):
yield line
def generate_email_body(self, push):
@@ -922,9 +1062,9 @@ class ReferenceChange(Change):
generate_update_summary() / generate_delete_summary()."""
change_summary = {
- 'create' : self.generate_create_summary,
- 'delete' : self.generate_delete_summary,
- 'update' : self.generate_update_summary,
+ 'create': self.generate_create_summary,
+ 'delete': self.generate_delete_summary,
+ 'update': self.generate_update_summary,
}[self.change_type](push)
for line in change_summary:
yield line
@@ -933,7 +1073,23 @@ class ReferenceChange(Change):
yield line
def generate_email_footer(self):
- return self.expand_lines(FOOTER_TEMPLATE)
+ return self.expand_lines(self.footer_template)
+
+ def generate_revision_change_graph(self, push):
+ if self.showgraph:
+ args = ['--graph'] + self.graphopts
+ for newold in ('new', 'old'):
+ has_newold = False
+ spec = push.get_commits_spec(newold, self)
+ for line in git_log(spec, args=args, keepends=True):
+ if not has_newold:
+ has_newold = True
+ yield '\n'
+ yield 'Graph of %s commits:\n\n' % (
+ {'new': 'new', 'old': 'discarded'}[newold],)
+ yield ' ' + line
+ if has_newold:
+ yield '\n'
def generate_revision_change_log(self, new_commits_list):
if self.showlog:
@@ -945,9 +1101,17 @@ class ReferenceChange(Change):
+ new_commits_list
+ ['--'],
keepends=True,
- ):
+ ):
yield line
+ def generate_new_revision_summary(self, tot, new_commits_list, push):
+ for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot):
+ yield line
+ for line in self.generate_revision_change_graph(push):
+ yield line
+ for line in self.generate_revision_change_log(new_commits_list):
+ yield line
+
def generate_revision_change_summary(self, push):
"""Generate a summary of the revisions added/removed by this change."""
@@ -960,7 +1124,7 @@ class ReferenceChange(Change):
sha1s.reverse()
tot = len(sha1s)
new_revisions = [
- Revision(self, GitObject(sha1), num=i+1, tot=tot)
+ Revision(self, GitObject(sha1), num=i + 1, tot=tot)
for (i, sha1) in enumerate(sha1s)
]
@@ -973,9 +1137,8 @@ class ReferenceChange(Change):
BRIEF_SUMMARY_TEMPLATE, action='new', text=subject,
)
yield '\n'
- for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot):
- yield line
- for line in self.generate_revision_change_log([r.rev.sha1 for r in new_revisions]):
+ for line in self.generate_new_revision_summary(
+ tot, [r.rev.sha1 for r in new_revisions], push):
yield line
else:
for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE):
@@ -993,16 +1156,16 @@ class ReferenceChange(Change):
# revisions in the summary even though we will not send
# new notification emails for them.
adds = list(generate_summaries(
- '--topo-order', '--reverse', '%s..%s'
- % (self.old.commit_sha1, self.new.commit_sha1,)
- ))
+ '--topo-order', '--reverse', '%s..%s'
+ % (self.old.commit_sha1, self.new.commit_sha1,)
+ ))
# List of the revisions that were removed from the branch
# by this update. This will be empty except for
# non-fast-forward updates.
discards = list(generate_summaries(
- '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,)
- ))
+ '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,)
+ ))
if adds:
new_commits_list = push.get_new_commits(self)
@@ -1071,13 +1234,14 @@ class ReferenceChange(Change):
yield '\n'
if new_commits:
- for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=len(new_commits)):
- yield line
- for line in self.generate_revision_change_log(new_commits_list):
+ for line in self.generate_new_revision_summary(
+ len(new_commits), new_commits_list, push):
yield line
else:
for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE):
yield line
+ for line in self.generate_revision_change_graph(push):
+ yield line
# The diffstat is shown from the old revision to the new
# revision. This is to show the truth of what happened in
@@ -1089,11 +1253,11 @@ class ReferenceChange(Change):
yield '\n'
yield 'Summary of changes:\n'
for line in read_git_lines(
- ['diff-tree']
- + self.diffopts
- + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
- keepends=True,
- ):
+ ['diff-tree']
+ + self.diffopts
+ + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
+ keepends=True,
+ ):
yield line
elif self.old.commit_sha1 and not self.new.commit_sha1:
@@ -1103,7 +1267,7 @@ class ReferenceChange(Change):
sha1s = list(push.get_discarded_commits(self))
tot = len(sha1s)
discarded_revisions = [
- Revision(self, GitObject(sha1), num=i+1, tot=tot)
+ Revision(self, GitObject(sha1), num=i + 1, tot=tot)
for (i, sha1) in enumerate(sha1s)
]
@@ -1116,6 +1280,8 @@ class ReferenceChange(Change):
yield r.expand(
BRIEF_SUMMARY_TEMPLATE, action='discards', text=subject,
)
+ for line in self.generate_revision_change_graph(push):
+ yield line
else:
for line in self.expand_lines(NO_DISCARDED_REVISIONS_TEMPLATE):
yield line
@@ -1161,6 +1327,150 @@ class BranchChange(ReferenceChange):
old=old, new=new, rev=rev,
)
self.recipients = environment.get_refchange_recipients(self)
+ self._single_revision = None
+
+ def send_single_combined_email(self, known_added_sha1s):
+ if not self.environment.combine_when_single_commit:
+ return None
+
+ # In the sadly-all-too-frequent usecase of people pushing only
+ # one of their commits at a time to a repository, users feel
+ # the reference change summary emails are noise rather than
+ # important signal. This is because, in this particular
+ # usecase, there is a reference change summary email for each
+ # new commit, and all these summaries do is point out that
+ # there is one new commit (which can readily be inferred by
+ # the existence of the individual revision email that is also
+ # sent). In such cases, our users prefer there to be a combined
+ # reference change summary/new revision email.
+ #
+ # So, if the change is an update and it doesn't discard any
+ # commits, and it adds exactly one non-merge commit (gerrit
+ # forces a workflow where every commit is individually merged
+ # and the git-multimail hook fired off for just this one
+ # change), then we send a combined refchange/revision email.
+ try:
+ # If this change is a reference update that doesn't discard
+ # any commits...
+ if self.change_type != 'update':
+ return None
+
+ if read_git_lines(
+ ['merge-base', self.old.sha1, self.new.sha1]
+ ) != [self.old.sha1]:
+ return None
+
+ # Check if this update introduced exactly one non-merge
+ # commit:
+
+ def split_line(line):
+ """Split line into (sha1, [parent,...])."""
+
+ words = line.split()
+ return (words[0], words[1:])
+
+ # Get the new commits introduced by the push as a list of
+ # (sha1, [parent,...])
+ new_commits = [
+ split_line(line)
+ for line in read_git_lines(
+ [
+ 'log', '-3', '--format=%H %P',
+ '%s..%s' % (self.old.sha1, self.new.sha1),
+ ]
+ )
+ ]
+
+ if not new_commits:
+ return None
+
+ # If the newest commit is a merge, save it for a later check
+ # but otherwise ignore it
+ merge = None
+ tot = len(new_commits)
+ if len(new_commits[0][1]) > 1:
+ merge = new_commits[0][0]
+ del new_commits[0]
+
+ # Our primary check: we can't combine if more than one commit
+ # is introduced. We also currently only combine if the new
+ # commit is a non-merge commit, though it may make sense to
+ # combine if it is a merge as well.
+ if not (
+ len(new_commits) == 1
+ and len(new_commits[0][1]) == 1
+ and new_commits[0][0] in known_added_sha1s
+ ):
+ return None
+
+ # We do not want to combine revision and refchange emails if
+ # those go to separate locations.
+ rev = Revision(self, GitObject(new_commits[0][0]), 1, tot)
+ if rev.recipients != self.recipients:
+ return None
+
+ # We ignored the newest commit if it was just a merge of the one
+ # commit being introduced. But we don't want to ignore that
+ # merge commit it it involved conflict resolutions. Check that.
+ if merge and merge != read_git_output(['diff-tree', '--cc', merge]):
+ return None
+
+ # We can combine the refchange and one new revision emails
+ # into one. Return the Revision that a combined email should
+ # be sent about.
+ return rev
+ except CommandError:
+ # Cannot determine number of commits in old..new or new..old;
+ # don't combine reference/revision emails:
+ return None
+
+ def generate_combined_email(self, push, revision, body_filter=None, extra_header_values={}):
+ values = revision.get_values()
+ if extra_header_values:
+ values.update(extra_header_values)
+ if 'subject' not in extra_header_values:
+ values['subject'] = self.expand(COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE, **values)
+
+ self._single_revision = revision
+ self.header_template = COMBINED_HEADER_TEMPLATE
+ self.intro_template = COMBINED_INTRO_TEMPLATE
+ self.footer_template = COMBINED_FOOTER_TEMPLATE
+ for line in self.generate_email(push, body_filter, values):
+ yield line
+
+ def generate_email_body(self, push):
+ '''Call the appropriate body generation routine.
+
+ If this is a combined refchange/revision email, the special logic
+ for handling this combined email comes from this function. For
+ other cases, we just use the normal handling.'''
+
+ # If self._single_revision isn't set; don't override
+ if not self._single_revision:
+ for line in super(BranchChange, self).generate_email_body(push):
+ yield line
+ return
+
+ # This is a combined refchange/revision email; we first provide
+ # some info from the refchange portion, and then call the revision
+ # generate_email_body function to handle the revision portion.
+ adds = list(generate_summaries(
+ '--topo-order', '--reverse', '%s..%s'
+ % (self.old.commit_sha1, self.new.commit_sha1,)
+ ))
+
+ yield self.expand("The following commit(s) were added to %(refname)s by this push:\n")
+ for (sha1, subject) in adds:
+ yield self.expand(
+ BRIEF_SUMMARY_TEMPLATE, action='new',
+ rev_short=sha1, text=subject,
+ )
+
+ yield self._single_revision.rev.short + " is described below\n"
+ yield '\n'
+
+ for line in self._single_revision.generate_email_body(push):
+ yield line
class AnnotatedTagChange(ReferenceChange):
@@ -1390,13 +1700,17 @@ class SendMailer(Mailer):
sys.exit(1)
try:
p.stdin.writelines(lines)
- except:
+ except Exception, e:
sys.stderr.write(
'*** Error while generating commit email\n'
'*** - mail sending aborted.\n'
)
- p.terminate()
- raise
+ try:
+ # subprocess.terminate() is not available in Python 2.4
+ p.terminate()
+ except AttributeError:
+ pass
+ raise e
else:
p.stdin.close()
retcode = p.wait()
@@ -1407,34 +1721,78 @@ class SendMailer(Mailer):
class SMTPMailer(Mailer):
"""Send emails using Python's smtplib."""
- def __init__(self, envelopesender, smtpserver):
+ def __init__(self, envelopesender, smtpserver,
+ smtpservertimeout=10.0, smtpserverdebuglevel=0,
+ smtpencryption='none',
+ smtpuser='', smtppass='',
+ ):
if not envelopesender:
sys.stderr.write(
'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n'
'please set either multimailhook.envelopeSender or user.email\n'
)
sys.exit(1)
+ if smtpencryption == 'ssl' and not (smtpuser and smtppass):
+ raise ConfigurationException(
+ 'Cannot use SMTPMailer with security option ssl '
+ 'without options username and password.'
+ )
self.envelopesender = envelopesender
self.smtpserver = smtpserver
+ self.smtpservertimeout = smtpservertimeout
+ self.smtpserverdebuglevel = smtpserverdebuglevel
+ self.security = smtpencryption
+ self.username = smtpuser
+ self.password = smtppass
try:
- self.smtp = smtplib.SMTP(self.smtpserver)
+ def call(klass, server, timeout):
+ try:
+ return klass(server, timeout=timeout)
+ except TypeError:
+ # Old Python versions do not have timeout= argument.
+ return klass(server)
+ if self.security == 'none':
+ self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout)
+ elif self.security == 'ssl':
+ self.smtp = call(smtplib.SMTP_SSL, self.smtpserver, timeout=self.smtpservertimeout)
+ elif self.security == 'tls':
+ if ':' not in self.smtpserver:
+ self.smtpserver += ':587' # default port for TLS
+ self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout)
+ self.smtp.ehlo()
+ self.smtp.starttls()
+ self.smtp.ehlo()
+ else:
+ sys.stdout.write('*** Error: Control reached an invalid option. ***')
+ sys.exit(1)
+ if self.smtpserverdebuglevel > 0:
+ sys.stdout.write(
+ "*** Setting debug on for SMTP server connection (%s) ***\n"
+ % self.smtpserverdebuglevel)
+ self.smtp.set_debuglevel(self.smtpserverdebuglevel)
except Exception, e:
- sys.stderr.write('*** Error establishing SMTP connection to %s***\n' % self.smtpserver)
+ sys.stderr.write(
+ '*** Error establishing SMTP connection to %s ***\n'
+ % self.smtpserver)
sys.stderr.write('*** %s\n' % str(e))
sys.exit(1)
def __del__(self):
- self.smtp.quit()
+ if hasattr(self, 'smtp'):
+ self.smtp.quit()
def send(self, lines, to_addrs):
try:
+ if self.username or self.password:
+ sys.stderr.write("*** Authenticating as %s ***\n" % self.username)
+ self.smtp.login(self.username, self.password)
msg = ''.join(lines)
# turn comma-separated list into Python list if needed.
if isinstance(to_addrs, basestring):
to_addrs = [email for (name, email) in getaddresses([to_addrs])]
self.smtp.sendmail(self.envelopesender, to_addrs, msg)
except Exception, e:
- sys.stderr.write('*** Error sending email***\n')
+ sys.stderr.write('*** Error sending email ***\n')
sys.stderr.write('*** %s\n' % str(e))
self.smtp.quit()
sys.exit(1)
@@ -1549,6 +1907,10 @@ class Environment(object):
True iff announce emails should include a shortlog.
+ refchange_showgraph (bool)
+
+ True iff refchanges emails should include a detailed graph.
+
refchange_showlog (bool)
True iff refchanges emails should include a detailed log.
@@ -1559,6 +1921,12 @@ class Environment(object):
summary email. The value should be a list of strings
representing words to be passed to the command.
+ graphopts (list of strings)
+
+ Analogous to diffopts, but contains options passed to
+ 'git log --graph' when generating the detailed graph for
+ a set of commits (see refchange_showgraph)
+
logopts (list of strings)
Analogous to diffopts, but contains options passed to
@@ -1571,6 +1939,17 @@ class Environment(object):
commit mail. The value should be a list of strings
representing words to be passed to the command.
+ quiet (bool)
+ On success do not write to stderr
+
+ stdout (bool)
+ Write email to stdout rather than emailing. Useful for debugging
+
+ combine_when_single_commit (bool)
+
+ True if a combined email should be produced when a single
+ new commit is pushed to a branch, False otherwise.
+
"""
REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
@@ -1580,9 +1959,14 @@ class Environment(object):
self.announce_show_shortlog = False
self.maxcommitemails = 500
self.diffopts = ['--stat', '--summary', '--find-copies-harder']
+ self.graphopts = ['--oneline', '--decorate']
self.logopts = []
+ self.refchange_showgraph = False
self.refchange_showlog = False
self.commitlogopts = ['-C', '--stat', '-p', '--cc']
+ self.quiet = False
+ self.stdout = False
+ self.combine_when_single_commit = True
self.COMPUTED_KEYS = [
'administrator',
@@ -1614,6 +1998,14 @@ class Environment(object):
def get_pusher_email(self):
return None
+ def get_fromaddr(self):
+ config = Config('user')
+ fromname = config.get('name', default='')
+ fromemail = config.get('email', default='')
+ if fromemail:
+ return formataddr([fromname, fromemail])
+ return self.get_sender()
+
def get_administrator(self):
return 'the administrator of this repository'
@@ -1631,7 +2023,7 @@ class Environment(object):
return CHARSET
def get_values(self):
- """Return a dictionary {keyword : expansion} for this Environment.
+ """Return a dictionary {keyword: expansion} for this Environment.
This method is called by Change._compute_values(). The keys
in the returned dictionary are available to be used in any of
@@ -1699,6 +2091,24 @@ class Environment(object):
return lines
+ def log_msg(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."""
+ sys.stderr.write(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."""
+ sys.stderr.write(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."""
+ sys.stderr.write(msg)
+
class ConfigEnvironmentMixin(Environment):
"""A mixin that sets self.config to its constructor's config argument.
@@ -1723,20 +2133,23 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
config=config, **kw
)
- self.announce_show_shortlog = config.get_bool(
- 'announceshortlog', default=self.announce_show_shortlog
- )
-
- self.refchange_showlog = config.get_bool(
- 'refchangeshowlog', default=self.refchange_showlog
- )
+ for var, cfg in (
+ ('announce_show_shortlog', 'announceshortlog'),
+ ('refchange_showgraph', 'refchangeShowGraph'),
+ ('refchange_showlog', 'refchangeshowlog'),
+ ('quiet', 'quiet'),
+ ('stdout', 'stdout'),
+ ):
+ val = config.get_bool(cfg)
+ if val is not None:
+ setattr(self, var, val)
maxcommitemails = config.get('maxcommitemails')
if maxcommitemails is not None:
try:
self.maxcommitemails = int(maxcommitemails)
except ValueError:
- sys.stderr.write(
+ self.log_warning(
'*** Malformed value for multimailhook.maxCommitEmails: %s\n' % maxcommitemails
+ '*** Expected a number. Ignoring.\n'
)
@@ -1745,6 +2158,10 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
if diffopts is not None:
self.diffopts = shlex.split(diffopts)
+ graphopts = config.get('graphOpts')
+ if graphopts is not None:
+ self.graphopts = shlex.split(graphopts)
+
logopts = config.get('logopts')
if logopts is not None:
self.logopts = shlex.split(logopts)
@@ -1756,14 +2173,18 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
reply_to = config.get('replyTo')
self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to)
if (
- self.__reply_to_refchange is not None
- and self.__reply_to_refchange.lower() == 'author'
- ):
+ self.__reply_to_refchange is not None
+ and self.__reply_to_refchange.lower() == 'author'
+ ):
raise ConfigurationException(
'"author" is not an allowed setting for replyToRefchange'
)
self.__reply_to_commit = config.get('replyToCommit', default=reply_to)
+ combine = config.get_bool('combineWhenSingleCommit')
+ if combine is not None:
+ self.combine_when_single_commit = combine
+
def get_administrator(self):
return (
self.config.get('administrator')
@@ -1779,8 +2200,12 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
def get_emailprefix(self):
emailprefix = self.config.get('emailprefix')
- if emailprefix and emailprefix.strip():
- return emailprefix.strip() + ' '
+ if emailprefix is not None:
+ emailprefix = emailprefix.strip()
+ if emailprefix:
+ return emailprefix + ' '
+ else:
+ return ''
else:
return '[%s] ' % (self.get_repo_shortname(),)
@@ -1791,14 +2216,7 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
fromaddr = self.config.get('from')
if fromaddr:
return fromaddr
- else:
- config = Config('user')
- fromname = config.get('name', default='')
- fromemail = config.get('email', default='')
- if fromemail:
- return formataddr([fromname, fromemail])
- else:
- return self.get_sender()
+ return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr()
def get_reply_to_refchange(self, refchange):
if self.__reply_to_refchange is None:
@@ -1814,7 +2232,7 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
if self.__reply_to_commit is None:
return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision)
elif self.__reply_to_commit.lower() == 'author':
- return revision.get_author()
+ return revision.author
elif self.__reply_to_commit.lower() == 'pusher':
return self.get_pusher_email()
elif self.__reply_to_commit.lower() == 'none':
@@ -1822,6 +2240,9 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
else:
return self.__reply_to_commit
+ def get_scancommitforcc(self):
+ return self.config.get('scancommitforcc')
+
class FilterLinesEnvironmentMixin(Environment):
"""Handle encoding and maximum line length of body lines.
@@ -1862,9 +2283,9 @@ class FilterLinesEnvironmentMixin(Environment):
class ConfigFilterLinesEnvironmentMixin(
- ConfigEnvironmentMixin,
- FilterLinesEnvironmentMixin,
- ):
+ ConfigEnvironmentMixin,
+ FilterLinesEnvironmentMixin,
+ ):
"""Handle encoding and maximum line length based on config."""
def __init__(self, config, **kw):
@@ -1896,9 +2317,9 @@ class MaxlinesEnvironmentMixin(Environment):
class ConfigMaxlinesEnvironmentMixin(
- ConfigEnvironmentMixin,
- MaxlinesEnvironmentMixin,
- ):
+ ConfigEnvironmentMixin,
+ MaxlinesEnvironmentMixin,
+ ):
"""Limit the email body to the number of lines specified in config."""
def __init__(self, config, **kw):
@@ -1927,9 +2348,9 @@ class FQDNEnvironmentMixin(Environment):
class ConfigFQDNEnvironmentMixin(
- ConfigEnvironmentMixin,
- FQDNEnvironmentMixin,
- ):
+ ConfigEnvironmentMixin,
+ FQDNEnvironmentMixin,
+ ):
"""Read the FQDN from the config."""
def __init__(self, config, **kw):
@@ -1970,10 +2391,10 @@ class StaticRecipientsEnvironmentMixin(Environment):
"""Set recipients statically based on constructor parameters."""
def __init__(
- self,
- refchange_recipients, announce_recipients, revision_recipients,
- **kw
- ):
+ self,
+ refchange_recipients, announce_recipients, revision_recipients, scancommitforcc,
+ **kw
+ ):
super(StaticRecipientsEnvironmentMixin, self).__init__(**kw)
# The recipients for various types of notification emails, as
@@ -1985,7 +2406,8 @@ class StaticRecipientsEnvironmentMixin(Environment):
# compute them once and for all:
if not (refchange_recipients
or announce_recipients
- or revision_recipients):
+ or revision_recipients
+ or scancommitforcc):
raise ConfigurationException('No email recipients configured!')
self.__refchange_recipients = refchange_recipients
self.__announce_recipients = announce_recipients
@@ -2002,9 +2424,9 @@ class StaticRecipientsEnvironmentMixin(Environment):
class ConfigRecipientsEnvironmentMixin(
- ConfigEnvironmentMixin,
- StaticRecipientsEnvironmentMixin
- ):
+ ConfigEnvironmentMixin,
+ StaticRecipientsEnvironmentMixin
+ ):
"""Determine recipients statically based on config."""
def __init__(self, config, **kw):
@@ -2019,6 +2441,7 @@ class ConfigRecipientsEnvironmentMixin(
revision_recipients=self._get_recipients(
config, 'commitlist', 'mailinglist',
),
+ scancommitforcc=config.get('scancommitforcc'),
**kw
)
@@ -2067,20 +2490,20 @@ class ProjectdescEnvironmentMixin(Environment):
class GenericEnvironmentMixin(Environment):
def get_pusher(self):
- return self.osenv.get('USER', 'unknown user')
+ return self.osenv.get('USER', self.osenv.get('USERNAME', 'unknown user'))
class GenericEnvironment(
- ProjectdescEnvironmentMixin,
- ConfigMaxlinesEnvironmentMixin,
- ComputeFQDNEnvironmentMixin,
- ConfigFilterLinesEnvironmentMixin,
- ConfigRecipientsEnvironmentMixin,
- PusherDomainEnvironmentMixin,
- ConfigOptionsEnvironmentMixin,
- GenericEnvironmentMixin,
- Environment,
- ):
+ ProjectdescEnvironmentMixin,
+ ConfigMaxlinesEnvironmentMixin,
+ ComputeFQDNEnvironmentMixin,
+ ConfigFilterLinesEnvironmentMixin,
+ ConfigRecipientsEnvironmentMixin,
+ PusherDomainEnvironmentMixin,
+ ConfigOptionsEnvironmentMixin,
+ GenericEnvironmentMixin,
+ Environment,
+ ):
pass
@@ -2097,6 +2520,45 @@ class GitoliteEnvironmentMixin(Environment):
def get_pusher(self):
return self.osenv.get('GL_USER', 'unknown user')
+ def get_fromaddr(self):
+ GL_USER = self.osenv.get('GL_USER')
+ if GL_USER is not None:
+ # Find the path to gitolite.conf. Note that gitolite v3
+ # did away with the GL_ADMINDIR and GL_CONF environment
+ # variables (they are now hard-coded).
+ GL_ADMINDIR = self.osenv.get(
+ 'GL_ADMINDIR',
+ os.path.expanduser(os.path.join('~', '.gitolite')))
+ GL_CONF = self.osenv.get(
+ 'GL_CONF',
+ os.path.join(GL_ADMINDIR, 'conf', 'gitolite.conf'))
+ if os.path.isfile(GL_CONF):
+ f = open(GL_CONF, 'rU')
+ try:
+ in_user_emails_section = False
+ re_template = r'^\s*#\s*{}\s*$'
+ re_begin, re_user, re_end = (
+ re.compile(re_template.format(x))
+ for x in (
+ r'BEGIN\s+USER\s+EMAILS',
+ re.escape(GL_USER) + r'\s+(.*)',
+ r'END\s+USER\s+EMAILS',
+ ))
+ for l in f:
+ l = l.rstrip('\n')
+ if not in_user_emails_section:
+ if re_begin.match(l):
+ in_user_emails_section = True
+ continue
+ if re_end.match(l):
+ break
+ m = re_user.match(l)
+ if m:
+ return m.group(1)
+ finally:
+ f.close()
+ return super(GitoliteEnvironmentMixin, self).get_fromaddr()
+
class IncrementalDateTime(object):
"""Simple wrapper to give incremental date/times.
@@ -2116,16 +2578,16 @@ class IncrementalDateTime(object):
class GitoliteEnvironment(
- ProjectdescEnvironmentMixin,
- ConfigMaxlinesEnvironmentMixin,
- ComputeFQDNEnvironmentMixin,
- ConfigFilterLinesEnvironmentMixin,
- ConfigRecipientsEnvironmentMixin,
- PusherDomainEnvironmentMixin,
- ConfigOptionsEnvironmentMixin,
- GitoliteEnvironmentMixin,
- Environment,
- ):
+ ProjectdescEnvironmentMixin,
+ ConfigMaxlinesEnvironmentMixin,
+ ComputeFQDNEnvironmentMixin,
+ ConfigFilterLinesEnvironmentMixin,
+ ConfigRecipientsEnvironmentMixin,
+ PusherDomainEnvironmentMixin,
+ ConfigOptionsEnvironmentMixin,
+ GitoliteEnvironmentMixin,
+ Environment,
+ ):
pass
@@ -2149,9 +2611,9 @@ class Push(object):
references.
The first step is to determine the "other" references--those
- unaffected by the current push. They are computed by
- Push._compute_other_ref_sha1s() by listing all references then
- removing any affected by this push.
+ unaffected by the current push. They are computed by listing all
+ references then removing any affected by this push. The results
+ are stored in Push._other_ref_sha1s.
The commits contained in the repository before this push were
@@ -2187,7 +2649,7 @@ class Push(object):
possible and working with SHA1s thereafter (because SHA1s are
immutable)."""
- # A map {(changeclass, changetype) : integer} specifying the order
+ # A map {(changeclass, changetype): integer} specifying the order
# that reference changes will be processed if multiple reference
# changes are included in a single push. The order is significant
# mostly because new commit notifications are threaded together
@@ -2211,66 +2673,134 @@ class Push(object):
])
)
- def __init__(self, changes):
+ def __init__(self, changes, ignore_other_refs=False):
self.changes = sorted(changes, key=self._sort_key)
+ self.__other_ref_sha1s = None
+ self.__cached_commits_spec = {}
- # The SHA-1s of commits referred to by references unaffected
- # by this push:
- other_ref_sha1s = self._compute_other_ref_sha1s()
+ if ignore_other_refs:
+ self.__other_ref_sha1s = set()
- self._old_rev_exclusion_spec = self._compute_rev_exclusion_spec(
- other_ref_sha1s.union(
- change.old.sha1
+ @classmethod
+ def _sort_key(klass, change):
+ return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,)
+
+ @property
+ def _other_ref_sha1s(self):
+ """The GitObjects referred to by references unaffected by this push.
+ """
+ if self.__other_ref_sha1s is None:
+ # The refnames being changed by this push:
+ updated_refs = set(
+ change.refname
for change in self.changes
- if change.old.type in ['commit', 'tag']
)
- )
- self._new_rev_exclusion_spec = self._compute_rev_exclusion_spec(
- other_ref_sha1s.union(
- change.new.sha1
- for change in self.changes
- if change.new.type in ['commit', 'tag']
+
+ # The SHA-1s of commits referred to by all references in this
+ # repository *except* updated_refs:
+ sha1s = set()
+ fmt = (
+ '%(objectname) %(objecttype) %(refname)\n'
+ '%(*objectname) %(*objecttype) %(refname)'
)
- )
+ for line in read_git_lines(
+ ['for-each-ref', '--format=%s' % (fmt,)]):
+ (sha1, type, name) = line.split(' ', 2)
+ if sha1 and type == 'commit' and name not in updated_refs:
+ sha1s.add(sha1)
- @classmethod
- def _sort_key(klass, change):
- return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,)
+ self.__other_ref_sha1s = sha1s
+
+ return self.__other_ref_sha1s
+
+ def _get_commits_spec_incl(self, new_or_old, reference_change=None):
+ """Get new or old SHA-1 from one or each of the changed refs.
- def _compute_other_ref_sha1s(self):
- """Return the GitObjects referred to by references unaffected by this push."""
+ Return a list of SHA-1 commit identifier strings suitable as
+ arguments to 'git rev-list' (or 'git log' or ...). The
+ returned identifiers are either the old or new values from one
+ or all of the changed references, depending on the values of
+ new_or_old and reference_change.
- # The refnames being changed by this push:
- updated_refs = set(
- change.refname
+ new_or_old is either the string 'new' or the string 'old'. If
+ 'new', the returned SHA-1 identifiers are the new values from
+ each changed reference. If 'old', the SHA-1 identifiers are
+ the old values from each changed reference.
+
+ If reference_change is specified and not None, only the new or
+ old reference from the specified reference is included in the
+ return value.
+
+ This function returns None if there are no matching revisions
+ (e.g., because a branch was deleted and new_or_old is 'new').
+ """
+
+ if not reference_change:
+ incl_spec = sorted(
+ getattr(change, new_or_old).sha1
+ for change in self.changes
+ if getattr(change, new_or_old)
+ )
+ if not incl_spec:
+ incl_spec = None
+ elif not getattr(reference_change, new_or_old).commit_sha1:
+ incl_spec = None
+ else:
+ incl_spec = [getattr(reference_change, new_or_old).commit_sha1]
+ return incl_spec
+
+ def _get_commits_spec_excl(self, new_or_old):
+ """Get exclusion revisions for determining new or discarded commits.
+
+ Return a list of strings suitable as arguments to 'git
+ rev-list' (or 'git log' or ...) that will exclude all
+ commits that, depending on the value of new_or_old, were
+ either previously in the repository (useful for determining
+ which commits are new to the repository) or currently in the
+ repository (useful for determining which commits were
+ discarded from the repository).
+
+ new_or_old is either the string 'new' or the string 'old'. If
+ 'new', the commits to be excluded are those that were in the
+ repository before the push. If 'old', the commits to be
+ excluded are those that are currently in the repository. """
+
+ old_or_new = {'old': 'new', 'new': 'old'}[new_or_old]
+ excl_revs = self._other_ref_sha1s.union(
+ getattr(change, old_or_new).sha1
for change in self.changes
+ if getattr(change, old_or_new).type in ['commit', 'tag']
)
+ return ['^' + sha1 for sha1 in sorted(excl_revs)]
- # The SHA-1s of commits referred to by all references in this
- # repository *except* updated_refs:
- sha1s = set()
- fmt = (
- '%(objectname) %(objecttype) %(refname)\n'
- '%(*objectname) %(*objecttype) %(refname)'
- )
- for line in read_git_lines(['for-each-ref', '--format=%s' % (fmt,)]):
- (sha1, type, name) = line.split(' ', 2)
- if sha1 and type == 'commit' and name not in updated_refs:
- sha1s.add(sha1)
+ def get_commits_spec(self, new_or_old, reference_change=None):
+ """Get rev-list arguments for added or discarded commits.
- return sha1s
+ Return a list of strings suitable as arguments to 'git
+ rev-list' (or 'git log' or ...) that select those commits
+ that, depending on the value of new_or_old, are either new to
+ the repository or were discarded from the repository.
- def _compute_rev_exclusion_spec(self, sha1s):
- """Return an exclusion specification for 'git rev-list'.
+ new_or_old is either the string 'new' or the string 'old'. If
+ 'new', the returned list is used to select commits that are
+ new to the repository. If 'old', the returned value is used
+ to select the commits that have been discarded from the
+ repository.
- git_objects is an iterable over GitObject instances. Return a
- string that can be passed to the standard input of 'git
- rev-list --stdin' to exclude all of the commits referred to by
- git_objects."""
+ If reference_change is specified and not None, the new or
+ discarded commits are limited to those that are reachable from
+ the new or old value of the specified reference.
- return ''.join(
- ['^%s\n' % (sha1,) for sha1 in sorted(sha1s)]
- )
+ This function returns None if there are no added (or discarded)
+ revisions.
+ """
+ key = (new_or_old, reference_change)
+ if key not in self.__cached_commits_spec:
+ ret = self._get_commits_spec_incl(new_or_old, reference_change)
+ if ret is not None:
+ ret.extend(self._get_commits_spec_excl(new_or_old))
+ self.__cached_commits_spec[key] = ret
+ return self.__cached_commits_spec[key]
def get_new_commits(self, reference_change=None):
"""Return a list of commits added by this push.
@@ -2280,19 +2810,8 @@ class Push(object):
reference_change is None, then return a list of *all* commits
added by this push."""
- if not reference_change:
- new_revs = sorted(
- change.new.sha1
- for change in self.changes
- if change.new
- )
- elif not reference_change.new.commit_sha1:
- return []
- else:
- new_revs = [reference_change.new.commit_sha1]
-
- cmd = ['rev-list', '--stdin'] + new_revs
- return read_git_lines(cmd, input=self._old_rev_exclusion_spec)
+ spec = self.get_commits_spec('new', reference_change)
+ return git_rev_list(spec)
def get_discarded_commits(self, reference_change):
"""Return a list of commits discarded by this push.
@@ -2301,13 +2820,8 @@ class Push(object):
entirely discarded from the repository by the part of this
push represented by reference_change."""
- if not reference_change.old.commit_sha1:
- return []
- else:
- old_revs = [reference_change.old.commit_sha1]
-
- cmd = ['rev-list', '--stdin'] + old_revs
- return read_git_lines(cmd, input=self._new_rev_exclusion_spec)
+ spec = self.get_commits_spec('old', reference_change)
+ return git_rev_list(spec)
def send_emails(self, mailer, body_filter=None):
"""Use send all of the notification emails needed for this push.
@@ -2325,30 +2839,43 @@ class Push(object):
unhandled_sha1s = set(self.get_new_commits())
send_date = IncrementalDateTime()
for change in self.changes:
+ sha1s = []
+ for sha1 in reversed(list(self.get_new_commits(change))):
+ if sha1 in unhandled_sha1s:
+ sha1s.append(sha1)
+ unhandled_sha1s.remove(sha1)
+
# Check if we've got anyone to send to
if not change.recipients:
- sys.stderr.write(
+ change.environment.log_warning(
'*** no recipients configured so no email will be sent\n'
'*** for %r update %s->%s\n'
% (change.refname, change.old.sha1, change.new.sha1,)
)
else:
- sys.stderr.write('Sending notification emails to: %s\n' % (change.recipients,))
- extra_values = {'send_date' : send_date.next()}
- mailer.send(
- change.generate_email(self, body_filter, extra_values),
- change.recipients,
- )
+ if not change.environment.quiet:
+ change.environment.log_msg(
+ 'Sending notification emails to: %s\n' % (change.recipients,))
+ extra_values = {'send_date': send_date.next()}
- sha1s = []
- for sha1 in reversed(list(self.get_new_commits(change))):
- if sha1 in unhandled_sha1s:
- sha1s.append(sha1)
- unhandled_sha1s.remove(sha1)
+ rev = change.send_single_combined_email(sha1s)
+ if rev:
+ mailer.send(
+ change.generate_combined_email(self, rev, body_filter, extra_values),
+ rev.recipients,
+ )
+ # This change is now fully handled; no need to handle
+ # individual revisions any further.
+ continue
+ else:
+ mailer.send(
+ change.generate_email(self, body_filter, extra_values),
+ change.recipients,
+ )
max_emails = change.environment.maxcommitemails
if max_emails and len(sha1s) > max_emails:
- sys.stderr.write(
+ 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
@@ -2356,9 +2883,13 @@ class Push(object):
return
for (num, sha1) in enumerate(sha1s):
- rev = Revision(change, GitObject(sha1), num=num+1, tot=len(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')
+ rev.recipients = rev.cc_recipients
+ rev.cc_recipients = None
if rev.recipients:
- extra_values = {'send_date' : send_date.next()}
+ extra_values = {'send_date': send_date.next()}
mailer.send(
rev.generate_email(self, body_filter, extra_values),
rev.recipients,
@@ -2366,7 +2897,7 @@ class Push(object):
# Consistency check:
if unhandled_sha1s:
- sys.stderr.write(
+ change.environment.log_error(
'ERROR: No emails were sent for the following new commits:\n'
' %s\n'
% ('\n '.join(sorted(unhandled_sha1s)),)
@@ -2384,7 +2915,7 @@ def run_as_post_receive_hook(environment, mailer):
push.send_emails(mailer, body_filter=environment.filter_body)
-def run_as_update_hook(environment, mailer, refname, oldrev, newrev):
+def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
changes = [
ReferenceChange.create(
environment,
@@ -2393,7 +2924,7 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev):
refname,
),
]
- push = Push(changes)
+ push = Push(changes, force_send)
push.send_emails(mailer, body_filter=environment.filter_body)
@@ -2402,9 +2933,18 @@ def choose_mailer(config, environment):
if mailer == 'smtp':
smtpserver = config.get('smtpserver', default='localhost')
+ smtpservertimeout = float(config.get('smtpservertimeout', default=10.0))
+ smtpserverdebuglevel = int(config.get('smtpserverdebuglevel', default=0))
+ smtpencryption = config.get('smtpencryption', default='none')
+ smtpuser = config.get('smtpuser', default='')
+ smtppass = config.get('smtppass', default='')
mailer = SMTPMailer(
envelopesender=(environment.get_sender() or environment.get_fromaddr()),
- smtpserver=smtpserver,
+ smtpserver=smtpserver, smtpservertimeout=smtpservertimeout,
+ smtpserverdebuglevel=smtpserverdebuglevel,
+ smtpencryption=smtpencryption,
+ smtpuser=smtpuser,
+ smtppass=smtppass,
)
elif mailer == 'sendmail':
command = config.get('sendmailcommand')
@@ -2412,7 +2952,7 @@ def choose_mailer(config, environment):
command = shlex.split(command)
mailer = SendMailer(command=command, envelopesender=environment.get_sender())
else:
- sys.stderr.write(
+ environment.log_error(
'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer
+ 'please use one of "smtp" or "sendmail".\n'
)
@@ -2421,8 +2961,8 @@ def choose_mailer(config, environment):
KNOWN_ENVIRONMENTS = {
- 'generic' : GenericEnvironmentMixin,
- 'gitolite' : GitoliteEnvironmentMixin,
+ 'generic': GenericEnvironmentMixin,
+ 'gitolite': GitoliteEnvironmentMixin,
}
@@ -2439,8 +2979,8 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
ConfigOptionsEnvironmentMixin,
]
environment_kw = {
- 'osenv' : osenv,
- 'config' : config,
+ 'osenv': osenv,
+ 'config': config,
}
if not env:
@@ -2459,6 +2999,7 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
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)
@@ -2499,6 +3040,14 @@ def main(args):
'(intended for debugging purposes).'
),
)
+ parser.add_option(
+ '--force-send', action='store_true', default=False,
+ help=(
+ 'Force sending refchange email when using as an update hook. '
+ 'This is useful to work around the unreliable new commits '
+ 'detection in this mode.'
+ ),
+ )
(options, args) = parser.parse_args(args)
@@ -2513,11 +3062,11 @@ 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))
+ for (k, v) in sorted(environment.get_values().items()):
+ sys.stderr.write(' %s : %r\n' % (k, v))
sys.stderr.write('\n')
- if options.stdout:
+ if options.stdout or environment.stdout:
mailer = OutputMailer(sys.stdout)
else:
mailer = choose_mailer(config, environment)
@@ -2528,7 +3077,7 @@ def main(args):
if len(args) != 3:
parser.error('Need zero or three non-option arguments')
(refname, oldrev, newrev) = args
- run_as_update_hook(environment, mailer, refname, oldrev, newrev)
+ run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send)
else:
run_as_post_receive_hook(environment, mailer)
except ConfigurationException, e:
diff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config
index 04eeaac413..d0e9b39201 100755
--- a/contrib/hooks/multimail/migrate-mailhook-config
+++ b/contrib/hooks/multimail/migrate-mailhook-config
@@ -22,6 +22,7 @@ OLD_NAMES = [
'showrev',
'emailmaxlines',
'diffopts',
+ 'scancommitforcc',
]
NEW_NAMES = [
@@ -38,6 +39,7 @@ NEW_NAMES = [
'emailmaxlines',
'diffopts',
'emaildomain',
+ 'scancommitforcc',
]
@@ -61,7 +63,7 @@ def _check_old_config_exists(old):
"""Check that at least one old configuration value is set."""
for name in OLD_NAMES:
- if old.has_key(name):
+ if name in old:
return True
return False
@@ -72,7 +74,7 @@ def _check_new_config_clear(new):
retval = True
for name in NEW_NAMES:
- if new.has_key(name):
+ if name in new:
if retval:
sys.stderr.write('INFO: The following configuration values already exist:\n\n')
sys.stderr.write(' "%s.%s"\n' % (new.section, name))
@@ -83,7 +85,7 @@ def _check_new_config_clear(new):
def erase_values(config, names):
for name in names:
- if config.has_key(name):
+ if name in config:
try:
sys.stderr.write('...unsetting "%s.%s"\n' % (config.section, name))
config.unset_all(name)
@@ -170,7 +172,7 @@ def migrate_config(strict=False, retain=False, overwrite=False):
)
name = 'showrev'
- if old.has_key(name):
+ if name in old:
msg = 'git-multimail does not support "%s.%s"' % (old.section, name,)
if strict:
sys.exit(
@@ -182,7 +184,7 @@ def migrate_config(strict=False, retain=False, overwrite=False):
sys.stderr.write('\nWARNING: %s (ignoring).\n\n' % (msg,))
for name in ['mailinglist', 'announcelist']:
- if old.has_key(name):
+ if name in old:
sys.stderr.write(
'...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
)
@@ -198,15 +200,15 @@ def migrate_config(strict=False, retain=False, overwrite=False):
)
new.set('announceshortlog', 'true')
- for name in ['envelopesender', 'emailmaxlines', 'diffopts']:
- if old.has_key(name):
+ for name in ['envelopesender', 'emailmaxlines', 'diffopts', 'scancommitforcc']:
+ if name in old:
sys.stderr.write(
'...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
)
new.set(name, old.get(name))
name = 'emailprefix'
- if old.has_key(name):
+ if name in old:
sys.stderr.write(
'...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
)
diff --git a/contrib/hooks/multimail/post-receive b/contrib/hooks/multimail/post-receive.example
index 4d46828ba5..43f7b6b635 100755
--- a/contrib/hooks/multimail/post-receive
+++ b/contrib/hooks/multimail/post-receive.example
@@ -2,16 +2,18 @@
"""Example post-receive hook based on git-multimail.
-This script is a simple example of a post-receive hook implemented
-using git_multimail.py as a Python module. It is intended to be
-customized before use; see the comments in the script to help you get
-started.
+The simplest way to use git-multimail is to use the script
+git_multimail.py directly as a post-receive hook, and to configure it
+using Git's configuration files and command-line parameters. You can
+also write your own Python wrapper for more advanced configurability,
+using git_multimail.py as a Python module.
-It is possible to use git_multimail.py itself as a post-receive or
-update hook, configured via git config settings and/or command-line
-parameters. But for more flexibility, it can also be imported as a
-Python module by a custom post-receive script as done here. The
-latter has the following advantages:
+This script is a simple example of such a post-receive hook. It is
+intended to be customized before use; see the comments in the script
+to help you get started.
+
+Using git-multimail as a Python module as done here provides more
+flexibility. It has the following advantages:
* The tool's behavior can be customized using arbitrary Python code,
without having to edit git_multimail.py.
@@ -56,8 +58,11 @@ config = git_multimail.Config('multimailhook')
# Select the type of environment:
-environment = git_multimail.GenericEnvironment(config=config)
-#environment = git_multimail.GitoliteEnvironment(config=config)
+try:
+ environment = git_multimail.GenericEnvironment(config=config)
+ #environment = git_multimail.GitoliteEnvironment(config=config)
+except git_multimail.ConfigurationException, e:
+ sys.exit(str(e))
# Choose the method of sending emails based on the git config:
diff --git a/contrib/hooks/pre-auto-gc-battery b/contrib/hooks/pre-auto-gc-battery
index 9d0c2d1990..6a2cdebdb7 100755
--- a/contrib/hooks/pre-auto-gc-battery
+++ b/contrib/hooks/pre-auto-gc-battery
@@ -33,7 +33,7 @@ elif grep -q "AC Power \+: 1" /proc/pmu/info 2>/dev/null
then
exit 0
elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt |
- grep -q "Currently drawing from 'AC Power'"
+ grep -q "drawing from 'AC Power'"
then
exit 0
fi
diff --git a/contrib/mw-to-git/git-remote-mediawiki.perl b/contrib/mw-to-git/git-remote-mediawiki.perl
index 3f8d993afa..8dd74a9a40 100755
--- a/contrib/mw-to-git/git-remote-mediawiki.perl
+++ b/contrib/mw-to-git/git-remote-mediawiki.perl
@@ -461,7 +461,12 @@ sub download_mw_mediafile {
my $response = $mediawiki->{ua}->get($download_url);
if ($response->code == HTTP_CODE_OK) {
- return $response->decoded_content;
+ # It is tempting to return
+ # $response->decoded_content({charset => "none"}), but
+ # when doing so, utf8::downgrade($content) fails with
+ # "Wide character in subroutine entry".
+ $response->decode();
+ return $response->content();
} else {
print {*STDERR} "Error downloading mediafile from :\n";
print {*STDERR} "URL: ${download_url}\n";
diff --git a/contrib/mw-to-git/t/install-wiki.sh b/contrib/mw-to-git/t/install-wiki.sh
index 70a53f67fd..c215213c4b 100755
--- a/contrib/mw-to-git/t/install-wiki.sh
+++ b/contrib/mw-to-git/t/install-wiki.sh
@@ -20,6 +20,8 @@ usage () {
echo " install | -i : Install a wiki on your computer."
echo " delete | -d : Delete the wiki and all its pages and "
echo " content."
+ echo " start | -s : Start the previously configured lighttpd daemon"
+ echo " stop : Stop lighttpd daemon."
}
@@ -33,6 +35,14 @@ case "$1" in
wiki_delete
exit 0
;;
+ "start" | "-s")
+ start_lighttpd
+ exit
+ ;;
+ "stop")
+ stop_lighttpd
+ exit
+ ;;
"--help" | "-h")
usage
exit 0
diff --git a/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh b/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh
index 5a0373935f..3ff3a09567 100755
--- a/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh
+++ b/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh
@@ -58,6 +58,25 @@ test_expect_success 'git clone works on previously created wiki with media files
test_cmp mw_dir_clone/Foo.txt mw_dir/Foo.txt
'
+test_expect_success 'git push can upload media (File:) files containing valid UTF-8' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir &&
+ (
+ cd mw_dir &&
+ "$PERL_PATH" -e "print STDOUT \"UTF-8 content: éèàéê€.\";" >Bar.txt &&
+ git add Bar.txt &&
+ git commit -m "add a text file with UTF-8 content" &&
+ git push
+ )
+'
+
+test_expect_success 'git clone works on previously created wiki with media files containing valid UTF-8' '
+ test_when_finished "rm -rf mw_dir mw_dir_clone" &&
+ git clone -c remote.origin.mediaimport=true \
+ mediawiki::'"$WIKI_URL"' mw_dir_clone &&
+ test_cmp mw_dir_clone/Bar.txt mw_dir/Bar.txt
+'
+
test_expect_success 'git push & pull work with locally renamed media files' '
wiki_reset &&
git clone mediawiki::'"$WIKI_URL"' mw_dir &&
diff --git a/contrib/mw-to-git/t/t9365-continuing-queries.sh b/contrib/mw-to-git/t/t9365-continuing-queries.sh
index 27e267f532..016454749f 100755
--- a/contrib/mw-to-git/t/t9365-continuing-queries.sh
+++ b/contrib/mw-to-git/t/t9365-continuing-queries.sh
@@ -9,7 +9,7 @@ test_check_precond
test_expect_success 'creating page w/ >500 revisions' '
wiki_reset &&
- for i in `test_seq 501`
+ for i in $(test_seq 501)
do
echo "creating revision $i" &&
wiki_editpage foo "revision $i<br/>" true
diff --git a/contrib/mw-to-git/t/test-gitmw-lib.sh b/contrib/mw-to-git/t/test-gitmw-lib.sh
index 3372b2af34..6546294f15 100755
--- a/contrib/mw-to-git/t/test-gitmw-lib.sh
+++ b/contrib/mw-to-git/t/test-gitmw-lib.sh
@@ -90,7 +90,7 @@ test_diff_directories () {
#
# Check that <dir> contains exactly <N> files
test_contains_N_files () {
- if test `ls -- "$1" | wc -l` -ne "$2"; then
+ if test $(ls -- "$1" | wc -l) -ne "$2"; then
echo "directory $1 should contain $2 files"
echo "it contains these files:"
ls "$1"
@@ -289,7 +289,6 @@ start_lighttpd () {
# Kill daemon lighttpd and removes files and folders associated.
stop_lighttpd () {
test -f "$WEB_TMP/pid" && kill $(cat "$WEB_TMP/pid")
- rm -rf "$WEB"
}
# Create the SQLite database of the MediaWiki. If the database file already
@@ -341,10 +340,10 @@ wiki_install () {
"http://download.wikimedia.org/mediawiki/$MW_VERSION_MAJOR/"\
"$MW_FILENAME. "\
"Please fix your connection and launch the script again."
- echo "$MW_FILENAME downloaded in `pwd`. "\
+ echo "$MW_FILENAME downloaded in $(pwd). "\
"You can delete it later if you want."
else
- echo "Reusing existing $MW_FILENAME downloaded in `pwd`."
+ echo "Reusing existing $MW_FILENAME downloaded in $(pwd)."
fi
archive_abs_path=$(pwd)/$MW_FILENAME
cd "$WIKI_DIR_INST/$WIKI_DIR_NAME/" ||
@@ -415,6 +414,7 @@ wiki_reset () {
wiki_delete () {
if test $LIGHTTPD = "true"; then
stop_lighttpd
+ rm -fr "$WEB"
else
# Delete the wiki's directory.
rm -rf "$WIKI_DIR_INST/$WIKI_DIR_NAME" ||
diff --git a/contrib/subtree/.gitignore b/contrib/subtree/.gitignore
index 91360a3d7f..0b9381abca 100644
--- a/contrib/subtree/.gitignore
+++ b/contrib/subtree/.gitignore
@@ -1,6 +1,7 @@
*~
git-subtree
-git-subtree.xml
git-subtree.1
+git-subtree.html
+git-subtree.xml
mainline
subproj
diff --git a/contrib/subtree/Makefile b/contrib/subtree/Makefile
index 4030a16898..3071baf493 100644
--- a/contrib/subtree/Makefile
+++ b/contrib/subtree/Makefile
@@ -1,19 +1,34 @@
+# The default target of this Makefile is...
+all::
+
-include ../../config.mak.autogen
-include ../../config.mak
prefix ?= /usr/local
+gitexecdir ?= $(prefix)/libexec/git-core
mandir ?= $(prefix)/share/man
-libexecdir ?= $(prefix)/libexec/git-core
-gitdir ?= $(shell git --exec-path)
man1dir ?= $(mandir)/man1
+htmldir ?= $(prefix)/share/doc/git-doc
+
+../../GIT-VERSION-FILE: FORCE
+ $(MAKE) -C ../../ GIT-VERSION-FILE
-gitver ?= $(word 3,$(shell git --version))
+-include ../../GIT-VERSION-FILE
# this should be set to a 'standard' bsd-type install program
-INSTALL ?= install
+INSTALL ?= install
+RM ?= rm -f
+
+ASCIIDOC = asciidoc
+XMLTO = xmlto
-ASCIIDOC_CONF = ../../Documentation/asciidoc.conf
-MANPAGE_NORMAL_XSL = ../../Documentation/manpage-normal.xsl
+ifndef SHELL_PATH
+ SHELL_PATH = /bin/sh
+endif
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+
+ASCIIDOC_CONF = ../../Documentation/asciidoc.conf
+MANPAGE_XSL = ../../Documentation/manpage-normal.xsl
GIT_SUBTREE_SH := git-subtree.sh
GIT_SUBTREE := git-subtree
@@ -23,37 +38,44 @@ GIT_SUBTREE_XML := git-subtree.xml
GIT_SUBTREE_TXT := git-subtree.txt
GIT_SUBTREE_HTML := git-subtree.html
-all: $(GIT_SUBTREE)
+all:: $(GIT_SUBTREE)
$(GIT_SUBTREE): $(GIT_SUBTREE_SH)
- cp $< $@ && chmod +x $@
+ sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' $< >$@
+ chmod +x $@
doc: $(GIT_SUBTREE_DOC) $(GIT_SUBTREE_HTML)
install: $(GIT_SUBTREE)
- $(INSTALL) -d -m 755 $(DESTDIR)$(libexecdir)
- $(INSTALL) -m 755 $(GIT_SUBTREE) $(DESTDIR)$(libexecdir)
+ $(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir)
+ $(INSTALL) -m 755 $(GIT_SUBTREE) $(DESTDIR)$(gitexecdir)
-install-doc: install-man
+install-doc: install-man install-html
install-man: $(GIT_SUBTREE_DOC)
$(INSTALL) -d -m 755 $(DESTDIR)$(man1dir)
$(INSTALL) -m 644 $^ $(DESTDIR)$(man1dir)
+install-html: $(GIT_SUBTREE_HTML)
+ $(INSTALL) -d -m 755 $(DESTDIR)$(htmldir)
+ $(INSTALL) -m 644 $^ $(DESTDIR)$(htmldir)
+
$(GIT_SUBTREE_DOC): $(GIT_SUBTREE_XML)
- xmlto -m $(MANPAGE_NORMAL_XSL) man $^
+ $(XMLTO) -m $(MANPAGE_XSL) man $^
$(GIT_SUBTREE_XML): $(GIT_SUBTREE_TXT)
- asciidoc -b docbook -d manpage -f $(ASCIIDOC_CONF) \
- -agit_version=$(gitver) $^
+ $(ASCIIDOC) -b docbook -d manpage -f $(ASCIIDOC_CONF) \
+ -agit_version=$(GIT_VERSION) $^
$(GIT_SUBTREE_HTML): $(GIT_SUBTREE_TXT)
- asciidoc -b xhtml11 -d manpage -f $(ASCIIDOC_CONF) \
- -agit_version=$(gitver) $^
+ $(ASCIIDOC) -b xhtml11 -d manpage -f $(ASCIIDOC_CONF) \
+ -agit_version=$(GIT_VERSION) $^
test:
$(MAKE) -C t/ test
clean:
- rm -f *~ *.xml *.html *.1
- rm -rf subproj mainline
+ $(RM) $(GIT_SUBTREE)
+ $(RM) *.xml *.html *.1
+
+.PHONY: FORCE
diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh
index db925ca769..9f06571851 100755
--- a/contrib/subtree/git-subtree.sh
+++ b/contrib/subtree/git-subtree.sh
@@ -26,7 +26,7 @@ b,branch= create a new branch from the split subtree
ignore-joins ignore prior --rejoin commits
onto= try connecting new tree to an existing one
rejoin merge the new branch back into HEAD
- options for 'add', 'merge', 'pull' and 'push'
+ options for 'add', 'merge', and 'pull'
squash merge subtree changes as a single commit
"
eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
@@ -51,14 +51,21 @@ prefix=
debug()
{
if [ -n "$debug" ]; then
- echo "$@" >&2
+ printf "%s\n" "$*" >&2
fi
}
say()
{
if [ -z "$quiet" ]; then
- echo "$@" >&2
+ printf "%s\n" "$*" >&2
+ fi
+}
+
+progress()
+{
+ if [ -z "$quiet" ]; then
+ printf "%s\r" "$*" >&2
fi
}
@@ -298,7 +305,7 @@ 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}"
- git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%B' "$1" |
+ git log -1 --pretty=format:'%an%n%ae%n%aD%n%cn%n%ce%n%cD%n%B' "$1" |
(
read GIT_AUTHOR_NAME
read GIT_AUTHOR_EMAIL
@@ -558,8 +565,9 @@ cmd_add_commit()
commit=$(add_squashed_msg "$rev" "$dir" |
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 "$rev") || exit $?
+ git commit-tree $tree $headp -p "$revp") || exit $?
fi
git reset "$commit" || exit $?
@@ -598,7 +606,7 @@ cmd_split()
eval "$grl" |
while read rev parents; do
revcount=$(($revcount + 1))
- say -n "$revcount/$revmax ($createcount) "
+ progress "$revcount/$revmax ($createcount)"
debug "Processing commit: $rev"
exists=$(cache_get $rev)
if [ -n "$exists" ]; then
diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt
index 02669b1534..60d76cdddf 100644
--- a/contrib/subtree/git-subtree.txt
+++ b/contrib/subtree/git-subtree.txt
@@ -81,12 +81,11 @@ merge::
changes into the latest <commit>. With '--squash',
creates only one commit that contains all the changes,
rather than merging in the entire history.
-
- If you use '--squash', the merge direction doesn't
- always have to be forward; you can use this command to
- go back in time from v2.5 to v2.4, for example. If your
- merge introduces a conflict, you can resolve it in the
- usual ways.
++
+If you use '--squash', the merge direction doesn't always have to be
+forward; you can use this command to go back in time from v2.5 to v2.4,
+for example. If your merge introduces a conflict, you can resolve it in
+the usual ways.
pull::
Exactly like 'merge', but parallels 'git pull' in that
@@ -107,21 +106,19 @@ split::
contents of <prefix> at the root of the project instead
of in a subdirectory. Thus, the newly created history
is suitable for export as a separate git repository.
-
- After splitting successfully, a single commit id is
- printed to stdout. This corresponds to the HEAD of the
- newly created tree, which you can manipulate however you
- want.
-
- Repeated splits of exactly the same history are
- guaranteed to be identical (ie. to produce the same
- commit ids). Because of this, if you add new commits
- and then re-split, the new commits will be attached as
- commits on top of the history you generated last time,
- so 'git merge' and friends will work as expected.
-
- Note that if you use '--squash' when you merge, you
- should usually not just '--rejoin' when you split.
++
+After splitting successfully, a single commit id is printed to stdout.
+This corresponds to the HEAD of the newly created tree, which you can
+manipulate however you want.
++
+Repeated splits of exactly the same history are guaranteed to be
+identical (i.e. to produce the same commit ids). Because of this, if
+you add new commits and then re-split, the new commits will be attached
+as commits on top of the history you generated last time, so 'git merge'
+and friends will work as expected.
++
+Note that if you use '--squash' when you merge, you should usually not
+just '--rejoin' when you split.
OPTIONS
@@ -149,111 +146,98 @@ OPTIONS
OPTIONS FOR add, merge, push, pull
----------------------------------
--squash::
- This option is only valid for add, merge, push and pull
+ This option is only valid for add, merge, and pull
commands.
-
- Instead of merging the entire history from the subtree
- project, produce only a single commit that contains all
- the differences you want to merge, and then merge that
- new commit into your project.
-
- Using this option helps to reduce log clutter. People
- rarely want to see every change that happened between
- v1.0 and v1.1 of the library they're using, since none of the
- interim versions were ever included in their application.
-
- Using '--squash' also helps avoid problems when the same
- subproject is included multiple times in the same
- project, or is removed and then re-added. In such a
- case, it doesn't make sense to combine the histories
- anyway, since it's unclear which part of the history
- belongs to which subtree.
-
- Furthermore, with '--squash', you can switch back and
- forth between different versions of a subtree, rather
- than strictly forward. 'git subtree merge --squash'
- always adjusts the subtree to match the exactly
- specified commit, even if getting to that commit would
- require undoing some changes that were added earlier.
-
- Whether or not you use '--squash', changes made in your
- local repository remain intact and can be later split
- and send upstream to the subproject.
++
+Instead of merging the entire history from the subtree project, produce
+only a single commit that contains all the differences you want to
+merge, and then merge that new commit into your project.
++
+Using this option helps to reduce log clutter. People rarely want to see
+every change that happened between v1.0 and v1.1 of the library they're
+using, since none of the interim versions were ever included in their
+application.
++
+Using '--squash' also helps avoid problems when the same subproject is
+included multiple times in the same project, or is removed and then
+re-added. In such a case, it doesn't make sense to combine the
+histories anyway, since it's unclear which part of the history belongs
+to which subtree.
++
+Furthermore, with '--squash', you can switch back and forth between
+different versions of a subtree, rather than strictly forward. 'git
+subtree merge --squash' always adjusts the subtree to match the exactly
+specified commit, even if getting to that commit would require undoing
+some changes that were added earlier.
++
+Whether or not you use '--squash', changes made in your local repository
+remain intact and can be later split and send upstream to the
+subproject.
OPTIONS FOR split
-----------------
--annotate=<annotation>::
This option is only valid for the split command.
-
- When generating synthetic history, add <annotation> as a
- prefix to each commit message. Since we're creating new
- commits with the same commit message, but possibly
- different content, from the original commits, this can help
- to differentiate them and avoid confusion.
-
- Whenever you split, you need to use the same
- <annotation>, or else you don't have a guarantee that
- the new re-created history will be identical to the old
- one. That will prevent merging from working correctly.
- git subtree tries to make it work anyway, particularly
- if you use --rejoin, but it may not always be effective.
++
+When generating synthetic history, add <annotation> as a prefix to each
+commit message. Since we're creating new commits with the same commit
+message, but possibly different content, from the original commits, this
+can help to differentiate them and avoid confusion.
++
+Whenever you split, you need to use the same <annotation>, or else you
+don't have a guarantee that the new re-created history will be identical
+to the old one. That will prevent merging from working correctly. git
+subtree tries to make it work anyway, particularly if you use --rejoin,
+but it may not always be effective.
-b <branch>::
--branch=<branch>::
This option is only valid for the split command.
-
- After generating the synthetic history, create a new
- branch called <branch> that contains the new history.
- This is suitable for immediate pushing upstream.
- <branch> must not already exist.
++
+After generating the synthetic history, create a new branch called
+<branch> that contains the new history. This is suitable for immediate
+pushing upstream. <branch> must not already exist.
--ignore-joins::
This option is only valid for the split command.
-
- If you use '--rejoin', git subtree attempts to optimize
- its history reconstruction to generate only the new
- commits since the last '--rejoin'. '--ignore-join'
- disables this behaviour, forcing it to regenerate the
- entire history. In a large project, this can take a
- long time.
++
+If you use '--rejoin', git subtree attempts to optimize its history
+reconstruction to generate only the new commits since the last
+'--rejoin'. '--ignore-join' disables this behaviour, forcing it to
+regenerate the entire history. In a large project, this can take a long
+time.
--onto=<onto>::
This option is only valid for the split command.
-
- If your subtree was originally imported using something
- other than git subtree, its history may not match what
- git subtree is expecting. In that case, you can specify
- the commit id <onto> that corresponds to the first
- revision of the subproject's history that was imported
- into your project, and git subtree will attempt to build
- its history from there.
-
- If you used 'git subtree add', you should never need
- this option.
++
+If your subtree was originally imported using something other than git
+subtree, its history may not match what git subtree is expecting. In
+that case, you can specify the commit id <onto> that corresponds to the
+first revision of the subproject's history that was imported into your
+project, and git subtree will attempt to build its history from there.
++
+If you used 'git subtree add', you should never need this option.
--rejoin::
This option is only valid for the split command.
-
- After splitting, merge the newly created synthetic
- history back into your main project. That way, future
- splits can search only the part of history that has
- been added since the most recent --rejoin.
-
- If your split commits end up merged into the upstream
- subproject, and then you want to get the latest upstream
- version, this will allow git's merge algorithm to more
- intelligently avoid conflicts (since it knows these
- synthetic commits are already part of the upstream
- repository).
-
- Unfortunately, using this option results in 'git log'
- showing an extra copy of every new commit that was
- created (the original, and the synthetic one).
-
- If you do all your merges with '--squash', don't use
- '--rejoin' when you split, because you don't want the
- subproject's history to be part of your project anyway.
++
+After splitting, merge the newly created synthetic history back into
+your main project. That way, future splits can search only the part of
+history that has been added since the most recent --rejoin.
++
+If your split commits end up merged into the upstream subproject, and
+then you want to get the latest upstream version, this will allow git's
+merge algorithm to more intelligently avoid conflicts (since it knows
+these synthetic commits are already part of the upstream repository).
++
+Unfortunately, using this option results in 'git log' showing an extra
+copy of every new commit that was created (the original, and the
+synthetic one).
++
+If you do all your merges with '--squash', don't use '--rejoin' when you
+split, because you don't want the subproject's history to be part of
+your project anyway.
EXAMPLE 1. Add command
diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh
index 8dc6840353..90519823be 100755
--- a/contrib/subtree/t/t7900-subtree.sh
+++ b/contrib/subtree/t/t7900-subtree.sh
@@ -62,151 +62,156 @@ last_commit_message()
}
test_expect_success 'init subproj' '
- test_create_repo subproj
+ test_create_repo subproj
'
# To the subproject!
cd subproj
test_expect_success 'add sub1' '
- create sub1 &&
- git commit -m "sub1" &&
- git branch sub1 &&
- git branch -m master subproj
+ create sub1 &&
+ git commit -m "sub1" &&
+ git branch sub1 &&
+ git branch -m master subproj
'
# Save this hash for testing later.
-subdir_hash=`git rev-parse HEAD`
+subdir_hash=$(git rev-parse HEAD)
test_expect_success 'add sub2' '
- create sub2 &&
- git commit -m "sub2" &&
- git branch sub2
+ create sub2 &&
+ git commit -m "sub2" &&
+ git branch sub2
'
test_expect_success 'add sub3' '
- create sub3 &&
- git commit -m "sub3" &&
- git branch sub3
+ create sub3 &&
+ git commit -m "sub3" &&
+ git branch sub3
'
# Back to mainline
cd ..
+test_expect_success 'enable log.date=relative to catch errors' '
+ git config log.date relative
+'
+
test_expect_success 'add main4' '
- create main4 &&
- git commit -m "main4" &&
- git branch -m master mainline &&
- git branch subdir
+ create main4 &&
+ git commit -m "main4" &&
+ git branch -m master mainline &&
+ git branch subdir
'
test_expect_success 'fetch subproj history' '
- git fetch ./subproj sub1 &&
- git branch sub1 FETCH_HEAD
+ git fetch ./subproj sub1 &&
+ git branch sub1 FETCH_HEAD
'
test_expect_success 'no subtree exists in main tree' '
- test_must_fail git subtree merge --prefix=subdir sub1
+ test_must_fail git subtree merge --prefix=subdir sub1
'
test_expect_success 'no pull from non-existant subtree' '
- test_must_fail git subtree pull --prefix=subdir ./subproj sub1
+ test_must_fail git subtree pull --prefix=subdir ./subproj sub1
'
test_expect_success 'check if --message works for add' '
- git subtree add --prefix=subdir --message="Added subproject" sub1 &&
- check_equal ''"$(last_commit_message)"'' "Added subproject" &&
- undo
+ git subtree add --prefix=subdir --message="Added subproject" sub1 &&
+ check_equal ''"$(last_commit_message)"'' "Added subproject" &&
+ undo
'
test_expect_success 'check if --message works as -m and --prefix as -P' '
- git subtree add -P subdir -m "Added subproject using git subtree" sub1 &&
- check_equal ''"$(last_commit_message)"'' "Added subproject using git subtree" &&
- undo
+ git subtree add -P subdir -m "Added subproject using git subtree" sub1 &&
+ check_equal ''"$(last_commit_message)"'' "Added subproject using git subtree" &&
+ undo
'
test_expect_success 'check if --message works with squash too' '
- git subtree add -P subdir -m "Added subproject with squash" --squash sub1 &&
- check_equal ''"$(last_commit_message)"'' "Added subproject with squash" &&
- undo
+ git subtree add -P subdir -m "Added subproject with squash" --squash sub1 &&
+ check_equal ''"$(last_commit_message)"'' "Added subproject with squash" &&
+ undo
'
test_expect_success 'add subproj to mainline' '
- git subtree add --prefix=subdir/ FETCH_HEAD &&
- check_equal ''"$(last_commit_message)"'' "Add '"'subdir/'"' from commit '"'"'''"$(git rev-parse sub1)"'''"'"'"
+ git subtree add --prefix=subdir/ FETCH_HEAD &&
+ check_equal ''"$(last_commit_message)"'' "Add '"'subdir/'"' from commit '"'"'''"$(git rev-parse sub1)"'''"'"'"
'
# this shouldn't actually do anything, since FETCH_HEAD is already a parent
test_expect_success 'merge fetched subproj' '
- git merge -m "merge -s -ours" -s ours FETCH_HEAD
+ git merge -m "merge -s -ours" -s ours FETCH_HEAD
'
test_expect_success 'add main-sub5' '
- create subdir/main-sub5 &&
- git commit -m "main-sub5"
+ create subdir/main-sub5 &&
+ git commit -m "main-sub5"
'
test_expect_success 'add main6' '
- create main6 &&
- git commit -m "main6 boring"
+ create main6 &&
+ git commit -m "main6 boring"
'
test_expect_success 'add main-sub7' '
- create subdir/main-sub7 &&
- git commit -m "main-sub7"
+ create subdir/main-sub7 &&
+ git commit -m "main-sub7"
'
test_expect_success 'fetch new subproj history' '
- git fetch ./subproj sub2 &&
- git branch sub2 FETCH_HEAD
+ git fetch ./subproj sub2 &&
+ git branch sub2 FETCH_HEAD
'
test_expect_success 'check if --message works for merge' '
- git subtree merge --prefix=subdir -m "Merged changes from subproject" sub2 &&
- check_equal ''"$(last_commit_message)"'' "Merged changes from subproject" &&
- undo
+ git subtree merge --prefix=subdir -m "Merged changes from subproject" sub2 &&
+ check_equal ''"$(last_commit_message)"'' "Merged changes from subproject" &&
+ undo
'
test_expect_success 'check if --message for merge works with squash too' '
- git subtree merge --prefix subdir -m "Merged changes from subproject using squash" --squash sub2 &&
- check_equal ''"$(last_commit_message)"'' "Merged changes from subproject using squash" &&
- undo
+ git subtree merge --prefix subdir -m "Merged changes from subproject using squash" --squash sub2 &&
+ check_equal ''"$(last_commit_message)"'' "Merged changes from subproject using squash" &&
+ undo
'
test_expect_success 'merge new subproj history into subdir' '
- git subtree merge --prefix=subdir FETCH_HEAD &&
- git branch pre-split &&
- check_equal ''"$(last_commit_message)"'' "Merge commit '"'"'"$(git rev-parse sub2)"'"'"' into mainline"
+ git subtree merge --prefix=subdir FETCH_HEAD &&
+ git branch pre-split &&
+ check_equal ''"$(last_commit_message)"'' "Merge commit '"'"'"$(git rev-parse sub2)"'"'"' into mainline" &&
+ undo
'
test_expect_success 'Check that prefix argument is required for split' '
- echo "You must provide the --prefix option." > expected &&
- test_must_fail git subtree split > actual 2>&1 &&
+ echo "You must provide the --prefix option." > expected &&
+ test_must_fail git subtree split > actual 2>&1 &&
test_debug "printf '"'"'expected: '"'"'" &&
- test_debug "cat expected" &&
+ test_debug "cat expected" &&
test_debug "printf '"'"'actual: '"'"'" &&
- test_debug "cat actual" &&
- test_cmp expected actual &&
- rm -f expected actual
+ test_debug "cat actual" &&
+ test_cmp expected actual &&
+ rm -f expected actual
'
test_expect_success 'Check that the <prefix> exists for a split' '
- echo "'"'"'non-existent-directory'"'"'" does not exist\; use "'"'"'git subtree add'"'"'" > expected &&
- test_must_fail git subtree split --prefix=non-existent-directory > actual 2>&1 &&
+ echo "'"'"'non-existent-directory'"'"'" does not exist\; use "'"'"'git subtree add'"'"'" > expected &&
+ test_must_fail git subtree split --prefix=non-existent-directory > actual 2>&1 &&
test_debug "printf '"'"'expected: '"'"'" &&
- test_debug "cat expected" &&
+ test_debug "cat expected" &&
test_debug "printf '"'"'actual: '"'"'" &&
- test_debug "cat actual" &&
- test_cmp expected actual
-# rm -f expected actual
+ test_debug "cat actual" &&
+ test_cmp expected actual
+# rm -f expected actual
'
test_expect_success 'check if --message works for split+rejoin' '
- spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
- git branch spl1 "$spl1" &&
- check_equal ''"$(last_commit_message)"'' "Split & rejoin" &&
- undo
+ spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
+ git branch spl1 "$spl1" &&
+ check_equal ''"$(last_commit_message)"'' "Split & rejoin" &&
+ undo
'
test_expect_success 'check split with --branch' '
@@ -218,79 +223,76 @@ test_expect_success 'check split with --branch' '
test_expect_success 'check hash of split' '
spl1=$(git subtree split --prefix subdir) &&
- undo &&
git subtree split --prefix subdir --branch splitbr1test &&
- check_equal ''"$(git rev-parse splitbr1test)"'' "$spl1"
- git checkout splitbr1test &&
- new_hash=$(git rev-parse HEAD~2) &&
- git checkout mainline &&
+ check_equal ''"$(git rev-parse splitbr1test)"'' "$spl1" &&
+ new_hash=$(git rev-parse splitbr1test~2) &&
check_equal ''"$new_hash"'' "$subdir_hash"
'
test_expect_success 'check split with --branch for an existing branch' '
- spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
- undo &&
- git branch splitbr2 sub1 &&
- git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr2 &&
- check_equal ''"$(git rev-parse splitbr2)"'' "$spl1"
+ spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
+ undo &&
+ git branch splitbr2 sub1 &&
+ git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr2 &&
+ check_equal ''"$(git rev-parse splitbr2)"'' "$spl1"
'
test_expect_success 'check split with --branch for an incompatible branch' '
- test_must_fail git subtree split --prefix subdir --onto FETCH_HEAD --branch subdir
+ test_must_fail git subtree split --prefix subdir --onto FETCH_HEAD --branch subdir
'
test_expect_success 'check split+rejoin' '
- spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
- undo &&
- git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --rejoin &&
- check_equal ''"$(last_commit_message)"'' "Split '"'"'subdir/'"'"' into commit '"'"'"$spl1"'"'"'"
+ spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' &&
+ undo &&
+ git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --rejoin &&
+ check_equal ''"$(last_commit_message)"'' "Split '"'"'subdir/'"'"' into commit '"'"'"$spl1"'"'"'"
'
test_expect_success 'add main-sub8' '
- create subdir/main-sub8 &&
- git commit -m "main-sub8"
+ create subdir/main-sub8 &&
+ git commit -m "main-sub8"
'
# To the subproject!
cd ./subproj
test_expect_success 'merge split into subproj' '
- git fetch .. spl1 &&
- git branch spl1 FETCH_HEAD &&
- git merge FETCH_HEAD
+ git fetch .. spl1 &&
+ git branch spl1 FETCH_HEAD &&
+ git merge FETCH_HEAD
'
test_expect_success 'add sub9' '
- create sub9 &&
- git commit -m "sub9"
+ create sub9 &&
+ git commit -m "sub9"
'
# Back to mainline
cd ..
test_expect_success 'split for sub8' '
- split2=''"$(git subtree split --annotate='"'*'"' --prefix subdir/ --rejoin)"''
- git branch split2 "$split2"
+ split2=''"$(git subtree split --annotate='"'*'"' --prefix subdir/ --rejoin)"'' &&
+ git branch split2 "$split2"
'
test_expect_success 'add main-sub10' '
- create subdir/main-sub10 &&
- git commit -m "main-sub10"
+ create subdir/main-sub10 &&
+ git commit -m "main-sub10"
'
test_expect_success 'split for sub10' '
- spl3=''"$(git subtree split --annotate='"'*'"' --prefix subdir --rejoin)"'' &&
- git branch spl3 "$spl3"
+ spl3=''"$(git subtree split --annotate='"'*'"' --prefix subdir --rejoin)"'' &&
+ git branch spl3 "$spl3"
'
# To the subproject!
cd ./subproj
test_expect_success 'merge split into subproj' '
- git fetch .. spl3 &&
- git branch spl3 FETCH_HEAD &&
- git merge FETCH_HEAD &&
- git branch subproj-merge-spl3
+ git fetch .. spl3 &&
+ git branch spl3 FETCH_HEAD &&
+ git merge FETCH_HEAD &&
+ git branch subproj-merge-spl3
'
chkm="main4 main6"
@@ -300,44 +302,44 @@ chks="sub1 sub2 sub3 sub9"
chks_sub=$(echo $chks | multiline | sed 's,^,subdir/,' | fixnl)
test_expect_success 'make sure exactly the right set of files ends up in the subproj' '
- subfiles=''"$(git ls-files | fixnl)"'' &&
- check_equal "$subfiles" "$chkms $chks"
+ subfiles=''"$(git ls-files | fixnl)"'' &&
+ check_equal "$subfiles" "$chkms $chks"
'
test_expect_success 'make sure the subproj history *only* contains commits that affect the subdir' '
- allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' &&
- check_equal "$allchanges" "$chkms $chks"
+ allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' &&
+ check_equal "$allchanges" "$chkms $chks"
'
# Back to mainline
cd ..
test_expect_success 'pull from subproj' '
- git fetch ./subproj subproj-merge-spl3 &&
- git branch subproj-merge-spl3 FETCH_HEAD &&
- git subtree pull --prefix=subdir ./subproj subproj-merge-spl3
+ git fetch ./subproj subproj-merge-spl3 &&
+ git branch subproj-merge-spl3 FETCH_HEAD &&
+ git subtree pull --prefix=subdir ./subproj subproj-merge-spl3
'
test_expect_success 'make sure exactly the right set of files ends up in the mainline' '
- mainfiles=''"$(git ls-files | fixnl)"'' &&
- check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub"
+ mainfiles=''"$(git ls-files | fixnl)"'' &&
+ check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub"
'
test_expect_success 'make sure each filename changed exactly once in the entire history' '
- # main-sub?? and /subdir/main-sub?? both change, because those are the
- # changes that were split into their own history. And subdir/sub?? never
- # change, since they were *only* changed in the subtree branch.
- allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' &&
- check_equal "$allchanges" ''"$(echo $chkms $chkm $chks $chkms_sub | multiline | sort | fixnl)"''
+ # main-sub?? and /subdir/main-sub?? both change, because those are the
+ # changes that were split into their own history. And subdir/sub?? never
+ # change, since they were *only* changed in the subtree branch.
+ allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' &&
+ check_equal "$allchanges" ''"$(echo $chkms $chkm $chks $chkms_sub | multiline | sort | fixnl)"''
'
test_expect_success 'make sure the --rejoin commits never make it into subproj' '
- check_equal ''"$(git log --pretty=format:'"'%s'"' HEAD^2 | grep -i split)"'' ""
+ check_equal ''"$(git log --pretty=format:'"'%s'"' HEAD^2 | grep -i split)"'' ""
'
test_expect_success 'make sure no "git subtree" tagged commits make it into subproj' '
- # They are meaningless to subproj since one side of the merge refers to the mainline
- check_equal ''"$(git log --pretty=format:'"'%s%n%b'"' HEAD^2 | grep "git-subtree.*:")"'' ""
+ # They are meaningless to subproj since one side of the merge refers to the mainline
+ check_equal ''"$(git log --pretty=format:'"'%s%n%b'"' HEAD^2 | grep "git-subtree.*:")"'' ""
'
# prepare second pair of repositories
@@ -345,27 +347,27 @@ mkdir test2
cd test2
test_expect_success 'init main' '
- test_create_repo main
+ test_create_repo main
'
cd main
test_expect_success 'add main1' '
- create main1 &&
- git commit -m "main1"
+ create main1 &&
+ git commit -m "main1"
'
cd ..
test_expect_success 'init sub' '
- test_create_repo sub
+ test_create_repo sub
'
cd sub
test_expect_success 'add sub2' '
- create sub2 &&
- git commit -m "sub2"
+ create sub2 &&
+ git commit -m "sub2"
'
cd ../main
@@ -373,33 +375,33 @@ cd ../main
# check if split can find proper base without --onto
test_expect_success 'add sub as subdir in main' '
- git fetch ../sub master &&
- git branch sub2 FETCH_HEAD &&
- git subtree add --prefix subdir sub2
+ git fetch ../sub master &&
+ git branch sub2 FETCH_HEAD &&
+ git subtree add --prefix subdir sub2
'
cd ../sub
test_expect_success 'add sub3' '
- create sub3 &&
- git commit -m "sub3"
+ create sub3 &&
+ git commit -m "sub3"
'
cd ../main
test_expect_success 'merge from sub' '
- git fetch ../sub master &&
- git branch sub3 FETCH_HEAD &&
- git subtree merge --prefix subdir sub3
+ git fetch ../sub master &&
+ git branch sub3 FETCH_HEAD &&
+ git subtree merge --prefix subdir sub3
'
test_expect_success 'add main-sub4' '
- create subdir/main-sub4 &&
- git commit -m "main-sub4"
+ create subdir/main-sub4 &&
+ git commit -m "main-sub4"
'
test_expect_success 'split for main-sub4 without --onto' '
- git subtree split --prefix subdir --branch mainsub4
+ git subtree split --prefix subdir --branch mainsub4
'
# at this point, the new commit parent should be sub3 if it is not,
@@ -408,21 +410,21 @@ test_expect_success 'split for main-sub4 without --onto' '
# itself)
test_expect_success 'check that the commit parent is sub3' '
- check_equal ''"$(git log --pretty=format:%P -1 mainsub4)"'' ''"$(git rev-parse sub3)"''
+ check_equal ''"$(git log --pretty=format:%P -1 mainsub4)"'' ''"$(git rev-parse sub3)"''
'
test_expect_success 'add main-sub5' '
- mkdir subdir2 &&
- create subdir2/main-sub5 &&
- git commit -m "main-sub5"
+ mkdir subdir2 &&
+ create subdir2/main-sub5 &&
+ git commit -m "main-sub5"
'
test_expect_success 'split for main-sub5 without --onto' '
- # 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,
+ # 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 accidentally been attached to something
- git subtree split --prefix subdir2 --branch mainsub5 &&
- check_equal ''"$(git log --pretty=format:%P -1 mainsub5)"'' ""
+ git subtree split --prefix subdir2 --branch mainsub5 &&
+ check_equal ''"$(git log --pretty=format:%P -1 mainsub5)"'' ""
'
# make sure no patch changes more than one file. The original set of commits
@@ -450,20 +452,20 @@ joincommits()
}
test_expect_success 'verify one file change per commit' '
- x= &&
- list=''"$(git log --pretty=format:'"'commit: %H'"' | joincommits)"'' &&
-# test_debug "echo HERE" &&
-# test_debug "echo ''"$list"''" &&
- (git log --pretty=format:'"'commit: %H'"' | joincommits |
- ( while read commit a b; do
- test_debug "echo Verifying commit "''"$commit"''
- test_debug "echo a: "''"$a"''
- test_debug "echo b: "''"$b"''
- check_equal "$b" ""
- x=1
- done
- check_equal "$x" 1
- ))
+ x= &&
+ list=''"$(git log --pretty=format:'"'commit: %H'"' | joincommits)"'' &&
+# test_debug "echo HERE" &&
+# test_debug "echo ''"$list"''" &&
+ (git log --pretty=format:'"'commit: %H'"' | joincommits |
+ ( while read commit a b; do
+ test_debug "echo Verifying commit "''"$commit"''
+ test_debug "echo a: "''"$a"''
+ test_debug "echo b: "''"$b"''
+ check_equal "$b" ""
+ x=1
+ done
+ check_equal "$x" 1
+ ))
'
test_done
diff --git a/contrib/svn-fe/Makefile b/contrib/svn-fe/Makefile
index 360d8da417..e8651aaf4b 100644
--- a/contrib/svn-fe/Makefile
+++ b/contrib/svn-fe/Makefile
@@ -1,18 +1,58 @@
all:: svn-fe$X
-CC = gcc
+CC = cc
RM = rm -f
MV = mv
CFLAGS = -g -O2 -Wall
LDFLAGS =
-ALL_CFLAGS = $(CFLAGS)
-ALL_LDFLAGS = $(LDFLAGS)
-EXTLIBS =
+EXTLIBS = -lz
+
+include ../../config.mak.uname
+-include ../../config.mak.autogen
+-include ../../config.mak
+
+ifeq ($(uname_S),Darwin)
+ ifndef NO_FINK
+ ifeq ($(shell test -d /sw/lib && echo y),y)
+ CFLAGS += -I/sw/include
+ LDFLAGS += -L/sw/lib
+ endif
+ endif
+ ifndef NO_DARWIN_PORTS
+ ifeq ($(shell test -d /opt/local/lib && echo y),y)
+ CFLAGS += -I/opt/local/include
+ LDFLAGS += -L/opt/local/lib
+ endif
+ endif
+endif
+
+ifndef NO_OPENSSL
+ EXTLIBS += -lssl
+ ifdef NEEDS_CRYPTO_WITH_SSL
+ EXTLIBS += -lcrypto
+ endif
+endif
+
+ifndef NO_PTHREADS
+ CFLAGS += $(PTHREADS_CFLAGS)
+ EXTLIBS += $(PTHREAD_LIBS)
+endif
+
+ifdef HAVE_CLOCK_GETTIME
+ CFLAGS += -DHAVE_CLOCK_GETTIME
+ EXTLIBS += -lrt
+endif
+
+ifdef NEEDS_LIBICONV
+ EXTLIBS += -liconv
+endif
GIT_LIB = ../../libgit.a
VCSSVN_LIB = ../../vcs-svn/lib.a
-LIBS = $(VCSSVN_LIB) $(GIT_LIB) $(EXTLIBS)
+XDIFF_LIB = ../../xdiff/lib.a
+
+LIBS = $(VCSSVN_LIB) $(GIT_LIB) $(XDIFF_LIB)
QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir
QUIET_SUBDIR1 =
@@ -33,12 +73,11 @@ ifndef V
endif
endif
-svn-fe$X: svn-fe.o $(VCSSVN_LIB) $(GIT_LIB)
- $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ svn-fe.o \
- $(ALL_LDFLAGS) $(LIBS)
+svn-fe$X: svn-fe.o $(VCSSVN_LIB) $(XDIFF_LIB) $(GIT_LIB)
+ $(QUIET_LINK)$(CC) $(CFLAGS) $(LDFLAGS) $(EXTLIBS) -o $@ svn-fe.o $(LIBS)
svn-fe.o: svn-fe.c ../../vcs-svn/svndump.h
- $(QUIET_CC)$(CC) -I../../vcs-svn -o $*.o -c $(ALL_CFLAGS) $<
+ $(QUIET_CC)$(CC) $(CFLAGS) -I../../vcs-svn -o $*.o -c $<
svn-fe.html: svn-fe.txt
$(QUIET_SUBDIR0)../../Documentation $(QUIET_SUBDIR1) \
@@ -54,6 +93,9 @@ svn-fe.1: svn-fe.txt
../../vcs-svn/lib.a: FORCE
$(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) vcs-svn/lib.a
+../../xdiff/lib.a: FORCE
+ $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) xdiff/lib.a
+
../../libgit.a: FORCE
$(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) libgit.a
diff --git a/contrib/svn-fe/svnrdump_sim.py b/contrib/svn-fe/svnrdump_sim.py
index 4e78a1c3cd..11ac6f6927 100755
--- a/contrib/svn-fe/svnrdump_sim.py
+++ b/contrib/svn-fe/svnrdump_sim.py
@@ -5,53 +5,64 @@ of the specified revision range.
To simulate incremental imports the environment variable SVNRMAX can be set
to the highest revision that should be available.
"""
-import sys, os
+import sys
+import os
if sys.hexversion < 0x02040000:
- # The limiter is the ValueError() calls. This may be too conservative
- sys.stderr.write("svnrdump-sim.py: requires Python 2.4 or later.\n")
- sys.exit(1)
+ # The limiter is the ValueError() calls. This may be too conservative
+ sys.stderr.write("svnrdump-sim.py: requires Python 2.4 or later.\n")
+ sys.exit(1)
+
def getrevlimit():
- var = 'SVNRMAX'
- if var in os.environ:
- return os.environ[var]
- return None
+ var = 'SVNRMAX'
+ if var in os.environ:
+ return os.environ[var]
+ return None
+
def writedump(url, lower, upper):
- if url.startswith('sim://'):
- filename = url[6:]
- if filename[-1] == '/': filename = filename[:-1] #remove terminating slash
- else:
- raise ValueError('sim:// url required')
- f = open(filename, 'r');
- state = 'header'
- wroterev = False
- while(True):
- l = f.readline()
- if l == '': break
- if state == 'header' and l.startswith('Revision-number: '):
- state = 'prefix'
- if state == 'prefix' and l == 'Revision-number: %s\n' % lower:
- state = 'selection'
- if not upper == 'HEAD' and state == 'selection' and l == 'Revision-number: %s\n' % upper:
- break;
+ if url.startswith('sim://'):
+ filename = url[6:]
+ if filename[-1] == '/':
+ filename = filename[:-1] # remove terminating slash
+ else:
+ raise ValueError('sim:// url required')
+ f = open(filename, 'r')
+ state = 'header'
+ wroterev = False
+ while(True):
+ l = f.readline()
+ if l == '':
+ break
+ if state == 'header' and l.startswith('Revision-number: '):
+ state = 'prefix'
+ if state == 'prefix' and l == 'Revision-number: %s\n' % lower:
+ state = 'selection'
+ if not upper == 'HEAD' and state == 'selection' and \
+ l == 'Revision-number: %s\n' % upper:
+ break
- if state == 'header' or state == 'selection':
- if state == 'selection': wroterev = True
- sys.stdout.write(l)
- return wroterev
+ if state == 'header' or state == 'selection':
+ if state == 'selection':
+ wroterev = True
+ sys.stdout.write(l)
+ return wroterev
if __name__ == "__main__":
- if not (len(sys.argv) in (3, 4, 5)):
- print("usage: %s dump URL -rLOWER:UPPER")
- sys.exit(1)
- if not sys.argv[1] == 'dump': raise NotImplementedError('only "dump" is suppported.')
- url = sys.argv[2]
- r = ('0', 'HEAD')
- if len(sys.argv) == 4 and sys.argv[3][0:2] == '-r':
- r = sys.argv[3][2:].lstrip().split(':')
- if not getrevlimit() is None: r[1] = getrevlimit()
- if writedump(url, r[0], r[1]): ret = 0
- else: ret = 1
- sys.exit(ret)
+ if not (len(sys.argv) in (3, 4, 5)):
+ print("usage: %s dump URL -rLOWER:UPPER")
+ sys.exit(1)
+ if not sys.argv[1] == 'dump':
+ raise NotImplementedError('only "dump" is suppported.')
+ url = sys.argv[2]
+ r = ('0', 'HEAD')
+ if len(sys.argv) == 4 and sys.argv[3][0:2] == '-r':
+ r = sys.argv[3][2:].lstrip().split(':')
+ if not getrevlimit() is None:
+ r[1] = getrevlimit()
+ if writedump(url, r[0], r[1]):
+ ret = 0
+ else:
+ ret = 1
+ sys.exit(ret)
diff --git a/contrib/thunderbird-patch-inline/appp.sh b/contrib/thunderbird-patch-inline/appp.sh
index 5eb4a51643..8dc73ece15 100755
--- a/contrib/thunderbird-patch-inline/appp.sh
+++ b/contrib/thunderbird-patch-inline/appp.sh
@@ -10,7 +10,7 @@ CONFFILE=~/.appprc
SEP="-=-=-=-=-=-=-=-=-=# Don't remove this line #=-=-=-=-=-=-=-=-=-"
if [ -e "$CONFFILE" ] ; then
- LAST_DIR=`grep -m 1 "^LAST_DIR=" "${CONFFILE}"|sed -e 's/^LAST_DIR=//'`
+ LAST_DIR=$(grep -m 1 "^LAST_DIR=" "${CONFFILE}"|sed -e 's/^LAST_DIR=//')
cd "${LAST_DIR}"
else
cd > /dev/null
@@ -25,11 +25,11 @@ fi
cd - > /dev/null
-SUBJECT=`sed -n -e '/^Subject: /p' "${PATCH}"`
-HEADERS=`sed -e '/^'"${SEP}"'$/,$d' $1`
-BODY=`sed -e "1,/${SEP}/d" $1`
-CMT_MSG=`sed -e '1,/^$/d' -e '/^---$/,$d' "${PATCH}"`
-DIFF=`sed -e '1,/^---$/d' "${PATCH}"`
+SUBJECT=$(sed -n -e '/^Subject: /p' "${PATCH}")
+HEADERS=$(sed -e '/^'"${SEP}"'$/,$d' $1)
+BODY=$(sed -e "1,/${SEP}/d" $1)
+CMT_MSG=$(sed -e '1,/^$/d' -e '/^---$/,$d' "${PATCH}")
+DIFF=$(sed -e '1,/^---$/d' "${PATCH}")
CCS=`echo -e "$CMT_MSG\n$HEADERS" | sed -n -e 's/^Cc: \(.*\)$/\1,/gp' \
-e 's/^Signed-off-by: \(.*\)/\1,/gp'`
@@ -48,7 +48,7 @@ if [ "x${BODY}x" != "xx" ] ; then
fi
echo "$DIFF" >> $1
-LAST_DIR=`dirname "${PATCH}"`
+LAST_DIR=$(dirname "${PATCH}")
grep -v "^LAST_DIR=" "${CONFFILE}" > "${CONFFILE}_"
echo "LAST_DIR=${LAST_DIR}" >> "${CONFFILE}_"
diff --git a/contrib/vim/README b/contrib/vim/README
deleted file mode 100644
index 8f16d06972..0000000000
--- a/contrib/vim/README
+++ /dev/null
@@ -1,22 +0,0 @@
-Syntax highlighting for git commit messages, config files, etc. is
-included with the vim distribution as of vim 7.2, and should work
-automatically.
-
-If you have an older version of vim, you can get the latest syntax
-files from the vim project:
-
- http://ftp.vim.org/pub/vim/runtime/syntax/git.vim
- http://ftp.vim.org/pub/vim/runtime/syntax/gitcommit.vim
- http://ftp.vim.org/pub/vim/runtime/syntax/gitconfig.vim
- http://ftp.vim.org/pub/vim/runtime/syntax/gitrebase.vim
- http://ftp.vim.org/pub/vim/runtime/syntax/gitsendemail.vim
-
-These files are also available via FTP at the same location.
-
-To install:
-
- 1. Copy these files to vim's syntax directory $HOME/.vim/syntax
- 2. To auto-detect the editing of various git-related filetypes:
-
- $ curl http://ftp.vim.org/pub/vim/runtime/filetype.vim |
- sed -ne '/^" Git$/, /^$/ p' >>$HOME/.vim/filetype.vim
diff --git a/contrib/workdir/git-new-workdir b/contrib/workdir/git-new-workdir
index 75e8b25817..888c34a521 100755
--- a/contrib/workdir/git-new-workdir
+++ b/contrib/workdir/git-new-workdir
@@ -10,6 +10,10 @@ die () {
exit 128
}
+failed () {
+ die "unable to create new workdir '$new_workdir'!"
+}
+
if test $# -lt 2 || test $# -gt 3
then
usage "$0 <repository> <new_workdir> [<branch>]"
@@ -35,7 +39,7 @@ esac
# don't link to a configured bare repository
isbare=$(git --git-dir="$git_dir" config --bool --get core.bare)
-if test ztrue = z$isbare
+if test ztrue = "z$isbare"
then
die "\"$git_dir\" has core.bare set to true," \
" remove from \"$git_dir/config\" to use $0"
@@ -48,35 +52,54 @@ then
"a complete repository."
fi
-# don't recreate a workdir over an existing repository
-if test -e "$new_workdir"
+# make sure the links in the workdir have full paths to the original repo
+git_dir=$(cd "$git_dir" && pwd) || exit 1
+
+# don't recreate a workdir over an existing directory, unless it's empty
+if test -d "$new_workdir"
then
- die "destination directory '$new_workdir' already exists."
+ if test $(ls -a1 "$new_workdir/." | wc -l) -ne 2
+ then
+ die "destination directory '$new_workdir' is not empty."
+ fi
+ cleandir="$new_workdir/.git"
+else
+ cleandir="$new_workdir"
fi
-# make sure the links use full paths
-git_dir=$(cd "$git_dir"; pwd)
+mkdir -p "$new_workdir/.git" || failed
+cleandir=$(cd "$cleandir" && pwd) || failed
-# create the workdir
-mkdir -p "$new_workdir/.git" || die "unable to create \"$new_workdir\"!"
+cleanup () {
+ rm -rf "$cleandir"
+}
+siglist="0 1 2 15"
+trap cleanup $siglist
# create the links to the original repo. explicitly exclude index, HEAD and
# logs/HEAD from the list since they are purely related to the current working
# directory, and should not be shared.
for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache svn
do
+ # create a containing directory if needed
case $x in
*/*)
- mkdir -p "$(dirname "$new_workdir/.git/$x")"
+ mkdir -p "$new_workdir/.git/${x%/*}"
;;
esac
- ln -s "$git_dir/$x" "$new_workdir/.git/$x"
+
+ ln -s "$git_dir/$x" "$new_workdir/.git/$x" || failed
done
-# now setup the workdir
-cd "$new_workdir"
+# commands below this are run in the context of the new workdir
+cd "$new_workdir" || failed
+
# copy the HEAD from the original repository as a default branch
-cp "$git_dir/HEAD" .git/HEAD
-# checkout the branch (either the same as HEAD from the original repository, or
-# the one that was asked for)
+cp "$git_dir/HEAD" .git/HEAD || failed
+
+# the workdir is set up. if the checkout fails, the user can fix it.
+trap - $siglist
+
+# checkout the branch (either the same as HEAD from the original repository,
+# or the one that was asked for)
git checkout -f $branch