diff options
Diffstat (limited to 'contrib')
46 files changed, 4527 insertions, 337 deletions
diff --git a/contrib/ciabot/ciabot.py b/contrib/ciabot/ciabot.py index bd24395d4c..36b5665ff8 100755 --- a/contrib/ciabot/ciabot.py +++ b/contrib/ciabot/ciabot.py @@ -47,7 +47,13 @@ # we default to that. # -import os, sys, commands, socket, urllib +import sys +if sys.hexversion < 0x02000000: + # The limiter is the xml.sax module + sys.stderr.write("ciabot.py: requires Python 2.0.0 or later.\n") + sys.exit(1) + +import os, commands, socket, urllib from xml.sax.saxutils import escape # Changeset URL prefix for your repo: when the commit ID is appended diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 5b255cba69..e769800393 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -13,6 +13,7 @@ # *) .git/remotes file names # *) git 'subcommands' # *) tree paths within 'ref:path/to/file' expressions +# *) file paths within current working directory and index # *) common --long-options # # To use these routines: @@ -23,10 +24,6 @@ # 3) Consider changing your PS1 to also show the current branch, # see git-prompt.sh for details. -if [[ -n ${ZSH_VERSION-} ]]; then - autoload -U +X bashcompinit && bashcompinit -fi - case "$COMP_WORDBREAKS" in *:*) : great ;; *) COMP_WORDBREAKS="$COMP_WORDBREAKS:" @@ -169,7 +166,6 @@ __git_reassemble_comp_words_by_ref() } if ! type _get_comp_words_by_ref >/dev/null 2>&1; then -if [[ -z ${ZSH_VERSION:+set} ]]; then _get_comp_words_by_ref () { local exclude cur_ words_ cword_ @@ -197,32 +193,6 @@ _get_comp_words_by_ref () shift done } -else -_get_comp_words_by_ref () -{ - while [ $# -gt 0 ]; do - case "$1" in - cur) - cur=${COMP_WORDS[COMP_CWORD]} - ;; - prev) - prev=${COMP_WORDS[COMP_CWORD-1]} - ;; - words) - words=("${COMP_WORDS[@]}") - ;; - cword) - cword=$COMP_CWORD - ;; - -n) - # assume COMP_WORDBREAKS is already set sanely - shift - ;; - esac - shift - done -} -fi fi # Generates completion reply with compgen, appending a space to possible @@ -264,6 +234,124 @@ __gitcomp_nl () COMPREPLY=($(compgen -P "${2-}" -S "${4- }" -W "$1" -- "${3-$cur}")) } +# Generates completion reply with compgen from newline-separated possible +# completion filenames. +# It accepts 1 to 3 arguments: +# 1: List of possible completion filenames, separated by a single newline. +# 2: A directory prefix to be added to each possible completion filename +# (optional). +# 3: Generate possible completion matches for this word (optional). +__gitcomp_file () +{ + local IFS=$'\n' + + # XXX does not work when the directory prefix contains a tilde, + # since tilde expansion is not applied. + # This means that COMPREPLY will be empty and Bash default + # completion will be used. + COMPREPLY=($(compgen -P "${2-}" -W "$1" -- "${3-$cur}")) + + # Tell Bash that compspec generates filenames. + compopt -o filenames 2>/dev/null +} + +__git_index_file_list_filter_compat () +{ + local path + + while read -r path; do + case "$path" in + ?*/*) echo "${path%%/*}/" ;; + *) echo "$path" ;; + esac + done +} + +__git_index_file_list_filter_bash () +{ + local path + + while read -r path; do + case "$path" in + ?*/*) + # XXX if we append a slash to directory names when using + # `compopt -o filenames`, Bash will append another slash. + # This is pretty stupid, and this the reason why we have to + # define a compatible version for this function. + echo "${path%%/*}" ;; + *) + echo "$path" ;; + esac + done +} + +# Process path list returned by "ls-files" and "diff-index --name-only" +# commands, in order to list only file names relative to a specified +# directory, and append a slash to directory names. +__git_index_file_list_filter () +{ + # Default to Bash >= 4.x + __git_index_file_list_filter_bash +} + +# Execute git ls-files, returning paths relative to the directory +# specified in the first argument, and using the options specified in +# the second argument. +__git_ls_files_helper () +{ + ( + test -n "${CDPATH+set}" && unset CDPATH + # NOTE: $2 is not quoted in order to support multiple options + cd "$1" && git ls-files --exclude-standard $2 + ) 2>/dev/null +} + + +# Execute git diff-index, returning paths relative to the directory +# specified in the first argument, and using the tree object id +# specified in the second argument. +__git_diff_index_helper () +{ + ( + test -n "${CDPATH+set}" && unset CDPATH + cd "$1" && git diff-index --name-only --relative "$2" + ) 2>/dev/null +} + +# __git_index_files accepts 1 or 2 arguments: +# 1: Options to pass to ls-files (required). +# Supported options are --cached, --modified, --deleted, --others, +# and --directory. +# 2: A directory path (optional). +# If provided, only files within the specified directory are listed. +# Sub directories are never recursed. Path must have a trailing +# slash. +__git_index_files () +{ + local dir="$(__gitdir)" root="${2-.}" + + if [ -d "$dir" ]; then + __git_ls_files_helper "$root" "$1" | __git_index_file_list_filter | + sort | uniq + fi +} + +# __git_diff_index_files accepts 1 or 2 arguments: +# 1) The id of a tree object. +# 2) A directory path (optional). +# If provided, only files within the specified directory are listed. +# Sub directories are never recursed. Path must have a trailing +# slash. +__git_diff_index_files () +{ + local dir="$(__gitdir)" root="${2-.}" + + if [ -d "$dir" ]; then + __git_diff_index_helper "$root" "$1" | __git_index_file_list_filter | + sort | uniq + fi +} + __git_heads () { local dir="$(__gitdir)" @@ -321,7 +409,7 @@ __git_refs () if [[ "$ref" == "$cur"* ]]; then echo "$ref" fi - done | uniq -u + done | sort | uniq -u fi return fi @@ -428,7 +516,7 @@ __git_complete_revlist_file () *) pfx="$ref:$pfx" ;; esac - __gitcomp_nl "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \ + __gitcomp_nl "$(git --git-dir="$(__gitdir)" ls-tree "$ls" 2>/dev/null \ | sed '/^100... blob /{ s,^.* ,, s,$, , @@ -461,6 +549,46 @@ __git_complete_revlist_file () } +# __git_complete_index_file requires 1 argument: the options to pass to +# ls-file +__git_complete_index_file () +{ + local pfx cur_="$cur" + + case "$cur_" in + ?*/*) + pfx="${cur_%/*}" + cur_="${cur_##*/}" + pfx="${pfx}/" + + __gitcomp_file "$(__git_index_files "$1" "$pfx")" "$pfx" "$cur_" + ;; + *) + __gitcomp_file "$(__git_index_files "$1")" "" "$cur_" + ;; + esac +} + +# __git_complete_diff_index_file requires 1 argument: the id of a tree +# object +__git_complete_diff_index_file () +{ + local pfx cur_="$cur" + + case "$cur_" in + ?*/*) + pfx="${cur_%/*}" + cur_="${cur_##*/}" + pfx="${pfx}/" + + __gitcomp_file "$(__git_diff_index_files "$1" "$pfx")" "$pfx" "$cur_" + ;; + *) + __gitcomp_file "$(__git_diff_index_files "$1")" "" "$cur_" + ;; + esac +} + __git_complete_file () { __git_complete_revlist_file @@ -562,10 +690,19 @@ __git_complete_strategy () return 1 } +__git_commands () { + if test -n "${GIT_TESTING_COMMAND_COMPLETION:-}" + then + printf "%s" "${GIT_TESTING_COMMAND_COMPLETION}" + else + git help -a|egrep '^ [a-zA-Z0-9]' + fi +} + __git_list_all_commands () { local i IFS=" "$'\n' - for i in $(git help -a|egrep '^ [a-zA-Z0-9]') + for i in $(__git_commands) do case $i in *--*) : helper pattern;; @@ -585,7 +722,7 @@ __git_list_porcelain_commands () { local i IFS=" "$'\n' __git_compute_all_commands - for i in "help" $__git_all_commands + for i in $__git_all_commands do case $i in *--*) : helper pattern;; @@ -594,6 +731,7 @@ __git_list_porcelain_commands () archimport) : import;; cat-file) : plumbing;; check-attr) : plumbing;; + check-ignore) : plumbing;; check-ref-format) : plumbing;; checkout-index) : plumbing;; commit-tree) : plumbing;; @@ -753,6 +891,43 @@ __git_has_doubledash () return 1 } +# Try to count non option arguments passed on the command line for the +# specified git command. +# When options are used, it is necessary to use the special -- option to +# tell the implementation were non option arguments begin. +# XXX this can not be improved, since options can appear everywhere, as +# an example: +# git mv x -n y +# +# __git_count_arguments requires 1 argument: the git command executed. +__git_count_arguments () +{ + local word i c=0 + + # Skip "git" (first argument) + for ((i=1; i < ${#words[@]}; i++)); do + word="${words[i]}" + + case "$word" in + --) + # Good; we can assume that the following are only non + # option arguments. + ((c = 0)) + ;; + "$1") + # Skip the specified git command and discard git + # main options + ((c = 0)) + ;; + ?*) + ((c++)) + ;; + esac + done + + printf "%d" $c +} + __git_whitespacelist="nowarn warn error error-all fix" _git_am () @@ -801,8 +976,6 @@ _git_apply () _git_add () { - __git_has_doubledash && return - case "$cur" in --*) __gitcomp " @@ -811,7 +984,9 @@ _git_add () " return esac - COMPREPLY=() + + # XXX should we check for --update and --all options ? + __git_complete_index_file "--others --modified" } _git_archive () @@ -875,11 +1050,15 @@ _git_branch () done case "$cur" in + --set-upstream-to=*) + __gitcomp "$(__git_refs)" "" "${cur##--set-upstream-to=}" + ;; --*) __gitcomp " --color --no-color --verbose --abbrev= --no-abbrev --track --no-track --contains --merged --no-merged - --set-upstream --edit-description --list + --set-upstream-to= --edit-description --list + --unset-upstream " ;; *) @@ -957,15 +1136,15 @@ _git_cherry_pick () _git_clean () { - __git_has_doubledash && return - case "$cur" in --*) __gitcomp "--dry-run --quiet" return ;; esac - COMPREPLY=() + + # XXX should we check for -x option ? + __git_complete_index_file "--others" } _git_clone () @@ -985,6 +1164,8 @@ _git_clone () --upload-pack --template= --depth + --single-branch + --branch " return ;; @@ -994,7 +1175,12 @@ _git_clone () _git_commit () { - __git_has_doubledash && return + case "$prev" in + -c|-C) + __gitcomp_nl "$(__git_refs)" "" "${cur}" + return + ;; + esac case "$cur" in --cleanup=*) @@ -1023,7 +1209,13 @@ _git_commit () " return esac - COMPREPLY=() + + if git rev-parse --verify --quiet HEAD >/dev/null; then + __git_complete_diff_index_file "HEAD" + else + # This is the first commit + __git_complete_index_file "--cached" + fi } _git_describe () @@ -1039,6 +1231,8 @@ _git_describe () __gitcomp_nl "$(__git_refs)" } +__git_diff_algorithms="myers minimal patience histogram" + __git_diff_common_options="--stat --numstat --shortstat --summary --patch-with-stat --name-only --name-status --color --no-color --color-words --no-renames --check @@ -1049,10 +1243,11 @@ __git_diff_common_options="--stat --numstat --shortstat --summary --no-ext-diff --no-prefix --src-prefix= --dst-prefix= --inter-hunk-context= - --patience + --patience --histogram --minimal --raw --dirstat --dirstat= --dirstat-by-file --dirstat-by-file= --cumulative + --diff-algorithm= " _git_diff () @@ -1060,6 +1255,10 @@ _git_diff () __git_has_doubledash && return case "$cur" in + --diff-algorithm=*) + __gitcomp "$__git_diff_algorithms" "" "${cur##--diff-algorithm=}" + return + ;; --*) __gitcomp "--cached --staged --pickaxe-all --pickaxe-regex --base --ours --theirs --no-index @@ -1072,7 +1271,7 @@ _git_diff () } __git_mergetools_common="diffuse ecmerge emerge kdiff3 meld opendiff - tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3 + tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3 codecompare " _git_difftool () @@ -1112,6 +1311,14 @@ _git_fetch () __git_complete_remote_or_refspec } +__git_format_patch_options=" + --stdout --attach --no-attach --thread --thread= --output-directory + --numbered --start-number --numbered-files --keep-subject --signoff + --signature --no-signature --in-reply-to= --cc= --full-index --binary + --not --all --cover-letter --no-prefix --src-prefix= --dst-prefix= + --inline --suffix= --ignore-if-in-upstream --subject-prefix= +" + _git_format_patch () { case "$cur" in @@ -1122,21 +1329,7 @@ _git_format_patch () return ;; --*) - __gitcomp " - --stdout --attach --no-attach --thread --thread= - --output-directory - --numbered --start-number - --numbered-files - --keep-subject - --signoff --signature --no-signature - --in-reply-to= --cc= - --full-index --binary - --not --all - --cover-letter - --no-prefix --src-prefix= --dst-prefix= - --inline --suffix= --ignore-if-in-upstream - --subject-prefix= - " + __gitcomp "$__git_format_patch_options" return ;; esac @@ -1247,8 +1440,6 @@ _git_init () _git_ls_files () { - __git_has_doubledash && return - case "$cur" in --*) __gitcomp "--cached --deleted --modified --others --ignored @@ -1261,7 +1452,10 @@ _git_ls_files () return ;; esac - COMPREPLY=() + + # XXX ignore options like --modified and always suggest all cached + # files. + __git_complete_index_file "--cached" } _git_ls_remote () @@ -1393,7 +1587,14 @@ _git_mv () return ;; esac - COMPREPLY=() + + if [ $(__git_count_arguments "mv") -gt 0 ]; then + # We need to show both cached and untracked files (including + # empty directories) since this may not be the last argument. + __git_complete_index_file "--cached --others --directory" + else + __git_complete_index_file "--cached" + fi } _git_name_rev () @@ -1550,6 +1751,12 @@ _git_send_email () __gitcomp "ssl tls" "" "${cur##--smtp-encryption=}" return ;; + --thread=*) + __gitcomp " + deep shallow + " "" "${cur##--thread=}" + return + ;; --*) __gitcomp "--annotate --bcc --cc --cc-cmd --chain-reply-to --compose --confirm= --dry-run --envelope-sender @@ -1559,11 +1766,12 @@ _git_send_email () --signed-off-by-cc --smtp-pass --smtp-server --smtp-server-port --smtp-encryption= --smtp-user --subject --suppress-cc= --suppress-from --thread --to - --validate --no-validate" + --validate --no-validate + $__git_format_patch_options" return ;; esac - COMPREPLY=() + __git_complete_revlist } _git_stage () @@ -1577,7 +1785,7 @@ __git_config_get_set_variables () while [ $c -gt 1 ]; do word="${words[c]}" case "$word" in - --global|--system|--file=*) + --system|--global|--local|--file=*) config_file="$word" break ;; @@ -1612,6 +1820,10 @@ _git_config () __gitcomp_nl "$(__git_refs)" return ;; + branch.*.rebase) + __gitcomp "false true" + return + ;; remote.*.fetch) local remote="${prev#remote.}" remote="${remote%.fetch}" @@ -1651,6 +1863,10 @@ _git_config () " return ;; + diff.submodule) + __gitcomp "log short" + return + ;; help.format) __gitcomp "man info web html" return @@ -1683,7 +1899,7 @@ _git_config () case "$cur" in --*) __gitcomp " - --global --system --file= + --system --global --local --file= --list --replace-all --get --get-all --get-regexp --add --unset --unset-all @@ -1846,16 +2062,18 @@ _git_config () core.whitespace core.worktree diff.autorefreshindex - diff.statGraphWidth diff.external diff.ignoreSubmodules diff.mnemonicprefix diff.noprefix diff.renameLimit diff.renames + diff.statGraphWidth + diff.submodule diff.suppressBlankEmpty diff.tool diff.wordRegex + diff.algorithm difftool. difftool.prompt fetch.recurseSubmodules @@ -2092,15 +2310,14 @@ _git_revert () _git_rm () { - __git_has_doubledash && return - case "$cur" in --*) __gitcomp "--cached --dry-run --ignore-unmatch --quiet" return ;; esac - COMPREPLY=() + + __git_complete_index_file "--cached" } _git_shortlog () @@ -2130,6 +2347,10 @@ _git_show () " "" "${cur#*=}" return ;; + --diff-algorithm=*) + __gitcomp "$__git_diff_algorithms" "" "${cur##--diff-algorithm=}" + return + ;; --*) __gitcomp "--pretty= --format= --abbrev-commit --oneline $__git_diff_common_options @@ -2425,20 +2646,88 @@ __gitk_main () __git_complete_revlist } -__git_func_wrap () -{ - if [[ -n ${ZSH_VERSION-} ]]; then - emulate -L bash - setopt KSH_TYPESET +if [[ -n ${ZSH_VERSION-} ]]; then + echo "WARNING: this script is deprecated, please see git-completion.zsh" 1>&2 + + autoload -U +X compinit && compinit + + __gitcomp () + { + emulate -L zsh + + local cur_="${3-$cur}" + + case "$cur_" in + --*=) + ;; + *) + local c IFS=$' \t\n' + local -a array + for c in ${=1}; do + c="$c${4-}" + case $c in + --*=*|*.) ;; + *) c="$c " ;; + esac + array[$#array+1]="$c" + done + compset -P '*[=:]' + compadd -Q -S '' -p "${2-}" -a -- array && _ret=0 + ;; + esac + } + + __gitcomp_nl () + { + emulate -L zsh + + local IFS=$'\n' + compset -P '*[=:]' + compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0 + } - # workaround zsh's bug that leaves 'words' as a special - # variable in versions < 4.3.12 - typeset -h words + __gitcomp_file () + { + emulate -L zsh - # workaround zsh's bug that quotes spaces in the COMPREPLY - # array if IFS doesn't contain spaces. - typeset -h IFS + local IFS=$'\n' + compset -P '*[=:]' + compadd -Q -p "${2-}" -f -- ${=1} && _ret=0 + } + + __git_zsh_helper () + { + emulate -L ksh + local cur cword prev + cur=${words[CURRENT-1]} + prev=${words[CURRENT-2]} + let cword=CURRENT-1 + __${service}_main + } + + _git () + { + emulate -L zsh + local _ret=1 + __git_zsh_helper + let _ret && _default -S '' && _ret=0 + return _ret + } + + compdef _git git gitk + return +elif [[ -n ${BASH_VERSION-} ]]; then + if ((${BASH_VERSINFO[0]} < 4)); then + # compopt is not supported + __git_index_file_list_filter () + { + __git_index_file_list_filter_compat + } fi +fi + +__git_func_wrap () +{ local cur words cword prev _get_comp_words_by_ref -n =: cur words cword prev $1 diff --git a/contrib/completion/git-completion.tcsh b/contrib/completion/git-completion.tcsh new file mode 100644 index 0000000000..eaacaf0c3e --- /dev/null +++ b/contrib/completion/git-completion.tcsh @@ -0,0 +1,128 @@ +#!tcsh +# +# tcsh completion support for core Git. +# +# Copyright (C) 2012 Marc Khouzam <marc.khouzam@gmail.com> +# Distributed under the GNU General Public License, version 2.0. +# +# When sourced, this script will generate a new script that uses +# the git-completion.bash script provided by core Git. This new +# script can be used by tcsh to perform git completion. +# The current script also issues the necessary tcsh 'complete' +# commands. +# +# To use this completion script: +# +# 0) You need tcsh 6.16.00 or newer. +# 1) Copy both this file and the bash completion script to ${HOME}. +# You _must_ use the name ${HOME}/.git-completion.bash for the +# bash script. +# (e.g. ~/.git-completion.tcsh and ~/.git-completion.bash). +# 2) Add the following line to your .tcshrc/.cshrc: +# source ~/.git-completion.tcsh +# 3) For completion similar to bash, it is recommended to also +# add the following line to your .tcshrc/.cshrc: +# set autolist=ambiguous +# It will tell tcsh to list the possible completion choices. + +set __git_tcsh_completion_version = `\echo ${tcsh} | \sed 's/\./ /g'` +if ( ${__git_tcsh_completion_version[1]} < 6 || \ + ( ${__git_tcsh_completion_version[1]} == 6 && \ + ${__git_tcsh_completion_version[2]} < 16 ) ) then + echo "git-completion.tcsh: Your version of tcsh is too old, you need version 6.16.00 or newer. Git completion will not work." + exit +endif +unset __git_tcsh_completion_version + +set __git_tcsh_completion_original_script = ${HOME}/.git-completion.bash +set __git_tcsh_completion_script = ${HOME}/.git-completion.tcsh.bash + +# Check that the user put the script in the right place +if ( ! -e ${__git_tcsh_completion_original_script} ) then + echo "git-completion.tcsh: Cannot find: ${__git_tcsh_completion_original_script}. Git completion will not work." + exit +endif + +cat << EOF > ${__git_tcsh_completion_script} +#!bash +# +# This script is GENERATED and will be overwritten automatically. +# Do not modify it directly. Instead, modify git-completion.tcsh +# and source it again. + +source ${__git_tcsh_completion_original_script} + +# Remove the colon as a completion separator because tcsh cannot handle it +COMP_WORDBREAKS=\${COMP_WORDBREAKS//:} + +# For file completion, tcsh needs the '/' to be appended to directories. +# By default, the bash script does not do that. +# We can achieve this by using the below compatibility +# method of the git-completion.bash script. +__git_index_file_list_filter () +{ + __git_index_file_list_filter_compat +} + +# Set COMP_WORDS in a way that can be handled by the bash script. +COMP_WORDS=(\$2) + +# The cursor is at the end of parameter #1. +# We must check for a space as the last character which will +# tell us that the previous word is complete and the cursor +# is on the next word. +if [ "\${2: -1}" == " " ]; then + # The last character is a space, so our location is at the end + # of the command-line array + COMP_CWORD=\${#COMP_WORDS[@]} +else + # The last character is not a space, so our location is on the + # last word of the command-line array, so we must decrement the + # count by 1 + COMP_CWORD=\$((\${#COMP_WORDS[@]}-1)) +fi + +# Call _git() or _gitk() of the bash script, based on the first argument +_\${1} + +IFS=\$'\n' +if [ \${#COMPREPLY[*]} -eq 0 ]; then + # No completions suggested. In this case, we want tcsh to perform + # standard file completion. However, there does not seem to be way + # to tell tcsh to do that. To help the user, we try to simulate + # file completion directly in this script. + # + # Known issues: + # - Possible completions are shown with their directory prefix. + # - Completions containing shell variables are not handled. + # - Completions with ~ as the first character are not handled. + + # No file completion should be done unless we are completing beyond + # the git sub-command. An improvement on the bash completion :) + if [ \${COMP_CWORD} -gt 1 ]; then + TO_COMPLETE="\${COMP_WORDS[\${COMP_CWORD}]}" + + # We don't support ~ expansion: too tricky. + if [ "\${TO_COMPLETE:0:1}" != "~" ]; then + # Use ls so as to add the '/' at the end of directories. + COMPREPLY=(\`ls -dp \${TO_COMPLETE}* 2> /dev/null\`) + fi + fi +fi + +# tcsh does not automatically remove duplicates, so we do it ourselves +echo "\${COMPREPLY[*]}" | sort | uniq + +# If there is a single completion and it is a directory, we output it +# a second time to trick tcsh into not adding a space after it. +if [ \${#COMPREPLY[*]} -eq 1 ] && [ "\${COMPREPLY[0]: -1}" == "/" ]; then + echo "\${COMPREPLY[*]}" +fi + +EOF + +# Don't need this variable anymore, so don't pollute the users environment +unset __git_tcsh_completion_original_script + +complete git 'p,*,`bash ${__git_tcsh_completion_script} git "${COMMAND_LINE}"`,' +complete gitk 'p,*,`bash ${__git_tcsh_completion_script} gitk "${COMMAND_LINE}"`,' diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh new file mode 100644 index 0000000000..2565d2eef4 --- /dev/null +++ b/contrib/completion/git-completion.zsh @@ -0,0 +1,205 @@ +#compdef git gitk + +# zsh completion wrapper for git +# +# Copyright (c) 2012-2013 Felipe Contreras <felipe.contreras@gmail.com> +# +# You need git's bash completion script installed somewhere, by default on the +# same directory as this script. +# +# If your script is on ~/.git-completion.sh instead, you can configure it on +# your ~/.zshrc: +# +# zstyle ':completion:*:*:git:*' script ~/.git-completion.sh +# +# The recommended way to install this script is to copy to +# '~/.zsh/completion/_git', and then add the following to your ~/.zshrc file: +# +# fpath=(~/.zsh/completion $fpath) + +complete () +{ + # do nothing + return 0 +} + +zstyle -T ':completion:*:*:git:*' tag-order && \ + zstyle ':completion:*:*:git:*' tag-order 'common-commands' + +zstyle -s ":completion:*:*:git:*" script script +test -z "$script" && script="$(dirname ${funcsourcetrace[1]%:*})"/git-completion.bash +ZSH_VERSION='' . "$script" + +__gitcomp () +{ + emulate -L zsh + + local cur_="${3-$cur}" + + case "$cur_" in + --*=) + ;; + *) + local c IFS=$' \t\n' + local -a array + for c in ${=1}; do + c="$c${4-}" + case $c in + --*=*|*.) ;; + *) c="$c " ;; + esac + array+=("$c") + done + compset -P '*[=:]' + compadd -Q -S '' -p "${2-}" -a -- array && _ret=0 + ;; + esac +} + +__gitcomp_nl () +{ + emulate -L zsh + + local IFS=$'\n' + compset -P '*[=:]' + compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0 +} + +__gitcomp_file () +{ + emulate -L zsh + + local IFS=$'\n' + compset -P '*[=:]' + compadd -Q -p "${2-}" -f -- ${=1} && _ret=0 +} + +__git_zsh_bash_func () +{ + emulate -L ksh + + local command=$1 + + local completion_func="_git_${command//-/_}" + declare -f $completion_func >/dev/null && $completion_func && return + + local expansion=$(__git_aliased_command "$command") + if [ -n "$expansion" ]; then + completion_func="_git_${expansion//-/_}" + declare -f $completion_func >/dev/null && $completion_func + fi +} + +__git_zsh_cmd_common () +{ + local -a list + list=( + add:'add file contents to the index' + bisect:'find by binary search the change that introduced a bug' + branch:'list, create, or delete branches' + checkout:'checkout a branch or paths to the working tree' + clone:'clone a repository into a new directory' + commit:'record changes to the repository' + diff:'show changes between commits, commit and working tree, etc' + fetch:'download objects and refs from another repository' + grep:'print lines matching a pattern' + init:'create an empty Git repository or reinitialize an existing one' + log:'show commit logs' + merge:'join two or more development histories together' + mv:'move or rename a file, a directory, or a symlink' + pull:'fetch from and merge with another repository or a local branch' + push:'update remote refs along with associated objects' + rebase:'forward-port local commits to the updated upstream head' + reset:'reset current HEAD to the specified state' + rm:'remove files from the working tree and from the index' + show:'show various types of objects' + status:'show the working tree status' + tag:'create, list, delete or verify a tag object signed with GPG') + _describe -t common-commands 'common commands' list && _ret=0 +} + +__git_zsh_cmd_alias () +{ + local -a list + list=(${${${(0)"$(git config -z --get-regexp '^alias\.')"}#alias.}%$'\n'*}) + _describe -t alias-commands 'aliases' list $* && _ret=0 +} + +__git_zsh_cmd_all () +{ + local -a list + emulate ksh -c __git_compute_all_commands + list=( ${=__git_all_commands} ) + _describe -t all-commands 'all commands' list && _ret=0 +} + +__git_zsh_main () +{ + local curcontext="$curcontext" state state_descr line + typeset -A opt_args + local -a orig_words + + orig_words=( ${words[@]} ) + + _arguments -C \ + '(-p --paginate --no-pager)'{-p,--paginate}'[pipe all output into ''less'']' \ + '(-p --paginate)--no-pager[do not pipe git output into a pager]' \ + '--git-dir=-[set the path to the repository]: :_directories' \ + '--bare[treat the repository as a bare repository]' \ + '(- :)--version[prints the git suite version]' \ + '--exec-path=-[path to where your core git programs are installed]:: :_directories' \ + '--html-path[print the path where git''s HTML documentation is installed]' \ + '--info-path[print the path where the Info files are installed]' \ + '--man-path[print the manpath (see `man(1)`) for the man pages]' \ + '--work-tree=-[set the path to the working tree]: :_directories' \ + '--namespace=-[set the git namespace]' \ + '--no-replace-objects[do not use replacement refs to replace git objects]' \ + '(- :)--help[prints the synopsis and a list of the most commonly used commands]: :->arg' \ + '(-): :->command' \ + '(-)*:: :->arg' && return + + case $state in + (command) + _alternative \ + 'alias-commands:alias:__git_zsh_cmd_alias' \ + 'common-commands:common:__git_zsh_cmd_common' \ + 'all-commands:all:__git_zsh_cmd_all' && _ret=0 + ;; + (arg) + local command="${words[1]}" __git_dir + + if (( $+opt_args[--bare] )); then + __git_dir='.' + else + __git_dir=${opt_args[--git-dir]} + fi + + (( $+opt_args[--help] )) && command='help' + + words=( ${orig_words[@]} ) + + __git_zsh_bash_func $command + ;; + esac +} + +_git () +{ + local _ret=1 + local cur cword prev + + cur=${words[CURRENT]} + prev=${words[CURRENT-1]} + let cword=CURRENT-1 + + if (( $+functions[__${service}_zsh_main] )); then + __${service}_zsh_main + else + emulate ksh -c __${service}_main + fi + + let _ret && _default && _ret=0 + return _ret +} + +_git diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh index bf20491ec3..054c52e90a 100644 --- a/contrib/completion/git-prompt.sh +++ b/contrib/completion/git-prompt.sh @@ -10,9 +10,22 @@ # 1) Copy this file to somewhere (e.g. ~/.git-prompt.sh). # 2) Add the following line to your .bashrc/.zshrc: # source ~/.git-prompt.sh -# 3) Change your PS1 to also show the current branch: -# Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' -# ZSH: PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ ' +# 3a) Change your PS1 to call __git_ps1 as +# command-substitution: +# Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' +# ZSH: PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ ' +# the optional argument will be used as format string. +# 3b) Alternatively, if you are using bash, __git_ps1 can be +# used for PROMPT_COMMAND with two parameters, <pre> and +# <post>, which are strings you would put in $PS1 before +# and after the status string generated by the git-prompt +# machinery. e.g. +# PROMPT_COMMAND='__git_ps1 "\u@\h:\w" "\\\$ "' +# will show username, at-sign, host, colon, cwd, then +# various status string, followed by dollar and SP, as +# your prompt. +# Optionally, you can supply a third argument with a printf +# format string to finetune the output of the branch status # # The argument to __git_ps1 will be displayed only if you are currently # in a git repository. The %s token will be the name of the current @@ -30,7 +43,10 @@ # # If you would like to see if there're untracked files, then you can set # GIT_PS1_SHOWUNTRACKEDFILES to a nonempty value. If there're untracked -# files, then a '%' will be shown next to the branch name. +# files, then a '%' will be shown next to the branch name. You can +# configure this per-repository with the bash.showUntrackedFiles +# variable, which defaults to true once GIT_PS1_SHOWUNTRACKEDFILES is +# enabled. # # If you would like to see the difference between HEAD and its upstream, # set GIT_PS1_SHOWUPSTREAM="auto". A "<" indicates you are behind, ">" @@ -49,6 +65,19 @@ # find one, or @{upstream} otherwise. Once you have set # GIT_PS1_SHOWUPSTREAM, you can override it on a per-repository basis by # setting the bash.showUpstream config variable. +# +# If you would like to see more information about the identity of +# commits checked out as a detached HEAD, set GIT_PS1_DESCRIBE_STYLE +# to one of these values: +# +# contains relative to newer annotated tag (v1.6.3.2~35) +# branch relative to newer tag or branch (master~4) +# describe relative to older annotated tag (v1.6.3.1-13-gdd42c2f) +# default exactly matching tag +# +# If you would like a colored hint about the current dirty state, set +# GIT_PS1_SHOWCOLORHINTS to a nonempty value. The colors are based on +# the colored output of "git status -sb". # __gitdir accepts 0 or 1 arguments (i.e., location) # returns location of .git repo @@ -195,11 +224,43 @@ __git_ps1_show_upstream () # __git_ps1 accepts 0 or 1 arguments (i.e., format string) -# returns text to add to bash PS1 prompt (includes branch name) +# when called from PS1 using command substitution +# in this mode it prints text to add to bash PS1 prompt (includes branch name) +# +# __git_ps1 requires 2 or 3 arguments when called from PROMPT_COMMAND (pc) +# in that case it _sets_ PS1. The arguments are parts of a PS1 string. +# when two arguments are given, the first is prepended and the second appended +# to the state string when assigned to PS1. +# The optional third parameter will be used as printf format string to further +# customize the output of the git-status string. +# In this mode you can request colored hints using GIT_PS1_SHOWCOLORHINTS=true __git_ps1 () { + local pcmode=no + local detached=no + local ps1pc_start='\u@\h:\w ' + local ps1pc_end='\$ ' + local printf_format=' (%s)' + + case "$#" in + 2|3) pcmode=yes + ps1pc_start="$1" + ps1pc_end="$2" + printf_format="${3:-$printf_format}" + ;; + 0|1) printf_format="${1:-$printf_format}" + ;; + *) return + ;; + esac + local g="$(__gitdir)" - if [ -n "$g" ]; then + if [ -z "$g" ]; then + if [ $pcmode = yes ]; then + #In PC mode PS1 always needs to be set + PS1="$ps1pc_start$ps1pc_end" + fi + else local r="" local b="" if [ -f "$g/rebase-merge/interactive" ]; then @@ -221,12 +282,14 @@ __git_ps1 () r="|MERGING" elif [ -f "$g/CHERRY_PICK_HEAD" ]; then r="|CHERRY-PICKING" + elif [ -f "$g/REVERT_HEAD" ]; then + r="|REVERTING" elif [ -f "$g/BISECT_LOG" ]; then r="|BISECTING" fi b="$(git symbolic-ref HEAD 2>/dev/null)" || { - + detached=yes b="$( case "${GIT_PS1_DESCRIBE_STYLE-}" in (contains) @@ -259,24 +322,25 @@ __git_ps1 () b="GIT_DIR!" fi elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then - if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ]; then - if [ "$(git config --bool bash.showDirtyState)" != "false" ]; then - git diff --no-ext-diff --quiet --exit-code || w="*" - if git rev-parse --quiet --verify HEAD >/dev/null; then - git diff-index --cached --quiet HEAD -- || i="+" - else - i="#" - fi + if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ] && + [ "$(git config --bool bash.showDirtyState)" != "false" ] + then + git diff --no-ext-diff --quiet --exit-code || w="*" + if git rev-parse --quiet --verify HEAD >/dev/null; then + git diff-index --cached --quiet HEAD -- || i="+" + else + i="#" fi fi if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$" fi - if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then - if [ -n "$(git ls-files --others --exclude-standard)" ]; then - u="%" - fi + if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] && + [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] && + [ -n "$(git ls-files --others --exclude-standard)" ] + then + u="%${ZSH_VERSION+%}" fi if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then @@ -285,6 +349,53 @@ __git_ps1 () fi local f="$w$i$s$u" - printf -- "${1:- (%s)}" "$c${b##refs/heads/}${f:+ $f}$r$p" + if [ $pcmode = yes ]; then + local gitstring= + if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then + local c_red='\e[31m' + local c_green='\e[32m' + local c_lblue='\e[1;34m' + local c_clear='\e[0m' + local bad_color=$c_red + local ok_color=$c_green + local branch_color="$c_clear" + local flags_color="$c_lblue" + local branchstring="$c${b##refs/heads/}" + + if [ $detached = no ]; then + branch_color="$ok_color" + else + branch_color="$bad_color" + fi + + # Setting gitstring directly with \[ and \] around colors + # is necessary to prevent wrapping issues! + gitstring="\[$branch_color\]$branchstring\[$c_clear\]" + + if [ -n "$w$i$s$u$r$p" ]; then + gitstring="$gitstring " + fi + if [ "$w" = "*" ]; then + gitstring="$gitstring\[$bad_color\]$w" + fi + if [ -n "$i" ]; then + gitstring="$gitstring\[$ok_color\]$i" + fi + if [ -n "$s" ]; then + gitstring="$gitstring\[$flags_color\]$s" + fi + if [ -n "$u" ]; then + gitstring="$gitstring\[$bad_color\]$u" + fi + gitstring="$gitstring\[$c_clear\]$r$p" + else + gitstring="$c${b##refs/heads/}${f:+ $f}$r$p" + fi + gitstring=$(printf -- "$printf_format" "$gitstring") + PS1="$ps1pc_start$gitstring$ps1pc_end" + else + # NO color option unless in PROMPT_COMMAND mode + printf -- "$printf_format" "$c${b##refs/heads/}${f:+ $f}$r$p" + fi fi } diff --git a/contrib/credential/gnome-keyring/.gitignore b/contrib/credential/gnome-keyring/.gitignore new file mode 100644 index 0000000000..88d8fcdbce --- /dev/null +++ b/contrib/credential/gnome-keyring/.gitignore @@ -0,0 +1 @@ +git-credential-gnome-keyring diff --git a/contrib/credential/gnome-keyring/Makefile b/contrib/credential/gnome-keyring/Makefile new file mode 100644 index 0000000000..e6561d8db6 --- /dev/null +++ b/contrib/credential/gnome-keyring/Makefile @@ -0,0 +1,24 @@ +MAIN:=git-credential-gnome-keyring +all:: $(MAIN) + +CC = gcc +RM = rm -f +CFLAGS = -g -O2 -Wall + +-include ../../../config.mak.autogen +-include ../../../config.mak + +INCS:=$(shell pkg-config --cflags gnome-keyring-1) +LIBS:=$(shell pkg-config --libs gnome-keyring-1) + +SRCS:=$(MAIN).c +OBJS:=$(SRCS:.c=.o) + +%.o: %.c + $(CC) $(CFLAGS) $(CPPFLAGS) $(INCS) -o $@ -c $< + +$(MAIN): $(OBJS) + $(CC) -o $@ $(LDFLAGS) $^ $(LIBS) + +clean: + @$(RM) $(MAIN) $(OBJS) diff --git a/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c b/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c new file mode 100644 index 0000000000..f2cdefee60 --- /dev/null +++ b/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2011 John Szakmeister <john@szakmeister.net> + * 2012 Philipp A. Hartmann <pah@qo.cx> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* + * Credits: + * - GNOME Keyring API handling originally written by John Szakmeister + * - ported to credential helper API by Philipp A. Hartmann + */ + +#include <stdio.h> +#include <string.h> +#include <stdarg.h> +#include <stdlib.h> +#include <errno.h> +#include <gnome-keyring.h> + +/* + * This credential struct and API is simplified from git's credential.{h,c} + */ +struct credential +{ + char *protocol; + char *host; + unsigned short port; + char *path; + char *username; + char *password; +}; + +#define CREDENTIAL_INIT \ + { NULL,NULL,0,NULL,NULL,NULL } + +void credential_init(struct credential *c); +void credential_clear(struct credential *c); +int credential_read(struct credential *c); +void credential_write(const struct credential *c); + +typedef int (*credential_op_cb)(struct credential*); + +struct credential_operation +{ + char *name; + credential_op_cb op; +}; + +#define CREDENTIAL_OP_END \ + { NULL,NULL } + +/* + * Table with operation callbacks is defined in concrete + * credential helper implementation and contains entries + * like { "get", function_to_get_credential } terminated + * by CREDENTIAL_OP_END. + */ +struct credential_operation const credential_helper_ops[]; + +/* ---------------- common helper functions ----------------- */ + +static inline void free_password(char *password) +{ + char *c = password; + if (!password) + return; + + while (*c) *c++ = '\0'; + free(password); +} + +static inline void warning(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + fprintf(stderr, "warning: "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n" ); + va_end(ap); +} + +static inline void error(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + fprintf(stderr, "error: "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n" ); + va_end(ap); +} + +static inline void die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap,fmt); + error(fmt, ap); + va_end(ap); + exit(EXIT_FAILURE); +} + +static inline void die_errno(int err) +{ + error("%s", strerror(err)); + exit(EXIT_FAILURE); +} + +static inline char *xstrdup(const char *str) +{ + char *ret = strdup(str); + if (!ret) + die_errno(errno); + + return ret; +} + +/* ----------------- GNOME Keyring functions ----------------- */ + +/* create a special keyring option string, if path is given */ +static char* keyring_object(struct credential *c) +{ + char* object = NULL; + + if (!c->path) + return object; + + object = (char*) malloc(strlen(c->host)+strlen(c->path)+8); + if(!object) + die_errno(errno); + + if(c->port) + sprintf(object,"%s:%hd/%s",c->host,c->port,c->path); + else + sprintf(object,"%s/%s",c->host,c->path); + + return object; +} + +int keyring_get(struct credential *c) +{ + char* object = NULL; + GList *entries; + GnomeKeyringNetworkPasswordData *password_data; + GnomeKeyringResult result; + + if (!c->protocol || !(c->host || c->path)) + return EXIT_FAILURE; + + object = keyring_object(c); + + result = gnome_keyring_find_network_password_sync( + c->username, + NULL /* domain */, + c->host, + object, + c->protocol, + NULL /* authtype */, + c->port, + &entries); + + free(object); + + if (result == GNOME_KEYRING_RESULT_NO_MATCH) + return EXIT_SUCCESS; + + if (result == GNOME_KEYRING_RESULT_CANCELLED) + return EXIT_SUCCESS; + + if (result != GNOME_KEYRING_RESULT_OK) { + error("%s",gnome_keyring_result_to_message(result)); + return EXIT_FAILURE; + } + + /* pick the first one from the list */ + password_data = (GnomeKeyringNetworkPasswordData *) entries->data; + + free_password(c->password); + c->password = xstrdup(password_data->password); + + if (!c->username) + c->username = xstrdup(password_data->user); + + gnome_keyring_network_password_list_free(entries); + + return EXIT_SUCCESS; +} + + +int keyring_store(struct credential *c) +{ + guint32 item_id; + char *object = NULL; + + /* + * Sanity check that what we are storing is actually sensible. + * In particular, we can't make a URL without a protocol field. + * Without either a host or pathname (depending on the scheme), + * we have no primary key. And without a username and password, + * we are not actually storing a credential. + */ + if (!c->protocol || !(c->host || c->path) || + !c->username || !c->password) + return EXIT_FAILURE; + + object = keyring_object(c); + + gnome_keyring_set_network_password_sync( + GNOME_KEYRING_DEFAULT, + c->username, + NULL /* domain */, + c->host, + object, + c->protocol, + NULL /* authtype */, + c->port, + c->password, + &item_id); + + free(object); + return EXIT_SUCCESS; +} + +int keyring_erase(struct credential *c) +{ + char *object = NULL; + GList *entries; + GnomeKeyringNetworkPasswordData *password_data; + GnomeKeyringResult result; + + /* + * Sanity check that we actually have something to match + * against. The input we get is a restrictive pattern, + * so technically a blank credential means "erase everything". + * But it is too easy to accidentally send this, since it is equivalent + * to empty input. So explicitly disallow it, and require that the + * pattern have some actual content to match. + */ + if (!c->protocol && !c->host && !c->path && !c->username) + return EXIT_FAILURE; + + object = keyring_object(c); + + result = gnome_keyring_find_network_password_sync( + c->username, + NULL /* domain */, + c->host, + object, + c->protocol, + NULL /* authtype */, + c->port, + &entries); + + free(object); + + if (result == GNOME_KEYRING_RESULT_NO_MATCH) + return EXIT_SUCCESS; + + if (result == GNOME_KEYRING_RESULT_CANCELLED) + return EXIT_SUCCESS; + + if (result != GNOME_KEYRING_RESULT_OK) + { + error("%s",gnome_keyring_result_to_message(result)); + return EXIT_FAILURE; + } + + /* pick the first one from the list (delete all matches?) */ + password_data = (GnomeKeyringNetworkPasswordData *) entries->data; + + result = gnome_keyring_item_delete_sync( + password_data->keyring, password_data->item_id); + + gnome_keyring_network_password_list_free(entries); + + if (result != GNOME_KEYRING_RESULT_OK) + { + error("%s",gnome_keyring_result_to_message(result)); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +/* + * Table with helper operation callbacks, used by generic + * credential helper main function. + */ +struct credential_operation const credential_helper_ops[] = +{ + { "get", keyring_get }, + { "store", keyring_store }, + { "erase", keyring_erase }, + CREDENTIAL_OP_END +}; + +/* ------------------ credential functions ------------------ */ + +void credential_init(struct credential *c) +{ + memset(c, 0, sizeof(*c)); +} + +void credential_clear(struct credential *c) +{ + free(c->protocol); + free(c->host); + free(c->path); + free(c->username); + free_password(c->password); + + credential_init(c); +} + +int credential_read(struct credential *c) +{ + char buf[1024]; + ssize_t line_len = 0; + char *key = buf; + char *value; + + while (fgets(buf, sizeof(buf), stdin)) + { + line_len = strlen(buf); + + if(buf[line_len-1]=='\n') + buf[--line_len]='\0'; + + if(!line_len) + break; + + value = strchr(buf,'='); + if(!value) { + warning("invalid credential line: %s", key); + return -1; + } + *value++ = '\0'; + + if (!strcmp(key, "protocol")) { + free(c->protocol); + c->protocol = xstrdup(value); + } else if (!strcmp(key, "host")) { + free(c->host); + c->host = xstrdup(value); + value = strrchr(c->host,':'); + if (value) { + *value++ = '\0'; + c->port = atoi(value); + } + } else if (!strcmp(key, "path")) { + free(c->path); + c->path = xstrdup(value); + } else if (!strcmp(key, "username")) { + free(c->username); + c->username = xstrdup(value); + } else if (!strcmp(key, "password")) { + free_password(c->password); + c->password = xstrdup(value); + while (*value) *value++ = '\0'; + } + /* + * Ignore other lines; we don't know what they mean, but + * this future-proofs us when later versions of git do + * learn new lines, and the helpers are updated to match. + */ + } + return 0; +} + +void credential_write_item(FILE *fp, const char *key, const char *value) +{ + if (!value) + return; + fprintf(fp, "%s=%s\n", key, value); +} + +void credential_write(const struct credential *c) +{ + /* only write username/password, if set */ + credential_write_item(stdout, "username", c->username); + credential_write_item(stdout, "password", c->password); +} + +static void usage(const char *name) +{ + struct credential_operation const *try_op = credential_helper_ops; + const char *basename = strrchr(name,'/'); + + basename = (basename) ? basename + 1 : name; + fprintf(stderr, "usage: %s <", basename); + while(try_op->name) { + fprintf(stderr,"%s",(try_op++)->name); + if(try_op->name) + fprintf(stderr,"%s","|"); + } + fprintf(stderr,"%s",">\n"); +} + +int main(int argc, char *argv[]) +{ + int ret = EXIT_SUCCESS; + + struct credential_operation const *try_op = credential_helper_ops; + struct credential cred = CREDENTIAL_INIT; + + if (!argv[1]) { + usage(argv[0]); + goto out; + } + + /* lookup operation callback */ + while(try_op->name && strcmp(argv[1], try_op->name)) + try_op++; + + /* unsupported operation given -- ignore silently */ + if(!try_op->name || !try_op->op) + goto out; + + ret = credential_read(&cred); + if(ret) + goto out; + + /* perform credential operation */ + ret = (*try_op->op)(&cred); + + credential_write(&cred); + +out: + credential_clear(&cred); + return ret; +} diff --git a/contrib/credential/osxkeychain/git-credential-osxkeychain.c b/contrib/credential/osxkeychain/git-credential-osxkeychain.c index 6beed123ab..3940202b36 100644 --- a/contrib/credential/osxkeychain/git-credential-osxkeychain.c +++ b/contrib/credential/osxkeychain/git-credential-osxkeychain.c @@ -154,7 +154,7 @@ static void read_credential(void) int main(int argc, const char **argv) { const char *usage = - "Usage: git credential-osxkeychain <get|store|erase>"; + "usage: git credential-osxkeychain <get|store|erase>"; if (!argv[1]) die(usage); diff --git a/contrib/credential/wincred/Makefile b/contrib/credential/wincred/Makefile new file mode 100644 index 0000000000..bad45ca47a --- /dev/null +++ b/contrib/credential/wincred/Makefile @@ -0,0 +1,14 @@ +all: git-credential-wincred.exe + +CC = gcc +RM = rm -f +CFLAGS = -O2 -Wall + +-include ../../../config.mak.autogen +-include ../../../config.mak + +git-credential-wincred.exe : git-credential-wincred.c + $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +clean: + $(RM) git-credential-wincred.exe diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c new file mode 100644 index 0000000000..a1d38f035b --- /dev/null +++ b/contrib/credential/wincred/git-credential-wincred.c @@ -0,0 +1,301 @@ +/* + * A git credential helper that interface with Windows' Credential Manager + * + */ +#include <windows.h> +#include <stdio.h> +#include <io.h> +#include <fcntl.h> + +/* common helpers */ + +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) + +static void die(const char *err, ...) +{ + char msg[4096]; + va_list params; + va_start(params, err); + vsnprintf(msg, sizeof(msg), err, params); + fprintf(stderr, "%s\n", msg); + va_end(params); + exit(1); +} + +static void *xmalloc(size_t size) +{ + void *ret = malloc(size); + if (!ret && !size) + ret = malloc(1); + if (!ret) + die("Out of memory"); + return ret; +} + +/* MinGW doesn't have wincred.h, so we need to define stuff */ + +typedef struct _CREDENTIAL_ATTRIBUTEW { + LPWSTR Keyword; + DWORD Flags; + DWORD ValueSize; + LPBYTE Value; +} CREDENTIAL_ATTRIBUTEW, *PCREDENTIAL_ATTRIBUTEW; + +typedef struct _CREDENTIALW { + DWORD Flags; + DWORD Type; + LPWSTR TargetName; + LPWSTR Comment; + FILETIME LastWritten; + DWORD CredentialBlobSize; + LPBYTE CredentialBlob; + DWORD Persist; + DWORD AttributeCount; + PCREDENTIAL_ATTRIBUTEW Attributes; + LPWSTR TargetAlias; + LPWSTR UserName; +} CREDENTIALW, *PCREDENTIALW; + +#define CRED_TYPE_GENERIC 1 +#define CRED_PERSIST_LOCAL_MACHINE 2 +#define CRED_MAX_ATTRIBUTES 64 + +typedef BOOL (WINAPI *CredWriteWT)(PCREDENTIALW, DWORD); +typedef BOOL (WINAPI *CredEnumerateWT)(LPCWSTR, DWORD, DWORD *, + PCREDENTIALW **); +typedef VOID (WINAPI *CredFreeT)(PVOID); +typedef BOOL (WINAPI *CredDeleteWT)(LPCWSTR, DWORD, DWORD); + +static HMODULE advapi; +static CredWriteWT CredWriteW; +static CredEnumerateWT CredEnumerateW; +static CredFreeT CredFree; +static CredDeleteWT CredDeleteW; + +static void load_cred_funcs(void) +{ + /* load DLLs */ + advapi = LoadLibrary("advapi32.dll"); + if (!advapi) + die("failed to load advapi32.dll"); + + /* get function pointers */ + CredWriteW = (CredWriteWT)GetProcAddress(advapi, "CredWriteW"); + CredEnumerateW = (CredEnumerateWT)GetProcAddress(advapi, + "CredEnumerateW"); + CredFree = (CredFreeT)GetProcAddress(advapi, "CredFree"); + CredDeleteW = (CredDeleteWT)GetProcAddress(advapi, "CredDeleteW"); + if (!CredWriteW || !CredEnumerateW || !CredFree || !CredDeleteW) + die("failed to load functions"); +} + +static WCHAR *wusername, *password, *protocol, *host, *path, target[1024]; + +static void write_item(const char *what, LPCWSTR wbuf, int wlen) +{ + char *buf; + int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, wlen, NULL, 0, NULL, + FALSE); + buf = xmalloc(len); + + if (!WideCharToMultiByte(CP_UTF8, 0, wbuf, wlen, buf, len, NULL, FALSE)) + die("WideCharToMultiByte failed!"); + + printf("%s=", what); + fwrite(buf, 1, len, stdout); + putchar('\n'); + free(buf); +} + +/* + * Match an (optional) expected string and a delimiter in the target string, + * consuming the matched text by updating the target pointer. + */ +static int match_part(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim) +{ + LPCWSTR delim_pos, start = *ptarget; + int len; + + /* find start of delimiter (or end-of-string if delim is empty) */ + if (*delim) + delim_pos = wcsstr(start, delim); + else + delim_pos = start + wcslen(start); + + /* + * match text up to delimiter, or end of string (e.g. the '/' after + * host is optional if not followed by a path) + */ + if (delim_pos) + len = delim_pos - start; + else + len = wcslen(start); + + /* update ptarget if we either found a delimiter or need a match */ + if (delim_pos || want) + *ptarget = delim_pos ? delim_pos + wcslen(delim) : start + len; + + return !want || (!wcsncmp(want, start, len) && !want[len]); +} + +static int match_cred(const CREDENTIALW *cred) +{ + LPCWSTR target = cred->TargetName; + if (wusername && wcscmp(wusername, cred->UserName)) + return 0; + + return match_part(&target, L"git", L":") && + match_part(&target, protocol, L"://") && + match_part(&target, wusername, L"@") && + match_part(&target, host, L"/") && + match_part(&target, path, L""); +} + +static void get_credential(void) +{ + CREDENTIALW **creds; + DWORD num_creds; + int i; + + if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds)) + return; + + /* search for the first credential that matches username */ + for (i = 0; i < num_creds; ++i) + if (match_cred(creds[i])) { + write_item("username", creds[i]->UserName, + wcslen(creds[i]->UserName)); + write_item("password", + (LPCWSTR)creds[i]->CredentialBlob, + creds[i]->CredentialBlobSize / sizeof(WCHAR)); + break; + } + + CredFree(creds); +} + +static void store_credential(void) +{ + CREDENTIALW cred; + + if (!wusername || !password) + return; + + cred.Flags = 0; + cred.Type = CRED_TYPE_GENERIC; + cred.TargetName = target; + cred.Comment = L"saved by git-credential-wincred"; + cred.CredentialBlobSize = (wcslen(password)) * sizeof(WCHAR); + cred.CredentialBlob = (LPVOID)password; + cred.Persist = CRED_PERSIST_LOCAL_MACHINE; + cred.AttributeCount = 0; + cred.Attributes = NULL; + cred.TargetAlias = NULL; + cred.UserName = wusername; + + if (!CredWriteW(&cred, 0)) + die("CredWrite failed"); +} + +static void erase_credential(void) +{ + CREDENTIALW **creds; + DWORD num_creds; + int i; + + if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds)) + return; + + for (i = 0; i < num_creds; ++i) { + if (match_cred(creds[i])) + CredDeleteW(creds[i]->TargetName, creds[i]->Type, 0); + } + + CredFree(creds); +} + +static WCHAR *utf8_to_utf16_dup(const char *str) +{ + int wlen = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + WCHAR *wstr = xmalloc(sizeof(WCHAR) * wlen); + MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, wlen); + return wstr; +} + +static void read_credential(void) +{ + char buf[1024]; + + while (fgets(buf, sizeof(buf), stdin)) { + char *v; + int len = strlen(buf); + /* strip trailing CR / LF */ + while (len && strchr("\r\n", buf[len - 1])) + buf[--len] = 0; + + if (!*buf) + break; + + v = strchr(buf, '='); + if (!v) + die("bad input: %s", buf); + *v++ = '\0'; + + if (!strcmp(buf, "protocol")) + protocol = utf8_to_utf16_dup(v); + else if (!strcmp(buf, "host")) + host = utf8_to_utf16_dup(v); + else if (!strcmp(buf, "path")) + path = utf8_to_utf16_dup(v); + else if (!strcmp(buf, "username")) { + wusername = utf8_to_utf16_dup(v); + } else if (!strcmp(buf, "password")) + password = utf8_to_utf16_dup(v); + else + die("unrecognized input"); + } +} + +int main(int argc, char *argv[]) +{ + const char *usage = + "usage: git credential-wincred <get|store|erase>\n"; + + if (!argv[1]) + die(usage); + + /* git use binary pipes to avoid CRLF-issues */ + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); + + read_credential(); + + load_cred_funcs(); + + if (!protocol || !(host || path)) + return 0; + + /* prepare 'target', the unique key for the credential */ + wcscpy(target, L"git:"); + wcsncat(target, protocol, ARRAY_SIZE(target)); + wcsncat(target, L"://", ARRAY_SIZE(target)); + if (wusername) { + wcsncat(target, wusername, ARRAY_SIZE(target)); + wcsncat(target, L"@", ARRAY_SIZE(target)); + } + if (host) + wcsncat(target, host, ARRAY_SIZE(target)); + if (path) { + wcsncat(target, L"/", ARRAY_SIZE(target)); + wcsncat(target, path, ARRAY_SIZE(target)); + } + + if (!strcmp(argv[1], "get")) + get_credential(); + else if (!strcmp(argv[1], "store")) + store_credential(); + else if (!strcmp(argv[1], "erase")) + erase_credential(); + /* otherwise, ignore unknown action */ + return 0; +} diff --git a/contrib/emacs/git.el b/contrib/emacs/git.el index 65c95d9d5a..5ffc506f6d 100644 --- a/contrib/emacs/git.el +++ b/contrib/emacs/git.el @@ -1671,7 +1671,7 @@ Commands: "Entry point into git-status mode." (interactive "DSelect directory: ") (setq dir (git-get-top-dir dir)) - (if (file-directory-p (concat (file-name-as-directory dir) ".git")) + (if (file-exists-p (concat (file-name-as-directory dir) ".git")) (let ((buffer (or (and git-reuse-status-buffer (git-find-status-buffer dir)) (create-file-buffer (expand-file-name "*git-status*" dir))))) (switch-to-buffer buffer) diff --git a/contrib/examples/builtin-fetch--tool.c b/contrib/examples/builtin-fetch--tool.c index 0d54aa7061..8bc8c7533a 100644 --- a/contrib/examples/builtin-fetch--tool.c +++ b/contrib/examples/builtin-fetch--tool.c @@ -96,7 +96,7 @@ static int update_local_ref(const char *name, strcpy(oldh, find_unique_abbrev(current->object.sha1, DEFAULT_ABBREV)); strcpy(newh, find_unique_abbrev(sha1_new, DEFAULT_ABBREV)); - if (in_merge_bases(current, &updated, 1)) { + if (in_merge_bases(current, updated)) { fprintf(stderr, "* %s: fast-forward to %s\n", name, note); fprintf(stderr, " old..new: %s..%s\n", oldh, newh); diff --git a/contrib/examples/git-remote.perl b/contrib/examples/git-remote.perl index b17952a785..d42df7b418 100755 --- a/contrib/examples/git-remote.perl +++ b/contrib/examples/git-remote.perl @@ -347,7 +347,7 @@ sub rm_remote { } sub add_usage { - print STDERR "Usage: git remote add [-f] [-t track]* [-m master] <name> <url>\n"; + print STDERR "usage: git remote add [-f] [-t track]* [-m master] <name> <url>\n"; exit(1); } @@ -380,7 +380,7 @@ elsif ($ARGV[0] eq 'show') { } } if ($i >= @ARGV) { - print STDERR "Usage: git remote show <remote>\n"; + print STDERR "usage: git remote show <remote>\n"; exit(1); } my $status = 0; @@ -410,7 +410,7 @@ elsif ($ARGV[0] eq 'prune') { } } if ($i >= @ARGV) { - print STDERR "Usage: git remote prune <remote>\n"; + print STDERR "usage: git remote prune <remote>\n"; exit(1); } my $status = 0; @@ -458,13 +458,13 @@ elsif ($ARGV[0] eq 'add') { } elsif ($ARGV[0] eq 'rm') { if (@ARGV <= 1) { - print STDERR "Usage: git remote rm <remote>\n"; + print STDERR "usage: git remote rm <remote>\n"; exit(1); } exit(rm_remote($ARGV[1])); } else { - print STDERR "Usage: git remote\n"; + print STDERR "usage: git remote\n"; print STDERR " git remote add <name> <url>\n"; print STDERR " git remote rm <name>\n"; print STDERR " git remote show <name>\n"; diff --git a/contrib/examples/git-svnimport.perl b/contrib/examples/git-svnimport.perl index b09ff8f12f..c414f0d9c7 100755 --- a/contrib/examples/git-svnimport.perl +++ b/contrib/examples/git-svnimport.perl @@ -36,7 +36,7 @@ our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T, sub usage() { print STDERR <<END; -Usage: ${\basename $0} # fetch/update GIT from SVN +usage: ${\basename $0} # fetch/update GIT from SVN [-o branch-for-HEAD] [-h] [-v] [-l max_rev] [-R repack_each_revs] [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname] [-d|-D] [-i] [-u] [-r] [-I ignorefilename] [-s start_chg] diff --git a/contrib/fast-import/git-import.perl b/contrib/fast-import/git-import.perl index f9fef6db28..0891b9e366 100755 --- a/contrib/fast-import/git-import.perl +++ b/contrib/fast-import/git-import.perl @@ -7,7 +7,7 @@ use strict; use File::Find; -my $USAGE = 'Usage: git-import branch import-message'; +my $USAGE = 'usage: git-import branch import-message'; my $branch = shift or die "$USAGE\n"; my $message = shift or die "$USAGE\n"; diff --git a/contrib/fast-import/git-import.sh b/contrib/fast-import/git-import.sh index 0ca7718d05..f8d803c5e2 100755 --- a/contrib/fast-import/git-import.sh +++ b/contrib/fast-import/git-import.sh @@ -5,7 +5,7 @@ # but is meant to be a simple fast-import example. if [ -z "$1" -o -z "$2" ]; then - echo "Usage: git-import branch import-message" + echo "usage: git-import branch import-message" exit 1 fi diff --git a/contrib/fast-import/import-zips.py b/contrib/fast-import/import-zips.py index 82f5ed3ddc..d12c296223 100755 --- a/contrib/fast-import/import-zips.py +++ b/contrib/fast-import/import-zips.py @@ -9,13 +9,18 @@ ## git log --stat import-zips from os import popen, path -from sys import argv, exit +from sys import argv, exit, hexversion, stderr from time import mktime from zipfile import ZipFile +if hexversion < 0x01060000: + # The limiter is the zipfile module + stderr.write("import-zips.py: requires Python 1.6.0 or later.\n") + exit(1) + if len(argv) < 2: - print 'Usage:', argv[0], '<zipfile>...' - exit(1) + print 'usage:', argv[0], '<zipfile>...' + exit(1) branch_ref = 'refs/heads/import-zips' committer_name = 'Z Ip Creator' @@ -23,51 +28,51 @@ committer_email = 'zip@example.com' fast_import = popen('git fast-import --quiet', 'w') def printlines(list): - for str in list: - fast_import.write(str + "\n") + for str in list: + fast_import.write(str + "\n") for zipfile in argv[1:]: - commit_time = 0 - next_mark = 1 - common_prefix = None - mark = dict() - - zip = ZipFile(zipfile, 'r') - for name in zip.namelist(): - if name.endswith('/'): - continue - info = zip.getinfo(name) - - if commit_time < info.date_time: - commit_time = info.date_time - if common_prefix == None: - common_prefix = name[:name.rfind('/') + 1] - else: - while not name.startswith(common_prefix): - last_slash = common_prefix[:-1].rfind('/') + 1 - common_prefix = common_prefix[:last_slash] - - mark[name] = ':' + str(next_mark) - next_mark += 1 - - printlines(('blob', 'mark ' + mark[name], \ - 'data ' + str(info.file_size))) - fast_import.write(zip.read(name) + "\n") - - committer = committer_name + ' <' + committer_email + '> %d +0000' % \ - mktime(commit_time + (0, 0, 0)) - - printlines(('commit ' + branch_ref, 'committer ' + committer, \ - 'data <<EOM', 'Imported from ' + zipfile + '.', 'EOM', \ - '', 'deleteall')) - - for name in mark.keys(): - fast_import.write('M 100644 ' + mark[name] + ' ' + - name[len(common_prefix):] + "\n") - - printlines(('', 'tag ' + path.basename(zipfile), \ - 'from ' + branch_ref, 'tagger ' + committer, \ - 'data <<EOM', 'Package ' + zipfile, 'EOM', '')) + commit_time = 0 + next_mark = 1 + common_prefix = None + mark = dict() + + zip = ZipFile(zipfile, 'r') + for name in zip.namelist(): + if name.endswith('/'): + continue + info = zip.getinfo(name) + + if commit_time < info.date_time: + commit_time = info.date_time + if common_prefix == None: + common_prefix = name[:name.rfind('/') + 1] + else: + while not name.startswith(common_prefix): + last_slash = common_prefix[:-1].rfind('/') + 1 + common_prefix = common_prefix[:last_slash] + + mark[name] = ':' + str(next_mark) + next_mark += 1 + + printlines(('blob', 'mark ' + mark[name], \ + 'data ' + str(info.file_size))) + fast_import.write(zip.read(name) + "\n") + + committer = committer_name + ' <' + committer_email + '> %d +0000' % \ + mktime(commit_time + (0, 0, 0)) + + printlines(('commit ' + branch_ref, 'committer ' + committer, \ + 'data <<EOM', 'Imported from ' + zipfile + '.', 'EOM', \ + '', 'deleteall')) + + for name in mark.keys(): + fast_import.write('M 100644 ' + mark[name] + ' ' + + name[len(common_prefix):] + "\n") + + printlines(('', 'tag ' + path.basename(zipfile), \ + 'from ' + branch_ref, 'tagger ' + committer, \ + 'data <<EOM', 'Package ' + zipfile, 'EOM', '')) if fast_import.close(): - exit(1) + exit(1) diff --git a/contrib/hg-to-git/hg-to-git.py b/contrib/hg-to-git/hg-to-git.py index 046cb2b268..232625a7b7 100755 --- a/contrib/hg-to-git/hg-to-git.py +++ b/contrib/hg-to-git/hg-to-git.py @@ -23,6 +23,11 @@ import os, os.path, sys import tempfile, pickle, getopt import re +if sys.hexversion < 0x02030000: + # The behavior of the pickle module changed significantly in 2.3 + sys.stderr.write("hg-to-git.py: requires Python 2.3 or later.\n") + sys.exit(1) + # Maps hg version -> git version hgvers = {} # List of children for each hg revision diff --git a/contrib/hooks/post-receive-email b/contrib/hooks/post-receive-email index b2171a092e..0e5b72d7f1 100755 --- a/contrib/hooks/post-receive-email +++ b/contrib/hooks/post-receive-email @@ -237,6 +237,7 @@ generate_email_header() X-Git-Reftype: $refname_type X-Git-Oldrev: $oldrev X-Git-Newrev: $newrev + Auto-Submitted: auto-generated This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing diff --git a/contrib/hooks/setgitperms.perl b/contrib/hooks/setgitperms.perl index a577ad095f..2770a1b1d2 100644 --- a/contrib/hooks/setgitperms.perl +++ b/contrib/hooks/setgitperms.perl @@ -24,7 +24,7 @@ use File::Find; use File::Basename; my $usage = -"Usage: setgitperms.perl [OPTION]... <--read|--write> +"usage: setgitperms.perl [OPTION]... <--read|--write> This program uses a file `.gitmeta` to store/restore permissions and uid/gid info for all files/dirs tracked by git in the repository. diff --git a/contrib/mw-to-git/.gitignore b/contrib/mw-to-git/.gitignore new file mode 100644 index 0000000000..b9196555e5 --- /dev/null +++ b/contrib/mw-to-git/.gitignore @@ -0,0 +1 @@ +git-remote-mediawiki diff --git a/contrib/mw-to-git/Makefile b/contrib/mw-to-git/Makefile index 3ed728b0ef..f14971987c 100644 --- a/contrib/mw-to-git/Makefile +++ b/contrib/mw-to-git/Makefile @@ -1,47 +1,17 @@ # -# Copyright (C) 2012 -# Charles Roussel <charles.roussel@ensimag.imag.fr> -# Simon Cathebras <simon.cathebras@ensimag.imag.fr> -# Julien Khayat <julien.khayat@ensimag.imag.fr> -# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> -# Simon Perrat <simon.perrat@ensimag.imag.fr> +# Copyright (C) 2013 +# Matthieu Moy <Matthieu.Moy@imag.fr> # ## Build git-remote-mediawiki --include ../../config.mak.autogen --include ../../config.mak +SCRIPT_PERL=git-remote-mediawiki.perl +GIT_ROOT_DIR=../.. +HERE=contrib/mw-to-git/ -ifndef PERL_PATH - PERL_PATH = /usr/bin/perl -endif -ifndef gitexecdir - gitexecdir = $(shell git --exec-path) -endif +SCRIPT_PERL_FULL=$(patsubst %,$(HERE)/%,$(SCRIPT_PERL)) -PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH)) -gitexecdir_SQ = $(subst ','\'',$(gitexecdir)) -SCRIPT = git-remote-mediawiki +all: build -.PHONY: install help doc test clean - -help: - @echo 'This is the help target of the Makefile. Current configuration:' - @echo ' gitexecdir = $(gitexecdir_SQ)' - @echo ' PERL_PATH = $(PERL_PATH_SQ)' - @echo 'Run "$(MAKE) install" to install $(SCRIPT) in gitexecdir' - @echo 'Run "$(MAKE) test" to run the testsuite' - -install: - sed -e '1s|#!.*/perl|#!$(PERL_PATH_SQ)|' $(SCRIPT) \ - > '$(gitexecdir_SQ)/$(SCRIPT)' - chmod +x '$(gitexecdir)/$(SCRIPT)' - -doc: - @echo 'Sorry, "make doc" is not implemented yet for $(SCRIPT)' - -test: - $(MAKE) -C t/ test - -clean: - $(RM) '$(gitexecdir)/$(SCRIPT)' - $(MAKE) -C t/ clean +build install clean: + $(MAKE) -C $(GIT_ROOT_DIR) SCRIPT_PERL=$(SCRIPT_PERL_FULL) \ + $@-perl-script diff --git a/contrib/mw-to-git/git-remote-mediawiki b/contrib/mw-to-git/git-remote-mediawiki.perl index 68555d4265..9c14c1f88d 100755 --- a/contrib/mw-to-git/git-remote-mediawiki +++ b/contrib/mw-to-git/git-remote-mediawiki.perl @@ -28,7 +28,7 @@ use warnings; use constant SLASH_REPLACEMENT => "%2F"; # It's not always possible to delete pages (may require some -# priviledges). Deleted pages are replaced with this content. +# privileges). Deleted pages are replaced with this content. use constant DELETED_CONTENT => "[[Category:Deleted]]\n"; # It's not possible to create empty pages. New empty files in Git are @@ -711,6 +711,14 @@ sub fetch_mw_revisions { return ($n, @revisions); } +sub fe_escape_path { + my $path = shift; + $path =~ s/\\/\\\\/g; + $path =~ s/"/\\"/g; + $path =~ s/\n/\\n/g; + return '"' . $path . '"'; +} + sub import_file_revision { my $commit = shift; my %commit = %{$commit}; @@ -738,15 +746,17 @@ sub import_file_revision { print STDOUT "from refs/mediawiki/$remotename/master^0\n"; } if ($content ne DELETED_CONTENT) { - print STDOUT "M 644 inline $title.mw\n"; + print STDOUT "M 644 inline " . + fe_escape_path($title . ".mw") . "\n"; literal_data($content); if (%mediafile) { - print STDOUT "M 644 inline $mediafile{title}\n"; + print STDOUT "M 644 inline " + . fe_escape_path($mediafile{title}) . "\n"; literal_data_raw($mediafile{content}); } print STDOUT "\n\n"; } else { - print STDOUT "D $title.mw\n"; + print STDOUT "D " . fe_escape_path($title . ".mw") . "\n"; } # mediawiki revision number in the git note @@ -831,7 +841,7 @@ sub mw_import_ref { if ($fetch_from == 1 && $n == 0) { print STDERR "You appear to have cloned an empty MediaWiki.\n"; # Something has to be done remote-helper side. If nothing is done, an error is - # thrown saying that HEAD is refering to unknown object 0000000000000000000 + # thrown saying that HEAD is referring to unknown object 0000000000000000000 # and the clone fails. } } @@ -1057,7 +1067,7 @@ sub mw_push_file { my $file_content; if ($page_deleted) { # Deleting a page usually requires - # special priviledges. A common + # special privileges. A common # convention is to replace the page # with this content instead: $file_content = DELETED_CONTENT; diff --git a/contrib/mw-to-git/git-remote-mediawiki.txt b/contrib/mw-to-git/git-remote-mediawiki.txt index 4d211f5b81..23b7ef9f62 100644 --- a/contrib/mw-to-git/git-remote-mediawiki.txt +++ b/contrib/mw-to-git/git-remote-mediawiki.txt @@ -4,4 +4,4 @@ objects from mediawiki just as one would do with a classic git repository thanks to remote-helpers. For more information, visit the wiki at -https://github.com/Bibzball/Git-Mediawiki/wiki +https://github.com/moy/Git-Mediawiki/wiki diff --git a/contrib/mw-to-git/t/README b/contrib/mw-to-git/t/README index 96e97390cf..03f6ee5d72 100644 --- a/contrib/mw-to-git/t/README +++ b/contrib/mw-to-git/t/README @@ -25,7 +25,7 @@ Principles and Technical Choices The test environment makes it easy to install and manipulate one or several MediaWiki instances. To allow developers to run the testsuite -easily, the environment does not require root priviledge (except to +easily, the environment does not require root privilege (except to install the required packages if needed). It starts a webserver instance on the user's account (using lighttpd greatly helps for that), and does not need a separate database daemon (thanks to the use @@ -81,7 +81,7 @@ parameters, please refer to the `test-gitmw-lib.sh` and ** `test_check_wiki_precond`: Check if the tests must be skipped or not. Please use this function -at the beggining of each new test file. +at the beginning of each new test file. ** `wiki_getpage`: Fetch a given page from the wiki and puts its content in the @@ -113,7 +113,7 @@ Tests if a given page exists on the wiki. ** `wiki_reset`: Reset the wiki, i.e. flush the database. Use this function at the -begining of each new test, except if the test re-uses the same wiki +beginning of each new test, except if the test re-uses the same wiki (and history) as the previous test. How to write a new test diff --git a/contrib/mw-to-git/t/install-wiki.sh b/contrib/mw-to-git/t/install-wiki.sh index c6d6fa3aef..70a53f67fd 100755 --- a/contrib/mw-to-git/t/install-wiki.sh +++ b/contrib/mw-to-git/t/install-wiki.sh @@ -15,7 +15,7 @@ fi . "$WIKI_TEST_DIR"/test-gitmw-lib.sh usage () { - echo "Usage: " + echo "usage: " echo " ./install-wiki.sh <install | delete | --help>" echo " install | -i : Install a wiki on your computer." echo " delete | -d : Delete the wiki and all its pages and " diff --git a/contrib/mw-to-git/t/install-wiki/LocalSettings.php b/contrib/mw-to-git/t/install-wiki/LocalSettings.php index 29f125116b..745e47e881 100644 --- a/contrib/mw-to-git/t/install-wiki/LocalSettings.php +++ b/contrib/mw-to-git/t/install-wiki/LocalSettings.php @@ -88,7 +88,7 @@ $wgShellLocale = "en_US.utf8"; ## Set $wgCacheDirectory to a writable directory on the web server ## to make your wiki go slightly faster. The directory should not -## be publically accessible from the web. +## be publicly accessible from the web. #$wgCacheDirectory = "$IP/cache"; # Site language code, should be one of the list in ./languages/Names.php 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 246d47d8fb..37021e200a 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 @@ -139,7 +139,7 @@ test_expect_success 'character $ in file name (git -> mw) ' ' ' -test_expect_failure 'capital at the begining of file names' ' +test_expect_failure 'capital at the beginning of file names' ' wiki_reset && git clone mediawiki::'"$WIKI_URL"' mw_dir_10 && ( @@ -156,7 +156,7 @@ test_expect_failure 'capital at the begining of file names' ' ' -test_expect_failure 'special character at the begining of file name from mw to git' ' +test_expect_failure 'special character at the beginning of file name from mw to git' ' wiki_reset && git clone mediawiki::'"$WIKI_URL"' mw_dir_11 && wiki_editpage {char_1 "expect to be renamed {char_1" false && @@ -189,7 +189,7 @@ test_expect_success 'Push page with title containing ":" other than namespace se wiki_page_exist NotANameSpace:Page ' -test_expect_success 'test of correct formating for file name from mw to git' ' +test_expect_success 'test of correct formatting for file name from mw to git' ' wiki_reset && git clone mediawiki::'"$WIKI_URL"' mw_dir_12 && wiki_editpage char_%_7b_1 "expect to be renamed char{_1" false && @@ -207,7 +207,7 @@ test_expect_success 'test of correct formating for file name from mw to git' ' ' -test_expect_failure 'test of correct formating for file name begining with special character' ' +test_expect_failure 'test of correct formatting for file name beginning with special character' ' wiki_reset && git clone mediawiki::'"$WIKI_URL"' mw_dir_13 && ( @@ -215,7 +215,7 @@ test_expect_failure 'test of correct formating for file name begining with speci echo "my new file {char_1" >\{char_1.mw && echo "my new file [char_2" >\[char_2.mw && git add . && - git commit -am "commiting some exotic file name..." && + git commit -am "committing some exotic file name..." && git push && git pull ) && @@ -226,7 +226,7 @@ test_expect_failure 'test of correct formating for file name begining with speci ' -test_expect_success 'test of correct formating for file name from git to mw' ' +test_expect_success 'test of correct formatting for file name from git to mw' ' wiki_reset && git clone mediawiki::'"$WIKI_URL"' mw_dir_14 && ( @@ -234,7 +234,7 @@ test_expect_success 'test of correct formating for file name from git to mw' ' echo "my new file char{_1" >Char\{_1.mw && echo "my new file char[_2" >Char\[_2.mw && git add . && - git commit -m "commiting some exotic file name..." && + git commit -m "committing some exotic file name..." && git push ) && wiki_getallpage ref_page_14 && @@ -318,4 +318,30 @@ test_expect_success 'git push with \ in format control' ' ' +test_expect_success 'fast-import meta-characters in page name (mw -> git)' ' + wiki_reset && + wiki_editpage \"file\"_\\_foo "expect to be called \"file\"_\\_foo" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_21 && + test_path_is_file mw_dir_21/\"file\"_\\_foo.mw && + wiki_getallpage ref_page_21 && + test_diff_directories mw_dir_21 ref_page_21 +' + + +test_expect_success 'fast-import meta-characters in page name (git -> mw) ' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_22 && + ( + cd mw_dir_22 && + echo "this file is called \"file\"_\\_foo.mw" >\"file\"_\\_foo && + git add . && + git commit -am "file \"file\"_\\_foo" && + git pull && + git push + ) && + wiki_getallpage ref_page_22 && + test_diff_directories mw_dir_22 ref_page_22 +' + + test_done diff --git a/contrib/p4import/git-p4import.py b/contrib/p4import/git-p4import.py index b6e534b65b..593d6a0682 100644 --- a/contrib/p4import/git-p4import.py +++ b/contrib/p4import/git-p4import.py @@ -14,6 +14,11 @@ import sys import time import getopt +if sys.hexversion < 0x02020000: + # The behavior of the marshal module changed significantly in 2.2 + sys.stderr.write("git-p4import.py: requires Python 2.2 or later.\n") + sys.exit(1) + from signal import signal, \ SIGPIPE, SIGINT, SIG_DFL, \ default_int_handler diff --git a/contrib/remote-helpers/Makefile b/contrib/remote-helpers/Makefile new file mode 100644 index 0000000000..9a76575f78 --- /dev/null +++ b/contrib/remote-helpers/Makefile @@ -0,0 +1,13 @@ +TESTS := $(wildcard test*.sh) + +export T := $(addprefix $(CURDIR)/,$(TESTS)) +export MAKE := $(MAKE) -e +export PATH := $(CURDIR):$(PATH) + +test: + $(MAKE) -C ../../t $@ + +$(TESTS): + $(MAKE) -C ../../t $(CURDIR)/$@ + +.PHONY: $(TESTS) diff --git a/contrib/remote-helpers/git-remote-bzr b/contrib/remote-helpers/git-remote-bzr new file mode 100755 index 0000000000..c5822e4ac9 --- /dev/null +++ b/contrib/remote-helpers/git-remote-bzr @@ -0,0 +1,725 @@ +#!/usr/bin/env python +# +# Copyright (c) 2012 Felipe Contreras +# + +# +# Just copy to your ~/bin, or anywhere in your $PATH. +# Then you can clone with: +# % git clone bzr::/path/to/bzr/repo/or/url +# +# For example: +# % git clone bzr::$HOME/myrepo +# or +# % git clone bzr::lp:myrepo +# + +import sys + +import bzrlib +if hasattr(bzrlib, "initialize"): + bzrlib.initialize() + +import bzrlib.plugin +bzrlib.plugin.load_plugins() + +import bzrlib.generate_ids +import bzrlib.transport + +import sys +import os +import json +import re +import StringIO + +NAME_RE = re.compile('^([^<>]+)') +AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$') +RAW_AUTHOR_RE = re.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)') + +def die(msg, *args): + sys.stderr.write('ERROR: %s\n' % (msg % args)) + sys.exit(1) + +def warn(msg, *args): + sys.stderr.write('WARNING: %s\n' % (msg % args)) + +def gittz(tz): + return '%+03d%02d' % (tz / 3600, tz % 3600 / 60) + +class Marks: + + def __init__(self, path): + self.path = path + self.tips = {} + self.marks = {} + self.rev_marks = {} + self.last_mark = 0 + self.load() + + def load(self): + if not os.path.exists(self.path): + return + + tmp = json.load(open(self.path)) + self.tips = tmp['tips'] + self.marks = tmp['marks'] + self.last_mark = tmp['last-mark'] + + for rev, mark in self.marks.iteritems(): + self.rev_marks[mark] = rev + + def dict(self): + return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark } + + def store(self): + json.dump(self.dict(), open(self.path, 'w')) + + def __str__(self): + return str(self.dict()) + + def from_rev(self, rev): + return self.marks[rev] + + def to_rev(self, mark): + return self.rev_marks[mark] + + def next_mark(self): + self.last_mark += 1 + return self.last_mark + + def get_mark(self, rev): + self.last_mark += 1 + self.marks[rev] = self.last_mark + return self.last_mark + + def is_marked(self, rev): + return self.marks.has_key(rev) + + def new_mark(self, rev, mark): + self.marks[rev] = mark + self.rev_marks[mark] = rev + self.last_mark = mark + + def get_tip(self, branch): + return self.tips.get(branch, None) + + def set_tip(self, branch, tip): + self.tips[branch] = tip + +class Parser: + + def __init__(self, repo): + self.repo = repo + self.line = self.get_line() + + def get_line(self): + return sys.stdin.readline().strip() + + def __getitem__(self, i): + return self.line.split()[i] + + def check(self, word): + return self.line.startswith(word) + + def each_block(self, separator): + while self.line != separator: + yield self.line + self.line = self.get_line() + + def __iter__(self): + return self.each_block('') + + def next(self): + self.line = self.get_line() + if self.line == 'done': + self.line = None + + def get_mark(self): + i = self.line.index(':') + 1 + return int(self.line[i:]) + + def get_data(self): + if not self.check('data'): + return None + i = self.line.index(' ') + 1 + size = int(self.line[i:]) + return sys.stdin.read(size) + + def get_author(self): + m = RAW_AUTHOR_RE.match(self.line) + if not m: + return None + _, name, email, date, tz = m.groups() + committer = '%s <%s>' % (name, email) + tz = int(tz) + tz = ((tz / 100) * 3600) + ((tz % 100) * 60) + return (committer, int(date), tz) + +def rev_to_mark(rev): + global marks + return marks.from_rev(rev) + +def mark_to_rev(mark): + global marks + return marks.to_rev(mark) + +def fixup_user(user): + name = mail = None + user = user.replace('"', '') + m = AUTHOR_RE.match(user) + if m: + name = m.group(1) + mail = m.group(2).strip() + else: + m = NAME_RE.match(user) + if m: + name = m.group(1).strip() + + return '%s <%s>' % (name, mail) + +def get_filechanges(cur, prev): + modified = {} + removed = {} + + changes = cur.changes_from(prev) + + for path, fid, kind in changes.added: + modified[path] = fid + for path, fid, kind in changes.removed: + removed[path] = None + for path, fid, kind, mod, _ in changes.modified: + modified[path] = fid + for oldpath, newpath, fid, kind, mod, _ in changes.renamed: + removed[oldpath] = None + modified[newpath] = fid + + return modified, removed + +def export_files(tree, files): + global marks, filenodes + + final = [] + for path, fid in files.iteritems(): + kind = tree.kind(fid) + + h = tree.get_file_sha1(fid) + + if kind == 'symlink': + d = tree.get_symlink_target(fid) + mode = '120000' + elif kind == 'file': + + if tree.is_executable(fid): + mode = '100755' + else: + mode = '100644' + + # is the blog already exported? + if h in filenodes: + mark = filenodes[h] + final.append((mode, mark, path)) + continue + + d = tree.get_file_text(fid) + elif kind == 'directory': + continue + else: + die("Unhandled kind '%s' for path '%s'" % (kind, path)) + + mark = marks.next_mark() + filenodes[h] = mark + + print "blob" + print "mark :%u" % mark + print "data %d" % len(d) + print d + + final.append((mode, mark, path)) + + return final + +def export_branch(branch, name): + global prefix, dirname + + ref = '%s/heads/%s' % (prefix, name) + tip = marks.get_tip(name) + + repo = branch.repository + repo.lock_read() + revs = branch.iter_merge_sorted_revisions(None, tip, 'exclude', 'forward') + count = 0 + + revs = [revid for revid, _, _, _ in revs if not marks.is_marked(revid)] + + for revid in revs: + + rev = repo.get_revision(revid) + + parents = rev.parent_ids + time = rev.timestamp + tz = rev.timezone + committer = rev.committer.encode('utf-8') + committer = "%s %u %s" % (fixup_user(committer), time, gittz(tz)) + author = committer + msg = rev.message.encode('utf-8') + + msg += '\n' + + if len(parents) == 0: + parent = bzrlib.revision.NULL_REVISION + else: + parent = parents[0] + + cur_tree = repo.revision_tree(revid) + prev = repo.revision_tree(parent) + modified, removed = get_filechanges(cur_tree, prev) + + modified_final = export_files(cur_tree, modified) + + if len(parents) == 0: + print 'reset %s' % ref + + print "commit %s" % ref + print "mark :%d" % (marks.get_mark(revid)) + print "author %s" % (author) + print "committer %s" % (committer) + print "data %d" % (len(msg)) + print msg + + for i, p in enumerate(parents): + try: + m = rev_to_mark(p) + except KeyError: + # ghost? + continue + if i == 0: + print "from :%s" % m + else: + print "merge :%s" % m + + for f in modified_final: + print "M %s :%u %s" % f + for f in removed: + print "D %s" % (f) + print + + count += 1 + if (count % 100 == 0): + print "progress revision %s (%d/%d)" % (revid, count, len(revs)) + print "#############################################################" + + repo.unlock() + + revid = branch.last_revision() + + # make sure the ref is updated + print "reset %s" % ref + print "from :%u" % rev_to_mark(revid) + print + + marks.set_tip(name, revid) + +def export_tag(repo, name): + global tags + try: + print "reset refs/tags/%s" % name + print "from :%u" % rev_to_mark(tags[name]) + print + except KeyError: + warn("TODO: fetch tag '%s'" % name) + +def do_import(parser): + global dirname + + branch = parser.repo + path = os.path.join(dirname, 'marks-git') + + print "feature done" + if os.path.exists(path): + print "feature import-marks=%s" % path + print "feature export-marks=%s" % path + sys.stdout.flush() + + while parser.check('import'): + ref = parser[1] + if ref.startswith('refs/heads/'): + name = ref[len('refs/heads/'):] + export_branch(branch, name) + if ref.startswith('refs/tags/'): + name = ref[len('refs/tags/'):] + export_tag(branch, name) + parser.next() + + print 'done' + + sys.stdout.flush() + +def parse_blob(parser): + global blob_marks + + parser.next() + mark = parser.get_mark() + parser.next() + data = parser.get_data() + blob_marks[mark] = data + parser.next() + +class CustomTree(): + + def __init__(self, repo, revid, parents, files): + global files_cache + + self.repo = repo + self.revid = revid + self.parents = parents + self.updates = {} + + def copy_tree(revid): + files = files_cache[revid] = {} + tree = repo.repository.revision_tree(revid) + repo.lock_read() + try: + for path, entry in tree.iter_entries_by_dir(): + files[path] = entry.file_id + finally: + repo.unlock() + return files + + if len(parents) == 0: + self.base_id = bzrlib.revision.NULL_REVISION + self.base_files = {} + else: + self.base_id = parents[0] + self.base_files = files_cache.get(self.base_id, None) + if not self.base_files: + self.base_files = copy_tree(self.base_id) + + self.files = files_cache[revid] = self.base_files.copy() + + for path, f in files.iteritems(): + fid = self.files.get(path, None) + if not fid: + fid = bzrlib.generate_ids.gen_file_id(path) + f['path'] = path + self.updates[fid] = f + + def last_revision(self): + return self.base_id + + def iter_changes(self): + changes = [] + + def get_parent(dirname, basename): + parent_fid = self.base_files.get(dirname, None) + if parent_fid: + return parent_fid + parent_fid = self.files.get(dirname, None) + if parent_fid: + return parent_fid + if basename == '': + return None + fid = bzrlib.generate_ids.gen_file_id(path) + d = add_entry(fid, dirname, 'directory') + return fid + + def add_entry(fid, path, kind, mode = None): + dirname, basename = os.path.split(path) + parent_fid = get_parent(dirname, basename) + + executable = False + if mode == '100755': + executable = True + elif mode == '120000': + kind = 'symlink' + + change = (fid, + (None, path), + True, + (False, True), + (None, parent_fid), + (None, basename), + (None, kind), + (None, executable)) + self.files[path] = change[0] + changes.append(change) + return change + + def update_entry(fid, path, kind, mode = None): + dirname, basename = os.path.split(path) + parent_fid = get_parent(dirname, basename) + + executable = False + if mode == '100755': + executable = True + elif mode == '120000': + kind = 'symlink' + + change = (fid, + (path, path), + True, + (True, True), + (None, parent_fid), + (None, basename), + (None, kind), + (None, executable)) + self.files[path] = change[0] + changes.append(change) + return change + + def remove_entry(fid, path, kind): + dirname, basename = os.path.split(path) + parent_fid = get_parent(dirname, basename) + change = (fid, + (path, None), + True, + (True, False), + (parent_fid, None), + (None, None), + (None, None), + (None, None)) + del self.files[path] + changes.append(change) + return change + + for fid, f in self.updates.iteritems(): + path = f['path'] + + if 'deleted' in f: + remove_entry(fid, path, 'file') + continue + + if path in self.base_files: + update_entry(fid, path, 'file', f['mode']) + else: + add_entry(fid, path, 'file', f['mode']) + + return changes + + def get_file_with_stat(self, file_id, path=None): + return (StringIO.StringIO(self.updates[file_id]['data']), None) + + def get_symlink_target(self, file_id): + return self.updates[file_id]['data'] + +def parse_commit(parser): + global marks, blob_marks, bmarks, parsed_refs + global mode + + parents = [] + + ref = parser[1] + parser.next() + + if ref != 'refs/heads/master': + die("bzr doesn't support multiple branches; use 'master'") + + commit_mark = parser.get_mark() + parser.next() + author = parser.get_author() + parser.next() + committer = parser.get_author() + parser.next() + data = parser.get_data() + parser.next() + if parser.check('from'): + parents.append(parser.get_mark()) + parser.next() + while parser.check('merge'): + parents.append(parser.get_mark()) + parser.next() + + files = {} + + for line in parser: + if parser.check('M'): + t, m, mark_ref, path = line.split(' ', 3) + mark = int(mark_ref[1:]) + f = { 'mode' : m, 'data' : blob_marks[mark] } + elif parser.check('D'): + t, path = line.split(' ') + f = { 'deleted' : True } + else: + die('Unknown file command: %s' % line) + files[path] = f + + repo = parser.repo + + committer, date, tz = committer + parents = [str(mark_to_rev(p)) for p in parents] + revid = bzrlib.generate_ids.gen_revision_id(committer, date) + props = {} + props['branch-nick'] = repo.nick + + mtree = CustomTree(repo, revid, parents, files) + changes = mtree.iter_changes() + + repo.lock_write() + try: + builder = repo.get_commit_builder(parents, None, date, tz, committer, props, revid) + try: + list(builder.record_iter_changes(mtree, mtree.last_revision(), changes)) + builder.finish_inventory() + builder.commit(data.decode('utf-8', 'replace')) + except Exception, e: + builder.abort() + raise + finally: + repo.unlock() + + parsed_refs[ref] = revid + marks.new_mark(revid, commit_mark) + +def parse_reset(parser): + global parsed_refs + + ref = parser[1] + parser.next() + + if ref != 'refs/heads/master': + die("bzr doesn't support multiple branches; use 'master'") + + # ugh + if parser.check('commit'): + parse_commit(parser) + return + if not parser.check('from'): + return + from_mark = parser.get_mark() + parser.next() + + parsed_refs[ref] = mark_to_rev(from_mark) + +def do_export(parser): + global parsed_refs, dirname, peer + + parser.next() + + for line in parser.each_block('done'): + if parser.check('blob'): + parse_blob(parser) + elif parser.check('commit'): + parse_commit(parser) + elif parser.check('reset'): + parse_reset(parser) + elif parser.check('tag'): + pass + elif parser.check('feature'): + pass + else: + die('unhandled export command: %s' % line) + + repo = parser.repo + + for ref, revid in parsed_refs.iteritems(): + if ref == 'refs/heads/master': + repo.generate_revision_history(revid, marks.get_tip('master')) + revno, revid = repo.last_revision_info() + if peer: + if hasattr(peer, "import_last_revision_info_and_tags"): + peer.import_last_revision_info_and_tags(repo, revno, revid) + else: + peer.import_last_revision_info(repo.repository, revno, revid) + wt = peer.bzrdir.open_workingtree() + else: + wt = repo.bzrdir.open_workingtree() + wt.update() + print "ok %s" % ref + print + +def do_capabilities(parser): + global dirname + + print "import" + print "export" + print "refspec refs/heads/*:%s/heads/*" % prefix + + path = os.path.join(dirname, 'marks-git') + + if os.path.exists(path): + print "*import-marks %s" % path + print "*export-marks %s" % path + + print + +def do_list(parser): + global tags + print "? refs/heads/%s" % 'master' + for tag, revid in parser.repo.tags.get_tag_dict().items(): + print "? refs/tags/%s" % tag + tags[tag] = revid + print "@refs/heads/%s HEAD" % 'master' + print + +def get_repo(url, alias): + global dirname, peer + + origin = bzrlib.bzrdir.BzrDir.open(url) + branch = origin.open_branch() + + if not isinstance(origin.transport, bzrlib.transport.local.LocalTransport): + clone_path = os.path.join(dirname, 'clone') + remote_branch = branch + if os.path.exists(clone_path): + # pull + d = bzrlib.bzrdir.BzrDir.open(clone_path) + branch = d.open_branch() + result = branch.pull(remote_branch, [], None, False) + else: + # clone + d = origin.sprout(clone_path, None, + hardlink=True, create_tree_if_local=False, + source_branch=remote_branch) + branch = d.open_branch() + branch.bind(remote_branch) + + peer = remote_branch + else: + peer = None + + return branch + +def main(args): + global marks, prefix, dirname + global tags, filenodes + global blob_marks + global parsed_refs + global files_cache + + alias = args[1] + url = args[2] + + prefix = 'refs/bzr/%s' % alias + tags = {} + filenodes = {} + blob_marks = {} + parsed_refs = {} + files_cache = {} + + gitdir = os.environ['GIT_DIR'] + dirname = os.path.join(gitdir, 'bzr', alias) + + if not os.path.exists(dirname): + os.makedirs(dirname) + + repo = get_repo(url, alias) + + marks_path = os.path.join(dirname, 'marks-int') + marks = Marks(marks_path) + + parser = Parser(repo) + for line in parser: + if parser.check('capabilities'): + do_capabilities(parser) + elif parser.check('list'): + do_list(parser) + elif parser.check('import'): + do_import(parser) + elif parser.check('export'): + do_export(parser) + else: + die('unhandled command: %s' % line) + sys.stdout.flush() + + marks.store() + +sys.exit(main(sys.argv)) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg new file mode 100755 index 0000000000..45f6c80d45 --- /dev/null +++ b/contrib/remote-helpers/git-remote-hg @@ -0,0 +1,806 @@ +#!/usr/bin/env python +# +# Copyright (c) 2012 Felipe Contreras +# + +# Inspired by Rocco Rutte's hg-fast-export + +# Just copy to your ~/bin, or anywhere in your $PATH. +# Then you can clone with: +# git clone hg::/path/to/mercurial/repo/ + +from mercurial import hg, ui, bookmarks, context, util, encoding + +import re +import sys +import os +import json +import shutil +import subprocess +import urllib + +# +# If you want to switch to hg-git compatibility mode: +# git config --global remote-hg.hg-git-compat true +# +# git: +# Sensible defaults for git. +# hg bookmarks are exported as git branches, hg branches are prefixed +# with 'branches/', HEAD is a special case. +# +# hg: +# Emulate hg-git. +# Only hg bookmarks are exported as git branches. +# Commits are modified to preserve hg information and allow bidirectionality. +# + +NAME_RE = re.compile('^([^<>]+)') +AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$') +AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$') +RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)') + +def die(msg, *args): + sys.stderr.write('ERROR: %s\n' % (msg % args)) + sys.exit(1) + +def warn(msg, *args): + sys.stderr.write('WARNING: %s\n' % (msg % args)) + +def gitmode(flags): + return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644' + +def gittz(tz): + return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60) + +def hgmode(mode): + m = { '100755': 'x', '120000': 'l' } + return m.get(mode, '') + +def get_config(config): + cmd = ['git', 'config', '--get', config] + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + output, _ = process.communicate() + return output + +class Marks: + + def __init__(self, path): + self.path = path + self.tips = {} + self.marks = {} + self.rev_marks = {} + self.last_mark = 0 + + self.load() + + def load(self): + if not os.path.exists(self.path): + return + + tmp = json.load(open(self.path)) + + self.tips = tmp['tips'] + self.marks = tmp['marks'] + self.last_mark = tmp['last-mark'] + + for rev, mark in self.marks.iteritems(): + self.rev_marks[mark] = int(rev) + + def dict(self): + return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark } + + def store(self): + json.dump(self.dict(), open(self.path, 'w')) + + def __str__(self): + return str(self.dict()) + + def from_rev(self, rev): + return self.marks[str(rev)] + + def to_rev(self, mark): + return self.rev_marks[mark] + + def get_mark(self, rev): + self.last_mark += 1 + self.marks[str(rev)] = self.last_mark + return self.last_mark + + def new_mark(self, rev, mark): + self.marks[str(rev)] = mark + self.rev_marks[mark] = rev + self.last_mark = mark + + def is_marked(self, rev): + return self.marks.has_key(str(rev)) + + def get_tip(self, branch): + return self.tips.get(branch, 0) + + def set_tip(self, branch, tip): + self.tips[branch] = tip + +class Parser: + + def __init__(self, repo): + self.repo = repo + self.line = self.get_line() + + def get_line(self): + return sys.stdin.readline().strip() + + def __getitem__(self, i): + return self.line.split()[i] + + def check(self, word): + return self.line.startswith(word) + + def each_block(self, separator): + while self.line != separator: + yield self.line + self.line = self.get_line() + + def __iter__(self): + return self.each_block('') + + def next(self): + self.line = self.get_line() + if self.line == 'done': + self.line = None + + def get_mark(self): + i = self.line.index(':') + 1 + return int(self.line[i:]) + + def get_data(self): + if not self.check('data'): + return None + i = self.line.index(' ') + 1 + size = int(self.line[i:]) + return sys.stdin.read(size) + + def get_author(self): + global bad_mail + + ex = None + m = RAW_AUTHOR_RE.match(self.line) + if not m: + return None + _, name, email, date, tz = m.groups() + if name and 'ext:' in name: + m = re.match('^(.+?) ext:\((.+)\)$', name) + if m: + name = m.group(1) + ex = urllib.unquote(m.group(2)) + + if email != bad_mail: + if name: + user = '%s <%s>' % (name, email) + else: + user = '<%s>' % (email) + else: + user = name + + if ex: + user += ex + + tz = int(tz) + tz = ((tz / 100) * 3600) + ((tz % 100) * 60) + return (user, int(date), -tz) + +def export_file(fc): + d = fc.data() + print "M %s inline %s" % (gitmode(fc.flags()), fc.path()) + print "data %d" % len(d) + print d + +def get_filechanges(repo, ctx, parent): + modified = set() + added = set() + removed = set() + + cur = ctx.manifest() + prev = repo[parent].manifest().copy() + + for fn in cur: + if fn in prev: + if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]): + modified.add(fn) + del prev[fn] + else: + added.add(fn) + removed |= set(prev.keys()) + + return added | modified, removed + +def fixup_user_git(user): + name = mail = None + user = user.replace('"', '') + m = AUTHOR_RE.match(user) + if m: + name = m.group(1) + mail = m.group(2).strip() + else: + m = NAME_RE.match(user) + if m: + name = m.group(1).strip() + return (name, mail) + +def fixup_user_hg(user): + def sanitize(name): + # stole this from hg-git + return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> ')) + + m = AUTHOR_HG_RE.match(user) + if m: + name = sanitize(m.group(1)) + mail = sanitize(m.group(2)) + ex = m.group(3) + if ex: + name += ' ext:(' + urllib.quote(ex) + ')' + else: + name = sanitize(user) + if '@' in user: + mail = name + else: + mail = None + + return (name, mail) + +def fixup_user(user): + global mode, bad_mail + + if mode == 'git': + name, mail = fixup_user_git(user) + else: + name, mail = fixup_user_hg(user) + + if not name: + name = bad_name + if not mail: + mail = bad_mail + + return '%s <%s>' % (name, mail) + +def get_repo(url, alias): + global dirname, peer + + myui = ui.ui() + myui.setconfig('ui', 'interactive', 'off') + + if hg.islocal(url): + repo = hg.repository(myui, url) + else: + local_path = os.path.join(dirname, 'clone') + if not os.path.exists(local_path): + peer, dstpeer = hg.clone(myui, {}, url, local_path, update=False, pull=True) + repo = dstpeer.local() + else: + repo = hg.repository(myui, local_path) + peer = hg.peer(myui, {}, url) + repo.pull(peer, heads=None, force=True) + + return repo + +def rev_to_mark(rev): + global marks + return marks.from_rev(rev) + +def mark_to_rev(mark): + global marks + return marks.to_rev(mark) + +def export_ref(repo, name, kind, head): + global prefix, marks, mode + + ename = '%s/%s' % (kind, name) + tip = marks.get_tip(ename) + + # mercurial takes too much time checking this + if tip and tip == head.rev(): + # nothing to do + return + revs = xrange(tip, head.rev() + 1) + count = 0 + + revs = [rev for rev in revs if not marks.is_marked(rev)] + + for rev in revs: + + c = repo[rev] + (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node()) + rev_branch = extra['branch'] + + author = "%s %d %s" % (fixup_user(user), time, gittz(tz)) + if 'committer' in extra: + user, time, tz = extra['committer'].rsplit(' ', 2) + committer = "%s %s %s" % (user, time, gittz(int(tz))) + else: + committer = author + + parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0] + + if len(parents) == 0: + modified = c.manifest().keys() + removed = [] + else: + modified, removed = get_filechanges(repo, c, parents[0]) + + desc += '\n' + + if mode == 'hg': + extra_msg = '' + + if rev_branch != 'default': + extra_msg += 'branch : %s\n' % rev_branch + + renames = [] + for f in c.files(): + if f not in c.manifest(): + continue + rename = c.filectx(f).renamed() + if rename: + renames.append((rename[0], f)) + + for e in renames: + extra_msg += "rename : %s => %s\n" % e + + for key, value in extra.iteritems(): + if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'): + continue + else: + extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value)) + + if extra_msg: + desc += '\n--HG--\n' + extra_msg + + if len(parents) == 0 and rev: + print 'reset %s/%s' % (prefix, ename) + + print "commit %s/%s" % (prefix, ename) + print "mark :%d" % (marks.get_mark(rev)) + print "author %s" % (author) + print "committer %s" % (committer) + print "data %d" % (len(desc)) + print desc + + if len(parents) > 0: + print "from :%s" % (rev_to_mark(parents[0])) + if len(parents) > 1: + print "merge :%s" % (rev_to_mark(parents[1])) + + for f in modified: + export_file(c.filectx(f)) + for f in removed: + print "D %s" % (f) + print + + count += 1 + if (count % 100 == 0): + print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs)) + print "#############################################################" + + # make sure the ref is updated + print "reset %s/%s" % (prefix, ename) + print "from :%u" % rev_to_mark(rev) + print + + marks.set_tip(ename, rev) + +def export_tag(repo, tag): + export_ref(repo, tag, 'tags', repo[tag]) + +def export_bookmark(repo, bmark): + head = bmarks[bmark] + export_ref(repo, bmark, 'bookmarks', head) + +def export_branch(repo, branch): + tip = get_branch_tip(repo, branch) + head = repo[tip] + export_ref(repo, branch, 'branches', head) + +def export_head(repo): + global g_head + export_ref(repo, g_head[0], 'bookmarks', g_head[1]) + +def do_capabilities(parser): + global prefix, dirname + + print "import" + print "export" + print "refspec refs/heads/branches/*:%s/branches/*" % prefix + print "refspec refs/heads/*:%s/bookmarks/*" % prefix + print "refspec refs/tags/*:%s/tags/*" % prefix + + path = os.path.join(dirname, 'marks-git') + + if os.path.exists(path): + print "*import-marks %s" % path + print "*export-marks %s" % path + + print + +def get_branch_tip(repo, branch): + global branches + + heads = branches.get(branch, None) + if not heads: + return None + + # verify there's only one head + if (len(heads) > 1): + warn("Branch '%s' has more than one head, consider merging" % branch) + # older versions of mercurial don't have this + if hasattr(repo, "branchtip"): + return repo.branchtip(branch) + + return heads[0] + +def list_head(repo, cur): + global g_head, bmarks + + head = bookmarks.readcurrent(repo) + if head: + node = repo[head] + else: + # fake bookmark from current branch + head = cur + node = repo['.'] + if not node: + node = repo['tip'] + if not node: + return + if head == 'default': + head = 'master' + bmarks[head] = node + + print "@refs/heads/%s HEAD" % head + g_head = (head, node) + +def do_list(parser): + global branches, bmarks, mode, track_branches + + repo = parser.repo + for bmark, node in bookmarks.listbookmarks(repo).iteritems(): + bmarks[bmark] = repo[node] + + cur = repo.dirstate.branch() + + list_head(repo, cur) + + if track_branches: + for branch in repo.branchmap(): + heads = repo.branchheads(branch) + if len(heads): + branches[branch] = heads + + for branch in branches: + print "? refs/heads/branches/%s" % branch + + for bmark in bmarks: + print "? refs/heads/%s" % bmark + + for tag, node in repo.tagslist(): + if tag == 'tip': + continue + print "? refs/tags/%s" % tag + + print + +def do_import(parser): + repo = parser.repo + + path = os.path.join(dirname, 'marks-git') + + print "feature done" + if os.path.exists(path): + print "feature import-marks=%s" % path + print "feature export-marks=%s" % path + sys.stdout.flush() + + tmp = encoding.encoding + encoding.encoding = 'utf-8' + + # lets get all the import lines + while parser.check('import'): + ref = parser[1] + + if (ref == 'HEAD'): + export_head(repo) + elif ref.startswith('refs/heads/branches/'): + branch = ref[len('refs/heads/branches/'):] + export_branch(repo, branch) + elif ref.startswith('refs/heads/'): + bmark = ref[len('refs/heads/'):] + export_bookmark(repo, bmark) + elif ref.startswith('refs/tags/'): + tag = ref[len('refs/tags/'):] + export_tag(repo, tag) + + parser.next() + + encoding.encoding = tmp + + print 'done' + +def parse_blob(parser): + global blob_marks + + parser.next() + mark = parser.get_mark() + parser.next() + data = parser.get_data() + blob_marks[mark] = data + parser.next() + return + +def get_merge_files(repo, p1, p2, files): + for e in repo[p1].files(): + if e not in files: + if e not in repo[p1].manifest(): + continue + f = { 'ctx' : repo[p1][e] } + files[e] = f + +def parse_commit(parser): + global marks, blob_marks, bmarks, parsed_refs + global mode + + from_mark = merge_mark = None + + ref = parser[1] + parser.next() + + commit_mark = parser.get_mark() + parser.next() + author = parser.get_author() + parser.next() + committer = parser.get_author() + parser.next() + data = parser.get_data() + parser.next() + if parser.check('from'): + from_mark = parser.get_mark() + parser.next() + if parser.check('merge'): + merge_mark = parser.get_mark() + parser.next() + if parser.check('merge'): + die('octopus merges are not supported yet') + + files = {} + + for line in parser: + if parser.check('M'): + t, m, mark_ref, path = line.split(' ', 3) + mark = int(mark_ref[1:]) + f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] } + elif parser.check('D'): + t, path = line.split(' ') + f = { 'deleted' : True } + else: + die('Unknown file command: %s' % line) + files[path] = f + + def getfilectx(repo, memctx, f): + of = files[f] + if 'deleted' in of: + raise IOError + if 'ctx' in of: + return of['ctx'] + is_exec = of['mode'] == 'x' + is_link = of['mode'] == 'l' + rename = of.get('rename', None) + return context.memfilectx(f, of['data'], + is_link, is_exec, rename) + + repo = parser.repo + + user, date, tz = author + extra = {} + + if committer != author: + extra['committer'] = "%s %u %u" % committer + + if from_mark: + p1 = repo.changelog.node(mark_to_rev(from_mark)) + else: + p1 = '\0' * 20 + + if merge_mark: + p2 = repo.changelog.node(mark_to_rev(merge_mark)) + else: + p2 = '\0' * 20 + + # + # If files changed from any of the parents, hg wants to know, but in git if + # nothing changed from the first parent, nothing changed. + # + if merge_mark: + get_merge_files(repo, p1, p2, files) + + if mode == 'hg': + i = data.find('\n--HG--\n') + if i >= 0: + tmp = data[i + len('\n--HG--\n'):].strip() + for k, v in [e.split(' : ') for e in tmp.split('\n')]: + if k == 'rename': + old, new = v.split(' => ', 1) + files[new]['rename'] = old + elif k == 'branch': + extra[k] = v + elif k == 'extra': + ek, ev = v.split(' : ', 1) + extra[ek] = urllib.unquote(ev) + data = data[:i] + + ctx = context.memctx(repo, (p1, p2), data, + files.keys(), getfilectx, + user, (date, tz), extra) + + tmp = encoding.encoding + encoding.encoding = 'utf-8' + + node = repo.commitctx(ctx) + + encoding.encoding = tmp + + rev = repo[node].rev() + + parsed_refs[ref] = node + + marks.new_mark(rev, commit_mark) + +def parse_reset(parser): + ref = parser[1] + parser.next() + # ugh + if parser.check('commit'): + parse_commit(parser) + return + if not parser.check('from'): + return + from_mark = parser.get_mark() + parser.next() + + node = parser.repo.changelog.node(mark_to_rev(from_mark)) + parsed_refs[ref] = node + +def parse_tag(parser): + name = parser[1] + parser.next() + from_mark = parser.get_mark() + parser.next() + tagger = parser.get_author() + parser.next() + data = parser.get_data() + parser.next() + + # nothing to do + +def do_export(parser): + global parsed_refs, bmarks, peer + + parser.next() + + for line in parser.each_block('done'): + if parser.check('blob'): + parse_blob(parser) + elif parser.check('commit'): + parse_commit(parser) + elif parser.check('reset'): + parse_reset(parser) + elif parser.check('tag'): + parse_tag(parser) + elif parser.check('feature'): + pass + else: + die('unhandled export command: %s' % line) + + for ref, node in parsed_refs.iteritems(): + if ref.startswith('refs/heads/branches'): + pass + elif ref.startswith('refs/heads/'): + bmark = ref[len('refs/heads/'):] + if bmark in bmarks: + old = bmarks[bmark].hex() + else: + old = '' + if not bookmarks.pushbookmark(parser.repo, bmark, old, node): + continue + elif ref.startswith('refs/tags/'): + tag = ref[len('refs/tags/'):] + parser.repo.tag([tag], node, None, True, None, {}) + else: + # transport-helper/fast-export bugs + continue + print "ok %s" % ref + + print + + if peer: + parser.repo.push(peer, force=False) + +def fix_path(alias, repo, orig_url): + repo_url = util.url(repo.url()) + url = util.url(orig_url) + if str(url) == str(repo_url): + return + cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % repo_url] + subprocess.call(cmd) + +def main(args): + global prefix, dirname, branches, bmarks + global marks, blob_marks, parsed_refs + global peer, mode, bad_mail, bad_name + global track_branches + + alias = args[1] + url = args[2] + peer = None + + hg_git_compat = False + track_branches = True + try: + if get_config('remote-hg.hg-git-compat') == 'true\n': + hg_git_compat = True + track_branches = False + if get_config('remote-hg.track-branches') == 'false\n': + track_branches = False + except subprocess.CalledProcessError: + pass + + if hg_git_compat: + mode = 'hg' + bad_mail = 'none@none' + bad_name = '' + else: + mode = 'git' + bad_mail = 'unknown' + bad_name = 'Unknown' + + if alias[4:] == url: + is_tmp = True + alias = util.sha1(alias).hexdigest() + else: + is_tmp = False + + gitdir = os.environ['GIT_DIR'] + dirname = os.path.join(gitdir, 'hg', alias) + branches = {} + bmarks = {} + blob_marks = {} + parsed_refs = {} + + repo = get_repo(url, alias) + prefix = 'refs/hg/%s' % alias + + if not is_tmp: + fix_path(alias, peer or repo, url) + + if not os.path.exists(dirname): + os.makedirs(dirname) + + marks_path = os.path.join(dirname, 'marks-hg') + marks = Marks(marks_path) + + parser = Parser(repo) + for line in parser: + if parser.check('capabilities'): + do_capabilities(parser) + elif parser.check('list'): + do_list(parser) + elif parser.check('import'): + do_import(parser) + elif parser.check('export'): + do_export(parser) + else: + die('unhandled command: %s' % line) + sys.stdout.flush() + + if not is_tmp: + marks.store() + else: + shutil.rmtree(dirname) + +sys.exit(main(sys.argv)) diff --git a/contrib/remote-helpers/test-bzr.sh b/contrib/remote-helpers/test-bzr.sh new file mode 100755 index 0000000000..70aa8a010a --- /dev/null +++ b/contrib/remote-helpers/test-bzr.sh @@ -0,0 +1,143 @@ +#!/bin/sh +# +# Copyright (c) 2012 Felipe Contreras +# + +test_description='Test remote-bzr' + +. ./test-lib.sh + +if ! test_have_prereq PYTHON; then + skip_all='skipping remote-bzr tests; python not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import bzrlib'; then + skip_all='skipping remote-bzr tests; bzr not available' + test_done +fi + +cmd=' +import bzrlib +bzrlib.initialize() +import bzrlib.plugin +bzrlib.plugin.load_plugins() +import bzrlib.plugins.fastimport +' + +if ! "$PYTHON_PATH" -c "$cmd"; then + echo "consider setting BZR_PLUGIN_PATH=$HOME/.bazaar/plugins" 1>&2 + skip_all='skipping remote-bzr tests; bzr-fastimport not available' + test_done +fi + +check () { + (cd $1 && + git log --format='%s' -1 && + git symbolic-ref HEAD) > actual && + (echo $2 && + echo "refs/heads/$3") > expected && + test_cmp expected actual +} + +bzr whoami "A U Thor <author@example.com>" + +test_expect_success 'cloning' ' + (bzr init bzrrepo && + cd bzrrepo && + echo one > content && + bzr add content && + bzr commit -m one + ) && + + git clone "bzr::$PWD/bzrrepo" gitrepo && + check gitrepo one master +' + +test_expect_success 'pulling' ' + (cd bzrrepo && + echo two > content && + bzr commit -m two + ) && + + (cd gitrepo && git pull) && + + check gitrepo two master +' + +test_expect_success 'pushing' ' + (cd gitrepo && + echo three > content && + git commit -a -m three && + git push + ) && + + echo three > expected && + cat bzrrepo/content > actual && + test_cmp expected actual +' + +test_expect_success 'roundtrip' ' + (cd gitrepo && + git pull && + git log --format="%s" -1 origin/master > actual) && + echo three > expected && + test_cmp expected actual && + + (cd gitrepo && git push && git pull) && + + (cd bzrrepo && + echo four > content && + bzr commit -m four + ) && + + (cd gitrepo && git pull && git push) && + + check gitrepo four master && + + (cd gitrepo && + echo five > content && + git commit -a -m five && + git push && git pull + ) && + + (cd bzrrepo && bzr revert) && + + echo five > expected && + cat bzrrepo/content > actual && + test_cmp expected actual +' + +cat > expected <<EOF +100644 blob 54f9d6da5c91d556e6b54340b1327573073030af content +100755 blob 68769579c3eaadbe555379b9c3538e6628bae1eb executable +120000 blob 6b584e8ece562ebffc15d38808cd6b98fc3d97ea link +EOF + +test_expect_success 'special modes' ' + (cd bzrrepo && + echo exec > executable + chmod +x executable && + bzr add executable + bzr commit -m exec && + ln -s content link + bzr add link + bzr commit -m link && + mkdir dir && + bzr add dir && + bzr commit -m dir) && + + (cd gitrepo && + git pull + git ls-tree HEAD > ../actual) && + + test_cmp expected actual && + + (cd gitrepo && + git cat-file -p HEAD:link > ../actual) && + + echo -n content > expected && + test_cmp expected actual +' + +test_done diff --git a/contrib/remote-helpers/test-hg-bidi.sh b/contrib/remote-helpers/test-hg-bidi.sh new file mode 100755 index 0000000000..2a5d85dd72 --- /dev/null +++ b/contrib/remote-helpers/test-hg-bidi.sh @@ -0,0 +1,244 @@ +#!/bin/sh +# +# Copyright (c) 2012 Felipe Contreras +# +# Base commands from hg-git tests: +# https://bitbucket.org/durin42/hg-git/src +# + +test_description='Test bidirectionality of remote-hg' + +. ./test-lib.sh + +if ! test_have_prereq PYTHON; then + skip_all='skipping remote-hg tests; python not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import mercurial'; then + skip_all='skipping remote-hg tests; mercurial not available' + test_done +fi + +# clone to a git repo +git_clone () { + hg -R $1 bookmark -f -r tip master && + git clone -q "hg::$PWD/$1" $2 +} + +# clone to an hg repo +hg_clone () { + ( + hg init $2 && + cd $1 && + git push -q "hg::$PWD/../$2" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' + ) && + + (cd $2 && hg -q update) +} + +# push an hg repo +hg_push () { + ( + cd $2 + old=$(git symbolic-ref --short HEAD) + git checkout -q -b tmp && + git fetch -q "hg::$PWD/../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' && + git checkout -q $old && + git branch -q -D tmp 2> /dev/null || true + ) +} + +hg_log () { + hg -R $1 log --graph --debug | grep -v 'tag: *default/' +} + +setup () { + ( + echo "[ui]" + echo "username = A U Thor <author@example.com>" + echo "[defaults]" + echo "backout = -d \"0 0\"" + echo "commit = -d \"0 0\"" + echo "debugrawcommit = -d \"0 0\"" + echo "tag = -d \"0 0\"" + ) >> "$HOME"/.hgrc && + git config --global remote-hg.hg-git-compat true + + HGEDITOR=/usr/bin/true + GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0230" + GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" + export HGEDITOR GIT_AUTHOR_DATE GIT_COMMITTER_DATE +} + +setup + +test_expect_success 'encoding' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -m "add älphà" && + + GIT_AUTHOR_NAME="tést èncödîng" && + export GIT_AUTHOR_NAME && + echo beta > beta && + git add beta && + git commit -m "add beta" && + + echo gamma > gamma && + git add gamma && + git commit -m "add gämmâ" && + + : TODO git config i18n.commitencoding latin-1 && + echo delta > delta && + git add delta && + git commit -m "add déltà" + ) && + + hg_clone gitrepo hgrepo && + git_clone hgrepo gitrepo2 && + hg_clone gitrepo2 hgrepo2 && + + HGENCODING=utf-8 hg_log hgrepo > expected && + HGENCODING=utf-8 hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_expect_success 'file removal' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + echo beta > beta && + git add beta && + git commit -m "add beta" + mkdir foo && + echo blah > foo/bar && + git add foo && + git commit -m "add foo" && + git rm alpha && + git commit -m "remove alpha" && + git rm foo/bar && + git commit -m "remove foo/bar" + ) && + + hg_clone gitrepo hgrepo && + git_clone hgrepo gitrepo2 && + hg_clone gitrepo2 hgrepo2 && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_expect_success 'git tags' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + git config receive.denyCurrentBranch ignore && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git tag alpha && + + echo beta > beta && + git add beta && + git commit -m "add beta" && + git tag -a -m "added tag beta" beta + ) && + + hg_clone gitrepo hgrepo && + git_clone hgrepo gitrepo2 && + hg_clone gitrepo2 hgrepo2 && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_expect_success 'hg branch' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -q -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone gitrepo hgrepo && + + cd hgrepo && + hg -q co master && + hg mv alpha beta && + hg -q commit -m "rename alpha to beta" && + hg branch gamma | grep -v "permanent and global" && + hg -q commit -m "started branch gamma" + ) && + + hg_push hgrepo gitrepo && + hg_clone gitrepo hgrepo2 && + + : TODO, avoid "master" bookmark && + (cd hgrepo2 && hg checkout gamma) && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_expect_success 'hg tags' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone gitrepo hgrepo && + + cd hgrepo && + hg co master && + hg tag alpha + ) && + + hg_push hgrepo gitrepo && + hg_clone gitrepo hgrepo2 && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_done diff --git a/contrib/remote-helpers/test-hg-hg-git.sh b/contrib/remote-helpers/test-hg-hg-git.sh new file mode 100755 index 0000000000..9aaf043669 --- /dev/null +++ b/contrib/remote-helpers/test-hg-hg-git.sh @@ -0,0 +1,536 @@ +#!/bin/sh +# +# Copyright (c) 2012 Felipe Contreras +# +# Base commands from hg-git tests: +# https://bitbucket.org/durin42/hg-git/src +# + +test_description='Test remote-hg output compared to hg-git' + +. ./test-lib.sh + +if ! test_have_prereq PYTHON; then + skip_all='skipping remote-hg tests; python not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import mercurial'; then + skip_all='skipping remote-hg tests; mercurial not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import hggit'; then + skip_all='skipping remote-hg tests; hg-git not available' + test_done +fi + +# clone to a git repo with git +git_clone_git () { + hg -R $1 bookmark -f -r tip master && + git clone -q "hg::$PWD/$1" $2 +} + +# clone to an hg repo with git +hg_clone_git () { + ( + hg init $2 && + cd $1 && + git push -q "hg::$PWD/../$2" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' + ) && + + (cd $2 && hg -q update) +} + +# clone to a git repo with hg +git_clone_hg () { + ( + git init -q $2 && + cd $1 && + hg bookmark -f -r tip master && + hg -q push -r master ../$2 || true + ) +} + +# clone to an hg repo with hg +hg_clone_hg () { + hg -q clone $1 $2 +} + +# push an hg repo with git +hg_push_git () { + ( + cd $2 + old=$(git symbolic-ref --short HEAD) + git checkout -q -b tmp && + git fetch -q "hg::$PWD/../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' && + git checkout -q $old && + git branch -q -D tmp 2> /dev/null || true + ) +} + +# push an hg git repo with hg +hg_push_hg () { + ( + cd $1 && + hg -q push ../$2 || true + ) +} + +hg_log () { + hg -R $1 log --graph --debug | grep -v 'tag: *default/' +} + +git_log () { + git --git-dir=$1/.git fast-export --branches +} + +setup () { + ( + echo "[ui]" + echo "username = A U Thor <author@example.com>" + echo "[defaults]" + echo "backout = -d \"0 0\"" + echo "commit = -d \"0 0\"" + echo "debugrawcommit = -d \"0 0\"" + echo "tag = -d \"0 0\"" + echo "[extensions]" + echo "hgext.bookmarks =" + echo "hggit =" + ) >> "$HOME"/.hgrc && + git config --global receive.denycurrentbranch warn + git config --global remote-hg.hg-git-compat true + + HGEDITOR=/usr/bin/true + + GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0230" + GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" + export HGEDITOR GIT_AUTHOR_DATE GIT_COMMITTER_DATE +} + +setup + +test_expect_success 'executable bit' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + echo alpha > alpha && + chmod 0644 alpha && + git add alpha && + git commit -m "add alpha" && + chmod 0755 alpha && + git add alpha && + git commit -m "set executable bit" && + chmod 0644 alpha && + git add alpha && + git commit -m "clear executable bit" + ) && + + for x in hg git; do + ( + hg_clone_$x gitrepo hgrepo-$x && + cd hgrepo-$x && + hg_log . && + hg manifest -r 1 -v && + hg manifest -v + ) > output-$x && + + git_clone_$x hgrepo-$x gitrepo2-$x && + git_log gitrepo2-$x > log-$x + done && + cp -r log-* output-* /tmp/foo/ && + + test_cmp output-hg output-git && + test_cmp log-hg log-git +' + +test_expect_success 'symlink' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + ln -s alpha beta && + git add beta && + git commit -m "add beta" + ) && + + for x in hg git; do + ( + hg_clone_$x gitrepo hgrepo-$x && + cd hgrepo-$x && + hg_log . && + hg manifest -v + ) > output-$x && + + git_clone_$x hgrepo-$x gitrepo2-$x && + git_log gitrepo2-$x > log-$x + done && + + test_cmp output-hg output-git && + test_cmp log-hg log-git +' + +test_expect_success 'merge conflict 1' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + hg init hgrepo1 && + cd hgrepo1 && + echo A > afile && + hg add afile && + hg ci -m "origin" && + + echo B > afile && + hg ci -m "A->B" && + + hg up -r0 && + echo C > afile && + hg ci -m "A->C" && + + hg merge -r1 || true && + echo C > afile && + hg resolve -m afile && + hg ci -m "merge to C" + ) && + + for x in hg git; do + git_clone_$x hgrepo1 gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'merge conflict 2' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + hg init hgrepo1 && + cd hgrepo1 && + echo A > afile && + hg add afile && + hg ci -m "origin" && + + echo B > afile && + hg ci -m "A->B" && + + hg up -r0 && + echo C > afile && + hg ci -m "A->C" && + + hg merge -r1 || true && + echo B > afile && + hg resolve -m afile && + hg ci -m "merge to B" + ) && + + for x in hg git; do + git_clone_$x hgrepo1 gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'converged merge' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + hg init hgrepo1 && + cd hgrepo1 && + echo A > afile && + hg add afile && + hg ci -m "origin" && + + echo B > afile && + hg ci -m "A->B" && + + echo C > afile && + hg ci -m "B->C" && + + hg up -r0 && + echo C > afile && + hg ci -m "A->C" && + + hg merge -r2 || true && + hg ci -m "merge" + ) && + + for x in hg git; do + git_clone_$x hgrepo1 gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'encoding' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -m "add älphà" && + + GIT_AUTHOR_NAME="tést èncödîng" && + export GIT_AUTHOR_NAME && + echo beta > beta && + git add beta && + git commit -m "add beta" && + + echo gamma > gamma && + git add gamma && + git commit -m "add gämmâ" && + + : TODO git config i18n.commitencoding latin-1 && + echo delta > delta && + git add delta && + git commit -m "add déltà" + ) && + + for x in hg git; do + hg_clone_$x gitrepo hgrepo-$x && + git_clone_$x hgrepo-$x gitrepo2-$x && + + HGENCODING=utf-8 hg_log hgrepo-$x > hg-log-$x && + git_log gitrepo2-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'file removal' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + echo beta > beta && + git add beta && + git commit -m "add beta" + mkdir foo && + echo blah > foo/bar && + git add foo && + git commit -m "add foo" && + git rm alpha && + git commit -m "remove alpha" && + git rm foo/bar && + git commit -m "remove foo/bar" + ) && + + for x in hg git; do + ( + hg_clone_$x gitrepo hgrepo-$x && + cd hgrepo-$x && + hg_log . && + hg manifest -r 3 && + hg manifest + ) > output-$x && + + git_clone_$x hgrepo-$x gitrepo2-$x && + git_log gitrepo2-$x > log-$x + done && + + test_cmp output-hg output-git && + test_cmp log-hg log-git +' + +test_expect_success 'git tags' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + git config receive.denyCurrentBranch ignore && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git tag alpha && + + echo beta > beta && + git add beta && + git commit -m "add beta" && + git tag -a -m "added tag beta" beta + ) && + + for x in hg git; do + hg_clone_$x gitrepo hgrepo-$x && + hg_log hgrepo-$x > log-$x + done && + + test_cmp log-hg log-git +' + +test_expect_success 'hg author' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + for x in hg git; do + ( + git init -q gitrepo-$x && + cd gitrepo-$x && + + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone_$x gitrepo-$x hgrepo-$x && + cd hgrepo-$x && + + hg co master && + echo beta > beta && + hg add beta && + hg commit -u "test" -m "add beta" && + + echo gamma >> beta && + hg commit -u "test <test@example.com> (comment)" -m "modify beta" && + + echo gamma > gamma && + hg add gamma && + hg commit -u "<test@example.com>" -m "add gamma" && + + echo delta > delta && + hg add delta && + hg commit -u "name<test@example.com>" -m "add delta" && + + echo epsilon > epsilon && + hg add epsilon && + hg commit -u "name <test@example.com" -m "add epsilon" && + + echo zeta > zeta && + hg add zeta && + hg commit -u " test " -m "add zeta" && + + echo eta > eta && + hg add eta && + hg commit -u "test < test@example.com >" -m "add eta" && + + echo theta > theta && + hg add theta && + hg commit -u "test >test@example.com>" -m "add theta" && + + echo iota > iota && + hg add iota && + hg commit -u "test <test <at> example <dot> com>" -m "add iota" + ) && + + hg_push_$x hgrepo-$x gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp git-log-hg git-log-git && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'hg branch' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + for x in hg git; do + ( + git init -q gitrepo-$x && + cd gitrepo-$x && + + echo alpha > alpha && + git add alpha && + git commit -q -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone_$x gitrepo-$x hgrepo-$x && + + cd hgrepo-$x && + hg -q co master && + hg mv alpha beta && + hg -q commit -m "rename alpha to beta" && + hg branch gamma | grep -v "permanent and global" && + hg -q commit -m "started branch gamma" + ) && + + hg_push_$x hgrepo-$x gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'hg tags' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + for x in hg git; do + ( + git init -q gitrepo-$x && + cd gitrepo-$x && + + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone_$x gitrepo-$x hgrepo-$x && + + cd hgrepo-$x && + hg co master && + hg tag alpha + ) && + + hg_push_$x hgrepo-$x gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + + ( + git --git-dir=gitrepo-$x/.git tag -l && + hg_log hgrepo2-$x && + cat hgrepo2-$x/.hgtags + ) > output-$x + done && + + test_cmp output-hg output-git +' + +test_done diff --git a/contrib/remote-helpers/test-hg.sh b/contrib/remote-helpers/test-hg.sh new file mode 100755 index 0000000000..7bb81f2f8e --- /dev/null +++ b/contrib/remote-helpers/test-hg.sh @@ -0,0 +1,121 @@ +#!/bin/sh +# +# Copyright (c) 2012 Felipe Contreras +# +# Base commands from hg-git tests: +# https://bitbucket.org/durin42/hg-git/src +# + +test_description='Test remote-hg' + +. ./test-lib.sh + +if ! test_have_prereq PYTHON; then + skip_all='skipping remote-hg tests; python not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import mercurial'; then + skip_all='skipping remote-hg tests; mercurial not available' + test_done +fi + +check () { + (cd $1 && + git log --format='%s' -1 && + git symbolic-ref HEAD) > actual && + (echo $2 && + echo "refs/heads/$3") > expected && + test_cmp expected actual +} + +setup () { + ( + echo "[ui]" + echo "username = H G Wells <wells@example.com>" + ) >> "$HOME"/.hgrc +} + +setup + +test_expect_success 'cloning' ' + test_when_finished "rm -rf gitrepo*" && + + ( + hg init hgrepo && + cd hgrepo && + echo zero > content && + hg add content && + hg commit -m zero + ) && + + git clone "hg::$PWD/hgrepo" gitrepo && + check gitrepo zero master +' + +test_expect_success 'cloning with branches' ' + test_when_finished "rm -rf gitrepo*" && + + ( + cd hgrepo && + hg branch next && + echo next > content && + hg commit -m next + ) && + + git clone "hg::$PWD/hgrepo" gitrepo && + check gitrepo next next && + + (cd hgrepo && hg checkout default) && + + git clone "hg::$PWD/hgrepo" gitrepo2 && + check gitrepo2 zero master +' + +test_expect_success 'cloning with bookmarks' ' + test_when_finished "rm -rf gitrepo*" && + + ( + cd hgrepo && + hg bookmark feature-a && + echo feature-a > content && + hg commit -m feature-a + ) && + + git clone "hg::$PWD/hgrepo" gitrepo && + check gitrepo feature-a feature-a +' + +test_expect_success 'cloning with detached head' ' + test_when_finished "rm -rf gitrepo*" && + + ( + cd hgrepo && + hg update -r 0 + ) && + + git clone "hg::$PWD/hgrepo" gitrepo && + check gitrepo zero master +' + +test_expect_success 'update bookmark' ' + test_when_finished "rm -rf gitrepo*" && + + ( + cd hgrepo && + hg bookmark devel + ) && + + ( + git clone "hg::$PWD/hgrepo" gitrepo && + cd gitrepo && + git checkout devel && + echo devel > content && + git commit -a -m devel && + git push + ) && + + hg -R hgrepo bookmarks | egrep "devel[ ]+3:" +' + +test_done diff --git a/contrib/stats/mailmap.pl b/contrib/stats/mailmap.pl index 4b852e2455..9513f5e35b 100755 --- a/contrib/stats/mailmap.pl +++ b/contrib/stats/mailmap.pl @@ -1,38 +1,70 @@ -#!/usr/bin/perl -w -my %mailmap = (); -open I, "<", ".mailmap"; -while (<I>) { - chomp; - next if /^#/; - if (my ($author, $mail) = /^(.*?)\s+<(.+)>$/) { - $mailmap{$mail} = $author; - } +#!/usr/bin/perl + +use warnings 'all'; +use strict; +use Getopt::Long; + +my $match_emails; +my $match_names; +my $order_by = 'count'; +Getopt::Long::Configure(qw(bundling)); +GetOptions( + 'emails|e!' => \$match_emails, + 'names|n!' => \$match_names, + 'count|c' => sub { $order_by = 'count' }, + 'time|t' => sub { $order_by = 'stamp' }, +) or exit 1; +$match_emails = 1 unless $match_names; + +my $email = {}; +my $name = {}; + +open(my $fh, '-|', "git log --format='%at <%aE> %aN'"); +while(<$fh>) { + my ($t, $e, $n) = /(\S+) <(\S+)> (.*)/; + mark($email, $e, $n, $t); + mark($name, $n, $e, $t); } -close I; - -my %mail2author = (); -open I, "git log --pretty='format:%ae %an' |"; -while (<I>) { - chomp; - my ($mail, $author) = split(/\t/, $_); - next if exists $mailmap{$mail}; - $mail2author{$mail} ||= {}; - $mail2author{$mail}{$author} ||= 0; - $mail2author{$mail}{$author}++; +close($fh); + +if ($match_emails) { + foreach my $e (dups($email)) { + foreach my $n (vals($email->{$e})) { + show($n, $e, $email->{$e}->{$n}); + } + print "\n"; + } } -close I; - -while (my ($mail, $authorcount) = each %mail2author) { - # %$authorcount is ($author => $count); - # sort and show the names from the most frequent ones. - my @names = (map { $_->[0] } - sort { $b->[1] <=> $a->[1] } - map { [$_, $authorcount->{$_}] } - keys %$authorcount); - if (1 < @names) { - for (@names) { - print "$_ <$mail>\n"; +if ($match_names) { + foreach my $n (dups($name)) { + foreach my $e (vals($name->{$n})) { + show($n, $e, $name->{$n}->{$e}); } + print "\n"; } } +exit 0; +sub mark { + my ($h, $k, $v, $t) = @_; + my $e = $h->{$k}->{$v} ||= { count => 0, stamp => 0 }; + $e->{count}++; + $e->{stamp} = $t unless $t < $e->{stamp}; +} + +sub dups { + my $h = shift; + return grep { keys($h->{$_}) > 1 } keys($h); +} + +sub vals { + my $h = shift; + return sort { + $h->{$b}->{$order_by} <=> $h->{$a}->{$order_by} + } keys($h); +} + +sub show { + my ($n, $e, $h) = @_; + print "$n <$e> ($h->{$order_by})\n"; +} diff --git a/contrib/subtree/.gitignore b/contrib/subtree/.gitignore index 7e77c9d022..91360a3d7f 100644 --- a/contrib/subtree/.gitignore +++ b/contrib/subtree/.gitignore @@ -1,4 +1,5 @@ *~ +git-subtree git-subtree.xml git-subtree.1 mainline diff --git a/contrib/subtree/Makefile b/contrib/subtree/Makefile index 05cdd5c9b2..b50750565f 100644 --- a/contrib/subtree/Makefile +++ b/contrib/subtree/Makefile @@ -30,12 +30,13 @@ $(GIT_SUBTREE): $(GIT_SUBTREE_SH) doc: $(GIT_SUBTREE_DOC) install: $(GIT_SUBTREE) - $(INSTALL) -m 755 $(GIT_SUBTREE) $(libexecdir) + $(INSTALL) -m 755 $(GIT_SUBTREE) $(DESTDIR)$(libexecdir) install-doc: install-man install-man: $(GIT_SUBTREE_DOC) - $(INSTALL) -m 644 $^ $(man1dir) + $(INSTALL) -d -m 755 $(DESTDIR)$(man1dir) + $(INSTALL) -m 644 $^ $(DESTDIR)$(man1dir) $(GIT_SUBTREE_DOC): $(GIT_SUBTREE_XML) xmlto -m $(MANPAGE_NORMAL_XSL) man $^ diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh index 920c664bb7..8a23f58ba0 100755 --- a/contrib/subtree/git-subtree.sh +++ b/contrib/subtree/git-subtree.sh @@ -9,6 +9,7 @@ if [ $# -eq 0 ]; then fi OPTS_SPEC="\ git subtree add --prefix=<prefix> <commit> +git subtree add --prefix=<prefix> <repository> <commit> git subtree merge --prefix=<prefix> <commit> git subtree pull --prefix=<prefix> <repository> <refspec...> git subtree push --prefix=<prefix> <repository> <refspec...> @@ -296,7 +297,7 @@ copy_commit() # We're going to set some environment vars here, so # do it in a subshell to get rid of them safely later debug copy_commit "{$1}" "{$2}" "{$3}" - git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" | + git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%B' "$1" | ( read GIT_AUTHOR_NAME read GIT_AUTHOR_EMAIL @@ -497,12 +498,23 @@ cmd_add() ensure_clean if [ $# -eq 1 ]; then - "cmd_add_commit" "$@" + git rev-parse -q --verify "$1^{commit}" >/dev/null || + die "'$1' does not refer to a commit" + + "cmd_add_commit" "$@" elif [ $# -eq 2 ]; then - "cmd_add_repository" "$@" + # Technically we could accept a refspec here but we're + # just going to turn around and add FETCH_HEAD under the + # specified directory. Allowing a refspec might be + # misleading because we won't do anything with any other + # branches fetched via the refspec. + git rev-parse -q --verify "$2^{commit}" >/dev/null || + die "'$2' does not refer to a commit" + + "cmd_add_repository" "$@" else say "error: parameters were '$@'" - die "Provide either a refspec or a repository and refspec." + die "Provide either a commit or a repository and commit." fi } diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt index 0c44fda011..7ba853eeda 100644 --- a/contrib/subtree/git-subtree.txt +++ b/contrib/subtree/git-subtree.txt @@ -9,7 +9,8 @@ git-subtree - Merge subtrees together and split repository into subtrees SYNOPSIS -------- [verse] -'git subtree' add -P <prefix> <commit> +'git subtree' add -P <prefix> <refspec> +'git subtree' add -P <prefix> <repository> <refspec> 'git subtree' pull -P <prefix> <repository> <refspec...> 'git subtree' push -P <prefix> <repository> <refspec...> 'git subtree' merge -P <prefix> <commit> @@ -93,7 +94,7 @@ pull:: repository. push:: - Does a 'split' (see above) using the <prefix> supplied + Does a 'split' (see below) using the <prefix> supplied and then does a 'git push' to push the result to the repository and refspec. This can be used to push your subtree to different branches of the remote repository. diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh index bc2eeb0944..b0f8536fca 100755 --- a/contrib/subtree/t/t7900-subtree.sh +++ b/contrib/subtree/t/t7900-subtree.sh @@ -60,7 +60,6 @@ last_commit_message() git log --pretty=format:%s -1 } -# 1 test_expect_success 'init subproj' ' test_create_repo subproj ' @@ -68,7 +67,6 @@ test_expect_success 'init subproj' ' # To the subproject! cd subproj -# 2 test_expect_success 'add sub1' ' create sub1 && git commit -m "sub1" && @@ -76,14 +74,16 @@ test_expect_success 'add sub1' ' git branch -m master subproj ' -# 3 +# Save this hash for testing later. + +subdir_hash=`git rev-parse HEAD` + test_expect_success 'add sub2' ' create sub2 && git commit -m "sub2" && git branch sub2 ' -# 4 test_expect_success 'add sub3' ' create sub3 && git commit -m "sub3" && @@ -93,7 +93,6 @@ test_expect_success 'add sub3' ' # Back to mainline cd .. -# 5 test_expect_success 'add main4' ' create main4 && git commit -m "main4" && @@ -101,101 +100,85 @@ test_expect_success 'add main4' ' git branch subdir ' -# 6 test_expect_success 'fetch subproj history' ' git fetch ./subproj sub1 && git branch sub1 FETCH_HEAD ' -# 7 test_expect_success 'no subtree exists in main tree' ' test_must_fail git subtree merge --prefix=subdir sub1 ' -# 8 test_expect_success 'no pull from non-existant subtree' ' test_must_fail git subtree pull --prefix=subdir ./subproj sub1 ' -# 9 test_expect_success 'check if --message works for add' ' git subtree add --prefix=subdir --message="Added subproject" sub1 && check_equal ''"$(last_commit_message)"'' "Added subproject" && undo ' -# 10 test_expect_success 'check if --message works as -m and --prefix as -P' ' git subtree add -P subdir -m "Added subproject using git subtree" sub1 && check_equal ''"$(last_commit_message)"'' "Added subproject using git subtree" && undo ' -# 11 test_expect_success 'check if --message works with squash too' ' git subtree add -P subdir -m "Added subproject with squash" --squash sub1 && check_equal ''"$(last_commit_message)"'' "Added subproject with squash" && undo ' -# 12 test_expect_success 'add subproj to mainline' ' git subtree add --prefix=subdir/ FETCH_HEAD && check_equal ''"$(last_commit_message)"'' "Add '"'subdir/'"' from commit '"'"'''"$(git rev-parse sub1)"'''"'"'" ' -# 13 # this shouldn't actually do anything, since FETCH_HEAD is already a parent test_expect_success 'merge fetched subproj' ' git merge -m "merge -s -ours" -s ours FETCH_HEAD ' -# 14 test_expect_success 'add main-sub5' ' create subdir/main-sub5 && git commit -m "main-sub5" ' -# 15 test_expect_success 'add main6' ' create main6 && git commit -m "main6 boring" ' -# 16 test_expect_success 'add main-sub7' ' create subdir/main-sub7 && git commit -m "main-sub7" ' -# 17 test_expect_success 'fetch new subproj history' ' git fetch ./subproj sub2 && git branch sub2 FETCH_HEAD ' -# 18 test_expect_success 'check if --message works for merge' ' git subtree merge --prefix=subdir -m "Merged changes from subproject" sub2 && check_equal ''"$(last_commit_message)"'' "Merged changes from subproject" && undo ' -# 19 test_expect_success 'check if --message for merge works with squash too' ' git subtree merge --prefix subdir -m "Merged changes from subproject using squash" --squash sub2 && check_equal ''"$(last_commit_message)"'' "Merged changes from subproject using squash" && undo ' -# 20 test_expect_success 'merge new subproj history into subdir' ' git subtree merge --prefix=subdir FETCH_HEAD && git branch pre-split && check_equal ''"$(last_commit_message)"'' "Merge commit '"'"'"$(git rev-parse sub2)"'"'"' into mainline" ' -# 21 test_expect_success 'Check that prefix argument is required for split' ' echo "You must provide the --prefix option." > expected && test_must_fail git subtree split > actual 2>&1 && @@ -207,7 +190,6 @@ test_expect_success 'Check that prefix argument is required for split' ' rm -f expected actual ' -# 22 test_expect_success 'Check that the <prefix> exists for a split' ' echo "'"'"'non-existent-directory'"'"'" does not exist\; use "'"'"'git subtree add'"'"'" > expected && test_must_fail git subtree split --prefix=non-existent-directory > actual 2>&1 && @@ -219,7 +201,6 @@ test_expect_success 'Check that the <prefix> exists for a split' ' # rm -f expected actual ' -# 23 test_expect_success 'check if --message works for split+rejoin' ' spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && git branch spl1 "$spl1" && @@ -227,15 +208,24 @@ test_expect_success 'check if --message works for split+rejoin' ' undo ' -# 24 test_expect_success 'check split with --branch' ' - spl1=$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin) && - undo && - git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr1 && - check_equal ''"$(git rev-parse splitbr1)"'' "$spl1" + spl1=$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin) && + undo && + git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr1 && + check_equal ''"$(git rev-parse splitbr1)"'' "$spl1" +' + +test_expect_success 'check hash of split' ' + spl1=$(git subtree split --prefix subdir) && + undo && + git subtree split --prefix subdir --branch splitbr1test && + check_equal ''"$(git rev-parse splitbr1test)"'' "$spl1" + git checkout splitbr1test && + new_hash=$(git rev-parse HEAD~2) && + git checkout mainline && + check_equal ''"$new_hash"'' "$subdir_hash" ' -# 25 test_expect_success 'check split with --branch for an existing branch' ' spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && undo && @@ -244,13 +234,10 @@ test_expect_success 'check split with --branch for an existing branch' ' check_equal ''"$(git rev-parse splitbr2)"'' "$spl1" ' -# 26 test_expect_success 'check split with --branch for an incompatible branch' ' test_must_fail git subtree split --prefix subdir --onto FETCH_HEAD --branch subdir ' - -# 27 test_expect_success 'check split+rejoin' ' spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && undo && @@ -258,7 +245,6 @@ test_expect_success 'check split+rejoin' ' check_equal ''"$(last_commit_message)"'' "Split '"'"'subdir/'"'"' into commit '"'"'"$spl1"'"'"'" ' -# 28 test_expect_success 'add main-sub8' ' create subdir/main-sub8 && git commit -m "main-sub8" @@ -267,14 +253,12 @@ test_expect_success 'add main-sub8' ' # To the subproject! cd ./subproj -# 29 test_expect_success 'merge split into subproj' ' git fetch .. spl1 && git branch spl1 FETCH_HEAD && git merge FETCH_HEAD ' -# 30 test_expect_success 'add sub9' ' create sub9 && git commit -m "sub9" @@ -283,19 +267,16 @@ test_expect_success 'add sub9' ' # Back to mainline cd .. -# 31 test_expect_success 'split for sub8' ' split2=''"$(git subtree split --annotate='"'*'"' --prefix subdir/ --rejoin)"'' git branch split2 "$split2" ' -# 32 test_expect_success 'add main-sub10' ' create subdir/main-sub10 && git commit -m "main-sub10" ' -# 33 test_expect_success 'split for sub10' ' spl3=''"$(git subtree split --annotate='"'*'"' --prefix subdir --rejoin)"'' && git branch spl3 "$spl3" @@ -304,7 +285,6 @@ test_expect_success 'split for sub10' ' # To the subproject! cd ./subproj -# 34 test_expect_success 'merge split into subproj' ' git fetch .. spl3 && git branch spl3 FETCH_HEAD && @@ -318,13 +298,11 @@ chkms_sub=$(echo $chkms | multiline | sed 's,^,subdir/,' | fixnl) chks="sub1 sub2 sub3 sub9" chks_sub=$(echo $chks | multiline | sed 's,^,subdir/,' | fixnl) -# 35 test_expect_success 'make sure exactly the right set of files ends up in the subproj' ' subfiles=''"$(git ls-files | fixnl)"'' && check_equal "$subfiles" "$chkms $chks" ' -# 36 test_expect_success 'make sure the subproj history *only* contains commits that affect the subdir' ' allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' && check_equal "$allchanges" "$chkms $chks" @@ -333,20 +311,17 @@ test_expect_success 'make sure the subproj history *only* contains commits that # Back to mainline cd .. -# 37 test_expect_success 'pull from subproj' ' git fetch ./subproj subproj-merge-spl3 && git branch subproj-merge-spl3 FETCH_HEAD && git subtree pull --prefix=subdir ./subproj subproj-merge-spl3 ' -# 38 test_expect_success 'make sure exactly the right set of files ends up in the mainline' ' mainfiles=''"$(git ls-files | fixnl)"'' && check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub" ' -# 39 test_expect_success 'make sure each filename changed exactly once in the entire history' ' # main-sub?? and /subdir/main-sub?? both change, because those are the # changes that were split into their own history. And subdir/sub?? never @@ -355,12 +330,10 @@ test_expect_success 'make sure each filename changed exactly once in the entire check_equal "$allchanges" ''"$(echo $chkms $chkm $chks $chkms_sub | multiline | sort | fixnl)"'' ' -# 40 test_expect_success 'make sure the --rejoin commits never make it into subproj' ' check_equal ''"$(git log --pretty=format:'"'%s'"' HEAD^2 | grep -i split)"'' "" ' -# 41 test_expect_success 'make sure no "git subtree" tagged commits make it into subproj' ' # They are meaningless to subproj since one side of the merge refers to the mainline check_equal ''"$(git log --pretty=format:'"'%s%n%b'"' HEAD^2 | grep "git-subtree.*:")"'' "" @@ -370,14 +343,12 @@ test_expect_success 'make sure no "git subtree" tagged commits make it into subp mkdir test2 cd test2 -# 42 test_expect_success 'init main' ' test_create_repo main ' cd main -# 43 test_expect_success 'add main1' ' create main1 && git commit -m "main1" @@ -385,14 +356,12 @@ test_expect_success 'add main1' ' cd .. -# 44 test_expect_success 'init sub' ' test_create_repo sub ' cd sub -# 45 test_expect_success 'add sub2' ' create sub2 && git commit -m "sub2" @@ -402,7 +371,6 @@ cd ../main # check if split can find proper base without --onto -# 46 test_expect_success 'add sub as subdir in main' ' git fetch ../sub master && git branch sub2 FETCH_HEAD && @@ -411,7 +379,6 @@ test_expect_success 'add sub as subdir in main' ' cd ../sub -# 47 test_expect_success 'add sub3' ' create sub3 && git commit -m "sub3" @@ -419,20 +386,17 @@ test_expect_success 'add sub3' ' cd ../main -# 48 test_expect_success 'merge from sub' ' git fetch ../sub master && git branch sub3 FETCH_HEAD && git subtree merge --prefix subdir sub3 ' -# 49 test_expect_success 'add main-sub4' ' create subdir/main-sub4 && git commit -m "main-sub4" ' -# 50 test_expect_success 'split for main-sub4 without --onto' ' git subtree split --prefix subdir --branch mainsub4 ' @@ -442,23 +406,20 @@ test_expect_success 'split for main-sub4 without --onto' ' # have been sub3, but it was not, because its cache was not set to # itself) -# 51 test_expect_success 'check that the commit parent is sub3' ' check_equal ''"$(git log --pretty=format:%P -1 mainsub4)"'' ''"$(git rev-parse sub3)"'' ' -# 52 test_expect_success 'add main-sub5' ' mkdir subdir2 && create subdir2/main-sub5 && git commit -m "main-sub5" ' -# 53 test_expect_success 'split for main-sub5 without --onto' ' # also test that we still can split out an entirely new subtree # if the parent of the first commit in the tree is not empty, - # then the new subtree has accidently been attached to something + # then the new subtree has accidentally been attached to something git subtree split --prefix subdir2 --branch mainsub5 && check_equal ''"$(git log --pretty=format:%P -1 mainsub5)"'' "" ' @@ -487,7 +448,6 @@ joincommits() echo "$commit $all" } -# 54 test_expect_success 'verify one file change per commit' ' x= && list=''"$(git log --pretty=format:'"'commit: %H'"' | joincommits)"'' && diff --git a/contrib/svn-fe/svn-fe.c b/contrib/svn-fe/svn-fe.c index 35db24f5ea..f363505abb 100644 --- a/contrib/svn-fe/svn-fe.c +++ b/contrib/svn-fe/svn-fe.c @@ -10,7 +10,8 @@ int main(int argc, char **argv) { if (svndump_init(NULL)) return 1; - svndump_read((argc > 1) ? argv[1] : NULL); + svndump_read((argc > 1) ? argv[1] : NULL, "refs/heads/master", + "refs/notes/svn/revs"); svndump_deinit(); svndump_reset(); return 0; diff --git a/contrib/svn-fe/svnrdump_sim.py b/contrib/svn-fe/svnrdump_sim.py new file mode 100755 index 0000000000..4e78a1c3cd --- /dev/null +++ b/contrib/svn-fe/svnrdump_sim.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +""" +Simulates svnrdump by replaying an existing dump from a file, taking care +of the specified revision range. +To simulate incremental imports the environment variable SVNRMAX can be set +to the highest revision that should be available. +""" +import sys, os + +if sys.hexversion < 0x02040000: + # The limiter is the ValueError() calls. This may be too conservative + sys.stderr.write("svnrdump-sim.py: requires Python 2.4 or later.\n") + sys.exit(1) + +def getrevlimit(): + var = 'SVNRMAX' + if var in os.environ: + return os.environ[var] + return None + +def writedump(url, lower, upper): + if url.startswith('sim://'): + filename = url[6:] + if filename[-1] == '/': filename = filename[:-1] #remove terminating slash + else: + raise ValueError('sim:// url required') + f = open(filename, 'r'); + state = 'header' + wroterev = False + while(True): + l = f.readline() + if l == '': break + if state == 'header' and l.startswith('Revision-number: '): + state = 'prefix' + if state == 'prefix' and l == 'Revision-number: %s\n' % lower: + state = 'selection' + if not upper == 'HEAD' and state == 'selection' and l == 'Revision-number: %s\n' % upper: + break; + + if state == 'header' or state == 'selection': + if state == 'selection': wroterev = True + sys.stdout.write(l) + return wroterev + +if __name__ == "__main__": + if not (len(sys.argv) in (3, 4, 5)): + print("usage: %s dump URL -rLOWER:UPPER") + sys.exit(1) + if not sys.argv[1] == 'dump': raise NotImplementedError('only "dump" is suppported.') + url = sys.argv[2] + r = ('0', 'HEAD') + if len(sys.argv) == 4 and sys.argv[3][0:2] == '-r': + r = sys.argv[3][2:].lstrip().split(':') + if not getrevlimit() is None: r[1] = getrevlimit() + if writedump(url, r[0], r[1]): ret = 0 + else: ret = 1 + sys.exit(ret) diff --git a/contrib/vim/README b/contrib/vim/README index fca1e17251..8f16d06972 100644 --- a/contrib/vim/README +++ b/contrib/vim/README @@ -17,16 +17,6 @@ To install: 1. Copy these files to vim's syntax directory $HOME/.vim/syntax 2. To auto-detect the editing of various git-related filetypes: - $ cat >>$HOME/.vim/filetype.vim <<'EOF' - autocmd BufNewFile,BufRead *.git/COMMIT_EDITMSG setf gitcommit - autocmd BufNewFile,BufRead *.git/config,.gitconfig setf gitconfig - autocmd BufNewFile,BufRead git-rebase-todo setf gitrebase - autocmd BufNewFile,BufRead .msg.[0-9]* - \ if getline(1) =~ '^From.*# This line is ignored.$' | - \ setf gitsendemail | - \ endif - autocmd BufNewFile,BufRead *.git/** - \ if getline(1) =~ '^\x\{40\}\>\|^ref: ' | - \ setf git | - \ endif - EOF + + $ curl http://ftp.vim.org/pub/vim/runtime/filetype.vim | + sed -ne '/^" Git$/, /^$/ p' >>$HOME/.vim/filetype.vim |