summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rw-r--r--contrib/completion/git-completion.bash162
-rw-r--r--contrib/completion/git-completion.tcsh107
-rw-r--r--contrib/completion/git-completion.zsh78
-rw-r--r--contrib/completion/git-prompt.sh119
-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/wincred/Makefile14
-rw-r--r--contrib/credential/wincred/git-credential-wincred.c357
-rw-r--r--contrib/emacs/git.el2
-rw-r--r--contrib/examples/builtin-fetch--tool.c2
-rwxr-xr-xcontrib/mw-to-git/git-remote-mediawiki16
-rwxr-xr-xcontrib/mw-to-git/t/t9362-mw-to-git-utf8.sh26
-rw-r--r--contrib/remote-helpers/Makefile13
-rwxr-xr-xcontrib/remote-helpers/git-remote-hg794
-rwxr-xr-xcontrib/remote-helpers/test-hg-bidi.sh243
-rwxr-xr-xcontrib/remote-helpers/test-hg-hg-git.sh466
-rwxr-xr-xcontrib/remote-helpers/test-hg.sh121
-rwxr-xr-xcontrib/stats/mailmap.pl96
-rw-r--r--contrib/subtree/.gitignore1
-rw-r--r--contrib/subtree/git-subtree.txt2
-rw-r--r--contrib/svn-fe/svn-fe.c3
-rwxr-xr-xcontrib/svn-fe/svnrdump_sim.py53
23 files changed, 3035 insertions, 110 deletions
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 5b255cba69..a4c48e179e 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -23,10 +23,6 @@
# 3) Consider changing your PS1 to also show the current branch,
# see git-prompt.sh for details.
-if [[ -n ${ZSH_VERSION-} ]]; then
- autoload -U +X bashcompinit && bashcompinit
-fi
-
case "$COMP_WORDBREAKS" in
*:*) : great ;;
*) COMP_WORDBREAKS="$COMP_WORDBREAKS:"
@@ -169,7 +165,6 @@ __git_reassemble_comp_words_by_ref()
}
if ! type _get_comp_words_by_ref >/dev/null 2>&1; then
-if [[ -z ${ZSH_VERSION:+set} ]]; then
_get_comp_words_by_ref ()
{
local exclude cur_ words_ cword_
@@ -197,32 +192,6 @@ _get_comp_words_by_ref ()
shift
done
}
-else
-_get_comp_words_by_ref ()
-{
- while [ $# -gt 0 ]; do
- case "$1" in
- cur)
- cur=${COMP_WORDS[COMP_CWORD]}
- ;;
- prev)
- prev=${COMP_WORDS[COMP_CWORD-1]}
- ;;
- words)
- words=("${COMP_WORDS[@]}")
- ;;
- cword)
- cword=$COMP_CWORD
- ;;
- -n)
- # assume COMP_WORDBREAKS is already set sanely
- shift
- ;;
- esac
- shift
- done
-}
-fi
fi
# Generates completion reply with compgen, appending a space to possible
@@ -321,7 +290,7 @@ __git_refs ()
if [[ "$ref" == "$cur"* ]]; then
echo "$ref"
fi
- done | uniq -u
+ done | sort | uniq -u
fi
return
fi
@@ -585,7 +554,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;;
@@ -875,11 +844,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
"
;;
*)
@@ -985,6 +958,8 @@ _git_clone ()
--upload-pack
--template=
--depth
+ --single-branch
+ --branch
"
return
;;
@@ -996,6 +971,13 @@ _git_commit ()
{
__git_has_doubledash && return
+ case "$prev" in
+ -c|-C)
+ __gitcomp_nl "$(__git_refs)" "" "${cur}"
+ return
+ ;;
+ esac
+
case "$cur" in
--cleanup=*)
__gitcomp "default strip verbatim whitespace
@@ -1072,7 +1054,7 @@ _git_diff ()
}
__git_mergetools_common="diffuse ecmerge emerge kdiff3 meld opendiff
- tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3
+ tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3 codecompare
"
_git_difftool ()
@@ -1112,6 +1094,14 @@ _git_fetch ()
__git_complete_remote_or_refspec
}
+__git_format_patch_options="
+ --stdout --attach --no-attach --thread --thread= --output-directory
+ --numbered --start-number --numbered-files --keep-subject --signoff
+ --signature --no-signature --in-reply-to= --cc= --full-index --binary
+ --not --all --cover-letter --no-prefix --src-prefix= --dst-prefix=
+ --inline --suffix= --ignore-if-in-upstream --subject-prefix=
+"
+
_git_format_patch ()
{
case "$cur" in
@@ -1122,21 +1112,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
@@ -1550,6 +1526,12 @@ _git_send_email ()
__gitcomp "ssl tls" "" "${cur##--smtp-encryption=}"
return
;;
+ --thread=*)
+ __gitcomp "
+ deep shallow
+ " "" "${cur##--thread=}"
+ return
+ ;;
--*)
__gitcomp "--annotate --bcc --cc --cc-cmd --chain-reply-to
--compose --confirm= --dry-run --envelope-sender
@@ -1559,11 +1541,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 ()
@@ -2425,20 +2408,71 @@ __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
- fi
+ __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
+ }
+
+ __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
+fi
+
+__git_func_wrap ()
+{
local cur words cword prev
_get_comp_words_by_ref -n =: cur words cword prev
$1
diff --git a/contrib/completion/git-completion.tcsh b/contrib/completion/git-completion.tcsh
new file mode 100644
index 0000000000..8aafb63315
--- /dev/null
+++ b/contrib/completion/git-completion.tcsh
@@ -0,0 +1,107 @@
+#!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:
+#
+# 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_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}
+
+# 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[*]} -gt 0 ]; then
+ echo "\${COMPREPLY[*]}" | sort | uniq
+else
+ # 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.
+ RESULT=(\`ls -dp \${TO_COMPLETE}* 2> /dev/null\`)
+ echo \${RESULT[*]}
+
+ # 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 [ \${#RESULT[*]} -eq 1 ] && [ "\${RESULT[0]: -1}" == "/" ]; then
+ echo \${RESULT[*]}
+ fi
+ fi
+ fi
+fi
+
+EOF
+
+# Don't need this variable anymore, so don't pollute the users environment
+unset __git_tcsh_completion_original_script
+
+complete git 'p,*,`bash ${__git_tcsh_completion_script} git "${COMMAND_LINE}"`,'
+complete gitk 'p,*,`bash ${__git_tcsh_completion_script} gitk "${COMMAND_LINE}"`,'
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
new file mode 100644
index 0000000000..45775021ff
--- /dev/null
+++ b/contrib/completion/git-completion.zsh
@@ -0,0 +1,78 @@
+#compdef git gitk
+
+# zsh completion wrapper for git
+#
+# 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 -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
+}
+
+_git ()
+{
+ local _ret=1
+ () {
+ emulate -L ksh
+ local cur cword prev
+ cur=${words[CURRENT-1]}
+ prev=${words[CURRENT-2]}
+ let cword=CURRENT-1
+ __${service}_main
+ }
+ let _ret && _default -S '' && _ret=0
+ return _ret
+}
+
+_git
diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh
index bf20491ec3..9bef0531c5 100644
--- a/contrib/completion/git-prompt.sh
+++ b/contrib/completion/git-prompt.sh
@@ -10,9 +10,22 @@
# 1) Copy this file to somewhere (e.g. ~/.git-prompt.sh).
# 2) Add the following line to your .bashrc/.zshrc:
# source ~/.git-prompt.sh
-# 3) Change your PS1 to also show the current branch:
-# Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
-# ZSH: PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ '
+# 3a) Change your PS1 to call __git_ps1 as
+# command-substitution:
+# Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
+# ZSH: PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ '
+# the optional argument will be used as format string.
+# 3b) Alternatively, if you are using bash, __git_ps1 can be
+# used for PROMPT_COMMAND with two parameters, <pre> and
+# <post>, which are strings you would put in $PS1 before
+# and after the status string generated by the git-prompt
+# machinery. e.g.
+# PROMPT_COMMAND='__git_ps1 "\u@\h:\w" "\\\$ "'
+# will show username, at-sign, host, colon, cwd, then
+# various status string, followed by dollar and SP, as
+# your prompt.
+# Optionally, you can supply a third argument with a printf
+# format string to finetune the output of the branch status
#
# The argument to __git_ps1 will be displayed only if you are currently
# in a git repository. The %s token will be the name of the current
@@ -49,6 +62,19 @@
# find one, or @{upstream} otherwise. Once you have set
# GIT_PS1_SHOWUPSTREAM, you can override it on a per-repository basis by
# setting the bash.showUpstream config variable.
+#
+# If you would like to see more information about the identity of
+# commits checked out as a detached HEAD, set GIT_PS1_DESCRIBE_STYLE
+# to one of these values:
+#
+# contains relative to newer annotated tag (v1.6.3.2~35)
+# branch relative to newer tag or branch (master~4)
+# describe relative to older annotated tag (v1.6.3.1-13-gdd42c2f)
+# default exactly matching tag
+#
+# If you would like a colored hint about the current dirty state, set
+# GIT_PS1_SHOWCOLORHINTS to a nonempty value. The colors are based on
+# the colored output of "git status -sb".
# __gitdir accepts 0 or 1 arguments (i.e., location)
# returns location of .git repo
@@ -195,11 +221,43 @@ __git_ps1_show_upstream ()
# __git_ps1 accepts 0 or 1 arguments (i.e., format string)
-# returns text to add to bash PS1 prompt (includes branch name)
+# when called from PS1 using command substitution
+# in this mode it prints text to add to bash PS1 prompt (includes branch name)
+#
+# __git_ps1 requires 2 or 3 arguments when called from PROMPT_COMMAND (pc)
+# in that case it _sets_ PS1. The arguments are parts of a PS1 string.
+# when two arguments are given, the first is prepended and the second appended
+# to the state string when assigned to PS1.
+# The optional third parameter will be used as printf format string to further
+# customize the output of the git-status string.
+# In this mode you can request colored hints using GIT_PS1_SHOWCOLORHINTS=true
__git_ps1 ()
{
+ local pcmode=no
+ local detached=no
+ local ps1pc_start='\u@\h:\w '
+ local ps1pc_end='\$ '
+ local printf_format=' (%s)'
+
+ case "$#" in
+ 2|3) pcmode=yes
+ ps1pc_start="$1"
+ ps1pc_end="$2"
+ printf_format="${3:-$printf_format}"
+ ;;
+ 0|1) printf_format="${1:-$printf_format}"
+ ;;
+ *) return
+ ;;
+ esac
+
local g="$(__gitdir)"
- if [ -n "$g" ]; then
+ if [ -z "$g" ]; then
+ if [ $pcmode = yes ]; then
+ #In PC mode PS1 always needs to be set
+ PS1="$ps1pc_start$ps1pc_end"
+ fi
+ else
local r=""
local b=""
if [ -f "$g/rebase-merge/interactive" ]; then
@@ -226,7 +284,7 @@ __git_ps1 ()
fi
b="$(git symbolic-ref HEAD 2>/dev/null)" || {
-
+ detached=yes
b="$(
case "${GIT_PS1_DESCRIBE_STYLE-}" in
(contains)
@@ -285,6 +343,53 @@ __git_ps1 ()
fi
local f="$w$i$s$u"
- printf -- "${1:- (%s)}" "$c${b##refs/heads/}${f:+ $f}$r$p"
+ if [ $pcmode = yes ]; then
+ local gitstring=
+ if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then
+ local c_red='\e[31m'
+ local c_green='\e[32m'
+ local c_lblue='\e[1;34m'
+ local c_clear='\e[0m'
+ local bad_color=$c_red
+ local ok_color=$c_green
+ local branch_color="$c_clear"
+ local flags_color="$c_lblue"
+ local branchstring="$c${b##refs/heads/}"
+
+ if [ $detached = no ]; then
+ branch_color="$ok_color"
+ else
+ branch_color="$bad_color"
+ fi
+
+ # Setting gitstring directly with \[ and \] around colors
+ # is necessary to prevent wrapping issues!
+ gitstring="\[$branch_color\]$branchstring\[$c_clear\]"
+
+ if [ -n "$w$i$s$u$r$p" ]; then
+ gitstring="$gitstring "
+ fi
+ if [ "$w" = "*" ]; then
+ gitstring="$gitstring\[$bad_color\]$w"
+ fi
+ if [ -n "$i" ]; then
+ gitstring="$gitstring\[$ok_color\]$i"
+ fi
+ if [ -n "$s" ]; then
+ gitstring="$gitstring\[$flags_color\]$s"
+ fi
+ if [ -n "$u" ]; then
+ gitstring="$gitstring\[$bad_color\]$u"
+ fi
+ gitstring="$gitstring\[$c_clear\]$r$p"
+ else
+ gitstring="$c${b##refs/heads/}${f:+ $f}$r$p"
+ fi
+ gitstring=$(printf -- "$printf_format" "$gitstring")
+ PS1="$ps1pc_start$gitstring$ps1pc_end"
+ else
+ # NO color option unless in PROMPT_COMMAND mode
+ printf -- "$printf_format" "$c${b##refs/heads/}${f:+ $f}$r$p"
+ fi
fi
}
diff --git a/contrib/credential/gnome-keyring/.gitignore b/contrib/credential/gnome-keyring/.gitignore
new file mode 100644
index 0000000000..88d8fcdbce
--- /dev/null
+++ b/contrib/credential/gnome-keyring/.gitignore
@@ -0,0 +1 @@
+git-credential-gnome-keyring
diff --git a/contrib/credential/gnome-keyring/Makefile b/contrib/credential/gnome-keyring/Makefile
new file mode 100644
index 0000000000..e6561d8db6
--- /dev/null
+++ b/contrib/credential/gnome-keyring/Makefile
@@ -0,0 +1,24 @@
+MAIN:=git-credential-gnome-keyring
+all:: $(MAIN)
+
+CC = gcc
+RM = rm -f
+CFLAGS = -g -O2 -Wall
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+INCS:=$(shell pkg-config --cflags gnome-keyring-1)
+LIBS:=$(shell pkg-config --libs gnome-keyring-1)
+
+SRCS:=$(MAIN).c
+OBJS:=$(SRCS:.c=.o)
+
+%.o: %.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) $(INCS) -o $@ -c $<
+
+$(MAIN): $(OBJS)
+ $(CC) -o $@ $(LDFLAGS) $^ $(LIBS)
+
+clean:
+ @$(RM) $(MAIN) $(OBJS)
diff --git a/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c b/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c
new file mode 100644
index 0000000000..41f61c5db3
--- /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/wincred/Makefile b/contrib/credential/wincred/Makefile
new file mode 100644
index 0000000000..bad45ca47a
--- /dev/null
+++ b/contrib/credential/wincred/Makefile
@@ -0,0 +1,14 @@
+all: git-credential-wincred.exe
+
+CC = gcc
+RM = rm -f
+CFLAGS = -O2 -Wall
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+git-credential-wincred.exe : git-credential-wincred.c
+ $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
+
+clean:
+ $(RM) git-credential-wincred.exe
diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c
new file mode 100644
index 0000000000..cbaec5f24b
--- /dev/null
+++ b/contrib/credential/wincred/git-credential-wincred.c
@@ -0,0 +1,357 @@
+/*
+ * A git credential helper that interface with Windows' Credential Manager
+ *
+ */
+#include <windows.h>
+#include <stdio.h>
+#include <io.h>
+#include <fcntl.h>
+
+/* common helpers */
+
+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;
+}
+
+static char *xstrdup(const char *str)
+{
+ char *ret = strdup(str);
+ 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 *CredUnPackAuthenticationBufferWT)(DWORD, PVOID, DWORD,
+ LPWSTR, DWORD *, LPWSTR, DWORD *, LPWSTR, DWORD *);
+typedef BOOL (WINAPI *CredEnumerateWT)(LPCWSTR, DWORD, DWORD *,
+ PCREDENTIALW **);
+typedef BOOL (WINAPI *CredPackAuthenticationBufferWT)(DWORD, LPWSTR, LPWSTR,
+ PBYTE, DWORD *);
+typedef VOID (WINAPI *CredFreeT)(PVOID);
+typedef BOOL (WINAPI *CredDeleteWT)(LPCWSTR, DWORD, DWORD);
+
+static HMODULE advapi, credui;
+static CredWriteWT CredWriteW;
+static CredUnPackAuthenticationBufferWT CredUnPackAuthenticationBufferW;
+static CredEnumerateWT CredEnumerateW;
+static CredPackAuthenticationBufferWT CredPackAuthenticationBufferW;
+static CredFreeT CredFree;
+static CredDeleteWT CredDeleteW;
+
+static void load_cred_funcs(void)
+{
+ /* load DLLs */
+ advapi = LoadLibrary("advapi32.dll");
+ credui = LoadLibrary("credui.dll");
+ if (!advapi || !credui)
+ die("failed to load DLLs");
+
+ /* get function pointers */
+ CredWriteW = (CredWriteWT)GetProcAddress(advapi, "CredWriteW");
+ CredUnPackAuthenticationBufferW = (CredUnPackAuthenticationBufferWT)
+ GetProcAddress(credui, "CredUnPackAuthenticationBufferW");
+ CredEnumerateW = (CredEnumerateWT)GetProcAddress(advapi,
+ "CredEnumerateW");
+ CredPackAuthenticationBufferW = (CredPackAuthenticationBufferWT)
+ GetProcAddress(credui, "CredPackAuthenticationBufferW");
+ CredFree = (CredFreeT)GetProcAddress(advapi, "CredFree");
+ CredDeleteW = (CredDeleteWT)GetProcAddress(advapi, "CredDeleteW");
+ if (!CredWriteW || !CredUnPackAuthenticationBufferW ||
+ !CredEnumerateW || !CredPackAuthenticationBufferW || !CredFree ||
+ !CredDeleteW)
+ die("failed to load functions");
+}
+
+static char target_buf[1024];
+static char *protocol, *host, *path, *username;
+static WCHAR *wusername, *password, *target;
+
+static void write_item(const char *what, WCHAR *wbuf)
+{
+ char *buf;
+ int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, NULL, 0, NULL,
+ FALSE);
+ buf = xmalloc(len);
+
+ if (!WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, buf, len, NULL, FALSE))
+ die("WideCharToMultiByte failed!");
+
+ printf("%s=", what);
+ fwrite(buf, 1, len - 1, stdout);
+ putchar('\n');
+ free(buf);
+}
+
+static int match_attr(const CREDENTIALW *cred, const WCHAR *keyword,
+ const char *want)
+{
+ int i;
+ if (!want)
+ return 1;
+
+ for (i = 0; i < cred->AttributeCount; ++i)
+ if (!wcscmp(cred->Attributes[i].Keyword, keyword))
+ return !strcmp((const char *)cred->Attributes[i].Value,
+ want);
+
+ return 0; /* not found */
+}
+
+static int match_cred(const CREDENTIALW *cred)
+{
+ return (!wusername || !wcscmp(wusername, cred->UserName)) &&
+ match_attr(cred, L"git_protocol", protocol) &&
+ match_attr(cred, L"git_host", host) &&
+ match_attr(cred, L"git_path", path);
+}
+
+static void get_credential(void)
+{
+ WCHAR *user_buf, *pass_buf;
+ DWORD user_buf_size = 0, pass_buf_size = 0;
+ CREDENTIALW **creds, *cred = NULL;
+ 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])) {
+ cred = creds[i];
+ break;
+ }
+ if (!cred)
+ return;
+
+ CredUnPackAuthenticationBufferW(0, cred->CredentialBlob,
+ cred->CredentialBlobSize, NULL, &user_buf_size, NULL, NULL,
+ NULL, &pass_buf_size);
+
+ user_buf = xmalloc(user_buf_size * sizeof(WCHAR));
+ pass_buf = xmalloc(pass_buf_size * sizeof(WCHAR));
+
+ if (!CredUnPackAuthenticationBufferW(0, cred->CredentialBlob,
+ cred->CredentialBlobSize, user_buf, &user_buf_size, NULL, NULL,
+ pass_buf, &pass_buf_size))
+ die("CredUnPackAuthenticationBuffer failed");
+
+ CredFree(creds);
+
+ /* zero-terminate (sizes include zero-termination) */
+ user_buf[user_buf_size - 1] = L'\0';
+ pass_buf[pass_buf_size - 1] = L'\0';
+
+ write_item("username", user_buf);
+ write_item("password", pass_buf);
+
+ free(user_buf);
+ free(pass_buf);
+}
+
+static void write_attr(CREDENTIAL_ATTRIBUTEW *attr, const WCHAR *keyword,
+ const char *value)
+{
+ attr->Keyword = (LPWSTR)keyword;
+ attr->Flags = 0;
+ attr->ValueSize = strlen(value) + 1; /* store zero-termination */
+ attr->Value = (LPBYTE)value;
+}
+
+static void store_credential(void)
+{
+ CREDENTIALW cred;
+ BYTE *auth_buf;
+ DWORD auth_buf_size = 0;
+ CREDENTIAL_ATTRIBUTEW attrs[CRED_MAX_ATTRIBUTES];
+
+ if (!wusername || !password)
+ return;
+
+ /* query buffer size */
+ CredPackAuthenticationBufferW(0, wusername, password,
+ NULL, &auth_buf_size);
+
+ auth_buf = xmalloc(auth_buf_size);
+
+ if (!CredPackAuthenticationBufferW(0, wusername, password,
+ auth_buf, &auth_buf_size))
+ die("CredPackAuthenticationBuffer failed");
+
+ cred.Flags = 0;
+ cred.Type = CRED_TYPE_GENERIC;
+ cred.TargetName = target;
+ cred.Comment = L"saved by git-credential-wincred";
+ cred.CredentialBlobSize = auth_buf_size;
+ cred.CredentialBlob = auth_buf;
+ cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
+ cred.AttributeCount = 1;
+ cred.Attributes = attrs;
+ cred.TargetAlias = NULL;
+ cred.UserName = wusername;
+
+ write_attr(attrs, L"git_protocol", protocol);
+
+ if (host) {
+ write_attr(attrs + cred.AttributeCount, L"git_host", host);
+ cred.AttributeCount++;
+ }
+
+ if (path) {
+ write_attr(attrs + cred.AttributeCount, L"git_path", path);
+ cred.AttributeCount++;
+ }
+
+ 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;
+
+ if (!strcmp(buf, "\n"))
+ break;
+ buf[strlen(buf)-1] = '\0';
+
+ v = strchr(buf, '=');
+ if (!v)
+ die("bad input: %s", buf);
+ *v++ = '\0';
+
+ if (!strcmp(buf, "protocol"))
+ protocol = xstrdup(v);
+ else if (!strcmp(buf, "host"))
+ host = xstrdup(v);
+ else if (!strcmp(buf, "path"))
+ path = xstrdup(v);
+ else if (!strcmp(buf, "username")) {
+ username = xstrdup(v);
+ 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 */
+ strncat(target_buf, "git:", sizeof(target_buf));
+ strncat(target_buf, protocol, sizeof(target_buf));
+ strncat(target_buf, "://", sizeof(target_buf));
+ if (username) {
+ strncat(target_buf, username, sizeof(target_buf));
+ strncat(target_buf, "@", sizeof(target_buf));
+ }
+ if (host)
+ strncat(target_buf, host, sizeof(target_buf));
+ if (path) {
+ strncat(target_buf, "/", sizeof(target_buf));
+ strncat(target_buf, path, sizeof(target_buf));
+ }
+
+ target = utf8_to_utf16_dup(target_buf);
+
+ if (!strcmp(argv[1], "get"))
+ get_credential();
+ else if (!strcmp(argv[1], "store"))
+ store_credential();
+ else if (!strcmp(argv[1], "erase"))
+ erase_credential();
+ /* otherwise, ignore unknown action */
+ return 0;
+}
diff --git a/contrib/emacs/git.el b/contrib/emacs/git.el
index 65c95d9d5a..5ffc506f6d 100644
--- a/contrib/emacs/git.el
+++ b/contrib/emacs/git.el
@@ -1671,7 +1671,7 @@ Commands:
"Entry point into git-status mode."
(interactive "DSelect directory: ")
(setq dir (git-get-top-dir dir))
- (if (file-directory-p (concat (file-name-as-directory dir) ".git"))
+ (if (file-exists-p (concat (file-name-as-directory dir) ".git"))
(let ((buffer (or (and git-reuse-status-buffer (git-find-status-buffer dir))
(create-file-buffer (expand-file-name "*git-status*" dir)))))
(switch-to-buffer buffer)
diff --git a/contrib/examples/builtin-fetch--tool.c b/contrib/examples/builtin-fetch--tool.c
index 0d54aa7061..8bc8c7533a 100644
--- a/contrib/examples/builtin-fetch--tool.c
+++ b/contrib/examples/builtin-fetch--tool.c
@@ -96,7 +96,7 @@ static int update_local_ref(const char *name,
strcpy(oldh, find_unique_abbrev(current->object.sha1, DEFAULT_ABBREV));
strcpy(newh, find_unique_abbrev(sha1_new, DEFAULT_ABBREV));
- if (in_merge_bases(current, &updated, 1)) {
+ if (in_merge_bases(current, updated)) {
fprintf(stderr, "* %s: fast-forward to %s\n",
name, note);
fprintf(stderr, " old..new: %s..%s\n", oldh, newh);
diff --git a/contrib/mw-to-git/git-remote-mediawiki b/contrib/mw-to-git/git-remote-mediawiki
index 68555d4265..094129de09 100755
--- a/contrib/mw-to-git/git-remote-mediawiki
+++ b/contrib/mw-to-git/git-remote-mediawiki
@@ -711,6 +711,14 @@ sub fetch_mw_revisions {
return ($n, @revisions);
}
+sub fe_escape_path {
+ my $path = shift;
+ $path =~ s/\\/\\\\/g;
+ $path =~ s/"/\\"/g;
+ $path =~ s/\n/\\n/g;
+ return '"' . $path . '"';
+}
+
sub import_file_revision {
my $commit = shift;
my %commit = %{$commit};
@@ -738,15 +746,17 @@ sub import_file_revision {
print STDOUT "from refs/mediawiki/$remotename/master^0\n";
}
if ($content ne DELETED_CONTENT) {
- print STDOUT "M 644 inline $title.mw\n";
+ print STDOUT "M 644 inline " .
+ fe_escape_path($title . ".mw") . "\n";
literal_data($content);
if (%mediafile) {
- print STDOUT "M 644 inline $mediafile{title}\n";
+ print STDOUT "M 644 inline "
+ . fe_escape_path($mediafile{title}) . "\n";
literal_data_raw($mediafile{content});
}
print STDOUT "\n\n";
} else {
- print STDOUT "D $title.mw\n";
+ print STDOUT "D " . fe_escape_path($title . ".mw") . "\n";
}
# mediawiki revision number in the git note
diff --git a/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh b/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh
index 246d47d8fb..b6405ce262 100755
--- a/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh
+++ b/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh
@@ -318,4 +318,30 @@ test_expect_success 'git push with \ in format control' '
'
+test_expect_success 'fast-import meta-characters in page name (mw -> git)' '
+ wiki_reset &&
+ wiki_editpage \"file\"_\\_foo "expect to be called \"file\"_\\_foo" false &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_21 &&
+ test_path_is_file mw_dir_21/\"file\"_\\_foo.mw &&
+ wiki_getallpage ref_page_21 &&
+ test_diff_directories mw_dir_21 ref_page_21
+'
+
+
+test_expect_success 'fast-import meta-characters in page name (git -> mw) ' '
+ wiki_reset &&
+ git clone mediawiki::'"$WIKI_URL"' mw_dir_22 &&
+ (
+ cd mw_dir_22 &&
+ echo "this file is called \"file\"_\\_foo.mw" >\"file\"_\\_foo &&
+ git add . &&
+ git commit -am "file \"file\"_\\_foo" &&
+ git pull &&
+ git push
+ ) &&
+ wiki_getallpage ref_page_22 &&
+ test_diff_directories mw_dir_22 ref_page_22
+'
+
+
test_done
diff --git a/contrib/remote-helpers/Makefile b/contrib/remote-helpers/Makefile
new file mode 100644
index 0000000000..9a76575f78
--- /dev/null
+++ b/contrib/remote-helpers/Makefile
@@ -0,0 +1,13 @@
+TESTS := $(wildcard test*.sh)
+
+export T := $(addprefix $(CURDIR)/,$(TESTS))
+export MAKE := $(MAKE) -e
+export PATH := $(CURDIR):$(PATH)
+
+test:
+ $(MAKE) -C ../../t $@
+
+$(TESTS):
+ $(MAKE) -C ../../t $(CURDIR)/$@
+
+.PHONY: $(TESTS)
diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg
new file mode 100755
index 0000000000..c7006000a6
--- /dev/null
+++ b/contrib/remote-helpers/git-remote-hg
@@ -0,0 +1,794 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2012 Felipe Contreras
+#
+
+# Inspired by Rocco Rutte's hg-fast-export
+
+# Just copy to your ~/bin, or anywhere in your $PATH.
+# Then you can clone with:
+# git clone hg::/path/to/mercurial/repo/
+
+from mercurial import hg, ui, bookmarks, context, util, encoding
+
+import re
+import sys
+import os
+import json
+import shutil
+import subprocess
+import urllib
+
+#
+# If you want to switch to hg-git compatibility mode:
+# git config --global remote-hg.hg-git-compat true
+#
+# git:
+# Sensible defaults for git.
+# hg bookmarks are exported as git branches, hg branches are prefixed
+# with 'branches/', HEAD is a special case.
+#
+# hg:
+# Emulate hg-git.
+# Only hg bookmarks are exported as git branches.
+# Commits are modified to preserve hg information and allow bidirectionality.
+#
+
+NAME_RE = re.compile('^([^<>]+)')
+AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
+AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$')
+RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
+
+def die(msg, *args):
+ sys.stderr.write('ERROR: %s\n' % (msg % args))
+ sys.exit(1)
+
+def warn(msg, *args):
+ sys.stderr.write('WARNING: %s\n' % (msg % args))
+
+def gitmode(flags):
+ return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
+
+def gittz(tz):
+ return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
+
+def hgmode(mode):
+ m = { '0100755': 'x', '0120000': 'l' }
+ return m.get(mode, '')
+
+def get_config(config):
+ cmd = ['git', 'config', '--get', config]
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ output, _ = process.communicate()
+ return output
+
+class Marks:
+
+ def __init__(self, path):
+ self.path = path
+ self.tips = {}
+ self.marks = {}
+ self.rev_marks = {}
+ self.last_mark = 0
+
+ self.load()
+
+ def load(self):
+ if not os.path.exists(self.path):
+ return
+
+ tmp = json.load(open(self.path))
+
+ self.tips = tmp['tips']
+ self.marks = tmp['marks']
+ self.last_mark = tmp['last-mark']
+
+ for rev, mark in self.marks.iteritems():
+ self.rev_marks[mark] = int(rev)
+
+ def dict(self):
+ return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
+
+ def store(self):
+ json.dump(self.dict(), open(self.path, 'w'))
+
+ def __str__(self):
+ return str(self.dict())
+
+ def from_rev(self, rev):
+ return self.marks[str(rev)]
+
+ def to_rev(self, mark):
+ return self.rev_marks[mark]
+
+ def get_mark(self, rev):
+ self.last_mark += 1
+ self.marks[str(rev)] = self.last_mark
+ return self.last_mark
+
+ def new_mark(self, rev, mark):
+ self.marks[str(rev)] = mark
+ self.rev_marks[mark] = rev
+ self.last_mark = mark
+
+ def is_marked(self, rev):
+ return self.marks.has_key(str(rev))
+
+ def get_tip(self, branch):
+ return self.tips.get(branch, 0)
+
+ def set_tip(self, branch, tip):
+ self.tips[branch] = tip
+
+class Parser:
+
+ def __init__(self, repo):
+ self.repo = repo
+ self.line = self.get_line()
+
+ def get_line(self):
+ return sys.stdin.readline().strip()
+
+ def __getitem__(self, i):
+ return self.line.split()[i]
+
+ def check(self, word):
+ return self.line.startswith(word)
+
+ def each_block(self, separator):
+ while self.line != separator:
+ yield self.line
+ self.line = self.get_line()
+
+ def __iter__(self):
+ return self.each_block('')
+
+ def next(self):
+ self.line = self.get_line()
+ if self.line == 'done':
+ self.line = None
+
+ def get_mark(self):
+ i = self.line.index(':') + 1
+ return int(self.line[i:])
+
+ def get_data(self):
+ if not self.check('data'):
+ return None
+ i = self.line.index(' ') + 1
+ size = int(self.line[i:])
+ return sys.stdin.read(size)
+
+ def get_author(self):
+ global bad_mail
+
+ ex = None
+ m = RAW_AUTHOR_RE.match(self.line)
+ if not m:
+ return None
+ _, name, email, date, tz = m.groups()
+ if name and 'ext:' in name:
+ m = re.match('^(.+?) ext:\((.+)\)$', name)
+ if m:
+ name = m.group(1)
+ ex = urllib.unquote(m.group(2))
+
+ if email != bad_mail:
+ if name:
+ user = '%s <%s>' % (name, email)
+ else:
+ user = '<%s>' % (email)
+ else:
+ user = name
+
+ if ex:
+ user += ex
+
+ tz = int(tz)
+ tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
+ return (user, int(date), -tz)
+
+def export_file(fc):
+ d = fc.data()
+ print "M %s inline %s" % (gitmode(fc.flags()), fc.path())
+ print "data %d" % len(d)
+ print d
+
+def get_filechanges(repo, ctx, parent):
+ modified = set()
+ added = set()
+ removed = set()
+
+ cur = ctx.manifest()
+ prev = repo[parent].manifest().copy()
+
+ for fn in cur:
+ if fn in prev:
+ if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
+ modified.add(fn)
+ del prev[fn]
+ else:
+ added.add(fn)
+ removed |= set(prev.keys())
+
+ return added | modified, removed
+
+def fixup_user_git(user):
+ name = mail = None
+ user = user.replace('"', '')
+ m = AUTHOR_RE.match(user)
+ if m:
+ name = m.group(1)
+ mail = m.group(2).strip()
+ else:
+ m = NAME_RE.match(user)
+ if m:
+ name = m.group(1).strip()
+ return (name, mail)
+
+def fixup_user_hg(user):
+ def sanitize(name):
+ # stole this from hg-git
+ return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
+
+ m = AUTHOR_HG_RE.match(user)
+ if m:
+ name = sanitize(m.group(1))
+ mail = sanitize(m.group(2))
+ ex = m.group(3)
+ if ex:
+ name += ' ext:(' + urllib.quote(ex) + ')'
+ else:
+ name = sanitize(user)
+ if '@' in user:
+ mail = name
+ else:
+ mail = None
+
+ return (name, mail)
+
+def fixup_user(user):
+ global mode, bad_mail
+
+ if mode == 'git':
+ name, mail = fixup_user_git(user)
+ else:
+ name, mail = fixup_user_hg(user)
+
+ if not name:
+ name = bad_name
+ if not mail:
+ mail = bad_mail
+
+ return '%s <%s>' % (name, mail)
+
+def get_repo(url, alias):
+ global dirname, peer
+
+ myui = ui.ui()
+ myui.setconfig('ui', 'interactive', 'off')
+
+ if hg.islocal(url):
+ repo = hg.repository(myui, url)
+ else:
+ local_path = os.path.join(dirname, 'clone')
+ if not os.path.exists(local_path):
+ peer, dstpeer = hg.clone(myui, {}, url, local_path, update=False, pull=True)
+ repo = dstpeer.local()
+ else:
+ repo = hg.repository(myui, local_path)
+ peer = hg.peer(myui, {}, url)
+ repo.pull(peer, heads=None, force=True)
+
+ return repo
+
+def rev_to_mark(rev):
+ global marks
+ return marks.from_rev(rev)
+
+def mark_to_rev(mark):
+ global marks
+ return marks.to_rev(mark)
+
+def export_ref(repo, name, kind, head):
+ global prefix, marks, mode
+
+ ename = '%s/%s' % (kind, name)
+ tip = marks.get_tip(ename)
+
+ # mercurial takes too much time checking this
+ if tip and tip == head.rev():
+ # nothing to do
+ return
+ revs = xrange(tip, head.rev() + 1)
+ count = 0
+
+ revs = [rev for rev in revs if not marks.is_marked(rev)]
+
+ for rev in revs:
+
+ c = repo[rev]
+ (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node())
+ rev_branch = extra['branch']
+
+ author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
+ if 'committer' in extra:
+ user, time, tz = extra['committer'].rsplit(' ', 2)
+ committer = "%s %s %s" % (user, time, gittz(int(tz)))
+ else:
+ committer = author
+
+ parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0]
+
+ if len(parents) == 0:
+ modified = c.manifest().keys()
+ removed = []
+ else:
+ modified, removed = get_filechanges(repo, c, parents[0])
+
+ 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))
+
+ desc += '\n'
+ if extra_msg:
+ desc += '\n--HG--\n' + extra_msg
+
+ if len(parents) == 0 and rev:
+ print 'reset %s/%s' % (prefix, ename)
+
+ print "commit %s/%s" % (prefix, ename)
+ print "mark :%d" % (marks.get_mark(rev))
+ print "author %s" % (author)
+ print "committer %s" % (committer)
+ print "data %d" % (len(desc))
+ print desc
+
+ if len(parents) > 0:
+ print "from :%s" % (rev_to_mark(parents[0]))
+ if len(parents) > 1:
+ print "merge :%s" % (rev_to_mark(parents[1]))
+
+ for f in modified:
+ export_file(c.filectx(f))
+ for f in removed:
+ print "D %s" % (f)
+ print
+
+ count += 1
+ if (count % 100 == 0):
+ print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs))
+ print "#############################################################"
+
+ # make sure the ref is updated
+ print "reset %s/%s" % (prefix, ename)
+ print "from :%u" % rev_to_mark(rev)
+ print
+
+ marks.set_tip(ename, rev)
+
+def export_tag(repo, tag):
+ export_ref(repo, tag, 'tags', repo[tag])
+
+def export_bookmark(repo, bmark):
+ head = bmarks[bmark]
+ export_ref(repo, bmark, 'bookmarks', head)
+
+def export_branch(repo, branch):
+ tip = get_branch_tip(repo, branch)
+ head = repo[tip]
+ export_ref(repo, branch, 'branches', head)
+
+def export_head(repo):
+ global g_head
+ export_ref(repo, g_head[0], 'bookmarks', g_head[1])
+
+def do_capabilities(parser):
+ global prefix, dirname
+
+ print "import"
+ print "export"
+ print "refspec refs/heads/branches/*:%s/branches/*" % prefix
+ print "refspec refs/heads/*:%s/bookmarks/*" % prefix
+ print "refspec refs/tags/*:%s/tags/*" % prefix
+
+ path = os.path.join(dirname, 'marks-git')
+
+ if os.path.exists(path):
+ print "*import-marks %s" % path
+ print "*export-marks %s" % path
+
+ print
+
+def get_branch_tip(repo, branch):
+ global branches
+
+ heads = branches.get(branch, None)
+ if not heads:
+ return None
+
+ # verify there's only one head
+ if (len(heads) > 1):
+ warn("Branch '%s' has more than one head, consider merging" % branch)
+ # older versions of mercurial don't have this
+ if hasattr(repo, "branchtip"):
+ return repo.branchtip(branch)
+
+ return heads[0]
+
+def list_head(repo, cur):
+ global g_head, bmarks
+
+ head = bookmarks.readcurrent(repo)
+ if head:
+ node = repo[head]
+ else:
+ # fake bookmark from current branch
+ head = cur
+ node = repo['.']
+ if not node:
+ node = repo['tip']
+ if not node:
+ return
+ if head == 'default':
+ head = 'master'
+ bmarks[head] = node
+
+ print "@refs/heads/%s HEAD" % head
+ g_head = (head, node)
+
+def do_list(parser):
+ global branches, bmarks, mode, track_branches
+
+ repo = parser.repo
+ for bmark, node in bookmarks.listbookmarks(repo).iteritems():
+ bmarks[bmark] = repo[node]
+
+ cur = repo.dirstate.branch()
+
+ list_head(repo, cur)
+
+ if track_branches:
+ for branch in repo.branchmap():
+ heads = repo.branchheads(branch)
+ if len(heads):
+ branches[branch] = heads
+
+ for branch in branches:
+ print "? refs/heads/branches/%s" % branch
+
+ for bmark in bmarks:
+ print "? refs/heads/%s" % bmark
+
+ for tag, node in repo.tagslist():
+ if tag == 'tip':
+ continue
+ print "? refs/tags/%s" % tag
+
+ print
+
+def do_import(parser):
+ repo = parser.repo
+
+ path = os.path.join(dirname, 'marks-git')
+
+ print "feature done"
+ if os.path.exists(path):
+ print "feature import-marks=%s" % path
+ print "feature export-marks=%s" % path
+ sys.stdout.flush()
+
+ tmp = encoding.encoding
+ encoding.encoding = 'utf-8'
+
+ # lets get all the import lines
+ while parser.check('import'):
+ ref = parser[1]
+
+ if (ref == 'HEAD'):
+ export_head(repo)
+ elif ref.startswith('refs/heads/branches/'):
+ branch = ref[len('refs/heads/branches/'):]
+ export_branch(repo, branch)
+ elif ref.startswith('refs/heads/'):
+ bmark = ref[len('refs/heads/'):]
+ export_bookmark(repo, bmark)
+ elif ref.startswith('refs/tags/'):
+ tag = ref[len('refs/tags/'):]
+ export_tag(repo, tag)
+
+ parser.next()
+
+ encoding.encoding = tmp
+
+ print 'done'
+
+def parse_blob(parser):
+ global blob_marks
+
+ parser.next()
+ mark = parser.get_mark()
+ parser.next()
+ data = parser.get_data()
+ blob_marks[mark] = data
+ parser.next()
+ return
+
+def get_merge_files(repo, p1, p2, files):
+ for e in repo[p1].files():
+ if e not in files:
+ if e not in repo[p1].manifest():
+ continue
+ f = { 'ctx' : repo[p1][e] }
+ files[e] = f
+
+def parse_commit(parser):
+ global marks, blob_marks, bmarks, parsed_refs
+ global mode
+
+ from_mark = merge_mark = None
+
+ ref = parser[1]
+ parser.next()
+
+ commit_mark = parser.get_mark()
+ parser.next()
+ author = parser.get_author()
+ parser.next()
+ committer = parser.get_author()
+ parser.next()
+ data = parser.get_data()
+ parser.next()
+ if parser.check('from'):
+ from_mark = parser.get_mark()
+ parser.next()
+ if parser.check('merge'):
+ merge_mark = parser.get_mark()
+ parser.next()
+ if parser.check('merge'):
+ die('octopus merges are not supported yet')
+
+ files = {}
+
+ for line in parser:
+ if parser.check('M'):
+ t, m, mark_ref, path = line.split(' ', 3)
+ mark = int(mark_ref[1:])
+ f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
+ elif parser.check('D'):
+ t, path = line.split(' ')
+ f = { 'deleted' : True }
+ else:
+ die('Unknown file command: %s' % line)
+ files[path] = f
+
+ def getfilectx(repo, memctx, f):
+ of = files[f]
+ if 'deleted' in of:
+ raise IOError
+ if 'ctx' in of:
+ return of['ctx']
+ is_exec = of['mode'] == 'x'
+ is_link = of['mode'] == 'l'
+ rename = of.get('rename', None)
+ return context.memfilectx(f, of['data'],
+ is_link, is_exec, rename)
+
+ repo = parser.repo
+
+ user, date, tz = author
+ extra = {}
+
+ if committer != author:
+ extra['committer'] = "%s %u %u" % committer
+
+ if from_mark:
+ p1 = repo.changelog.node(mark_to_rev(from_mark))
+ else:
+ p1 = '\0' * 20
+
+ if merge_mark:
+ p2 = repo.changelog.node(mark_to_rev(merge_mark))
+ else:
+ p2 = '\0' * 20
+
+ #
+ # If files changed from any of the parents, hg wants to know, but in git if
+ # nothing changed from the first parent, nothing changed.
+ #
+ if merge_mark:
+ get_merge_files(repo, p1, p2, files)
+
+ if mode == 'hg':
+ i = data.find('\n--HG--\n')
+ if i >= 0:
+ tmp = data[i + len('\n--HG--\n'):].strip()
+ for k, v in [e.split(' : ') for e in tmp.split('\n')]:
+ if k == 'rename':
+ old, new = v.split(' => ', 1)
+ files[new]['rename'] = old
+ elif k == 'branch':
+ extra[k] = v
+ elif k == 'extra':
+ ek, ev = v.split(' : ', 1)
+ extra[ek] = urllib.unquote(ev)
+ data = data[:i]
+
+ ctx = context.memctx(repo, (p1, p2), data,
+ files.keys(), getfilectx,
+ user, (date, tz), extra)
+
+ tmp = encoding.encoding
+ encoding.encoding = 'utf-8'
+
+ node = repo.commitctx(ctx)
+
+ encoding.encoding = tmp
+
+ rev = repo[node].rev()
+
+ parsed_refs[ref] = node
+
+ marks.new_mark(rev, commit_mark)
+
+def parse_reset(parser):
+ ref = parser[1]
+ parser.next()
+ # ugh
+ if parser.check('commit'):
+ parse_commit(parser)
+ return
+ if not parser.check('from'):
+ return
+ from_mark = parser.get_mark()
+ parser.next()
+
+ node = parser.repo.changelog.node(mark_to_rev(from_mark))
+ parsed_refs[ref] = node
+
+def parse_tag(parser):
+ name = parser[1]
+ parser.next()
+ from_mark = parser.get_mark()
+ parser.next()
+ tagger = parser.get_author()
+ parser.next()
+ data = parser.get_data()
+ parser.next()
+
+ # nothing to do
+
+def do_export(parser):
+ global parsed_refs, bmarks, peer
+
+ parser.next()
+
+ for line in parser.each_block('done'):
+ if parser.check('blob'):
+ parse_blob(parser)
+ elif parser.check('commit'):
+ parse_commit(parser)
+ elif parser.check('reset'):
+ parse_reset(parser)
+ elif parser.check('tag'):
+ parse_tag(parser)
+ elif parser.check('feature'):
+ pass
+ else:
+ die('unhandled export command: %s' % line)
+
+ for ref, node in parsed_refs.iteritems():
+ if ref.startswith('refs/heads/branches'):
+ pass
+ elif ref.startswith('refs/heads/'):
+ bmark = ref[len('refs/heads/'):]
+ if bmark in bmarks:
+ old = bmarks[bmark].hex()
+ else:
+ old = ''
+ if not bookmarks.pushbookmark(parser.repo, bmark, old, node):
+ continue
+ elif ref.startswith('refs/tags/'):
+ tag = ref[len('refs/tags/'):]
+ parser.repo.tag([tag], node, None, True, None, {})
+ else:
+ # transport-helper/fast-export bugs
+ continue
+ print "ok %s" % ref
+
+ print
+
+ if peer:
+ parser.repo.push(peer, force=False)
+
+def main(args):
+ global prefix, dirname, branches, bmarks
+ global marks, blob_marks, parsed_refs
+ global peer, mode, bad_mail, bad_name
+ global track_branches
+
+ alias = args[1]
+ url = args[2]
+ peer = None
+
+ hg_git_compat = False
+ track_branches = True
+ try:
+ if get_config('remote-hg.hg-git-compat') == 'true\n':
+ hg_git_compat = True
+ track_branches = False
+ if get_config('remote-hg.track-branches') == 'false\n':
+ track_branches = False
+ except subprocess.CalledProcessError:
+ pass
+
+ if hg_git_compat:
+ mode = 'hg'
+ bad_mail = 'none@none'
+ bad_name = ''
+ else:
+ mode = 'git'
+ bad_mail = 'unknown'
+ bad_name = 'Unknown'
+
+ if alias[4:] == url:
+ is_tmp = True
+ alias = util.sha1(alias).hexdigest()
+ else:
+ is_tmp = False
+
+ gitdir = os.environ['GIT_DIR']
+ dirname = os.path.join(gitdir, 'hg', alias)
+ branches = {}
+ bmarks = {}
+ blob_marks = {}
+ parsed_refs = {}
+
+ repo = get_repo(url, alias)
+ prefix = 'refs/hg/%s' % alias
+
+ if not os.path.exists(dirname):
+ os.makedirs(dirname)
+
+ marks_path = os.path.join(dirname, 'marks-hg')
+ marks = Marks(marks_path)
+
+ parser = Parser(repo)
+ for line in parser:
+ if parser.check('capabilities'):
+ do_capabilities(parser)
+ elif parser.check('list'):
+ do_list(parser)
+ elif parser.check('import'):
+ do_import(parser)
+ elif parser.check('export'):
+ do_export(parser)
+ else:
+ die('unhandled command: %s' % line)
+ sys.stdout.flush()
+
+ if not is_tmp:
+ marks.store()
+ else:
+ shutil.rmtree(dirname)
+
+sys.exit(main(sys.argv))
diff --git a/contrib/remote-helpers/test-hg-bidi.sh b/contrib/remote-helpers/test-hg-bidi.sh
new file mode 100755
index 0000000000..1d61982436
--- /dev/null
+++ b/contrib/remote-helpers/test-hg-bidi.sh
@@ -0,0 +1,243 @@
+#!/bin/sh
+#
+# Copyright (c) 2012 Felipe Contreras
+#
+# Base commands from hg-git tests:
+# https://bitbucket.org/durin42/hg-git/src
+#
+
+test_description='Test bidirectionality of remote-hg'
+
+. ./test-lib.sh
+
+if ! test_have_prereq PYTHON; then
+ skip_all='skipping remote-hg tests; python not available'
+ test_done
+fi
+
+if ! "$PYTHON_PATH" -c 'import mercurial'; then
+ skip_all='skipping remote-hg tests; mercurial not available'
+ test_done
+fi
+
+# clone to a git repo
+git_clone () {
+ hg -R $1 bookmark -f -r tip master &&
+ git clone -q "hg::$PWD/$1" $2
+}
+
+# clone to an hg repo
+hg_clone () {
+ (
+ hg init $2 &&
+ cd $1 &&
+ git push -q "hg::$PWD/../$2" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*'
+ ) &&
+
+ (cd $2 && hg -q update)
+}
+
+# push an hg repo
+hg_push () {
+ (
+ cd $2
+ old=$(git symbolic-ref --short HEAD)
+ git checkout -q -b tmp &&
+ git fetch -q "hg::$PWD/../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' &&
+ git checkout -q $old &&
+ git branch -q -D tmp 2> /dev/null || true
+ )
+}
+
+hg_log () {
+ hg -R $1 log --graph --debug | grep -v 'tag: *default/'
+}
+
+setup () {
+ (
+ echo "[ui]"
+ echo "username = A U Thor <author@example.com>"
+ echo "[defaults]"
+ echo "backout = -d \"0 0\""
+ echo "commit = -d \"0 0\""
+ echo "debugrawcommit = -d \"0 0\""
+ echo "tag = -d \"0 0\""
+ ) >> "$HOME"/.hgrc &&
+ git config --global remote-hg.hg-git-compat true
+
+ export HGEDITOR=/usr/bin/true
+
+ export GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0230"
+ export GIT_COMMITTER_DATE="$GIT_AUTHOR_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à" &&
+
+ export GIT_AUTHOR_NAME="tést èncödîng" &&
+ echo beta > beta &&
+ git add beta &&
+ git commit -m "add beta" &&
+
+ echo gamma > gamma &&
+ git add gamma &&
+ git commit -m "add gämmâ" &&
+
+ : TODO git config i18n.commitencoding latin-1 &&
+ echo delta > delta &&
+ git add delta &&
+ git commit -m "add déltà"
+ ) &&
+
+ hg_clone gitrepo hgrepo &&
+ git_clone hgrepo gitrepo2 &&
+ hg_clone gitrepo2 hgrepo2 &&
+
+ HGENCODING=utf-8 hg_log hgrepo > expected &&
+ HGENCODING=utf-8 hg_log hgrepo2 > actual &&
+
+ test_cmp expected actual
+'
+
+test_expect_success 'file removal' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ git init -q gitrepo &&
+ cd gitrepo &&
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -m "add alpha" &&
+ echo beta > beta &&
+ git add beta &&
+ git commit -m "add beta"
+ mkdir foo &&
+ echo blah > foo/bar &&
+ git add foo &&
+ git commit -m "add foo" &&
+ git rm alpha &&
+ git commit -m "remove alpha" &&
+ git rm foo/bar &&
+ git commit -m "remove foo/bar"
+ ) &&
+
+ hg_clone gitrepo hgrepo &&
+ git_clone hgrepo gitrepo2 &&
+ hg_clone gitrepo2 hgrepo2 &&
+
+ hg_log hgrepo > expected &&
+ hg_log hgrepo2 > actual &&
+
+ test_cmp expected actual
+'
+
+test_expect_success 'git tags' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ git init -q gitrepo &&
+ cd gitrepo &&
+ git config receive.denyCurrentBranch ignore &&
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -m "add alpha" &&
+ git tag alpha &&
+
+ echo beta > beta &&
+ git add beta &&
+ git commit -m "add beta" &&
+ git tag -a -m "added tag beta" beta
+ ) &&
+
+ hg_clone gitrepo hgrepo &&
+ git_clone hgrepo gitrepo2 &&
+ hg_clone gitrepo2 hgrepo2 &&
+
+ hg_log hgrepo > expected &&
+ hg_log hgrepo2 > actual &&
+
+ test_cmp expected actual
+'
+
+test_expect_success 'hg branch' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ git init -q gitrepo &&
+ cd gitrepo &&
+
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -q -m "add alpha" &&
+ git checkout -q -b not-master
+ ) &&
+
+ (
+ hg_clone gitrepo hgrepo &&
+
+ cd hgrepo &&
+ hg -q co master &&
+ hg mv alpha beta &&
+ hg -q commit -m "rename alpha to beta" &&
+ hg branch gamma | grep -v "permanent and global" &&
+ hg -q commit -m "started branch gamma"
+ ) &&
+
+ hg_push hgrepo gitrepo &&
+ hg_clone gitrepo hgrepo2 &&
+
+ : TODO, avoid "master" bookmark &&
+ (cd hgrepo2 && hg checkout gamma) &&
+
+ hg_log hgrepo > expected &&
+ hg_log hgrepo2 > actual &&
+
+ test_cmp expected actual
+'
+
+test_expect_success 'hg tags' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ git init -q gitrepo &&
+ cd gitrepo &&
+
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -m "add alpha" &&
+ git checkout -q -b not-master
+ ) &&
+
+ (
+ hg_clone gitrepo hgrepo &&
+
+ cd hgrepo &&
+ hg co master &&
+ hg tag alpha
+ ) &&
+
+ hg_push hgrepo gitrepo &&
+ hg_clone gitrepo hgrepo2 &&
+
+ hg_log hgrepo > expected &&
+ hg_log hgrepo2 > actual &&
+
+ test_cmp expected actual
+'
+
+test_done
diff --git a/contrib/remote-helpers/test-hg-hg-git.sh b/contrib/remote-helpers/test-hg-hg-git.sh
new file mode 100755
index 0000000000..3e76d9fb60
--- /dev/null
+++ b/contrib/remote-helpers/test-hg-hg-git.sh
@@ -0,0 +1,466 @@
+#!/bin/sh
+#
+# Copyright (c) 2012 Felipe Contreras
+#
+# Base commands from hg-git tests:
+# https://bitbucket.org/durin42/hg-git/src
+#
+
+test_description='Test remote-hg output compared to hg-git'
+
+. ./test-lib.sh
+
+if ! test_have_prereq PYTHON; then
+ skip_all='skipping remote-hg tests; python not available'
+ test_done
+fi
+
+if ! "$PYTHON_PATH" -c 'import mercurial'; then
+ skip_all='skipping remote-hg tests; mercurial not available'
+ test_done
+fi
+
+if ! "$PYTHON_PATH" -c 'import hggit'; then
+ skip_all='skipping remote-hg tests; hg-git not available'
+ test_done
+fi
+
+# clone to a git repo with git
+git_clone_git () {
+ hg -R $1 bookmark -f -r tip master &&
+ git clone -q "hg::$PWD/$1" $2
+}
+
+# clone to an hg repo with git
+hg_clone_git () {
+ (
+ hg init $2 &&
+ cd $1 &&
+ git push -q "hg::$PWD/../$2" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*'
+ ) &&
+
+ (cd $2 && hg -q update)
+}
+
+# clone to a git repo with hg
+git_clone_hg () {
+ (
+ git init -q $2 &&
+ cd $1 &&
+ hg bookmark -f -r tip master &&
+ hg -q push -r master ../$2 || true
+ )
+}
+
+# clone to an hg repo with hg
+hg_clone_hg () {
+ hg -q clone $1 $2
+}
+
+# push an hg repo with git
+hg_push_git () {
+ (
+ cd $2
+ old=$(git symbolic-ref --short HEAD)
+ git checkout -q -b tmp &&
+ git fetch -q "hg::$PWD/../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' &&
+ git checkout -q $old &&
+ git branch -q -D tmp 2> /dev/null || true
+ )
+}
+
+# push an hg git repo with hg
+hg_push_hg () {
+ (
+ cd $1 &&
+ hg -q push ../$2 || true
+ )
+}
+
+hg_log () {
+ hg -R $1 log --graph --debug | grep -v 'tag: *default/'
+}
+
+git_log () {
+ git --git-dir=$1/.git fast-export --branches
+}
+
+setup () {
+ (
+ echo "[ui]"
+ echo "username = A U Thor <author@example.com>"
+ echo "[defaults]"
+ echo "backout = -d \"0 0\""
+ echo "commit = -d \"0 0\""
+ echo "debugrawcommit = -d \"0 0\""
+ echo "tag = -d \"0 0\""
+ echo "[extensions]"
+ echo "hgext.bookmarks ="
+ echo "hggit ="
+ ) >> "$HOME"/.hgrc &&
+ git config --global receive.denycurrentbranch warn
+ git config --global remote-hg.hg-git-compat true
+
+ export HGEDITOR=/usr/bin/true
+
+ export GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0230"
+ export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
+}
+
+setup
+
+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à" &&
+
+ export GIT_AUTHOR_NAME="tést èncödîng" &&
+ echo beta > beta &&
+ git add beta &&
+ git commit -m "add beta" &&
+
+ echo gamma > gamma &&
+ git add gamma &&
+ git commit -m "add gämmâ" &&
+
+ : TODO git config i18n.commitencoding latin-1 &&
+ echo delta > delta &&
+ git add delta &&
+ git commit -m "add déltà"
+ ) &&
+
+ for x in hg git; do
+ hg_clone_$x gitrepo hgrepo-$x &&
+ git_clone_$x hgrepo-$x gitrepo2-$x &&
+
+ HGENCODING=utf-8 hg_log hgrepo-$x > hg-log-$x &&
+ git_log gitrepo2-$x > git-log-$x
+ done &&
+
+ test_cmp hg-log-hg hg-log-git &&
+ test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'file removal' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ git init -q gitrepo &&
+ cd gitrepo &&
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -m "add alpha" &&
+ echo beta > beta &&
+ git add beta &&
+ git commit -m "add beta"
+ mkdir foo &&
+ echo blah > foo/bar &&
+ git add foo &&
+ git commit -m "add foo" &&
+ git rm alpha &&
+ git commit -m "remove alpha" &&
+ git rm foo/bar &&
+ git commit -m "remove foo/bar"
+ ) &&
+
+ for x in hg git; do
+ (
+ hg_clone_$x gitrepo hgrepo-$x &&
+ cd hgrepo-$x &&
+ hg_log . &&
+ hg manifest -r 3 &&
+ hg manifest
+ ) > output-$x &&
+
+ git_clone_$x hgrepo-$x gitrepo2-$x &&
+ git_log gitrepo2-$x > log-$x
+ done &&
+
+ test_cmp output-hg output-git &&
+ test_cmp log-hg log-git
+'
+
+test_expect_success 'git tags' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ (
+ git init -q gitrepo &&
+ cd gitrepo &&
+ git config receive.denyCurrentBranch ignore &&
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -m "add alpha" &&
+ git tag alpha &&
+
+ echo beta > beta &&
+ git add beta &&
+ git commit -m "add beta" &&
+ git tag -a -m "added tag beta" beta
+ ) &&
+
+ for x in hg git; do
+ hg_clone_$x gitrepo hgrepo-$x &&
+ hg_log hgrepo-$x > log-$x
+ done &&
+
+ test_cmp log-hg log-git
+'
+
+test_expect_success 'hg author' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ for x in hg git; do
+ (
+ git init -q gitrepo-$x &&
+ cd gitrepo-$x &&
+
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -m "add alpha" &&
+ git checkout -q -b not-master
+ ) &&
+
+ (
+ hg_clone_$x gitrepo-$x hgrepo-$x &&
+ cd hgrepo-$x &&
+
+ hg co master &&
+ echo beta > beta &&
+ hg add beta &&
+ hg commit -u "test" -m "add beta" &&
+
+ echo gamma >> beta &&
+ hg commit -u "test <test@example.com> (comment)" -m "modify beta" &&
+
+ echo gamma > gamma &&
+ hg add gamma &&
+ hg commit -u "<test@example.com>" -m "add gamma" &&
+
+ echo delta > delta &&
+ hg add delta &&
+ hg commit -u "name<test@example.com>" -m "add delta" &&
+
+ echo epsilon > epsilon &&
+ hg add epsilon &&
+ hg commit -u "name <test@example.com" -m "add epsilon" &&
+
+ echo zeta > zeta &&
+ hg add zeta &&
+ hg commit -u " test " -m "add zeta" &&
+
+ echo eta > eta &&
+ hg add eta &&
+ hg commit -u "test < test@example.com >" -m "add eta" &&
+
+ echo theta > theta &&
+ hg add theta &&
+ hg commit -u "test >test@example.com>" -m "add theta" &&
+
+ echo iota > iota &&
+ hg add iota &&
+ hg commit -u "test <test <at> example <dot> com>" -m "add iota"
+ ) &&
+
+ hg_push_$x hgrepo-$x gitrepo-$x &&
+ hg_clone_$x gitrepo-$x hgrepo2-$x &&
+
+ hg_log hgrepo2-$x > hg-log-$x &&
+ git_log gitrepo-$x > git-log-$x
+ done &&
+
+ test_cmp git-log-hg git-log-git &&
+
+ test_cmp hg-log-hg hg-log-git &&
+ test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'hg branch' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ for x in hg git; do
+ (
+ git init -q gitrepo-$x &&
+ cd gitrepo-$x &&
+
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -q -m "add alpha" &&
+ git checkout -q -b not-master
+ ) &&
+
+ (
+ hg_clone_$x gitrepo-$x hgrepo-$x &&
+
+ cd hgrepo-$x &&
+ hg -q co master &&
+ hg mv alpha beta &&
+ hg -q commit -m "rename alpha to beta" &&
+ hg branch gamma | grep -v "permanent and global" &&
+ hg -q commit -m "started branch gamma"
+ ) &&
+
+ hg_push_$x hgrepo-$x gitrepo-$x &&
+ hg_clone_$x gitrepo-$x hgrepo2-$x &&
+
+ hg_log hgrepo2-$x > hg-log-$x &&
+ git_log gitrepo-$x > git-log-$x
+ done &&
+
+ test_cmp hg-log-hg hg-log-git &&
+ test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'hg tags' '
+ mkdir -p tmp && cd tmp &&
+ test_when_finished "cd .. && rm -rf tmp" &&
+
+ for x in hg git; do
+ (
+ git init -q gitrepo-$x &&
+ cd gitrepo-$x &&
+
+ echo alpha > alpha &&
+ git add alpha &&
+ git commit -m "add alpha" &&
+ git checkout -q -b not-master
+ ) &&
+
+ (
+ hg_clone_$x gitrepo-$x hgrepo-$x &&
+
+ cd hgrepo-$x &&
+ hg co master &&
+ hg tag alpha
+ ) &&
+
+ hg_push_$x hgrepo-$x gitrepo-$x &&
+ hg_clone_$x gitrepo-$x hgrepo2-$x &&
+
+ (
+ git --git-dir=gitrepo-$x/.git tag -l &&
+ hg_log hgrepo2-$x &&
+ cat hgrepo2-$x/.hgtags
+ ) > output-$x
+ done &&
+
+ test_cmp output-hg output-git
+'
+
+test_done
diff --git a/contrib/remote-helpers/test-hg.sh b/contrib/remote-helpers/test-hg.sh
new file mode 100755
index 0000000000..5f81dfae6c
--- /dev/null
+++ b/contrib/remote-helpers/test-hg.sh
@@ -0,0 +1,121 @@
+#!/bin/sh
+#
+# Copyright (c) 2012 Felipe Contreras
+#
+# Base commands from hg-git tests:
+# https://bitbucket.org/durin42/hg-git/src
+#
+
+test_description='Test remote-hg'
+
+. ./test-lib.sh
+
+if ! test_have_prereq PYTHON; then
+ skip_all='skipping remote-hg tests; python not available'
+ test_done
+fi
+
+if ! "$PYTHON_PATH" -c 'import mercurial'; then
+ skip_all='skipping remote-hg tests; mercurial not available'
+ test_done
+fi
+
+check () {
+ (cd $1 &&
+ git log --format='%s' -1 &&
+ git symbolic-ref HEAD) > actual &&
+ (echo $2 &&
+ echo "refs/heads/$3") > expected &&
+ test_cmp expected actual
+}
+
+setup () {
+ (
+ echo "[ui]"
+ echo "username = H G Wells <wells@example.com>"
+ ) >> "$HOME"/.hgrc
+}
+
+setup
+
+test_expect_success 'cloning' '
+ test_when_finished "rm -rf gitrepo*" &&
+
+ (
+ hg init hgrepo &&
+ cd hgrepo &&
+ echo zero > content &&
+ hg add content &&
+ hg commit -m zero
+ ) &&
+
+ git clone "hg::$PWD/hgrepo" gitrepo &&
+ check gitrepo zero master
+'
+
+test_expect_success 'cloning with branches' '
+ test_when_finished "rm -rf gitrepo*" &&
+
+ (
+ cd hgrepo &&
+ hg branch next &&
+ echo next > content &&
+ hg commit -m next
+ ) &&
+
+ git clone "hg::$PWD/hgrepo" gitrepo &&
+ check gitrepo next next &&
+
+ (cd hgrepo && hg checkout default) &&
+
+ git clone "hg::$PWD/hgrepo" gitrepo2 &&
+ check gitrepo2 zero master
+'
+
+test_expect_success 'cloning with bookmarks' '
+ test_when_finished "rm -rf gitrepo*" &&
+
+ (
+ cd hgrepo &&
+ hg bookmark feature-a &&
+ echo feature-a > content &&
+ hg commit -m feature-a
+ ) &&
+
+ git clone "hg::$PWD/hgrepo" gitrepo &&
+ check gitrepo feature-a feature-a
+'
+
+test_expect_success 'cloning with detached head' '
+ test_when_finished "rm -rf gitrepo*" &&
+
+ (
+ cd hgrepo &&
+ hg update -r 0
+ ) &&
+
+ git clone "hg::$PWD/hgrepo" gitrepo &&
+ check gitrepo zero master
+'
+
+test_expect_success 'update bookmark' '
+ test_when_finished "rm -rf gitrepo*" &&
+
+ (
+ cd hgrepo &&
+ hg bookmark devel
+ ) &&
+
+ (
+ git clone "hg::$PWD/hgrepo" gitrepo &&
+ cd gitrepo &&
+ git checkout devel &&
+ echo devel > content &&
+ git commit -a -m devel &&
+ git push
+ ) &&
+
+ hg -R hgrepo bookmarks | grep "devel\s\+3:"
+'
+
+test_done
diff --git a/contrib/stats/mailmap.pl b/contrib/stats/mailmap.pl
index 4b852e2455..9513f5e35b 100755
--- a/contrib/stats/mailmap.pl
+++ b/contrib/stats/mailmap.pl
@@ -1,38 +1,70 @@
-#!/usr/bin/perl -w
-my %mailmap = ();
-open I, "<", ".mailmap";
-while (<I>) {
- chomp;
- next if /^#/;
- if (my ($author, $mail) = /^(.*?)\s+<(.+)>$/) {
- $mailmap{$mail} = $author;
- }
+#!/usr/bin/perl
+
+use warnings 'all';
+use strict;
+use Getopt::Long;
+
+my $match_emails;
+my $match_names;
+my $order_by = 'count';
+Getopt::Long::Configure(qw(bundling));
+GetOptions(
+ 'emails|e!' => \$match_emails,
+ 'names|n!' => \$match_names,
+ 'count|c' => sub { $order_by = 'count' },
+ 'time|t' => sub { $order_by = 'stamp' },
+) or exit 1;
+$match_emails = 1 unless $match_names;
+
+my $email = {};
+my $name = {};
+
+open(my $fh, '-|', "git log --format='%at <%aE> %aN'");
+while(<$fh>) {
+ my ($t, $e, $n) = /(\S+) <(\S+)> (.*)/;
+ mark($email, $e, $n, $t);
+ mark($name, $n, $e, $t);
}
-close I;
-
-my %mail2author = ();
-open I, "git log --pretty='format:%ae %an' |";
-while (<I>) {
- chomp;
- my ($mail, $author) = split(/\t/, $_);
- next if exists $mailmap{$mail};
- $mail2author{$mail} ||= {};
- $mail2author{$mail}{$author} ||= 0;
- $mail2author{$mail}{$author}++;
+close($fh);
+
+if ($match_emails) {
+ foreach my $e (dups($email)) {
+ foreach my $n (vals($email->{$e})) {
+ show($n, $e, $email->{$e}->{$n});
+ }
+ print "\n";
+ }
}
-close I;
-
-while (my ($mail, $authorcount) = each %mail2author) {
- # %$authorcount is ($author => $count);
- # sort and show the names from the most frequent ones.
- my @names = (map { $_->[0] }
- sort { $b->[1] <=> $a->[1] }
- map { [$_, $authorcount->{$_}] }
- keys %$authorcount);
- if (1 < @names) {
- for (@names) {
- print "$_ <$mail>\n";
+if ($match_names) {
+ foreach my $n (dups($name)) {
+ foreach my $e (vals($name->{$n})) {
+ show($n, $e, $name->{$n}->{$e});
}
+ print "\n";
}
}
+exit 0;
+sub mark {
+ my ($h, $k, $v, $t) = @_;
+ my $e = $h->{$k}->{$v} ||= { count => 0, stamp => 0 };
+ $e->{count}++;
+ $e->{stamp} = $t unless $t < $e->{stamp};
+}
+
+sub dups {
+ my $h = shift;
+ return grep { keys($h->{$_}) > 1 } keys($h);
+}
+
+sub vals {
+ my $h = shift;
+ return sort {
+ $h->{$b}->{$order_by} <=> $h->{$a}->{$order_by}
+ } keys($h);
+}
+
+sub show {
+ my ($n, $e, $h) = @_;
+ print "$n <$e> ($h->{$order_by})\n";
+}
diff --git a/contrib/subtree/.gitignore b/contrib/subtree/.gitignore
index 7e77c9d022..91360a3d7f 100644
--- a/contrib/subtree/.gitignore
+++ b/contrib/subtree/.gitignore
@@ -1,4 +1,5 @@
*~
+git-subtree
git-subtree.xml
git-subtree.1
mainline
diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt
index 0c44fda011..c5bce41ac7 100644
--- a/contrib/subtree/git-subtree.txt
+++ b/contrib/subtree/git-subtree.txt
@@ -93,7 +93,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/svn-fe/svn-fe.c b/contrib/svn-fe/svn-fe.c
index 35db24f5ea..f363505abb 100644
--- a/contrib/svn-fe/svn-fe.c
+++ b/contrib/svn-fe/svn-fe.c
@@ -10,7 +10,8 @@ int main(int argc, char **argv)
{
if (svndump_init(NULL))
return 1;
- svndump_read((argc > 1) ? argv[1] : NULL);
+ svndump_read((argc > 1) ? argv[1] : NULL, "refs/heads/master",
+ "refs/notes/svn/revs");
svndump_deinit();
svndump_reset();
return 0;
diff --git a/contrib/svn-fe/svnrdump_sim.py b/contrib/svn-fe/svnrdump_sim.py
new file mode 100755
index 0000000000..1cfac4a6f8
--- /dev/null
+++ b/contrib/svn-fe/svnrdump_sim.py
@@ -0,0 +1,53 @@
+#!/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
+
+
+def getrevlimit():
+ var = 'SVNRMAX'
+ if os.environ.has_key(var):
+ 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)