summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rwxr-xr-xcontrib/ciabot/ciabot.py8
-rw-r--r--[-rwxr-xr-x]contrib/completion/git-completion.bash813
-rw-r--r--contrib/completion/git-completion.tcsh128
-rw-r--r--contrib/completion/git-completion.zsh205
-rw-r--r--contrib/completion/git-prompt.sh412
-rw-r--r--contrib/credential/gnome-keyring/.gitignore1
-rw-r--r--contrib/credential/gnome-keyring/Makefile24
-rw-r--r--contrib/credential/gnome-keyring/git-credential-gnome-keyring.c445
-rw-r--r--contrib/credential/netrc/Makefile5
-rwxr-xr-xcontrib/credential/netrc/git-credential-netrc421
-rw-r--r--contrib/credential/netrc/test.netrc13
-rwxr-xr-xcontrib/credential/netrc/test.pl106
-rw-r--r--contrib/credential/osxkeychain/git-credential-osxkeychain.c2
-rw-r--r--contrib/credential/wincred/Makefile14
-rw-r--r--contrib/credential/wincred/git-credential-wincred.c301
-rw-r--r--contrib/emacs/git.el2
-rw-r--r--contrib/examples/builtin-fetch--tool.c2
-rwxr-xr-xcontrib/examples/git-remote.perl10
-rwxr-xr-xcontrib/examples/git-svnimport.perl2
-rwxr-xr-xcontrib/fast-import/git-import.perl2
-rwxr-xr-xcontrib/fast-import/git-import.sh2
-rwxr-xr-xcontrib/fast-import/import-zips.py99
-rwxr-xr-xcontrib/git-jump/git-jump4
-rwxr-xr-xcontrib/hg-to-git/hg-to-git.py5
-rwxr-xr-xcontrib/hooks/post-receive-email3
-rw-r--r--contrib/hooks/setgitperms.perl2
-rw-r--r--contrib/mw-to-git/.gitignore1
-rw-r--r--contrib/mw-to-git/Makefile17
-rwxr-xr-xcontrib/mw-to-git/git-remote-mediawiki.perl (renamed from contrib/mw-to-git/git-remote-mediawiki)795
-rw-r--r--contrib/mw-to-git/git-remote-mediawiki.txt2
-rw-r--r--contrib/mw-to-git/t/.gitignore4
-rw-r--r--contrib/mw-to-git/t/Makefile31
-rw-r--r--contrib/mw-to-git/t/README124
-rwxr-xr-xcontrib/mw-to-git/t/install-wiki.sh45
-rw-r--r--contrib/mw-to-git/t/install-wiki/.gitignore1
-rw-r--r--contrib/mw-to-git/t/install-wiki/LocalSettings.php129
-rw-r--r--contrib/mw-to-git/t/install-wiki/db_install.php120
-rw-r--r--contrib/mw-to-git/t/push-pull-tests.sh144
-rwxr-xr-xcontrib/mw-to-git/t/t9360-mw-to-git-clone.sh257
-rwxr-xr-xcontrib/mw-to-git/t/t9361-mw-to-git-push-pull.sh24
-rwxr-xr-xcontrib/mw-to-git/t/t9362-mw-to-git-utf8.sh347
-rwxr-xr-xcontrib/mw-to-git/t/t9363-mw-to-git-export-import.sh198
-rwxr-xr-xcontrib/mw-to-git/t/t9364-pull-by-rev.sh17
-rwxr-xr-xcontrib/mw-to-git/t/test-gitmw-lib.sh435
-rwxr-xr-xcontrib/mw-to-git/t/test-gitmw.pl225
-rw-r--r--contrib/mw-to-git/t/test.config35
-rw-r--r--contrib/p4import/git-p4import.py5
-rw-r--r--contrib/remote-helpers/Makefile14
-rwxr-xr-xcontrib/remote-helpers/git-remote-bzr959
-rwxr-xr-xcontrib/remote-helpers/git-remote-hg984
-rwxr-xr-xcontrib/remote-helpers/test-bzr.sh341
-rwxr-xr-xcontrib/remote-helpers/test-hg-bidi.sh247
-rwxr-xr-xcontrib/remote-helpers/test-hg-hg-git.sh536
-rwxr-xr-xcontrib/remote-helpers/test-hg.sh157
-rwxr-xr-xcontrib/stats/mailmap.pl96
-rw-r--r--contrib/subtree/.gitignore1
-rw-r--r--contrib/subtree/Makefile5
-rwxr-xr-xcontrib/subtree/git-subtree.sh20
-rw-r--r--contrib/subtree/git-subtree.txt5
-rwxr-xr-xcontrib/subtree/t/t7900-subtree.sh80
-rw-r--r--contrib/svn-fe/svn-fe.c3
-rwxr-xr-xcontrib/svn-fe/svnrdump_sim.py57
-rw-r--r--contrib/vim/README16
63 files changed, 8810 insertions, 698 deletions
diff --git a/contrib/ciabot/ciabot.py b/contrib/ciabot/ciabot.py
index bd24395..36b5665 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 1fb896b..1c35eef 100755..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:
@@ -20,50 +21,8 @@
# 1) Copy this file to somewhere (e.g. ~/.git-completion.sh).
# 2) Add the following line to your .bashrc/.zshrc:
# source ~/.git-completion.sh
-#
-# 3) Consider changing your PS1 to also show the current branch:
-# Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
-# ZSH: PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ '
-#
-# 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 branch.
-#
-# In addition, if you set GIT_PS1_SHOWDIRTYSTATE to a nonempty
-# value, unstaged (*) and staged (+) changes will be shown next
-# to the branch name. You can configure this per-repository
-# with the bash.showDirtyState variable, which defaults to true
-# once GIT_PS1_SHOWDIRTYSTATE is enabled.
-#
-# You can also see if currently something is stashed, by setting
-# GIT_PS1_SHOWSTASHSTATE to a nonempty value. If something is stashed,
-# then a '$' will be shown next to the branch name.
-#
-# 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.
-#
-# If you would like to see the difference between HEAD and its
-# upstream, set GIT_PS1_SHOWUPSTREAM="auto". A "<" indicates
-# you are behind, ">" indicates you are ahead, and "<>"
-# indicates you have diverged. You can further control
-# behaviour by setting GIT_PS1_SHOWUPSTREAM to a space-separated
-# list of values:
-# verbose show number of commits ahead/behind (+/-) upstream
-# legacy don't use the '--count' option available in recent
-# versions of git-rev-list
-# git always compare HEAD to @{upstream}
-# svn always compare HEAD to your SVN upstream
-# By default, __git_ps1 will compare HEAD to your SVN upstream
-# if it can find one, or @{upstream} otherwise. Once you have
-# set GIT_PS1_SHOWUPSTREAM, you can override it on a
-# per-repository basis by setting the bash.showUpstream config
-# variable.
-#
-
-if [[ -n ${ZSH_VERSION-} ]]; then
- autoload -U +X bashcompinit && bashcompinit
-fi
+# 3) Consider changing your PS1 to also show the current branch,
+# see git-prompt.sh for details.
case "$COMP_WORDBREAKS" in
*:*) : great ;;
@@ -74,9 +33,14 @@ esac
# returns location of .git repo
__gitdir ()
{
+ # Note: this function is duplicated in git-prompt.sh
+ # When updating it, make sure you update the other one to match.
if [ -z "${1-}" ]; then
if [ -n "${__git_dir-}" ]; then
echo "$__git_dir"
+ elif [ -n "${GIT_DIR-}" ]; then
+ test -d "${GIT_DIR-}" || return 1
+ echo "$GIT_DIR"
elif [ -d .git ]; then
echo .git
else
@@ -89,234 +53,6 @@ __gitdir ()
fi
}
-# stores the divergence from upstream in $p
-# used by GIT_PS1_SHOWUPSTREAM
-__git_ps1_show_upstream ()
-{
- local key value
- local svn_remote svn_url_pattern count n
- local upstream=git legacy="" verbose=""
-
- svn_remote=()
- # get some config options from git-config
- local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')"
- while read -r key value; do
- case "$key" in
- bash.showupstream)
- GIT_PS1_SHOWUPSTREAM="$value"
- if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then
- p=""
- return
- fi
- ;;
- svn-remote.*.url)
- svn_remote[ $((${#svn_remote[@]} + 1)) ]="$value"
- svn_url_pattern+="\\|$value"
- upstream=svn+git # default upstream is SVN if available, else git
- ;;
- esac
- done <<< "$output"
-
- # parse configuration values
- for option in ${GIT_PS1_SHOWUPSTREAM}; do
- case "$option" in
- git|svn) upstream="$option" ;;
- verbose) verbose=1 ;;
- legacy) legacy=1 ;;
- esac
- done
-
- # Find our upstream
- case "$upstream" in
- git) upstream="@{upstream}" ;;
- svn*)
- # get the upstream from the "git-svn-id: ..." in a commit message
- # (git-svn uses essentially the same procedure internally)
- local svn_upstream=($(git log --first-parent -1 \
- --grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null))
- if [[ 0 -ne ${#svn_upstream[@]} ]]; then
- svn_upstream=${svn_upstream[ ${#svn_upstream[@]} - 2 ]}
- svn_upstream=${svn_upstream%@*}
- local n_stop="${#svn_remote[@]}"
- for ((n=1; n <= n_stop; n++)); do
- svn_upstream=${svn_upstream#${svn_remote[$n]}}
- done
-
- if [[ -z "$svn_upstream" ]]; then
- # default branch name for checkouts with no layout:
- upstream=${GIT_SVN_ID:-git-svn}
- else
- upstream=${svn_upstream#/}
- fi
- elif [[ "svn+git" = "$upstream" ]]; then
- upstream="@{upstream}"
- fi
- ;;
- esac
-
- # Find how many commits we are ahead/behind our upstream
- if [[ -z "$legacy" ]]; then
- count="$(git rev-list --count --left-right \
- "$upstream"...HEAD 2>/dev/null)"
- else
- # produce equivalent output to --count for older versions of git
- local commits
- if commits="$(git rev-list --left-right "$upstream"...HEAD 2>/dev/null)"
- then
- local commit behind=0 ahead=0
- for commit in $commits
- do
- case "$commit" in
- "<"*) ((behind++)) ;;
- *) ((ahead++)) ;;
- esac
- done
- count="$behind $ahead"
- else
- count=""
- fi
- fi
-
- # calculate the result
- if [[ -z "$verbose" ]]; then
- case "$count" in
- "") # no upstream
- p="" ;;
- "0 0") # equal to upstream
- p="=" ;;
- "0 "*) # ahead of upstream
- p=">" ;;
- *" 0") # behind upstream
- p="<" ;;
- *) # diverged from upstream
- p="<>" ;;
- esac
- else
- case "$count" in
- "") # no upstream
- p="" ;;
- "0 0") # equal to upstream
- p=" u=" ;;
- "0 "*) # ahead of upstream
- p=" u+${count#0 }" ;;
- *" 0") # behind upstream
- p=" u-${count% 0}" ;;
- *) # diverged from upstream
- p=" u+${count#* }-${count% *}" ;;
- esac
- fi
-
-}
-
-
-# __git_ps1 accepts 0 or 1 arguments (i.e., format string)
-# returns text to add to bash PS1 prompt (includes branch name)
-__git_ps1 ()
-{
- local g="$(__gitdir)"
- if [ -n "$g" ]; then
- local r=""
- local b=""
- if [ -f "$g/rebase-merge/interactive" ]; then
- r="|REBASE-i"
- b="$(cat "$g/rebase-merge/head-name")"
- elif [ -d "$g/rebase-merge" ]; then
- r="|REBASE-m"
- b="$(cat "$g/rebase-merge/head-name")"
- else
- if [ -d "$g/rebase-apply" ]; then
- if [ -f "$g/rebase-apply/rebasing" ]; then
- r="|REBASE"
- elif [ -f "$g/rebase-apply/applying" ]; then
- r="|AM"
- else
- r="|AM/REBASE"
- fi
- elif [ -f "$g/MERGE_HEAD" ]; then
- r="|MERGING"
- elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
- r="|CHERRY-PICKING"
- elif [ -f "$g/BISECT_LOG" ]; then
- r="|BISECTING"
- fi
-
- b="$(git symbolic-ref HEAD 2>/dev/null)" || {
-
- b="$(
- case "${GIT_PS1_DESCRIBE_STYLE-}" in
- (contains)
- git describe --contains HEAD ;;
- (branch)
- git describe --contains --all HEAD ;;
- (describe)
- git describe HEAD ;;
- (* | default)
- git describe --tags --exact-match HEAD ;;
- esac 2>/dev/null)" ||
-
- b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." ||
- b="unknown"
- b="($b)"
- }
- fi
-
- local w=""
- local i=""
- local s=""
- local u=""
- local c=""
- local p=""
-
- if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
- if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
- c="BARE:"
- else
- 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
- 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
- fi
-
- if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
- __git_ps1_show_upstream
- fi
- fi
-
- local f="$w$i$s$u"
- printf -- "${1:- (%s)}" "$c${b##refs/heads/}${f:+ $f}$r$p"
- fi
-}
-
-__gitcomp_1 ()
-{
- local c IFS=$' \t\n'
- for c in $1; do
- c="$c$2"
- case $c in
- --*=*|*.) ;;
- *) c="$c " ;;
- esac
- printf '%s\n' "$c"
- done
-}
-
# The following function is based on code from:
#
# bash_completion - programmable completion functions for bash 3.2+
@@ -417,7 +153,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_
@@ -445,36 +180,20 @@ _get_comp_words_by_ref ()
shift
done
}
-else
-_get_comp_words_by_ref ()
+fi
+
+__gitcompadd ()
{
- 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
+ local i=0
+ for x in $1; do
+ if [[ "$x" == "$3"* ]]; then
+ COMPREPLY[i++]="$2$x$4"
+ fi
done
}
-fi
-fi
-# Generates completion reply with compgen, appending a space to possible
-# completion words, if necessary.
+# Generates completion reply, appending a space to possible completion words,
+# if necessary.
# It accepts 1 to 4 arguments:
# 1: List of possible completion words.
# 2: A prefix to be added to each possible completion word (optional).
@@ -486,19 +205,25 @@ __gitcomp ()
case "$cur_" in
--*=)
- COMPREPLY=()
;;
*)
- local IFS=$'\n'
- COMPREPLY=($(compgen -P "${2-}" \
- -W "$(__gitcomp_1 "${1-}" "${4-}")" \
- -- "$cur_"))
+ local c i=0 IFS=$' \t\n'
+ for c in $1; do
+ c="$c${4-}"
+ if [[ $c == "$cur_"* ]]; then
+ case $c in
+ --*=*|*.) ;;
+ *) c="$c " ;;
+ esac
+ COMPREPLY[i++]="${2-}$c"
+ fi
+ done
;;
esac
}
-# Generates completion reply with compgen from newline-separated possible
-# completion words by appending a space to all of them.
+# Generates completion reply from newline-separated possible completion words
+# by appending a space to all of them.
# It accepts 1 to 4 arguments:
# 1: List of possible completion words, separated by a single newline.
# 2: A prefix to be added to each possible completion word (optional).
@@ -509,7 +234,125 @@ __gitcomp ()
__gitcomp_nl ()
{
local IFS=$'\n'
- COMPREPLY=($(compgen -P "${2-}" -S "${4- }" -W "$1" -- "${3-$cur}"))
+ __gitcompadd "$1" "${2-}" "${3-$cur}" "${4- }"
+}
+
+# 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 ()
@@ -569,7 +412,7 @@ __git_refs ()
if [[ "$ref" == "$cur"* ]]; then
echo "$ref"
fi
- done | uniq -u
+ done | sort | uniq -u
fi
return
fi
@@ -676,7 +519,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,$, ,
@@ -709,6 +552,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
@@ -734,7 +617,6 @@ __git_complete_remote_or_refspec ()
case "$cmd" in
push) no_complete_refspec=1 ;;
fetch)
- COMPREPLY=()
return
;;
*) ;;
@@ -750,7 +632,6 @@ __git_complete_remote_or_refspec ()
return
fi
if [ $no_complete_refspec = 1 ]; then
- COMPREPLY=()
return
fi
[ "$remote" = "." ] && remote=
@@ -810,10 +691,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;;
@@ -833,7 +723,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;;
@@ -1002,6 +892,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 ()
@@ -1025,7 +952,6 @@ _git_am ()
"
return
esac
- COMPREPLY=()
}
_git_apply ()
@@ -1045,13 +971,10 @@ _git_apply ()
"
return
esac
- COMPREPLY=()
}
_git_add ()
{
- __git_has_doubledash && return
-
case "$cur" in
--*)
__gitcomp "
@@ -1060,7 +983,9 @@ _git_add ()
"
return
esac
- COMPREPLY=()
+
+ # XXX should we check for --update and --all options ?
+ __git_complete_index_file "--others --modified"
}
_git_archive ()
@@ -1105,7 +1030,6 @@ _git_bisect ()
__gitcomp_nl "$(__git_refs)"
;;
*)
- COMPREPLY=()
;;
esac
}
@@ -1124,11 +1048,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
"
;;
*)
@@ -1194,9 +1122,14 @@ _git_cherry ()
_git_cherry_pick ()
{
+ local dir="$(__gitdir)"
+ if [ -f "$dir"/CHERRY_PICK_HEAD ]; then
+ __gitcomp "--continue --quit --abort"
+ return
+ fi
case "$cur" in
--*)
- __gitcomp "--edit --no-commit"
+ __gitcomp "--edit --no-commit --signoff --strategy= --mainline"
;;
*)
__gitcomp_nl "$(__git_refs)"
@@ -1206,15 +1139,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 ()
@@ -1234,16 +1167,22 @@ _git_clone ()
--upload-pack
--template=
--depth
+ --single-branch
+ --branch
"
return
;;
esac
- COMPREPLY=()
}
_git_commit ()
{
- __git_has_doubledash && return
+ case "$prev" in
+ -c|-C)
+ __gitcomp_nl "$(__git_refs)" "" "${cur}"
+ return
+ ;;
+ esac
case "$cur" in
--cleanup=*)
@@ -1263,7 +1202,8 @@ _git_commit ()
--*)
__gitcomp "
--all --author= --signoff --verify --no-verify
- --edit --amend --include --only --interactive
+ --edit --no-edit
+ --amend --include --only --interactive
--dry-run --reuse-message= --reedit-message=
--reset-author --file= --message= --template=
--cleanup= --untracked-files --untracked-files=
@@ -1271,7 +1211,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 ()
@@ -1287,6 +1233,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
@@ -1297,10 +1245,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 ()
@@ -1308,6 +1257,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
@@ -1320,7 +1273,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 ()
@@ -1360,6 +1313,15 @@ _git_fetch ()
__git_complete_remote_or_refspec
}
+__git_format_patch_options="
+ --stdout --attach --no-attach --thread --thread= --no-thread
+ --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=
+ --output-directory --reroll-count --to= --quiet --notes
+"
+
_git_format_patch ()
{
case "$cur" in
@@ -1370,21 +1332,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
@@ -1402,7 +1350,6 @@ _git_fsck ()
return
;;
esac
- COMPREPLY=()
}
_git_gc ()
@@ -1413,7 +1360,6 @@ _git_gc ()
return
;;
esac
- COMPREPLY=()
}
_git_gitk ()
@@ -1490,13 +1436,10 @@ _git_init ()
return
;;
esac
- COMPREPLY=()
}
_git_ls_files ()
{
- __git_has_doubledash && return
-
case "$cur" in
--*)
__gitcomp "--cached --deleted --modified --others --ignored
@@ -1509,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 ()
@@ -1625,7 +1571,6 @@ _git_mergetool ()
return
;;
esac
- COMPREPLY=()
}
_git_merge_base ()
@@ -1641,7 +1586,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 ()
@@ -1798,6 +1750,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
@@ -1807,11 +1765,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 ()
@@ -1825,7 +1784,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
;;
@@ -1852,7 +1811,7 @@ __git_config_get_set_variables ()
_git_config ()
{
case "$prev" in
- branch.*.remote)
+ branch.*.remote|branch.*.pushremote)
__gitcomp_nl "$(__git_remotes)"
return
;;
@@ -1860,11 +1819,19 @@ _git_config ()
__gitcomp_nl "$(__git_refs)"
return
;;
+ branch.*.rebase)
+ __gitcomp "false true"
+ return
+ ;;
+ remote.pushdefault)
+ __gitcomp_nl "$(__git_remotes)"
+ return
+ ;;
remote.*.fetch)
local remote="${prev#remote.}"
remote="${remote%.fetch}"
if [ -z "$cur" ]; then
- COMPREPLY=("refs/heads/")
+ __gitcomp_nl "refs/heads/" "" "" ""
return
fi
__gitcomp_nl "$(__git_refs_remotes "$remote")"
@@ -1899,6 +1866,10 @@ _git_config ()
"
return
;;
+ diff.submodule)
+ __gitcomp "log short"
+ return
+ ;;
help.format)
__gitcomp "man info web html"
return
@@ -1924,14 +1895,13 @@ _git_config ()
return
;;
*.*)
- COMPREPLY=()
return
;;
esac
case "$cur" in
--*)
__gitcomp "
- --global --system --file=
+ --system --global --local --file=
--list --replace-all
--get --get-all --get-regexp
--add --unset --unset-all
@@ -1941,7 +1911,7 @@ _git_config ()
;;
branch.*.*)
local pfx="${cur%.*}." cur_="${cur##*.}"
- __gitcomp "remote merge mergeoptions rebase" "$pfx" "$cur_"
+ __gitcomp "remote pushremote merge mergeoptions rebase" "$pfx" "$cur_"
return
;;
branch.*)
@@ -2094,16 +2064,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
@@ -2234,6 +2206,7 @@ _git_config ()
receive.fsckObjects
receive.unpackLimit
receive.updateserverinfo
+ remote.pushdefault
remotes.
repack.usedeltabaseoffset
rerere.autoupdate
@@ -2281,7 +2254,7 @@ _git_config ()
_git_remote ()
{
- local subcommands="add rename rm set-head set-branches set-url show prune update"
+ local subcommands="add rename remove set-head set-branches set-url show prune update"
local subcommand="$(__git_find_on_cmdline "$subcommands")"
if [ -z "$subcommand" ]; then
__gitcomp "$subcommands"
@@ -2289,7 +2262,7 @@ _git_remote ()
fi
case "$subcommand" in
- rename|rm|set-url|show|prune)
+ rename|remove|set-url|show|prune)
__gitcomp_nl "$(__git_remotes)"
;;
set-head|set-branches)
@@ -2304,7 +2277,6 @@ _git_remote ()
__gitcomp "$c"
;;
*)
- COMPREPLY=()
;;
esac
}
@@ -2340,15 +2312,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 ()
@@ -2378,6 +2349,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
@@ -2417,8 +2392,6 @@ _git_stash ()
*)
if [ -z "$(__git_find_on_cmdline "$save_opts")" ]; then
__gitcomp "$subcommands"
- else
- COMPREPLY=()
fi
;;
esac
@@ -2431,14 +2404,12 @@ _git_stash ()
__gitcomp "--index --quiet"
;;
show,--*|drop,--*|branch,--*)
- COMPREPLY=()
;;
show,*|apply,*|drop,*|pop,*|branch,*)
__gitcomp_nl "$(git --git-dir="$(__gitdir)" stash list \
| sed -n -e 's/:.*//p')"
;;
*)
- COMPREPLY=()
;;
esac
fi
@@ -2448,7 +2419,7 @@ _git_submodule ()
{
__git_has_doubledash && return
- local subcommands="add status init update summary foreach sync"
+ local subcommands="add status init deinit update summary foreach sync"
if [ -z "$(__git_find_on_cmdline "$subcommands")" ]; then
case "$cur" in
--*)
@@ -2480,7 +2451,7 @@ _git_svn ()
--no-metadata --use-svm-props --use-svnsync-props
--log-window-size= --no-checkout --quiet
--repack-flags --use-log-author --localtime
- --ignore-paths= $remote_opts
+ --ignore-paths= --include-paths= $remote_opts
"
local init_opts="
--template= --shared= --trunk= --tags=
@@ -2555,7 +2526,6 @@ _git_svn ()
__gitcomp "--revision= --parent"
;;
*)
- COMPREPLY=()
;;
esac
fi
@@ -2580,13 +2550,10 @@ _git_tag ()
case "$prev" in
-m|-F)
- COMPREPLY=()
;;
-*|tag)
if [ $f = 1 ]; then
__gitcomp_nl "$(__git_tags)"
- else
- COMPREPLY=()
fi
;;
*)
@@ -2673,20 +2640,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
- # workaround zsh's bug that leaves 'words' as a special
- # variable in versions < 4.3.12
- typeset -h words
+ autoload -U +X compinit && compinit
- # workaround zsh's bug that quotes spaces in the COMPREPLY
- # array if IFS doesn't contain spaces.
- typeset -h IFS
+ __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
+ }
+
+ __gitcomp_file ()
+ {
+ emulate -L zsh
+
+ 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 0000000..eaacaf0
--- /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 0000000..2565d2e
--- /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
new file mode 100644
index 0000000..eaf5c36
--- /dev/null
+++ b/contrib/completion/git-prompt.sh
@@ -0,0 +1,412 @@
+# bash/zsh git prompt support
+#
+# Copyright (C) 2006,2007 Shawn O. Pearce <spearce@spearce.org>
+# Distributed under the GNU General Public License, version 2.0.
+#
+# This script allows you to see the current branch in your prompt.
+#
+# To enable:
+#
+# 1) Copy this file to somewhere (e.g. ~/.git-prompt.sh).
+# 2) Add the following line to your .bashrc/.zshrc:
+# source ~/.git-prompt.sh
+# 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
+# branch.
+#
+# In addition, if you set GIT_PS1_SHOWDIRTYSTATE to a nonempty value,
+# unstaged (*) and staged (+) changes will be shown next to the branch
+# name. You can configure this per-repository with the
+# bash.showDirtyState variable, which defaults to true once
+# GIT_PS1_SHOWDIRTYSTATE is enabled.
+#
+# You can also see if currently something is stashed, by setting
+# GIT_PS1_SHOWSTASHSTATE to a nonempty value. If something is stashed,
+# then a '$' will be shown next to the branch name.
+#
+# 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. 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, ">"
+# indicates you are ahead, "<>" indicates you have diverged and "="
+# indicates that there is no difference. You can further control
+# behaviour by setting GIT_PS1_SHOWUPSTREAM to a space-separated list
+# of values:
+#
+# verbose show number of commits ahead/behind (+/-) upstream
+# legacy don't use the '--count' option available in recent
+# versions of git-rev-list
+# git always compare HEAD to @{upstream}
+# svn always compare HEAD to your SVN upstream
+#
+# By default, __git_ps1 will compare HEAD to your SVN upstream if it can
+# find one, or @{upstream} otherwise. Once you have set
+# GIT_PS1_SHOWUPSTREAM, you can override it on a per-repository basis by
+# 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
+__gitdir ()
+{
+ # Note: this function is duplicated in git-completion.bash
+ # When updating it, make sure you update the other one to match.
+ if [ -z "${1-}" ]; then
+ if [ -n "${__git_dir-}" ]; then
+ echo "$__git_dir"
+ elif [ -n "${GIT_DIR-}" ]; then
+ test -d "${GIT_DIR-}" || return 1
+ echo "$GIT_DIR"
+ elif [ -d .git ]; then
+ echo .git
+ else
+ git rev-parse --git-dir 2>/dev/null
+ fi
+ elif [ -d "$1/.git" ]; then
+ echo "$1/.git"
+ else
+ echo "$1"
+ fi
+}
+
+# stores the divergence from upstream in $p
+# used by GIT_PS1_SHOWUPSTREAM
+__git_ps1_show_upstream ()
+{
+ local key value
+ local svn_remote svn_url_pattern count n
+ local upstream=git legacy="" verbose=""
+
+ svn_remote=()
+ # get some config options from git-config
+ local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')"
+ while read -r key value; do
+ case "$key" in
+ bash.showupstream)
+ GIT_PS1_SHOWUPSTREAM="$value"
+ if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then
+ p=""
+ return
+ fi
+ ;;
+ svn-remote.*.url)
+ svn_remote[ $((${#svn_remote[@]} + 1)) ]="$value"
+ svn_url_pattern+="\\|$value"
+ upstream=svn+git # default upstream is SVN if available, else git
+ ;;
+ esac
+ done <<< "$output"
+
+ # parse configuration values
+ for option in ${GIT_PS1_SHOWUPSTREAM}; do
+ case "$option" in
+ git|svn) upstream="$option" ;;
+ verbose) verbose=1 ;;
+ legacy) legacy=1 ;;
+ esac
+ done
+
+ # Find our upstream
+ case "$upstream" in
+ git) upstream="@{upstream}" ;;
+ svn*)
+ # get the upstream from the "git-svn-id: ..." in a commit message
+ # (git-svn uses essentially the same procedure internally)
+ local svn_upstream=($(git log --first-parent -1 \
+ --grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null))
+ if [[ 0 -ne ${#svn_upstream[@]} ]]; then
+ svn_upstream=${svn_upstream[ ${#svn_upstream[@]} - 2 ]}
+ svn_upstream=${svn_upstream%@*}
+ local n_stop="${#svn_remote[@]}"
+ for ((n=1; n <= n_stop; n++)); do
+ svn_upstream=${svn_upstream#${svn_remote[$n]}}
+ done
+
+ if [[ -z "$svn_upstream" ]]; then
+ # default branch name for checkouts with no layout:
+ upstream=${GIT_SVN_ID:-git-svn}
+ else
+ upstream=${svn_upstream#/}
+ fi
+ elif [[ "svn+git" = "$upstream" ]]; then
+ upstream="@{upstream}"
+ fi
+ ;;
+ esac
+
+ # Find how many commits we are ahead/behind our upstream
+ if [[ -z "$legacy" ]]; then
+ count="$(git rev-list --count --left-right \
+ "$upstream"...HEAD 2>/dev/null)"
+ else
+ # produce equivalent output to --count for older versions of git
+ local commits
+ if commits="$(git rev-list --left-right "$upstream"...HEAD 2>/dev/null)"
+ then
+ local commit behind=0 ahead=0
+ for commit in $commits
+ do
+ case "$commit" in
+ "<"*) ((behind++)) ;;
+ *) ((ahead++)) ;;
+ esac
+ done
+ count="$behind $ahead"
+ else
+ count=""
+ fi
+ fi
+
+ # calculate the result
+ if [[ -z "$verbose" ]]; then
+ case "$count" in
+ "") # no upstream
+ p="" ;;
+ "0 0") # equal to upstream
+ p="=" ;;
+ "0 "*) # ahead of upstream
+ p=">" ;;
+ *" 0") # behind upstream
+ p="<" ;;
+ *) # diverged from upstream
+ p="<>" ;;
+ esac
+ else
+ case "$count" in
+ "") # no upstream
+ p="" ;;
+ "0 0") # equal to upstream
+ p=" u=" ;;
+ "0 "*) # ahead of upstream
+ p=" u+${count#0 }" ;;
+ *" 0") # behind upstream
+ p=" u-${count% 0}" ;;
+ *) # diverged from upstream
+ p=" u+${count#* }-${count% *}" ;;
+ esac
+ fi
+
+}
+
+
+# __git_ps1 accepts 0 or 1 arguments (i.e., format string)
+# 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 [ -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=""
+ local step=""
+ local total=""
+ if [ -d "$g/rebase-merge" ]; then
+ b="$(cat "$g/rebase-merge/head-name")"
+ step=$(cat "$g/rebase-merge/msgnum")
+ total=$(cat "$g/rebase-merge/end")
+ if [ -f "$g/rebase-merge/interactive" ]; then
+ r="|REBASE-i"
+ else
+ r="|REBASE-m"
+ fi
+ else
+ if [ -d "$g/rebase-apply" ]; then
+ step=$(cat "$g/rebase-apply/next")
+ total=$(cat "$g/rebase-apply/last")
+ if [ -f "$g/rebase-apply/rebasing" ]; then
+ r="|REBASE"
+ elif [ -f "$g/rebase-apply/applying" ]; then
+ r="|AM"
+ else
+ r="|AM/REBASE"
+ fi
+ elif [ -f "$g/MERGE_HEAD" ]; then
+ 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)
+ git describe --contains HEAD ;;
+ (branch)
+ git describe --contains --all HEAD ;;
+ (describe)
+ git describe HEAD ;;
+ (* | default)
+ git describe --tags --exact-match HEAD ;;
+ esac 2>/dev/null)" ||
+
+ b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." ||
+ b="unknown"
+ b="($b)"
+ }
+ fi
+
+ if [ -n "$step" ] && [ -n "$total" ]; then
+ r="$r $step/$total"
+ fi
+
+ local w=""
+ local i=""
+ local s=""
+ local u=""
+ local c=""
+ local p=""
+
+ if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
+ if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
+ c="BARE:"
+ else
+ b="GIT_DIR!"
+ fi
+ elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
+ 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-}" ] &&
+ [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] &&
+ [ -n "$(git ls-files --others --exclude-standard)" ]
+ then
+ u="%${ZSH_VERSION+%}"
+ fi
+
+ if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
+ __git_ps1_show_upstream
+ fi
+ fi
+
+ local f="$w$i$s$u"
+ 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 0000000..88d8fcd
--- /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 0000000..e6561d8
--- /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 0000000..f2cdefe
--- /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/netrc/Makefile b/contrib/credential/netrc/Makefile
new file mode 100644
index 0000000..51b7613
--- /dev/null
+++ b/contrib/credential/netrc/Makefile
@@ -0,0 +1,5 @@
+test:
+ ./test.pl
+
+testverbose:
+ ./test.pl -d -v
diff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc
new file mode 100755
index 0000000..6c51c43
--- /dev/null
+++ b/contrib/credential/netrc/git-credential-netrc
@@ -0,0 +1,421 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Getopt::Long;
+use File::Basename;
+
+my $VERSION = "0.1";
+
+my %options = (
+ help => 0,
+ debug => 0,
+ verbose => 0,
+ insecure => 0,
+ file => [],
+
+ # identical token maps, e.g. host -> host, will be inserted later
+ tmap => {
+ port => 'protocol',
+ machine => 'host',
+ path => 'path',
+ login => 'username',
+ user => 'username',
+ password => 'password',
+ }
+ );
+
+# Map each credential protocol token to itself on the netrc side.
+foreach (values %{$options{tmap}}) {
+ $options{tmap}->{$_} = $_;
+}
+
+# Now, $options{tmap} has a mapping from the netrc format to the Git credential
+# helper protocol.
+
+# Next, we build the reverse token map.
+
+# When $rmap{foo} contains 'bar', that means that what the Git credential helper
+# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file. Keys in
+# %rmap are what we expect to read from the netrc/authinfo file.
+
+my %rmap;
+foreach my $k (keys %{$options{tmap}}) {
+ push @{$rmap{$options{tmap}->{$k}}}, $k;
+}
+
+Getopt::Long::Configure("bundling");
+
+# TODO: maybe allow the token map $options{tmap} to be configurable.
+GetOptions(\%options,
+ "help|h",
+ "debug|d",
+ "insecure|k",
+ "verbose|v",
+ "file|f=s@",
+ );
+
+if ($options{help}) {
+ my $shortname = basename($0);
+ $shortname =~ s/git-credential-//;
+
+ print <<EOHIPPUS;
+
+$0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] [-v] [-k] get
+
+Version $VERSION by tzz\@lifelogs.com. License: BSD.
+
+Options:
+
+ -f|--file AUTHFILE : specify netrc-style files. Files with the .gpg extension
+ will be decrypted by GPG before parsing. Multiple -f
+ arguments are OK. They are processed in order, and the
+ first matching entry found is returned via the credential
+ helper protocol (see below).
+
+ When no -f option is given, .authinfo.gpg, .netrc.gpg,
+ .authinfo, and .netrc files in your home directory are used
+ in this order.
+
+ -k|--insecure : ignore bad file ownership or permissions
+
+ -d|--debug : turn on debugging (developer info)
+
+ -v|--verbose : be more verbose (show files and information found)
+
+To enable this credential helper:
+
+ git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2'
+
+(Note that Git will prepend "git-credential-" to the helper name and look for it
+in the path.)
+
+...and if you want lots of debugging info:
+
+ git config credential.helper '$shortname -f AUTHFILE -d'
+
+...or to see the files opened and data found:
+
+ git config credential.helper '$shortname -f AUTHFILE -v'
+
+Only "get" mode is supported by this credential helper. It opens every AUTHFILE
+and looks for the first entry that matches the requested search criteria:
+
+ 'port|protocol':
+ The protocol that will be used (e.g., https). (protocol=X)
+
+ 'machine|host':
+ The remote hostname for a network credential. (host=X)
+
+ 'path':
+ The path with which the credential will be used. (path=X)
+
+ 'login|user|username':
+ The credential’s username, if we already have one. (username=X)
+
+Thus, when we get this query on STDIN:
+
+host=github.com
+protocol=https
+username=tzz
+
+this credential helper will look for the first entry in every AUTHFILE that
+matches
+
+machine github.com port https login tzz
+
+OR
+
+machine github.com protocol https login tzz
+
+OR... etc. acceptable tokens as listed above. Any unknown tokens are
+simply ignored.
+
+Then, the helper will print out whatever tokens it got from the entry, including
+"password" tokens, mapping back to Git's helper protocol; e.g. "port" is mapped
+back to "protocol". Any redundant entry tokens (part of the original query) are
+skipped.
+
+Again, note that only the first matching entry from all the AUTHFILEs, processed
+in the sequence given on the command line, is used.
+
+Netrc/authinfo tokens can be quoted as 'STRING' or "STRING".
+
+No caching is performed by this credential helper.
+
+EOHIPPUS
+
+ exit 0;
+}
+
+my $mode = shift @ARGV;
+
+# Credentials must get a parameter, so die if it's missing.
+die "Syntax: $0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] get" unless defined $mode;
+
+# Only support 'get' mode; with any other unsupported ones we just exit.
+exit 0 unless $mode eq 'get';
+
+my $files = $options{file};
+
+# if no files were given, use a predefined list.
+# note that .gpg files come first
+unless (scalar @$files) {
+ my @candidates = qw[
+ ~/.authinfo.gpg
+ ~/.netrc.gpg
+ ~/.authinfo
+ ~/.netrc
+ ];
+
+ $files = $options{file} = [ map { glob $_ } @candidates ];
+}
+
+my $query = read_credential_data_from_stdin();
+
+FILE:
+foreach my $file (@$files) {
+ my $gpgmode = $file =~ m/\.gpg$/;
+ unless (-r $file) {
+ log_verbose("Unable to read $file; skipping it");
+ next FILE;
+ }
+
+ # the following check is copied from Net::Netrc, for non-GPG files
+ # OS/2 and Win32 do not handle stat in a way compatible with this check :-(
+ unless ($gpgmode || $options{insecure} ||
+ $^O eq 'os2'
+ || $^O eq 'MSWin32'
+ || $^O eq 'MacOS'
+ || $^O =~ /^cygwin/) {
+ my @stat = stat($file);
+
+ if (@stat) {
+ if ($stat[2] & 077) {
+ log_verbose("Insecure $file (mode=%04o); skipping it",
+ $stat[2] & 07777);
+ next FILE;
+ }
+
+ if ($stat[4] != $<) {
+ log_verbose("Not owner of $file; skipping it");
+ next FILE;
+ }
+ }
+ }
+
+ my @entries = load_netrc($file, $gpgmode);
+
+ unless (scalar @entries) {
+ if ($!) {
+ log_verbose("Unable to open $file: $!");
+ } else {
+ log_verbose("No netrc entries found in $file");
+ }
+
+ next FILE;
+ }
+
+ my $entry = find_netrc_entry($query, @entries);
+ if ($entry) {
+ print_credential_data($entry, $query);
+ # we're done!
+ last FILE;
+ }
+}
+
+exit 0;
+
+sub load_netrc {
+ my $file = shift @_;
+ my $gpgmode = shift @_;
+
+ my $io;
+ if ($gpgmode) {
+ my @cmd = (qw(gpg --decrypt), $file);
+ log_verbose("Using GPG to open $file: [@cmd]");
+ open $io, "-|", @cmd;
+ } else {
+ log_verbose("Opening $file...");
+ open $io, '<', $file;
+ }
+
+ # nothing to do if the open failed (we log the error later)
+ return unless $io;
+
+ # Net::Netrc does this, but the functionality is merged with the file
+ # detection logic, so we have to extract just the part we need
+ my @netrc_entries = net_netrc_loader($io);
+
+ # these entries will use the credential helper protocol token names
+ my @entries;
+
+ foreach my $nentry (@netrc_entries) {
+ my %entry;
+ my $num_port;
+
+ if (!defined $nentry->{machine}) {
+ next;
+ }
+ if (defined $nentry->{port} && $nentry->{port} =~ m/^\d+$/) {
+ $num_port = $nentry->{port};
+ delete $nentry->{port};
+ }
+
+ # create the new entry for the credential helper protocol
+ $entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;
+
+ # for "host X port Y" where Y is an integer (captured by
+ # $num_port above), set the host to "X:Y"
+ if (defined $entry{host} && defined $num_port) {
+ $entry{host} = join(':', $entry{host}, $num_port);
+ }
+
+ push @entries, \%entry;
+ }
+
+ return @entries;
+}
+
+sub net_netrc_loader {
+ my $fh = shift @_;
+ my @entries;
+ my ($mach, $macdef, $tok, @tok);
+
+ LINE:
+ while (<$fh>) {
+ undef $macdef if /\A\n\Z/;
+
+ if ($macdef) {
+ next LINE;
+ }
+
+ s/^\s*//;
+ chomp;
+
+ while (length && s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*//) {
+ (my $tok = $+) =~ s/\\(.)/$1/g;
+ push(@tok, $tok);
+ }
+
+ TOKEN:
+ while (@tok) {
+ if ($tok[0] eq "default") {
+ shift(@tok);
+ $mach = { machine => undef };
+ next TOKEN;
+ }
+
+ $tok = shift(@tok);
+
+ if ($tok eq "machine") {
+ my $host = shift @tok;
+ $mach = { machine => $host };
+ push @entries, $mach;
+ } elsif (exists $options{tmap}->{$tok}) {
+ unless ($mach) {
+ log_debug("Skipping token $tok because no machine was given");
+ next TOKEN;
+ }
+
+ my $value = shift @tok;
+ unless (defined $value) {
+ log_debug("Token $tok had no value, skipping it.");
+ next TOKEN;
+ }
+
+ # Following line added by rmerrell to remove '/' escape char in .netrc
+ $value =~ s/\/\\/\\/g;
+ $mach->{$tok} = $value;
+ } elsif ($tok eq "macdef") { # we ignore macros
+ next TOKEN unless $mach;
+ my $value = shift @tok;
+ $macdef = 1;
+ }
+ }
+ }
+
+ return @entries;
+}
+
+sub read_credential_data_from_stdin {
+ # the query: start with every token with no value
+ my %q = map { $_ => undef } values(%{$options{tmap}});
+
+ while (<STDIN>) {
+ next unless m/^([^=]+)=(.+)/;
+
+ my ($token, $value) = ($1, $2);
+ die "Unknown search token $token" unless exists $q{$token};
+ $q{$token} = $value;
+ log_debug("We were given search token $token and value $value");
+ }
+
+ foreach (sort keys %q) {
+ log_debug("Searching for %s = %s", $_, $q{$_} || '(any value)');
+ }
+
+ return \%q;
+}
+
+# takes the search tokens and then a list of entries
+# each entry is a hash reference
+sub find_netrc_entry {
+ my $query = shift @_;
+
+ ENTRY:
+ foreach my $entry (@_)
+ {
+ my $entry_text = join ', ', map { "$_=$entry->{$_}" } keys %$entry;
+ foreach my $check (sort keys %$query) {
+ if (defined $query->{$check}) {
+ log_debug("compare %s [%s] to [%s] (entry: %s)",
+ $check,
+ $entry->{$check},
+ $query->{$check},
+ $entry_text);
+ unless ($query->{$check} eq $entry->{$check}) {
+ next ENTRY;
+ }
+ } else {
+ log_debug("OK: any value satisfies check $check");
+ }
+ }
+
+ return $entry;
+ }
+
+ # nothing was found
+ return;
+}
+
+sub print_credential_data {
+ my $entry = shift @_;
+ my $query = shift @_;
+
+ log_debug("entry has passed all the search checks");
+ TOKEN:
+ foreach my $git_token (sort keys %$entry) {
+ log_debug("looking for useful token $git_token");
+ # don't print unknown (to the credential helper protocol) tokens
+ next TOKEN unless exists $query->{$git_token};
+
+ # don't print things asked in the query (the entry matches them)
+ next TOKEN if defined $query->{$git_token};
+
+ log_debug("FOUND: $git_token=$entry->{$git_token}");
+ printf "%s=%s\n", $git_token, $entry->{$git_token};
+ }
+}
+sub log_verbose {
+ return unless $options{verbose};
+ printf STDERR @_;
+ printf STDERR "\n";
+}
+
+sub log_debug {
+ return unless $options{debug};
+ printf STDERR @_;
+ printf STDERR "\n";
+}
diff --git a/contrib/credential/netrc/test.netrc b/contrib/credential/netrc/test.netrc
new file mode 100644
index 0000000..ba119a9
--- /dev/null
+++ b/contrib/credential/netrc/test.netrc
@@ -0,0 +1,13 @@
+machine imap login tzz@lifelogs.com port imaps password letmeknow
+machine imap login bob port imaps password bobwillknow
+
+# comment test
+
+machine imap2 login tzz port 1099 password tzzknow
+machine imap2 login bob password bobwillknow
+
+# another command
+
+machine github.com
+ multilinetoken anothervalue
+ login carol password carolknows
diff --git a/contrib/credential/netrc/test.pl b/contrib/credential/netrc/test.pl
new file mode 100755
index 0000000..169b646
--- /dev/null
+++ b/contrib/credential/netrc/test.pl
@@ -0,0 +1,106 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+use Test;
+use IPC::Open2;
+
+BEGIN { plan tests => 15 }
+
+my @global_credential_args = @ARGV;
+my $netrc = './test.netrc';
+print "# Testing insecure file, nothing should be found\n";
+chmod 0644, $netrc;
+my $cred = run_credential(['-f', $netrc, 'get'],
+ { host => 'github.com' });
+
+ok(scalar keys %$cred, 0, "Got 0 keys from insecure file");
+
+print "# Testing missing file, nothing should be found\n";
+chmod 0644, $netrc;
+$cred = run_credential(['-f', '///nosuchfile///', 'get'],
+ { host => 'github.com' });
+
+ok(scalar keys %$cred, 0, "Got 0 keys from missing file");
+
+chmod 0600, $netrc;
+
+print "# Testing with invalid data\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+ "bad data");
+ok(scalar keys %$cred, 4, "Got first found keys with bad data");
+
+print "# Testing netrc file for a missing corovamilkbar entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+ { host => 'corovamilkbar' });
+
+ok(scalar keys %$cred, 0, "Got no corovamilkbar keys");
+
+print "# Testing netrc file for a github.com entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+ { host => 'github.com' });
+
+ok(scalar keys %$cred, 2, "Got 2 Github keys");
+
+ok($cred->{password}, 'carolknows', "Got correct Github password");
+ok($cred->{username}, 'carol', "Got correct Github username");
+
+print "# Testing netrc file for a username-specific entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+ { host => 'imap', username => 'bob' });
+
+ok(scalar keys %$cred, 2, "Got 2 username-specific keys");
+
+ok($cred->{password}, 'bobwillknow', "Got correct user-specific password");
+ok($cred->{protocol}, 'imaps', "Got correct user-specific protocol");
+
+print "# Testing netrc file for a host:port-specific entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+ { host => 'imap2:1099' });
+
+ok(scalar keys %$cred, 2, "Got 2 host:port-specific keys");
+
+ok($cred->{password}, 'tzzknow', "Got correct host:port-specific password");
+ok($cred->{username}, 'tzz', "Got correct host:port-specific username");
+
+print "# Testing netrc file that 'host:port kills host' entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+ { host => 'imap2' });
+
+ok(scalar keys %$cred, 2, "Got 2 'host:port kills host' keys");
+
+ok($cred->{password}, 'bobwillknow', "Got correct 'host:port kills host' password");
+ok($cred->{username}, 'bob', "Got correct 'host:port kills host' username");
+
+sub run_credential
+{
+ my $args = shift @_;
+ my $data = shift @_;
+ my $pid = open2(my $chld_out, my $chld_in,
+ './git-credential-netrc', @global_credential_args,
+ @$args);
+
+ die "Couldn't open pipe to netrc credential helper: $!" unless $pid;
+
+ if (ref $data eq 'HASH')
+ {
+ print $chld_in "$_=$data->{$_}\n" foreach sort keys %$data;
+ }
+ else
+ {
+ print $chld_in "$data\n";
+ }
+
+ close $chld_in;
+ my %ret;
+
+ while (<$chld_out>)
+ {
+ chomp;
+ next unless m/^([^=]+)=(.+)/;
+
+ $ret{$1} = $2;
+ }
+
+ return \%ret;
+}
diff --git a/contrib/credential/osxkeychain/git-credential-osxkeychain.c b/contrib/credential/osxkeychain/git-credential-osxkeychain.c
index 6beed12..3940202 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 0000000..bad45ca
--- /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 0000000..a1d38f0
--- /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 65c95d9..5ffc506 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 0d54aa7..8bc8c75 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 b17952a..d42df7b 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 b09ff8f..c414f0d 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 f9fef6d..0891b9e 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 0ca7718..f8d803c 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 82f5ed3..d12c296 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/git-jump/git-jump b/contrib/git-jump/git-jump
index a33674e..dc90cd6 100755
--- a/contrib/git-jump/git-jump
+++ b/contrib/git-jump/git-jump
@@ -21,9 +21,9 @@ open_editor() {
}
mode_diff() {
- git diff --relative "$@" |
+ git diff --no-prefix --relative "$@" |
perl -ne '
- if (m{^\+\+\+ b/(.*)}) { $file = $1; next }
+ if (m{^\+\+\+ (.*)}) { $file = $1; next }
defined($file) or next;
if (m/^@@ .*\+(\d+)/) { $line = $1; next }
defined($line) or next;
diff --git a/contrib/hg-to-git/hg-to-git.py b/contrib/hg-to-git/hg-to-git.py
index 046cb2b..232625a 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 01af9df..0e5b72d 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
@@ -403,7 +404,7 @@ generate_update_branch_email()
echo " \\"
echo " O -- O -- O ($oldrev)"
echo ""
- echo "The removed revisions are not necessarilly gone - if another reference"
+ echo "The removed revisions are not necessarily gone - if another reference"
echo "still refers to them they will stay in the repository."
rewind_only=1
else
diff --git a/contrib/hooks/setgitperms.perl b/contrib/hooks/setgitperms.perl
index a577ad0..2770a1b 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 0000000..b919655
--- /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
new file mode 100644
index 0000000..f149719
--- /dev/null
+++ b/contrib/mw-to-git/Makefile
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2013
+# Matthieu Moy <Matthieu.Moy@imag.fr>
+#
+## Build git-remote-mediawiki
+
+SCRIPT_PERL=git-remote-mediawiki.perl
+GIT_ROOT_DIR=../..
+HERE=contrib/mw-to-git/
+
+SCRIPT_PERL_FULL=$(patsubst %,$(HERE)/%,$(SCRIPT_PERL))
+
+all: build
+
+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 c18bfa1..9c14c1f 100755
--- a/contrib/mw-to-git/git-remote-mediawiki
+++ b/contrib/mw-to-git/git-remote-mediawiki.perl
@@ -9,47 +9,26 @@
# License: GPL v2 or later
# Gateway between Git and MediaWiki.
-# https://github.com/Bibzball/Git-Mediawiki/wiki
-#
-# Known limitations:
-#
-# - Only wiki pages are managed, no support for [[File:...]]
-# attachments.
-#
-# - Poor performance in the best case: it takes forever to check
-# whether we're up-to-date (on fetch or push) or to fetch a few
-# revisions from a large wiki, because we use exclusively a
-# page-based synchronization. We could switch to a wiki-wide
-# synchronization when the synchronization involves few revisions
-# but the wiki is large.
-#
-# - Git renames could be turned into MediaWiki renames (see TODO
-# below)
-#
-# - login/password support requires the user to write the password
-# cleartext in a file (see TODO below).
-#
-# - No way to import "one page, and all pages included in it"
-#
-# - Multiple remote MediaWikis have not been very well tested.
+# Documentation & bugtracker: https://github.com/moy/Git-Mediawiki/
use strict;
use MediaWiki::API;
use DateTime::Format::ISO8601;
-use encoding 'utf8';
-# use encoding 'utf8' doesn't change STDERROR
-# but we're going to output UTF-8 filenames to STDERR
+# By default, use UTF-8 to communicate with Git and the user
binmode STDERR, ":utf8";
+binmode STDOUT, ":utf8";
use URI::Escape;
+use IPC::Open2;
+
use warnings;
# Mediawiki filenames can contain forward slashes. This variable decides by which pattern they should be replaced
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
@@ -59,6 +38,9 @@ use constant EMPTY_CONTENT => "<!-- empty page -->\n";
# used to reflect file creation or deletion in diff.
use constant NULL_SHA1 => "0000000000000000000000000000000000000000";
+# Used on Git's side to reflect empty edit messages on the wiki
+use constant EMPTY_MESSAGE => '*Empty MediaWiki Message*';
+
my $remotename = $ARGV[0];
my $url = $ARGV[1];
@@ -71,10 +53,18 @@ chomp(@tracked_pages);
my @tracked_categories = split(/[ \n]/, run_git("config --get-all remote.". $remotename .".categories"));
chomp(@tracked_categories);
+# Import media files on pull
+my $import_media = run_git("config --get --bool remote.". $remotename .".mediaimport");
+chomp($import_media);
+$import_media = ($import_media eq "true");
+
+# Export media files on push
+my $export_media = run_git("config --get --bool remote.". $remotename .".mediaexport");
+chomp($export_media);
+$export_media = !($export_media eq "false");
+
my $wiki_login = run_git("config --get remote.". $remotename .".mwLogin");
-# TODO: ideally, this should be able to read from keyboard, but we're
-# inside a remote helper, so our stdin is connect to git, not to a
-# terminal.
+# Note: mwPassword is discourraged. Use the credential system instead.
my $wiki_passwd = run_git("config --get remote.". $remotename .".mwPassword");
my $wiki_domain = run_git("config --get remote.". $remotename .".mwDomain");
chomp($wiki_login);
@@ -86,6 +76,21 @@ my $shallow_import = run_git("config --get --bool remote.". $remotename .".shall
chomp($shallow_import);
$shallow_import = ($shallow_import eq "true");
+# Fetch (clone and pull) by revisions instead of by pages. This behavior
+# is more efficient when we have a wiki with lots of pages and we fetch
+# the revisions quite often so that they concern only few pages.
+# Possible values:
+# - by_rev: perform one query per new revision on the remote wiki
+# - by_page: query each tracked page for new revision
+my $fetch_strategy = run_git("config --get remote.$remotename.fetchStrategy");
+unless ($fetch_strategy) {
+ $fetch_strategy = run_git("config --get mediawiki.fetchStrategy");
+}
+chomp($fetch_strategy);
+unless ($fetch_strategy) {
+ $fetch_strategy = "by_page";
+}
+
# Dumb push: don't update notes and mediawiki ref to reflect the last push.
#
# Configurable with mediawiki.dumbPush, or per-remote with
@@ -151,32 +156,152 @@ while (<STDIN>) {
########################## Functions ##############################
+## credential API management (generic functions)
+
+sub credential_read {
+ my %credential;
+ my $reader = shift;
+ my $op = shift;
+ while (<$reader>) {
+ my ($key, $value) = /([^=]*)=(.*)/;
+ if (not defined $key) {
+ die "ERROR receiving response from git credential $op:\n$_\n";
+ }
+ $credential{$key} = $value;
+ }
+ return %credential;
+}
+
+sub credential_write {
+ my $credential = shift;
+ my $writer = shift;
+ # url overwrites other fields, so it must come first
+ print $writer "url=$credential->{url}\n" if exists $credential->{url};
+ while (my ($key, $value) = each(%$credential) ) {
+ if (length $value && $key ne 'url') {
+ print $writer "$key=$value\n";
+ }
+ }
+}
+
+sub credential_run {
+ my $op = shift;
+ my $credential = shift;
+ my $pid = open2(my $reader, my $writer, "git credential $op");
+ credential_write($credential, $writer);
+ print $writer "\n";
+ close($writer);
+
+ if ($op eq "fill") {
+ %$credential = credential_read($reader, $op);
+ } else {
+ if (<$reader>) {
+ die "ERROR while running git credential $op:\n$_";
+ }
+ }
+ close($reader);
+ waitpid($pid, 0);
+ my $child_exit_status = $? >> 8;
+ if ($child_exit_status != 0) {
+ die "'git credential $op' failed with code $child_exit_status.";
+ }
+}
+
# MediaWiki API instance, created lazily.
my $mediawiki;
sub mw_connect_maybe {
if ($mediawiki) {
- return;
+ return;
}
$mediawiki = MediaWiki::API->new;
$mediawiki->{config}->{api_url} = "$url/api.php";
if ($wiki_login) {
- if (!$mediawiki->login({
- lgname => $wiki_login,
- lgpassword => $wiki_passwd,
- lgdomain => $wiki_domain,
- })) {
- print STDERR "Failed to log in mediawiki user \"$wiki_login\" on $url\n";
- print STDERR "(error " .
- $mediawiki->{error}->{code} . ': ' .
- $mediawiki->{error}->{details} . ")\n";
- exit 1;
+ my %credential = (url => $url);
+ $credential{username} = $wiki_login;
+ $credential{password} = $wiki_passwd;
+ credential_run("fill", \%credential);
+ my $request = {lgname => $credential{username},
+ lgpassword => $credential{password},
+ lgdomain => $wiki_domain};
+ if ($mediawiki->login($request)) {
+ credential_run("approve", \%credential);
+ print STDERR "Logged in mediawiki user \"$credential{username}\".\n";
} else {
- print STDERR "Logged in with user \"$wiki_login\".\n";
+ print STDERR "Failed to log in mediawiki user \"$credential{username}\" on $url\n";
+ print STDERR " (error " .
+ $mediawiki->{error}->{code} . ': ' .
+ $mediawiki->{error}->{details} . ")\n";
+ credential_run("reject", \%credential);
+ exit 1;
+ }
+ }
+}
+
+## Functions for listing pages on the remote wiki
+sub get_mw_tracked_pages {
+ my $pages = shift;
+ get_mw_page_list(\@tracked_pages, $pages);
+}
+
+sub get_mw_page_list {
+ my $page_list = shift;
+ my $pages = shift;
+ my @some_pages = @$page_list;
+ while (@some_pages) {
+ my $last = 50;
+ if ($#some_pages < $last) {
+ $last = $#some_pages;
+ }
+ my @slice = @some_pages[0..$last];
+ get_mw_first_pages(\@slice, $pages);
+ @some_pages = @some_pages[51..$#some_pages];
+ }
+}
+
+sub get_mw_tracked_categories {
+ my $pages = shift;
+ foreach my $category (@tracked_categories) {
+ if (index($category, ':') < 0) {
+ # Mediawiki requires the Category
+ # prefix, but let's not force the user
+ # to specify it.
+ $category = "Category:" . $category;
+ }
+ my $mw_pages = $mediawiki->list( {
+ action => 'query',
+ list => 'categorymembers',
+ cmtitle => $category,
+ cmlimit => 'max' } )
+ || die $mediawiki->{error}->{code} . ': '
+ . $mediawiki->{error}->{details};
+ foreach my $page (@{$mw_pages}) {
+ $pages->{$page->{title}} = $page;
}
}
}
+sub get_mw_all_pages {
+ my $pages = shift;
+ # No user-provided list, get the list of pages from the API.
+ my $mw_pages = $mediawiki->list({
+ action => 'query',
+ list => 'allpages',
+ aplimit => 'max'
+ });
+ if (!defined($mw_pages)) {
+ print STDERR "fatal: could not get the list of wiki pages.\n";
+ print STDERR "fatal: '$url' does not appear to be a mediawiki\n";
+ print STDERR "fatal: make sure '$url/api.php' is a valid page.\n";
+ exit 1;
+ }
+ foreach my $page (@{$mw_pages}) {
+ $pages->{$page->{title}} = $page;
+ }
+}
+
+# queries the wiki for a set of pages. Meant to be used within a loop
+# querying the wiki for slices of page list.
sub get_mw_first_pages {
my $some_pages = shift;
my @some_pages = @{$some_pages};
@@ -205,70 +330,45 @@ sub get_mw_first_pages {
}
}
+# Get the list of pages to be fetched according to configuration.
sub get_mw_pages {
mw_connect_maybe();
+ print STDERR "Listing pages on remote wiki...\n";
+
my %pages; # hash on page titles to avoid duplicates
my $user_defined;
if (@tracked_pages) {
$user_defined = 1;
# The user provided a list of pages titles, but we
# still need to query the API to get the page IDs.
-
- my @some_pages = @tracked_pages;
- while (@some_pages) {
- my $last = 50;
- if ($#some_pages < $last) {
- $last = $#some_pages;
- }
- my @slice = @some_pages[0..$last];
- get_mw_first_pages(\@slice, \%pages);
- @some_pages = @some_pages[51..$#some_pages];
- }
+ get_mw_tracked_pages(\%pages);
}
if (@tracked_categories) {
$user_defined = 1;
- foreach my $category (@tracked_categories) {
- if (index($category, ':') < 0) {
- # Mediawiki requires the Category
- # prefix, but let's not force the user
- # to specify it.
- $category = "Category:" . $category;
- }
- my $mw_pages = $mediawiki->list( {
- action => 'query',
- list => 'categorymembers',
- cmtitle => $category,
- cmlimit => 'max' } )
- || die $mediawiki->{error}->{code} . ': ' . $mediawiki->{error}->{details};
- foreach my $page (@{$mw_pages}) {
- $pages{$page->{title}} = $page;
- }
- }
+ get_mw_tracked_categories(\%pages);
}
if (!$user_defined) {
- # No user-provided list, get the list of pages from
- # the API.
- my $mw_pages = $mediawiki->list({
- action => 'query',
- list => 'allpages',
- aplimit => 500,
- });
- if (!defined($mw_pages)) {
- print STDERR "fatal: could not get the list of wiki pages.\n";
- print STDERR "fatal: '$url' does not appear to be a mediawiki\n";
- print STDERR "fatal: make sure '$url/api.php' is a valid page.\n";
- exit 1;
- }
- foreach my $page (@{$mw_pages}) {
- $pages{$page->{title}} = $page;
+ get_mw_all_pages(\%pages);
+ }
+ if ($import_media) {
+ print STDERR "Getting media files for selected pages...\n";
+ if ($user_defined) {
+ get_linked_mediafiles(\%pages);
+ } else {
+ get_all_mediafiles(\%pages);
}
}
- return values(%pages);
+ print STDERR (scalar keys %pages) . " pages found.\n";
+ return %pages;
}
+# usage: $out = run_git("command args");
+# $out = run_git("command args", "raw"); # don't interpret output as UTF-8.
sub run_git {
- open(my $git, "-|:encoding(UTF-8)", "git " . $_[0]);
+ my $args = shift;
+ my $encoding = (shift || "encoding(UTF-8)");
+ open(my $git, "-|:$encoding", "git " . $args);
my $res = do { local $/; <$git> };
close($git);
@@ -276,6 +376,123 @@ sub run_git {
}
+sub get_all_mediafiles {
+ my $pages = shift;
+ # Attach list of all pages for media files from the API,
+ # they are in a different namespace, only one namespace
+ # can be queried at the same moment
+ my $mw_pages = $mediawiki->list({
+ action => 'query',
+ list => 'allpages',
+ apnamespace => get_mw_namespace_id("File"),
+ aplimit => 'max'
+ });
+ if (!defined($mw_pages)) {
+ print STDERR "fatal: could not get the list of pages for media files.\n";
+ print STDERR "fatal: '$url' does not appear to be a mediawiki\n";
+ print STDERR "fatal: make sure '$url/api.php' is a valid page.\n";
+ exit 1;
+ }
+ foreach my $page (@{$mw_pages}) {
+ $pages->{$page->{title}} = $page;
+ }
+}
+
+sub get_linked_mediafiles {
+ my $pages = shift;
+ my @titles = map $_->{title}, values(%{$pages});
+
+ # The query is split in small batches because of the MW API limit of
+ # the number of links to be returned (500 links max).
+ my $batch = 10;
+ while (@titles) {
+ if ($#titles < $batch) {
+ $batch = $#titles;
+ }
+ my @slice = @titles[0..$batch];
+
+ # pattern 'page1|page2|...' required by the API
+ my $mw_titles = join('|', @slice);
+
+ # Media files could be included or linked from
+ # a page, get all related
+ my $query = {
+ action => 'query',
+ prop => 'links|images',
+ titles => $mw_titles,
+ plnamespace => get_mw_namespace_id("File"),
+ pllimit => 'max'
+ };
+ my $result = $mediawiki->api($query);
+
+ while (my ($id, $page) = each(%{$result->{query}->{pages}})) {
+ my @media_titles;
+ if (defined($page->{links})) {
+ my @link_titles = map $_->{title}, @{$page->{links}};
+ push(@media_titles, @link_titles);
+ }
+ if (defined($page->{images})) {
+ my @image_titles = map $_->{title}, @{$page->{images}};
+ push(@media_titles, @image_titles);
+ }
+ if (@media_titles) {
+ get_mw_page_list(\@media_titles, $pages);
+ }
+ }
+
+ @titles = @titles[($batch+1)..$#titles];
+ }
+}
+
+sub get_mw_mediafile_for_page_revision {
+ # Name of the file on Wiki, with the prefix.
+ my $filename = shift;
+ my $timestamp = shift;
+ my %mediafile;
+
+ # Search if on a media file with given timestamp exists on
+ # MediaWiki. In that case download the file.
+ my $query = {
+ action => 'query',
+ prop => 'imageinfo',
+ titles => "File:" . $filename,
+ iistart => $timestamp,
+ iiend => $timestamp,
+ iiprop => 'timestamp|archivename|url',
+ iilimit => 1
+ };
+ my $result = $mediawiki->api($query);
+
+ my ($fileid, $file) = each( %{$result->{query}->{pages}} );
+ # If not defined it means there is no revision of the file for
+ # given timestamp.
+ if (defined($file->{imageinfo})) {
+ $mediafile{title} = $filename;
+
+ my $fileinfo = pop(@{$file->{imageinfo}});
+ $mediafile{timestamp} = $fileinfo->{timestamp};
+ # Mediawiki::API's download function doesn't support https URLs
+ # and can't download old versions of files.
+ print STDERR "\tDownloading file $mediafile{title}, version $mediafile{timestamp}\n";
+ $mediafile{content} = download_mw_mediafile($fileinfo->{url});
+ }
+ return %mediafile;
+}
+
+sub download_mw_mediafile {
+ my $url = shift;
+
+ my $response = $mediawiki->{ua}->get($url);
+ if ($response->code == 200) {
+ return $response->decoded_content;
+ } else {
+ print STDERR "Error downloading mediafile from :\n";
+ print STDERR "URL: $url\n";
+ print STDERR "Server response: " . $response->code . " " . $response->message . "\n";
+ exit 1;
+ }
+}
+
sub get_last_local_revision {
# Get note regarding last mediawiki revision
my $note = run_git("notes --ref=$remotename/mediawiki show refs/mediawiki/$remotename/master 2>/dev/null");
@@ -297,13 +514,36 @@ sub get_last_local_revision {
# Remember the timestamp corresponding to a revision id.
my %basetimestamps;
+# Get the last remote revision without taking in account which pages are
+# tracked or not. This function makes a single request to the wiki thus
+# avoid a loop onto all tracked pages. This is useful for the fetch-by-rev
+# option.
+sub get_last_global_remote_rev {
+ mw_connect_maybe();
+
+ my $query = {
+ action => 'query',
+ list => 'recentchanges',
+ prop => 'revisions',
+ rclimit => '1',
+ rcdir => 'older',
+ };
+ my $result = $mediawiki->api($query);
+ return $result->{query}->{recentchanges}[0]->{revid};
+}
+
+# Get the last remote revision concerning the tracked pages and the tracked
+# categories.
sub get_last_remote_revision {
mw_connect_maybe();
- my @pages = get_mw_pages();
+ my %pages_hash = get_mw_pages();
+ my @pages = values(%pages_hash);
my $max_rev_num = 0;
+ print STDERR "Getting last revision id on tracked pages...\n";
+
foreach my $page (@pages) {
my $id = $page->{pageid};
@@ -379,6 +619,16 @@ sub literal_data {
print STDOUT "data ", bytes::length($content), "\n", $content;
}
+sub literal_data_raw {
+ # Output possibly binary content.
+ my ($content) = @_;
+ # Avoid confusion between size in bytes and in characters
+ utf8::downgrade($content);
+ binmode STDOUT, ":raw";
+ print STDOUT "data ", bytes::length($content), "\n", $content;
+ binmode STDOUT, ":utf8";
+}
+
sub mw_capabilities {
# Revisions are imported to the private namespace
# refs/mediawiki/$remotename/ by the helper and fetched into
@@ -461,11 +711,24 @@ 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};
my $full_import = shift;
my $n = shift;
+ my $mediafile = shift;
+ my %mediafile;
+ if ($mediafile) {
+ %mediafile = %{$mediafile};
+ }
my $title = $commit{title};
my $comment = $commit{comment};
@@ -483,11 +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 "
+ . 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
@@ -547,8 +816,6 @@ sub mw_import_ref {
mw_connect_maybe();
- my @pages = get_mw_pages();
-
print STDERR "Searching revisions...\n";
my $last_local = get_last_local_revision();
my $fetch_from = $last_local + 1;
@@ -557,36 +824,111 @@ sub mw_import_ref {
} else {
print STDERR ", fetching from here.\n";
}
+
+ my $n = 0;
+ if ($fetch_strategy eq "by_rev") {
+ print STDERR "Fetching & writing export data by revs...\n";
+ $n = mw_import_ref_by_revs($fetch_from);
+ } elsif ($fetch_strategy eq "by_page") {
+ print STDERR "Fetching & writing export data by pages...\n";
+ $n = mw_import_ref_by_pages($fetch_from);
+ } else {
+ print STDERR "fatal: invalid fetch strategy \"$fetch_strategy\".\n";
+ print STDERR "Check your configuration variables remote.$remotename.fetchStrategy and mediawiki.fetchStrategy\n";
+ exit 1;
+ }
+
+ 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 referring to unknown object 0000000000000000000
+ # and the clone fails.
+ }
+}
+
+sub mw_import_ref_by_pages {
+
+ my $fetch_from = shift;
+ my %pages_hash = get_mw_pages();
+ my @pages = values(%pages_hash);
+
my ($n, @revisions) = fetch_mw_revisions(\@pages, $fetch_from);
- # Creation of the fast-import stream
- print STDERR "Fetching & writing export data...\n";
+ @revisions = sort {$a->{revid} <=> $b->{revid}} @revisions;
+ my @revision_ids = map $_->{revid}, @revisions;
- $n = 0;
+ return mw_import_revids($fetch_from, \@revision_ids, \%pages_hash);
+}
+
+sub mw_import_ref_by_revs {
+
+ my $fetch_from = shift;
+ my %pages_hash = get_mw_pages();
+
+ my $last_remote = get_last_global_remote_rev();
+ my @revision_ids = $fetch_from..$last_remote;
+ return mw_import_revids($fetch_from, \@revision_ids, \%pages_hash);
+}
+
+# Import revisions given in second argument (array of integers).
+# Only pages appearing in the third argument (hash indexed by page titles)
+# will be imported.
+sub mw_import_revids {
+ my $fetch_from = shift;
+ my $revision_ids = shift;
+ my $pages = shift;
+
+ my $n = 0;
+ my $n_actual = 0;
my $last_timestamp = 0; # Placeholer in case $rev->timestamp is undefined
- foreach my $pagerevid (sort {$a->{revid} <=> $b->{revid}} @revisions) {
+ foreach my $pagerevid (@$revision_ids) {
+ # Count page even if we skip it, since we display
+ # $n/$total and $total includes skipped pages.
+ $n++;
+
# fetch the content of the pages
my $query = {
action => 'query',
prop => 'revisions',
rvprop => 'content|timestamp|comment|user|ids',
- revids => $pagerevid->{revid},
+ revids => $pagerevid,
};
my $result = $mediawiki->api($query);
- my $rev = pop(@{$result->{query}->{pages}->{$pagerevid->{pageid}}->{revisions}});
+ if (!$result) {
+ die "Failed to retrieve modified page for revision $pagerevid";
+ }
- $n++;
+ if (defined($result->{query}->{badrevids}->{$pagerevid})) {
+ # The revision id does not exist on the remote wiki.
+ next;
+ }
+
+ if (!defined($result->{query}->{pages})) {
+ die "Invalid revision $pagerevid.";
+ }
+
+ my @result_pages = values(%{$result->{query}->{pages}});
+ my $result_page = $result_pages[0];
+ my $rev = $result_pages[0]->{revisions}->[0];
+
+ my $page_title = $result_page->{title};
+
+ if (!exists($pages->{$page_title})) {
+ print STDERR "$n/", scalar(@$revision_ids),
+ ": Skipping revision #$rev->{revid} of $page_title\n";
+ next;
+ }
+
+ $n_actual++;
my %commit;
$commit{author} = $rev->{user} || 'Anonymous';
- $commit{comment} = $rev->{comment} || '*Empty MediaWiki Message*';
- $commit{title} = mediawiki_smudge_filename(
- $result->{query}->{pages}->{$pagerevid->{pageid}}->{title}
- );
- $commit{mw_revision} = $pagerevid->{revid};
+ $commit{comment} = $rev->{comment} || EMPTY_MESSAGE;
+ $commit{title} = mediawiki_smudge_filename($page_title);
+ $commit{mw_revision} = $rev->{revid};
$commit{content} = mediawiki_smudge($rev->{'*'});
if (!defined($rev->{timestamp})) {
@@ -596,17 +938,23 @@ sub mw_import_ref {
}
$commit{date} = DateTime::Format::ISO8601->parse_datetime($last_timestamp);
- print STDERR "$n/", scalar(@revisions), ": Revision #$pagerevid->{revid} of $commit{title}\n";
-
- import_file_revision(\%commit, ($fetch_from == 1), $n);
+ # Differentiates classic pages and media files.
+ my ($namespace, $filename) = $page_title =~ /^([^:]*):(.*)$/;
+ my %mediafile;
+ if ($namespace) {
+ my $id = get_mw_namespace_id($namespace);
+ if ($id && $id == get_mw_namespace_id("File")) {
+ %mediafile = get_mw_mediafile_for_page_revision($filename, $rev->{timestamp});
+ }
+ }
+ # If this is a revision of the media page for new version
+ # of a file do one common commit for both file and media page.
+ # Else do commit only for that page.
+ print STDERR "$n/", scalar(@$revision_ids), ": Revision #$rev->{revid} of $commit{title}\n";
+ import_file_revision(\%commit, ($fetch_from == 1), $n_actual, \%mediafile);
}
- 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
- # and the clone fails.
- }
+ return $n_actual;
}
sub error_non_fast_forward {
@@ -624,6 +972,63 @@ sub error_non_fast_forward {
return 0;
}
+sub mw_upload_file {
+ my $complete_file_name = shift;
+ my $new_sha1 = shift;
+ my $extension = shift;
+ my $file_deleted = shift;
+ my $summary = shift;
+ my $newrevid;
+ my $path = "File:" . $complete_file_name;
+ my %hashFiles = get_allowed_file_extensions();
+ if (!exists($hashFiles{$extension})) {
+ print STDERR "$complete_file_name is not a permitted file on this wiki.\n";
+ print STDERR "Check the configuration of file uploads in your mediawiki.\n";
+ return $newrevid;
+ }
+ # Deleting and uploading a file requires a priviledged user
+ if ($file_deleted) {
+ mw_connect_maybe();
+ my $query = {
+ action => 'delete',
+ title => $path,
+ reason => $summary
+ };
+ if (!$mediawiki->edit($query)) {
+ print STDERR "Failed to delete file on remote wiki\n";
+ print STDERR "Check your permissions on the remote site. Error code:\n";
+ print STDERR $mediawiki->{error}->{code} . ':' . $mediawiki->{error}->{details};
+ exit 1;
+ }
+ } else {
+ # Don't let perl try to interpret file content as UTF-8 => use "raw"
+ my $content = run_git("cat-file blob $new_sha1", "raw");
+ if ($content ne "") {
+ mw_connect_maybe();
+ $mediawiki->{config}->{upload_url} =
+ "$url/index.php/Special:Upload";
+ $mediawiki->edit({
+ action => 'upload',
+ filename => $complete_file_name,
+ comment => $summary,
+ file => [undef,
+ $complete_file_name,
+ Content => $content],
+ ignorewarnings => 1,
+ }, {
+ skip_encoding => 1
+ } ) || die $mediawiki->{error}->{code} . ':'
+ . $mediawiki->{error}->{details};
+ my $last_file_page = $mediawiki->get_page({title => $path});
+ $newrevid = $last_file_page->{revid};
+ print STDERR "Pushed file: $new_sha1 - $complete_file_name.\n";
+ } else {
+ print STDERR "Empty file $complete_file_name not pushed.\n";
+ }
+ }
+ return $newrevid;
+}
+
sub mw_push_file {
my $diff_info = shift;
# $diff_info contains a string in this format:
@@ -636,7 +1041,12 @@ sub mw_push_file {
my $summary = shift;
# MediaWiki revision number. Keep the previous one by default,
# in case there's no edit to perform.
- my $newrevid = shift;
+ my $oldrevid = shift;
+ my $newrevid;
+
+ if ($summary eq EMPTY_MESSAGE) {
+ $summary = '';
+ }
my $new_sha1 = $diff_info_split[3];
my $old_sha1 = $diff_info_split[2];
@@ -644,13 +1054,20 @@ sub mw_push_file {
my $page_deleted = ($new_sha1 eq NULL_SHA1);
$complete_file_name = mediawiki_clean_filename($complete_file_name);
- if (substr($complete_file_name,-3) eq ".mw") {
- my $title = substr($complete_file_name,0,-3);
-
+ my ($title, $extension) = $complete_file_name =~ /^(.*)\.([^\.]*)$/;
+ if (!defined($extension)) {
+ $extension = "";
+ }
+ if ($extension eq "mw") {
+ my $ns = get_mw_namespace_id_for_page($complete_file_name);
+ if ($ns && $ns == get_mw_namespace_id("File") && (!$export_media)) {
+ print STDERR "Ignoring media file related page: $complete_file_name\n";
+ return ($oldrevid, "ok");
+ }
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;
@@ -664,7 +1081,7 @@ sub mw_push_file {
action => 'edit',
summary => $summary,
title => $title,
- basetimestamp => $basetimestamps{$newrevid},
+ basetimestamp => $basetimestamps{$oldrevid},
text => mediawiki_clean($file_content, $page_created),
}, {
skip_encoding => 1 # Helps with names with accentuated characters
@@ -676,7 +1093,7 @@ sub mw_push_file {
$mediawiki->{error}->{code} .
' from mediwiki: ' . $mediawiki->{error}->{details} .
".\n";
- return ($newrevid, "non-fast-forward");
+ return ($oldrevid, "non-fast-forward");
} else {
# Other errors. Shouldn't happen => just die()
die 'Fatal: Error ' .
@@ -686,9 +1103,14 @@ sub mw_push_file {
}
$newrevid = $result->{edit}->{newrevid};
print STDERR "Pushed file: $new_sha1 - $title\n";
+ } elsif ($export_media) {
+ $newrevid = mw_upload_file($complete_file_name, $new_sha1,
+ $extension, $page_deleted,
+ $summary);
} else {
- print STDERR "$complete_file_name not a mediawiki file (Not pushable on this version of git-remote-mediawiki).\n"
+ print STDERR "Ignoring media file $title\n";
}
+ $newrevid = ($newrevid or $oldrevid);
return ($newrevid, "ok");
}
@@ -760,16 +1182,26 @@ sub mw_push_revision {
if ($last_local_revid > 0) {
my $parsed_sha1 = $remoteorigin_sha1;
# Find a path from last MediaWiki commit to pushed commit
+ print STDERR "Computing path from local to remote ...\n";
+ my @local_ancestry = split(/\n/, run_git("rev-list --boundary --parents $local ^$parsed_sha1"));
+ my %local_ancestry;
+ foreach my $line (@local_ancestry) {
+ if (my ($child, $parents) = $line =~ m/^-?([a-f0-9]+) ([a-f0-9 ]+)/) {
+ foreach my $parent (split(' ', $parents)) {
+ $local_ancestry{$parent} = $child;
+ }
+ } elsif (!$line =~ m/^([a-f0-9]+)/) {
+ die "Unexpected output from git rev-list: $line";
+ }
+ }
while ($parsed_sha1 ne $HEAD_sha1) {
- my @commit_info = grep(/^$parsed_sha1/, split(/\n/, run_git("rev-list --children $local")));
- if (!@commit_info) {
+ my $child = $local_ancestry{$parsed_sha1};
+ if (!$child) {
+ printf STDERR "Cannot find a path in history from remote commit to last commit\n";
return error_non_fast_forward($remote);
}
- my @commit_info_split = split(/ |\n/, $commit_info[0]);
- # $commit_info_split[1] is the sha1 of the commit to export
- # $commit_info_split[0] is the sha1 of its direct child
- push(@commit_pairs, \@commit_info_split);
- $parsed_sha1 = $commit_info_split[1];
+ push(@commit_pairs, [$parsed_sha1, $child]);
+ $parsed_sha1 = $child;
}
} else {
# No remote mediawiki revision. Export the whole
@@ -791,8 +1223,8 @@ sub mw_push_revision {
# TODO: we could detect rename, and encode them with a #redirect on the wiki.
# TODO: for now, it's just a delete+add
my @diff_info_list = split(/\0/, $diff_infos);
- # Keep the first line of the commit message as mediawiki comment for the revision
- my $commit_msg = (split(/\n/, run_git("show --pretty=format:\"%s\" $sha1_commit")))[0];
+ # Keep the subject line of the commit message as mediawiki comment for the revision
+ my $commit_msg = run_git("log --no-walk --format=\"%s\" $sha1_commit");
chomp($commit_msg);
# Push every blob
while (@diff_info_list) {
@@ -817,7 +1249,7 @@ sub mw_push_revision {
}
}
unless ($dumb_push) {
- run_git("notes --ref=$remotename/mediawiki add -m \"mediawiki_revision: $mw_revision\" $sha1_commit");
+ run_git("notes --ref=$remotename/mediawiki add -f -m \"mediawiki_revision: $mw_revision\" $sha1_commit");
run_git("update-ref -m \"Git-MediaWiki push\" refs/mediawiki/$remotename/master $sha1_commit $sha1_child");
}
}
@@ -825,3 +1257,104 @@ sub mw_push_revision {
print STDOUT "ok $remote\n";
return 1;
}
+
+sub get_allowed_file_extensions {
+ mw_connect_maybe();
+
+ my $query = {
+ action => 'query',
+ meta => 'siteinfo',
+ siprop => 'fileextensions'
+ };
+ my $result = $mediawiki->api($query);
+ my @file_extensions= map $_->{ext},@{$result->{query}->{fileextensions}};
+ my %hashFile = map {$_ => 1}@file_extensions;
+
+ return %hashFile;
+}
+
+# In memory cache for MediaWiki namespace ids.
+my %namespace_id;
+
+# Namespaces whose id is cached in the configuration file
+# (to avoid duplicates)
+my %cached_mw_namespace_id;
+
+# Return MediaWiki id for a canonical namespace name.
+# Ex.: "File", "Project".
+sub get_mw_namespace_id {
+ mw_connect_maybe();
+ my $name = shift;
+
+ if (!exists $namespace_id{$name}) {
+ # Look at configuration file, if the record for that namespace is
+ # already cached. Namespaces are stored in form:
+ # "Name_of_namespace:Id_namespace", ex.: "File:6".
+ my @temp = split(/[\n]/, run_git("config --get-all remote."
+ . $remotename .".namespaceCache"));
+ chomp(@temp);
+ foreach my $ns (@temp) {
+ my ($n, $id) = split(/:/, $ns);
+ if ($id eq 'notANameSpace') {
+ $namespace_id{$n} = {is_namespace => 0};
+ } else {
+ $namespace_id{$n} = {is_namespace => 1, id => $id};
+ }
+ $cached_mw_namespace_id{$n} = 1;
+ }
+ }
+
+ if (!exists $namespace_id{$name}) {
+ print STDERR "Namespace $name not found in cache, querying the wiki ...\n";
+ # NS not found => get namespace id from MW and store it in
+ # configuration file.
+ my $query = {
+ action => 'query',
+ meta => 'siteinfo',
+ siprop => 'namespaces'
+ };
+ my $result = $mediawiki->api($query);
+
+ while (my ($id, $ns) = each(%{$result->{query}->{namespaces}})) {
+ if (defined($ns->{id}) && defined($ns->{canonical})) {
+ $namespace_id{$ns->{canonical}} = {is_namespace => 1, id => $ns->{id}};
+ if ($ns->{'*'}) {
+ # alias (e.g. french Fichier: as alias for canonical File:)
+ $namespace_id{$ns->{'*'}} = {is_namespace => 1, id => $ns->{id}};
+ }
+ }
+ }
+ }
+
+ my $ns = $namespace_id{$name};
+ my $id;
+
+ unless (defined $ns) {
+ print STDERR "No such namespace $name on MediaWiki.\n";
+ $ns = {is_namespace => 0};
+ $namespace_id{$name} = $ns;
+ }
+
+ if ($ns->{is_namespace}) {
+ $id = $ns->{id};
+ }
+
+ # Store "notANameSpace" as special value for inexisting namespaces
+ my $store_id = ($id || 'notANameSpace');
+
+ # Store explicitely requested namespaces on disk
+ if (!exists $cached_mw_namespace_id{$name}) {
+ run_git("config --add remote.". $remotename
+ .".namespaceCache \"". $name .":". $store_id ."\"");
+ $cached_mw_namespace_id{$name} = 1;
+ }
+ return $id;
+}
+
+sub get_mw_namespace_id_for_page {
+ if (my ($namespace) = $_[0] =~ /^([^:]*):/) {
+ return get_mw_namespace_id($namespace);
+ } else {
+ return;
+ }
+}
diff --git a/contrib/mw-to-git/git-remote-mediawiki.txt b/contrib/mw-to-git/git-remote-mediawiki.txt
index 4d211f5..23b7ef9 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/.gitignore b/contrib/mw-to-git/t/.gitignore
new file mode 100644
index 0000000..a7a40b4
--- /dev/null
+++ b/contrib/mw-to-git/t/.gitignore
@@ -0,0 +1,4 @@
+WEB/
+wiki/
+trash directory.t*/
+test-results/
diff --git a/contrib/mw-to-git/t/Makefile b/contrib/mw-to-git/t/Makefile
new file mode 100644
index 0000000..f422203
--- /dev/null
+++ b/contrib/mw-to-git/t/Makefile
@@ -0,0 +1,31 @@
+#
+# 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>
+#
+## Test git-remote-mediawiki
+
+all: test
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
+
+.PHONY: help test clean all
+
+help:
+ @echo 'Run "$(MAKE) test" to launch test scripts'
+ @echo 'Run "$(MAKE) clean" to remove trash folders'
+
+test:
+ @for t in $(T); do \
+ echo "$$t"; \
+ "./$$t" || exit 1; \
+ done
+
+clean:
+ $(RM) -r 'trash directory'.*
diff --git a/contrib/mw-to-git/t/README b/contrib/mw-to-git/t/README
new file mode 100644
index 0000000..03f6ee5
--- /dev/null
+++ b/contrib/mw-to-git/t/README
@@ -0,0 +1,124 @@
+Tests for Mediawiki-to-Git
+==========================
+
+Introduction
+------------
+This manual describes how to install the git-remote-mediawiki test
+environment on a machine with git installed on it.
+
+Prerequisite
+------------
+
+In order to run this test environment correctly, you will need to
+install the following packages (Debian/Ubuntu names, may need to be
+adapted for another distribution):
+
+* lighttpd
+* php5
+* php5-cgi
+* php5-cli
+* php5-curl
+* php5-sqlite
+
+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 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
+of sqlite).
+
+Run the test environment
+------------------------
+
+Install a new wiki
+~~~~~~~~~~~~~~~~~~
+
+Once you have all the prerequisite, you need to install a MediaWiki
+instance on your machine. If you already have one, it is still
+strongly recommended to install one with the script provided. Here's
+how to work it:
+
+a. change directory to contrib/mw-to-git/t/
+b. if needed, edit test.config to choose your installation parameters
+c. run `./install-wiki.sh install`
+d. check on your favourite web browser if your wiki is correctly
+ installed.
+
+Remove an existing wiki
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Edit the file test.config to fit the wiki you want to delete, and then
+execute the command `./install-wiki.sh delete` from the
+contrib/mw-to-git/t directory.
+
+Run the existing tests
+~~~~~~~~~~~~~~~~~~~~~~
+
+The provided tests are currently in the `contrib/mw-to-git/t` directory.
+The files are all the t936[0-9]-*.sh shell scripts.
+
+a. Run all tests:
+To do so, run "make test" from the contrib/mw-to-git/ directory.
+
+b. Run a specific test:
+To run a given test <test_name>, run ./<test_name> from the
+contrib/mw-to-git/t directory.
+
+How to create new tests
+-----------------------
+
+Available functions
+~~~~~~~~~~~~~~~~~~~
+
+The test environment of git-remote-mediawiki provides some functions
+useful to test its behaviour. for more details about the functions'
+parameters, please refer to the `test-gitmw-lib.sh` and
+`test-gitmw.pl` files.
+
+** `test_check_wiki_precond`:
+Check if the tests must be skipped or not. Please use this function
+at the beginning of each new test file.
+
+** `wiki_getpage`:
+Fetch a given page from the wiki and puts its content in the
+directory in parameter.
+
+** `wiki_delete_page`:
+Delete a given page from the wiki.
+
+** `wiki_edit_page`:
+Create or modify a given page in the wiki. You can specify several
+parameters like a summary for the page edition, or add the page to a
+given category.
+See test-gitmw.pl for more details.
+
+** `wiki_getallpage`:
+Fetch all pages from the wiki into a given directory. The directory
+is created if it does not exists.
+
+** `test_diff_directories`:
+Compare the content of two directories. The content must be the same.
+Use this function to compare the content of a git directory and a wiki
+one created by wiki_getallpage.
+
+** `test_contains_N_files`:
+Check if the given directory contains a given number of file.
+
+** `wiki_page_exists`:
+Tests if a given page exists on the wiki.
+
+** `wiki_reset`:
+Reset the wiki, i.e. flush the database. Use this function at the
+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
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Please, follow the standards given by git. See git/t/README.
+New file should be named as t936[0-9]-*.sh.
+Be sure to reset your wiki regulary with the function `wiki_reset`.
diff --git a/contrib/mw-to-git/t/install-wiki.sh b/contrib/mw-to-git/t/install-wiki.sh
new file mode 100755
index 0000000..70a53f6
--- /dev/null
+++ b/contrib/mw-to-git/t/install-wiki.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+# This script installs or deletes a MediaWiki on your computer.
+# It requires a web server with PHP and SQLite running. In addition, if you
+# do not have MediaWiki sources on your computer, the option 'install'
+# downloads them for you.
+# Please set the CONFIGURATION VARIABLES in ./test-gitmw-lib.sh
+
+WIKI_TEST_DIR=$(cd "$(dirname "$0")" && pwd)
+
+if test -z "$WIKI_TEST_DIR"
+then
+ WIKI_TEST_DIR=.
+fi
+
+. "$WIKI_TEST_DIR"/test-gitmw-lib.sh
+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 "
+ echo " content."
+}
+
+
+# Argument: install, delete, --help | -h
+case "$1" in
+ "install" | "-i")
+ wiki_install
+ exit 0
+ ;;
+ "delete" | "-d")
+ wiki_delete
+ exit 0
+ ;;
+ "--help" | "-h")
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Invalid argument: $1"
+ usage
+ exit 1
+ ;;
+esac
diff --git a/contrib/mw-to-git/t/install-wiki/.gitignore b/contrib/mw-to-git/t/install-wiki/.gitignore
new file mode 100644
index 0000000..b5a2a44
--- /dev/null
+++ b/contrib/mw-to-git/t/install-wiki/.gitignore
@@ -0,0 +1 @@
+wikidb.sqlite
diff --git a/contrib/mw-to-git/t/install-wiki/LocalSettings.php b/contrib/mw-to-git/t/install-wiki/LocalSettings.php
new file mode 100644
index 0000000..745e47e
--- /dev/null
+++ b/contrib/mw-to-git/t/install-wiki/LocalSettings.php
@@ -0,0 +1,129 @@
+<?php
+# This file was automatically generated by the MediaWiki 1.19.0
+# installer. If you make manual changes, please keep track in case you
+# need to recreate them later.
+#
+# See includes/DefaultSettings.php for all configurable settings
+# and their default values, but don't forget to make changes in _this_
+# file, not there.
+#
+# Further documentation for configuration settings may be found at:
+# http://www.mediawiki.org/wiki/Manual:Configuration_settings
+
+# Protect against web entry
+if ( !defined( 'MEDIAWIKI' ) ) {
+ exit;
+}
+
+## Uncomment this to disable output compression
+# $wgDisableOutputCompression = true;
+
+$wgSitename = "Git-MediaWiki-Test";
+$wgMetaNamespace = "Git-MediaWiki-Test";
+
+## The URL base path to the directory containing the wiki;
+## defaults for all runtime URL paths are based off of this.
+## For more information on customizing the URLs please see:
+## http://www.mediawiki.org/wiki/Manual:Short_URL
+$wgScriptPath = "@WG_SCRIPT_PATH@";
+$wgScriptExtension = ".php";
+
+## The protocol and server name to use in fully-qualified URLs
+$wgServer = "@WG_SERVER@";
+
+## The relative URL path to the skins directory
+$wgStylePath = "$wgScriptPath/skins";
+
+## The relative URL path to the logo. Make sure you change this from the default,
+## or else you'll overwrite your logo when you upgrade!
+$wgLogo = "$wgStylePath/common/images/wiki.png";
+
+## UPO means: this is also a user preference option
+
+$wgEnableEmail = true;
+$wgEnableUserEmail = true; # UPO
+
+$wgEmergencyContact = "apache@localhost";
+$wgPasswordSender = "apache@localhost";
+
+$wgEnotifUserTalk = false; # UPO
+$wgEnotifWatchlist = false; # UPO
+$wgEmailAuthentication = true;
+
+## Database settings
+$wgDBtype = "sqlite";
+$wgDBserver = "";
+$wgDBname = "@WG_SQLITE_DATAFILE@";
+$wgDBuser = "";
+$wgDBpassword = "";
+
+# SQLite-specific settings
+$wgSQLiteDataDir = "@WG_SQLITE_DATADIR@";
+
+
+## Shared memory settings
+$wgMainCacheType = CACHE_NONE;
+$wgMemCachedServers = array();
+
+## To enable image uploads, make sure the 'images' directory
+## is writable, then set this to true:
+$wgEnableUploads = true;
+$wgUseImageMagick = true;
+$wgImageMagickConvertCommand ="@CONVERT@";
+$wgFileExtensions[] = 'txt';
+
+# InstantCommons allows wiki to use images from http://commons.wikimedia.org
+$wgUseInstantCommons = false;
+
+## If you use ImageMagick (or any other shell command) on a
+## Linux server, this will need to be set to the name of an
+## available UTF-8 locale
+$wgShellLocale = "en_US.utf8";
+
+## If you want to use image uploads under safe mode,
+## create the directories images/archive, images/thumb and
+## images/temp, and make them all writable. Then uncomment
+## this, if it's not already uncommented:
+#$wgHashedUploadDirectory = false;
+
+## Set $wgCacheDirectory to a writable directory on the web server
+## to make your wiki go slightly faster. The directory should not
+## be publicly accessible from the web.
+#$wgCacheDirectory = "$IP/cache";
+
+# Site language code, should be one of the list in ./languages/Names.php
+$wgLanguageCode = "en";
+
+$wgSecretKey = "1c912bfe3519fb70f5dc523ecc698111cd43d81a11c585b3eefb28f29c2699b7";
+#$wgSecretKey = "@SECRETKEY@";
+
+
+# Site upgrade key. Must be set to a string (default provided) to turn on the
+# web installer while LocalSettings.php is in place
+$wgUpgradeKey = "ddae7dc87cd0a645";
+
+## Default skin: you can change the default skin. Use the internal symbolic
+## names, ie 'standard', 'nostalgia', 'cologneblue', 'monobook', 'vector':
+$wgDefaultSkin = "vector";
+
+## For attaching licensing metadata to pages, and displaying an
+## appropriate copyright notice / icon. GNU Free Documentation
+## License and Creative Commons licenses are supported so far.
+$wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright
+$wgRightsUrl = "";
+$wgRightsText = "";
+$wgRightsIcon = "";
+
+# Path to the GNU diff3 utility. Used for conflict resolution.
+$wgDiff3 = "/usr/bin/diff3";
+
+# Query string length limit for ResourceLoader. You should only set this if
+# your web server has a query string length limit (then set it to that limit),
+# or if you have suhosin.get.max_value_length set in php.ini (then set it to
+# that value)
+$wgResourceLoaderMaxQueryLength = -1;
+
+
+
+# End of automatically generated settings.
+# Add more configuration options below.
diff --git a/contrib/mw-to-git/t/install-wiki/db_install.php b/contrib/mw-to-git/t/install-wiki/db_install.php
new file mode 100644
index 0000000..0f3f4e0
--- /dev/null
+++ b/contrib/mw-to-git/t/install-wiki/db_install.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * This script generates a SQLite database for a MediaWiki version 1.19.0
+ * You must specify the login of the admin (argument 1) and its
+ * password (argument 2) and the folder where the database file
+ * is located (absolute path in argument 3).
+ * It is used by the script install-wiki.sh in order to make easy the
+ * installation of a MediaWiki.
+ *
+ * In order to generate a SQLite database file, MediaWiki ask the user
+ * to submit some forms in its web browser. This script simulates this
+ * behavior though the functions <get> and <submit>
+ *
+ */
+$argc = $_SERVER['argc'];
+$argv = $_SERVER['argv'];
+
+$login = $argv[2];
+$pass = $argv[3];
+$tmp = $argv[4];
+$port = $argv[5];
+
+$url = 'http://localhost:'.$port.'/wiki/mw-config/index.php';
+$db_dir = urlencode($tmp);
+$tmp_cookie = tempnam($tmp, "COOKIE_");
+/*
+ * Fetchs a page with cURL.
+ */
+function get($page_name = "") {
+ $curl = curl_init();
+ $page_name_add = "";
+ if ($page_name != "") {
+ $page_name_add = '?page='.$page_name;
+ }
+ $url = $GLOBALS['url'].$page_name_add;
+ $tmp_cookie = $GLOBALS['tmp_cookie'];
+ curl_setopt($curl, CURLOPT_COOKIEJAR, $tmp_cookie);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($curl, CURLOPT_COOKIEFILE, $tmp_cookie);
+ curl_setopt($curl, CURLOPT_HEADER, true);
+ curl_setopt($curl, CURLOPT_URL, $url);
+
+ $page = curl_exec($curl);
+ if (!$page) {
+ die("Could not get page: $url\n");
+ }
+ curl_close($curl);
+ return $page;
+}
+
+/*
+ * Submits a form with cURL.
+ */
+function submit($page_name, $option = "") {
+ $curl = curl_init();
+ $datapost = 'submit-continue=Continue+%E2%86%92';
+ if ($option != "") {
+ $datapost = $option.'&'.$datapost;
+ }
+ $url = $GLOBALS['url'].'?page='.$page_name;
+ $tmp_cookie = $GLOBALS['tmp_cookie'];
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_POST, true);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $datapost);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curl, CURLOPT_COOKIEJAR, $tmp_cookie);
+ curl_setopt($curl, CURLOPT_COOKIEFILE, $tmp_cookie);
+
+ $page = curl_exec($curl);
+ if (!$page) {
+ die("Could not get page: $url\n");
+ }
+ curl_close($curl);
+ return "$page";
+}
+
+/*
+ * Here starts this script: simulates the behavior of the user
+ * submitting forms to generates the database file.
+ * Note this simulation was made for the MediaWiki version 1.19.0,
+ * we can't assume it works with other versions.
+ *
+ */
+
+$page = get();
+if (!preg_match('/input type="hidden" value="([0-9]+)" name="LanguageRequestTime"/',
+ $page, $matches)) {
+ echo "Unexpected content for page downloaded:\n";
+ echo "$page";
+ die;
+};
+$timestamp = $matches[1];
+$language = "LanguageRequestTime=$timestamp&uselang=en&ContLang=en";
+$page = submit('Language', $language);
+
+submit('Welcome');
+
+$db_config = 'DBType=sqlite';
+$db_config = $db_config.'&sqlite_wgSQLiteDataDir='.$db_dir;
+$db_config = $db_config.'&sqlite_wgDBname='.$argv[1];
+submit('DBConnect', $db_config);
+
+$wiki_config = 'config_wgSitename=TEST';
+$wiki_config = $wiki_config.'&config__NamespaceType=site-name';
+$wiki_config = $wiki_config.'&config_wgMetaNamespace=MyWiki';
+$wiki_config = $wiki_config.'&config__AdminName='.$login;
+
+$wiki_config = $wiki_config.'&config__AdminPassword='.$pass;
+$wiki_config = $wiki_config.'&config__AdminPassword2='.$pass;
+
+$wiki_config = $wiki_config.'&wiki__configEmail=email%40email.org';
+$wiki_config = $wiki_config.'&config__SkipOptional=skip';
+submit('Name', $wiki_config);
+submit('Install');
+submit('Install');
+
+unlink($tmp_cookie);
+?>
diff --git a/contrib/mw-to-git/t/push-pull-tests.sh b/contrib/mw-to-git/t/push-pull-tests.sh
new file mode 100644
index 0000000..9da2dc5
--- /dev/null
+++ b/contrib/mw-to-git/t/push-pull-tests.sh
@@ -0,0 +1,144 @@
+test_push_pull () {
+
+ test_expect_success 'Git pull works after adding a new wiki page' '
+ wiki_reset &&
+
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_1 &&
+ wiki_editpage Foo "page created after the git clone" false &&
+
+ (
+ cd mw_dir_1 &&
+ git pull
+ ) &&
+
+ wiki_getallpage ref_page_1 &&
+ test_diff_directories mw_dir_1 ref_page_1
+ '
+
+ test_expect_success 'Git pull works after editing a wiki page' '
+ wiki_reset &&
+
+ wiki_editpage Foo "page created before the git clone" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_2 &&
+ wiki_editpage Foo "new line added on the wiki" true &&
+
+ (
+ cd mw_dir_2 &&
+ git pull
+ ) &&
+
+ wiki_getallpage ref_page_2 &&
+ test_diff_directories mw_dir_2 ref_page_2
+ '
+
+ test_expect_success 'git pull works on conflict handled by auto-merge' '
+ wiki_reset &&
+
+ wiki_editpage Foo "1 init
+3
+5
+ " false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_3 &&
+
+ wiki_editpage Foo "1 init
+2 content added on wiki after clone
+3
+5
+ " false &&
+
+ (
+ cd mw_dir_3 &&
+ echo "1 init
+3
+4 content added on git after clone
+5
+" >Foo.mw &&
+ git commit -am "conflicting change on foo" &&
+ git pull &&
+ git push
+ )
+ '
+
+ test_expect_success 'Git push works after adding a file .mw' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_4 &&
+ wiki_getallpage ref_page_4 &&
+ (
+ cd mw_dir_4 &&
+ test_path_is_missing Foo.mw &&
+ touch Foo.mw &&
+ echo "hello world" >>Foo.mw &&
+ git add Foo.mw &&
+ git commit -m "Foo" &&
+ git push
+ ) &&
+ wiki_getallpage ref_page_4 &&
+ test_diff_directories mw_dir_4 ref_page_4
+ '
+
+ test_expect_success 'Git push works after editing a file .mw' '
+ wiki_reset &&
+ wiki_editpage "Foo" "page created before the git clone" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_5 &&
+
+ (
+ cd mw_dir_5 &&
+ echo "new line added in the file Foo.mw" >>Foo.mw &&
+ git commit -am "edit file Foo.mw" &&
+ git push
+ ) &&
+
+ wiki_getallpage ref_page_5 &&
+ test_diff_directories mw_dir_5 ref_page_5
+ '
+
+ test_expect_failure 'Git push works after deleting a file' '
+ wiki_reset &&
+ wiki_editpage Foo "wiki page added before git clone" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_6 &&
+
+ (
+ cd mw_dir_6 &&
+ git rm Foo.mw &&
+ git commit -am "page Foo.mw deleted" &&
+ git push
+ ) &&
+
+ test_must_fail wiki_page_exist Foo
+ '
+
+ test_expect_success 'Merge conflict expected and solving it' '
+ wiki_reset &&
+
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_7 &&
+ wiki_editpage Foo "1 conflict
+3 wiki
+4" false &&
+
+ (
+ cd mw_dir_7 &&
+ echo "1 conflict
+2 git
+4" >Foo.mw &&
+ git add Foo.mw &&
+ git commit -m "conflict created" &&
+ test_must_fail git pull &&
+ "$PERL_PATH" -pi -e "s/[<=>].*//g" Foo.mw &&
+ git commit -am "merge conflict solved" &&
+ git push
+ )
+ '
+
+ test_expect_failure 'git pull works after deleting a wiki page' '
+ wiki_reset &&
+ wiki_editpage Foo "wiki page added before the git clone" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_8 &&
+
+ wiki_delete_page Foo &&
+ (
+ cd mw_dir_8 &&
+ git pull &&
+ test_path_is_missing Foo.mw
+ )
+ '
+}
diff --git a/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh b/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh
new file mode 100755
index 0000000..811a90c
--- /dev/null
+++ b/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh
@@ -0,0 +1,257 @@
+#!/bin/sh
+#
+# 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>
+#
+# License: GPL v2 or later
+
+
+test_description='Test the Git Mediawiki remote helper: git clone'
+
+. ./test-gitmw-lib.sh
+. $TEST_DIRECTORY/test-lib.sh
+
+
+test_check_precond
+
+
+test_expect_success 'Git clone creates the expected git log with one file' '
+ wiki_reset &&
+ wiki_editpage foo "this is not important" false -c cat -s "this must be the same" &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_1 &&
+ (
+ cd mw_dir_1 &&
+ git log --format=%s HEAD^..HEAD >log.tmp
+ ) &&
+ echo "this must be the same" >msg.tmp &&
+ diff -b mw_dir_1/log.tmp msg.tmp
+'
+
+
+test_expect_success 'Git clone creates the expected git log with multiple files' '
+ wiki_reset &&
+ wiki_editpage daddy "this is not important" false -s="this must be the same" &&
+ wiki_editpage daddy "neither is this" true -s="this must also be the same" &&
+ wiki_editpage daddy "neither is this" true -s="same same same" &&
+ wiki_editpage dj "dont care" false -s="identical" &&
+ wiki_editpage dj "dont care either" true -s="identical too" &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_2 &&
+ (
+ cd mw_dir_2 &&
+ git log --format=%s Daddy.mw >logDaddy.tmp &&
+ git log --format=%s Dj.mw >logDj.tmp
+ ) &&
+ echo "same same same" >msgDaddy.tmp &&
+ echo "this must also be the same" >>msgDaddy.tmp &&
+ echo "this must be the same" >>msgDaddy.tmp &&
+ echo "identical too" >msgDj.tmp &&
+ echo "identical" >>msgDj.tmp &&
+ diff -b mw_dir_2/logDaddy.tmp msgDaddy.tmp &&
+ diff -b mw_dir_2/logDj.tmp msgDj.tmp
+'
+
+
+test_expect_success 'Git clone creates only Main_Page.mw with an empty wiki' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_3 &&
+ test_contains_N_files mw_dir_3 1 &&
+ test_path_is_file mw_dir_3/Main_Page.mw
+'
+
+test_expect_success 'Git clone does not fetch a deleted page' '
+ wiki_reset &&
+ wiki_editpage foo "this page must be deleted before the clone" false &&
+ wiki_delete_page foo &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_4 &&
+ test_contains_N_files mw_dir_4 1 &&
+ test_path_is_file mw_dir_4/Main_Page.mw &&
+ test_path_is_missing mw_dir_4/Foo.mw
+'
+
+test_expect_success 'Git clone works with page added' '
+ wiki_reset &&
+ wiki_editpage foo " I will be cloned" false &&
+ wiki_editpage bar "I will be cloned" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_5 &&
+ wiki_getallpage ref_page_5 &&
+ test_diff_directories mw_dir_5 ref_page_5 &&
+ wiki_delete_page foo &&
+ wiki_delete_page bar
+'
+
+test_expect_success 'Git clone works with an edited page ' '
+ wiki_reset &&
+ wiki_editpage foo "this page will be edited" \
+ false -s "first edition of page foo"&&
+ wiki_editpage foo "this page has been edited and must be on the clone " true &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_6 &&
+ test_path_is_file mw_dir_6/Foo.mw &&
+ test_path_is_file mw_dir_6/Main_Page.mw &&
+ wiki_getallpage mw_dir_6/page_ref_6 &&
+ test_diff_directories mw_dir_6 mw_dir_6/page_ref_6 &&
+ (
+ cd mw_dir_6 &&
+ git log --format=%s HEAD^ Foo.mw > ../Foo.log
+ ) &&
+ echo "first edition of page foo" > FooExpect.log &&
+ diff FooExpect.log Foo.log
+'
+
+
+test_expect_success 'Git clone works with several pages and some deleted ' '
+ wiki_reset &&
+ wiki_editpage foo "this page will not be deleted" false &&
+ wiki_editpage bar "I must not be erased" false &&
+ wiki_editpage namnam "I will not be there at the end" false &&
+ wiki_editpage nyancat "nyan nyan nyan delete me" false &&
+ wiki_delete_page namnam &&
+ wiki_delete_page nyancat &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_7 &&
+ test_path_is_file mw_dir_7/Foo.mw &&
+ test_path_is_file mw_dir_7/Bar.mw &&
+ test_path_is_missing mw_dir_7/Namnam.mw &&
+ test_path_is_missing mw_dir_7/Nyancat.mw &&
+ wiki_getallpage mw_dir_7/page_ref_7 &&
+ test_diff_directories mw_dir_7 mw_dir_7/page_ref_7
+'
+
+
+test_expect_success 'Git clone works with one specific page cloned ' '
+ wiki_reset &&
+ wiki_editpage foo "I will not be cloned" false &&
+ wiki_editpage bar "Do not clone me" false &&
+ wiki_editpage namnam "I will be cloned :)" false -s="this log must stay" &&
+ wiki_editpage nyancat "nyan nyan nyan you cant clone me" false &&
+ git clone -c remote.origin.pages=namnam \
+ mediawiki::'"$WIKI_URL"' mw_dir_8 &&
+ test_contains_N_files mw_dir_8 1 &&
+ test_path_is_file mw_dir_8/Namnam.mw &&
+ test_path_is_missing mw_dir_8/Main_Page.mw &&
+ (
+ cd mw_dir_8 &&
+ echo "this log must stay" >msg.tmp &&
+ git log --format=%s >log.tmp &&
+ diff -b msg.tmp log.tmp
+ ) &&
+ wiki_check_content mw_dir_8/Namnam.mw Namnam
+'
+
+test_expect_success 'Git clone works with multiple specific page cloned ' '
+ wiki_reset &&
+ wiki_editpage foo "I will be there" false &&
+ wiki_editpage bar "I will not disapear" false &&
+ wiki_editpage namnam "I be erased" false &&
+ wiki_editpage nyancat "nyan nyan nyan you will not erase me" false &&
+ wiki_delete_page namnam &&
+ git clone -c remote.origin.pages="foo bar nyancat namnam" \
+ mediawiki::'"$WIKI_URL"' mw_dir_9 &&
+ test_contains_N_files mw_dir_9 3 &&
+ test_path_is_missing mw_dir_9/Namnam.mw &&
+ test_path_is_file mw_dir_9/Foo.mw &&
+ test_path_is_file mw_dir_9/Nyancat.mw &&
+ test_path_is_file mw_dir_9/Bar.mw &&
+ wiki_check_content mw_dir_9/Foo.mw Foo &&
+ wiki_check_content mw_dir_9/Bar.mw Bar &&
+ wiki_check_content mw_dir_9/Nyancat.mw Nyancat
+'
+
+test_expect_success 'Mediawiki-clone of several specific pages on wiki' '
+ wiki_reset &&
+ wiki_editpage foo "foo 1" false &&
+ wiki_editpage bar "bar 1" false &&
+ wiki_editpage dummy "dummy 1" false &&
+ wiki_editpage cloned_1 "cloned_1 1" false &&
+ wiki_editpage cloned_2 "cloned_2 2" false &&
+ wiki_editpage cloned_3 "cloned_3 3" false &&
+ mkdir -p ref_page_10 &&
+ wiki_getpage cloned_1 ref_page_10 &&
+ wiki_getpage cloned_2 ref_page_10 &&
+ wiki_getpage cloned_3 ref_page_10 &&
+ git clone -c remote.origin.pages="cloned_1 cloned_2 cloned_3" \
+ mediawiki::'"$WIKI_URL"' mw_dir_10 &&
+ test_diff_directories mw_dir_10 ref_page_10
+'
+
+test_expect_success 'Git clone works with the shallow option' '
+ wiki_reset &&
+ wiki_editpage foo "1st revision, should be cloned" false &&
+ wiki_editpage bar "1st revision, should be cloned" false &&
+ wiki_editpage nyan "1st revision, should not be cloned" false &&
+ wiki_editpage nyan "2nd revision, should be cloned" false &&
+ git -c remote.origin.shallow=true clone \
+ mediawiki::'"$WIKI_URL"' mw_dir_11 &&
+ test_contains_N_files mw_dir_11 4 &&
+ test_path_is_file mw_dir_11/Nyan.mw &&
+ test_path_is_file mw_dir_11/Foo.mw &&
+ test_path_is_file mw_dir_11/Bar.mw &&
+ test_path_is_file mw_dir_11/Main_Page.mw &&
+ (
+ cd mw_dir_11 &&
+ test `git log --oneline Nyan.mw | wc -l` -eq 1 &&
+ test `git log --oneline Foo.mw | wc -l` -eq 1 &&
+ test `git log --oneline Bar.mw | wc -l` -eq 1 &&
+ test `git log --oneline Main_Page.mw | wc -l ` -eq 1
+ ) &&
+ wiki_check_content mw_dir_11/Nyan.mw Nyan &&
+ wiki_check_content mw_dir_11/Foo.mw Foo &&
+ wiki_check_content mw_dir_11/Bar.mw Bar &&
+ wiki_check_content mw_dir_11/Main_Page.mw Main_Page
+'
+
+test_expect_success 'Git clone works with the shallow option with a delete page' '
+ wiki_reset &&
+ wiki_editpage foo "1st revision, will be deleted" false &&
+ wiki_editpage bar "1st revision, should be cloned" false &&
+ wiki_editpage nyan "1st revision, should not be cloned" false &&
+ wiki_editpage nyan "2nd revision, should be cloned" false &&
+ wiki_delete_page foo &&
+ git -c remote.origin.shallow=true clone \
+ mediawiki::'"$WIKI_URL"' mw_dir_12 &&
+ test_contains_N_files mw_dir_12 3 &&
+ test_path_is_file mw_dir_12/Nyan.mw &&
+ test_path_is_missing mw_dir_12/Foo.mw &&
+ test_path_is_file mw_dir_12/Bar.mw &&
+ test_path_is_file mw_dir_12/Main_Page.mw &&
+ (
+ cd mw_dir_12 &&
+ test `git log --oneline Nyan.mw | wc -l` -eq 1 &&
+ test `git log --oneline Bar.mw | wc -l` -eq 1 &&
+ test `git log --oneline Main_Page.mw | wc -l ` -eq 1
+ ) &&
+ wiki_check_content mw_dir_12/Nyan.mw Nyan &&
+ wiki_check_content mw_dir_12/Bar.mw Bar &&
+ wiki_check_content mw_dir_12/Main_Page.mw Main_Page
+'
+
+test_expect_success 'Test of fetching a category' '
+ wiki_reset &&
+ wiki_editpage Foo "I will be cloned" false -c=Category &&
+ wiki_editpage Bar "Meet me on the repository" false -c=Category &&
+ wiki_editpage Dummy "I will not come" false &&
+ wiki_editpage BarWrong "I will stay online only" false -c=NotCategory &&
+ git clone -c remote.origin.categories="Category" \
+ mediawiki::'"$WIKI_URL"' mw_dir_13 &&
+ wiki_getallpage ref_page_13 Category &&
+ test_diff_directories mw_dir_13 ref_page_13
+'
+
+test_expect_success 'Test of resistance to modification of category on wiki for clone' '
+ wiki_reset &&
+ wiki_editpage Tobedeleted "this page will be deleted" false -c=Catone &&
+ wiki_editpage Tobeedited "this page will be modified" false -c=Catone &&
+ wiki_editpage Normalone "this page wont be modified and will be on git" false -c=Catone &&
+ wiki_editpage Notconsidered "this page will not appear on local" false &&
+ wiki_editpage Othercategory "this page will not appear on local" false -c=Cattwo &&
+ wiki_editpage Tobeedited "this page have been modified" true -c=Catone &&
+ wiki_delete_page Tobedeleted
+ git clone -c remote.origin.categories="Catone" \
+ mediawiki::'"$WIKI_URL"' mw_dir_14 &&
+ wiki_getallpage ref_page_14 Catone &&
+ test_diff_directories mw_dir_14 ref_page_14
+'
+
+test_done
diff --git a/contrib/mw-to-git/t/t9361-mw-to-git-push-pull.sh b/contrib/mw-to-git/t/t9361-mw-to-git-push-pull.sh
new file mode 100755
index 0000000..9ea2014
--- /dev/null
+++ b/contrib/mw-to-git/t/t9361-mw-to-git-push-pull.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# 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>
+#
+# License: GPL v2 or later
+
+# tests for git-remote-mediawiki
+
+test_description='Test the Git Mediawiki remote helper: git push and git pull simple test cases'
+
+. ./test-gitmw-lib.sh
+. ./push-pull-tests.sh
+. $TEST_DIRECTORY/test-lib.sh
+
+test_check_precond
+
+test_push_pull
+
+test_done
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
new file mode 100755
index 0000000..37021e2
--- /dev/null
+++ b/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh
@@ -0,0 +1,347 @@
+#!/bin/sh
+#
+# 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>
+#
+# License: GPL v2 or later
+
+# tests for git-remote-mediawiki
+
+test_description='Test git-mediawiki with special characters in filenames'
+
+. ./test-gitmw-lib.sh
+. $TEST_DIRECTORY/test-lib.sh
+
+
+test_check_precond
+
+
+test_expect_success 'Git clone works for a wiki with accents in the page names' '
+ wiki_reset &&
+ wiki_editpage féé "This page must be délétéd before clone" false &&
+ wiki_editpage kèè "This page must be deleted before clone" false &&
+ wiki_editpage hàà "This page must be deleted before clone" false &&
+ wiki_editpage kîî "This page must be deleted before clone" false &&
+ wiki_editpage foo "This page must be deleted before clone" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_1 &&
+ wiki_getallpage ref_page_1 &&
+ test_diff_directories mw_dir_1 ref_page_1
+'
+
+
+test_expect_success 'Git pull works with a wiki with accents in the pages names' '
+ wiki_reset &&
+ wiki_editpage kîî "this page must be cloned" false &&
+ wiki_editpage foo "this page must be cloned" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_2 &&
+ wiki_editpage éàîôû "This page must be pulled" false &&
+ (
+ cd mw_dir_2 &&
+ git pull
+ ) &&
+ wiki_getallpage ref_page_2 &&
+ test_diff_directories mw_dir_2 ref_page_2
+'
+
+
+test_expect_success 'Cloning a chosen page works with accents' '
+ wiki_reset &&
+ wiki_editpage kîî "this page must be cloned" false &&
+ git clone -c remote.origin.pages=kîî \
+ mediawiki::'"$WIKI_URL"' mw_dir_3 &&
+ wiki_check_content mw_dir_3/Kîî.mw Kîî &&
+ test_path_is_file mw_dir_3/Kîî.mw &&
+ rm -rf mw_dir_3
+'
+
+
+test_expect_success 'The shallow option works with accents' '
+ wiki_reset &&
+ wiki_editpage néoà "1st revision, should not be cloned" false &&
+ wiki_editpage néoà "2nd revision, should be cloned" false &&
+ git -c remote.origin.shallow=true clone \
+ mediawiki::'"$WIKI_URL"' mw_dir_4 &&
+ test_contains_N_files mw_dir_4 2 &&
+ test_path_is_file mw_dir_4/Néoà.mw &&
+ test_path_is_file mw_dir_4/Main_Page.mw &&
+ (
+ cd mw_dir_4 &&
+ test `git log --oneline Néoà.mw | wc -l` -eq 1 &&
+ test `git log --oneline Main_Page.mw | wc -l ` -eq 1
+ ) &&
+ wiki_check_content mw_dir_4/Néoà.mw Néoà &&
+ wiki_check_content mw_dir_4/Main_Page.mw Main_Page
+'
+
+
+test_expect_success 'Cloning works when page name first letter has an accent' '
+ wiki_reset &&
+ wiki_editpage îî "this page must be cloned" false &&
+ git clone -c remote.origin.pages=îî \
+ mediawiki::'"$WIKI_URL"' mw_dir_5 &&
+ test_path_is_file mw_dir_5/Îî.mw &&
+ wiki_check_content mw_dir_5/Îî.mw Îî
+'
+
+
+test_expect_success 'Git push works with a wiki with accents' '
+ wiki_reset &&
+ wiki_editpage féé "lots of accents : éèàÖ" false &&
+ wiki_editpage foo "this page must be cloned" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_6 &&
+ (
+ cd mw_dir_6 &&
+ echo "A wild Pîkächû appears on the wiki" >Pîkächû.mw &&
+ git add Pîkächû.mw &&
+ git commit -m "A new page appears" &&
+ git push
+ ) &&
+ wiki_getallpage ref_page_6 &&
+ test_diff_directories mw_dir_6 ref_page_6
+'
+
+test_expect_success 'Git clone works with accentsand spaces' '
+ wiki_reset &&
+ wiki_editpage "é à î" "this page must be délété before the clone" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_7 &&
+ wiki_getallpage ref_page_7 &&
+ test_diff_directories mw_dir_7 ref_page_7
+'
+
+test_expect_success 'character $ 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_8 &&
+ test_path_is_file mw_dir_8/File_\$_foo.mw &&
+ wiki_getallpage ref_page_8 &&
+ test_diff_directories mw_dir_8 ref_page_8
+'
+
+
+
+test_expect_success 'character $ in file name (git -> mw) ' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_9 &&
+ (
+ cd mw_dir_9 &&
+ echo "this file is called File_\$_foo.mw" >File_\$_foo.mw &&
+ git add . &&
+ git commit -am "file File_\$_foo.mw" &&
+ git pull &&
+ git push
+ ) &&
+ wiki_getallpage ref_page_9 &&
+ test_diff_directories mw_dir_9 ref_page_9
+'
+
+
+test_expect_failure 'capital at the beginning of file names' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_10 &&
+ (
+ cd mw_dir_10 &&
+ echo "my new file foo" >foo.mw &&
+ echo "my new file Foo... Finger crossed" >Foo.mw &&
+ git add . &&
+ git commit -am "file foo.mw" &&
+ git pull &&
+ git push
+ ) &&
+ wiki_getallpage ref_page_10 &&
+ test_diff_directories mw_dir_10 ref_page_10
+'
+
+
+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 &&
+ wiki_editpage [char_2 "expect to be renamed [char_2" false &&
+ (
+ cd mw_dir_11 &&
+ git pull
+ ) &&
+ test_path_is_file mw_dir_11/{char_1 &&
+ test_path_is_file mw_dir_11/[char_2
+'
+
+test_expect_success 'Pull page with title containing ":" other than namespace separator' '
+ wiki_editpage Foo:Bar content false &&
+ (
+ cd mw_dir_11 &&
+ git pull
+ ) &&
+ test_path_is_file mw_dir_11/Foo:Bar.mw
+'
+
+test_expect_success 'Push page with title containing ":" other than namespace separator' '
+ (
+ cd mw_dir_11 &&
+ echo content >NotANameSpace:Page.mw &&
+ git add NotANameSpace:Page.mw &&
+ git commit -m "add page with colon" &&
+ git push
+ ) &&
+ wiki_page_exist NotANameSpace:Page
+'
+
+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 &&
+ wiki_editpage char_%_5b_2 "expect to be renamed char{_2" false &&
+ (
+ cd mw_dir_12 &&
+ git pull
+ ) &&
+ test_path_is_file mw_dir_12/Char\{_1.mw &&
+ test_path_is_file mw_dir_12/Char\[_2.mw &&
+ wiki_getallpage ref_page_12 &&
+ mv ref_page_12/Char_%_7b_1.mw ref_page_12/Char\{_1.mw &&
+ mv ref_page_12/Char_%_5b_2.mw ref_page_12/Char\[_2.mw &&
+ test_diff_directories mw_dir_12 ref_page_12
+'
+
+
+test_expect_failure 'test of correct formatting for file name beginning with special character' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_13 &&
+ (
+ cd mw_dir_13 &&
+ echo "my new file {char_1" >\{char_1.mw &&
+ echo "my new file [char_2" >\[char_2.mw &&
+ git add . &&
+ git commit -am "committing some exotic file name..." &&
+ git push &&
+ git pull
+ ) &&
+ wiki_getallpage ref_page_13 &&
+ test_path_is_file ref_page_13/{char_1.mw &&
+ test_path_is_file ref_page_13/[char_2.mw &&
+ test_diff_directories mw_dir_13 ref_page_13
+'
+
+
+test_expect_success 'test of correct formatting for file name from git to mw' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_14 &&
+ (
+ cd mw_dir_14 &&
+ echo "my new file char{_1" >Char\{_1.mw &&
+ echo "my new file char[_2" >Char\[_2.mw &&
+ git add . &&
+ git commit -m "committing some exotic file name..." &&
+ git push
+ ) &&
+ wiki_getallpage ref_page_14 &&
+ mv mw_dir_14/Char\{_1.mw mw_dir_14/Char_%_7b_1.mw &&
+ mv mw_dir_14/Char\[_2.mw mw_dir_14/Char_%_5b_2.mw &&
+ test_diff_directories mw_dir_14 ref_page_14
+'
+
+
+test_expect_success 'git clone with /' '
+ wiki_reset &&
+ wiki_editpage \/fo\/o "this is not important" false -c=Deleted &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_15 &&
+ test_path_is_file mw_dir_15/%2Ffo%2Fo.mw &&
+ wiki_check_content mw_dir_15/%2Ffo%2Fo.mw \/fo\/o
+'
+
+
+test_expect_success 'git push with /' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_16 &&
+ echo "I will be on the wiki" >mw_dir_16/%2Ffo%2Fo.mw &&
+ (
+ cd mw_dir_16 &&
+ git add %2Ffo%2Fo.mw &&
+ git commit -m " %2Ffo%2Fo added" &&
+ git push
+ ) &&
+ wiki_page_exist \/fo\/o &&
+ wiki_check_content mw_dir_16/%2Ffo%2Fo.mw \/fo\/o
+
+'
+
+
+test_expect_success 'git clone with \' '
+ wiki_reset &&
+ wiki_editpage \\ko\\o "this is not important" false -c=Deleted &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_17 &&
+ test_path_is_file mw_dir_17/\\ko\\o.mw &&
+ wiki_check_content mw_dir_17/\\ko\\o.mw \\ko\\o
+'
+
+
+test_expect_success 'git push with \' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_18 &&
+ echo "I will be on the wiki" >mw_dir_18/\\ko\\o.mw &&
+ (
+ cd mw_dir_18 &&
+ git add \\ko\\o.mw &&
+ git commit -m " \\ko\\o added" &&
+ git push
+ )&&
+ wiki_page_exist \\ko\\o &&
+ wiki_check_content mw_dir_18/\\ko\\o.mw \\ko\\o
+
+'
+
+test_expect_success 'git clone with \ in format control' '
+ wiki_reset &&
+ wiki_editpage \\no\\o "this is not important" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_19 &&
+ test_path_is_file mw_dir_19/\\no\\o.mw &&
+ wiki_check_content mw_dir_19/\\no\\o.mw \\no\\o
+'
+
+
+test_expect_success 'git push with \ in format control' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_20 &&
+ echo "I will be on the wiki" >mw_dir_20/\\fo\\o.mw &&
+ (
+ cd mw_dir_20 &&
+ git add \\fo\\o.mw &&
+ git commit -m " \\fo\\o added" &&
+ git push
+ )&&
+ wiki_page_exist \\fo\\o &&
+ wiki_check_content mw_dir_20/\\fo\\o.mw \\fo\\o
+
+'
+
+
+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/mw-to-git/t/t9363-mw-to-git-export-import.sh b/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh
new file mode 100755
index 0000000..5a03739
--- /dev/null
+++ b/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh
@@ -0,0 +1,198 @@
+#!/bin/sh
+#
+# 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>
+#
+# License: GPL v2 or later
+
+# tests for git-remote-mediawiki
+
+test_description='Test the Git Mediawiki remote helper: git push and git pull simple test cases'
+
+. ./test-gitmw-lib.sh
+. $TEST_DIRECTORY/test-lib.sh
+
+
+test_check_precond
+
+
+test_git_reimport () {
+ git -c remote.origin.dumbPush=true push &&
+ git -c remote.origin.mediaImport=true pull --rebase
+}
+
+# Don't bother with permissions, be administrator by default
+test_expect_success 'setup config' '
+ git config --global remote.origin.mwLogin WikiAdmin &&
+ git config --global remote.origin.mwPassword AdminPass &&
+ test_might_fail git config --global --unset remote.origin.mediaImport
+'
+
+test_expect_success 'git push can upload media (File:) files' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir &&
+ (
+ cd mw_dir &&
+ echo "hello world" >Foo.txt &&
+ git add Foo.txt &&
+ git commit -m "add a text file" &&
+ git push &&
+ "$PERL_PATH" -e "print STDOUT \"binary content: \".chr(255);" >Foo.txt &&
+ git add Foo.txt &&
+ git commit -m "add a text file with binary content" &&
+ git push
+ )
+'
+
+test_expect_success 'git clone works on previously created wiki with media files' '
+ test_when_finished "rm -rf mw_dir mw_dir_clone" &&
+ git clone -c remote.origin.mediaimport=true \
+ mediawiki::'"$WIKI_URL"' mw_dir_clone &&
+ test_cmp mw_dir_clone/Foo.txt mw_dir/Foo.txt &&
+ (cd mw_dir_clone && git checkout HEAD^) &&
+ (cd mw_dir && git checkout HEAD^) &&
+ test_cmp mw_dir_clone/Foo.txt mw_dir/Foo.txt
+'
+
+test_expect_success 'git push & pull work with locally renamed media files' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir &&
+ test_when_finished "rm -fr mw_dir" &&
+ (
+ cd mw_dir &&
+ echo "A File" >Foo.txt &&
+ git add Foo.txt &&
+ git commit -m "add a file" &&
+ git mv Foo.txt Bar.txt &&
+ git commit -m "Rename a file" &&
+ test_git_reimport &&
+ echo "A File" >expect &&
+ test_cmp expect Bar.txt &&
+ test_path_is_missing Foo.txt
+ )
+'
+
+test_expect_success 'git push can propagate local page deletion' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir &&
+ test_when_finished "rm -fr mw_dir" &&
+ (
+ cd mw_dir &&
+ test_path_is_missing Foo.mw &&
+ echo "hello world" >Foo.mw &&
+ git add Foo.mw &&
+ git commit -m "Add the page Foo" &&
+ git push &&
+ rm -f Foo.mw &&
+ git commit -am "Delete the page Foo" &&
+ test_git_reimport &&
+ test_path_is_missing Foo.mw
+ )
+'
+
+test_expect_success 'git push can propagate local media file deletion' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir &&
+ test_when_finished "rm -fr mw_dir" &&
+ (
+ cd mw_dir &&
+ echo "hello world" >Foo.txt &&
+ git add Foo.txt &&
+ git commit -m "Add the text file Foo" &&
+ git rm Foo.txt &&
+ git commit -m "Delete the file Foo" &&
+ test_git_reimport &&
+ test_path_is_missing Foo.txt
+ )
+'
+
+# test failure: the file is correctly uploaded, and then deleted but
+# as no page link to it, the import (which looks at page revisions)
+# doesn't notice the file deletion on the wiki. We fetch the list of
+# files from the wiki, but as the file is deleted, it doesn't appear.
+test_expect_failure 'git pull correctly imports media file deletion when no page link to it' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir &&
+ test_when_finished "rm -fr mw_dir" &&
+ (
+ cd mw_dir &&
+ echo "hello world" >Foo.txt &&
+ git add Foo.txt &&
+ git commit -m "Add the text file Foo" &&
+ git push &&
+ git rm Foo.txt &&
+ git commit -m "Delete the file Foo" &&
+ test_git_reimport &&
+ test_path_is_missing Foo.txt
+ )
+'
+
+test_expect_success 'git push properly warns about insufficient permissions' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir &&
+ test_when_finished "rm -fr mw_dir" &&
+ (
+ cd mw_dir &&
+ echo "A File" >foo.forbidden &&
+ git add foo.forbidden &&
+ git commit -m "add a file" &&
+ git push 2>actual &&
+ test_i18ngrep "foo.forbidden is not a permitted file" actual
+ )
+'
+
+test_expect_success 'setup a repository with media files' '
+ wiki_reset &&
+ wiki_editpage testpage "I am linking a file [[File:File.txt]]" false &&
+ echo "File content" >File.txt &&
+ wiki_upload_file File.txt &&
+ echo "Another file content" >AnotherFile.txt &&
+ wiki_upload_file AnotherFile.txt
+'
+
+test_expect_success 'git clone works with one specific page cloned and mediaimport=true' '
+ git clone -c remote.origin.pages=testpage \
+ -c remote.origin.mediaimport=true \
+ mediawiki::'"$WIKI_URL"' mw_dir_15 &&
+ test_when_finished "rm -rf mw_dir_15" &&
+ test_contains_N_files mw_dir_15 3 &&
+ test_path_is_file mw_dir_15/Testpage.mw &&
+ test_path_is_file mw_dir_15/File:File.txt.mw &&
+ test_path_is_file mw_dir_15/File.txt &&
+ test_path_is_missing mw_dir_15/Main_Page.mw &&
+ test_path_is_missing mw_dir_15/File:AnotherFile.txt.mw &&
+ test_path_is_missing mw_dir_15/AnothetFile.txt &&
+ wiki_check_content mw_dir_15/Testpage.mw Testpage &&
+ test_cmp mw_dir_15/File.txt File.txt
+'
+
+test_expect_success 'git clone works with one specific page cloned and mediaimport=false' '
+ test_when_finished "rm -rf mw_dir_16" &&
+ git clone -c remote.origin.pages=testpage \
+ mediawiki::'"$WIKI_URL"' mw_dir_16 &&
+ test_contains_N_files mw_dir_16 1 &&
+ test_path_is_file mw_dir_16/Testpage.mw &&
+ test_path_is_missing mw_dir_16/File:File.txt.mw &&
+ test_path_is_missing mw_dir_16/File.txt &&
+ test_path_is_missing mw_dir_16/Main_Page.mw &&
+ wiki_check_content mw_dir_16/Testpage.mw Testpage
+'
+
+# should behave like mediaimport=false
+test_expect_success 'git clone works with one specific page cloned and mediaimport unset' '
+ test_when_finished "rm -fr mw_dir_17" &&
+ git clone -c remote.origin.pages=testpage \
+ mediawiki::'"$WIKI_URL"' mw_dir_17 &&
+ test_contains_N_files mw_dir_17 1 &&
+ test_path_is_file mw_dir_17/Testpage.mw &&
+ test_path_is_missing mw_dir_17/File:File.txt.mw &&
+ test_path_is_missing mw_dir_17/File.txt &&
+ test_path_is_missing mw_dir_17/Main_Page.mw &&
+ wiki_check_content mw_dir_17/Testpage.mw Testpage
+'
+
+test_done
diff --git a/contrib/mw-to-git/t/t9364-pull-by-rev.sh b/contrib/mw-to-git/t/t9364-pull-by-rev.sh
new file mode 100755
index 0000000..5c22457
--- /dev/null
+++ b/contrib/mw-to-git/t/t9364-pull-by-rev.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+test_description='Test the Git Mediawiki remote helper: git pull by revision'
+
+. ./test-gitmw-lib.sh
+. ./push-pull-tests.sh
+. $TEST_DIRECTORY/test-lib.sh
+
+test_check_precond
+
+test_expect_success 'configuration' '
+ git config --global mediawiki.fetchStrategy by_rev
+'
+
+test_push_pull
+
+test_done
diff --git a/contrib/mw-to-git/t/test-gitmw-lib.sh b/contrib/mw-to-git/t/test-gitmw-lib.sh
new file mode 100755
index 0000000..3b2cfac
--- /dev/null
+++ b/contrib/mw-to-git/t/test-gitmw-lib.sh
@@ -0,0 +1,435 @@
+# 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>
+# License: GPL v2 or later
+
+#
+# CONFIGURATION VARIABLES
+# You might want to change these ones
+#
+
+. ./test.config
+
+WIKI_URL=http://"$SERVER_ADDR:$PORT/$WIKI_DIR_NAME"
+CURR_DIR=$(pwd)
+TEST_OUTPUT_DIRECTORY=$(pwd)
+TEST_DIRECTORY="$CURR_DIR"/../../../t
+
+export TEST_OUTPUT_DIRECTORY TEST_DIRECTORY CURR_DIR
+
+if test "$LIGHTTPD" = "false" ; then
+ PORT=80
+else
+ WIKI_DIR_INST="$CURR_DIR/$WEB_WWW"
+fi
+
+wiki_upload_file () {
+ "$CURR_DIR"/test-gitmw.pl upload_file "$@"
+}
+
+wiki_getpage () {
+ "$CURR_DIR"/test-gitmw.pl get_page "$@"
+}
+
+wiki_delete_page () {
+ "$CURR_DIR"/test-gitmw.pl delete_page "$@"
+}
+
+wiki_editpage () {
+ "$CURR_DIR"/test-gitmw.pl edit_page "$@"
+}
+
+die () {
+ die_with_status 1 "$@"
+}
+
+die_with_status () {
+ status=$1
+ shift
+ echo >&2 "$*"
+ exit "$status"
+}
+
+
+# Check the preconditions to run git-remote-mediawiki's tests
+test_check_precond () {
+ if ! test_have_prereq PERL
+ then
+ skip_all='skipping gateway git-mw tests, perl not available'
+ test_done
+ fi
+
+ if [ ! -f "$GIT_BUILD_DIR"/git-remote-mediawiki ];
+ then
+ echo "No remote mediawiki for git found. Copying it in git"
+ echo "cp $GIT_BUILD_DIR/contrib/mw-to-git/git-remote-mediawiki $GIT_BUILD_DIR/"
+ ln -s "$GIT_BUILD_DIR"/contrib/mw-to-git/git-remote-mediawiki "$GIT_BUILD_DIR"
+ fi
+
+ if [ ! -d "$WIKI_DIR_INST/$WIKI_DIR_NAME" ];
+ then
+ skip_all='skipping gateway git-mw tests, no mediawiki found'
+ test_done
+ fi
+}
+
+# test_diff_directories <dir_git> <dir_wiki>
+#
+# Compare the contents of directories <dir_git> and <dir_wiki> with diff
+# and errors if they do not match. The program will
+# not look into .git in the process.
+# Warning: the first argument MUST be the directory containing the git data
+test_diff_directories () {
+ rm -rf "$1_tmp"
+ mkdir -p "$1_tmp"
+ cp "$1"/*.mw "$1_tmp"
+ diff -r -b "$1_tmp" "$2"
+}
+
+# $1=<dir>
+# $2=<N>
+#
+# Check that <dir> contains exactly <N> files
+test_contains_N_files () {
+ if test `ls -- "$1" | wc -l` -ne "$2"; then
+ echo "directory $1 sould contain $2 files"
+ echo "it contains these files:"
+ ls "$1"
+ false
+ fi
+}
+
+
+# wiki_check_content <file_name> <page_name>
+#
+# Compares the contents of the file <file_name> and the wiki page
+# <page_name> and exits with error 1 if they do not match.
+wiki_check_content () {
+ mkdir -p wiki_tmp
+ wiki_getpage "$2" wiki_tmp
+ # replacement of forbidden character in file name
+ page_name=$(printf "%s\n" "$2" | sed -e "s/\//%2F/g")
+
+ diff -b "$1" wiki_tmp/"$page_name".mw
+ if test $? -ne 0
+ then
+ rm -rf wiki_tmp
+ error "ERROR: file $2 not found on wiki"
+ fi
+ rm -rf wiki_tmp
+}
+
+# wiki_page_exist <page_name>
+#
+# Check the existence of the page <page_name> on the wiki and exits
+# with error if it is absent from it.
+wiki_page_exist () {
+ mkdir -p wiki_tmp
+ wiki_getpage "$1" wiki_tmp
+ page_name=$(printf "%s\n" "$1" | sed "s/\//%2F/g")
+ if test -f wiki_tmp/"$page_name".mw ; then
+ rm -rf wiki_tmp
+ else
+ rm -rf wiki_tmp
+ error "test failed: file $1 not found on wiki"
+ fi
+}
+
+# wiki_getallpagename
+#
+# Fetch the name of each page on the wiki.
+wiki_getallpagename () {
+ "$CURR_DIR"/test-gitmw.pl getallpagename
+}
+
+# wiki_getallpagecategory <category>
+#
+# Fetch the name of each page belonging to <category> on the wiki.
+wiki_getallpagecategory () {
+ "$CURR_DIR"/test-gitmw.pl getallpagename "$@"
+}
+
+# wiki_getallpage <dest_dir> [<category>]
+#
+# Fetch all the pages from the wiki and place them in the directory
+# <dest_dir>.
+# If <category> is define, then wiki_getallpage fetch the pages included
+# in <category>.
+wiki_getallpage () {
+ if test -z "$2";
+ then
+ wiki_getallpagename
+ else
+ wiki_getallpagecategory "$2"
+ fi
+ mkdir -p "$1"
+ while read -r line; do
+ wiki_getpage "$line" $1;
+ done < all.txt
+}
+
+# ================= Install part =================
+
+error () {
+ echo "$@" >&2
+ exit 1
+}
+
+# config_lighttpd
+#
+# Create the configuration files and the folders necessary to start lighttpd.
+# Overwrite any existing file.
+config_lighttpd () {
+ mkdir -p $WEB
+ mkdir -p $WEB_TMP
+ mkdir -p $WEB_WWW
+ cat > $WEB/lighttpd.conf <<EOF
+ server.document-root = "$CURR_DIR/$WEB_WWW"
+ server.port = $PORT
+ server.pid-file = "$CURR_DIR/$WEB_TMP/pid"
+
+ server.modules = (
+ "mod_rewrite",
+ "mod_redirect",
+ "mod_access",
+ "mod_accesslog",
+ "mod_fastcgi"
+ )
+
+ index-file.names = ("index.php" , "index.html")
+
+ mimetype.assign = (
+ ".pdf" => "application/pdf",
+ ".sig" => "application/pgp-signature",
+ ".spl" => "application/futuresplash",
+ ".class" => "application/octet-stream",
+ ".ps" => "application/postscript",
+ ".torrent" => "application/x-bittorrent",
+ ".dvi" => "application/x-dvi",
+ ".gz" => "application/x-gzip",
+ ".pac" => "application/x-ns-proxy-autoconfig",
+ ".swf" => "application/x-shockwave-flash",
+ ".tar.gz" => "application/x-tgz",
+ ".tgz" => "application/x-tgz",
+ ".tar" => "application/x-tar",
+ ".zip" => "application/zip",
+ ".mp3" => "audio/mpeg",
+ ".m3u" => "audio/x-mpegurl",
+ ".wma" => "audio/x-ms-wma",
+ ".wax" => "audio/x-ms-wax",
+ ".ogg" => "application/ogg",
+ ".wav" => "audio/x-wav",
+ ".gif" => "image/gif",
+ ".jpg" => "image/jpeg",
+ ".jpeg" => "image/jpeg",
+ ".png" => "image/png",
+ ".xbm" => "image/x-xbitmap",
+ ".xpm" => "image/x-xpixmap",
+ ".xwd" => "image/x-xwindowdump",
+ ".css" => "text/css",
+ ".html" => "text/html",
+ ".htm" => "text/html",
+ ".js" => "text/javascript",
+ ".asc" => "text/plain",
+ ".c" => "text/plain",
+ ".cpp" => "text/plain",
+ ".log" => "text/plain",
+ ".conf" => "text/plain",
+ ".text" => "text/plain",
+ ".txt" => "text/plain",
+ ".dtd" => "text/xml",
+ ".xml" => "text/xml",
+ ".mpeg" => "video/mpeg",
+ ".mpg" => "video/mpeg",
+ ".mov" => "video/quicktime",
+ ".qt" => "video/quicktime",
+ ".avi" => "video/x-msvideo",
+ ".asf" => "video/x-ms-asf",
+ ".asx" => "video/x-ms-asf",
+ ".wmv" => "video/x-ms-wmv",
+ ".bz2" => "application/x-bzip",
+ ".tbz" => "application/x-bzip-compressed-tar",
+ ".tar.bz2" => "application/x-bzip-compressed-tar",
+ "" => "text/plain"
+ )
+
+ fastcgi.server = ( ".php" =>
+ ("localhost" =>
+ ( "socket" => "$CURR_DIR/$WEB_TMP/php.socket",
+ "bin-path" => "$PHP_DIR/php-cgi -c $CURR_DIR/$WEB/php.ini"
+
+ )
+ )
+ )
+EOF
+
+ cat > $WEB/php.ini <<EOF
+ session.save_path ='$CURR_DIR/$WEB_TMP'
+EOF
+}
+
+# start_lighttpd
+#
+# Start or restart daemon lighttpd. If restart, rewrite configuration files.
+start_lighttpd () {
+ if test -f "$WEB_TMP/pid"; then
+ echo "Instance already running. Restarting..."
+ stop_lighttpd
+ fi
+ config_lighttpd
+ "$LIGHTTPD_DIR"/lighttpd -f "$WEB"/lighttpd.conf
+
+ if test $? -ne 0 ; then
+ echo "Could not execute http deamon lighttpd"
+ exit 1
+ fi
+}
+
+# stop_lighttpd
+#
+# Kill daemon lighttpd and removes files and folders associated.
+stop_lighttpd () {
+ test -f "$WEB_TMP/pid" && kill $(cat "$WEB_TMP/pid")
+ rm -rf "$WEB"
+}
+
+# Create the SQLite database of the MediaWiki. If the database file already
+# exists, it will be deleted.
+# This script should be runned from the directory where $FILES_FOLDER is
+# located.
+create_db () {
+ rm -f "$TMP/$DB_FILE"
+
+ echo "Generating the SQLite database file. It can take some time ..."
+ # Run the php script to generate the SQLite database file
+ # with cURL calls.
+ php "$FILES_FOLDER/$DB_INSTALL_SCRIPT" $(basename "$DB_FILE" .sqlite) \
+ "$WIKI_ADMIN" "$WIKI_PASSW" "$TMP" "$PORT"
+
+ if [ ! -f "$TMP/$DB_FILE" ] ; then
+ error "Can't create database file $TMP/$DB_FILE. Try to run ./install-wiki.sh delete first."
+ fi
+
+ # Copy the generated database file into the directory the
+ # user indicated.
+ cp "$TMP/$DB_FILE" "$FILES_FOLDER" ||
+ error "Unable to copy $TMP/$DB_FILE to $FILES_FOLDER"
+}
+
+# Install a wiki in your web server directory.
+wiki_install () {
+ if test $LIGHTTPD = "true" ; then
+ start_lighttpd
+ fi
+
+ SERVER_ADDR=$SERVER_ADDR:$PORT
+ # In this part, we change directory to $TMP in order to download,
+ # unpack and copy the files of MediaWiki
+ (
+ mkdir -p "$WIKI_DIR_INST/$WIKI_DIR_NAME"
+ if [ ! -d "$WIKI_DIR_INST/$WIKI_DIR_NAME" ] ; then
+ error "Folder $WIKI_DIR_INST/$WIKI_DIR_NAME doesn't exist.
+ Please create it and launch the script again."
+ fi
+
+ # Fetch MediaWiki's archive if not already present in the TMP directory
+ cd "$TMP"
+ if [ ! -f "$MW_VERSION.tar.gz" ] ; then
+ echo "Downloading $MW_VERSION sources ..."
+ wget "http://download.wikimedia.org/mediawiki/1.19/mediawiki-1.19.0.tar.gz" ||
+ error "Unable to download "\
+ "http://download.wikimedia.org/mediawiki/1.19/"\
+ "mediawiki-1.19.0.tar.gz. "\
+ "Please fix your connection and launch the script again."
+ echo "$MW_VERSION.tar.gz downloaded in `pwd`. "\
+ "You can delete it later if you want."
+ else
+ echo "Reusing existing $MW_VERSION.tar.gz downloaded in `pwd`."
+ fi
+ archive_abs_path=$(pwd)/"$MW_VERSION.tar.gz"
+ cd "$WIKI_DIR_INST/$WIKI_DIR_NAME/" ||
+ error "can't cd to $WIKI_DIR_INST/$WIKI_DIR_NAME/"
+ tar xzf "$archive_abs_path" --strip-components=1 ||
+ error "Unable to extract WikiMedia's files from $archive_abs_path to "\
+ "$WIKI_DIR_INST/$WIKI_DIR_NAME"
+ ) || exit 1
+
+ create_db
+
+ # Copy the generic LocalSettings.php in the web server's directory
+ # And modify parameters according to the ones set at the top
+ # of this script.
+ # Note that LocalSettings.php is never modified.
+ if [ ! -f "$FILES_FOLDER/LocalSettings.php" ] ; then
+ error "Can't find $FILES_FOLDER/LocalSettings.php " \
+ "in the current folder. "\
+ "Please run the script inside its folder."
+ fi
+ cp "$FILES_FOLDER/LocalSettings.php" \
+ "$FILES_FOLDER/LocalSettings-tmp.php" ||
+ error "Unable to copy $FILES_FOLDER/LocalSettings.php " \
+ "to $FILES_FOLDER/LocalSettings-tmp.php"
+
+ # Parse and set the LocalSettings file of the user according to the
+ # CONFIGURATION VARIABLES section at the beginning of this script
+ file_swap="$FILES_FOLDER/LocalSettings-swap.php"
+ sed "s,@WG_SCRIPT_PATH@,/$WIKI_DIR_NAME," \
+ "$FILES_FOLDER/LocalSettings-tmp.php" > "$file_swap"
+ mv "$file_swap" "$FILES_FOLDER/LocalSettings-tmp.php"
+ sed "s,@WG_SERVER@,http://$SERVER_ADDR," \
+ "$FILES_FOLDER/LocalSettings-tmp.php" > "$file_swap"
+ mv "$file_swap" "$FILES_FOLDER/LocalSettings-tmp.php"
+ sed "s,@WG_SQLITE_DATADIR@,$TMP," \
+ "$FILES_FOLDER/LocalSettings-tmp.php" > "$file_swap"
+ mv "$file_swap" "$FILES_FOLDER/LocalSettings-tmp.php"
+ sed "s,@WG_SQLITE_DATAFILE@,$( basename $DB_FILE .sqlite)," \
+ "$FILES_FOLDER/LocalSettings-tmp.php" > "$file_swap"
+ mv "$file_swap" "$FILES_FOLDER/LocalSettings-tmp.php"
+
+ mv "$FILES_FOLDER/LocalSettings-tmp.php" \
+ "$WIKI_DIR_INST/$WIKI_DIR_NAME/LocalSettings.php" ||
+ error "Unable to move $FILES_FOLDER/LocalSettings-tmp.php" \
+ "in $WIKI_DIR_INST/$WIKI_DIR_NAME"
+ echo "File $FILES_FOLDER/LocalSettings.php is set in" \
+ " $WIKI_DIR_INST/$WIKI_DIR_NAME"
+
+ echo "Your wiki has been installed. You can check it at
+ http://$SERVER_ADDR/$WIKI_DIR_NAME"
+}
+
+# Reset the database of the wiki and the password of the admin
+#
+# Warning: This function must be called only in a subdirectory of t/ directory
+wiki_reset () {
+ # Copy initial database of the wiki
+ if [ ! -f "../$FILES_FOLDER/$DB_FILE" ] ; then
+ error "Can't find ../$FILES_FOLDER/$DB_FILE in the current folder."
+ fi
+ cp "../$FILES_FOLDER/$DB_FILE" "$TMP" ||
+ error "Can't copy ../$FILES_FOLDER/$DB_FILE in $TMP"
+ echo "File $FILES_FOLDER/$DB_FILE is set in $TMP"
+}
+
+# Delete the wiki created in the web server's directory and all its content
+# saved in the database.
+wiki_delete () {
+ if test $LIGHTTPD = "true"; then
+ stop_lighttpd
+ else
+ # Delete the wiki's directory.
+ rm -rf "$WIKI_DIR_INST/$WIKI_DIR_NAME" ||
+ error "Wiki's directory $WIKI_DIR_INST/" \
+ "$WIKI_DIR_NAME could not be deleted"
+ # Delete the wiki's SQLite database.
+ rm -f "$TMP/$DB_FILE" ||
+ error "Database $TMP/$DB_FILE could not be deleted."
+ fi
+
+ # Delete the wiki's SQLite database
+ rm -f "$TMP/$DB_FILE" || error "Database $TMP/$DB_FILE could not be deleted."
+ rm -f "$FILES_FOLDER/$DB_FILE"
+ rm -rf "$TMP/$MW_VERSION"
+}
diff --git a/contrib/mw-to-git/t/test-gitmw.pl b/contrib/mw-to-git/t/test-gitmw.pl
new file mode 100755
index 0000000..0ff7625
--- /dev/null
+++ b/contrib/mw-to-git/t/test-gitmw.pl
@@ -0,0 +1,225 @@
+#!/usr/bin/perl -w -s
+# 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>
+# License: GPL v2 or later
+
+# Usage:
+# ./test-gitmw.pl <command> [argument]*
+# Execute in terminal using the name of the function to call as first
+# parameter, and the function's arguments as following parameters
+#
+# Example:
+# ./test-gitmw.pl "get_page" foo .
+# will call <wiki_getpage> with arguments <foo> and <.>
+#
+# Available functions are:
+# "get_page"
+# "delete_page"
+# "edit_page"
+# "getallpagename"
+
+use MediaWiki::API;
+use Getopt::Long;
+use encoding 'utf8';
+use DateTime::Format::ISO8601;
+use open ':encoding(utf8)';
+use constant SLASH_REPLACEMENT => "%2F";
+
+#Parsing of the config file
+
+my $configfile = "$ENV{'CURR_DIR'}/test.config";
+my %config;
+open my $CONFIG, "<", $configfile or die "can't open $configfile: $!";
+while (<$CONFIG>)
+{
+ chomp;
+ s/#.*//;
+ s/^\s+//;
+ s/\s+$//;
+ next unless length;
+ my ($key, $value) = split (/\s*=\s*/,$_, 2);
+ $config{$key} = $value;
+ last if ($key eq 'LIGHTTPD' and $value eq 'false');
+ last if ($key eq 'PORT');
+}
+close $CONFIG or die "can't close $configfile: $!";
+
+my $wiki_address = "http://$config{'SERVER_ADDR'}".":"."$config{'PORT'}";
+my $wiki_url = "$wiki_address/$config{'WIKI_DIR_NAME'}/api.php";
+my $wiki_admin = "$config{'WIKI_ADMIN'}";
+my $wiki_admin_pass = "$config{'WIKI_PASSW'}";
+my $mw = MediaWiki::API->new;
+$mw->{config}->{api_url} = $wiki_url;
+
+
+# wiki_login <name> <password>
+#
+# Logs the user with <name> and <password> in the global variable
+# of the mediawiki $mw
+sub wiki_login {
+ $mw->login( { lgname => "$_[0]",lgpassword => "$_[1]" } )
+ || die "getpage: login failed";
+}
+
+# wiki_getpage <wiki_page> <dest_path>
+#
+# fetch a page <wiki_page> from the wiki referenced in the global variable
+# $mw and copies its content in directory dest_path
+sub wiki_getpage {
+ my $pagename = $_[0];
+ my $destdir = $_[1];
+
+ my $page = $mw->get_page( { title => $pagename } );
+ if (!defined($page)) {
+ die "getpage: wiki does not exist";
+ }
+
+ my $content = $page->{'*'};
+ if (!defined($content)) {
+ die "getpage: page does not exist";
+ }
+
+ $pagename=$page->{'title'};
+ # Replace spaces by underscore in the page name
+ $pagename =~ s/ /_/g;
+ $pagename =~ s/\//%2F/g;
+ open(my $file, ">$destdir/$pagename.mw");
+ print $file "$content";
+ close ($file);
+
+}
+
+# wiki_delete_page <page_name>
+#
+# delete the page with name <page_name> from the wiki referenced
+# in the global variable $mw
+sub wiki_delete_page {
+ my $pagename = $_[0];
+
+ my $exist=$mw->get_page({title => $pagename});
+
+ if (defined($exist->{'*'})){
+ $mw->edit({ action => 'delete',
+ title => $pagename})
+ || die $mw->{error}->{code} . ": " . $mw->{error}->{details};
+ } else {
+ die "no page with such name found: $pagename\n";
+ }
+}
+
+# wiki_editpage <wiki_page> <wiki_content> <wiki_append> [-c=<category>] [-s=<summary>]
+#
+# Edit a page named <wiki_page> with content <wiki_content> on the wiki
+# referenced with the global variable $mw
+# If <wiki_append> == true : append <wiki_content> at the end of the actual
+# content of the page <wiki_page>
+# If <wik_page> doesn't exist, that page is created with the <wiki_content>
+sub wiki_editpage {
+ my $wiki_page = $_[0];
+ my $wiki_content = $_[1];
+ my $wiki_append = $_[2];
+ my $summary = "";
+ my ($summ, $cat) = ();
+ GetOptions('s=s' => \$summ, 'c=s' => \$cat);
+
+ my $append = 0;
+ if (defined($wiki_append) && $wiki_append eq 'true') {
+ $append=1;
+ }
+
+ my $previous_text ="";
+
+ if ($append) {
+ my $ref = $mw->get_page( { title => $wiki_page } );
+ $previous_text = $ref->{'*'};
+ }
+
+ my $text = $wiki_content;
+ if (defined($previous_text)) {
+ $text="$previous_text$text";
+ }
+
+ # Eventually, add this page to a category.
+ if (defined($cat)) {
+ my $category_name="[[Category:$cat]]";
+ $text="$text\n $category_name";
+ }
+ if(defined($summ)){
+ $summary=$summ;
+ }
+
+ $mw->edit( { action => 'edit', title => $wiki_page, summary => $summary, text => "$text"} );
+}
+
+# wiki_getallpagename [<category>]
+#
+# Fetch all pages of the wiki referenced by the global variable $mw
+# and print the names of each one in the file all.txt with a new line
+# ("\n") between these.
+# If the argument <category> is defined, then this function get only the pages
+# belonging to <category>.
+sub wiki_getallpagename {
+ # fetch the pages of the wiki
+ if (defined($_[0])) {
+ my $mw_pages = $mw->list ( { action => 'query',
+ list => 'categorymembers',
+ cmtitle => "Category:$_[0]",
+ cmnamespace => 0,
+ cmlimit => 500 },
+ )
+ || die $mw->{error}->{code}.": ".$mw->{error}->{details};
+ open(my $file, ">all.txt");
+ foreach my $page (@{$mw_pages}) {
+ print $file "$page->{title}\n";
+ }
+ close ($file);
+
+ } else {
+ my $mw_pages = $mw->list({
+ action => 'query',
+ list => 'allpages',
+ aplimit => 500,
+ })
+ || die $mw->{error}->{code}.": ".$mw->{error}->{details};
+ open(my $file, ">all.txt");
+ foreach my $page (@{$mw_pages}) {
+ print $file "$page->{title}\n";
+ }
+ close ($file);
+ }
+}
+
+sub wiki_upload_file {
+ my $file_name = $_[0];
+ my $resultat = $mw->edit ( {
+ action => 'upload',
+ filename => $file_name,
+ comment => 'upload a file',
+ file => [ $file_name ],
+ ignorewarnings=>1,
+ }, {
+ skip_encoding => 1
+ } ) || die $mw->{error}->{code} . ' : ' . $mw->{error}->{details};
+}
+
+
+
+# Main part of this script: parse the command line arguments
+# and select which function to execute
+my $fct_to_call = shift;
+
+wiki_login($wiki_admin, $wiki_admin_pass);
+
+my %functions_to_call = qw(
+ upload_file wiki_upload_file
+ get_page wiki_getpage
+ delete_page wiki_delete_page
+ edit_page wiki_editpage
+ getallpagename wiki_getallpagename
+);
+die "$0 ERROR: wrong argument" unless exists $functions_to_call{$fct_to_call};
+&{$functions_to_call{$fct_to_call}}(@ARGV);
diff --git a/contrib/mw-to-git/t/test.config b/contrib/mw-to-git/t/test.config
new file mode 100644
index 0000000..958b37b
--- /dev/null
+++ b/contrib/mw-to-git/t/test.config
@@ -0,0 +1,35 @@
+# Name of the web server's directory dedicated to the wiki is WIKI_DIR_NAME
+WIKI_DIR_NAME=wiki
+
+# Login and password of the wiki's admin
+WIKI_ADMIN=WikiAdmin
+WIKI_PASSW=AdminPass
+
+# Address of the web server
+SERVER_ADDR=localhost
+
+# SQLite database of the wiki, named DB_FILE, is located in TMP
+TMP=/tmp
+DB_FILE=wikidb.sqlite
+
+# If LIGHTTPD is not set to true, the script will use the defaut
+# web server running in WIKI_DIR_INST.
+WIKI_DIR_INST=/var/www
+
+# If LIGHTTPD is set to true, the script will use Lighttpd to run
+# the wiki.
+LIGHTTPD=true
+
+# The variables below are useful only if LIGHTTPD is set to true.
+PORT=1234
+PHP_DIR=/usr/bin
+LIGHTTPD_DIR=/usr/sbin
+WEB=WEB
+WEB_TMP=$WEB/tmp
+WEB_WWW=$WEB/www
+
+# The variables below are used by the script to install a wiki.
+# You should not modify these unless you are modifying the script itself.
+MW_VERSION=mediawiki-1.19.0
+FILES_FOLDER=install-wiki
+DB_INSTALL_SCRIPT=db_install.php
diff --git a/contrib/p4import/git-p4import.py b/contrib/p4import/git-p4import.py
index b6e534b..593d6a0 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 0000000..239161d
--- /dev/null
+++ b/contrib/remote-helpers/Makefile
@@ -0,0 +1,14 @@
+TESTS := $(wildcard test*.sh)
+
+export T := $(addprefix $(CURDIR)/,$(TESTS))
+export MAKE := $(MAKE) -e
+export PATH := $(CURDIR):$(PATH)
+export TEST_LINT := test-lint-executable test-lint-shell-syntax
+
+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 0000000..10300c6
--- /dev/null
+++ b/contrib/remote-helpers/git-remote-bzr
@@ -0,0 +1,959 @@
+#!/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
+#
+# If you want to specify which branches you want track (per repo):
+# git config remote-bzr.branches 'trunk, devel, test'
+#
+
+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 bzrlib.errors
+import bzrlib.ui
+import bzrlib.urlutils
+import bzrlib.branch
+
+import sys
+import os
+import json
+import re
+import StringIO
+import atexit, shutil, hashlib, urlparse, subprocess
+
+NAME_RE = re.compile('^([^<>]+)')
+AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
+EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
+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)
+
+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] = 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 str(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 rev in self.marks
+
+ 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 = EMAIL_RE.match(user)
+ if m:
+ name = m.group(1)
+ mail = m.group(2)
+ else:
+ m = NAME_RE.match(user)
+ if m:
+ name = m.group(1).strip()
+
+ if not name:
+ name = 'unknown'
+ if not mail:
+ mail = 'Unknown'
+
+ return '%s <%s>' % (name, mail)
+
+def get_filechanges(cur, prev):
+ modified = {}
+ removed = {}
+
+ changes = cur.changes_from(prev)
+
+ def u(s):
+ return s.encode('utf-8')
+
+ for path, fid, kind in changes.added:
+ modified[u(path)] = fid
+ for path, fid, kind in changes.removed:
+ removed[u(path)] = None
+ for path, fid, kind, mod, _ in changes.modified:
+ modified[u(path)] = fid
+ for oldpath, newpath, fid, kind, mod, _ in changes.renamed:
+ removed[u(oldpath)] = None
+ if kind == 'directory':
+ lst = cur.list_files(from_dir=newpath, recursive=True)
+ for path, file_class, kind, fid, entry in lst:
+ if kind != 'directory':
+ modified[u(newpath + '/' + path)] = fid
+ else:
+ modified[u(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 blob 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(repo, name):
+ global prefix
+
+ ref = '%s/heads/%s' % (prefix, name)
+ tip = marks.get_tip(name)
+
+ branch = bzrlib.branch.Branch.open(branches[name])
+ repo = branch.repository
+
+ branch.lock_read()
+ revs = branch.iter_merge_sorted_revisions(None, tip, 'exclude', 'forward')
+ try:
+ tip_revno = branch.revision_id_to_revno(tip)
+ last_revno, _ = branch.last_revision_info()
+ total = last_revno - tip_revno
+ except bzrlib.errors.NoSuchRevision:
+ tip_revno = 0
+ total = 0
+
+ for revid, _, seq, _ in revs:
+
+ if marks.is_marked(revid):
+ continue
+
+ rev = repo.get_revision(revid)
+ revno = seq[0]
+
+ 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))
+ authors = rev.get_apparent_authors()
+ if authors:
+ author = authors[0].encode('utf-8')
+ author = "%s %u %s" % (fixup_user(author), time, gittz(tz))
+ else:
+ 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 removed:
+ print "D %s" % (f,)
+ for f in modified_final:
+ print "M %s :%u %s" % f
+ print
+
+ if len(seq) > 1:
+ # let's skip branch revisions from the progress report
+ continue
+
+ progress = (revno - tip_revno)
+ if (progress % 100 == 0):
+ if total:
+ print "progress revision %d '%s' (%d/%d)" % (revno, name, progress, total)
+ else:
+ print "progress revision %d '%s' (%d)" % (revno, name, progress)
+
+ branch.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, prefix
+
+ ref = '%s/tags/%s' % (prefix, name)
+ print "reset %s" % ref
+ print "from :%u" % rev_to_mark(tags[name])
+ print
+
+def do_import(parser):
+ global dirname
+
+ 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
+ print "feature force"
+ sys.stdout.flush()
+
+ while parser.check('import'):
+ ref = parser[1]
+ if ref.startswith('refs/heads/'):
+ name = ref[len('refs/heads/'):]
+ export_branch(repo, name)
+ if ref.startswith('refs/tags/'):
+ name = ref[len('refs/tags/'):]
+ export_tag(repo, 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, branch, revid, parents, files):
+ global files_cache
+
+ self.updates = {}
+ self.branch = branch
+
+ def copy_tree(revid):
+ files = files_cache[revid] = {}
+ branch.lock_read()
+ tree = branch.repository.revision_tree(revid)
+ try:
+ for path, entry in tree.iter_entries_by_dir():
+ files[path] = [entry.file_id, None]
+ finally:
+ branch.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()
+ self.rev_files = {}
+
+ for path, data in self.files.iteritems():
+ fid, mark = data
+ self.rev_files[fid] = [path, mark]
+
+ for path, f in files.iteritems():
+ fid, mark = self.files.get(path, [None, None])
+ if not fid:
+ fid = bzrlib.generate_ids.gen_file_id(path)
+ f['path'] = path
+ self.rev_files[fid] = [path, mark]
+ self.updates[fid] = f
+
+ def last_revision(self):
+ return self.base_id
+
+ def iter_changes(self):
+ changes = []
+
+ def get_parent(dirname, basename):
+ parent_fid, mark = self.base_files.get(dirname, [None, None])
+ if parent_fid:
+ return parent_fid
+ parent_fid, mark = self.files.get(dirname, [None, None])
+ if parent_fid:
+ return parent_fid
+ if basename == '':
+ return None
+ fid = bzrlib.generate_ids.gen_file_id(path)
+ 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], None]
+ changes.append(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], None]
+ changes.append(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)
+
+ 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'])
+
+ self.files[path][1] = f['mark']
+ self.rev_files[fid][1] = f['mark']
+
+ return changes
+
+ def get_content(self, file_id):
+ path, mark = self.rev_files[file_id]
+ if mark:
+ return blob_marks[mark]
+
+ # last resort
+ tree = self.branch.repository.revision_tree(self.base_id)
+ return tree.get_file_text(file_id)
+
+ def get_file_with_stat(self, file_id, path=None):
+ content = self.get_content(file_id)
+ return (StringIO.StringIO(content), None)
+
+ def get_symlink_target(self, file_id):
+ return self.get_content(file_id)
+
+ def id2path(self, file_id):
+ path, mark = self.rev_files[file_id]
+ return path
+
+def c_style_unescape(string):
+ if string[0] == string[-1] == '"':
+ return string.decode('string-escape')[1:-1]
+ return string
+
+def parse_commit(parser):
+ global marks, blob_marks, parsed_refs
+ global mode
+
+ parents = []
+
+ ref = parser[1]
+ parser.next()
+
+ if ref.startswith('refs/heads/'):
+ name = ref[len('refs/heads/'):]
+ branch = bzrlib.branch.Branch.open(branches[name])
+ else:
+ die('unknown ref')
+
+ 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()
+
+ # fast-export adds an extra newline
+ if data[-1] == '\n':
+ data = data[:-1]
+
+ 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, 'mark' : mark }
+ elif parser.check('D'):
+ t, path = line.split(' ')
+ f = { 'deleted' : True }
+ else:
+ die('Unknown file command: %s' % line)
+ path = c_style_unescape(path).decode('utf-8')
+ files[path] = f
+
+ committer, date, tz = committer
+ parents = [mark_to_rev(p) for p in parents]
+ revid = bzrlib.generate_ids.gen_revision_id(committer, date)
+ props = {}
+ props['branch-nick'] = branch.nick
+
+ mtree = CustomTree(branch, revid, parents, files)
+ changes = mtree.iter_changes()
+
+ branch.lock_write()
+ try:
+ builder = branch.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:
+ branch.unlock()
+
+ parsed_refs[ref] = revid
+ marks.new_mark(revid, commit_mark)
+
+def parse_reset(parser):
+ global parsed_refs
+
+ 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()
+
+ parsed_refs[ref] = mark_to_rev(from_mark)
+
+def do_export(parser):
+ global parsed_refs, dirname
+
+ 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)
+
+ for ref, revid in parsed_refs.iteritems():
+ if ref.startswith('refs/heads/'):
+ name = ref[len('refs/heads/'):]
+ branch = bzrlib.branch.Branch.open(branches[name])
+ branch.generate_revision_history(revid, marks.get_tip(name))
+
+ if name in peers:
+ peer = bzrlib.branch.Branch.open(peers[name])
+ try:
+ peer.bzrdir.push_branch(branch, revision_id=revid)
+ except bzrlib.errors.DivergedBranches:
+ print "error %s non-fast forward" % ref
+ continue
+
+ try:
+ wt = branch.bzrdir.open_workingtree()
+ wt.update()
+ except bzrlib.errors.NoWorkingTree:
+ pass
+ elif ref.startswith('refs/tags/'):
+ # TODO: implement tag push
+ print "error %s pushing tags not supported" % ref
+ continue
+ else:
+ # transport-helper/fast-export bugs
+ continue
+
+ print "ok %s" % ref
+
+ print
+
+def do_capabilities(parser):
+ global dirname
+
+ print "import"
+ print "export"
+ print "refspec refs/heads/*:%s/heads/*" % 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 ref_is_valid(name):
+ return not True in [c in name for c in '~^: \\']
+
+def do_list(parser):
+ global tags
+
+ master_branch = None
+
+ for name in branches:
+ if not master_branch:
+ master_branch = name
+ print "? refs/heads/%s" % name
+
+ branch = bzrlib.branch.Branch.open(branches[master_branch])
+ branch.lock_read()
+ for tag, revid in branch.tags.get_tag_dict().items():
+ try:
+ branch.revision_id_to_dotted_revno(revid)
+ except bzrlib.errors.NoSuchRevision:
+ continue
+ if not ref_is_valid(tag):
+ continue
+ print "? refs/tags/%s" % tag
+ tags[tag] = revid
+ branch.unlock()
+
+ print "@refs/heads/%s HEAD" % master_branch
+ print
+
+def get_remote_branch(origin, remote_branch, name):
+ global dirname, peers
+
+ branch_path = os.path.join(dirname, 'clone', name)
+ if os.path.exists(branch_path):
+ # pull
+ d = bzrlib.bzrdir.BzrDir.open(branch_path)
+ branch = d.open_branch()
+ try:
+ branch.pull(remote_branch, [], None, False)
+ except bzrlib.errors.DivergedBranches:
+ # use remote branch for now
+ return remote_branch
+ else:
+ # clone
+ d = origin.sprout(branch_path, None,
+ hardlink=True, create_tree_if_local=False,
+ force_new_repo=False,
+ source_branch=remote_branch)
+ branch = d.open_branch()
+
+ return branch
+
+def find_branches(repo, wanted):
+ transport = repo.bzrdir.root_transport
+
+ for fn in transport.iter_files_recursive():
+ if not fn.endswith('.bzr/branch-format'):
+ continue
+
+ name = subdir = fn[:-len('/.bzr/branch-format')]
+ name = name if name != '' else 'master'
+ name = name.replace('/', '+')
+
+ if wanted and not name in wanted:
+ continue
+
+ try:
+ cur = transport.clone(subdir)
+ branch = bzrlib.branch.Branch.open_from_transport(cur)
+ except bzrlib.errors.NotBranchError:
+ continue
+ else:
+ yield name, branch
+
+def get_repo(url, alias):
+ global dirname, peer, branches
+
+ normal_url = bzrlib.urlutils.normalize_url(url)
+ origin = bzrlib.bzrdir.BzrDir.open(url)
+ is_local = isinstance(origin.transport, bzrlib.transport.local.LocalTransport)
+
+ shared_path = os.path.join(gitdir, 'bzr')
+ try:
+ shared_dir = bzrlib.bzrdir.BzrDir.open(shared_path)
+ except bzrlib.errors.NotBranchError:
+ shared_dir = bzrlib.bzrdir.BzrDir.create(shared_path)
+ try:
+ shared_repo = shared_dir.open_repository()
+ except bzrlib.errors.NoRepositoryPresent:
+ shared_repo = shared_dir.create_repository(shared=True)
+
+ if not is_local:
+ clone_path = os.path.join(dirname, 'clone')
+ if not os.path.exists(clone_path):
+ os.mkdir(clone_path)
+ else:
+ # check and remove old organization
+ try:
+ bdir = bzrlib.bzrdir.BzrDir.open(clone_path)
+ bdir.destroy_repository()
+ except bzrlib.errors.NotBranchError:
+ pass
+ except bzrlib.errors.NoRepositoryPresent:
+ pass
+
+ try:
+ repo = origin.open_repository()
+ if not repo.user_transport.listable():
+ # this repository is not usable for us
+ raise bzrlib.errors.NoRepositoryPresent(repo.bzrdir)
+ except bzrlib.errors.NoRepositoryPresent:
+ # branch
+
+ name = 'master'
+ remote_branch = origin.open_branch()
+
+ if not is_local:
+ peers[name] = remote_branch.base
+ branch = get_remote_branch(origin, remote_branch, name)
+ else:
+ branch = remote_branch
+
+ branches[name] = branch.base
+
+ return branch.repository
+ else:
+ # repository
+
+ wanted = get_config('remote-bzr.branches').rstrip().split(', ')
+ # stupid python
+ wanted = [e for e in wanted if e]
+
+ for name, remote_branch in find_branches(repo, wanted):
+
+ if not is_local:
+ peers[name] = remote_branch.base
+ branch = get_remote_branch(origin, remote_branch, name)
+ else:
+ branch = remote_branch
+
+ branches[name] = branch.base
+
+ return repo
+
+def fix_path(alias, orig_url):
+ url = urlparse.urlparse(orig_url, 'file')
+ if url.scheme != 'file' or os.path.isabs(url.path):
+ return
+ abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
+ cmd = ['git', 'config', 'remote.%s.url' % alias, "bzr::%s" % abs_url]
+ subprocess.call(cmd)
+
+def main(args):
+ global marks, prefix, gitdir, dirname
+ global tags, filenodes
+ global blob_marks
+ global parsed_refs
+ global files_cache
+ global is_tmp
+ global branches, peers
+
+ alias = args[1]
+ url = args[2]
+
+ tags = {}
+ filenodes = {}
+ blob_marks = {}
+ parsed_refs = {}
+ files_cache = {}
+ marks = None
+ branches = {}
+ peers = {}
+
+ if alias[5:] == url:
+ is_tmp = True
+ alias = hashlib.sha1(alias).hexdigest()
+ else:
+ is_tmp = False
+
+ prefix = 'refs/bzr/%s' % alias
+ gitdir = os.environ['GIT_DIR']
+ dirname = os.path.join(gitdir, 'bzr', alias)
+
+ if not is_tmp:
+ fix_path(alias, url)
+
+ if not os.path.exists(dirname):
+ os.makedirs(dirname)
+
+ if hasattr(bzrlib.ui.ui_factory, 'be_quiet'):
+ bzrlib.ui.ui_factory.be_quiet(True)
+
+ 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()
+
+def bye():
+ if not marks:
+ return
+ if not is_tmp:
+ marks.store()
+ else:
+ shutil.rmtree(dirname)
+
+atexit.register(bye)
+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 0000000..1dd3d70
--- /dev/null
+++ b/contrib/remote-helpers/git-remote-hg
@@ -0,0 +1,984 @@
+#!/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/
+#
+# For remote repositories a local clone is stored in
+# "$GIT_DIR/hg/origin/clone/.hg/".
+
+from mercurial import hg, ui, bookmarks, context, encoding, node, error, extensions
+
+import re
+import sys
+import os
+import json
+import shutil
+import subprocess
+import urllib
+import atexit
+import urlparse, hashlib
+
+#
+# If you are not in hg-git-compat mode and want to disable the tracking of
+# named branches:
+# git config --global remote-hg.track-branches false
+#
+# If you don't want to force pushes (and thus risk creating new remote heads):
+# git config --global remote-hg.force-push false
+#
+# If you want the equivalent of hg's clone/pull--insecure option:
+# git config --global remote-hg.insecure true
+#
+# 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('^([^<>]+?)? ?<([^<>]*)>$')
+EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
+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 hghex(node):
+ return hg.node.hex(node)
+
+def hgref(ref):
+ return ref.replace('___', ' ')
+
+def gitref(ref):
+ return ref.replace(' ', '___')
+
+def get_config(config):
+ cmd = ['git', 'config', '--get', config]
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ output, _ = process.communicate()
+ return output
+
+def get_config_bool(config, default=False):
+ value = get_config(config).rstrip('\n')
+ if value == "true":
+ return True
+ elif value == "false":
+ return False
+ else:
+ return default
+
+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 next_mark(self):
+ self.last_mark += 1
+ return self.last_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 str(rev) in self.marks
+
+ 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 fix_file_path(path):
+ if not os.path.isabs(path):
+ return path
+ return os.path.relpath(path, '/')
+
+def export_files(files):
+ global marks, filenodes
+
+ final = []
+ for f in files:
+ fid = node.hex(f.filenode())
+
+ if fid in filenodes:
+ mark = filenodes[fid]
+ else:
+ mark = marks.next_mark()
+ filenodes[fid] = mark
+ d = f.data()
+
+ print "blob"
+ print "mark :%u" % mark
+ print "data %d" % len(d)
+ print d
+
+ path = fix_file_path(f.path())
+ final.append((gitmode(f.flags()), mark, path))
+
+ return final
+
+def get_filechanges(repo, ctx, parent):
+ modified = set()
+ added = set()
+ removed = set()
+
+ # load earliest manifest first for caching reasons
+ prev = repo[parent].manifest().copy()
+ cur = ctx.manifest()
+
+ 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 = EMAIL_RE.match(user)
+ if m:
+ name = m.group(1)
+ mail = m.group(2)
+ 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')
+ myui.fout = sys.stderr
+
+ if get_config_bool('remote-hg.insecure'):
+ myui.setconfig('web', 'cacerts', '')
+
+ try:
+ mod = extensions.load(myui, 'hgext.schemes', None)
+ mod.extsetup(myui)
+ except ImportError:
+ pass
+
+ if hg.islocal(url):
+ repo = hg.repository(myui, url)
+ else:
+ local_path = os.path.join(dirname, 'clone')
+ if not os.path.exists(local_path):
+ try:
+ peer, dstpeer = hg.clone(myui, {}, url, local_path, update=True, pull=True)
+ except:
+ die('Repository error')
+ repo = dstpeer.local()
+ else:
+ repo = hg.repository(myui, local_path)
+ try:
+ peer = hg.peer(myui, {}, url)
+ except:
+ die('Repository error')
+ 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)
+
+ 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)
+
+ modified_final = export_files(c.filectx(f) for f in modified)
+
+ 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_final:
+ print "M %s :%u %s" % f
+ for f in removed:
+ print "D %s" % (fix_file_path(f))
+ print
+
+ count += 1
+ if (count % 100 == 0):
+ print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs))
+
+ # 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[hgref(tag)])
+
+def export_bookmark(repo, bmark):
+ head = bmarks[hgref(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 branch_tip(repo, branch):
+ # older versions of mercurial don't have this
+ if hasattr(repo, 'branchtip'):
+ return repo.branchtip(branch)
+ else:
+ return repo.branchtags()[branch]
+
+def get_branch_tip(repo, branch):
+ global branches
+
+ heads = branches.get(hgref(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)
+ return branch_tip(repo, hgref(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
+
+ head = gitref(head)
+ print "@refs/heads/%s HEAD" % head
+ g_head = (head, node)
+
+def do_list(parser):
+ global branches, bmarks, 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" % gitref(branch)
+
+ for bmark in bmarks:
+ print "? refs/heads/%s" % gitref(bmark)
+
+ for tag, node in repo.tagslist():
+ if tag == 'tip':
+ continue
+ print "? refs/tags/%s" % gitref(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()
+
+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, 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')
+
+ # fast-export adds an extra newline
+ if data[-1] == '\n':
+ data = data[:-1]
+
+ 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(' ', 1)
+ 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)
+
+ # Check if the ref is supposed to be a named branch
+ if ref.startswith('refs/heads/branches/'):
+ branch = ref[len('refs/heads/branches/'):]
+ extra['branch'] = hgref(branch)
+
+ 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(' : ', 1) 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):
+ global parsed_refs
+
+ 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()
+
+ parsed_tags[name] = (tagger, data)
+
+def write_tag(repo, tag, node, msg, author):
+ branch = repo[node].branch()
+ tip = branch_tip(repo, branch)
+ tip = repo[tip]
+
+ def getfilectx(repo, memctx, f):
+ try:
+ fctx = tip.filectx(f)
+ data = fctx.data()
+ except error.ManifestLookupError:
+ data = ""
+ content = data + "%s %s\n" % (hghex(node), tag)
+ return context.memfilectx(f, content, False, False, None)
+
+ p1 = tip.hex()
+ p2 = '\0' * 20
+ if not author:
+ author = (None, 0, 0)
+ user, date, tz = author
+
+ ctx = context.memctx(repo, (p1, p2), msg,
+ ['.hgtags'], getfilectx,
+ user, (date, tz), {'branch' : branch})
+
+ tmp = encoding.encoding
+ encoding.encoding = 'utf-8'
+
+ tagnode = repo.commitctx(ctx)
+
+ encoding.encoding = tmp
+
+ return tagnode
+
+def do_export(parser):
+ global parsed_refs, bmarks, peer
+
+ p_bmarks = []
+
+ 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'):
+ branch = ref[len('refs/heads/branches/'):]
+ if branch in branches and node in branches[branch]:
+ # up to date
+ continue
+ print "ok %s" % ref
+ elif ref.startswith('refs/heads/'):
+ bmark = ref[len('refs/heads/'):]
+ p_bmarks.append((bmark, node))
+ continue
+ elif ref.startswith('refs/tags/'):
+ tag = ref[len('refs/tags/'):]
+ tag = hgref(tag)
+ author, msg = parsed_tags.get(tag, (None, None))
+ if mode == 'git':
+ if not msg:
+ msg = 'Added tag %s for changeset %s' % (tag, hghex(node[:6]));
+ write_tag(parser.repo, tag, node, msg, author)
+ else:
+ fp = parser.repo.opener('localtags', 'a')
+ fp.write('%s %s\n' % (hghex(node), tag))
+ fp.close()
+ print "ok %s" % ref
+ else:
+ # transport-helper/fast-export bugs
+ continue
+
+ if peer:
+ parser.repo.push(peer, force=force_push, newbranch=True)
+
+ # handle bookmarks
+ for bmark, node in p_bmarks:
+ ref = 'refs/heads/' + bmark
+ new = hghex(node)
+
+ if bmark in bmarks:
+ old = bmarks[bmark].hex()
+ else:
+ old = ''
+
+ if old == new:
+ continue
+
+ if bmark == 'master' and 'master' not in parser.repo._bookmarks:
+ # fake bookmark
+ print "ok %s" % ref
+ continue
+ elif bookmarks.pushbookmark(parser.repo, bmark, old, new):
+ # updated locally
+ pass
+ else:
+ print "error %s" % ref
+ continue
+
+ if peer:
+ rb = peer.listkeys('bookmarks')
+ old = rb.get(bmark, '')
+ if not peer.pushkey('bookmarks', bmark, old, new):
+ print "error %s" % ref
+ continue
+
+ print "ok %s" % ref
+
+ print
+
+def fix_path(alias, repo, orig_url):
+ url = urlparse.urlparse(orig_url, 'file')
+ if url.scheme != 'file' or os.path.isabs(url.path):
+ return
+ abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
+ cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_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, force_push, is_tmp
+ global parsed_tags
+ global filenodes
+
+ alias = args[1]
+ url = args[2]
+ peer = None
+
+ hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
+ track_branches = get_config_bool('remote-hg.track-branches', True)
+ force_push = get_config_bool('remote-hg.force-push')
+
+ 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 = hashlib.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 = {}
+ marks = None
+ parsed_tags = {}
+ filenodes = {}
+
+ 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)
+
+ if sys.platform == 'win32':
+ import msvcrt
+ msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+
+ 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()
+
+def bye():
+ if not marks:
+ return
+ if not is_tmp:
+ marks.store()
+ else:
+ shutil.rmtree(dirname)
+
+atexit.register(bye)
+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 0000000..5dfa070
--- /dev/null
+++ b/contrib/remote-helpers/test-bzr.sh
@@ -0,0 +1,341 @@
+#!/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
+
+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) &&
+
+ printf content > expected &&
+ test_cmp expected actual
+'
+
+cat > expected <<EOF
+100644 blob 54f9d6da5c91d556e6b54340b1327573073030af content
+100755 blob 68769579c3eaadbe555379b9c3538e6628bae1eb executable
+120000 blob 6b584e8ece562ebffc15d38808cd6b98fc3d97ea link
+040000 tree 35c0caa46693cef62247ac89a680f0c5ce32b37b movedir-new
+EOF
+
+test_expect_success 'moving directory' '
+ (cd bzrrepo &&
+ mkdir movedir &&
+ echo one > movedir/one &&
+ echo two > movedir/two &&
+ bzr add movedir &&
+ bzr commit -m movedir &&
+ bzr mv movedir movedir-new &&
+ bzr commit -m movedir-new) &&
+
+ (cd gitrepo &&
+ git pull &&
+ git ls-tree HEAD > ../actual) &&
+
+ test_cmp expected actual
+'
+
+test_expect_success 'different authors' '
+ (cd bzrrepo &&
+ echo john >> content &&
+ bzr commit -m john \
+ --author "Jane Rey <jrey@example.com>" \
+ --author "John Doe <jdoe@example.com>") &&
+
+ (cd gitrepo &&
+ git pull &&
+ git show --format="%an <%ae>, %cn <%ce>" --quiet > ../actual) &&
+
+ echo "Jane Rey <jrey@example.com>, A U Thor <author@example.com>" > expected &&
+ test_cmp expected actual
+'
+
+test_expect_success 'fetch utf-8 filenames' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp && LC_ALL=C" &&
+
+ LC_ALL=en_US.UTF-8
+ export LC_ALL
+ (
+ bzr init bzrrepo &&
+ cd bzrrepo &&
+
+ echo test >> "ærø" &&
+ bzr add "ærø" &&
+ echo test >> "ø~?" &&
+ bzr add "ø~?" &&
+ bzr commit -m add-utf-8 &&
+ echo test >> "ærø" &&
+ bzr commit -m test-utf-8 &&
+ bzr rm "ø~?" &&
+ bzr mv "ærø" "ø~?" &&
+ bzr commit -m bzr-mv-utf-8
+ ) &&
+
+ (
+ git clone "bzr::$PWD/bzrrepo" gitrepo &&
+ cd gitrepo &&
+ git -c core.quotepath=false ls-files > ../actual
+ ) &&
+ echo "ø~?" > expected &&
+ test_cmp expected actual
+'
+
+test_expect_success 'push utf-8 filenames' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp && LC_ALL=C" &&
+
+ LC_ALL=en_US.UTF-8
+ export LC_ALL
+
+ (
+ bzr init bzrrepo &&
+ cd bzrrepo &&
+
+ echo one >> content &&
+ bzr add content &&
+ bzr commit -m one
+ ) &&
+
+ (
+ git clone "bzr::$PWD/bzrrepo" gitrepo &&
+ cd gitrepo &&
+
+ echo test >> "ærø" &&
+ git add "ærø" &&
+ git commit -m utf-8 &&
+
+ git push
+ ) &&
+
+ (cd bzrrepo && bzr ls > ../actual) &&
+ printf "content\nærø\n" > expected &&
+ test_cmp expected actual
+'
+
+test_expect_success 'pushing a merge' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ bzr init bzrrepo &&
+ cd bzrrepo &&
+ echo one > content &&
+ bzr add content &&
+ bzr commit -m one
+ ) &&
+
+ git clone "bzr::$PWD/bzrrepo" gitrepo &&
+
+ (
+ cd bzrrepo &&
+ echo two > content &&
+ bzr commit -m two
+ ) &&
+
+ (
+ cd gitrepo &&
+ echo three > content &&
+ git commit -a -m three &&
+ git fetch &&
+ git merge origin/master || true &&
+ echo three > content &&
+ git commit -a --no-edit &&
+ git push
+ ) &&
+
+ echo three > expected &&
+ cat bzrrepo/content > actual &&
+ test_cmp expected actual
+'
+
+cat > expected <<EOF
+origin/HEAD
+origin/branch
+origin/trunk
+EOF
+
+test_expect_success 'proper bzr repo' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ bzr init-repo bzrrepo &&
+
+ bzr init bzrrepo/trunk &&
+ (
+ cd bzrrepo/trunk &&
+ echo one >> content &&
+ bzr add content &&
+ bzr commit -m one
+ ) &&
+
+ bzr branch bzrrepo/trunk bzrrepo/branch &&
+ (
+ cd bzrrepo/branch &&
+ echo two >> content &&
+ bzr commit -m one
+ ) &&
+
+ git clone "bzr::$PWD/bzrrepo" gitrepo &&
+ (
+ cd gitrepo &&
+ git for-each-ref --format "%(refname:short)" refs/remotes/origin > ../actual
+ ) &&
+
+ test_cmp ../expected actual
+'
+
+test_expect_success 'strip' '
+ # Do not imitate this style; always chdir inside a subshell instead
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ bzr init bzrrepo &&
+ cd bzrrepo &&
+
+ echo one >> content &&
+ bzr add content &&
+ bzr commit -m one &&
+
+ echo two >> content &&
+ bzr commit -m two
+ ) &&
+
+ git clone "bzr::$PWD/bzrrepo" gitrepo &&
+
+ (
+ cd bzrrepo &&
+ bzr uncommit --force &&
+
+ echo three >> content &&
+ bzr commit -m three &&
+
+ echo four >> content &&
+ bzr commit -m four &&
+ bzr log --line | sed -e "s/^[0-9][0-9]*: //" > ../expected
+ ) &&
+
+ (cd gitrepo &&
+ git fetch &&
+ git log --format="%an %ad %s" --date=short origin/master > ../actual) &&
+
+ 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 0000000..f569697
--- /dev/null
+++ b/contrib/remote-helpers/test-hg-bidi.sh
@@ -0,0 +1,247 @@
+#!/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 () {
+ git clone -q "hg::$PWD/$1" $2
+}
+
+# clone to an hg repo
+hg_clone () {
+ (
+ hg init $2 &&
+ hg -R $2 bookmark -i master &&
+ 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 >log &&
+ grep -v 'tag: *default/' log
+}
+
+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 "graphlog ="
+ ) >> "$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 &&
+
+ : Back to the common revision &&
+ (cd hgrepo && hg checkout default) &&
+
+ 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 0000000..7f579c8
--- /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 () {
+ git clone -q "hg::$PWD/$1" $2
+}
+
+# clone to an hg repo with git
+hg_clone_git () {
+ (
+ hg init $2 &&
+ hg -R $2 bookmark -i master &&
+ 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 -i -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 >log &&
+ grep -v 'tag: *default/' log
+}
+
+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 ="
+ echo "graphlog ="
+ ) >> "$HOME"/.hgrc &&
+ git config --global receive.denycurrentbranch warn
+ git config --global remote-hg.hg-git-compat true
+ git config --global remote-hg.track-branches false
+
+ 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 &&
+
+ 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 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 0000000..8de2aa7
--- /dev/null
+++ b/contrib/remote-helpers/test-hg.sh
@@ -0,0 +1,157 @@
+#!/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:"
+'
+
+author_test () {
+ echo $1 >> content &&
+ hg commit -u "$2" -m "add $1" &&
+ echo "$3" >> ../expected
+}
+
+test_expect_success 'authors' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ hg init hgrepo &&
+ cd hgrepo &&
+
+ touch content &&
+ hg add content &&
+
+ author_test alpha "" "H G Wells <wells@example.com>" &&
+ author_test beta "test" "test <unknown>" &&
+ author_test beta "test <test@example.com> (comment)" "test <test@example.com>" &&
+ author_test gamma "<test@example.com>" "Unknown <test@example.com>" &&
+ author_test delta "name<test@example.com>" "name <test@example.com>" &&
+ author_test epsilon "name <test@example.com" "name <test@example.com>" &&
+ author_test zeta " test " "test <unknown>" &&
+ author_test eta "test < test@example.com >" "test <test@example.com>" &&
+ author_test theta "test >test@example.com>" "test <test@example.com>" &&
+ author_test iota "test < test <at> example <dot> com>" "test <unknown>" &&
+ author_test kappa "test@example.com" "Unknown <test@example.com>"
+ ) &&
+
+ git clone "hg::$PWD/hgrepo" gitrepo &&
+ git --git-dir=gitrepo/.git log --reverse --format="%an <%ae>" > actual &&
+
+ test_cmp expected actual
+'
+
+test_done
diff --git a/contrib/stats/mailmap.pl b/contrib/stats/mailmap.pl
index 4b852e2..9513f5e 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 7e77c9d..91360a3 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 05cdd5c..b507505 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 920c664..8a23f58 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 0c44fda..7ba853e 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 bc2eeb0..b0f8536 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 35db24f..f363505 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 0000000..4e78a1c
--- /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 fca1e17..8f16d06 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