diff options
Diffstat (limited to 'contrib')
22 files changed, 1561 insertions, 7264 deletions
diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index 675e2ae6fb..3dc7ffcd98 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -71,6 +71,10 @@ if(USE_VCPKG) # In the vcpkg edition, we need this to be able to link to libcurl set(CURL_NO_CURL_CMAKE ON) + + # Copy the necessary vcpkg DLLs (like iconv) to the install dir + set(X_VCPKG_APPLOCAL_DEPS_INSTALL ON) + set(CMAKE_TOOLCHAIN_FILE ${VCPKG_DIR}/scripts/buildsystems/vcpkg.cmake CACHE STRING "Vcpkg toolchain file") endif() find_program(SH_EXE sh PATHS "C:/Program Files/Git/bin") @@ -262,7 +266,20 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows") elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_compile_definitions(PROCFS_EXECUTABLE_PATH="/proc/self/exe" HAVE_DEV_TTY ) - list(APPEND compat_SOURCES unix-socket.c) + list(APPEND compat_SOURCES unix-socket.c unix-stream-server.c) +endif() + +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + list(APPEND compat_SOURCES compat/simple-ipc/ipc-shared.c compat/simple-ipc/ipc-win32.c) + add_compile_definitions(SUPPORTS_SIMPLE_IPC) + set(SUPPORTS_SIMPLE_IPC 1) +else() + # Simple IPC requires both Unix sockets and pthreads on Unix-based systems. + if(NOT NO_UNIX_SOCKETS AND NOT NO_PTHREADS) + list(APPEND compat_SOURCES compat/simple-ipc/ipc-shared.c compat/simple-ipc/ipc-unix-socket.c) + add_compile_definitions(SUPPORTS_SIMPLE_IPC) + set(SUPPORTS_SIMPLE_IPC 1) + endif() endif() set(EXE_EXTENSION ${CMAKE_EXECUTABLE_SUFFIX}) @@ -704,13 +721,17 @@ endif() parse_makefile_for_executables(git_builtin_extra "BUILT_INS") +option(SKIP_DASHED_BUILT_INS "Skip hardlinking the dashed versions of the built-ins") + #Creating hardlinks +if(NOT SKIP_DASHED_BUILT_INS) foreach(s ${git_SOURCES} ${git_builtin_extra}) string(REPLACE "${CMAKE_SOURCE_DIR}/builtin/" "" s ${s}) string(REPLACE ".c" "" s ${s}) file(APPEND ${CMAKE_BINARY_DIR}/CreateLinks.cmake "file(CREATE_LINK git${EXE_EXTENSION} git-${s}${EXE_EXTENSION})\n") list(APPEND git_links ${CMAKE_BINARY_DIR}/git-${s}${EXE_EXTENSION}) endforeach() +endif() if(CURL_FOUND) set(remote_exes @@ -826,15 +847,19 @@ list(TRANSFORM git_shell_scripts PREPEND "${CMAKE_BINARY_DIR}/") list(TRANSFORM git_perl_scripts PREPEND "${CMAKE_BINARY_DIR}/") #install -install(TARGETS git git-shell +foreach(program ${PROGRAMS_BUILT}) +if(program STREQUAL "git" OR program STREQUAL "git-shell") +install(TARGETS ${program} RUNTIME DESTINATION bin) +else() +install(TARGETS ${program} + RUNTIME DESTINATION libexec/git-core) +endif() +endforeach() + install(PROGRAMS ${CMAKE_BINARY_DIR}/git-cvsserver DESTINATION bin) -list(REMOVE_ITEM PROGRAMS_BUILT git git-shell) -install(TARGETS ${PROGRAMS_BUILT} - RUNTIME DESTINATION libexec/git-core) - set(bin_links git-receive-pack git-upload-archive git-upload-pack) @@ -847,12 +872,12 @@ install(CODE "file(CREATE_LINK ${CMAKE_INSTALL_PREFIX}/bin/git-shell${EXE_EXTENS foreach(b ${git_links}) string(REPLACE "${CMAKE_BINARY_DIR}" "" b ${b}) - install(CODE "file(CREATE_LINK ${CMAKE_INSTALL_PREFIX}/bin/git${EXE_EXTENSION} ${CMAKE_INSTALL_PREFIX}/libexec/git-core/${b}${EXE_EXTENSION})") + install(CODE "file(CREATE_LINK ${CMAKE_INSTALL_PREFIX}/bin/git${EXE_EXTENSION} ${CMAKE_INSTALL_PREFIX}/libexec/git-core/${b})") endforeach() foreach(b ${git_http_links}) string(REPLACE "${CMAKE_BINARY_DIR}" "" b ${b}) - install(CODE "file(CREATE_LINK ${CMAKE_INSTALL_PREFIX}/libexec/git-core/git-remote-http${EXE_EXTENSION} ${CMAKE_INSTALL_PREFIX}/libexec/git-core/${b}${EXE_EXTENSION})") + install(CODE "file(CREATE_LINK ${CMAKE_INSTALL_PREFIX}/libexec/git-core/git-remote-http${EXE_EXTENSION} ${CMAKE_INSTALL_PREFIX}/libexec/git-core/${b})") endforeach() install(PROGRAMS ${git_shell_scripts} ${git_perl_scripts} ${CMAKE_BINARY_DIR}/git-p4 @@ -975,6 +1000,7 @@ file(APPEND ${CMAKE_BINARY_DIR}/GIT-BUILD-OPTIONS "X='${EXE_EXTENSION}'\n") file(APPEND ${CMAKE_BINARY_DIR}/GIT-BUILD-OPTIONS "NO_GETTEXT='${NO_GETTEXT}'\n") file(APPEND ${CMAKE_BINARY_DIR}/GIT-BUILD-OPTIONS "RUNTIME_PREFIX='${RUNTIME_PREFIX}'\n") file(APPEND ${CMAKE_BINARY_DIR}/GIT-BUILD-OPTIONS "NO_PYTHON='${NO_PYTHON}'\n") +file(APPEND ${CMAKE_BINARY_DIR}/GIT-BUILD-OPTIONS "SUPPORTS_SIMPLE_IPC='${SUPPORTS_SIMPLE_IPC}'\n") if(USE_VCPKG) file(APPEND ${CMAKE_BINARY_DIR}/GIT-BUILD-OPTIONS "PATH=\"$PATH:$TEST_DIRECTORY/../compat/vcbuild/vcpkg/installed/x64-windows/bin\"\n") endif() diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 7dc6cd8eb8..4bdd27ddc8 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -77,7 +77,7 @@ __git_find_repo_path () test -d "$__git_dir" && __git_repo_path="$__git_dir" elif [ -n "${GIT_DIR-}" ]; then - test -d "${GIT_DIR-}" && + test -d "$GIT_DIR" && __git_repo_path="$GIT_DIR" elif [ -d .git ]; then __git_repo_path=.git @@ -427,7 +427,7 @@ __gitcomp_builtin () if [ -z "$options" ]; then local completion_helper - if [ "$GIT_COMPLETION_SHOW_ALL" = "1" ]; then + if [ "${GIT_COMPLETION_SHOW_ALL-}" = "1" ]; then completion_helper="--git-completion-helper-all" else completion_helper="--git-completion-helper" @@ -744,7 +744,7 @@ __git_refs () track="" ;; *) - for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD REBASE_HEAD; do + for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD REBASE_HEAD CHERRY_PICK_HEAD; do case "$i" in $match*) if [ -e "$dir/$i" ]; then @@ -1006,8 +1006,8 @@ __git_complete_revlist () __git_complete_remote_or_refspec () { - local cur_="$cur" cmd="${words[1]}" - local i c=2 remote="" pfx="" lhs=1 no_complete_refspec=0 + local cur_="$cur" cmd="${words[__git_cmd_idx]}" + local i c=$((__git_cmd_idx+1)) remote="" pfx="" lhs=1 no_complete_refspec=0 if [ "$cmd" = "remote" ]; then ((c++)) fi @@ -1129,7 +1129,7 @@ __git_pretty_aliases () # __git_aliased_command requires 1 argument __git_aliased_command () { - local cur=$1 last list word cmdline + local cur=$1 last list= word cmdline while [[ -n "$cur" ]]; do if [[ "$list" == *" $cur "* ]]; then @@ -1176,7 +1176,7 @@ __git_aliased_command () # --show-idx: Optionally show the index of the found word in the $words array. __git_find_on_cmdline () { - local word c=1 show_idx + local word c="$__git_cmd_idx" show_idx while test $# -gt 1; do case "$1" in @@ -1221,7 +1221,7 @@ __git_find_last_on_cmdline () done local wordlist="$1" - while [ $c -gt 1 ]; do + while [ $c -gt "$__git_cmd_idx" ]; do ((c--)) for word in $wordlist; do if [ "$word" = "${words[c]}" ]; then @@ -1306,7 +1306,7 @@ __git_count_arguments () local word i c=0 # Skip "git" (first argument) - for ((i=1; i < ${#words[@]}; i++)); do + for ((i=$__git_cmd_idx; i < ${#words[@]}; i++)); do word="${words[i]}" case "$word" in @@ -1333,6 +1333,7 @@ __git_whitespacelist="nowarn warn error error-all fix" __git_patchformat="mbox stgit stgit-series hg mboxrd" __git_showcurrentpatch="diff raw" __git_am_inprogress_options="--skip --continue --resolved --abort --quit --show-current-patch" +__git_quoted_cr="nowarn warn strip" _git_am () { @@ -1354,6 +1355,10 @@ _git_am () __gitcomp "$__git_showcurrentpatch" "" "${cur##--show-current-patch=}" return ;; + --quoted-cr=*) + __gitcomp "$__git_quoted_cr" "" "${cur##--quoted-cr=}" + return + ;; --*) __gitcomp_builtin am "" \ "$__git_am_inprogress_options" @@ -1442,7 +1447,7 @@ __git_ref_fieldlist="refname objecttype objectsize objectname upstream push HEAD _git_branch () { - local i c=1 only_local_ref="n" has_r="n" + local i c="$__git_cmd_idx" only_local_ref="n" has_r="n" while [ $c -lt $cword ]; do i="${words[c]}" @@ -1474,12 +1479,12 @@ _git_branch () _git_bundle () { - local cmd="${words[2]}" + local cmd="${words[__git_cmd_idx+1]}" case "$cword" in - 2) + $((__git_cmd_idx+1))) __gitcomp "create list-heads verify unbundle" ;; - 3) + $((__git_cmd_idx+2))) # looking for a file ;; *) @@ -1724,6 +1729,7 @@ __git_diff_common_options="--stat --numstat --shortstat --summary --indent-heuristic --no-indent-heuristic --textconv --no-textconv --patch --no-patch + --anchored= " __git_diff_difftool_options="--cached --staged --pickaxe-all --pickaxe-regex @@ -1894,7 +1900,7 @@ _git_grep () esac case "$cword,$prev" in - 2,*|*,-*) + $((__git_cmd_idx+1)),*|*,-*) __git_complete_symbol && return ;; esac @@ -1910,7 +1916,7 @@ _git_help () return ;; esac - if test -n "$GIT_TESTING_ALL_COMMAND_LIST" + if test -n "${GIT_TESTING_ALL_COMMAND_LIST-}" then __gitcomp "$GIT_TESTING_ALL_COMMAND_LIST $(__git --list-cmds=alias,list-guide) gitk" else @@ -2474,7 +2480,7 @@ _git_switch () __git_config_get_set_variables () { local prevword word config_file= c=$cword - while [ $c -gt 1 ]; do + while [ $c -gt "$__git_cmd_idx" ]; do word="${words[c]}" case "$word" in --system|--global|--local|--file=*) @@ -3013,66 +3019,48 @@ _git_sparse_checkout () _git_stash () { - local save_opts='--all --keep-index --no-keep-index --quiet --patch --include-untracked' local subcommands='push list show apply clear drop pop create branch' local subcommand="$(__git_find_on_cmdline "$subcommands save")" - if [ -z "$subcommand" -a -n "$(__git_find_on_cmdline "-p")" ]; then - subcommand="push" - fi + if [ -z "$subcommand" ]; then - case "$cur" in - --*) - __gitcomp "$save_opts" + case "$((cword - __git_cmd_idx)),$cur" in + *,--*) + __gitcomp_builtin stash_push ;; - sa*) - if [ -z "$(__git_find_on_cmdline "$save_opts")" ]; then - __gitcomp "save" - fi + 1,sa*) + __gitcomp "save" ;; - *) - if [ -z "$(__git_find_on_cmdline "$save_opts")" ]; then - __gitcomp "$subcommands" - fi + 1,*) + __gitcomp "$subcommands" ;; esac - else - case "$subcommand,$cur" in - push,--*) - __gitcomp "$save_opts --message" - ;; - save,--*) - __gitcomp "$save_opts" - ;; - apply,--*|pop,--*) - __gitcomp "--index --quiet" - ;; - drop,--*) - __gitcomp "--quiet" - ;; - list,--*) - __gitcomp "--name-status --oneline --patch-with-stat" - ;; - show,--*) - __gitcomp "$__git_diff_common_options" - ;; - branch,--*) - ;; - branch,*) - if [ $cword -eq 3 ]; then - __git_complete_refs - else - __gitcomp_nl "$(__git stash list \ - | sed -n -e 's/:.*//p')" - fi - ;; - show,*|apply,*|drop,*|pop,*) + return + fi + + case "$subcommand,$cur" in + list,--*) + # NEEDSWORK: can we somehow unify this with the options in _git_log() and _git_show() + __gitcomp_builtin stash_list "$__git_log_common_options $__git_diff_common_options" + ;; + show,--*) + __gitcomp_builtin stash_show "$__git_diff_common_options" + ;; + *,--*) + __gitcomp_builtin "stash_$subcommand" + ;; + branch,*) + if [ $cword -eq $((__git_cmd_idx+2)) ]; then + __git_complete_refs + else __gitcomp_nl "$(__git stash list \ | sed -n -e 's/:.*//p')" - ;; - *) - ;; - esac - fi + fi + ;; + show,*|apply,*|drop,*|pop,*) + __gitcomp_nl "$(__git stash list \ + | sed -n -e 's/:.*//p')" + ;; + esac } _git_submodule () @@ -3225,7 +3213,7 @@ _git_svn () _git_tag () { - local i c=1 f=0 + local i c="$__git_cmd_idx" f=0 while [ $c -lt $cword ]; do i="${words[c]}" case "$i" in @@ -3268,9 +3256,10 @@ _git_whatchanged () __git_complete_worktree_paths () { local IFS=$'\n' + # Generate completion reply from worktree list skipping the first + # entry: it's the path of the main worktree, which can't be moved, + # removed, locked, etc. __gitcomp_nl "$(git worktree list --porcelain | - # Skip the first entry: it's the path of the main worktree, - # which can't be moved, removed, locked, etc. sed -n -e '2,$ s/^worktree //p')" } @@ -3398,21 +3387,40 @@ __git_main () { local i c=1 command __git_dir __git_repo_path local __git_C_args C_args_count=0 + local __git_cmd_idx while [ $c -lt $cword ]; do i="${words[c]}" case "$i" in - --git-dir=*) __git_dir="${i#--git-dir=}" ;; - --git-dir) ((c++)) ; __git_dir="${words[c]}" ;; - --bare) __git_dir="." ;; - --help) command="help"; break ;; - -c|--work-tree|--namespace) ((c++)) ;; - -C) __git_C_args[C_args_count++]=-C + --git-dir=*) + __git_dir="${i#--git-dir=}" + ;; + --git-dir) + ((c++)) + __git_dir="${words[c]}" + ;; + --bare) + __git_dir="." + ;; + --help) + command="help" + break + ;; + -c|--work-tree|--namespace) + ((c++)) + ;; + -C) + __git_C_args[C_args_count++]=-C ((c++)) __git_C_args[C_args_count++]="${words[c]}" ;; - -*) ;; - *) command="$i"; break ;; + -*) + ;; + *) + command="$i" + __git_cmd_idx="$c" + break + ;; esac ((c++)) done @@ -3434,7 +3442,8 @@ __git_main () ;; esac case "$cur" in - --*) __gitcomp " + --*) + __gitcomp " --paginate --no-pager --git-dir= @@ -3504,6 +3513,7 @@ fi __git_func_wrap () { local cur words cword prev + local __git_cmd_idx=0 _get_comp_words_by_ref -n =: cur words cword prev $1 } diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh index 6c56296997..cac6f61881 100644 --- a/contrib/completion/git-completion.zsh +++ b/contrib/completion/git-completion.zsh @@ -251,7 +251,7 @@ __git_zsh_main () done ;; (arg) - local command="${words[1]}" __git_dir + local command="${words[1]}" __git_dir __git_cmd_idx=1 if (( $+opt_args[--bare] )); then __git_dir='.' diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh index 4640a1535d..db7c0068fb 100644 --- a/contrib/completion/git-prompt.sh +++ b/contrib/completion/git-prompt.sh @@ -433,8 +433,8 @@ __git_ps1 () fi local sparse="" - if [ -z "${GIT_PS1_COMPRESSSPARSESTATE}" ] && - [ -z "${GIT_PS1_OMITSPARSESTATE}" ] && + if [ -z "${GIT_PS1_COMPRESSSPARSESTATE-}" ] && + [ -z "${GIT_PS1_OMITSPARSESTATE-}" ] && [ "$(git config --bool core.sparseCheckout)" = "true" ]; then sparse="|SPARSE" fi @@ -543,7 +543,7 @@ __git_ps1 () u="%${ZSH_VERSION+%}" fi - if [ -n "${GIT_PS1_COMPRESSSPARSESTATE}" ] && + if [ -n "${GIT_PS1_COMPRESSSPARSESTATE-}" ] && [ "$(git config --bool core.sparseCheckout)" = "true" ]; then h="?" fi diff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES deleted file mode 100644 index 35791fd02c..0000000000 --- a/contrib/hooks/multimail/CHANGES +++ /dev/null @@ -1,285 +0,0 @@ -Release 1.5.0 -============= - -Backward-incompatible change ----------------------------- - -The name of classes for environment was misnamed as `*Environement`. -It is now `*Environment`. - -New features ------------- - -* A Thread-Index header is now added to each email sent (except for - combined emails where it would not make sense), so that MS Outlook - properly groups messages by threads even though they have a - different subject line. Unfortunately, even adding this header the - threading still seems to be unreliable, but it is unclear whether - this is an issue on our side or on MS Outlook's side (see discussion - here: https://github.com/git-multimail/git-multimail/pull/194). - -* A new variable multimailhook.ExcludeMergeRevisions was added to send - notification emails only for non-merge commits. - -* For gitolite environment, it is now possible to specify the mail map - in a separate file in addition to gitolite.conf, using the variable - multimailhook.MailaddressMap. - -Internal changes ----------------- - -* The testsuite now uses GIT_PRINT_SHA1_ELLIPSIS where needed for - compatibility with recent Git versions. Only tests are affected. - -* We don't try to install pyflakes in the continuous integration job - for old Python versions where it's no longer available. - -* Stop using the deprecated cgi.escape in Python 3. - -* New flake8 warnings have been fixed. - -* Python 3.6 is now tested against on Travis-CI. - -* A bunch of lgtm.com warnings have been fixed. - -Bug fixes ---------- - -* SMTPMailer logs in only once now. It used to re-login for each email - sent which triggered errors for some SMTP servers. - -* migrate-mailhook-config was broken by internal refactoring, it - should now work again. - -This version was tested with Python 2.6 to 3.7. It was tested with Git -1.7.10.406.gdc801, 2.15.1 and 2.20.1.98.gecbdaf0. - -Release 1.4.0 -============= - -New features to troubleshoot a git-multimail installation ---------------------------------------------------------- - -* One can now perform a basic check of git-multimail's setup by - running the hook with the environment variable - GIT_MULTIMAIL_CHECK_SETUP set to a non-empty string. See - doc/troubleshooting.rst for details. - -* A new log files system was added. See the multimailhook.logFile, - multimailhook.errorLogFile and multimailhook.debugLogFile variables. - -* git_multimail.py can now be made more verbose using - multimailhook.verbose. - -* A new option --check-ref-filter is now available to help debugging - the refFilter* options. - -Formatting emails ------------------ - -* Formatting of emails was made slightly more compact, to reduce the - odds of having long subject lines truncated or wrapped in short list - of commits. - -* multimailhook.emailPrefix may now use the '%(repo_shortname)s' - placeholder for the repository's short name. - -* A new option multimailhook.subjectMaxLength is available to truncate - overly long subject lines. - -Bug fixes and minor changes ---------------------------- - -* Options refFilterDoSendRegex and refFilterDontSendRegex were - essentially broken. They should work now. - -* The behavior when both refFilter{Do,Dont}SendRegex and - refFilter{Exclusion,Inclusion}Regex are set have been slightly - changed. Exclusion/Inclusion is now strictly stronger than - DoSend/DontSend. - -* The management of precedence when a setting can be computed in - multiple ways has been considerably refactored and modified. - multimailhook.from and multimailhook.reponame now have precedence - over the environment-specific settings ($GL_REPO/$GL_USER for - gitolite, --stash-user/repo for Stash, --submitter/--project for - Gerrit). - -* The coverage of the testsuite has been considerably improved. All - configuration variables now appear at least once in the testsuite. - -This version was tested with Python 2.6 to 3.5. It also mostly works -with Python 2.4, but there is one known breakage in the testsuite -related to non-ascii characters. It was tested with Git -1.7.10.406.gdc801, 1.8.5.6, 2.1.4, and 2.10.0.rc0.1.g07c9292. - -Release 1.3.1 (bugfix-only release) -=================================== - -* Generate links to commits in combined emails (it was done only for - commit emails in 1.3.0). - -* Fix broken links on PyPi. - -Release 1.3.0 -============= - -* New options multimailhook.htmlInIntro and multimailhook.htmlInFooter - now allow using HTML in the introduction and footer of emails (e.g. - for a more pleasant formatting or to insert a link to the commit on - a web interface). - -* A new option multimailhook.commitBrowseURL gives a simpler (and less - flexible) way to add a link to a web interface for commit emails - than multimailhook.htmlInIntro and multimailhook.htmlInFooter. - -* A new public function config.add_config_parameters was added to - allow custom hooks to set specific Git configuration variables - without modifying the configuration files. See an example in - post-receive.example. - -* Error handling for SMTP has been improved (we used to print Python - backtraces for legitimate errors). - -* The SMTP mailer can now check TLS certificates when the newly added - configuration variable multimailhook.smtpCACerts. - -* Python 3 portability has been improved. - -* The documentation's formatting has been improved. - -* The testsuite has been improved (we now use pyflakes to check for - errors in the code). - -This version has been tested with Python 2.4 and 2.6 to 3.5, and Git -v1.7.10-406-gdc801e7, 2.1.4 and 2.8.1.339.g3ad15fd. - -No change since 1.3 RC1. - -Release 1.2.0 -============= - -* It is now possible to exclude some refs (e.g. exclude some branches - or tags). See refFilterDoSendRegex, refFilterDontSendRegex, - refFilterInclusionRegex and refFilterExclusionRegex. - -* New commitEmailFormat option which can be set to "html" to generate - simple colorized diffs using HTML for the commit emails. - -* git-multimail can now be ran as a Gerrit ref-updated hook, or from - Atlassian BitBucket Server (formerly known as Atlassian Stash). - -* The From: field is now more customizeable. It can be set - independently for refchange emails and commit emails (see - fromCommit, fromRefChange). The special values pusher and author can - be used in these configuration variable. - -* A new command-line option, --version, was added. The version is also - available in the X-Git-Multimail-Version header of sent emails. - -* Set X-Git-NotificationType header to differentiate the various types - of notifications. Current values are: diff, ref_changed_plus_diff, - ref_changed. - -* Preliminary support for Python 3. The testsuite passes with Python 3, - but it has not received as much testing as the Python 2 version yet. - -* Several encoding-related fixes. UTF-8 characters work in more - situations (but non-ascii characters in email address are still not - supported). - -* The testsuite and its documentation has been greatly improved. - -Plus all the bugfixes from version 1.1.1. - -This version has been tested with Python 2.4 and 2.6 to 3.5, and Git -v1.7.10-406-gdc801e7, git-1.8.2.3 and 2.6.0. Git versions prior to -v1.7.10-406-gdc801e7 probably work, but cannot run the testsuite -properly. - -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 -============= - -* Fix encoding of non-ASCII email addresses in email headers. - -* Fix backwards-compatibility bugs for older Python 2.x versions. - -* Fix a backwards-compatibility bug for Git 1.7.1. - -* Add an option commitDiffOpts to customize logs for revisions. - -* Pass "-oi" to sendmail by default to prevent premature termination - on a line containing only ".". - -* Stagger email "Date:" values in an attempt to help mail clients - thread the emails in the right order. - -* If a mailing list setting is missing, just skip sending the - corresponding email (with a warning) instead of failing. - -* Add a X-Git-Host header that can be used for email filtering. - -* Allow the sender's fully-qualified domain name to be configured. - -* Minor documentation improvements. - -* Add this CHANGES file. - - -Release 0.9.0 -============= - -* Initial release. diff --git a/contrib/hooks/multimail/CONTRIBUTING.rst b/contrib/hooks/multimail/CONTRIBUTING.rst deleted file mode 100644 index de20a54287..0000000000 --- a/contrib/hooks/multimail/CONTRIBUTING.rst +++ /dev/null @@ -1,60 +0,0 @@ -Contributing -============ - -git-multimail is an open-source project, built by volunteers. We would -welcome your help! - -The current maintainers are `Matthieu Moy <http://matthieu-moy.fr>`__ and -`Michael Haggerty <https://github.com/mhagger>`__. - -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`_. - -Whenever enough changes to git-multimail have accumulated, a new -code-drop of git-multimail will be submitted for inclusion in the Git -project. - -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/git/git/blob/master/Documentation/SubmittingPatches#L234>`__. - -Please vote for issues you would like to be addressed in priority -(click "add your reaction" and then the "+1" thumbs-up button on the -GitHub issue). - -General discussion of git-multimail can take place on the main `Git -mailing list`_. - -Please CC emails regarding git-multimail to the maintainers so that we -don't overlook them. - -Help needed: testers/maintainer for specific environments/OS ------------------------------------------------------------- - -The current maintainer uses and tests git-multimail on Linux with the -Generic environment. More testers, or better contributors are needed -to test git-multimail on other real-life setups: - -* Mac OS X, Windows: git-multimail is currently not supported on these - platforms. But since we have no external dependencies and try to - write code as portable as possible, it is possible that - git-multimail already runs there and if not, it is likely that it - could be ported easily. - - Patches to improve support for Windows and OS X are welcome. - Ideally, there would be a sub-maintainer for each OS who would test - at least once before each release (around twice a year). - -* Gerrit, Stash, Gitolite environments: although the testsuite - contains tests for these environments, a tester/maintainer for each - environment would be welcome to test and report failure (or success) - on real-life environments periodically (here also, feedback before - each release would be highly appreciated). - - -.. _`git-multimail repository on GitHub`: https://github.com/git-multimail/git-multimail -.. _`Git mailing list`: git@vger.kernel.org diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git index 044444245d..c427efc7bd 100644 --- a/contrib/hooks/multimail/README.Git +++ b/contrib/hooks/multimail/README.Git @@ -1,15 +1,7 @@ -This copy of git-multimail is distributed as part of the "contrib" -section of the Git project as a convenience to Git users. git-multimail is developed as an independent project at the following website: https://github.com/git-multimail/git-multimail -The version in this directory was obtained from the upstream project -on January 07 2019 and consists of the "git-multimail" subdirectory from -revision - - 04e80e6c40be465cc62b6c246f0fcb8fd2cfd454 refs/tags/1.5.0 - -Please see the README file in this directory for information about how -to report bugs or contribute to git-multimail. +Please refer to that project page for information about how to report +bugs or contribute to git-multimail. diff --git a/contrib/hooks/multimail/README.migrate-from-post-receive-email b/contrib/hooks/multimail/README.migrate-from-post-receive-email deleted file mode 100644 index 1e6a976699..0000000000 --- a/contrib/hooks/multimail/README.migrate-from-post-receive-email +++ /dev/null @@ -1,145 +0,0 @@ -git-multimail is close to, but not exactly, a plug-in replacement for -the old Git project script contrib/hooks/post-receive-email. This -document describes the differences and explains how to configure -git-multimail to get behavior closest to that of post-receive-email. - -If you are in a hurry -===================== - -A script called migrate-mailhook-config is included with -git-multimail. If you run this script within a Git repository that is -configured to use post-receive-email, it will convert the -configuration settings into the approximate equivalent settings for -git-multimail. For more information, run - - migrate-mailhook-config --help - - -Configuration differences -========================= - -* The names of the config options for git-multimail are in namespace - "multimailhook.*" instead of "hooks.*". (Editorial comment: - post-receive-email should never have used such a generic top-level - namespace.) - -* In emails about new annotated tags, post-receive-email includes a - shortlog of all changes since the previous annotated tag. To get - this behavior with git-multimail, you need to set - multimailhook.announceshortlog to true: - - git config multimailhook.announceshortlog true - -* multimailhook.commitlist -- This is a new configuration variable. - Recipients listed here will receive a separate email for each new - commit. However, if this variable is *not* set, it defaults to the - value of multimailhook.mailinglist. Therefore, if you *don't* want - the members of multimailhook.mailinglist to receive one email per - commit, then set this value to the empty string: - - git config multimailhook.commitlist '' - -* multimailhook.emailprefix -- If this value is not set, then the - subjects of generated emails are prefixed with the short name of the - repository enclosed in square brackets; e.g., "[myrepo]". - post-receive-email defaults to prefix "[SCM]" if this option is not - set. So if you were using the old default and want to retain it - (for example, to avoid having to change your email filters), set - this variable explicitly to the old value: - - git config multimailhook.emailprefix "[SCM]" - -* The "multimailhook.showrev" configuration option is not supported. - Its main use is obsoleted by the one-email-per-commit feature of - git-multimail. - - -Other differences -================= - -This section describes other differences in the behavior of -git-multimail vs. post-receive-email. For full details, please refer -to the main README file: - -* One email per commit. For each reference change, the script first - outputs one email summarizing the reference change (including - one-line summaries of the new commits), then it outputs a separate - email for each new commit that was introduced, including patches. - These one-email-per-commit emails go to the addresses listed in - multimailhook.commitlist. post-receive-email sends only one email - for each *reference* that is changed, no matter how many commits - were added to the reference. - -* Better algorithm for detecting new commits. post-receive-email - processes one reference change at a time, which causes it to fail to - describe new commits that were included in multiple branches. For - example, if a single push adds the "*" commits in the diagram below, - then post-receive-email would never include the details of the two - commits that are common to "master" and "branch" in its - notifications. - - o---o---o---*---*---* <-- master - \ - *---* <-- branch - - git-multimail analyzes all reference modifications to determine - which commits were not present before the change, therefore avoiding - that error. - -* In reference change emails, git-multimail tells which commits have - been added to the reference vs. are entirely new to the repository, - and which commits that have been omitted from the reference - vs. entirely discarded from the repository. - -* The environment in which Git is running can be configured via an - "Environment" abstraction. - -* Built-in support for Gitolite-managed repositories. - -* Instead of using full SHA1 object names in emails, git-multimail - mostly uses abbreviated SHA1s, plus one-line log message summaries - where appropriate. - -* In the schematic diagrams that explain non-fast-forward commits, - git-multimail shows the names of the branches involved. - -* The emails generated by git-multimail include the name of the Git - repository that was modified; this is convenient for recipients who - are monitoring multiple repositories. - -* git-multimail allows the email "From" addresses to be configured. - -* The recipients lists (multimailhook.mailinglist, - multimailhook.refchangelist, multimailhook.announcelist, and - multimailhook.commitlist) can be comma-separated values and/or - multivalued settings in the config file; e.g., - - [multimailhook] - mailinglist = mr.brown@example.com, mr.black@example.com - announcelist = Him <him@example.com> - announcelist = Jim <jim@example.com> - announcelist = pop@example.com - - This might make it easier to maintain short recipients lists without - requiring full-fledged mailing list software. - -* By default, git-multimail sets email "Reply-To" headers to reply to - the pusher (for reference updates) and to the author (for commit - notifications). By default, the pusher's email address is - constructed by appending "multimailhook.emaildomain" to the pusher's - username. - -* The generated emails contain a configurable footer. By default, it - lists the name of the administrator who should be contacted to - unsubscribe from notification emails. - -* New option multimailhook.emailmaxlinelength to limit the length of - lines in the main part of the email body. The default limit is 500 - characters. - -* New option multimailhook.emailstrictutf8 to ensure that the main - part of the email body is valid UTF-8. Invalid characters are - turned into the Unicode replacement character, U+FFFD. By default - this option is turned on. - -* Written in Python. Easier to add new features. diff --git a/contrib/hooks/multimail/README.rst b/contrib/hooks/multimail/README.rst deleted file mode 100644 index 7c0fc4a6ef..0000000000 --- a/contrib/hooks/multimail/README.rst +++ /dev/null @@ -1,774 +0,0 @@ -git-multimail version 1.5.0 -=========================== - -.. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master - :target: https://travis-ci.org/git-multimail/git-multimail - -git-multimail is a tool for sending notification emails on pushes to a -Git repository. It includes a Python module called ``git_multimail.py``, -which can either be used as a hook script directly or can be imported -as a Python module into another script. - -git-multimail is derived from the Git project's old -contrib/hooks/post-receive-email, and is mostly compatible with that -script. See README.migrate-from-post-receive-email for details about -the differences and for how to migrate from post-receive-email to -git-multimail. - -git-multimail, like the rest of the Git project, is licensed under -GPLv2 (see the COPYING file for details). - -Please note: although, as a convenience, git-multimail may be -distributed along with the main Git project, development of -git-multimail takes place in its own, separate project. Please, read -`<CONTRIBUTING.rst>`__ for more information. - - -By default, for each push received by the repository, git-multimail: - -1. Outputs one email summarizing each reference that was changed. - These "reference change" (called "refchange" below) emails describe - the nature of the change (e.g., was the reference created, deleted, - fast-forwarded, etc.) and include a one-line summary of each commit - that was added to the reference. - -2. Outputs one email for each new commit that was introduced by the - reference change. These "commit" emails include a list of the - files changed by the commit, followed by the diffs of files - modified by the commit. The commit emails are threaded to the - corresponding reference change email via "In-Reply-To". This style - (similar to the "git format-patch" style used on the Git mailing - list) makes it easy to scan through the emails, jump to patches - that need further attention, and write comments about specific - commits. Commits are handled in reverse topological order (i.e., - parents shown before children). For example:: - - [git] branch master updated - + [git] 01/08: doc: fix xref link from api docs to manual pages - + [git] 02/08: api-credentials.txt: show the big picture first - + [git] 03/08: api-credentials.txt: mention credential.helper explicitly - + [git] 04/08: api-credentials.txt: add "see also" section - + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&' - + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix' - + [git] 07/08: Merge branch 'mm/api-credentials-doc' - + [git] 08/08: Git 1.7.11-rc2 - - By default, each commit appears in exactly one commit email, the - first time that it is pushed to the repository. If a commit is later - merged into another branch, then a one-line summary of the commit - is included in the reference change email (as usual), but no - additional commit email is generated. See - `multimailhook.refFilter(Inclusion|Exclusion|DoSend|DontSend)Regex` - below to configure which branches and tags are watched by the hook. - - By default, reference change emails have their "Reply-To" field set - to the person who pushed the change, and commit emails have their - "Reply-To" field set to the author of the commit. - -3. Output one "announce" mail for each new annotated tag, including - information about the tag and optionally a shortlog describing the - changes since the previous tag. Such emails might be useful if you - use annotated tags to mark releases of your project. - - -Requirements ------------- - -* Python 2.x, version 2.4 or later. No non-standard Python modules - are required. git-multimail has preliminary support for Python 3 - (but it has been better tested with Python 2). - -* The ``git`` command must be in your PATH. git-multimail is known to - work with Git versions back to 1.7.1. (Earlier versions have not - been tested; if you do so, please report your results.) - -* To send emails using the default configuration, a standard sendmail - program must be located at '/usr/sbin/sendmail' or - '/usr/lib/sendmail' and must be configured correctly to send emails. - If this is not the case, set multimailhook.sendmailCommand, or see - the multimailhook.mailer configuration variable below for how to - configure git-multimail to send emails via an SMTP server. - -* git-multimail is currently tested only on Linux. It may or may not - work on other platforms such as Windows and Mac OS. See - `<CONTRIBUTING.rst>`__ to improve the situation. - - -Invocation ----------- - -``git_multimail.py`` is designed to be used as a ``post-receive`` hook in a -Git repository (see githooks(5)). Link or copy it to -$GIT_DIR/hooks/post-receive within the repository for which email -notifications are desired. Usually it should be installed on the -central repository for a project, to which all commits are eventually -pushed. - -For use on pre-v1.5.1 Git servers, ``git_multimail.py`` can also work as -an ``update`` hook, taking its arguments on the command line. To use -this script in this manner, link or copy it to $GIT_DIR/hooks/update. -Please note that the script is not completely reliable in this mode -[1]_. - -Alternatively, ``git_multimail.py`` can be imported as a Python module -into your own Python post-receive script. This method is a bit more -work, but allows the behavior of the hook to be customized using -arbitrary Python code. For example, you can use a custom environment -(perhaps inheriting from GenericEnvironment or GitoliteEnvironment) to - -* change how the user who did the push is determined - -* read users' email addresses from an LDAP server or from a database - -* decide which users should be notified about which commits based on - the contents of the commits (e.g., for users who want to be notified - only about changes affecting particular files or subdirectories) - -Or you can change how emails are sent by writing your own Mailer -class. The ``post-receive`` script in this directory demonstrates how -to use ``git_multimail.py`` as a Python module. (If you make interesting -changes of this type, please consider sharing them with the -community.) - - -Troubleshooting/FAQ -------------------- - -Please read `<doc/troubleshooting.rst>`__ for frequently asked -questions and common issues with git-multimail. - - -Configuration -------------- - -By default, git-multimail mostly takes its configuration from the -following ``git config`` settings: - -multimailhook.environment - This describes the general environment of the repository. In most - cases, you do not need to specify a value for this variable: - `git-multimail` will autodetect which environment to use. - Currently supported values: - - generic - the username of the pusher is read from $USER or $USERNAME and - the repository name is derived from the repository's path. - - gitolite - Environment to use when ``git-multimail`` is ran as a gitolite_ - hook. - - The username of the pusher is read from $GL_USER, the repository - name is read from $GL_REPO, and the From: header value is - optionally read from gitolite.conf (see multimailhook.from). - - For more information about gitolite and git-multimail, read - `<doc/gitolite.rst>`__ - - stash - Environment to use when ``git-multimail`` is ran as an Atlassian - BitBucket Server (formerly known as Atlassian Stash) hook. - - **Warning:** this mode was provided by a third-party contributor - and never tested by the git-multimail maintainers. It is - provided as-is and may or may not work for you. - - This value is automatically assumed when the stash-specific - flags (``--stash-user`` and ``--stash-repo``) are specified on - the command line. When this environment is active, the username - and repo come from these two command line flags, which must be - specified. - - gerrit - Environment to use when ``git-multimail`` is ran as a - ``ref-updated`` Gerrit hook. - - This value is used when the gerrit-specific command line flags - (``--oldrev``, ``--newrev``, ``--refname``, ``--project``) for - gerrit's ref-updated hook are present. When this environment is - active, the username of the pusher is taken from the - ``--submitter`` argument if that command line option is passed, - otherwise 'Gerrit' is used. The repository name is taken from - the ``--project`` option on the command line, which must be passed. - - For more information about gerrit and git-multimail, read - `<doc/gerrit.rst>`__ - - If none of these environments is suitable for your setup, then you - can implement a Python class that inherits from Environment and - instantiate it via a script that looks like the example - post-receive script. - - The environment value can be specified on the command line using - the ``--environment`` option. If it is not specified on the - command line or by ``multimailhook.environment``, the value is - guessed as follows: - - * If stash-specific (respectively gerrit-specific) command flags - are present on the command-line, then ``stash`` (respectively - ``gerrit``) is used. - - * If the environment variables $GL_USER and $GL_REPO are set, then - ``gitolite`` is used. - - * If none of the above apply, then ``generic`` is used. - -multimailhook.repoName - A short name of this Git repository, to be used in various places - in the notification email text. The default is to use $GL_REPO - for gitolite repositories, or otherwise to derive this value from - the repository path name. - -multimailhook.mailingList - The list of email addresses to which notification emails should be - sent, as RFC 2822 email addresses separated by commas. This - configuration option can be multivalued. Leave it unset or set it - to the empty string to not send emails by default. The next few - settings can be used to configure specific address lists for - specific types of notification email. - -multimailhook.refchangeList - The list of email addresses to which summary emails about - reference changes should be sent, as RFC 2822 email addresses - separated by commas. This configuration option can be - multivalued. The default is the value in - multimailhook.mailingList. Set this value to "none" (or the empty - string) to prevent reference change emails from being sent even if - multimailhook.mailingList is set. - -multimailhook.announceList - The list of email addresses to which emails about new annotated - tags should be sent, as RFC 2822 email addresses separated by - commas. This configuration option can be multivalued. The - default is the value in multimailhook.refchangeList or - multimailhook.mailingList. Set this value to "none" (or the empty - string) to prevent annotated tag announcement emails from being sent - even if one of the other values is set. - -multimailhook.commitList - The list of email addresses to which emails about individual new - commits should be sent, as RFC 2822 email addresses separated by - commas. This configuration option can be multivalued. The - default is the value in multimailhook.mailingList. Set this value - to "none" (or the empty string) to prevent notification emails about - individual commits from being sent even if - multimailhook.mailingList is set. - -multimailhook.announceShortlog - If this option is set to true, then emails about changes to - annotated tags include a shortlog of changes since the previous - tag. This can be useful if the annotated tags represent releases; - then the shortlog will be a kind of rough summary of what has - happened since the last release. But if your tagging policy is - not so straightforward, then the shortlog might be confusing - rather than useful. Default is false. - -multimailhook.commitEmailFormat - The format of email messages for the individual commits, can be "text" or - "html". In the latter case, the emails will include diffs using colorized - HTML instead of plain text used by default. Note that this currently the - ref change emails are always sent in plain text. - - Note that when using "html", the formatting is done by parsing the - output of ``git log`` with ``-p``. When using - ``multimailhook.commitLogOpts`` to specify a ``--format`` for - ``git log``, one may get false positive (e.g. lines in the body of - the message starting with ``+++`` or ``---`` colored in red or - green). - - By default, all the message is HTML-escaped. See - ``multimailhook.htmlInIntro`` to change this behavior. - -multimailhook.commitBrowseURL - Used to generate a link to an online repository browser in commit - emails. This variable must be a string. Format directives like - ``%(<variable>)s`` will be expanded the same way as template - strings. In particular, ``%(id)s`` will be replaced by the full - Git commit identifier (40-chars hexadecimal). - - If the string does not contain any format directive, then - ``%(id)s`` will be automatically added to the string. If you don't - want ``%(id)s`` to be automatically added, use the empty format - directive ``%()s`` anywhere in the string. - - For example, a suitable value for the git-multimail project itself - would be - ``https://github.com/git-multimail/git-multimail/commit/%(id)s``. - -multimailhook.htmlInIntro, multimailhook.htmlInFooter - When generating an HTML message, git-multimail escapes any HTML - sequence by default. This means that if a template contains HTML - like ``<a href="foo">link</a>``, the reader will see the HTML - source code and not a proper link. - - Set ``multimailhook.htmlInIntro`` to true to allow writing HTML - formatting in introduction templates. Similarly, set - ``multimailhook.htmlInFooter`` for HTML in the footer. - - Variables expanded in the template are still escaped. For example, - if a repository's path contains a ``<``, it will be rendered as - such in the message. - - Read `<doc/customizing-emails.rst>`__ for more details and - examples. - -multimailhook.refchangeShowGraph - If this option is set to true, then summary emails about reference - changes will additionally include: - - * 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. - Default is false. - -multimailhook.mailer - This option changes the way emails are sent. Accepted values are: - - * **sendmail (the default)**: use the command ``/usr/sbin/sendmail`` or - ``/usr/lib/sendmail`` (or sendmailCommand, if configured). This - mode can be further customized via the following options: - - multimailhook.sendmailCommand - The command used by mailer ``sendmail`` to send emails. Shell - quoting is allowed in the value of this setting, but remember that - Git requires double-quotes to be escaped; e.g.:: - - git config multimailhook.sendmailcommand '/usr/sbin/sendmail -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). - - multimailhook.envelopeSender - If set then pass this value to sendmail via the -f option to set - the envelope sender address. - - * **smtp**: use Python's smtplib. This is useful when the sendmail - command is not available on the system. This mode can be - further customized via the following options: - - multimailhook.smtpServer - The name of the SMTP server to connect to. The value can - also include a colon and a port number; e.g., - ``mail.example.com:25``. Default is 'localhost' using port 25. - - multimailhook.smtpUser, multimailhook.smtpPass - Server username and password. Required if smtpEncryption is 'ssl'. - Note that the username and password currently need to be - set cleartext in the configuration file, which is not - recommended. If you need to use this option, be sure your - configuration file is read-only. - - multimailhook.envelopeSender - The sender address to be passed to the SMTP server. If - unset, then the value of multimailhook.from is used. - - multimailhook.smtpServerTimeout - Timeout in seconds. Default is 10. - - multimailhook.smtpEncryption - Set the security type. Allowed values: ``none``, ``ssl``, ``tls`` (starttls). - Default is ``none``. - - multimailhook.smtpCACerts - Set the path to a list of trusted CA certificate to verify the - server certificate, only supported when ``smtpEncryption`` is - ``tls``. If unset or empty, the server certificate is not - verified. If it targets a file containing a list of trusted CA - certificates (PEM format) these CAs will be used to verify the - server certificate. For debian, you can set - ``/etc/ssl/certs/ca-certificates.crt`` for using the system - trusted CAs. For self-signed server, you can add your server - certificate to the system store:: - - cd /usr/local/share/ca-certificates/ - openssl s_client -starttls smtp \ - -connect mail.example.net:587 -showcerts \ - </dev/null 2>/dev/null \ - | openssl x509 -outform PEM >mail.example.net.crt - update-ca-certificates - - and used the updated ``/etc/ssl/certs/ca-certificates.crt``. Or - directly use your ``/path/to/mail.example.net.crt``. Default is - unset. - - multimailhook.smtpServerDebugLevel - Integer number. Set to greater than 0 to activate debugging. - -multimailhook.from, multimailhook.fromCommit, multimailhook.fromRefchange - If set, use this value in the From: field of generated emails. - ``fromCommit`` is used for commit emails, ``fromRefchange`` is - used for refchange emails, and ``from`` is used as fall-back in - all cases. - - The value for these variables can be either: - - - An email address, which will be used directly. - - - The value ``pusher``, in which case the pusher's address (if - available) will be used. - - - The value ``author`` (meaningful only for ``fromCommit``), in which - case the commit author's address will be used. - - If config values are unset, the value of the From: header is - determined as follows: - - 1. (gitolite environment only) - 1.a) If ``multimailhook.MailaddressMap`` is set, and is a path - to an existing file (if relative, it is considered relative to - the place where ``gitolite.conf`` is located), then this file - should contain lines like:: - - username Firstname Lastname <email@example.com> - - git-multimail will then look for a line where ``$GL_USER`` - matches the ``username`` part, and use the rest of the line for - the ``From:`` header. - - 1.b) 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.MailaddressMap - (gitolite environment only) - File to look for a ``From:`` address based on the user doing the - push. Defaults to unset. See ``multimailhook.from`` for details. - -multimailhook.administrator - The name and/or email address of the administrator of the Git - repository; used in FOOTER_TEMPLATE. Default is - multimailhook.envelopesender if it is set; otherwise a generic - string is used. - -multimailhook.emailPrefix - All emails have this string prepended to their subjects, to aid - email filtering (though filtering based on the X-Git-* email - headers is probably more robust). Default is the short name of - the repository in square brackets; e.g., ``[myrepo]``. Set this - value to the empty string to suppress the email prefix. You may - use the placeholder ``%(repo_shortname)s`` for the short name of - the repository. - -multimailhook.emailMaxLines - The maximum number of lines that should be included in the body of - a generated email. If not specified, there is no limit. Lines - beyond the limit are suppressed and counted, and a final line is - added indicating the number of suppressed lines. - -multimailhook.emailMaxLineLength - The maximum length of a line in the email body. Lines longer than - this limit are truncated to this length with a trailing ``[...]`` - added to indicate the missing text. The default is 500, because - (a) diffs with longer lines are probably from binary files, for - which a diff is useless, and (b) even if a text file has such long - lines, the diffs are probably unreadable anyway. To disable line - truncation, set this option to 0. - -multimailhook.subjectMaxLength - The maximum length of the subject line (i.e. the ``oneline`` field - in templates, not including the prefix). Lines longer than this - limit are truncated to this length with a trailing ``[...]`` added - to indicate the missing text. This option The default is to use - ``multimailhook.emailMaxLineLength``. This option avoids sending - emails with overly long subject lines, but should not be needed if - the commit messages follow the Git convention (one short subject - line, then a blank line, then the message body). To disable line - truncation, set this option to 0. - -multimailhook.maxCommitEmails - The maximum number of commit emails to send for a given change. - When the number of patches is larger that this value, only the - summary refchange email is sent. This can avoid accidental - mailbombing, for example on an initial push. To disable commit - emails limit, set this option to 0. The default is 500. - -multimailhook.excludeMergeRevisions - When sending out revision emails, do not consider merge commits (the - functional equivalent of `rev-list --no-merges`). - The default is `false` (send merge commit emails). - -multimailhook.emailStrictUTF8 - If this boolean option is set to `true`, then the main part of the - email body is forced to be valid UTF-8. Any characters that are - not valid UTF-8 are converted to the Unicode replacement - character, U+FFFD. The default is `true`. - - This option is ineffective with Python 3, where non-UTF-8 - characters are unconditionally replaced. - -multimailhook.diffOpts - Options passed to ``git diff-tree`` when generating the summary - information for ReferenceChange emails. Default is ``--stat - --summary --find-copies-harder``. Add -p to those options to - include a unified diff of changes in addition to the usual summary - output. Shell quoting is allowed; see ``multimailhook.logOpts`` for - 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 - reference change emails (used only if refchangeShowLog is set). - 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:: - - git config multimailhook.logopts '--pretty=format:"%h %aN <%aE>%n%s%n%n%b%n"' - - If you want to set this by editing your configuration file - directly, remember that Git requires double-quotes to be escaped - (see git-config(1) for more information):: - - [multimailhook] - logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\" - -multimailhook.commitLogOpts - Options passed to ``git log`` to generate additional info for - revision change emails. For example, adding --ignore-all-spaces - will suppress whitespace changes. The default options are ``-C - --stat -p --cc``. Shell quoting is allowed; see - multimailhook.logOpts for details. - -multimailhook.dateSubstitute - String to use as a substitute for ``Date:`` in the output of ``git - log`` while formatting commit messages. This is useful to avoid - emitting a line that can be interpreted by mailers as the start of - a cited message (Zimbra webmail in particular). Defaults to - ``CommitDate:``. Set to an empty string or ``none`` to deactivate - the behavior. - -multimailhook.emailDomain - Domain name appended to the username of the person doing the push - to convert it into an email address - (via ``"%s@%s" % (username, emaildomain)``). More complicated - schemes can be implemented by overriding Environment and - overriding its get_pusher_email() method. - -multimailhook.replyTo, multimailhook.replyToCommit, multimailhook.replyToRefchange - Addresses to use in the Reply-To: field for commit emails - (replyToCommit) and refchange emails (replyToRefchange). - multimailhook.replyTo is used as default when replyToCommit or - replyToRefchange is not set. The shortcuts ``pusher`` and - ``author`` are allowed with the same semantics as for - ``multimailhook.from``. In addition, the value ``none`` can be - used to omit the ``Reply-To:`` field. - - The default is ``pusher`` for refchange emails, and ``author`` for - commit emails. - -multimailhook.quiet - Do not output the list of email recipients from the hook - -multimailhook.stdout - For debugging, send emails to stdout rather than to the - mailer. Equivalent to the --stdout command line option - -multimailhook.scanCommitForCc - If this option is set to true, than recipients from lines in commit body - that starts with ``CC:`` will be added to CC list. - Default: false - -multimailhook.combineWhenSingleCommit - If this option is set to true and a single new commit is pushed to - a branch, combine the summary and commit email messages into a - single email. - Default: true - -multimailhook.refFilterInclusionRegex, multimailhook.refFilterExclusionRegex, multimailhook.refFilterDoSendRegex, multimailhook.refFilterDontSendRegex - **Warning:** these options are experimental. They should work, but - the user-interface is not stable yet (in particular, the option - names may change). If you want to participate in stabilizing the - feature, please contact the maintainers and/or send pull-requests. - If you are happy with the current shape of the feature, please - report it too. - - Regular expressions that can be used to limit refs for which email - updates will be sent. It is an error to specify both an inclusion - and an exclusion regex. If a ``refFilterInclusionRegex`` is - specified, emails will only be sent for refs which match this - regex. If a ``refFilterExclusionRegex`` regex is specified, - emails will be sent for all refs except those that match this - regex (or that match a predefined regex specific to the - environment, such as "^refs/notes" for most environments and - "^refs/notes|^refs/changes" for the gerrit environment). - - The expressions are matched against the complete refname, and is - considered to match if any substring matches. For example, to - filter-out all tags, set ``refFilterExclusionRegex`` to - ``^refs/tags/`` (note the leading ``^`` but no trailing ``$``). If - you set ``refFilterExclusionRegex`` to ``master``, then any ref - containing ``master`` will be excluded (the ``master`` branch, but - also ``refs/tags/master`` or ``refs/heads/foo-master-bar``). - - ``refFilterDoSendRegex`` and ``refFilterDontSendRegex`` are - analogous to ``refFilterInclusionRegex`` and - ``refFilterExclusionRegex`` with one difference: with - ``refFilterDoSendRegex`` and ``refFilterDontSendRegex``, commits - introduced by one excluded ref will not be considered as new when - they reach an included ref. Typically, if you add a branch ``foo`` - to ``refFilterDontSendRegex``, push commits to this branch, and - later merge branch ``foo`` into ``master``, then the notification - email for ``master`` will contain a commit email only for the - merge commit. If you include ``foo`` in - ``refFilterExclusionRegex``, then at the time of merge, you will - receive one commit email per commit in the branch. - - These variables can be multi-valued, like:: - - [multimailhook] - refFilterExclusionRegex = ^refs/tags/ - refFilterExclusionRegex = ^refs/heads/master$ - - You can also provide a whitespace-separated list like:: - - [multimailhook] - refFilterExclusionRegex = ^refs/tags/ ^refs/heads/master$ - - Both examples exclude tags and the master branch, and are - equivalent to:: - - [multimailhook] - refFilterExclusionRegex = ^refs/tags/|^refs/heads/master$ - - ``refFilterInclusionRegex`` and ``refFilterExclusionRegex`` are - strictly stronger than ``refFilterDoSendRegex`` and - ``refFilterDontSendRegex``. In other words, adding a ref to a - DoSend/DontSend regex has no effect if it is already excluded by a - Exclusion/Inclusion regex. - -multimailhook.logFile, multimailhook.errorLogFile, multimailhook.debugLogFile - - When set, these variable designate path to files where - git-multimail will log some messages. Normal messages and error - messages are sent to ``logFile``, and error messages are also sent - to ``errorLogFile``. Debug messages and all other messages are - sent to ``debugLogFile``. The recommended way is to set only one - of these variables, but it is also possible to set several of them - (part of the information is then duplicated in several log files, - for example errors are duplicated to all log files). - - Relative path are relative to the Git repository where the push is - done. - -multimailhook.verbose - - Verbosity level of git-multimail on its standard output. By - default, show only error and info messages. If set to true, show - also debug messages. - -Email filtering aids --------------------- - -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``. - - -Customizing email contents --------------------------- - -git-multimail mostly generates emails by expanding templates. The -templates can be customized. To avoid the need to edit -``git_multimail.py`` directly, the preferred way to change the templates -is to write a separate Python script that imports ``git_multimail.py`` as -a module, then replaces the templates in place. See the provided -post-receive script for an example of how this is done. - - -Customizing git-multimail for your environment ----------------------------------------------- - -git-multimail is mostly customized via an "environment" that describes -the local environment in which Git is running. Two types of -environment are built in: - -GenericEnvironment - a stand-alone Git repository. - -GitoliteEnvironment - a Git repository that is managed by gitolite_. 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 -option to the script. - -If you need to customize the script in ways that are not supported by -the existing environments, you can define your own environment class -class using arbitrary Python code. To do so, you need to import -``git_multimail.py`` as a Python module, as demonstrated by the example -post-receive script. Then implement your environment class; it should -usually inherit from one of the existing Environment classes and -possibly one or more of the EnvironmentMixin classes. Then set the -``environment`` variable to an instance of your own environment class -and pass it to ``run_as_post_receive_hook()``. - -The standard environment classes, GenericEnvironment and -GitoliteEnvironment, are in fact themselves put together out of a -number of mixin classes, each of which handles one aspect of the -customization. For the finest control over your configuration, you -can specify exactly which mixin classes your own environment class -should inherit from, and override individual methods (or even add your -own mixin classes) to implement entirely new behaviors. If you -implement any mixins that might be useful to other people, please -consider sharing them with the community! - - -Getting involved ----------------- - -Please, read `<CONTRIBUTING.rst>`__ for instructions on how to -contribute to git-multimail. - - -Footnotes ---------- - -.. [1] Because of the way information is passed to update hooks, the - script's method of determining whether a commit has already - been seen does not work when it is used as an ``update`` script. - In particular, no notification email will be generated for a - new commit that is added to multiple references in the same - push. A workaround is to use --force-send to force sending the - emails. - -.. _gitolite: https://github.com/sitaramc/gitolite diff --git a/contrib/hooks/multimail/doc/customizing-emails.rst b/contrib/hooks/multimail/doc/customizing-emails.rst deleted file mode 100644 index 3f5b67f768..0000000000 --- a/contrib/hooks/multimail/doc/customizing-emails.rst +++ /dev/null @@ -1,56 +0,0 @@ -Customizing the content and formatting of emails -================================================ - -Overloading template strings ----------------------------- - -The content of emails is generated based on template strings defined -in ``git_multimail.py``. You can customize these template strings -without changing the script itself, by defining a Python wrapper -around it. The python wrapper should ``import git_multimail`` and then -override the ``git_multimail.*`` strings like this:: - - import sys # needed for sys.argv - - # Import and customize git_multimail: - import git_multimail - git_multimail.REVISION_INTRO_TEMPLATE = """...""" - git_multimail.COMBINED_INTRO_TEMPLATE = git_multimail.REVISION_INTRO_TEMPLATE - - # start git_multimail itself: - git_multimail.main(sys.argv[1:]) - -The template strings can use any value already used in the existing -templates (read the source code). - -Using HTML in template strings ------------------------------- - -If ``multimailhook.commitEmailFormat`` is set to HTML, then -git-multimail will generate HTML emails for commit notifications. The -log and diff will be formatted automatically by git-multimail. By -default, any HTML special character in the templates will be escaped. - -To use HTML formatting in the introduction of the email, set -``multimailhook.htmlInIntro`` to ``true``. Then, the template can -contain any HTML tags, that will be sent as-is in the email. For -example, to add some formatting and a link to the online commit, use -a format like:: - - git_multimail.REVISION_INTRO_TEMPLATE = """\ - <span style="color:#808080">This is an automated email from the git hooks/post-receive script.</span><br /><br /> - - <strong>%(pusher)s</strong> pushed a commit to %(refname_type)s %(short_refname)s - in repository %(repo_shortname)s.<br /> - - <a href="https://github.com/git-multimail/git-multimail/commit/%(newrev)s">View on GitHub</a>. - """ - -Note that the values expanded from ``%(variable)s`` in the format -strings will still be escaped. - -For a less flexible but easier to set up way to add a link to commit -emails, see ``multimailhook.commitBrowseURL``. - -Similarly, one can set ``multimailhook.htmlInFooter`` and override any -of the ``*_FOOTER*`` template strings. diff --git a/contrib/hooks/multimail/doc/gerrit.rst b/contrib/hooks/multimail/doc/gerrit.rst deleted file mode 100644 index 8011d05dec..0000000000 --- a/contrib/hooks/multimail/doc/gerrit.rst +++ /dev/null @@ -1,56 +0,0 @@ -Setting up git-multimail on Gerrit -================================== - -Gerrit has its own email-sending system, but you may prefer using -``git-multimail`` instead. It supports Gerrit natively as a Gerrit -``ref-updated`` hook (Warning: `Gerrit hooks -<https://gerrit-review.googlesource.com/Documentation/config-hooks.html>`__ -are distinct from Git hooks). Setting up ``git-multimail`` on a Gerrit -installation can be done following the instructions below. - -The explanations show an easy way to set up ``git-multimail``, -but leave ``git-multimail`` installed and unconfigured for a while. If -you run Gerrit on a production server, it is advised that you -execute the step "Set up the hook" last to avoid confusing your users -in the meantime. - -Set up the hook ---------------- - -Create a directory ``$site_path/hooks/`` if it does not exist (if you -don't know what ``$site_path`` is, run ``gerrit.sh status`` and look -for a ``GERRIT_SITE`` line). Either copy ``git_multimail.py`` to -``$site_path/hooks/ref-updated`` or create a wrapper script like -this:: - - #! /bin/sh - exec /path/to/git_multimail.py "$@" - -In both cases, make sure the file is named exactly -``$site_path/hooks/ref-updated`` and is executable. - -(Alternatively, you may configure the ``[hooks]`` section of -gerrit.config) - -Configuration -------------- - -Log on the gerrit server and edit ``$site_path/git/$project/config`` -to configure ``git-multimail``. - -Troubleshooting ---------------- - -Warning: this will disable ``git-multimail`` during the debug, and -could confuse your users. Don't run on a production server. - -To debug configuration issues with ``git-multimail``, you can add the -``--stdout`` option when calling ``git_multimail.py`` like this:: - - #!/bin/sh - exec /path/to/git-multimail/git-multimail/git_multimail.py \ - --stdout "$@" >> /tmp/log.txt - -and try pushing from a test repository. You should see the source of -the email that would have been sent in the output of ``git push`` in -the file ``/tmp/log.txt``. diff --git a/contrib/hooks/multimail/doc/gitolite.rst b/contrib/hooks/multimail/doc/gitolite.rst deleted file mode 100644 index 5054833105..0000000000 --- a/contrib/hooks/multimail/doc/gitolite.rst +++ /dev/null @@ -1,118 +0,0 @@ -Setting up git-multimail on gitolite -==================================== - -``git-multimail`` supports gitolite 3 natively. -The explanations below show an easy way to set up ``git-multimail``, -but leave ``git-multimail`` installed and unconfigured for a while. If -you run gitolite on a production server, it is advised that you -execute the step "Set up the hook" last to avoid confusing your users -in the meantime. - -Set up the hook ---------------- - -Log in as your gitolite user. - -Create a file ``.gitolite/hooks/common/post-receive`` on your gitolite -account containing (adapt the path, obviously):: - - #!/bin/sh - exec /path/to/git-multimail/git-multimail/git_multimail.py "$@" - -Make sure it's executable (``chmod +x``). Record the hook in -gitolite:: - - gitolite setup - -Configuration -------------- - -First, you have to allow the admin to set Git configuration variables. - -As gitolite user, edit the line containing ``GIT_CONFIG_KEYS`` in file -``.gitolite.rc``, to make it look like:: - - GIT_CONFIG_KEYS => 'multimailhook\..*', - -You can now log out and return to your normal user. - -In the ``gitolite-admin`` clone, edit the file ``conf/gitolite.conf`` -and add:: - - repo @all - # Not strictly needed as git_multimail.py will chose gitolite if - # $GL_USER is set. - config multimailhook.environment = gitolite - config multimailhook.mailingList = # Where emails should be sent - config multimailhook.from = # From address to use - -Note that by default, gitolite forbids ``<`` and ``>`` in variable -values (for security/paranoia reasons, see -`compensating for UNSAFE_PATT -<http://gitolite.com/gitolite/git-config/index.html#compensating-for-unsafe95patt>`__ -in gitolite's documentation for explanations and a way to disable -this). As a consequence, you will not be able to use ``First Last -<First.Last@example.com>`` as recipient email, but specifying -``First.Last@example.com`` alone works. - -Obviously, you can customize all parameters on a per-repository basis by -adding these ``config multimailhook.*`` lines in the section -corresponding to a repository or set of repositories. - -To activate ``git-multimail`` on a per-repository basis, do not set -``multimailhook.mailingList`` in the ``@all`` section and set it only -for repositories for which you want ``git-multimail``. - -Alternatively, you can set up the ``From:`` field on a per-user basis -by adding a ``BEGIN USER EMAILS``/``END USER EMAILS`` section (see -``../README``). - -Specificities of Gitolite for Configuration -------------------------------------------- - -Empty configuration variables -............................. - -With gitolite, the syntax ``config multimailhook.commitList = ""`` -unsets the variable instead of setting it to an empty string (see -`here -<http://gitolite.com/gitolite/git-config.html#an-important-warning-about-deleting-a-config-line>`__). -As a result, there is no way to set a variable to the empty string. -In all most places where an empty value is required, git-multimail -now allows to specify special ``"none"`` value (case-sensitive) to -mean the same. - -Alternatively, one can use ``" "`` (a single space) instead of ``""``. -In most cases (in particular ``multimailhook.*List`` variables), this -will be equivalent to an empty string. - -If you have a use-case where ``"none"`` is not an acceptable value and -you need ``" "`` or ``""`` instead, please report it as a bug to -git-multimail. - -Allowing Regular Expressions in Configuration -............................................. - -gitolite has a mechanism to prevent unsafe configuration variable -values, which prevent characters like ``|`` commonly used in regular -expressions. If you do not need the safety feature of gitolite and -need to use regular expressions in your configuration (e.g. for -``multimailhook.refFilter*`` variables), set -`UNSAFE_PATT -<http://gitolite.com/gitolite/git-config.html#unsafe-patt>`__ to a -less restrictive value. - -Troubleshooting ---------------- - -Warning: this will disable ``git-multimail`` during the debug, and -could confuse your users. Don't run on a production server. - -To debug configuration issues with ``git-multimail``, you can add the -``--stdout`` option when calling ``git_multimail.py`` like this:: - - #!/bin/sh - exec /path/to/git-multimail/git-multimail/git_multimail.py --stdout "$@" - -and try pushing from a test repository. You should see the source of -the email that would have been sent in the output of ``git push``. diff --git a/contrib/hooks/multimail/doc/troubleshooting.rst b/contrib/hooks/multimail/doc/troubleshooting.rst deleted file mode 100644 index 651b509ee6..0000000000 --- a/contrib/hooks/multimail/doc/troubleshooting.rst +++ /dev/null @@ -1,78 +0,0 @@ -Troubleshooting issues with git-multimail: a FAQ -================================================ - -How to check that git-multimail is properly set up? ---------------------------------------------------- - -Since version 1.4.0, git-multimail allows a simple self-checking of -its configuration: run it with the environment variable -``GIT_MULTIMAIL_CHECK_SETUP`` set to a non-empty string. You should -get something like this:: - - $ GIT_MULTIMAIL_CHECK_SETUP=true /home/moy/dev/git-multimail/git-multimail/git_multimail.py - Environment values: - administrator : 'the administrator of this repository' - charset : 'utf-8' - emailprefix : '[git-multimail] ' - fqdn : 'anie' - projectdesc : 'UNNAMED PROJECT' - pusher : 'moy' - repo_path : '/home/moy/dev/git-multimail' - repo_shortname : 'git-multimail' - - Now, checking that git-multimail's standard input is properly set ... - Please type some text and then press Return - foo - You have just entered: - foo - git-multimail seems properly set up. - -If you forgot to set an important variable, you may get instead:: - - $ GIT_MULTIMAIL_CHECK_SETUP=true /home/moy/dev/git-multimail/git-multimail/git_multimail.py - No email recipients configured! - -Do not set ``$GIT_MULTIMAIL_CHECK_SETUP`` other than for testing your -configuration: it would disable the hook completely. - -Git is not using the right address in the From/To/Reply-To field ----------------------------------------------------------------- - -First, make sure that git-multimail actually uses what you think it is -using. A lot happens to your email (especially when posting to a -mailing-list) between the time `git_multimail.py` sends it and the -time it reaches your inbox. - -A simple test (to do on a test repository, do not use in production as -it would disable email sending): change your post-receive hook to call -`git_multimail.py` with the `--stdout` option, and try to push to the -repository. You should see something like:: - - Counting objects: 3, done. - Writing objects: 100% (3/3), 263 bytes | 0 bytes/s, done. - Total 3 (delta 0), reused 0 (delta 0) - remote: Sending notification emails to: foo.bar@example.com - remote: =========================================================================== - remote: Date: Mon, 25 Apr 2016 18:39:59 +0200 - remote: To: foo.bar@example.com - remote: Subject: [git] branch master updated: foo - remote: MIME-Version: 1.0 - remote: Content-Type: text/plain; charset=utf-8 - remote: Content-Transfer-Encoding: 8bit - remote: Message-ID: <20160425163959.2311.20498@anie> - remote: From: Auth Or <Foo.Bar@example.com> - remote: Reply-To: Auth Or <Foo.Bar@example.com> - remote: X-Git-Host: example - ... - remote: -- - remote: To stop receiving notification emails like this one, please contact - remote: the administrator of this repository. - remote: =========================================================================== - To /path/to/repo - 6278f04..e173f20 master -> master - -Note: this does not include the sender (Return-Path: header), as it is -not part of the message content but passed to the mailer. Some mailer -show the ``Sender:`` field instead of the ``From:`` field (for -example, Zimbra Webmail shows ``From: <sender-field> on behalf of -<from-field>``). diff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py deleted file mode 100755 index f563be82fc..0000000000 --- a/contrib/hooks/multimail/git_multimail.py +++ /dev/null @@ -1,4346 +0,0 @@ -#! /usr/bin/env python - -__version__ = '1.5.0' - -# Copyright (c) 2015-2016 Matthieu Moy and others -# Copyright (c) 2012-2014 Michael Haggerty and others -# Derived from contrib/hooks/post-receive-email, which is -# Copyright (c) 2007 Andy Parkins -# and also includes contributions by other authors. -# -# This file is part of git-multimail. -# -# git-multimail is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License version -# 2 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see -# <http://www.gnu.org/licenses/>. - -"""Generate notification emails for pushes to a git repository. - -This hook sends emails describing changes introduced by pushes to a -git repository. For each reference that was changed, it emits one -ReferenceChange email summarizing how the reference was changed, -followed by one Revision email for each new commit that was introduced -by the reference change. - -Each commit is announced in exactly one Revision email. If the same -commit is merged into another branch in the same or a later push, then -the ReferenceChange email will list the commit's SHA1 and its one-line -summary, but no new Revision email will be generated. - -This script is designed to be used as a "post-receive" hook in a git -repository (see githooks(5)). It can also be used as an "update" -script, but this usage is not completely reliable and is deprecated. - -To help with debugging, this script accepts a --stdout option, which -causes the emails to be written to standard output rather than sent -using sendmail. - -See the accompanying README file for the complete documentation. - -""" - -import sys -import os -import re -import bisect -import socket -import subprocess -import shlex -import optparse -import logging -import smtplib -try: - import ssl -except ImportError: - # Python < 2.6 do not have ssl, but that's OK if we don't use it. - pass -import time - -import uuid -import base64 - -PYTHON3 = sys.version_info >= (3, 0) - -if sys.version_info <= (2, 5): - def all(iterable): - for element in iterable: - if not element: - return False - return True - - -def is_ascii(s): - return all(ord(c) < 128 and ord(c) > 0 for c in s) - - -if PYTHON3: - def is_string(s): - return isinstance(s, str) - - def str_to_bytes(s): - return s.encode(ENCODING) - - def bytes_to_str(s, errors='strict'): - return s.decode(ENCODING, errors) - - unicode = str - - def write_str(f, msg): - # Try outputting with the default encoding. If it fails, - # try UTF-8. - try: - f.buffer.write(msg.encode(sys.getdefaultencoding())) - except UnicodeEncodeError: - f.buffer.write(msg.encode(ENCODING)) - - def read_line(f): - # Try reading with the default encoding. If it fails, - # try UTF-8. - out = f.buffer.readline() - try: - return out.decode(sys.getdefaultencoding()) - except UnicodeEncodeError: - return out.decode(ENCODING) - - import html - - def html_escape(s): - return html.escape(s) - -else: - def is_string(s): - try: - return isinstance(s, basestring) - except NameError: # Silence Pyflakes warning - raise - - def str_to_bytes(s): - return s - - def bytes_to_str(s, errors='strict'): - return s - - def write_str(f, msg): - f.write(msg) - - def read_line(f): - return f.readline() - - def next(it): - return it.next() - - import cgi - - def html_escape(s): - return cgi.escape(s, True) - -try: - from email.charset import Charset - from email.utils import make_msgid - from email.utils import getaddresses - from email.utils import formataddr - from email.utils import formatdate - from email.header import Header -except ImportError: - # Prior to Python 2.5, the email module used different names: - from email.Charset import Charset - from email.Utils import make_msgid - from email.Utils import getaddresses - from email.Utils import formataddr - from email.Utils import formatdate - from email.Header import Header - - -DEBUG = False - -ZEROS = '0' * 40 -LOGBEGIN = '- Log -----------------------------------------------------------------\n' -LOGEND = '-----------------------------------------------------------------------\n' - -ADDR_HEADERS = set(['from', 'to', 'cc', 'bcc', 'reply-to', 'sender']) - -# It is assumed in many places that the encoding is uniformly UTF-8, -# so changing these constants is unsupported. But define them here -# anyway, to make it easier to find (at least most of) the places -# where the encoding is important. -(ENCODING, CHARSET) = ('UTF-8', 'utf-8') - - -REF_CREATED_SUBJECT_TEMPLATE = ( - '%(emailprefix)s%(refname_type)s %(short_refname)s created' - ' (now %(newrev_short)s)' - ) -REF_UPDATED_SUBJECT_TEMPLATE = ( - '%(emailprefix)s%(refname_type)s %(short_refname)s updated' - ' (%(oldrev_short)s -> %(newrev_short)s)' - ) -REF_DELETED_SUBJECT_TEMPLATE = ( - '%(emailprefix)s%(refname_type)s %(short_refname)s deleted' - ' (was %(oldrev_short)s)' - ) - -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 -Subject: %(subject)s -MIME-Version: 1.0 -Content-Type: text/%(contenttype)s; charset=%(charset)s -Content-Transfer-Encoding: 8bit -Message-ID: %(msgid)s -From: %(fromaddr)s -Reply-To: %(reply_to)s -Thread-Index: %(thread_index)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-NotificationType: ref_changed -X-Git-Multimail-Version: %(multimail_version)s -Auto-Submitted: auto-generated -""" - -REFCHANGE_INTRO_TEMPLATE = """\ -This is an automated email from the git hooks/post-receive script. - -%(pusher)s pushed a change to %(refname_type)s %(short_refname)s -in repository %(repo_shortname)s. - -""" - - -FOOTER_TEMPLATE = """\ - --- \n\ -To stop receiving notification emails like this one, please contact -%(administrator)s. -""" - - -REWIND_ONLY_TEMPLATE = """\ -This update removed existing revisions from the reference, leaving the -reference pointing at a previous point in the repository history. - - * -- * -- N %(refname)s (%(newrev_short)s) - \\ - O -- O -- O (%(oldrev_short)s) - -Any revisions marked "omit" are not gone; other references still -refer to them. Any revisions marked "discard" are gone forever. -""" - - -NON_FF_TEMPLATE = """\ -This update added new revisions after undoing existing revisions. -That is to say, some revisions that were in the old version of the -%(refname_type)s are not in the new version. This situation occurs -when a user --force pushes a change and generates a repository -containing something like this: - - * -- * -- B -- O -- O -- O (%(oldrev_short)s) - \\ - N -- N -- N %(refname)s (%(newrev_short)s) - -You should already have received notification emails for all of the O -revisions, and so the following emails describe only the N revisions -from the common base, B. - -Any revisions marked "omit" are not gone; other references still -refer to them. Any revisions marked "discard" are gone forever. -""" - - -NO_NEW_REVISIONS_TEMPLATE = """\ -No new revisions were added by this update. -""" - - -DISCARDED_REVISIONS_TEMPLATE = """\ -This change permanently discards the following revisions: -""" - - -NO_DISCARDED_REVISIONS_TEMPLATE = """\ -The revisions that were on this %(refname_type)s are still contained in -other references; therefore, this change does not discard any commits -from the repository. -""" - - -NEW_REVISIONS_TEMPLATE = """\ -The %(tot)s revisions listed above as "new" are entirely new to this -repository and will be described in separate emails. The revisions -listed as "add" were already present in the repository and have only -been added to this reference. - -""" - - -TAG_CREATED_TEMPLATE = """\ - at %(newrev_short)-8s (%(newrev_type)s) -""" - - -TAG_UPDATED_TEMPLATE = """\ -*** WARNING: tag %(short_refname)s was modified! *** - - from %(oldrev_short)-8s (%(oldrev_type)s) - to %(newrev_short)-8s (%(newrev_type)s) -""" - - -TAG_DELETED_TEMPLATE = """\ -*** WARNING: tag %(short_refname)s was deleted! *** - -""" - - -# The template used in summary tables. It looks best if this uses the -# same alignment as TAG_CREATED_TEMPLATE and TAG_UPDATED_TEMPLATE. -BRIEF_SUMMARY_TEMPLATE = """\ -%(action)8s %(rev_short)-8s %(text)s -""" - - -NON_COMMIT_UPDATE_TEMPLATE = """\ -This is an unusual reference change because the reference did not -refer to a commit either before or after the change. We do not know -how to provide full information about this reference change. -""" - - -REVISION_HEADER_TEMPLATE = """\ -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/%(contenttype)s; charset=%(charset)s -Content-Transfer-Encoding: 8bit -From: %(fromaddr)s -Reply-To: %(reply_to)s -In-Reply-To: %(reply_to_msgid)s -References: %(reply_to_msgid)s -Thread-Index: %(thread_index)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-Rev: %(rev)s -X-Git-NotificationType: diff -X-Git-Multimail-Version: %(multimail_version)s -Auto-Submitted: auto-generated -""" - -REVISION_INTRO_TEMPLATE = """\ -This is an automated email from the git hooks/post-receive script. - -%(pusher)s pushed a commit to %(refname_type)s %(short_refname)s -in repository %(repo_shortname)s. - -""" - -LINK_TEXT_TEMPLATE = """\ -View the commit online: -%(browse_url)s - -""" - -LINK_HTML_TEMPLATE = """\ -<p><a href="%(browse_url)s">View the commit online</a>.</p> -""" - - -REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE - - -# 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/%(contenttype)s; 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 -X-Git-NotificationType: ref_changed_plus_diff -X-Git-Multimail-Version: %(multimail_version)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 - self.retcode = retcode - Exception.__init__( - self, - 'Command "%s" failed with retcode %s' % (' '.join(cmd), retcode,) - ) - - -class ConfigurationException(Exception): - pass - - -# The "git" program (this could be changed to include a full path): -GIT_EXECUTABLE = 'git' - - -# How "git" should be invoked (including global arguments), as a list -# of words. This variable is usually initialized automatically by -# read_git_output() via choose_git_command(), but if a value is set -# here then it will be used unconditionally. -GIT_CMD = None - - -def choose_git_command(): - """Decide how to invoke git, and record the choice in GIT_CMD.""" - - global GIT_CMD - - if GIT_CMD is None: - try: - # Check to see whether the "-c" option is accepted (it was - # only added in Git 1.7.2). We don't actually use the - # output of "git --version", though if we needed more - # specific version information this would be the place to - # do it. - cmd = [GIT_EXECUTABLE, '-c', 'foo.bar=baz', '--version'] - read_output(cmd) - GIT_CMD = [GIT_EXECUTABLE, '-c', 'i18n.logoutputencoding=%s' % (ENCODING,)] - except CommandError: - GIT_CMD = [GIT_EXECUTABLE] - - -def read_git_output(args, input=None, keepends=False, **kw): - """Read the output of a Git command.""" - - if GIT_CMD is None: - choose_git_command() - - return read_output(GIT_CMD + args, input=input, keepends=keepends, **kw) - - -def read_output(cmd, input=None, keepends=False, **kw): - if input: - stdin = subprocess.PIPE - input = str_to_bytes(input) - else: - stdin = None - errors = 'strict' - if 'errors' in kw: - errors = kw['errors'] - del kw['errors'] - p = subprocess.Popen( - tuple(str_to_bytes(w) for w in cmd), - stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw - ) - (out, err) = p.communicate(input) - out = bytes_to_str(out, errors=errors) - retcode = p.wait() - if retcode: - raise CommandError(cmd, retcode) - if not keepends: - out = out.rstrip('\n\r') - return out - - -def read_git_lines(args, keepends=False, **kw): - """Return the lines output by Git command. - - Return as single lines, with newlines stripped off.""" - - return read_git_output(args, keepends=True, **kw).splitlines(keepends) - - -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.""" - - # Convert to unicode, if required. - if not isinstance(text, unicode): - text = unicode(text, 'utf-8') - - if is_ascii(text): - charset = 'ascii' - else: - charset = 'utf-8' - - return Header(text, header_name=header_name, charset=Charset(charset)).encode() - - -def addr_header_encode(text, header_name=None): - """Encode and line-wrap the value of an email header field containing - email addresses.""" - - # Convert to unicode, if required. - if not isinstance(text, unicode): - text = unicode(text, 'utf-8') - - text = ', '.join( - formataddr((header_encode(name), emailaddr)) - for name, emailaddr in getaddresses([text]) - ) - - if is_ascii(text): - charset = 'ascii' - else: - charset = 'utf-8' - - return Header(text, header_name=header_name, charset=Charset(charset)).encode() - - -class Config(object): - def __init__(self, section, git_config=None): - """Represent a section of the git configuration. - - If git_config is specified, it is passed to "git config" in - the GIT_CONFIG environment variable, meaning that "git config" - will read the specified path rather than the Git default - config paths.""" - - self.section = section - if git_config: - self.env = os.environ.copy() - self.env['GIT_CONFIG'] = git_config - else: - self.env = None - - @staticmethod - def _split(s): - """Split NUL-terminated values.""" - - words = s.split('\0') - assert words[-1] == '' - return words[:-1] - - @staticmethod - def add_config_parameters(c): - """Add configuration parameters to Git. - - c is either an str or a list of str, each element being of the - form 'var=val' or 'var', with the same syntax and meaning as - the argument of 'git -c var=val'. - """ - if isinstance(c, str): - c = (c,) - parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '') - if parameters: - parameters += ' ' - # git expects GIT_CONFIG_PARAMETERS to be of the form - # "'name1=value1' 'name2=value2' 'name3=value3'" - # including everything inside the double quotes (but not the double - # quotes themselves). Spacing is critical. Also, if a value contains - # a literal single quote that quote must be represented using the - # four character sequence: '\'' - parameters += ' '.join("'" + x.replace("'", "'\\''") + "'" for x in c) - os.environ['GIT_CONFIG_PARAMETERS'] = parameters - - def get(self, name, default=None): - try: - values = self._split(read_git_output( - ['config', '--get', '--null', '%s.%s' % (self.section, name)], - env=self.env, keepends=True, - )) - assert len(values) == 1 - return values[0] - except CommandError: - return default - - def get_bool(self, name, default=None): - try: - value = read_git_output( - ['config', '--get', '--bool', '%s.%s' % (self.section, name)], - env=self.env, - ) - except CommandError: - return default - return value == 'true' - - def get_all(self, name, default=None): - """Read a (possibly multivalued) setting from the configuration. - - Return the result as a list of values, or default if the name - is unset.""" - - try: - return self._split(read_git_output( - ['config', '--get-all', '--null', '%s.%s' % (self.section, name)], - env=self.env, keepends=True, - )) - except CommandError: - t, e, traceback = sys.exc_info() - if e.retcode == 1: - # "the section or key is invalid"; i.e., there is no - # value for the specified key. - return default - else: - raise - - def set(self, name, value): - read_git_output( - ['config', '%s.%s' % (self.section, name), value], - env=self.env, - ) - - def add(self, name, value): - read_git_output( - ['config', '--add', '%s.%s' % (self.section, name), value], - env=self.env, - ) - - def __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( - ['config', '--unset-all', '%s.%s' % (self.section, name)], - env=self.env, - ) - except CommandError: - t, e, traceback = sys.exc_info() - if e.retcode == 5: - # The name doesn't exist, which is what we wanted anyway... - pass - else: - raise - - def set_recipients(self, name, value): - self.unset_all(name) - for pair in getaddresses([value]): - self.add(name, formataddr(pair)) - - -def generate_summaries(*log_args): - """Generate a brief summary for each revision requested. - - log_args are strings that will be passed directly to "git log" as - revision selectors. Iterate over (sha1_short, subject) for each - commit specified by log_args (subject is the first line of the - commit message as a string without EOLs).""" - - cmd = [ - 'log', '--abbrev', '--format=%h %s', - ] + list(log_args) + ['--'] - for line in read_git_lines(cmd): - yield tuple(line.split(' ', 1)) - - -def limit_lines(lines, max_lines): - for (index, line) in enumerate(lines): - if index < max_lines: - yield line - - if index >= max_lines: - yield '... %d lines suppressed ...\n' % (index + 1 - max_lines,) - - -def limit_linelength(lines, max_linelength): - for line in lines: - # Don't forget that lines always include a trailing newline. - if len(line) > max_linelength + 1: - line = line[:max_linelength - 7] + ' [...]\n' - yield line - - -class CommitSet(object): - """A (constant) set of object names. - - The set should be initialized with full SHA1 object names. The - __contains__() method returns True iff its argument is an - abbreviation of any the names in the set.""" - - def __init__(self, names): - self._names = sorted(names) - - def __len__(self): - return len(self._names) - - def __contains__(self, sha1_abbrev): - """Return True iff this set contains sha1_abbrev (which might be abbreviated).""" - - i = bisect.bisect_left(self._names, sha1_abbrev) - return i < len(self) and self._names[i].startswith(sha1_abbrev) - - -class GitObject(object): - def __init__(self, sha1, type=None): - if sha1 == ZEROS: - self.sha1 = self.type = self.commit_sha1 = None - else: - self.sha1 = sha1 - self.type = type or read_git_output(['cat-file', '-t', self.sha1]) - - if self.type == 'commit': - self.commit_sha1 = self.sha1 - elif self.type == 'tag': - try: - self.commit_sha1 = read_git_output( - ['rev-parse', '--verify', '%s^0' % (self.sha1,)] - ) - except CommandError: - # Cannot deref tag to determine commit_sha1 - self.commit_sha1 = None - else: - self.commit_sha1 = None - - self.short = read_git_output(['rev-parse', '--short', sha1]) - - def get_summary(self): - """Return (sha1_short, subject) for this commit.""" - - if not self.sha1: - raise ValueError('Empty commit has no summary') - - return next(iter(generate_summaries('--no-walk', self.sha1))) - - def __eq__(self, other): - return isinstance(other, GitObject) and self.sha1 == other.sha1 - - def __ne__(self, other): - return not self == other - - def __hash__(self): - return hash(self.sha1) - - def __nonzero__(self): - return bool(self.sha1) - - def __bool__(self): - """Python 2 backward compatibility""" - return self.__nonzero__() - - def __str__(self): - return self.sha1 or ZEROS - - -class Change(object): - """A Change that has been made to the Git repository. - - Abstract class from which both Revisions and ReferenceChanges are - derived. A Change knows how to generate a notification email - describing itself.""" - - def __init__(self, environment): - self.environment = environment - self._values = None - self._contains_html_diff = False - - def _contains_diff(self): - # We do contain a diff, should it be rendered in HTML? - if self.environment.commit_email_format == "html": - self._contains_html_diff = True - - def _compute_values(self): - """Return a dictionary {keyword: expansion} for this Change. - - Derived classes overload this method to add more entries to - the return value. This method is used internally by - get_values(). The return value should always be a new - dictionary.""" - - values = self.environment.get_values() - fromaddr = self.environment.get_fromaddr(change=self) - if fromaddr is not None: - values['fromaddr'] = fromaddr - values['multimail_version'] = get_version() - return values - - # Aliases usable in template strings. Tuple of pairs (destination, - # source). - VALUES_ALIAS = ( - ("id", "newrev"), - ) - - def get_values(self, **extra_values): - """Return a dictionary {keyword: expansion} for this Change. - - Return a dictionary mapping keywords to the values that they - should be expanded to for this Change (used when interpolating - template strings). If any keyword arguments are supplied, add - those to the return value as well. The return value is always - a new dictionary.""" - - if self._values is None: - self._values = self._compute_values() - - values = self._values.copy() - if extra_values: - values.update(extra_values) - - for alias, val in self.VALUES_ALIAS: - values[alias] = values[val] - return values - - def expand(self, template, **extra_values): - """Expand template. - - Expand the template (which should be a string) using string - interpolation of the values for this Change. If any keyword - arguments are provided, also include those in the keywords - available for interpolation.""" - - return template % self.get_values(**extra_values) - - def expand_lines(self, template, html_escape_val=False, **extra_values): - """Break template into lines and expand each line.""" - - values = self.get_values(**extra_values) - if html_escape_val: - for k in values: - if is_string(values[k]): - values[k] = html_escape(values[k]) - for line in template.splitlines(True): - yield line % values - - def expand_header_lines(self, template, **extra_values): - """Break template into lines and expand each line as an RFC 2822 header. - - Encode values and split up lines that are too long. Silently - skip lines that contain references to unknown variables.""" - - values = self.get_values(**extra_values) - if self._contains_html_diff: - self._content_type = 'html' - else: - self._content_type = 'plain' - values['contenttype'] = self._content_type - - for line in template.splitlines(): - (name, value) = line.split(': ', 1) - - try: - value = value % values - except KeyError: - t, e, traceback = sys.exc_info() - if DEBUG: - self.environment.log_warning( - 'Warning: unknown variable %r in the following line; line skipped:\n' - ' %s\n' - % (e.args[0], line,) - ) - else: - if name.lower() in ADDR_HEADERS: - value = addr_header_encode(value, name) - else: - value = header_encode(value, name) - for splitline in ('%s: %s\n' % (name, value)).splitlines(True): - yield splitline - - def generate_email_header(self): - """Generate the RFC 2822 email headers for this Change, a line at a time. - - The output should not include the trailing blank line.""" - - raise NotImplementedError() - - def generate_browse_link(self, base_url): - """Generate a link to an online repository browser.""" - return iter(()) - - def generate_email_intro(self, html_escape_val=False): - """Generate the email intro for this Change, a line at a time. - - The output will be used as the standard boilerplate at the top - of the email body.""" - - raise NotImplementedError() - - def generate_email_body(self, push): - """Generate the main part of the email body, a line at a time. - - The text in the body might be truncated after a specified - number of lines (see multimailhook.emailmaxlines).""" - - raise NotImplementedError() - - def generate_email_footer(self, html_escape_val): - """Generate the footer of the email, a line at a time. - - The footer is always included, irrespective of - multimailhook.emailmaxlines.""" - - raise NotImplementedError() - - def _wrap_for_html(self, lines): - """Wrap the lines in HTML <pre> tag when using HTML format. - - Escape special HTML characters and add <pre> and </pre> tags around - the given lines if we should be generating HTML as indicated by - self._contains_html_diff being set to true. - """ - if self._contains_html_diff: - yield "<pre style='margin:0'>\n" - - for line in lines: - yield html_escape(line) - - yield '</pre>\n' - else: - for line in lines: - yield line - - def generate_email(self, push, body_filter=None, extra_header_values={}): - """Generate an email describing this change. - - Iterate over the lines (including the header lines) of an - email describing this change. If body_filter is not None, - then use it to filter the lines that are intended for the - email body. - - 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()""" - - for line in self.generate_email_header(**extra_header_values): - yield line - yield '\n' - html_escape_val = (self.environment.html_in_intro and - self._contains_html_diff) - intro = self.generate_email_intro(html_escape_val) - if not self.environment.html_in_intro: - intro = self._wrap_for_html(intro) - for line in intro: - yield line - - if self.environment.commitBrowseURL: - for line in self.generate_browse_link(self.environment.commitBrowseURL): - yield line - - body = self.generate_email_body(push) - if body_filter is not None: - body = body_filter(body) - - diff_started = False - if self._contains_html_diff: - # "white-space: pre" is the default, but we need to - # specify it again in case the message is viewed in a - # webmail which wraps it in an element setting white-space - # to something else (Zimbra does this and sets - # white-space: pre-line). - yield '<pre style="white-space: pre; background: #F8F8F8">' - for line in body: - if self._contains_html_diff: - # This is very, very naive. It would be much better to really - # parse the diff, i.e. look at how many lines do we have in - # the hunk headers instead of blindly highlighting everything - # that looks like it might be part of a diff. - bgcolor = '' - fgcolor = '' - if line.startswith('--- a/'): - diff_started = True - bgcolor = 'e0e0ff' - elif line.startswith('diff ') or line.startswith('index '): - diff_started = True - fgcolor = '808080' - elif diff_started: - if line.startswith('+++ '): - bgcolor = 'e0e0ff' - elif line.startswith('@@'): - bgcolor = 'e0e0e0' - elif line.startswith('+'): - bgcolor = 'e0ffe0' - elif line.startswith('-'): - bgcolor = 'ffe0e0' - elif line.startswith('commit '): - fgcolor = '808000' - elif line.startswith(' '): - fgcolor = '404040' - - # Chop the trailing LF, we don't want it inside <pre>. - line = html_escape(line[:-1]) - - if bgcolor or fgcolor: - style = 'display:block; white-space:pre;' - if bgcolor: - style += 'background:#' + bgcolor + ';' - if fgcolor: - style += 'color:#' + fgcolor + ';' - # Use a <span style='display:block> to color the - # whole line. The newline must be inside the span - # to display properly both in Firefox and in - # text-based browser. - line = "<span style='%s'>%s\n</span>" % (style, line) - else: - line = line + '\n' - - yield line - if self._contains_html_diff: - yield '</pre>' - html_escape_val = (self.environment.html_in_footer and - self._contains_html_diff) - footer = self.generate_email_footer(html_escape_val) - if not self.environment.html_in_footer: - footer = self._wrap_for_html(footer) - for line in footer: - yield line - - def get_specific_fromaddr(self): - """For kinds of Changes which specify it, return the kind-specific - From address to use.""" - return None - - -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 - self.rev = rev - self.change_type = self.reference_change.change_type - self.refname = self.reference_change.refname - self.num = num - self.tot = tot - self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1]) - self.recipients = self.environment.get_revision_recipients(self) - - # -s is short for --no-patch, but -s works on older git's (e.g. 1.7) - self.parents = read_git_lines(['show', '-s', '--format=%P', - self.rev.sha1])[0].split() - - 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' % (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) - - oneline = read_git_output( - ['log', '--format=%s', '--no-walk', self.rev.sha1] - ) - - max_subject_length = self.environment.get_max_subject_length() - if max_subject_length > 0 and len(oneline) > max_subject_length: - oneline = oneline[:max_subject_length - 6] + ' [...]' - - values['rev'] = self.rev.sha1 - values['parents'] = ' '.join(self.parents) - values['rev_short'] = self.rev.short - values['change_type'] = self.change_type - values['refname'] = self.refname - values['newrev'] = self.rev.sha1 - values['short_refname'] = self.reference_change.short_refname - values['refname_type'] = self.reference_change.refname_type - values['reply_to_msgid'] = self.reference_change.msgid - values['thread_index'] = self.reference_change.thread_index - 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 - - reply_to = self.environment.get_reply_to_commit(self) - if reply_to: - values['reply_to'] = reply_to - - return values - - def generate_email_header(self, **extra_values): - for line in self.expand_header_lines( - REVISION_HEADER_TEMPLATE, **extra_values - ): - yield line - - def generate_browse_link(self, base_url): - if '%(' not in base_url: - base_url += '%(id)s' - url = "".join(self.expand_lines(base_url)) - if self._content_type == 'html': - for line in self.expand_lines(LINK_HTML_TEMPLATE, - html_escape_val=True, - browse_url=url): - yield line - elif self._content_type == 'plain': - for line in self.expand_lines(LINK_TEXT_TEMPLATE, - html_escape_val=False, - browse_url=url): - yield line - else: - raise NotImplementedError("Content-type %s unsupported. Please report it as a bug.") - - def generate_email_intro(self, html_escape_val=False): - for line in self.expand_lines(REVISION_INTRO_TEMPLATE, - html_escape_val=html_escape_val): - yield line - - def generate_email_body(self, push): - """Show this revision.""" - - for line in read_git_lines( - ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1], - keepends=True, - errors='replace'): - if line.startswith('Date: ') and self.environment.date_substitute: - yield self.environment.date_substitute + line[len('Date: '):] - else: - yield line - - def generate_email_footer(self, html_escape_val): - return self.expand_lines(REVISION_FOOTER_TEMPLATE, - html_escape_val=html_escape_val) - - def generate_email(self, push, body_filter=None, extra_header_values={}): - self._contains_diff() - return Change.generate_email(self, push, body_filter, extra_header_values) - - def get_specific_fromaddr(self): - return self.environment.from_commit - - -class ReferenceChange(Change): - """A Change to a Git reference. - - An abstract class representing a create, update, or delete of a - Git reference. Derived classes handle specific types of reference - (e.g., tags vs. branches). These classes generate the main - reference change email summarizing the reference change and - whether it caused any any commits to be added or removed. - - ReferenceChange objects are usually created using the static - create() method, which has the logic to decide which derived class - to instantiate.""" - - REF_RE = re.compile(r'^refs\/(?P<area>[^\/]+)\/(?P<shortname>.*)$') - - @staticmethod - def create(environment, oldrev, newrev, refname): - """Return a ReferenceChange object representing the change. - - Return an object that represents the type of change that is being - made. oldrev and newrev should be SHA1s or ZEROS.""" - - old = GitObject(oldrev) - new = GitObject(newrev) - rev = new or old - - # The revision type tells us what type the commit is, combined with - # the location of the ref we can decide between - # - working branch - # - tracking branch - # - unannotated tag - # - annotated tag - m = ReferenceChange.REF_RE.match(refname) - if m: - area = m.group('area') - short_refname = m.group('shortname') - else: - area = '' - short_refname = refname - - if rev.type == 'tag': - # Annotated tag: - klass = AnnotatedTagChange - elif rev.type == 'commit': - if area == 'tags': - # Non-annotated tag: - klass = NonAnnotatedTagChange - elif area == 'heads': - # Branch: - klass = BranchChange - elif area == 'remotes': - # Tracking branch: - environment.log_warning( - '*** Push-update of tracking branch %r\n' - '*** - incomplete email generated.' - % (refname,) - ) - klass = OtherReferenceChange - else: - # Some other reference namespace: - environment.log_warning( - '*** Push-update of strange reference %r\n' - '*** - incomplete email generated.' - % (refname,) - ) - klass = OtherReferenceChange - else: - # Anything else (is there anything else?) - environment.log_warning( - '*** Unknown type of update to %r (%s)\n' - '*** - incomplete email generated.' - % (refname, rev.type,) - ) - klass = OtherReferenceChange - - return klass( - environment, - refname=refname, short_refname=short_refname, - old=old, new=new, rev=rev, - ) - - @staticmethod - def make_thread_index(): - """Return a string appropriate for the Thread-Index header, - needed by MS Outlook to get threading right. - - The format is (base64-encoded): - - 1 byte must be 1 - - 5 bytes encode a date (hardcoded here) - - 16 bytes for a globally unique identifier - - FIXME: Unfortunately, even with the Thread-Index field, MS - Outlook doesn't seem to do the threading reliably (see - https://github.com/git-multimail/git-multimail/pull/194). - """ - thread_index = b'\x01\x00\x00\x12\x34\x56' + uuid.uuid4().bytes - return base64.standard_b64encode(thread_index).decode('ascii') - - def __init__(self, environment, refname, short_refname, old, new, rev): - Change.__init__(self, environment) - self.change_type = { - (False, True): 'create', - (True, True): 'update', - (True, False): 'delete', - }[bool(old), bool(new)] - self.refname = refname - self.short_refname = short_refname - self.old = old - self.new = new - self.rev = rev - self.msgid = make_msgid() - self.thread_index = self.make_thread_index() - 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) - - values['change_type'] = self.change_type - values['refname_type'] = self.refname_type - values['refname'] = self.refname - values['short_refname'] = self.short_refname - values['msgid'] = self.msgid - values['thread_index'] = self.thread_index - values['recipients'] = self.recipients - values['oldrev'] = str(self.old) - values['oldrev_short'] = self.old.short - values['newrev'] = str(self.new) - values['newrev_short'] = self.new.short - - if self.old: - values['oldrev_type'] = self.old.type - if self.new: - values['newrev_type'] = self.new.type - - reply_to = self.environment.get_reply_to_refchange(self) - if reply_to: - values['reply_to'] = reply_to - - return values - - def 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, - }[self.change_type] - return self.expand(template) - - def generate_email_header(self, **extra_values): - if 'subject' not in extra_values: - extra_values['subject'] = self.get_subject() - - for line in self.expand_header_lines( - self.header_template, **extra_values - ): - yield line - - def generate_email_intro(self, html_escape_val=False): - for line in self.expand_lines(self.intro_template, - html_escape_val=html_escape_val): - yield line - - def generate_email_body(self, push): - """Call the appropriate body-generation routine. - - Call one of generate_create_summary() / - generate_update_summary() / generate_delete_summary().""" - - change_summary = { - 'create': self.generate_create_summary, - 'delete': self.generate_delete_summary, - 'update': self.generate_update_summary, - }[self.change_type](push) - for line in change_summary: - yield line - - for line in self.generate_revision_change_summary(push): - yield line - - def generate_email_footer(self, html_escape_val): - return self.expand_lines(self.footer_template, - html_escape_val=html_escape_val) - - def generate_revision_change_graph(self, push): - if self.showgraph: - 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: - yield '\n' - yield 'Detailed log of new commits:\n\n' - for line in read_git_lines( - ['log', '--no-walk'] + - self.logopts + - new_commits_list + - ['--'], - keepends=True, - ): - yield line - - def generate_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.""" - - if self.new.commit_sha1 and not self.old.commit_sha1: - # A new reference was created. List the new revisions - # brought by the new reference (i.e., those revisions that - # were not in the repository before this reference - # change). - sha1s = list(push.get_new_commits(self)) - sha1s.reverse() - tot = len(sha1s) - new_revisions = [ - Revision(self, GitObject(sha1), num=i + 1, tot=tot) - for (i, sha1) in enumerate(sha1s) - ] - - if new_revisions: - yield self.expand('This %(refname_type)s includes the following new commits:\n') - yield '\n' - for r in new_revisions: - (sha1, subject) = r.rev.get_summary() - yield r.expand( - BRIEF_SUMMARY_TEMPLATE, action='new', text=subject, - ) - yield '\n' - for line in self.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): - yield line - - elif self.new.commit_sha1 and self.old.commit_sha1: - # A reference was changed to point at a different commit. - # List the revisions that were removed and/or added *from - # that reference* by this reference change, along with a - # diff between the trees for its old and new values. - - # List of the revisions that were added to the branch by - # this update. Note this list can include revisions that - # have already had notification emails; we want such - # revisions in the summary even though we will not send - # new notification emails for them. - adds = list(generate_summaries( - '--topo-order', '--reverse', '%s..%s' - % (self.old.commit_sha1, self.new.commit_sha1,) - )) - - # List of the revisions that were removed from the branch - # by this update. This will be empty except for - # non-fast-forward updates. - discards = list(generate_summaries( - '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,) - )) - - if adds: - new_commits_list = push.get_new_commits(self) - else: - new_commits_list = [] - new_commits = CommitSet(new_commits_list) - - if discards: - discarded_commits = CommitSet(push.get_discarded_commits(self)) - else: - discarded_commits = CommitSet([]) - - if discards and adds: - for (sha1, subject) in discards: - if sha1 in discarded_commits: - action = 'discard' - else: - action = 'omit' - yield self.expand( - BRIEF_SUMMARY_TEMPLATE, action=action, - rev_short=sha1, text=subject, - ) - for (sha1, subject) in adds: - if sha1 in new_commits: - action = 'new' - else: - action = 'add' - yield self.expand( - BRIEF_SUMMARY_TEMPLATE, action=action, - rev_short=sha1, text=subject, - ) - yield '\n' - for line in self.expand_lines(NON_FF_TEMPLATE): - yield line - - elif discards: - for (sha1, subject) in discards: - if sha1 in discarded_commits: - action = 'discard' - else: - action = 'omit' - yield self.expand( - BRIEF_SUMMARY_TEMPLATE, action=action, - rev_short=sha1, text=subject, - ) - yield '\n' - for line in self.expand_lines(REWIND_ONLY_TEMPLATE): - yield line - - elif adds: - (sha1, subject) = self.old.get_summary() - yield self.expand( - BRIEF_SUMMARY_TEMPLATE, action='from', - rev_short=sha1, text=subject, - ) - for (sha1, subject) in adds: - if sha1 in new_commits: - action = 'new' - else: - action = 'add' - yield self.expand( - BRIEF_SUMMARY_TEMPLATE, action=action, - rev_short=sha1, text=subject, - ) - - yield '\n' - - if new_commits: - 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 - # this change. There's no point showing the stat from the - # base to the new revision because the base is effectively a - # random revision at this point - the user will be interested - # in what this revision changed - including the undoing of - # previous revisions in the case of non-fast-forward updates. - yield '\n' - yield 'Summary of changes:\n' - for line in read_git_lines( - ['diff-tree'] + - self.diffopts + - ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)], - keepends=True, - ): - yield line - - elif self.old.commit_sha1 and not self.new.commit_sha1: - # A reference was deleted. List the revisions that were - # removed from the repository by this reference change. - - sha1s = list(push.get_discarded_commits(self)) - tot = len(sha1s) - discarded_revisions = [ - Revision(self, GitObject(sha1), num=i + 1, tot=tot) - for (i, sha1) in enumerate(sha1s) - ] - - if discarded_revisions: - for line in self.expand_lines(DISCARDED_REVISIONS_TEMPLATE): - yield line - yield '\n' - for r in discarded_revisions: - (sha1, subject) = r.rev.get_summary() - yield r.expand( - BRIEF_SUMMARY_TEMPLATE, action='discard', 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 - - elif not self.old.commit_sha1 and not self.new.commit_sha1: - for line in self.expand_lines(NON_COMMIT_UPDATE_TEMPLATE): - yield line - - def generate_create_summary(self, push): - """Called for the creation of a reference.""" - - # This is a new reference and so oldrev is not valid - (sha1, subject) = self.new.get_summary() - yield self.expand( - BRIEF_SUMMARY_TEMPLATE, action='at', - rev_short=sha1, text=subject, - ) - yield '\n' - - def generate_update_summary(self, push): - """Called for the change of a pre-existing branch.""" - - return iter([]) - - def generate_delete_summary(self, push): - """Called for the deletion of any type of reference.""" - - (sha1, subject) = self.old.get_summary() - yield self.expand( - BRIEF_SUMMARY_TEMPLATE, action='was', - rev_short=sha1, text=subject, - ) - yield '\n' - - def get_specific_fromaddr(self): - return self.environment.from_refchange - - -class BranchChange(ReferenceChange): - refname_type = 'branch' - - def __init__(self, environment, refname, short_refname, old, new, rev): - ReferenceChange.__init__( - self, environment, - refname=refname, short_refname=short_refname, - old=old, new=new, rev=rev, - ) - self.recipients = environment.get_refchange_recipients(self) - 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._contains_diff() - self.header_template = COMBINED_HEADER_TEMPLATE - self.intro_template = COMBINED_INTRO_TEMPLATE - self.footer_template = COMBINED_FOOTER_TEMPLATE - - def revision_gen_link(base_url): - # revision is used only to generate the body, and - # _content_type is set while generating headers. Get it - # from the BranchChange object. - revision._content_type = self._content_type - return revision.generate_browse_link(base_url) - self.generate_browse_link = revision_gen_link - for line in self.generate_email(push, body_filter, values): - yield line - - 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): - refname_type = 'annotated tag' - - def __init__(self, environment, refname, short_refname, old, new, rev): - ReferenceChange.__init__( - self, environment, - refname=refname, short_refname=short_refname, - old=old, new=new, rev=rev, - ) - self.recipients = environment.get_announce_recipients(self) - self.show_shortlog = environment.announce_show_shortlog - - ANNOTATED_TAG_FORMAT = ( - '%(*objectname)\n' - '%(*objecttype)\n' - '%(taggername)\n' - '%(taggerdate)' - ) - - def describe_tag(self, push): - """Describe the new value of an annotated tag.""" - - # Use git for-each-ref to pull out the individual fields from - # the tag - [tagobject, tagtype, tagger, tagged] = read_git_lines( - ['for-each-ref', '--format=%s' % (self.ANNOTATED_TAG_FORMAT,), self.refname], - ) - - yield self.expand( - BRIEF_SUMMARY_TEMPLATE, action='tagging', - rev_short=tagobject, text='(%s)' % (tagtype,), - ) - if tagtype == 'commit': - # If the tagged object is a commit, then we assume this is a - # release, and so we calculate which tag this tag is - # replacing - try: - prevtag = read_git_output(['describe', '--abbrev=0', '%s^' % (self.new,)]) - except CommandError: - prevtag = None - if prevtag: - yield ' replaces %s\n' % (prevtag,) - else: - prevtag = None - yield ' length %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),) - - yield ' by %s\n' % (tagger,) - yield ' on %s\n' % (tagged,) - yield '\n' - - # Show the content of the tag message; this might contain a - # change log or release notes so is worth displaying. - yield LOGBEGIN - contents = list(read_git_lines(['cat-file', 'tag', self.new.sha1], keepends=True)) - contents = contents[contents.index('\n') + 1:] - if contents and contents[-1][-1:] != '\n': - contents.append('\n') - for line in contents: - yield line - - if self.show_shortlog and tagtype == 'commit': - # Only commit tags make sense to have rev-list operations - # performed on them - yield '\n' - if prevtag: - # Show changes since the previous release - revlist = read_git_output( - ['rev-list', '--pretty=short', '%s..%s' % (prevtag, self.new,)], - keepends=True, - ) - else: - # No previous tag, show all the changes since time - # began - revlist = read_git_output( - ['rev-list', '--pretty=short', '%s' % (self.new,)], - keepends=True, - ) - for line in read_git_lines(['shortlog'], input=revlist, keepends=True): - yield line - - yield LOGEND - yield '\n' - - def generate_create_summary(self, push): - """Called for the creation of an annotated tag.""" - - for line in self.expand_lines(TAG_CREATED_TEMPLATE): - yield line - - for line in self.describe_tag(push): - yield line - - def generate_update_summary(self, push): - """Called for the update of an annotated tag. - - This is probably a rare event and may not even be allowed.""" - - for line in self.expand_lines(TAG_UPDATED_TEMPLATE): - yield line - - for line in self.describe_tag(push): - yield line - - def generate_delete_summary(self, push): - """Called when a non-annotated reference is updated.""" - - for line in self.expand_lines(TAG_DELETED_TEMPLATE): - yield line - - yield self.expand(' tag was %(oldrev_short)s\n') - yield '\n' - - -class NonAnnotatedTagChange(ReferenceChange): - refname_type = 'tag' - - def __init__(self, environment, refname, short_refname, old, new, rev): - ReferenceChange.__init__( - self, environment, - refname=refname, short_refname=short_refname, - old=old, new=new, rev=rev, - ) - self.recipients = environment.get_refchange_recipients(self) - - def generate_create_summary(self, push): - """Called for the creation of an annotated tag.""" - - for line in self.expand_lines(TAG_CREATED_TEMPLATE): - yield line - - def generate_update_summary(self, push): - """Called when a non-annotated reference is updated.""" - - for line in self.expand_lines(TAG_UPDATED_TEMPLATE): - yield line - - def generate_delete_summary(self, push): - """Called when a non-annotated reference is updated.""" - - for line in self.expand_lines(TAG_DELETED_TEMPLATE): - yield line - - for line in ReferenceChange.generate_delete_summary(self, push): - yield line - - -class OtherReferenceChange(ReferenceChange): - refname_type = 'reference' - - def __init__(self, environment, refname, short_refname, old, new, rev): - # We use the full refname as short_refname, because otherwise - # the full name of the reference would not be obvious from the - # text of the email. - ReferenceChange.__init__( - self, environment, - refname=refname, short_refname=refname, - old=old, new=new, rev=rev, - ) - self.recipients = environment.get_refchange_recipients(self) - - -class Mailer(object): - """An object that can send emails.""" - - def __init__(self, environment): - self.environment = environment - - def close(self): - pass - - def send(self, lines, to_addrs): - """Send an email consisting of lines. - - lines must be an iterable over the lines constituting the - header and body of the email. to_addrs is a list of recipient - addresses (can be needed even if lines already contains a - "To:" field). It can be either a string (comma-separated list - of email addresses) or a Python list of individual email - addresses. - - """ - - raise NotImplementedError() - - -class SendMailer(Mailer): - """Send emails using 'sendmail -oi -t'.""" - - SENDMAIL_CANDIDATES = [ - '/usr/sbin/sendmail', - '/usr/lib/sendmail', - ] - - @staticmethod - def find_sendmail(): - for path in SendMailer.SENDMAIL_CANDIDATES: - if os.access(path, os.X_OK): - return path - else: - raise ConfigurationException( - 'No sendmail executable found. ' - 'Try setting multimailhook.sendmailCommand.' - ) - - def __init__(self, environment, command=None, envelopesender=None): - """Construct a SendMailer instance. - - command should be the command and arguments used to invoke - sendmail, as a list of strings. If an envelopesender is - provided, it will also be passed to the command, via '-f - envelopesender'.""" - super(SendMailer, self).__init__(environment) - if command: - self.command = command[:] - else: - self.command = [self.find_sendmail(), '-oi', '-t'] - - if envelopesender: - self.command.extend(['-f', envelopesender]) - - def send(self, lines, to_addrs): - try: - p = subprocess.Popen(self.command, stdin=subprocess.PIPE) - except OSError: - self.environment.get_logger().error( - '*** Cannot execute command: %s\n' % ' '.join(self.command) + - '*** %s\n' % sys.exc_info()[1] + - '*** Try setting multimailhook.mailer to "smtp"\n' + - '*** to send emails without using the sendmail command.\n' - ) - sys.exit(1) - try: - lines = (str_to_bytes(line) for line in lines) - p.stdin.writelines(lines) - except Exception: - self.environment.get_logger().error( - '*** Error while generating commit email\n' - '*** - mail sending aborted.\n' - ) - if hasattr(p, 'terminate'): - # subprocess.terminate() is not available in Python 2.4 - p.terminate() - else: - import signal - os.kill(p.pid, signal.SIGTERM) - raise - else: - p.stdin.close() - retcode = p.wait() - if retcode: - raise CommandError(self.command, retcode) - - -class SMTPMailer(Mailer): - """Send emails using Python's smtplib.""" - - def __init__(self, environment, - envelopesender, smtpserver, - smtpservertimeout=10.0, smtpserverdebuglevel=0, - smtpencryption='none', - smtpuser='', smtppass='', - smtpcacerts='' - ): - super(SMTPMailer, self).__init__(environment) - if not envelopesender: - self.environment.get_logger().error( - 'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n' - 'please set either multimailhook.envelopeSender or user.email\n' - ) - 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 - self.smtpcacerts = smtpcacerts - self.loggedin = False - try: - 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': - if self.smtpcacerts: - raise smtplib.SMTPException( - "Checking certificate is not supported for ssl, prefer starttls" - ) - self.smtp = call(smtplib.SMTP_SSL, self.smtpserver, timeout=self.smtpservertimeout) - elif self.security == 'tls': - if 'ssl' not in sys.modules: - self.environment.get_logger().error( - '*** Your Python version does not have the ssl library installed\n' - '*** smtpEncryption=tls is not available.\n' - '*** Either upgrade Python to 2.6 or later\n' - ' or use git_multimail.py version 1.2.\n') - if ':' not in self.smtpserver: - self.smtpserver += ':587' # default port for TLS - self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout) - # start: ehlo + starttls - # equivalent to - # self.smtp.ehlo() - # self.smtp.starttls() - # with access to the ssl layer - self.smtp.ehlo() - if not self.smtp.has_extn("starttls"): - raise smtplib.SMTPException("STARTTLS extension not supported by server") - resp, reply = self.smtp.docmd("STARTTLS") - if resp != 220: - raise smtplib.SMTPException("Wrong answer to the STARTTLS command") - if self.smtpcacerts: - self.smtp.sock = ssl.wrap_socket( - self.smtp.sock, - ca_certs=self.smtpcacerts, - cert_reqs=ssl.CERT_REQUIRED - ) - else: - self.smtp.sock = ssl.wrap_socket( - self.smtp.sock, - cert_reqs=ssl.CERT_NONE - ) - self.environment.get_logger().error( - '*** Warning, the server certificate is not verified (smtp) ***\n' - '*** set the option smtpCACerts ***\n' - ) - if not hasattr(self.smtp.sock, "read"): - # using httplib.FakeSocket with Python 2.5.x or earlier - self.smtp.sock.read = self.smtp.sock.recv - self.smtp.file = smtplib.SSLFakeFile(self.smtp.sock) - self.smtp.helo_resp = None - self.smtp.ehlo_resp = None - self.smtp.esmtp_features = {} - self.smtp.does_esmtp = 0 - # end: ehlo + starttls - self.smtp.ehlo() - else: - sys.stdout.write('*** Error: Control reached an invalid option. ***') - 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: - self.environment.get_logger().error( - '*** Error establishing SMTP connection to %s ***\n' - '*** %s\n' - % (self.smtpserver, sys.exc_info()[1])) - sys.exit(1) - - def close(self): - if hasattr(self, 'smtp'): - self.smtp.quit() - del self.smtp - - def __del__(self): - self.close() - - def send(self, lines, to_addrs): - try: - if self.username or self.password: - if not self.loggedin: - self.smtp.login(self.username, self.password) - self.loggedin = True - msg = ''.join(lines) - # turn comma-separated list into Python list if needed. - if is_string(to_addrs): - to_addrs = [email for (name, email) in getaddresses([to_addrs])] - self.smtp.sendmail(self.envelopesender, to_addrs, msg) - except socket.timeout: - self.environment.get_logger().error( - '*** Error sending email ***\n' - '*** SMTP server timed out (timeout is %s)\n' - % self.smtpservertimeout) - except smtplib.SMTPResponseException: - err = sys.exc_info()[1] - self.environment.get_logger().error( - '*** Error sending email ***\n' - '*** Error %d: %s\n' - % (err.smtp_code, bytes_to_str(err.smtp_error))) - try: - smtp = self.smtp - # delete the field before quit() so that in case of - # error, self.smtp is deleted anyway. - del self.smtp - smtp.quit() - except: - self.environment.get_logger().error( - '*** Error closing the SMTP connection ***\n' - '*** Exiting anyway ... ***\n' - '*** %s\n' % sys.exc_info()[1]) - sys.exit(1) - - -class OutputMailer(Mailer): - """Write emails to an output stream, bracketed by lines of '=' characters. - - This is intended for debugging purposes.""" - - SEPARATOR = '=' * 75 + '\n' - - def __init__(self, f, environment=None): - super(OutputMailer, self).__init__(environment=environment) - self.f = f - - def send(self, lines, to_addrs): - write_str(self.f, self.SEPARATOR) - for line in lines: - write_str(self.f, line) - write_str(self.f, self.SEPARATOR) - - -def get_git_dir(): - """Determine GIT_DIR. - - Determine GIT_DIR either from the GIT_DIR environment variable or - from the working directory, using Git's usual rules.""" - - try: - return read_git_output(['rev-parse', '--git-dir']) - except CommandError: - sys.stderr.write('fatal: git_multimail: not in a git directory\n') - sys.exit(1) - - -class Environment(object): - """Describes the environment in which the push is occurring. - - An Environment object encapsulates information about the local - environment. For example, it knows how to determine: - - * the name of the repository to which the push occurred - - * what user did the push - - * what users want to be informed about various types of changes. - - An Environment object is expected to have the following methods: - - get_repo_shortname() - - Return a short name for the repository, for display - purposes. - - get_repo_path() - - Return the absolute path to the Git repository. - - get_emailprefix() - - Return a string that will be prefixed to every email's - subject. - - get_pusher() - - Return the username of the person who pushed the changes. - This value is used in the email body to indicate who - pushed the change. - - get_pusher_email() (may return None) - - Return the email address of the person who pushed the - changes. The value should be a single RFC 2822 email - address as a string; e.g., "Joe User <user@example.com>" - if available, otherwise "user@example.com". If set, the - value is used as the Reply-To address for refchange - emails. If it is impossible to determine the pusher's - email, this attribute should be set to None (in which case - no Reply-To header will be output). - - get_sender() - - Return the address to be used as the 'From' email address - in the email envelope. - - get_fromaddr(change=None) - - Return the 'From' email address used in the email 'From:' - headers. If the change is known when this function is - called, it is passed in as the 'change' parameter. (May - be a full RFC 2822 email address like 'Joe User - <user@example.com>'.) - - get_administrator() - - Return the name and/or email of the repository - administrator. This value is used in the footer as the - person to whom requests to be removed from the - notification list should be sent. Ideally, it should - include a valid email address. - - get_reply_to_refchange() - get_reply_to_commit() - - Return the address to use in the email "Reply-To" header, - as a string. These can be an RFC 2822 email address, or - None to omit the "Reply-To" header. - get_reply_to_refchange() is used for refchange emails; - get_reply_to_commit() is used for individual commit - emails. - - get_ref_filter_regex() - - Return a tuple -- a compiled regex, and a boolean indicating - whether the regex picks refs to include (if False, the regex - matches on refs to exclude). - - get_default_ref_ignore_regex() - - Return a regex that should be ignored for both what emails - to send and when computing what commits are considered new - to the repository. Default is "^refs/notes/". - - get_max_subject_length() - - Return an int giving the maximal length for the subject - (git log --oneline). - - They should also define the following attributes: - - announce_show_shortlog (bool) - - True iff announce emails should include a shortlog. - - commit_email_format (string) - - If "html", generate commit emails in HTML instead of plain text - used by default. - - html_in_intro (bool) - html_in_footer (bool) - - When generating HTML emails, the introduction (respectively, - the footer) will be HTML-escaped iff html_in_intro (respectively, - the footer) is true. When false, only the values used to expand - the template are escaped. - - refchange_showgraph (bool) - - True iff refchanges emails should include a detailed graph. - - refchange_showlog (bool) - - True iff refchanges emails should include a detailed log. - - diffopts (list of strings) - - The options that should be passed to 'git diff' for the - summary email. The value should be a list of strings - representing words to be passed to the command. - - 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 - 'git log' when generating the detailed log for a set of - commits (see refchange_showlog) - - commitlogopts (list of strings) - - The options that should be passed to 'git log' for each - commit mail. The value should be a list of strings - representing words to be passed to the command. - - date_substitute (string) - - String to be used in substitution for 'Date:' at start of - line in the output of 'git log'. - - 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. - - from_refchange, from_commit (strings) - - Addresses to use for the From: field for refchange emails - and commit emails respectively. Set from - multimailhook.fromRefchange and multimailhook.fromCommit - by ConfigEnvironmentMixin. - - log_file, error_log_file, debug_log_file (string) - - Name of a file to which logs should be sent. - - verbose (int) - - How verbose the system should be. - - 0 (default): show info, errors, ... - - 1 : show basic debug info - """ - - REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$') - - def __init__(self, osenv=None): - self.osenv = osenv or os.environ - self.announce_show_shortlog = False - self.commit_email_format = "text" - self.html_in_intro = False - self.html_in_footer = False - self.commitBrowseURL = None - self.maxcommitemails = 500 - self.excludemergerevisions = False - 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.date_substitute = 'AuthorDate: ' - self.quiet = False - self.stdout = False - self.combine_when_single_commit = True - self.logger = None - - self.COMPUTED_KEYS = [ - 'administrator', - 'charset', - 'emailprefix', - 'pusher', - 'pusher_email', - 'repo_path', - 'repo_shortname', - 'sender', - ] - - self._values = None - - def get_logger(self): - """Get (possibly creates) the logger associated to this environment.""" - if self.logger is None: - self.logger = Logger(self) - return self.logger - - def get_repo_shortname(self): - """Use the last part of the repo path, with ".git" stripped off if present.""" - - basename = os.path.basename(os.path.abspath(self.get_repo_path())) - m = self.REPO_NAME_RE.match(basename) - if m: - return m.group('name') - else: - return basename - - def get_pusher(self): - raise NotImplementedError() - - def get_pusher_email(self): - return None - - def get_fromaddr(self, change=None): - 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' - - def get_emailprefix(self): - return '' - - def get_repo_path(self): - if read_git_output(['rev-parse', '--is-bare-repository']) == 'true': - path = get_git_dir() - else: - path = read_git_output(['rev-parse', '--show-toplevel']) - return os.path.abspath(path) - - def get_charset(self): - return CHARSET - - def get_values(self): - """Return a dictionary {keyword: expansion} for this Environment. - - This method is called by Change._compute_values(). The keys - in the returned dictionary are available to be used in any of - the templates. The dictionary is created by calling - self.get_NAME() for each of the attributes named in - COMPUTED_KEYS and recording those that do not return None. - The return value is always a new dictionary.""" - - if self._values is None: - values = {'': ''} # %()s expands to the empty string. - - for key in self.COMPUTED_KEYS: - value = getattr(self, 'get_%s' % (key,))() - if value is not None: - values[key] = value - - self._values = values - - return self._values.copy() - - def get_refchange_recipients(self, refchange): - """Return the recipients for notifications about refchange. - - Return the list of email addresses to which notifications - about the specified ReferenceChange should be sent.""" - - raise NotImplementedError() - - def get_announce_recipients(self, annotated_tag_change): - """Return the recipients for notifications about annotated_tag_change. - - Return the list of email addresses to which notifications - about the specified AnnotatedTagChange should be sent.""" - - raise NotImplementedError() - - def get_reply_to_refchange(self, refchange): - return self.get_pusher_email() - - def get_revision_recipients(self, revision): - """Return the recipients for messages about revision. - - Return the list of email addresses to which notifications - about the specified Revision should be sent. This method - could be overridden, for example, to take into account the - contents of the revision when deciding whom to notify about - it. For example, there could be a scheme for users to express - interest in particular files or subdirectories, and only - receive notification emails for revisions that affecting those - files.""" - - raise NotImplementedError() - - def get_reply_to_commit(self, revision): - return revision.author - - def get_default_ref_ignore_regex(self): - # The commit messages of git notes are essentially meaningless - # and "filenames" in git notes commits are an implementational - # detail that might surprise users at first. As such, we - # would need a completely different method for handling emails - # of git notes in order for them to be of benefit for users, - # which we simply do not have right now. - return "^refs/notes/" - - def get_max_subject_length(self): - """Return the maximal subject line (git log --oneline) length. - Longer subject lines will be truncated.""" - raise NotImplementedError() - - def filter_body(self, lines): - """Filter the lines intended for an email body. - - lines is an iterable over the lines that would go into the - email body. Filter it (e.g., limit the number of lines, the - line length, character set, etc.), returning another iterable. - See FilterLinesEnvironmentMixin and MaxlinesEnvironmentMixin - for classes implementing this functionality.""" - - return lines - - 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.""" - self.get_logger().info(msg) - - def log_warning(self, msg): - """Write the string msg on a log file or on stderr. - - Sends the text to stderr by default, override to change the behavior.""" - self.get_logger().warning(msg) - - def log_error(self, msg): - """Write the string msg on a log file or on stderr. - - Sends the text to stderr by default, override to change the behavior.""" - self.get_logger().error(msg) - - def check(self): - pass - - -class ConfigEnvironmentMixin(Environment): - """A mixin that sets self.config to its constructor's config argument. - - This class's constructor consumes the "config" argument. - - Mixins that need to inspect the config should inherit from this - class (1) to make sure that "config" is still in the constructor - arguments with its own constructor runs and/or (2) to be sure that - self.config is set after construction.""" - - def __init__(self, config, **kw): - super(ConfigEnvironmentMixin, self).__init__(**kw) - self.config = config - - -class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): - """An Environment that reads most of its information from "git config".""" - - @staticmethod - def forbid_field_values(name, value, forbidden): - for forbidden_val in forbidden: - if value is not None and value.lower() == forbidden: - raise ConfigurationException( - '"%s" is not an allowed setting for %s' % (value, name) - ) - - def __init__(self, config, **kw): - super(ConfigOptionsEnvironmentMixin, self).__init__( - config=config, **kw - ) - - 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) - - commit_email_format = config.get('commitEmailFormat') - if commit_email_format is not None: - if commit_email_format != "html" and commit_email_format != "text": - self.log_warning( - '*** Unknown value for multimailhook.commitEmailFormat: %s\n' % - commit_email_format + - '*** Expected either "text" or "html". Ignoring.\n' - ) - else: - self.commit_email_format = commit_email_format - - html_in_intro = config.get_bool('htmlInIntro') - if html_in_intro is not None: - self.html_in_intro = html_in_intro - - html_in_footer = config.get_bool('htmlInFooter') - if html_in_footer is not None: - self.html_in_footer = html_in_footer - - self.commitBrowseURL = config.get('commitBrowseURL') - - self.excludemergerevisions = config.get('excludeMergeRevisions') - - maxcommitemails = config.get('maxcommitemails') - if maxcommitemails is not None: - try: - self.maxcommitemails = int(maxcommitemails) - except ValueError: - self.log_warning( - '*** Malformed value for multimailhook.maxCommitEmails: %s\n' - % maxcommitemails + - '*** Expected a number. Ignoring.\n' - ) - - diffopts = config.get('diffopts') - if diffopts is not None: - self.diffopts = shlex.split(diffopts) - - 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) - - commitlogopts = config.get('commitlogopts') - if commitlogopts is not None: - self.commitlogopts = shlex.split(commitlogopts) - - date_substitute = config.get('dateSubstitute') - if date_substitute == 'none': - self.date_substitute = None - elif date_substitute is not None: - self.date_substitute = date_substitute - - reply_to = config.get('replyTo') - self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to) - self.forbid_field_values('replyToRefchange', - self.__reply_to_refchange, - ['author']) - self.__reply_to_commit = config.get('replyToCommit', default=reply_to) - - self.from_refchange = config.get('fromRefchange') - self.forbid_field_values('fromRefchange', - self.from_refchange, - ['author', 'none']) - self.from_commit = config.get('fromCommit') - self.forbid_field_values('fromCommit', - self.from_commit, - ['none']) - - combine = config.get_bool('combineWhenSingleCommit') - if combine is not None: - self.combine_when_single_commit = combine - - self.log_file = config.get('logFile', default=None) - self.error_log_file = config.get('errorLogFile', default=None) - self.debug_log_file = config.get('debugLogFile', default=None) - if config.get_bool('Verbose', default=False): - self.verbose = 1 - else: - self.verbose = 0 - - def get_administrator(self): - return ( - self.config.get('administrator') or - self.get_sender() or - super(ConfigOptionsEnvironmentMixin, self).get_administrator() - ) - - def get_repo_shortname(self): - return ( - self.config.get('reponame') or - super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname() - ) - - def get_emailprefix(self): - emailprefix = self.config.get('emailprefix') - if emailprefix is not None: - emailprefix = emailprefix.strip() - if emailprefix: - emailprefix += ' ' - else: - emailprefix = '[%(repo_shortname)s] ' - short_name = self.get_repo_shortname() - try: - return emailprefix % {'repo_shortname': short_name} - except: - self.get_logger().error( - '*** Invalid multimailhook.emailPrefix: %s\n' % emailprefix + - '*** %s\n' % sys.exc_info()[1] + - "*** Only the '%(repo_shortname)s' placeholder is allowed\n" - ) - raise ConfigurationException( - '"%s" is not an allowed setting for emailPrefix' % emailprefix - ) - - def get_sender(self): - return self.config.get('envelopesender') - - def process_addr(self, addr, change): - if addr.lower() == 'author': - if hasattr(change, 'author'): - return change.author - else: - return None - elif addr.lower() == 'pusher': - return self.get_pusher_email() - elif addr.lower() == 'none': - return None - else: - return addr - - def get_fromaddr(self, change=None): - fromaddr = self.config.get('from') - if change: - specific_fromaddr = change.get_specific_fromaddr() - if specific_fromaddr: - fromaddr = specific_fromaddr - if fromaddr: - fromaddr = self.process_addr(fromaddr, change) - if fromaddr: - return fromaddr - return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr(change) - - def get_reply_to_refchange(self, refchange): - if self.__reply_to_refchange is None: - return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_refchange(refchange) - else: - return self.process_addr(self.__reply_to_refchange, refchange) - - def get_reply_to_commit(self, revision): - if self.__reply_to_commit is None: - return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision) - else: - return self.process_addr(self.__reply_to_commit, revision) - - def get_scancommitforcc(self): - return self.config.get('scancommitforcc') - - -class FilterLinesEnvironmentMixin(Environment): - """Handle encoding and maximum line length of body lines. - - email_max_line_length (int or None) - - The maximum length of any single line in the email body. - Longer lines are truncated at that length with ' [...]' - appended. - - strict_utf8 (bool) - - If this field is set to True, then the email body text is - expected to be UTF-8. Any invalid characters are - converted to U+FFFD, the Unicode replacement character - (encoded as UTF-8, of course). - - """ - - def __init__(self, strict_utf8=True, - email_max_line_length=500, max_subject_length=500, - **kw): - super(FilterLinesEnvironmentMixin, self).__init__(**kw) - self.__strict_utf8 = strict_utf8 - self.__email_max_line_length = email_max_line_length - self.__max_subject_length = max_subject_length - - def filter_body(self, lines): - lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines) - if self.__strict_utf8: - if not PYTHON3: - lines = (line.decode(ENCODING, 'replace') for line in lines) - # Limit the line length in Unicode-space to avoid - # splitting characters: - if self.__email_max_line_length > 0: - lines = limit_linelength(lines, self.__email_max_line_length) - if not PYTHON3: - lines = (line.encode(ENCODING, 'replace') for line in lines) - elif self.__email_max_line_length: - lines = limit_linelength(lines, self.__email_max_line_length) - - return lines - - def get_max_subject_length(self): - return self.__max_subject_length - - -class ConfigFilterLinesEnvironmentMixin( - ConfigEnvironmentMixin, - FilterLinesEnvironmentMixin, - ): - """Handle encoding and maximum line length based on config.""" - - def __init__(self, config, **kw): - strict_utf8 = config.get_bool('emailstrictutf8', default=None) - if strict_utf8 is not None: - kw['strict_utf8'] = strict_utf8 - - email_max_line_length = config.get('emailmaxlinelength') - if email_max_line_length is not None: - kw['email_max_line_length'] = int(email_max_line_length) - - max_subject_length = config.get('subjectMaxLength', default=email_max_line_length) - if max_subject_length is not None: - kw['max_subject_length'] = int(max_subject_length) - - super(ConfigFilterLinesEnvironmentMixin, self).__init__( - config=config, **kw - ) - - -class MaxlinesEnvironmentMixin(Environment): - """Limit the email body to a specified number of lines.""" - - def __init__(self, emailmaxlines, **kw): - super(MaxlinesEnvironmentMixin, self).__init__(**kw) - self.__emailmaxlines = emailmaxlines - - def filter_body(self, lines): - lines = super(MaxlinesEnvironmentMixin, self).filter_body(lines) - if self.__emailmaxlines > 0: - lines = limit_lines(lines, self.__emailmaxlines) - return lines - - -class ConfigMaxlinesEnvironmentMixin( - ConfigEnvironmentMixin, - MaxlinesEnvironmentMixin, - ): - """Limit the email body to the number of lines specified in config.""" - - def __init__(self, config, **kw): - emailmaxlines = int(config.get('emailmaxlines', default='0')) - super(ConfigMaxlinesEnvironmentMixin, self).__init__( - config=config, - emailmaxlines=emailmaxlines, - **kw - ) - - -class FQDNEnvironmentMixin(Environment): - """A mixin that sets the host's FQDN to its constructor argument.""" - - def __init__(self, fqdn, **kw): - super(FQDNEnvironmentMixin, self).__init__(**kw) - self.COMPUTED_KEYS += ['fqdn'] - self.__fqdn = fqdn - - def get_fqdn(self): - """Return the fully-qualified domain name for this host. - - Return None if it is unavailable or unwanted.""" - - return self.__fqdn - - -class ConfigFQDNEnvironmentMixin( - ConfigEnvironmentMixin, - FQDNEnvironmentMixin, - ): - """Read the FQDN from the config.""" - - def __init__(self, config, **kw): - fqdn = config.get('fqdn') - super(ConfigFQDNEnvironmentMixin, self).__init__( - config=config, - fqdn=fqdn, - **kw - ) - - -class ComputeFQDNEnvironmentMixin(FQDNEnvironmentMixin): - """Get the FQDN by calling socket.getfqdn().""" - - def __init__(self, **kw): - super(ComputeFQDNEnvironmentMixin, self).__init__( - fqdn=socket.getfqdn(), - **kw - ) - - -class PusherDomainEnvironmentMixin(ConfigEnvironmentMixin): - """Deduce pusher_email from pusher by appending an emaildomain.""" - - def __init__(self, **kw): - super(PusherDomainEnvironmentMixin, self).__init__(**kw) - self.__emaildomain = self.config.get('emaildomain') - - def get_pusher_email(self): - if self.__emaildomain: - # Derive the pusher's full email address in the default way: - return '%s@%s' % (self.get_pusher(), self.__emaildomain) - else: - return super(PusherDomainEnvironmentMixin, self).get_pusher_email() - - -class StaticRecipientsEnvironmentMixin(Environment): - """Set recipients statically based on constructor parameters.""" - - def __init__( - self, - refchange_recipients, announce_recipients, revision_recipients, scancommitforcc, - **kw - ): - super(StaticRecipientsEnvironmentMixin, self).__init__(**kw) - - # The recipients for various types of notification emails, as - # RFC 2822 email addresses separated by commas (or the empty - # string if no recipients are configured). Although there is - # a mechanism to choose the recipient lists based on on the - # actual *contents* of the change being reported, we only - # choose based on the *type* of the change. Therefore we can - # compute them once and for all: - self.__refchange_recipients = refchange_recipients - self.__announce_recipients = announce_recipients - self.__revision_recipients = revision_recipients - - def check(self): - if not (self.get_refchange_recipients(None) or - self.get_announce_recipients(None) or - self.get_revision_recipients(None) or - self.get_scancommitforcc()): - raise ConfigurationException('No email recipients configured!') - super(StaticRecipientsEnvironmentMixin, self).check() - - def get_refchange_recipients(self, refchange): - if self.__refchange_recipients is None: - return super(StaticRecipientsEnvironmentMixin, - self).get_refchange_recipients(refchange) - return self.__refchange_recipients - - def get_announce_recipients(self, annotated_tag_change): - if self.__announce_recipients is None: - return super(StaticRecipientsEnvironmentMixin, - self).get_refchange_recipients(annotated_tag_change) - return self.__announce_recipients - - def get_revision_recipients(self, revision): - if self.__revision_recipients is None: - return super(StaticRecipientsEnvironmentMixin, - self).get_refchange_recipients(revision) - return self.__revision_recipients - - -class CLIRecipientsEnvironmentMixin(Environment): - """Mixin storing recipients information coming from the - command-line.""" - - def __init__(self, cli_recipients=None, **kw): - super(CLIRecipientsEnvironmentMixin, self).__init__(**kw) - self.__cli_recipients = cli_recipients - - def get_refchange_recipients(self, refchange): - if self.__cli_recipients is None: - return super(CLIRecipientsEnvironmentMixin, - self).get_refchange_recipients(refchange) - return self.__cli_recipients - - def get_announce_recipients(self, annotated_tag_change): - if self.__cli_recipients is None: - return super(CLIRecipientsEnvironmentMixin, - self).get_announce_recipients(annotated_tag_change) - return self.__cli_recipients - - def get_revision_recipients(self, revision): - if self.__cli_recipients is None: - return super(CLIRecipientsEnvironmentMixin, - self).get_revision_recipients(revision) - return self.__cli_recipients - - -class ConfigRecipientsEnvironmentMixin( - ConfigEnvironmentMixin, - StaticRecipientsEnvironmentMixin - ): - """Determine recipients statically based on config.""" - - def __init__(self, config, **kw): - super(ConfigRecipientsEnvironmentMixin, self).__init__( - config=config, - refchange_recipients=self._get_recipients( - config, 'refchangelist', 'mailinglist', - ), - announce_recipients=self._get_recipients( - config, 'announcelist', 'refchangelist', 'mailinglist', - ), - revision_recipients=self._get_recipients( - config, 'commitlist', 'mailinglist', - ), - scancommitforcc=config.get('scancommitforcc'), - **kw - ) - - def _get_recipients(self, config, *names): - """Return the recipients for a particular type of message. - - Return the list of email addresses to which a particular type - of notification email should be sent, by looking at the config - value for "multimailhook.$name" for each of names. Use the - value from the first name that is configured. The return - value is a (possibly empty) string containing RFC 2822 email - addresses separated by commas. If no configuration could be - found, raise a ConfigurationException.""" - - for name in names: - lines = config.get_all(name) - if lines is not None: - lines = [line.strip() for line in lines] - # Single "none" is a special value equivalen to empty string. - if lines == ['none']: - lines = [''] - return ', '.join(lines) - else: - return '' - - -class StaticRefFilterEnvironmentMixin(Environment): - """Set branch filter statically based on constructor parameters.""" - - def __init__(self, ref_filter_incl_regex, ref_filter_excl_regex, - ref_filter_do_send_regex, ref_filter_dont_send_regex, - **kw): - super(StaticRefFilterEnvironmentMixin, self).__init__(**kw) - - if ref_filter_incl_regex and ref_filter_excl_regex: - raise ConfigurationException( - "Cannot specify both a ref inclusion and exclusion regex.") - self.__is_inclusion_filter = bool(ref_filter_incl_regex) - default_exclude = self.get_default_ref_ignore_regex() - if ref_filter_incl_regex: - ref_filter_regex = ref_filter_incl_regex - elif ref_filter_excl_regex: - ref_filter_regex = ref_filter_excl_regex + '|' + default_exclude - else: - ref_filter_regex = default_exclude - try: - self.__compiled_regex = re.compile(ref_filter_regex) - except Exception: - raise ConfigurationException( - 'Invalid Ref Filter Regex "%s": %s' % (ref_filter_regex, sys.exc_info()[1])) - - if ref_filter_do_send_regex and ref_filter_dont_send_regex: - raise ConfigurationException( - "Cannot specify both a ref doSend and dontSend regex.") - self.__is_do_send_filter = bool(ref_filter_do_send_regex) - if ref_filter_do_send_regex: - ref_filter_send_regex = ref_filter_do_send_regex - elif ref_filter_dont_send_regex: - ref_filter_send_regex = ref_filter_dont_send_regex - else: - ref_filter_send_regex = '.*' - self.__is_do_send_filter = True - try: - self.__send_compiled_regex = re.compile(ref_filter_send_regex) - except Exception: - raise ConfigurationException( - 'Invalid Ref Filter Regex "%s": %s' % - (ref_filter_send_regex, sys.exc_info()[1])) - - def get_ref_filter_regex(self, send_filter=False): - if send_filter: - return self.__send_compiled_regex, self.__is_do_send_filter - else: - return self.__compiled_regex, self.__is_inclusion_filter - - -class ConfigRefFilterEnvironmentMixin( - ConfigEnvironmentMixin, - StaticRefFilterEnvironmentMixin - ): - """Determine branch filtering statically based on config.""" - - def _get_regex(self, config, key): - """Get a list of whitespace-separated regex. The refFilter* config - variables are multivalued (hence the use of get_all), and we - allow each entry to be a whitespace-separated list (hence the - split on each line). The whole thing is glued into a single regex.""" - values = config.get_all(key) - if values is None: - return values - items = [] - for line in values: - for i in line.split(): - items.append(i) - if items == []: - return None - return '|'.join(items) - - def __init__(self, config, **kw): - super(ConfigRefFilterEnvironmentMixin, self).__init__( - config=config, - ref_filter_incl_regex=self._get_regex(config, 'refFilterInclusionRegex'), - ref_filter_excl_regex=self._get_regex(config, 'refFilterExclusionRegex'), - ref_filter_do_send_regex=self._get_regex(config, 'refFilterDoSendRegex'), - ref_filter_dont_send_regex=self._get_regex(config, 'refFilterDontSendRegex'), - **kw - ) - - -class ProjectdescEnvironmentMixin(Environment): - """Make a "projectdesc" value available for templates. - - By default, it is set to the first line of $GIT_DIR/description - (if that file is present and appears to be set meaningfully).""" - - def __init__(self, **kw): - super(ProjectdescEnvironmentMixin, self).__init__(**kw) - self.COMPUTED_KEYS += ['projectdesc'] - - def get_projectdesc(self): - """Return a one-line description of the project.""" - - git_dir = get_git_dir() - try: - projectdesc = open(os.path.join(git_dir, 'description')).readline().strip() - if projectdesc and not projectdesc.startswith('Unnamed repository'): - return projectdesc - except IOError: - pass - - return 'UNNAMED PROJECT' - - -class GenericEnvironmentMixin(Environment): - def get_pusher(self): - return self.osenv.get('USER', self.osenv.get('USERNAME', 'unknown user')) - - -class GitoliteEnvironmentHighPrecMixin(Environment): - def get_pusher(self): - return self.osenv.get('GL_USER', 'unknown user') - - -class GitoliteEnvironmentLowPrecMixin( - ConfigEnvironmentMixin, - Environment): - - def get_repo_shortname(self): - # The gitolite environment variable $GL_REPO is a pretty good - # repo_shortname (though it's probably not as good as a value - # the user might have explicitly put in his config). - return ( - self.osenv.get('GL_REPO', None) or - super(GitoliteEnvironmentLowPrecMixin, self).get_repo_shortname() - ) - - @staticmethod - def _compile_regex(re_template): - return ( - re.compile(re_template % x) - for x in ( - r'BEGIN\s+USER\s+EMAILS', - r'([^\s]+)\s+(.*)', - r'END\s+USER\s+EMAILS', - )) - - def get_fromaddr(self, change=None): - 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')) - - mailaddress_map = self.config.get('MailaddressMap') - # If relative, consider relative to GL_CONF: - if mailaddress_map: - mailaddress_map = os.path.join(os.path.dirname(GL_CONF), - mailaddress_map) - if os.path.isfile(mailaddress_map): - f = open(mailaddress_map, 'rU') - try: - # Leading '#' is optional - re_begin, re_user, re_end = self._compile_regex( - r'^(?:\s*#)?\s*%s\s*$') - for l in f: - l = l.rstrip('\n') - if re_begin.match(l) or re_end.match(l): - continue # Ignore these lines - m = re_user.match(l) - if m: - if m.group(1) == GL_USER: - return m.group(2) - else: - continue # Not this user, but not an error - raise ConfigurationException( - "Syntax error in mail address map.\n" - "Check file {}.\n" - "Line: {}".format(mailaddress_map, l)) - - finally: - f.close() - - if os.path.isfile(GL_CONF): - f = open(GL_CONF, 'rU') - try: - in_user_emails_section = False - re_begin, re_user, re_end = self._compile_regex( - r'^\s*#\s*%s\s*$') - 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 and m.group(1) == GL_USER: - return m.group(2) - finally: - f.close() - return super(GitoliteEnvironmentLowPrecMixin, self).get_fromaddr(change) - - -class IncrementalDateTime(object): - """Simple wrapper to give incremental date/times. - - Each call will result in a date/time a second later than the - previous call. This can be used to falsify email headers, to - increase the likelihood that email clients sort the emails - correctly.""" - - def __init__(self): - self.time = time.time() - self.next = self.__next__ # Python 2 backward compatibility - - def __next__(self): - formatted = formatdate(self.time, True) - self.time += 1 - return formatted - - -class StashEnvironmentHighPrecMixin(Environment): - def __init__(self, user=None, repo=None, **kw): - super(StashEnvironmentHighPrecMixin, - self).__init__(user=user, repo=repo, **kw) - self.__user = user - self.__repo = repo - - def get_pusher(self): - return re.match(r'(.*?)\s*<', self.__user).group(1) - - def get_pusher_email(self): - return self.__user - - -class StashEnvironmentLowPrecMixin(Environment): - def __init__(self, user=None, repo=None, **kw): - super(StashEnvironmentLowPrecMixin, self).__init__(**kw) - self.__repo = repo - self.__user = user - - def get_repo_shortname(self): - return self.__repo - - def get_fromaddr(self, change=None): - return self.__user - - -class GerritEnvironmentHighPrecMixin(Environment): - def __init__(self, project=None, submitter=None, update_method=None, **kw): - super(GerritEnvironmentHighPrecMixin, - self).__init__(submitter=submitter, project=project, **kw) - self.__project = project - self.__submitter = submitter - self.__update_method = update_method - "Make an 'update_method' value available for templates." - self.COMPUTED_KEYS += ['update_method'] - - def get_pusher(self): - if self.__submitter: - if self.__submitter.find('<') != -1: - # Submitter has a configured email, we transformed - # __submitter into an RFC 2822 string already. - return re.match(r'(.*?)\s*<', self.__submitter).group(1) - else: - # Submitter has no configured email, it's just his name. - return self.__submitter - else: - # If we arrive here, this means someone pushed "Submit" from - # the gerrit web UI for the CR (or used one of the programmatic - # APIs to do the same, such as gerrit review) and the - # merge/push was done by the Gerrit user. It was technically - # triggered by someone else, but sadly we have no way of - # determining who that someone else is at this point. - return 'Gerrit' # 'unknown user'? - - def get_pusher_email(self): - if self.__submitter: - return self.__submitter - else: - return super(GerritEnvironmentHighPrecMixin, self).get_pusher_email() - - def get_default_ref_ignore_regex(self): - default = super(GerritEnvironmentHighPrecMixin, self).get_default_ref_ignore_regex() - return default + '|^refs/changes/|^refs/cache-automerge/|^refs/meta/' - - def get_revision_recipients(self, revision): - # Merge commits created by Gerrit when users hit "Submit this patchset" - # in the Web UI (or do equivalently with REST APIs or the gerrit review - # command) are not something users want to see an individual email for. - # Filter them out. - committer = read_git_output(['log', '--no-walk', '--format=%cN', - revision.rev.sha1]) - if committer == 'Gerrit Code Review': - return [] - else: - return super(GerritEnvironmentHighPrecMixin, self).get_revision_recipients(revision) - - def get_update_method(self): - return self.__update_method - - -class GerritEnvironmentLowPrecMixin(Environment): - def __init__(self, project=None, submitter=None, **kw): - super(GerritEnvironmentLowPrecMixin, self).__init__(**kw) - self.__project = project - self.__submitter = submitter - - def get_repo_shortname(self): - return self.__project - - def get_fromaddr(self, change=None): - if self.__submitter and self.__submitter.find('<') != -1: - return self.__submitter - else: - return super(GerritEnvironmentLowPrecMixin, self).get_fromaddr(change) - - -class Push(object): - """Represent an entire push (i.e., a group of ReferenceChanges). - - It is easy to figure out what commits were added to a *branch* by - a Reference change: - - git rev-list change.old..change.new - - or removed from a *branch*: - - git rev-list change.new..change.old - - But it is not quite so trivial to determine which entirely new - commits were added to the *repository* by a push and which old - commits were discarded by a push. A big part of the job of this - class is to figure out these things, and to make sure that new - commits are only detailed once even if they were added to multiple - references. - - The first step is to determine the "other" references--those - unaffected by the current push. They are computed by 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 - - git rev-list other1 other2 other3 ... change1.old change2.old ... - - Where "changeN.old" is the old value of one of the references - affected by this push. - - The commits contained in the repository after this push are - - git rev-list other1 other2 other3 ... change1.new change2.new ... - - The commits added by this push are the difference between these - two sets, which can be written - - git rev-list \ - ^other1 ^other2 ... \ - ^change1.old ^change2.old ... \ - change1.new change2.new ... - - The commits removed by this push can be computed by - - git rev-list \ - ^other1 ^other2 ... \ - ^change1.new ^change2.new ... \ - change1.old change2.old ... - - The last point is that it is possible that other pushes are - occurring simultaneously to this one, so reference values can - change at any time. It is impossible to eliminate all race - conditions, but we reduce the window of time during which problems - can occur by translating reference names to SHA1s as soon as - possible and working with SHA1s thereafter (because SHA1s are - immutable).""" - - # A map {(changeclass, changetype): integer} specifying the order - # that reference changes will be processed if multiple reference - # changes are included in a single push. The order is significant - # mostly because new commit notifications are threaded together - # with the first reference change that includes the commit. The - # following order thus causes commits to be grouped with branch - # changes (as opposed to tag changes) if possible. - SORT_ORDER = dict( - (value, i) for (i, value) in enumerate([ - (BranchChange, 'update'), - (BranchChange, 'create'), - (AnnotatedTagChange, 'update'), - (AnnotatedTagChange, 'create'), - (NonAnnotatedTagChange, 'update'), - (NonAnnotatedTagChange, 'create'), - (BranchChange, 'delete'), - (AnnotatedTagChange, 'delete'), - (NonAnnotatedTagChange, 'delete'), - (OtherReferenceChange, 'update'), - (OtherReferenceChange, 'create'), - (OtherReferenceChange, 'delete'), - ]) - ) - - def __init__(self, environment, changes, ignore_other_refs=False): - self.changes = sorted(changes, key=self._sort_key) - self.__other_ref_sha1s = None - self.__cached_commits_spec = {} - self.environment = environment - - if ignore_other_refs: - self.__other_ref_sha1s = set() - - @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 - ) - - # 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)' - ) - ref_filter_regex, is_inclusion_filter = \ - self.environment.get_ref_filter_regex() - 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 and - include_ref(name, ref_filter_regex, is_inclusion_filter)): - sha1s.add(sha1) - - 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. - - 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. - - 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)] - - def get_commits_spec(self, new_or_old, reference_change=None): - """Get rev-list arguments for added or discarded commits. - - 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. - - 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. - - 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. - - 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. - - Return a list of the object names of commits that were added - by the part of this push represented by reference_change. If - reference_change is None, then return a list of *all* commits - added by this push.""" - - 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. - - Return a list of the object names of commits that were - entirely discarded from the repository by the part of this - push represented by reference_change.""" - - 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. - - Use send all of the notification emails (including reference - change emails and commit emails) needed for this push. Send - the emails using mailer. If body_filter is not None, then use - it to filter the lines that are intended for the email - body.""" - - # The sha1s of commits that were introduced by this push. - # They will be removed from this set as they are processed, to - # guarantee that one (and only one) email is generated for - # each new commit. - unhandled_sha1s = set(self.get_new_commits()) - 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: - change.environment.log_warning( - '*** no recipients configured so no email will be sent\n' - '*** for %r update %s->%s' - % (change.refname, change.old.sha1, change.new.sha1,) - ) - else: - if not change.environment.quiet: - change.environment.log_msg( - 'Sending notification emails to: %s' % (change.recipients,)) - extra_values = {'send_date': next(send_date)} - - 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: - 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' % max_emails - ) - return - - for (num, sha1) in enumerate(sha1s): - rev = Revision(change, GitObject(sha1), num=num + 1, tot=len(sha1s)) - if len(rev.parents) > 1 and change.environment.excludemergerevisions: - # skipping a merge commit - continue - if not rev.recipients and rev.cc_recipients: - change.environment.log_msg('*** Replacing Cc: with To:') - rev.recipients = rev.cc_recipients - rev.cc_recipients = None - if rev.recipients: - extra_values = {'send_date': next(send_date)} - mailer.send( - rev.generate_email(self, body_filter, extra_values), - rev.recipients, - ) - - # Consistency check: - if unhandled_sha1s: - change.environment.log_error( - 'ERROR: No emails were sent for the following new commits:\n' - ' %s' - % ('\n '.join(sorted(unhandled_sha1s)),) - ) - - -def include_ref(refname, ref_filter_regex, is_inclusion_filter): - does_match = bool(ref_filter_regex.search(refname)) - if is_inclusion_filter: - return does_match - else: # exclusion filter -- we include the ref if the regex doesn't match - return not does_match - - -def run_as_post_receive_hook(environment, mailer): - environment.check() - send_filter_regex, send_is_inclusion_filter = environment.get_ref_filter_regex(True) - ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(False) - changes = [] - while True: - line = read_line(sys.stdin) - if line == '': - break - (oldrev, newrev, refname) = line.strip().split(' ', 2) - environment.get_logger().debug( - "run_as_post_receive_hook: oldrev=%s, newrev=%s, refname=%s" % - (oldrev, newrev, refname)) - - if not include_ref(refname, ref_filter_regex, is_inclusion_filter): - continue - if not include_ref(refname, send_filter_regex, send_is_inclusion_filter): - continue - changes.append( - ReferenceChange.create(environment, oldrev, newrev, refname) - ) - if not changes: - mailer.close() - return - push = Push(environment, changes) - try: - push.send_emails(mailer, body_filter=environment.filter_body) - finally: - mailer.close() - - -def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False): - environment.check() - send_filter_regex, send_is_inclusion_filter = environment.get_ref_filter_regex(True) - ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(False) - if not include_ref(refname, ref_filter_regex, is_inclusion_filter): - return - if not include_ref(refname, send_filter_regex, send_is_inclusion_filter): - return - changes = [ - ReferenceChange.create( - environment, - read_git_output(['rev-parse', '--verify', oldrev]), - read_git_output(['rev-parse', '--verify', newrev]), - refname, - ), - ] - if not changes: - mailer.close() - return - push = Push(environment, changes, force_send) - try: - push.send_emails(mailer, body_filter=environment.filter_body) - finally: - mailer.close() - - -def check_ref_filter(environment): - send_filter_regex, send_is_inclusion = environment.get_ref_filter_regex(True) - ref_filter_regex, ref_is_inclusion = environment.get_ref_filter_regex(False) - - def inc_exc_lusion(b): - if b: - return 'inclusion' - else: - return 'exclusion' - - if send_filter_regex: - sys.stdout.write("DoSend/DontSend filter regex (" + - (inc_exc_lusion(send_is_inclusion)) + - '): ' + send_filter_regex.pattern + - '\n') - if send_filter_regex: - sys.stdout.write("Include/Exclude filter regex (" + - (inc_exc_lusion(ref_is_inclusion)) + - '): ' + ref_filter_regex.pattern + - '\n') - sys.stdout.write(os.linesep) - - sys.stdout.write( - "Refs marked as EXCLUDE are excluded by either refFilterInclusionRegex\n" - "or refFilterExclusionRegex. No emails will be sent for commits included\n" - "in these refs.\n" - "Refs marked as DONT-SEND are excluded by either refFilterDoSendRegex or\n" - "refFilterDontSendRegex, but not by either refFilterInclusionRegex or\n" - "refFilterExclusionRegex. Emails will be sent for commits included in these\n" - "refs only when the commit reaches a ref which isn't excluded.\n" - "Refs marked as DO-SEND are not excluded by any filter. Emails will\n" - "be sent normally for commits included in these refs.\n") - - sys.stdout.write(os.linesep) - - for refname in read_git_lines(['for-each-ref', '--format', '%(refname)']): - sys.stdout.write(refname) - if not include_ref(refname, ref_filter_regex, ref_is_inclusion): - sys.stdout.write(' EXCLUDE') - elif not include_ref(refname, send_filter_regex, send_is_inclusion): - sys.stdout.write(' DONT-SEND') - else: - sys.stdout.write(' DO-SEND') - - sys.stdout.write(os.linesep) - - -def show_env(environment, out): - out.write('Environment values:\n') - for (k, v) in sorted(environment.get_values().items()): - if k: # Don't show the {'' : ''} pair. - out.write(' %s : %r\n' % (k, v)) - out.write('\n') - # Flush to avoid interleaving with further log output - out.flush() - - -def check_setup(environment): - environment.check() - show_env(environment, sys.stdout) - sys.stdout.write("Now, checking that git-multimail's standard input " - "is properly set ..." + os.linesep) - sys.stdout.write("Please type some text and then press Return" + os.linesep) - stdin = sys.stdin.readline() - sys.stdout.write("You have just entered:" + os.linesep) - sys.stdout.write(stdin) - sys.stdout.write("git-multimail seems properly set up." + os.linesep) - - -def choose_mailer(config, environment): - mailer = config.get('mailer', default='sendmail') - - 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='') - smtpcacerts = config.get('smtpcacerts', default='') - mailer = SMTPMailer( - environment, - envelopesender=(environment.get_sender() or environment.get_fromaddr()), - smtpserver=smtpserver, smtpservertimeout=smtpservertimeout, - smtpserverdebuglevel=smtpserverdebuglevel, - smtpencryption=smtpencryption, - smtpuser=smtpuser, - smtppass=smtppass, - smtpcacerts=smtpcacerts - ) - elif mailer == 'sendmail': - command = config.get('sendmailcommand') - if command: - command = shlex.split(command) - mailer = SendMailer(environment, - command=command, envelopesender=environment.get_sender()) - else: - environment.log_error( - 'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer + - 'please use one of "smtp" or "sendmail".' - ) - sys.exit(1) - return mailer - - -KNOWN_ENVIRONMENTS = { - 'generic': {'highprec': GenericEnvironmentMixin}, - 'gitolite': {'highprec': GitoliteEnvironmentHighPrecMixin, - 'lowprec': GitoliteEnvironmentLowPrecMixin}, - 'stash': {'highprec': StashEnvironmentHighPrecMixin, - 'lowprec': StashEnvironmentLowPrecMixin}, - 'gerrit': {'highprec': GerritEnvironmentHighPrecMixin, - 'lowprec': GerritEnvironmentLowPrecMixin}, - } - - -def choose_environment(config, osenv=None, env=None, recipients=None, - hook_info=None): - env_name = choose_environment_name(config, env, osenv) - environment_klass = build_environment_klass(env_name) - env = build_environment(environment_klass, env_name, config, - osenv, recipients, hook_info) - return env - - -def choose_environment_name(config, env, osenv): - if not osenv: - osenv = os.environ - - if not env: - env = config.get('environment') - - if not env: - if 'GL_USER' in osenv and 'GL_REPO' in osenv: - env = 'gitolite' - else: - env = 'generic' - return env - - -COMMON_ENVIRONMENT_MIXINS = [ - ConfigRecipientsEnvironmentMixin, - CLIRecipientsEnvironmentMixin, - ConfigRefFilterEnvironmentMixin, - ProjectdescEnvironmentMixin, - ConfigMaxlinesEnvironmentMixin, - ComputeFQDNEnvironmentMixin, - ConfigFilterLinesEnvironmentMixin, - PusherDomainEnvironmentMixin, - ConfigOptionsEnvironmentMixin, - ] - - -def build_environment_klass(env_name): - if 'class' in KNOWN_ENVIRONMENTS[env_name]: - return KNOWN_ENVIRONMENTS[env_name]['class'] - - environment_mixins = [] - known_env = KNOWN_ENVIRONMENTS[env_name] - if 'highprec' in known_env: - high_prec_mixin = known_env['highprec'] - environment_mixins.append(high_prec_mixin) - environment_mixins = environment_mixins + COMMON_ENVIRONMENT_MIXINS - if 'lowprec' in known_env: - low_prec_mixin = known_env['lowprec'] - environment_mixins.append(low_prec_mixin) - environment_mixins.append(Environment) - klass_name = env_name.capitalize() + 'Environment' - environment_klass = type( - klass_name, - tuple(environment_mixins), - {}, - ) - KNOWN_ENVIRONMENTS[env_name]['class'] = environment_klass - return environment_klass - - -GerritEnvironment = build_environment_klass('gerrit') -StashEnvironment = build_environment_klass('stash') -GitoliteEnvironment = build_environment_klass('gitolite') -GenericEnvironment = build_environment_klass('generic') - - -def build_environment(environment_klass, env, config, - osenv, recipients, hook_info): - environment_kw = { - 'osenv': osenv, - 'config': config, - } - - if env == 'stash': - environment_kw['user'] = hook_info['stash_user'] - environment_kw['repo'] = hook_info['stash_repo'] - elif env == 'gerrit': - environment_kw['project'] = hook_info['project'] - environment_kw['submitter'] = hook_info['submitter'] - environment_kw['update_method'] = hook_info['update_method'] - - environment_kw['cli_recipients'] = recipients - - return environment_klass(**environment_kw) - - -def get_version(): - oldcwd = os.getcwd() - try: - try: - os.chdir(os.path.dirname(os.path.realpath(__file__))) - git_version = read_git_output(['describe', '--tags', 'HEAD']) - if git_version == __version__: - return git_version - else: - return '%s (%s)' % (__version__, git_version) - except: - pass - finally: - os.chdir(oldcwd) - return __version__ - - -def compute_gerrit_options(options, args, required_gerrit_options, - raw_refname): - if None in required_gerrit_options: - raise SystemExit("Error: Specify all of --oldrev, --newrev, --refname, " - "and --project; or none of them.") - - if options.environment not in (None, 'gerrit'): - raise SystemExit("Non-gerrit environments incompatible with --oldrev, " - "--newrev, --refname, and --project") - options.environment = 'gerrit' - - if args: - raise SystemExit("Error: Positional parameters not allowed with " - "--oldrev, --newrev, and --refname.") - - # Gerrit oddly omits 'refs/heads/' in the refname when calling - # ref-updated hook; put it back. - git_dir = get_git_dir() - if (not os.path.exists(os.path.join(git_dir, raw_refname)) and - os.path.exists(os.path.join(git_dir, 'refs', 'heads', - raw_refname))): - options.refname = 'refs/heads/' + options.refname - - # New revisions can appear in a gerrit repository either due to someone - # pushing directly (in which case options.submitter will be set), or they - # can press "Submit this patchset" in the web UI for some CR (in which - # case options.submitter will not be set and gerrit will not have provided - # us the information about who pressed the button). - # - # Note for the nit-picky: I'm lumping in REST API calls and the ssh - # gerrit review command in with "Submit this patchset" button, since they - # have the same effect. - if options.submitter: - update_method = 'pushed' - # The submitter argument is almost an RFC 2822 email address; change it - # from 'User Name (email@domain)' to 'User Name <email@domain>' so it is - options.submitter = options.submitter.replace('(', '<').replace(')', '>') - else: - update_method = 'submitted' - # Gerrit knew who submitted this patchset, but threw that information - # away when it invoked this hook. However, *IF* Gerrit created a - # merge to bring the patchset in (project 'Submit Type' is either - # "Always Merge", or is "Merge if Necessary" and happens to be - # necessary for this particular CR), then it will have the committer - # of that merge be 'Gerrit Code Review' and the author will be the - # person who requested the submission of the CR. Since this is fairly - # likely for most gerrit installations (of a reasonable size), it's - # worth the extra effort to try to determine the actual submitter. - rev_info = read_git_lines(['log', '--no-walk', '--merges', - '--format=%cN%n%aN <%aE>', options.newrev]) - if rev_info and rev_info[0] == 'Gerrit Code Review': - options.submitter = rev_info[1] - - # We pass back refname, oldrev, newrev as args because then the - # gerrit ref-updated hook is much like the git update hook - return (options, - [options.refname, options.oldrev, options.newrev], - {'project': options.project, 'submitter': options.submitter, - 'update_method': update_method}) - - -def check_hook_specific_args(options, args): - raw_refname = options.refname - # Convert each string option unicode for Python3. - if PYTHON3: - opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname', - 'project', 'submitter', 'stash_user', 'stash_repo'] - for opt in opts: - if not hasattr(options, opt): - continue - obj = getattr(options, opt) - if obj: - enc = obj.encode('utf-8', 'surrogateescape') - dec = enc.decode('utf-8', 'replace') - setattr(options, opt, dec) - - # First check for stash arguments - if (options.stash_user is None) != (options.stash_repo is None): - raise SystemExit("Error: Specify both of --stash-user and " - "--stash-repo or neither.") - if options.stash_user: - options.environment = 'stash' - return options, args, {'stash_user': options.stash_user, - 'stash_repo': options.stash_repo} - - # Finally, check for gerrit specific arguments - required_gerrit_options = (options.oldrev, options.newrev, options.refname, - options.project) - if required_gerrit_options != (None,) * 4: - return compute_gerrit_options(options, args, required_gerrit_options, - raw_refname) - - # No special options in use, just return what we started with - return options, args, {} - - -class Logger(object): - def parse_verbose(self, verbose): - if verbose > 0: - return logging.DEBUG - else: - return logging.INFO - - def create_log_file(self, environment, name, path, verbosity): - log_file = logging.getLogger(name) - file_handler = logging.FileHandler(path) - log_fmt = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s") - file_handler.setFormatter(log_fmt) - log_file.addHandler(file_handler) - log_file.setLevel(verbosity) - return log_file - - def __init__(self, environment): - self.environment = environment - self.loggers = [] - stderr_log = logging.getLogger('git_multimail.stderr') - - class EncodedStderr(object): - def write(self, x): - write_str(sys.stderr, x) - - def flush(self): - sys.stderr.flush() - - stderr_handler = logging.StreamHandler(EncodedStderr()) - stderr_log.addHandler(stderr_handler) - stderr_log.setLevel(self.parse_verbose(environment.verbose)) - self.loggers.append(stderr_log) - - if environment.debug_log_file is not None: - debug_log_file = self.create_log_file( - environment, 'git_multimail.debug', environment.debug_log_file, logging.DEBUG) - self.loggers.append(debug_log_file) - - if environment.log_file is not None: - log_file = self.create_log_file( - environment, 'git_multimail.file', environment.log_file, logging.INFO) - self.loggers.append(log_file) - - if environment.error_log_file is not None: - error_log_file = self.create_log_file( - environment, 'git_multimail.error', environment.error_log_file, logging.ERROR) - self.loggers.append(error_log_file) - - def info(self, msg, *args, **kwargs): - for l in self.loggers: - l.info(msg, *args, **kwargs) - - def debug(self, msg, *args, **kwargs): - for l in self.loggers: - l.debug(msg, *args, **kwargs) - - def warning(self, msg, *args, **kwargs): - for l in self.loggers: - l.warning(msg, *args, **kwargs) - - def error(self, msg, *args, **kwargs): - for l in self.loggers: - l.error(msg, *args, **kwargs) - - -def main(args): - parser = optparse.OptionParser( - description=__doc__, - usage='%prog [OPTIONS]\n or: %prog [OPTIONS] REFNAME OLDREV NEWREV', - ) - - parser.add_option( - '--environment', '--env', action='store', type='choice', - choices=list(KNOWN_ENVIRONMENTS.keys()), default=None, - help=( - 'Choose type of environment is in use. Default is taken from ' - 'multimailhook.environment if set; otherwise "generic".' - ), - ) - parser.add_option( - '--stdout', action='store_true', default=False, - help='Output emails to stdout rather than sending them.', - ) - parser.add_option( - '--recipients', action='store', default=None, - help='Set list of email recipients for all types of emails.', - ) - parser.add_option( - '--show-env', action='store_true', default=False, - help=( - 'Write to stderr the values determined for the environment ' - '(intended for debugging purposes), then proceed normally.' - ), - ) - 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.' - ), - ) - parser.add_option( - '-c', metavar="<name>=<value>", action='append', - help=( - 'Pass a configuration parameter through to git. The value given ' - 'will override values from configuration files. See the -c option ' - 'of git(1) for more details. (Only works with git >= 1.7.3)' - ), - ) - parser.add_option( - '--version', '-v', action='store_true', default=False, - help=( - "Display git-multimail's version" - ), - ) - - parser.add_option( - '--python-version', action='store_true', default=False, - help=( - "Display the version of Python used by git-multimail" - ), - ) - - parser.add_option( - '--check-ref-filter', action='store_true', default=False, - help=( - 'List refs and show information on how git-multimail ' - 'will process them.' - ) - ) - - # The following options permit this script to be run as a gerrit - # ref-updated hook. See e.g. - # code.google.com/p/gerrit/source/browse/Documentation/config-hooks.txt - # We suppress help for these items, since these are specific to gerrit, - # and we don't want users directly using them any way other than how the - # gerrit ref-updated hook is called. - parser.add_option('--oldrev', action='store', help=optparse.SUPPRESS_HELP) - parser.add_option('--newrev', action='store', help=optparse.SUPPRESS_HELP) - parser.add_option('--refname', action='store', help=optparse.SUPPRESS_HELP) - parser.add_option('--project', action='store', help=optparse.SUPPRESS_HELP) - parser.add_option('--submitter', action='store', help=optparse.SUPPRESS_HELP) - - # The following allow this to be run as a stash asynchronous post-receive - # hook (almost identical to a git post-receive hook but triggered also for - # merges of pull requests from the UI). We suppress help for these items, - # since these are specific to stash. - parser.add_option('--stash-user', action='store', help=optparse.SUPPRESS_HELP) - parser.add_option('--stash-repo', action='store', help=optparse.SUPPRESS_HELP) - - (options, args) = parser.parse_args(args) - (options, args, hook_info) = check_hook_specific_args(options, args) - - if options.version: - sys.stdout.write('git-multimail version ' + get_version() + '\n') - return - - if options.python_version: - sys.stdout.write('Python version ' + sys.version + '\n') - return - - if options.c: - Config.add_config_parameters(options.c) - - config = Config('multimailhook') - - environment = None - try: - environment = choose_environment( - config, osenv=os.environ, - env=options.environment, - recipients=options.recipients, - hook_info=hook_info, - ) - - if options.show_env: - show_env(environment, sys.stderr) - - if options.stdout or environment.stdout: - mailer = OutputMailer(sys.stdout, environment) - else: - mailer = choose_mailer(config, environment) - - must_check_setup = os.environ.get('GIT_MULTIMAIL_CHECK_SETUP') - if must_check_setup == '': - must_check_setup = False - if options.check_ref_filter: - check_ref_filter(environment) - elif must_check_setup: - check_setup(environment) - # Dual mode: if arguments were specified on the command line, run - # like an update hook; otherwise, run as a post-receive hook. - elif args: - if len(args) != 3: - parser.error('Need zero or three non-option arguments') - (refname, oldrev, newrev) = args - environment.get_logger().debug( - "run_as_update_hook: refname=%s, oldrev=%s, newrev=%s, force_send=%s" % - (refname, oldrev, newrev, options.force_send)) - run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send) - else: - run_as_post_receive_hook(environment, mailer) - except ConfigurationException: - sys.exit(sys.exc_info()[1]) - except SystemExit: - raise - except Exception: - t, e, tb = sys.exc_info() - import traceback - sys.stderr.write('\n') # Avoid mixing message with previous output - msg = ( - 'Exception \'' + t.__name__ + - '\' raised. Please report this as a bug to\n' - 'https://github.com/git-multimail/git-multimail/issues\n' - 'with the information below:\n\n' - 'git-multimail version ' + get_version() + '\n' - 'Python version ' + sys.version + '\n' + - traceback.format_exc()) - try: - environment.get_logger().error(msg) - except: - sys.stderr.write(msg) - sys.exit(1) - - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config deleted file mode 100755 index 241ba22fa3..0000000000 --- a/contrib/hooks/multimail/migrate-mailhook-config +++ /dev/null @@ -1,274 +0,0 @@ -#! /usr/bin/env python - -"""Migrate a post-receive-email configuration to be usable with git_multimail.py. - -See README.migrate-from-post-receive-email for more information. - -""" - -import sys -import optparse - -from git_multimail import CommandError -from git_multimail import Config -from git_multimail import read_output - - -OLD_NAMES = [ - 'mailinglist', - 'announcelist', - 'envelopesender', - 'emailprefix', - 'showrev', - 'emailmaxlines', - 'diffopts', - 'scancommitforcc', - ] - -NEW_NAMES = [ - 'environment', - 'reponame', - 'mailinglist', - 'refchangelist', - 'commitlist', - 'announcelist', - 'announceshortlog', - 'envelopesender', - 'administrator', - 'emailprefix', - 'emailmaxlines', - 'diffopts', - 'emaildomain', - 'scancommitforcc', - ] - - -INFO = """\ - -SUCCESS! - -Your post-receive-email configuration has been converted to -git-multimail format. Please see README and -README.migrate-from-post-receive-email to learn about other -git-multimail configuration possibilities. - -For example, git-multimail has the following new options with no -equivalent in post-receive-email. You might want to read about them -to see if they would be useful in your situation: - -""" - - -def _check_old_config_exists(old): - """Check that at least one old configuration value is set.""" - - for name in OLD_NAMES: - if name in old: - return True - - return False - - -def _check_new_config_clear(new): - """Check that none of the new configuration names are set.""" - - retval = True - for name in NEW_NAMES: - if 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)) - retval = False - - return retval - - -def erase_values(config, names): - for name in names: - if name in config: - try: - sys.stderr.write('...unsetting "%s.%s"\n' % (config.section, name)) - config.unset_all(name) - except CommandError: - sys.stderr.write( - '\nWARNING: could not unset "%s.%s". ' - 'Perhaps it is not set at the --local level?\n\n' - % (config.section, name) - ) - - -def is_section_empty(section, local): - """Return True iff the specified configuration section is empty. - - Iff local is True, use the --local option when invoking 'git - config'.""" - - if local: - local_option = ['--local'] - else: - local_option = [] - - try: - read_output( - ['git', 'config'] + - local_option + - ['--get-regexp', '^%s\.' % (section,)] - ) - except CommandError: - t, e, traceback = sys.exc_info() - if e.retcode == 1: - # This means that no settings were found. - return True - else: - raise - else: - return False - - -def remove_section_if_empty(section): - """If the specified configuration section is empty, delete it.""" - - try: - empty = is_section_empty(section, local=True) - except CommandError: - # Older versions of git do not support the --local option, so - # if the first attempt fails, try without --local. - try: - empty = is_section_empty(section, local=False) - except CommandError: - sys.stderr.write( - '\nINFO: If configuration section "%s.*" is empty, you might want ' - 'to delete it.\n\n' - % (section,) - ) - return - - if empty: - sys.stderr.write('...removing section "%s.*"\n' % (section,)) - read_output(['git', 'config', '--remove-section', section]) - else: - sys.stderr.write( - '\nINFO: Configuration section "%s.*" still has contents. ' - 'It will not be deleted.\n\n' - % (section,) - ) - - -def migrate_config(strict=False, retain=False, overwrite=False): - old = Config('hooks') - new = Config('multimailhook') - if not _check_old_config_exists(old): - sys.exit( - 'Your repository has no post-receive-email configuration. ' - 'Nothing to do.' - ) - if not _check_new_config_clear(new): - if overwrite: - sys.stderr.write('\nWARNING: Erasing the above values...\n\n') - erase_values(new, NEW_NAMES) - else: - sys.exit( - '\nERROR: Refusing to overwrite existing values. Use the --overwrite\n' - 'option to continue anyway.' - ) - - name = 'showrev' - if name in old: - msg = 'git-multimail does not support "%s.%s"' % (old.section, name,) - if strict: - sys.exit( - 'ERROR: %s.\n' - 'Please unset that value then try again, or run without --strict.' - % (msg,) - ) - else: - sys.stderr.write('\nWARNING: %s (ignoring).\n\n' % (msg,)) - - for name in ['mailinglist', 'announcelist']: - if name in old: - sys.stderr.write( - '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name) - ) - old_recipients = old.get_all(name, default=None) - old_recipients = ', '.join(o.strip() for o in old_recipients) - new.set_recipients(name, old_recipients) - - if strict: - sys.stderr.write( - '...setting "%s.commitlist" to the empty string\n' % (new.section,) - ) - new.set_recipients('commitlist', '') - sys.stderr.write( - '...setting "%s.announceshortlog" to "true"\n' % (new.section,) - ) - new.set('announceshortlog', 'true') - - for name in ['envelopesender', 'emailmaxlines', 'diffopts', '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 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)) - elif strict: - sys.stderr.write( - '...setting "%s.%s" to "[SCM]" to preserve old subject lines\n' - % (new.section, name) - ) - new.set(name, '[SCM]') - - if not retain: - erase_values(old, OLD_NAMES) - remove_section_if_empty(old.section) - - sys.stderr.write(INFO) - for name in NEW_NAMES: - if name not in OLD_NAMES: - sys.stderr.write(' "%s.%s"\n' % (new.section, name,)) - sys.stderr.write('\n') - - -def main(args): - parser = optparse.OptionParser( - description=__doc__, - usage='%prog [OPTIONS]', - ) - - parser.add_option( - '--strict', action='store_true', default=False, - help=( - 'Slavishly configure git-multimail as closely as possible to ' - 'the post-receive-email configuration. Default is to turn ' - 'on some new features that have no equivalent in post-receive-email.' - ), - ) - parser.add_option( - '--retain', action='store_true', default=False, - help=( - 'Retain the post-receive-email configuration values. ' - 'Default is to delete them after the new values are set.' - ), - ) - parser.add_option( - '--overwrite', action='store_true', default=False, - help=( - 'Overwrite any existing git-multimail configuration settings. ' - 'Default is to abort if such settings already exist.' - ), - ) - - (options, args) = parser.parse_args(args) - - if args: - parser.error('Unexpected arguments: %s' % (' '.join(args),)) - - migrate_config(strict=options.strict, retain=options.retain, overwrite=options.overwrite) - - -main(sys.argv[1:]) diff --git a/contrib/hooks/multimail/post-receive.example b/contrib/hooks/multimail/post-receive.example deleted file mode 100755 index 0f98c5a23d..0000000000 --- a/contrib/hooks/multimail/post-receive.example +++ /dev/null @@ -1,101 +0,0 @@ -#! /usr/bin/env python - -"""Example post-receive hook based on git-multimail. - -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. - -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. - -* Configuration settings can be read from other sources; for example, - user names and email addresses could be read from LDAP or from a - database. Or the settings can even be hardcoded in the importing - Python script, if this is preferred. - -This script is a very basic example of how to use git_multimail.py as -a module. The comments below explain some of the points at which the -script's behavior could be changed or customized. - -""" - -import sys - -# If necessary, add the path to the directory containing -# git_multimail.py to the Python path as follows. (This is not -# necessary if git_multimail.py is in the same directory as this -# script): - -#LIBDIR = 'path/to/directory/containing/module' -#sys.path.insert(0, LIBDIR) - -import git_multimail - -# It is possible to modify the output templates here; e.g.: - -#git_multimail.FOOTER_TEMPLATE = """\ -# -#-- \n\ -#This email was generated by the wonderful git-multimail tool. -#""" - - -# Specify which "git config" section contains the configuration for -# git-multimail: -config = git_multimail.Config('multimailhook') - -# Set some Git configuration variables. Equivalent to passing var=val -# to "git -c var=val" each time git is called, or to adding the -# configuration in .git/config (must come before instantiating the -# environment) : -#git_multimail.Config.add_config_parameters('multimailhook.commitEmailFormat=html') -#git_multimail.Config.add_config_parameters(('user.name=foo', 'user.email=foo@example.com')) - -# Select the type of environment: -try: - environment = git_multimail.GenericEnvironment(config=config) - #environment = git_multimail.GitoliteEnvironment(config=config) -except git_multimail.ConfigurationException: - sys.stderr.write('*** %s\n' % sys.exc_info()[1]) - sys.exit(1) - - -# Choose the method of sending emails based on the git config: -mailer = git_multimail.choose_mailer(config, environment) - -# Alternatively, you may hardcode the mailer using code like one of -# the following: - -# Use "/usr/sbin/sendmail -oi -t" to send emails. The envelopesender -# argument is optional: -#mailer = git_multimail.SendMailer( -# command=['/usr/sbin/sendmail', '-oi', '-t'], -# envelopesender='git-repo@example.com', -# ) - -# Use Python's smtplib to send emails. Both arguments are required. -#mailer = git_multimail.SMTPMailer( -# environment=environment, -# envelopesender='git-repo@example.com', -# # The smtpserver argument can also include a port number; e.g., -# # smtpserver='mail.example.com:25' -# smtpserver='mail.example.com', -# ) - -# OutputMailer is intended only for testing; it writes the emails to -# the specified file stream. -#mailer = git_multimail.OutputMailer(sys.stdout) - - -# Read changes from stdin and send notification emails: -git_multimail.run_as_post_receive_hook(environment, mailer) diff --git a/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh b/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh index 4c39bda7bf..f08890d9e7 100755 --- a/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh +++ b/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh @@ -86,7 +86,7 @@ test_expect_success 'Git clone works with page added' ' test_expect_success 'Git clone works with an edited page ' ' wiki_reset && wiki_editpage foo "this page will be edited" \ - false -s "first edition of page foo"&& + false -s "first edition of page foo" && wiki_editpage foo "this page has been edited and must be on the clone " true && git clone mediawiki::'"$WIKI_URL"' mw_dir_6 && test_path_is_file mw_dir_6/Foo.mw && diff --git a/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh b/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh index 6b0dbdac4d..526d92850f 100755 --- a/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh +++ b/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh @@ -287,7 +287,7 @@ test_expect_success 'git push with \' ' git add \\ko\\o.mw && git commit -m " \\ko\\o added" && git push - )&& + ) && wiki_page_exist \\ko\\o && wiki_check_content mw_dir_18/\\ko\\o.mw \\ko\\o @@ -311,7 +311,7 @@ test_expect_success 'git push with \ in format control' ' git add \\fo\\o.mw && git commit -m " \\fo\\o added" && git push - )&& + ) && wiki_page_exist \\fo\\o && wiki_check_content mw_dir_20/\\fo\\o.mw \\fo\\o diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh index 868e18b9a1..7f767b5c38 100755 --- a/contrib/subtree/git-subtree.sh +++ b/contrib/subtree/git-subtree.sh @@ -4,208 +4,265 @@ # # Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com> # -if test $# -eq 0 + +if test -z "$GIT_EXEC_PATH" || ! test -f "$GIT_EXEC_PATH/git-sh-setup" || { + test "${PATH#"${GIT_EXEC_PATH}:"}" = "$PATH" && + test ! "$GIT_EXEC_PATH" -ef "${PATH%%:*}" 2>/dev/null +} then - set -- -h + basename=${0##*[/\\]} + echo >&2 'It looks like either your git installation or your' + echo >&2 'git-subtree installation is broken.' + echo >&2 + echo >&2 "Tips:" + echo >&2 " - If \`git --exec-path\` does not print the correct path to" + echo >&2 " your git install directory, then set the GIT_EXEC_PATH" + echo >&2 " environment variable to the correct directory." + echo >&2 " - Make sure that your \`$basename\` file is either in your" + echo >&2 " PATH or in your git exec path (\`$(git --exec-path)\`)." + echo >&2 " - You should run git-subtree as \`git ${basename#git-}\`," + echo >&2 " not as \`$basename\`." >&2 + exit 126 fi + OPTS_SPEC="\ git subtree add --prefix=<prefix> <commit> git subtree add --prefix=<prefix> <repository> <ref> git subtree merge --prefix=<prefix> <commit> +git subtree split --prefix=<prefix> [<commit>] git subtree pull --prefix=<prefix> <repository> <ref> -git subtree push --prefix=<prefix> <repository> <ref> -git subtree split --prefix=<prefix> <commit> +git subtree push --prefix=<prefix> <repository> <refspec> -- h,help show the help q quiet d show debug messages P,prefix= the name of the subdir to split out -m,message= use the given message as the commit message for the merge commit - options for 'split' + options for 'split' (also: 'push') annotate= add a prefix to commit message of new commits 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', and 'pull' + options for 'add' and 'merge' (also: 'pull', 'split --rejoin', and 'push --rejoin') squash merge subtree changes as a single commit +m,message= use the given message as the commit message for the merge commit " -eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" - -PATH=$PATH:$(git --exec-path) -. git-sh-setup -require_work_tree - -quiet= -branch= -debug= -command= -onto= -rejoin= -ignore_joins= -annotate= -squash= -message= -prefix= +indent=0 +# Usage: debug [MSG...] debug () { - if test -n "$debug" - then - printf "%s\n" "$*" >&2 - fi -} - -say () { - if test -z "$quiet" + if test -n "$arg_debug" then - printf "%s\n" "$*" >&2 + printf "%$(($indent * 2))s%s\n" '' "$*" >&2 fi } +# Usage: progress [MSG...] progress () { - if test -z "$quiet" + if test -z "$GIT_QUIET" then - printf "%s\r" "$*" >&2 + if test -z "$arg_debug" + then + # Debug mode is off. + # + # Print one progress line that we keep updating (use + # "\r" to return to the beginning of the line, rather + # than "\n" to start a new line). This only really + # works when stderr is a terminal. + printf "%s\r" "$*" >&2 + else + # Debug mode is on. The `debug` function is regularly + # printing to stderr. + # + # Don't do the one-line-with-"\r" thing, because on a + # terminal the debug output would overwrite and hide the + # progress output. Add a "progress:" prefix to make the + # progress output and the debug output easy to + # distinguish. This ensures maximum readability whether + # stderr is a terminal or a file. + printf "progress: %s\n" "$*" >&2 + fi fi } +# Usage: assert CMD... assert () { if ! "$@" then - die "assertion failed: " "$@" + die "assertion failed: $*" fi } -ensure_single_rev () { - if test $# -ne 1 +main () { + if test $# -eq 0 then - die "You must provide exactly one revision. Got: '$@'" + set -- -h fi -} - -while test $# -gt 0 -do - opt="$1" - shift - - case "$opt" in - -q) - quiet=1 - ;; - -d) - debug=1 - ;; - --annotate) - annotate="$1" - shift - ;; - --no-annotate) - annotate= - ;; - -b) - branch="$1" - shift - ;; - -P) - prefix="${1%/}" + set_args="$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" + eval "$set_args" + . git-sh-setup + require_work_tree + + # First figure out the command and whether we use --rejoin, so + # that we can provide more helpful validation when we do the + # "real" flag parsing. + arg_split_rejoin= + allow_split= + allow_addmerge= + while test $# -gt 0 + do + opt="$1" shift + case "$opt" in + --annotate|-b|-P|-m|--onto) + shift + ;; + --rejoin) + arg_split_rejoin=1 + ;; + --no-rejoin) + arg_split_rejoin= + ;; + --) + break + ;; + esac + done + arg_command=$1 + case "$arg_command" in + add|merge|pull) + allow_addmerge=1 ;; - -m) - message="$1" - shift + split|push) + allow_split=1 + allow_addmerge=$arg_split_rejoin ;; - --no-prefix) - prefix= + *) + die "Unknown command '$arg_command'" ;; - --onto) - onto="$1" + esac + # Reset the arguments array for "real" flag parsing. + eval "$set_args" + + # Begin "real" flag parsing. + arg_debug= + arg_prefix= + arg_split_branch= + arg_split_onto= + arg_split_ignore_joins= + arg_split_annotate= + arg_addmerge_squash= + arg_addmerge_message= + while test $# -gt 0 + do + opt="$1" shift - ;; - --no-onto) - onto= - ;; - --rejoin) - rejoin=1 - ;; - --no-rejoin) - rejoin= - ;; - --ignore-joins) - ignore_joins=1 - ;; - --no-ignore-joins) - ignore_joins= - ;; - --squash) - squash=1 - ;; - --no-squash) - squash= - ;; - --) - break + + case "$opt" in + -q) + GIT_QUIET=1 + ;; + -d) + arg_debug=1 + ;; + --annotate) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_split_annotate="$1" + shift + ;; + --no-annotate) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_split_annotate= + ;; + -b) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_split_branch="$1" + shift + ;; + -P) + arg_prefix="${1%/}" + shift + ;; + -m) + test -n "$allow_addmerge" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_addmerge_message="$1" + shift + ;; + --no-prefix) + arg_prefix= + ;; + --onto) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_split_onto="$1" + shift + ;; + --no-onto) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_split_onto= + ;; + --rejoin) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + ;; + --no-rejoin) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + ;; + --ignore-joins) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_split_ignore_joins=1 + ;; + --no-ignore-joins) + test -n "$allow_split" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_split_ignore_joins= + ;; + --squash) + test -n "$allow_addmerge" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_addmerge_squash=1 + ;; + --no-squash) + test -n "$allow_addmerge" || die "The '$opt' flag does not make sense with 'git subtree $arg_command'." + arg_addmerge_squash= + ;; + --) + break + ;; + *) + die "Unexpected option: $opt" + ;; + esac + done + shift + + if test -z "$arg_prefix" + then + die "You must provide the --prefix option." + fi + + case "$arg_command" in + add) + test -e "$arg_prefix" && + die "prefix '$arg_prefix' already exists." ;; *) - die "Unexpected option: $opt" + test -e "$arg_prefix" || + die "'$arg_prefix' does not exist; use 'git subtree add'" ;; esac -done - -command="$1" -shift - -case "$command" in -add|merge|pull) - default= - ;; -split|push) - default="--default HEAD" - ;; -*) - die "Unknown command '$command'" - ;; -esac - -if test -z "$prefix" -then - die "You must provide the --prefix option." -fi -case "$command" in -add) - test -e "$prefix" && - die "prefix '$prefix' already exists." - ;; -*) - test -e "$prefix" || - die "'$prefix' does not exist; use 'git subtree add'" - ;; -esac - -dir="$(dirname "$prefix/.")" - -if test "$command" != "pull" && - test "$command" != "add" && - test "$command" != "push" -then - revs=$(git rev-parse $default --revs-only "$@") || exit $? - dirs=$(git rev-parse --no-revs --no-flags "$@") || exit $? - ensure_single_rev $revs - if test -n "$dirs" - then - die "Error: Use --prefix instead of bare filenames." - fi -fi + dir="$(dirname "$arg_prefix/.")" + + debug "command: {$arg_command}" + debug "quiet: {$GIT_QUIET}" + debug "dir: {$dir}" + debug "opts: {$*}" + debug -debug "command: {$command}" -debug "quiet: {$quiet}" -debug "revs: {$revs}" -debug "dir: {$dir}" -debug "opts: {$*}" -debug + "cmd_$arg_command" "$@" +} +# Usage: cache_setup cache_setup () { + assert test $# = 0 cachedir="$GIT_DIR/subtree-cache/$$" rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir" @@ -216,6 +273,7 @@ cache_setup () { debug "Using cachedir: $cachedir" >&2 } +# Usage: cache_get [REVS...] cache_get () { for oldrev in "$@" do @@ -227,6 +285,7 @@ cache_get () { done } +# Usage: cache_miss [REVS...] cache_miss () { for oldrev in "$@" do @@ -237,24 +296,30 @@ cache_miss () { done } +# Usage: check_parents PARENTS_EXPR check_parents () { - missed=$(cache_miss "$1") - local indent=$(($2 + 1)) + assert test $# = 1 + missed=$(cache_miss "$1") || exit $? + local indent=$(($indent + 1)) for miss in $missed do if ! test -r "$cachedir/notree/$miss" then - debug " incorrect order: $miss" - process_split_commit "$miss" "" "$indent" + debug "incorrect order: $miss" + process_split_commit "$miss" "" fi done } +# Usage: set_notree REV set_notree () { + assert test $# = 1 echo "1" > "$cachedir/notree/$1" } +# Usage: cache_set OLDREV NEWREV cache_set () { + assert test $# = 2 oldrev="$1" newrev="$2" if test "$oldrev" != "latest_old" && @@ -266,7 +331,9 @@ cache_set () { echo "$newrev" >"$cachedir/$oldrev" } +# Usage: rev_exists REV rev_exists () { + assert test $# = 1 if git rev-parse "$1" >/dev/null 2>&1 then return 0 @@ -275,32 +342,25 @@ rev_exists () { fi } -rev_is_descendant_of_branch () { - newrev="$1" - branch="$2" - branch_hash=$(git rev-parse "$branch") - match=$(git rev-list -1 "$branch_hash" "^$newrev") - - if test -z "$match" - then - return 0 - else - return 1 - fi -} - -# if a commit doesn't have a parent, this might not work. But we only want +# Usage: try_remove_previous REV +# +# If a commit doesn't have a parent, this might not work. But we only want # to remove the parent from the rev-list, and since it doesn't exist, it won't # be there anyway, so do nothing in that case. try_remove_previous () { + assert test $# = 1 if rev_exists "$1^" then echo "^$1^" fi } +# Usage: find_latest_squash DIR find_latest_squash () { + assert test $# = 1 debug "Looking for latest squash ($dir)..." + local indent=$(($indent + 1)) + dir="$1" sq= main= @@ -319,7 +379,7 @@ find_latest_squash () { main="$b" ;; git-subtree-split:) - sub="$(git rev-parse "$b^0")" || + sub="$(git rev-parse "$b^{commit}")" || die "could not rev-parse split hash $b from commit $sq" ;; END) @@ -329,7 +389,8 @@ find_latest_squash () { then # a rejoin commit? # Pretend its sub was a squash. - sq="$sub" + sq=$(git rev-parse --verify "$sq^2") || + die fi debug "Squash found: $sq $sub" echo "$sq" "$sub" @@ -340,22 +401,26 @@ find_latest_squash () { sub= ;; esac - done + done || exit $? } +# Usage: find_existing_splits DIR REV find_existing_splits () { + assert test $# = 2 debug "Looking for prior splits..." + local indent=$(($indent + 1)) + dir="$1" - revs="$2" + rev="$2" main= sub= local grep_format="^git-subtree-dir: $dir/*\$" - if test -n "$ignore_joins" + if test -n "$arg_split_ignore_joins" then grep_format="^Add '$dir/' from commit '" fi git log --grep="$grep_format" \ - --no-show-signature --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs | + --no-show-signature --pretty=format:'START %H%n%s%n%n%b%nEND%n' "$rev" | while read a b junk do case "$a" in @@ -366,11 +431,11 @@ find_existing_splits () { main="$b" ;; git-subtree-split:) - sub="$(git rev-parse "$b^0")" || + sub="$(git rev-parse "$b^{commit}")" || die "could not rev-parse split hash $b from commit $sq" ;; END) - debug " Main is: '$main'" + debug "Main is: '$main'" if test -z "$main" -a -n "$sub" then # squash commits refer to a subtree @@ -389,10 +454,12 @@ find_existing_splits () { sub= ;; esac - done + done || exit $? } +# Usage: copy_commit REV TREE FLAGS_STR copy_commit () { + assert test $# = 3 # 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}" @@ -411,23 +478,32 @@ copy_commit () { GIT_COMMITTER_EMAIL \ GIT_COMMITTER_DATE ( - printf "%s" "$annotate" + printf "%s" "$arg_split_annotate" cat ) | git commit-tree "$2" $3 # reads the rest of stdin ) || die "Can't copy commit $1" } +# Usage: add_msg DIR LATEST_OLD LATEST_NEW add_msg () { + assert test $# = 3 dir="$1" latest_old="$2" latest_new="$3" - if test -n "$message" + if test -n "$arg_addmerge_message" then - commit_message="$message" + commit_message="$arg_addmerge_message" else commit_message="Add '$dir/' from commit '$latest_new'" fi + if test -n "$arg_split_rejoin" + then + # If this is from a --rejoin, then rejoin_msg has + # already inserted the `git-subtree-xxx:` tags + echo "$commit_message" + return + fi cat <<-EOF $commit_message @@ -437,22 +513,26 @@ add_msg () { EOF } +# Usage: add_squashed_msg REV DIR add_squashed_msg () { - if test -n "$message" + assert test $# = 2 + if test -n "$arg_addmerge_message" then - echo "$message" + echo "$arg_addmerge_message" else echo "Merge commit '$1' as '$2'" fi } +# Usage: rejoin_msg DIR LATEST_OLD LATEST_NEW rejoin_msg () { + assert test $# = 3 dir="$1" latest_old="$2" latest_new="$3" - if test -n "$message" + if test -n "$arg_addmerge_message" then - commit_message="$message" + commit_message="$arg_addmerge_message" else commit_message="Split '$dir/' into commit '$latest_new'" fi @@ -465,7 +545,9 @@ rejoin_msg () { EOF } +# Usage: squash_msg DIR OLD_SUBTREE_COMMIT NEW_SUBTREE_COMMIT squash_msg () { + assert test $# = 3 dir="$1" oldsub="$2" newsub="$3" @@ -487,12 +569,16 @@ squash_msg () { echo "git-subtree-split: $newsub" } +# Usage: toptree_for_commit COMMIT toptree_for_commit () { + assert test $# = 1 commit="$1" git rev-parse --verify "$commit^{tree}" || exit $? } +# Usage: subtree_for_commit COMMIT DIR subtree_for_commit () { + assert test $# = 2 commit="$1" dir="$2" git ls-tree "$commit" -- "$dir" | @@ -503,17 +589,19 @@ subtree_for_commit () { test "$type" = "commit" && continue # ignore submodules echo $tree break - done + done || exit $? } +# Usage: tree_changed TREE [PARENTS...] tree_changed () { + assert test $# -gt 0 tree=$1 shift if test $# -ne 1 then return 0 # weird parents, consider it changed else - ptree=$(toptree_for_commit $1) + ptree=$(toptree_for_commit $1) || exit $? if test "$ptree" != "$tree" then return 0 # changed @@ -523,7 +611,9 @@ tree_changed () { fi } +# Usage: new_squash_commit OLD_SQUASHED_COMMIT OLD_NONSQUASHED_COMMIT NEW_NONSQUASHED_COMMIT new_squash_commit () { + assert test $# = 3 old="$1" oldsub="$2" newsub="$3" @@ -538,7 +628,9 @@ new_squash_commit () { fi } +# Usage: copy_or_skip REV TREE NEWPARENTS copy_or_skip () { + assert test $# = 3 rev="$1" tree="$2" newparents="$3" @@ -613,7 +705,9 @@ copy_or_skip () { fi } +# Usage: ensure_clean ensure_clean () { + assert test $# = 0 if ! git diff-index HEAD --exit-code --quiet 2>&1 then die "Working tree has modifications. Cannot add." @@ -624,15 +718,18 @@ ensure_clean () { fi } +# Usage: ensure_valid_ref_format REF ensure_valid_ref_format () { + assert test $# = 1 git check-ref-format "refs/heads/$1" || die "'$1' does not look like a ref" } +# Usage: process_split_commit REV PARENTS process_split_commit () { + assert test $# = 2 local rev="$1" local parents="$2" - local indent=$3 if test $indent -eq 0 then @@ -647,20 +744,21 @@ process_split_commit () { progress "$revcount/$revmax ($createcount) [$extracount]" debug "Processing commit: $rev" - exists=$(cache_get "$rev") + local indent=$(($indent + 1)) + exists=$(cache_get "$rev") || exit $? if test -n "$exists" then - debug " prior: $exists" + debug "prior: $exists" return fi createcount=$(($createcount + 1)) - debug " parents: $parents" - check_parents "$parents" "$indent" - newparents=$(cache_get $parents) - debug " newparents: $newparents" + debug "parents: $parents" + check_parents "$parents" + newparents=$(cache_get $parents) || exit $? + debug "newparents: $newparents" - tree=$(subtree_for_commit "$rev" "$dir") - debug " tree is: $tree" + tree=$(subtree_for_commit "$rev" "$dir") || exit $? + debug "tree is: $tree" # ugly. is there no better way to tell if this is a subtree # vs. a mainline commit? Does it matter? @@ -675,17 +773,15 @@ process_split_commit () { fi newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $? - debug " newrev is: $newrev" + debug "newrev is: $newrev" cache_set "$rev" "$newrev" cache_set latest_new "$newrev" cache_set latest_old "$rev" } +# Usage: cmd_add REV +# Or: cmd_add REPOSITORY REF cmd_add () { - if test -e "$dir" - then - die "'$dir' already exists. Cannot add." - fi ensure_clean @@ -707,27 +803,35 @@ cmd_add () { cmd_add_repository "$@" else - say "error: parameters were '$@'" + say >&2 "error: parameters were '$*'" die "Provide either a commit or a repository and commit." fi } +# Usage: cmd_add_repository REPOSITORY REFSPEC cmd_add_repository () { + assert test $# = 2 echo "git fetch" "$@" repository=$1 refspec=$2 git fetch "$@" || exit $? - revs=FETCH_HEAD - set -- $revs - cmd_add_commit "$@" + cmd_add_commit FETCH_HEAD } +# Usage: cmd_add_commit REV cmd_add_commit () { - rev=$(git rev-parse $default --revs-only "$@") || exit $? - ensure_single_rev $rev + # The rev has already been validated by cmd_add(), we just + # need to normalize it. + assert test $# = 1 + rev=$(git rev-parse --verify "$1^{commit}") || exit $? debug "Adding $dir as '$rev'..." - git read-tree --prefix="$dir" $rev || exit $? + if test -z "$arg_split_rejoin" + then + # Only bother doing this if this is a genuine 'add', + # not a synthetic 'add' from '--rejoin'. + git read-tree --prefix="$dir" $rev || exit $? + fi git checkout -- "$dir" || exit $? tree=$(git write-tree) || exit $? @@ -739,44 +843,61 @@ cmd_add_commit () { headp= fi - if test -n "$squash" + if test -n "$arg_addmerge_squash" then rev=$(new_squash_commit "" "" "$rev") || exit $? commit=$(add_squashed_msg "$rev" "$dir" | git commit-tree "$tree" $headp -p "$rev") || exit $? else - revp=$(peel_committish "$rev") && + revp=$(peel_committish "$rev") || exit $? commit=$(add_msg "$dir" $headrev "$rev" | git commit-tree "$tree" $headp -p "$revp") || exit $? fi git reset "$commit" || exit $? - say "Added dir '$dir'" + say >&2 "Added dir '$dir'" } +# Usage: cmd_split [REV] cmd_split () { + if test $# -eq 0 + then + rev=$(git rev-parse HEAD) + elif test $# -eq 1 + then + rev=$(git rev-parse -q --verify "$1^{commit}") || + die "'$1' does not refer to a commit" + else + die "You must provide exactly one revision. Got: '$*'" + fi + + if test -n "$arg_split_rejoin" + then + ensure_clean + fi + debug "Splitting $dir..." cache_setup || exit $? - if test -n "$onto" + if test -n "$arg_split_onto" then - debug "Reading history for --onto=$onto..." - git rev-list $onto | + debug "Reading history for --onto=$arg_split_onto..." + git rev-list $arg_split_onto | while read rev do # the 'onto' history is already just the subdir, so # any parent we find there can be used verbatim - debug " cache: $rev" + debug "cache: $rev" cache_set "$rev" "$rev" - done + done || exit $? fi - unrevs="$(find_existing_splits "$dir" "$revs")" + unrevs="$(find_existing_splits "$dir" "$rev")" || exit $? # We can't restrict rev-list to only $dir here, because some of our # parents have the $dir contents the root, and those won't match. # (and rev-list --follow doesn't seem to solve this) - grl='git rev-list --topo-order --reverse --parents $revs $unrevs' + grl='git rev-list --topo-order --reverse --parents $rev $unrevs' revmax=$(eval "$grl" | wc -l) revcount=0 createcount=0 @@ -784,52 +905,58 @@ cmd_split () { eval "$grl" | while read rev parents do - process_split_commit "$rev" "$parents" 0 + process_split_commit "$rev" "$parents" done || exit $? - latest_new=$(cache_get latest_new) + latest_new=$(cache_get latest_new) || exit $? if test -z "$latest_new" then die "No new revisions were found" fi - if test -n "$rejoin" + if test -n "$arg_split_rejoin" then debug "Merging split branch into HEAD..." - latest_old=$(cache_get latest_old) - git merge -s ours \ - --allow-unrelated-histories \ - -m "$(rejoin_msg "$dir" "$latest_old" "$latest_new")" \ - "$latest_new" >&2 || exit $? + latest_old=$(cache_get latest_old) || exit $? + arg_addmerge_message="$(rejoin_msg "$dir" "$latest_old" "$latest_new")" || exit $? + if test -z "$(find_latest_squash "$dir")" + then + cmd_add "$latest_new" >&2 || exit $? + else + cmd_merge "$latest_new" >&2 || exit $? + fi fi - if test -n "$branch" + if test -n "$arg_split_branch" then - if rev_exists "refs/heads/$branch" + if rev_exists "refs/heads/$arg_split_branch" then - if ! rev_is_descendant_of_branch "$latest_new" "$branch" + if ! git merge-base --is-ancestor "$arg_split_branch" "$latest_new" then - die "Branch '$branch' is not an ancestor of commit '$latest_new'." + die "Branch '$arg_split_branch' is not an ancestor of commit '$latest_new'." fi action='Updated' else action='Created' fi git update-ref -m 'subtree split' \ - "refs/heads/$branch" "$latest_new" || exit $? - say "$action branch '$branch'" + "refs/heads/$arg_split_branch" "$latest_new" || exit $? + say >&2 "$action branch '$arg_split_branch'" fi echo "$latest_new" exit 0 } +# Usage: cmd_merge REV cmd_merge () { - rev=$(git rev-parse $default --revs-only "$@") || exit $? - ensure_single_rev $rev + test $# -eq 1 || + die "You must provide exactly one revision. Got: '$*'" + rev=$(git rev-parse -q --verify "$1^{commit}") || + die "'$1' does not refer to a commit" ensure_clean - if test -n "$squash" + if test -n "$arg_addmerge_squash" then - first_split="$(find_latest_squash "$dir")" + first_split="$(find_latest_squash "$dir")" || exit $? if test -z "$first_split" then die "Can't squash-merge: '$dir' was never added." @@ -839,7 +966,7 @@ cmd_merge () { sub=$2 if test "$sub" = "$rev" then - say "Subtree is already at commit $rev." + say >&2 "Subtree is already at commit $rev." exit 0 fi new=$(new_squash_commit "$old" "$sub" "$rev") || exit $? @@ -847,26 +974,16 @@ cmd_merge () { rev="$new" fi - version=$(git version) - if test "$version" \< "git version 1.7" + if test -n "$arg_addmerge_message" then - if test -n "$message" - then - git merge -s subtree --message="$message" "$rev" - else - git merge -s subtree "$rev" - fi + git merge -Xsubtree="$arg_prefix" \ + --message="$arg_addmerge_message" "$rev" else - if test -n "$message" - then - git merge -Xsubtree="$prefix" \ - --message="$message" "$rev" - else - git merge -Xsubtree="$prefix" $rev - fi + git merge -Xsubtree="$arg_prefix" $rev fi } +# Usage: cmd_pull REPOSITORY REMOTEREF cmd_pull () { if test $# -ne 2 then @@ -875,27 +992,36 @@ cmd_pull () { ensure_clean ensure_valid_ref_format "$2" git fetch "$@" || exit $? - revs=FETCH_HEAD - set -- $revs - cmd_merge "$@" + cmd_merge FETCH_HEAD } +# Usage: cmd_push REPOSITORY [+][LOCALREV:]REMOTEREF cmd_push () { if test $# -ne 2 then - die "You must provide <repository> <ref>" + die "You must provide <repository> <refspec>" fi - ensure_valid_ref_format "$2" if test -e "$dir" then repository=$1 - refspec=$2 + refspec=${2#+} + remoteref=${refspec#*:} + if test "$remoteref" = "$refspec" + then + localrevname_presplit=HEAD + else + localrevname_presplit=${refspec%%:*} + fi + ensure_valid_ref_format "$remoteref" + localrev_presplit=$(git rev-parse -q --verify "$localrevname_presplit^{commit}") || + die "'$localrevname_presplit' does not refer to a commit" + echo "git push using: " "$repository" "$refspec" - localrev=$(git subtree split --prefix="$prefix") || die - git push "$repository" "$localrev":"refs/heads/$refspec" + localrev=$(cmd_split "$localrev_presplit") || die + git push "$repository" "$localrev":"refs/heads/$remoteref" else die "'$dir' must already exist. Try 'git subtree add'." fi } -"cmd_$command" "$@" +main "$@" diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt index 0db02fe3c0..9cddfa2654 100644 --- a/contrib/subtree/git-subtree.txt +++ b/contrib/subtree/git-subtree.txt @@ -9,13 +9,14 @@ git-subtree - Merge subtrees together and split repository into subtrees SYNOPSIS -------- [verse] -'git subtree' add -P <prefix> <commit> -'git subtree' add -P <prefix> <repository> <ref> -'git subtree' pull -P <prefix> <repository> <ref> -'git subtree' push -P <prefix> <repository> <ref> -'git subtree' merge -P <prefix> <commit> -'git subtree' split -P <prefix> [OPTIONS] [<commit>] +'git subtree' [<options>] -P <prefix> add <local-commit> +'git subtree' [<options>] -P <prefix> add <repository> <remote-ref> +'git subtree' [<options>] -P <prefix> merge <local-commit> +'git subtree' [<options>] -P <prefix> split [<local-commit>] +[verse] +'git subtree' [<options>] -P <prefix> pull <repository> <remote-ref> +'git subtree' [<options>] -P <prefix> push <repository> <refspec> DESCRIPTION ----------- @@ -28,7 +29,7 @@ as a subdirectory of your application. Subtrees are not to be confused with submodules, which are meant for the same task. Unlike submodules, subtrees do not need any special -constructions (like .gitmodules files or gitlinks) be present in +constructions (like '.gitmodules' files or gitlinks) be present in your repository, and do not force end-users of your repository to do anything special or to understand how subtrees work. A subtree is just a subdirectory that can be @@ -59,70 +60,71 @@ project as much as possible. That is, if you make a change that affects both the library and the main application, commit it in two pieces. That way, when you split the library commits out later, their descriptions will still make sense. But if this -isn't important to you, it's not *necessary*. git subtree will +isn't important to you, it's not *necessary*. 'git subtree' will simply leave out the non-library-related parts of the commit when it splits it out into the subproject later. COMMANDS -------- -add:: +add <local-commit>:: +add <repository> <remote-ref>:: Create the <prefix> subtree by importing its contents - from the given <commit> or <repository> and remote <ref>. + from the given <local-commit> or <repository> and <remote-ref>. A new commit is created automatically, joining the imported - project's history with your own. With '--squash', imports + project's history with your own. With '--squash', import only a single commit from the subproject, rather than its entire history. -merge:: - Merge recent changes up to <commit> into the <prefix> +merge <local-commit>:: + Merge recent changes up to <local-commit> into the <prefix> subtree. As with normal 'git merge', this doesn't remove your own local changes; it just merges those - changes into the latest <commit>. With '--squash', - creates only one commit that contains all the changes, + changes into the latest <local-commit>. With '--squash', + create 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. - -pull:: - Exactly like 'merge', but parallels 'git pull' in that - it fetches the given ref from the specified remote - repository. - -push:: - Does a 'split' (see below) using the <prefix> supplied - and then does a 'git push' to push the result to the - repository and ref. This can be used to push your - subtree to different branches of the remote repository. - -split:: + +split [<local-commit>]:: Extract a new, synthetic project history from the - history of the <prefix> subtree. The new history + history of the <prefix> subtree of <local-commit>, or of + HEAD if no <local-commit> is given. The new history includes only the commits (including merges) that affected <prefix>, and each of those commits now has the 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. +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. +identical (i.e. to produce the same commit IDs) as long as the +settings passed to 'split' (such as '--annotate') are the same. +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. +pull <repository> <remote-ref>:: + Exactly like 'merge', but parallels 'git pull' in that + it fetches the given ref from the specified remote + repository. + +push <repository> [+][<local-commit>:]<remote-ref>:: + Does a 'split' using the <prefix> subtree of <local-commit> + and then does a 'git push' to push the result to the + <repository> and <remote-ref>. This can be used to push your + subtree to different branches of the remote repository. Just + as with 'split', if no <local-commit> is given, then HEAD is + used. The optional leading '+' is ignored. -OPTIONS -------- +OPTIONS FOR ALL COMMANDS +------------------------ -q:: --quiet:: Suppress unnecessary output messages on stderr. @@ -137,21 +139,16 @@ OPTIONS want to manipulate. This option is mandatory for all commands. --m <message>:: ---message=<message>:: - This option is only valid for add, merge, pull, and split --rejoin. - Specify <message> as the commit message for the merge commit. - +OPTIONS FOR 'add' AND 'merge' (ALSO: 'pull', 'split --rejoin', AND 'push --rejoin') +----------------------------------------------------------------------------------- +These options for 'add' and 'merge' may also be given to 'pull' (which +wraps 'merge'), 'split --rejoin' (which wraps either 'add' or 'merge' +as appropriate), and 'push --rejoin' (which wraps 'split --rejoin'). -OPTIONS FOR add, merge, and pull --------------------------------- --squash:: - 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. + 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 @@ -174,57 +171,53 @@ 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. +-m <message>:: +--message=<message>:: + Specify <message> as the commit message for the merge commit. + +OPTIONS FOR 'split' (ALSO: 'push') +---------------------------------- +These options for 'split' may also be given to 'push' (which wraps +'split'). -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. + 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, +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-joins' disables this behavior, 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 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. + 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 @@ -235,13 +228,12 @@ 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. +If you do all your merges with '--squash', make sure you also use +'--squash' when you 'split --rejoin'. -EXAMPLE 1. Add command ----------------------- +EXAMPLE 1. 'add' command +------------------------ Let's assume that you have a local repository that you would like to add an external vendor library to. In this case we will add the git-subtree repository as a subdirectory of your already existing @@ -253,15 +245,15 @@ git-extensions repository in ~/git-extensions/: 'master' needs to be a valid remote ref and can be a different branch name -You can omit the --squash flag, but doing so will increase the number +You can omit the '--squash' flag, but doing so will increase the number of commits that are included in your local repository. We now have a ~/git-extensions/git-subtree directory containing code from the master branch of git://github.com/apenwarr/git-subtree.git in our git-extensions repository. -EXAMPLE 2. Extract a subtree using commit, merge and pull ---------------------------------------------------------- +EXAMPLE 2. Extract a subtree using 'commit', 'merge' and 'pull' +--------------------------------------------------------------- Let's use the repository for the git source code as an example. First, get your own copy of the git.git repository: @@ -269,7 +261,7 @@ First, get your own copy of the git.git repository: $ cd test-git gitweb (commit 1130ef3) was merged into git as of commit -0a8f4f0, after which it was no longer maintained separately. +0a8f4f0, after which it was no longer maintained separately. But imagine it had been maintained separately, and we wanted to extract git's changes to gitweb since that time, to share with the upstream. You could do this: @@ -279,14 +271,14 @@ the upstream. You could do this: --branch gitweb-latest $ gitk gitweb-latest $ git push git@github.com:whatever/gitweb.git gitweb-latest:master - + (We use '0a8f4f0^..' because that means "all the changes from 0a8f4f0 to the current version, including 0a8f4f0 itself.") If gitweb had originally been merged using 'git subtree add' (or -a previous split had already been done with --rejoin specified) +a previous split had already been done with '--rejoin' specified) then you can do all your splits without having to remember any -weird commit ids: +weird commit IDs: $ git subtree split --prefix=gitweb --annotate='(split) ' --rejoin \ --branch gitweb-latest2 @@ -313,7 +305,7 @@ And fast forward again: $ git subtree merge --prefix=gitweb --squash gitweb-latest And notice that your change is still intact: - + $ ls -l gitweb/myfile And you can split it out and look at your changes versus @@ -321,8 +313,8 @@ the standard gitweb: git log gitweb-latest..$(git subtree split --prefix=gitweb) -EXAMPLE 3. Extract a subtree using branch ------------------------------------------ +EXAMPLE 3. Extract a subtree using a branch +------------------------------------------- Suppose you have a source directory with many files and subdirectories, and you want to extract the lib directory to its own git project. Here's a short way to do it: diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh index 57ff4b25c1..4153b65321 100755 --- a/contrib/subtree/t/t7900-subtree.sh +++ b/contrib/subtree/t/t7900-subtree.sh @@ -5,72 +5,24 @@ # test_description='Basic porcelain support for subtrees -This test verifies the basic operation of the add, pull, merge -and split subcommands of git subtree. +This test verifies the basic operation of the add, merge, split, pull, +and push subcommands of git subtree. ' TEST_DIRECTORY=$(pwd)/../../../t -export TEST_DIRECTORY +. "$TEST_DIRECTORY"/test-lib.sh -. ../../../t/test-lib.sh - -subtree_test_create_repo() -{ +# Use our own wrapper around test-lib.sh's test_create_repo, in order +# to set log.date=relative. `git subtree` parses the output of `git +# log`, and so it must be careful to not be affected by settings that +# change the `git log` output. We test this by setting +# log.date=relative for every repo in the tests. +subtree_test_create_repo () { test_create_repo "$1" && - ( - cd "$1" && - git config log.date relative - ) -} - -create() -{ - echo "$1" >"$1" && - git add "$1" -} - -check_equal() -{ - test_debug 'echo' - test_debug "echo \"check a:\" \"{$1}\"" - test_debug "echo \" b:\" \"{$2}\"" - if [ "$1" = "$2" ]; then - return 0 - else - return 1 - fi -} - -undo() -{ - git reset --hard HEAD~ + git -C "$1" config log.date relative } -# Make sure no patch changes more than one file. -# The original set of commits changed only one file each. -# A multi-file change would imply that we pruned commits -# too aggressively. -join_commits() -{ - commit= - all= - while read x y; do - if [ -z "$x" ]; then - continue - elif [ "$x" = "commit:" ]; then - if [ -n "$commit" ]; then - echo "$commit $all" - all= - fi - commit="$y" - else - all="$all $y" - fi - done - echo "$commit $all" -} - -test_create_commit() ( +test_create_commit () ( repo=$1 && commit=$2 && cd "$repo" && @@ -81,98 +33,116 @@ test_create_commit() ( git commit -m "$commit" || error "Could not commit" ) -last_commit_message() -{ - git log --pretty=format:%s -1 +test_wrong_flag() { + test_must_fail "$@" >out 2>err && + test_must_be_empty out && + grep "flag does not make sense with" err } -subtree_test_count=0 -next_test() { - subtree_test_count=$(($subtree_test_count+1)) +last_commit_subject () { + git log --pretty=format:%s -1 } +test_expect_success 'shows short help text for -h' ' + test_expect_code 129 git subtree -h >out 2>err && + test_must_be_empty err && + grep -e "^ *or: git subtree pull" out && + grep -e --annotate out +' + # # Tests for 'git subtree add' # -next_test test_expect_success 'no merge from non-existent subtree' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && test_must_fail git subtree merge --prefix="sub dir" FETCH_HEAD ) ' -next_test test_expect_success 'no pull from non-existent subtree' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && - test_must_fail git subtree pull --prefix="sub dir" ./"sub proj" master - )' + cd "$test_count" && + git fetch ./"sub proj" HEAD && + test_must_fail git subtree pull --prefix="sub dir" ./"sub proj" HEAD + ) +' + +test_expect_success 'add rejects flags for split' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + test_wrong_flag git subtree add --prefix="sub dir" --annotate=foo FETCH_HEAD && + test_wrong_flag git subtree add --prefix="sub dir" --branch=foo FETCH_HEAD && + test_wrong_flag git subtree add --prefix="sub dir" --ignore-joins FETCH_HEAD && + test_wrong_flag git subtree add --prefix="sub dir" --onto=foo FETCH_HEAD && + test_wrong_flag git subtree add --prefix="sub dir" --rejoin FETCH_HEAD + ) +' -next_test test_expect_success 'add subproj as subtree into sub dir/ with --prefix' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD && - check_equal "$(last_commit_message)" "Add '\''sub dir/'\'' from commit '\''$(git rev-parse FETCH_HEAD)'\''" + test "$(last_commit_subject)" = "Add '\''sub dir/'\'' from commit '\''$(git rev-parse FETCH_HEAD)'\''" ) ' -next_test test_expect_success 'add subproj as subtree into sub dir/ with --prefix and --message' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" --message="Added subproject" FETCH_HEAD && - check_equal "$(last_commit_message)" "Added subproject" + test "$(last_commit_subject)" = "Added subproject" ) ' -next_test test_expect_success 'add subproj as subtree into sub dir/ with --prefix as -P and --message as -m' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add -P "sub dir" -m "Added subproject" FETCH_HEAD && - check_equal "$(last_commit_message)" "Added subproject" + test "$(last_commit_subject)" = "Added subproject" ) ' -next_test test_expect_success 'add subproj as subtree into sub dir/ with --squash and --prefix and --message' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" --message="Added subproject with squash" --squash FETCH_HEAD && - check_equal "$(last_commit_message)" "Added subproject with squash" + test "$(last_commit_subject)" = "Added subproject with squash" ) ' @@ -180,100 +150,117 @@ test_expect_success 'add subproj as subtree into sub dir/ with --squash and --pr # Tests for 'git subtree merge' # -next_test +test_expect_success 'merge rejects flags for split' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count/sub proj" sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + test_wrong_flag git subtree merge --prefix="sub dir" --annotate=foo FETCH_HEAD && + test_wrong_flag git subtree merge --prefix="sub dir" --branch=foo FETCH_HEAD && + test_wrong_flag git subtree merge --prefix="sub dir" --ignore-joins FETCH_HEAD && + test_wrong_flag git subtree merge --prefix="sub dir" --onto=foo FETCH_HEAD && + test_wrong_flag git subtree merge --prefix="sub dir" --rejoin FETCH_HEAD + ) +' + test_expect_success 'merge new subproj history into sub dir/ with --prefix' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub2 && + test_create_commit "$test_count/sub proj" sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && - check_equal "$(last_commit_message)" "Merge commit '\''$(git rev-parse FETCH_HEAD)'\''" + test "$(last_commit_subject)" = "Merge commit '\''$(git rev-parse FETCH_HEAD)'\''" ) ' -next_test test_expect_success 'merge new subproj history into sub dir/ with --prefix and --message' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub2 && + test_create_commit "$test_count/sub proj" sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" --message="Merged changes from subproject" FETCH_HEAD && - check_equal "$(last_commit_message)" "Merged changes from subproject" + test "$(last_commit_subject)" = "Merged changes from subproject" ) ' -next_test test_expect_success 'merge new subproj history into sub dir/ with --squash and --prefix and --message' ' - subtree_test_create_repo "$subtree_test_count/sub proj" && - subtree_test_create_repo "$subtree_test_count" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count/sub proj" && + subtree_test_create_repo "$test_count" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub2 && + test_create_commit "$test_count/sub proj" sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" --message="Merged changes from subproject using squash" --squash FETCH_HEAD && - check_equal "$(last_commit_message)" "Merged changes from subproject using squash" + test "$(last_commit_subject)" = "Merged changes from subproject using squash" ) ' -next_test test_expect_success 'merge the added subproj again, should do nothing' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD && # this shouldn not actually do anything, since FETCH_HEAD # is already a parent result=$(git merge -s ours -m "merge -s -ours" FETCH_HEAD) && - check_equal "${result}" "Already up to date." + test "${result}" = "Already up to date." ) ' -next_test test_expect_success 'merge new subproj history into subdir/ with a slash appended to the argument of --prefix' ' - test_create_repo "$test_count" && - test_create_repo "$test_count/subproj" && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/subproj" && test_create_commit "$test_count" main1 && test_create_commit "$test_count/subproj" sub1 && ( cd "$test_count" && - git fetch ./subproj master && + git fetch ./subproj HEAD && git subtree add --prefix=subdir/ FETCH_HEAD ) && test_create_commit "$test_count/subproj" sub2 && ( cd "$test_count" && - git fetch ./subproj master && + git fetch ./subproj HEAD && git subtree merge --prefix=subdir/ FETCH_HEAD && - check_equal "$(last_commit_message)" "Merge commit '\''$(git rev-parse FETCH_HEAD)'\''" + test "$(last_commit_subject)" = "Merge commit '\''$(git rev-parse FETCH_HEAD)'\''" ) ' @@ -281,18 +268,17 @@ test_expect_success 'merge new subproj history into subdir/ with a slash appende # Tests for 'git subtree split' # -next_test test_expect_success 'split requires option --prefix' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD && - 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 "printf '"actual: "'" && @@ -301,18 +287,17 @@ test_expect_success 'split requires option --prefix' ' ) ' -next_test test_expect_success 'split requires path given by option --prefix must exist' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD && - 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 "printf '"actual: "'" && @@ -321,222 +306,696 @@ test_expect_success 'split requires path given by option --prefix must exist' ' ) ' -next_test +test_expect_success 'split rejects flags for add' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && + test_wrong_flag git subtree split --prefix="sub dir" --squash && + test_wrong_flag git subtree split --prefix="sub dir" --message=foo + ) +' + test_expect_success 'split sub dir/ with --rejoin' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && git subtree split --prefix="sub dir" --annotate="*" --rejoin && - check_equal "$(last_commit_message)" "Split '\''sub dir/'\'' into commit '\''$split_hash'\''" + test "$(last_commit_subject)" = "Split '\''sub dir/'\'' into commit '\''$split_hash'\''" ) - ' +' -next_test test_expect_success 'split sub dir/ with --rejoin from scratch' ' - subtree_test_create_repo "$subtree_test_count" && - test_create_commit "$subtree_test_count" main1 && + subtree_test_create_repo "$test_count" && + test_create_commit "$test_count" main1 && ( - cd "$subtree_test_count" && + cd "$test_count" && mkdir "sub dir" && echo file >"sub dir"/file && git add "sub dir/file" && git commit -m"sub dir file" && split_hash=$(git subtree split --prefix="sub dir" --rejoin) && git subtree split --prefix="sub dir" --rejoin && - check_equal "$(last_commit_message)" "Split '\''sub dir/'\'' into commit '\''$split_hash'\''" + test "$(last_commit_subject)" = "Split '\''sub dir/'\'' into commit '\''$split_hash'\''" ) - ' +' -next_test test_expect_success 'split sub dir/ with --rejoin and --message' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && git subtree split --prefix="sub dir" --message="Split & rejoin" --annotate="*" --rejoin && - check_equal "$(last_commit_message)" "Split & rejoin" + test "$(last_commit_subject)" = "Split & rejoin" + ) +' + +test_expect_success 'split "sub dir"/ with --rejoin and --squash' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" --squash FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git subtree pull --prefix="sub dir" --squash ./"sub proj" HEAD && + MAIN=$(git rev-parse --verify HEAD) && + SUB=$(git -C "sub proj" rev-parse --verify HEAD) && + + SPLIT=$(git subtree split --prefix="sub dir" --annotate="*" --rejoin --squash) && + + test_must_fail git merge-base --is-ancestor $SUB HEAD && + test_must_fail git merge-base --is-ancestor $SPLIT HEAD && + git rev-list HEAD ^$MAIN >commit-list && + test_line_count = 2 commit-list && + test "$(git rev-parse --verify HEAD:)" = "$(git rev-parse --verify $MAIN:)" && + test "$(git rev-parse --verify HEAD:"sub dir")" = "$(git rev-parse --verify $SPLIT:)" && + test "$(git rev-parse --verify HEAD^1)" = $MAIN && + test "$(git rev-parse --verify HEAD^2)" != $SPLIT && + test "$(git rev-parse --verify HEAD^2:)" = "$(git rev-parse --verify $SPLIT:)" && + test "$(last_commit_subject)" = "Split '\''sub dir/'\'' into commit '\''$SPLIT'\''" ) ' -next_test +test_expect_success 'split then pull "sub dir"/ with --rejoin and --squash' ' + # 1. "add" + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + git -C "$test_count" subtree --prefix="sub dir" add --squash ./"sub proj" HEAD && + + # 2. commit from parent + test_create_commit "$test_count" "sub dir"/main-sub1 && + + # 3. "split --rejoin --squash" + git -C "$test_count" subtree --prefix="sub dir" split --rejoin --squash && + + # 4. "pull --squash" + test_create_commit "$test_count/sub proj" sub2 && + git -C "$test_count" subtree -d --prefix="sub dir" pull --squash ./"sub proj" HEAD && + + test_must_fail git merge-base HEAD FETCH_HEAD +' + test_expect_success 'split "sub dir"/ with --branch' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br && - check_equal "$(git rev-parse subproj-br)" "$split_hash" + test "$(git rev-parse subproj-br)" = "$split_hash" ) ' -next_test test_expect_success 'check hash of split' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br && - check_equal "$(git rev-parse subproj-br)" "$split_hash" && + test "$(git rev-parse subproj-br)" = "$split_hash" && # Check hash of split new_hash=$(git rev-parse subproj-br^2) && ( cd ./"sub proj" && subdir_hash=$(git rev-parse HEAD) && - check_equal ''"$new_hash"'' "$subdir_hash" + test "$new_hash" = "$subdir_hash" ) ) ' -next_test test_expect_success 'split "sub dir"/ with --branch for an existing branch' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git branch subproj-br FETCH_HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br && - check_equal "$(git rev-parse subproj-br)" "$split_hash" + test "$(git rev-parse subproj-br)" = "$split_hash" ) ' -next_test test_expect_success 'split "sub dir"/ with --branch for an incompatible branch' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && + cd "$test_count" && git branch init HEAD && - git fetch ./"sub proj" master && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && test_must_fail git subtree split --prefix="sub dir" --branch init ) ' # +# Tests for 'git subtree pull' +# + +test_expect_success 'pull requires option --prefix' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count/sub proj" sub2 && + ( + cd "$test_count" && + test_must_fail git subtree pull ./"sub proj" HEAD >out 2>err && + + echo "You must provide the --prefix option." >expected && + test_must_be_empty out && + test_cmp expected err + ) +' + +test_expect_success 'pull requires path given by option --prefix must exist' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + test_must_fail git subtree pull --prefix="sub dir" ./"sub proj" HEAD >out 2>err && + + echo "'\''sub dir'\'' does not exist; use '\''git subtree add'\''" >expected && + test_must_be_empty out && + test_cmp expected err + ) +' + +test_expect_success 'pull basic operation' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count/sub proj" sub2 && + ( + cd "$test_count" && + exp=$(git -C "sub proj" rev-parse --verify HEAD:) && + git subtree pull --prefix="sub dir" ./"sub proj" HEAD && + act=$(git rev-parse --verify HEAD:"sub dir") && + test "$act" = "$exp" + ) +' + +test_expect_success 'pull rejects flags for split' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count/sub proj" sub2 && + ( + test_must_fail git subtree pull --prefix="sub dir" --annotate=foo ./"sub proj" HEAD && + test_must_fail git subtree pull --prefix="sub dir" --branch=foo ./"sub proj" HEAD && + test_must_fail git subtree pull --prefix="sub dir" --ignore-joins ./"sub proj" HEAD && + test_must_fail git subtree pull --prefix="sub dir" --onto=foo ./"sub proj" HEAD && + test_must_fail git subtree pull --prefix="sub dir" --rejoin ./"sub proj" HEAD + ) +' + +# +# Tests for 'git subtree push' +# + +test_expect_success 'push requires option --prefix' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD && + echo "You must provide the --prefix option." >expected && + test_must_fail git subtree push "./sub proj" from-mainline >actual 2>&1 && + test_debug "printf '"expected: "'" && + test_debug "cat expected" && + test_debug "printf '"actual: "'" && + test_debug "cat actual" && + test_cmp expected actual + ) +' + +test_expect_success 'push requires path given by option --prefix must exist' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD && + echo "'\''non-existent-directory'\'' does not exist; use '\''git subtree add'\''" >expected && + test_must_fail git subtree push --prefix=non-existent-directory "./sub proj" from-mainline >actual 2>&1 && + test_debug "printf '"expected: "'" && + test_debug "cat expected" && + test_debug "printf '"actual: "'" && + test_debug "cat actual" && + test_cmp expected actual + ) +' + +test_expect_success 'push rejects flags for add' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + test_wrong_flag git subtree split --prefix="sub dir" --squash ./"sub proj" from-mainline && + test_wrong_flag git subtree split --prefix="sub dir" --message=foo ./"sub proj" from-mainline + ) +' + +test_expect_success 'push basic operation' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + before=$(git rev-parse --verify HEAD) && + split_hash=$(git subtree split --prefix="sub dir") && + git subtree push --prefix="sub dir" ./"sub proj" from-mainline && + test "$before" = "$(git rev-parse --verify HEAD)" && + test "$split_hash" = "$(git -C "sub proj" rev-parse --verify refs/heads/from-mainline)" + ) +' + +test_expect_success 'push sub dir/ with --rejoin' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && + git subtree push --prefix="sub dir" --annotate="*" --rejoin ./"sub proj" from-mainline && + test "$(last_commit_subject)" = "Split '\''sub dir/'\'' into commit '\''$split_hash'\''" && + test "$split_hash" = "$(git -C "sub proj" rev-parse --verify refs/heads/from-mainline)" + ) +' + +test_expect_success 'push sub dir/ with --rejoin from scratch' ' + subtree_test_create_repo "$test_count" && + test_create_commit "$test_count" main1 && + ( + cd "$test_count" && + mkdir "sub dir" && + echo file >"sub dir"/file && + git add "sub dir/file" && + git commit -m"sub dir file" && + split_hash=$(git subtree split --prefix="sub dir" --rejoin) && + git init --bare "sub proj.git" && + git subtree push --prefix="sub dir" --rejoin ./"sub proj.git" from-mainline && + test "$(last_commit_subject)" = "Split '\''sub dir/'\'' into commit '\''$split_hash'\''" && + test "$split_hash" = "$(git -C "sub proj.git" rev-parse --verify refs/heads/from-mainline)" + ) +' + +test_expect_success 'push sub dir/ with --rejoin and --message' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + git subtree push --prefix="sub dir" --message="Split & rejoin" --annotate="*" --rejoin ./"sub proj" from-mainline && + test "$(last_commit_subject)" = "Split & rejoin" && + split_hash="$(git rev-parse --verify HEAD^2)" && + test "$split_hash" = "$(git -C "sub proj" rev-parse --verify refs/heads/from-mainline)" + ) +' + +test_expect_success 'push "sub dir"/ with --rejoin and --squash' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" --squash FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git subtree pull --prefix="sub dir" --squash ./"sub proj" HEAD && + MAIN=$(git rev-parse --verify HEAD) && + SUB=$(git -C "sub proj" rev-parse --verify HEAD) && + + SPLIT=$(git subtree split --prefix="sub dir" --annotate="*") && + git subtree push --prefix="sub dir" --annotate="*" --rejoin --squash ./"sub proj" from-mainline && + + test_must_fail git merge-base --is-ancestor $SUB HEAD && + test_must_fail git merge-base --is-ancestor $SPLIT HEAD && + git rev-list HEAD ^$MAIN >commit-list && + test_line_count = 2 commit-list && + test "$(git rev-parse --verify HEAD:)" = "$(git rev-parse --verify $MAIN:)" && + test "$(git rev-parse --verify HEAD:"sub dir")" = "$(git rev-parse --verify $SPLIT:)" && + test "$(git rev-parse --verify HEAD^1)" = $MAIN && + test "$(git rev-parse --verify HEAD^2)" != $SPLIT && + test "$(git rev-parse --verify HEAD^2:)" = "$(git rev-parse --verify $SPLIT:)" && + test "$(last_commit_subject)" = "Split '\''sub dir/'\'' into commit '\''$SPLIT'\''" && + test "$SPLIT" = "$(git -C "sub proj" rev-parse --verify refs/heads/from-mainline)" + ) +' + +test_expect_success 'push "sub dir"/ with --branch' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && + git subtree push --prefix="sub dir" --annotate="*" --branch subproj-br ./"sub proj" from-mainline && + test "$(git rev-parse subproj-br)" = "$split_hash" && + test "$split_hash" = "$(git -C "sub proj" rev-parse --verify refs/heads/from-mainline)" + ) +' + +test_expect_success 'check hash of push' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && + git subtree push --prefix="sub dir" --annotate="*" --branch subproj-br ./"sub proj" from-mainline && + test "$(git rev-parse subproj-br)" = "$split_hash" && + # Check hash of split + new_hash=$(git rev-parse subproj-br^2) && + ( + cd ./"sub proj" && + subdir_hash=$(git rev-parse HEAD) && + test "$new_hash" = "$subdir_hash" + ) && + test "$split_hash" = "$(git -C "sub proj" rev-parse --verify refs/heads/from-mainline)" + ) +' + +test_expect_success 'push "sub dir"/ with --branch for an existing branch' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git branch subproj-br FETCH_HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + split_hash=$(git subtree split --prefix="sub dir" --annotate="*") && + git subtree push --prefix="sub dir" --annotate="*" --branch subproj-br ./"sub proj" from-mainline && + test "$(git rev-parse subproj-br)" = "$split_hash" && + test "$split_hash" = "$(git -C "sub proj" rev-parse --verify refs/heads/from-mainline)" + ) +' + +test_expect_success 'push "sub dir"/ with --branch for an incompatible branch' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git branch init HEAD && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree merge --prefix="sub dir" FETCH_HEAD && + test_must_fail git subtree push --prefix="sub dir" --branch init "./sub proj" from-mainline + ) +' + +test_expect_success 'push "sub dir"/ with a local rev' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD + ) && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd "$test_count" && + bad_tree=$(git rev-parse --verify HEAD:"sub dir") && + good_tree=$(git rev-parse --verify HEAD^:"sub dir") && + git subtree push --prefix="sub dir" --annotate="*" ./"sub proj" HEAD^:from-mainline && + split_tree=$(git -C "sub proj" rev-parse --verify refs/heads/from-mainline:) && + test "$split_tree" = "$good_tree" + ) +' + +# # Validity checking # -next_test test_expect_success 'make sure exactly the right set of files ends up in the subproj' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count/sub proj" sub3 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub3 && + test_create_commit "$test_count/sub proj" sub3 && + test_create_commit "$test_count" "sub dir"/main-sub3 && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub4 && + test_create_commit "$test_count/sub proj" sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub4 && + test_create_commit "$test_count" "sub dir"/main-sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD && @@ -547,46 +1006,45 @@ test_expect_success 'make sure exactly the right set of files ends up in the sub ) ' -next_test test_expect_success 'make sure the subproj *only* contains commits that affect the "sub dir"' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count/sub proj" sub3 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub3 && + test_create_commit "$test_count/sub proj" sub3 && + test_create_commit "$test_count" "sub dir"/main-sub3 && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub4 && + test_create_commit "$test_count/sub proj" sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub4 && + test_create_commit "$test_count" "sub dir"/main-sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD && @@ -598,52 +1056,51 @@ test_expect_success 'make sure the subproj *only* contains commits that affect t ) ' -next_test test_expect_success 'make sure exactly the right set of files ends up in the mainline' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count/sub proj" sub3 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub3 && + test_create_commit "$test_count/sub proj" sub3 && + test_create_commit "$test_count" "sub dir"/main-sub3 && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub4 && + test_create_commit "$test_count/sub proj" sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub4 && + test_create_commit "$test_count" "sub dir"/main-sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && ( - cd "$subtree_test_count" && - git subtree pull --prefix="sub dir" ./"sub proj" master && + cd "$test_count" && + git subtree pull --prefix="sub dir" ./"sub proj" HEAD && test_write_lines main1 main2 >chkm && test_write_lines main-sub1 main-sub2 main-sub3 main-sub4 >chkms && @@ -657,53 +1114,52 @@ test_expect_success 'make sure exactly the right set of files ends up in the mai ) ' -next_test test_expect_success 'make sure each filename changed exactly once in the entire history' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && + cd "$test_count" && git config log.date relative && - git fetch ./"sub proj" master && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count/sub proj" sub3 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub3 && + test_create_commit "$test_count/sub proj" sub3 && + test_create_commit "$test_count" "sub dir"/main-sub3 && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub4 && + test_create_commit "$test_count/sub proj" sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub4 && + test_create_commit "$test_count" "sub dir"/main-sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && ( - cd "$subtree_test_count" && - git subtree pull --prefix="sub dir" ./"sub proj" master && + cd "$test_count" && + git subtree pull --prefix="sub dir" ./"sub proj" HEAD && test_write_lines main1 main2 >chkm && test_write_lines sub1 sub2 sub3 sub4 >chks && @@ -723,105 +1179,103 @@ test_expect_success 'make sure each filename changed exactly once in the entire ) ' -next_test test_expect_success 'make sure the --rejoin commits never make it into subproj' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count/sub proj" sub3 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub3 && + test_create_commit "$test_count/sub proj" sub3 && + test_create_commit "$test_count" "sub dir"/main-sub3 && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub4 && + test_create_commit "$test_count/sub proj" sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub4 && + test_create_commit "$test_count" "sub dir"/main-sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && ( - cd "$subtree_test_count" && - git subtree pull --prefix="sub dir" ./"sub proj" master && - check_equal "$(git log --pretty=format:"%s" HEAD^2 | grep -i split)" "" + cd "$test_count" && + git subtree pull --prefix="sub dir" ./"sub proj" HEAD && + test "$(git log --pretty=format:"%s" HEAD^2 | grep -i split)" = "" ) ' -next_test test_expect_success 'make sure no "git subtree" tagged commits make it into subproj' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count/sub proj" sub3 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub3 && + test_create_commit "$test_count/sub proj" sub3 && + test_create_commit "$test_count" "sub dir"/main-sub3 && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub4 && + test_create_commit "$test_count/sub proj" sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub4 && + test_create_commit "$test_count" "sub dir"/main-sub4 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --annotate="*" --branch subproj-br --rejoin ) && ( - cd "$subtree_test_count/sub proj" && + cd "$test_count/sub proj" && git fetch .. subproj-br && git merge FETCH_HEAD ) && ( - cd "$subtree_test_count" && - git subtree pull --prefix="sub dir" ./"sub proj" master && + cd "$test_count" && + git subtree pull --prefix="sub dir" ./"sub proj" HEAD && # 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.*:")" "" + test "$(git log --pretty=format:"%s%n%b" HEAD^2 | grep "git-subtree.*:")" = "" ) ' @@ -829,145 +1283,135 @@ test_expect_success 'make sure no "git subtree" tagged commits make it into subp # A new set of tests # -next_test test_expect_success 'make sure "git subtree split" find the correct parent' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub2 && + test_create_commit "$test_count/sub proj" sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git branch subproj-ref FETCH_HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" "sub dir"/main-sub1 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --branch subproj-br && # at this point, the new commit parent should be subproj-ref, if it is - # not, something went wrong (the "newparent" of "master~" commit should + # not, something went wrong (the "newparent" of "HEAD~" commit should # have been sub2, but it was not, because its cache was not set to # itself) - check_equal "$(git log --pretty=format:%P -1 subproj-br)" "$(git rev-parse subproj-ref)" + test "$(git log --pretty=format:%P -1 subproj-br)" = "$(git rev-parse subproj-ref)" ) ' -next_test test_expect_success 'split a new subtree without --onto option' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count/sub proj" sub2 && + test_create_commit "$test_count/sub proj" sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" "sub dir"/main-sub1 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --branch subproj-br ) && - mkdir "$subtree_test_count"/"sub dir2" && - test_create_commit "$subtree_test_count" "sub dir2"/main-sub2 && + mkdir "$test_count"/"sub dir2" && + test_create_commit "$test_count" "sub dir2"/main-sub2 && ( - cd "$subtree_test_count" && + cd "$test_count" && # 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="sub dir2" --branch subproj2-br && - check_equal "$(git log --pretty=format:%P -1 subproj2-br)" "" + test "$(git log --pretty=format:%P -1 subproj2-br)" = "" ) ' -next_test test_expect_success 'verify one file change per commit' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git branch sub1 FETCH_HEAD && git subtree add --prefix="sub dir" sub1 ) && - test_create_commit "$subtree_test_count/sub proj" sub2 && + test_create_commit "$test_count/sub proj" sub2 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" "sub dir"/main-sub1 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir" --branch subproj-br ) && - mkdir "$subtree_test_count"/"sub dir2" && - test_create_commit "$subtree_test_count" "sub dir2"/main-sub2 && + mkdir "$test_count"/"sub dir2" && + test_create_commit "$test_count" "sub dir2"/main-sub2 && ( - cd "$subtree_test_count" && + cd "$test_count" && git subtree split --prefix="sub dir2" --branch subproj2-br && - x= && - git log --pretty=format:"commit: %H" | join_commits | - ( - 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 - ) + git log --format="%H" >commit-list && + while read commit + do + git log -n1 --format="" --name-only "$commit" >file-list && + test_line_count -le 1 file-list || return 1 + done <commit-list ) ' -next_test test_expect_success 'push split to subproj' ' - subtree_test_create_repo "$subtree_test_count" && - subtree_test_create_repo "$subtree_test_count/sub proj" && - test_create_commit "$subtree_test_count" main1 && - test_create_commit "$subtree_test_count/sub proj" sub1 && + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && ( - cd "$subtree_test_count" && - git fetch ./"sub proj" master && + cd "$test_count" && + git fetch ./"sub proj" HEAD && git subtree add --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub1 && - test_create_commit "$subtree_test_count" main2 && - test_create_commit "$subtree_test_count/sub proj" sub2 && - test_create_commit "$subtree_test_count" "sub dir"/main-sub2 && - ( - cd $subtree_test_count/"sub proj" && - git branch sub-branch-1 && - cd .. && - git fetch ./"sub proj" master && + test_create_commit "$test_count" "sub dir"/main-sub1 && + test_create_commit "$test_count" main2 && + test_create_commit "$test_count/sub proj" sub2 && + test_create_commit "$test_count" "sub dir"/main-sub2 && + ( + cd $test_count/"sub proj" && + git branch sub-branch-1 && + cd .. && + git fetch ./"sub proj" HEAD && git subtree merge --prefix="sub dir" FETCH_HEAD ) && - test_create_commit "$subtree_test_count" "sub dir"/main-sub3 && - ( - cd "$subtree_test_count" && - git subtree push ./"sub proj" --prefix "sub dir" sub-branch-1 && - cd ./"sub proj" && - git checkout sub-branch-1 && - check_equal "$(last_commit_message)" "sub dir/main-sub3" + test_create_commit "$test_count" "sub dir"/main-sub3 && + ( + cd "$test_count" && + git subtree push ./"sub proj" --prefix "sub dir" sub-branch-1 && + cd ./"sub proj" && + git checkout sub-branch-1 && + test "$(last_commit_subject)" = "sub dir/main-sub3" ) ' @@ -991,43 +1435,43 @@ test_expect_success 'push split to subproj' ' # set of commits. # -next_test test_expect_success 'subtree descendant check' ' - subtree_test_create_repo "$subtree_test_count" && - test_create_commit "$subtree_test_count" folder_subtree/a && + subtree_test_create_repo "$test_count" && + defaultBranch=$(sed "s,ref: refs/heads/,," "$test_count/.git/HEAD") && + test_create_commit "$test_count" folder_subtree/a && ( - cd "$subtree_test_count" && + cd "$test_count" && git branch branch ) && - test_create_commit "$subtree_test_count" folder_subtree/0 && - test_create_commit "$subtree_test_count" folder_subtree/b && - cherry=$(cd "$subtree_test_count"; git rev-parse HEAD) && + test_create_commit "$test_count" folder_subtree/0 && + test_create_commit "$test_count" folder_subtree/b && + cherry=$(cd "$test_count"; git rev-parse HEAD) && ( - cd "$subtree_test_count" && + cd "$test_count" && git checkout branch ) && - test_create_commit "$subtree_test_count" commit_on_branch && + test_create_commit "$test_count" commit_on_branch && ( - cd "$subtree_test_count" && + cd "$test_count" && git cherry-pick $cherry && - git checkout master && + git checkout $defaultBranch && git merge -m "merge should be kept on subtree" branch && git branch no_subtree_work_branch ) && - test_create_commit "$subtree_test_count" folder_subtree/d && + test_create_commit "$test_count" folder_subtree/d && ( - cd "$subtree_test_count" && + cd "$test_count" && git checkout no_subtree_work_branch ) && - test_create_commit "$subtree_test_count" not_a_subtree_change && + test_create_commit "$test_count" not_a_subtree_change && ( - cd "$subtree_test_count" && - git checkout master && + cd "$test_count" && + git checkout $defaultBranch && git merge -m "merge should be skipped on subtree" no_subtree_work_branch && - git subtree split --prefix folder_subtree/ --branch subtree_tip master && + git subtree split --prefix folder_subtree/ --branch subtree_tip $defaultBranch && git subtree split --prefix folder_subtree/ --branch subtree_branch branch && - check_equal $(git rev-list --count subtree_tip..subtree_branch) 0 + test $(git rev-list --count subtree_tip..subtree_branch) = 0 ) ' diff --git a/contrib/subtree/todo b/contrib/subtree/todo index 0d0e777651..32d2ce3a40 100644 --- a/contrib/subtree/todo +++ b/contrib/subtree/todo @@ -23,9 +23,9 @@ "pull" and "merge" commands should fail if you've never merged that --prefix before - + docs should provide an example of "add" - + note that the initial split doesn't *have* to have a commitid specified... that's just an optimization @@ -33,7 +33,7 @@ get a misleading "prefix must end with /" message from one of the other git tools that git-subtree calls. Should detect this situation and print the *real* problem. - + "pull --squash" should do fetch-synthesize-merge, but instead just does "pull" directly, which doesn't work at all. |