summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rw-r--r--contrib/ciabot/INSTALL54
-rw-r--r--contrib/ciabot/README3
-rwxr-xr-xcontrib/ciabot/ciabot.py179
-rwxr-xr-xcontrib/ciabot/ciabot.sh133
-rw-r--r--[-rwxr-xr-x]contrib/completion/git-completion.bash408
-rw-r--r--contrib/completion/git-prompt.sh289
-rw-r--r--contrib/credential/osxkeychain/Makefile7
-rw-r--r--contrib/credential/wincred/Makefile14
-rw-r--r--contrib/credential/wincred/git-credential-wincred.c357
-rw-r--r--contrib/diff-highlight/README109
-rwxr-xr-xcontrib/diff-highlight/diff-highlight109
-rw-r--r--contrib/diffall/README31
-rwxr-xr-xcontrib/diffall/git-diffall257
-rw-r--r--contrib/emacs/git-blame.el75
-rw-r--r--contrib/examples/builtin-fetch--tool.c4
-rwxr-xr-xcontrib/fast-import/git-p42606
-rw-r--r--contrib/fast-import/git-p4.README12
-rw-r--r--contrib/fast-import/git-p4.bat1
-rw-r--r--contrib/mw-to-git/Makefile47
-rwxr-xr-xcontrib/mw-to-git/git-remote-mediawiki777
-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.sh321
-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/persistent-https/LICENSE202
-rw-r--r--contrib/persistent-https/Makefile38
-rw-r--r--contrib/persistent-https/README62
-rw-r--r--contrib/persistent-https/client.go189
-rw-r--r--contrib/persistent-https/main.go82
-rw-r--r--contrib/persistent-https/proxy.go190
-rw-r--r--contrib/persistent-https/socket.go97
-rwxr-xr-xcontrib/rerere-train.sh2
-rw-r--r--contrib/subtree/.gitignore5
-rw-r--r--contrib/subtree/COPYING339
-rw-r--r--contrib/subtree/INSTALL28
-rw-r--r--contrib/subtree/Makefile52
-rw-r--r--contrib/subtree/README8
-rwxr-xr-xcontrib/subtree/git-subtree.sh712
-rw-r--r--contrib/subtree/git-subtree.txt366
-rw-r--r--contrib/subtree/t/Makefile69
-rwxr-xr-xcontrib/subtree/t/t7900-subtree.sh508
-rw-r--r--contrib/subtree/todo50
-rw-r--r--contrib/svn-fe/svn-fe.txt10
55 files changed, 7330 insertions, 3261 deletions
diff --git a/contrib/ciabot/INSTALL b/contrib/ciabot/INSTALL
new file mode 100644
index 0000000000..7222961d35
--- /dev/null
+++ b/contrib/ciabot/INSTALL
@@ -0,0 +1,54 @@
+= Installation instructions =
+
+Two scripts are included. The Python one (ciabot.py) is faster and
+more capable; the shell one (ciabot.sh) is a fallback in case Python
+gives your git hosting site indigestion. (I know of no such sites.)
+
+It is no longer necessary to modify the script in order to put it
+in place; in fact, this is now discouraged. It is entirely
+configurable with the following git config variables:
+
+ciabot.project = name of the project
+ciabot.repo = name of the project repo for gitweb/cgit purposes
+ciabot.xmlrpc = if true, ship notifications via XML-RPC
+ciabot.revformat = format in which the revision is shown
+
+The revformat variable may have the following values
+raw -> full hex ID of commit
+short -> first 12 chars of hex ID
+describe -> describe relative to last tag, falling back to short
+
+ciabot.project defaults to the directory name of the repository toplevel.
+ciabot.repo defaults to ciabot.project lowercased.
+ciabot.xmlrpc defaults to True
+ciabot.revformat defaults to 'describe'.
+
+This means that in the normal case you need not do any configuration at all,
+however setting ciabot.project will allow the hook to run slightly faster.
+
+Once you've set these variables, try your script with -n to see the
+notification message dumped to stdout and verify that it looks sane.
+
+To live-test these scripts, your project needs to have been registered with
+the CIA site. Here are the steps:
+
+1. Open an IRC window on irc://freenode/commits or your registered
+ project IRC channel.
+
+2. Run ciabot.py and/or ciabot.sh from any directory under git
+ control.
+
+You should see a notification on the channel for your most recent commit.
+
+After verifying correct function, install one of these scripts either
+in a post-commit hook or in an update hook.
+
+In post-commit, run it without arguments. It will query for
+current HEAD and the latest commit ID to get the information it
+needs.
+
+In update, call it with a refname followed by a list of commits:
+You want to reverse the order git rev-list emits because it lists
+from most recent to oldest.
+
+/path/to/ciabot.py ${refname} $(git rev-list ${oldhead}..${newhead} | tac)
diff --git a/contrib/ciabot/README b/contrib/ciabot/README
index 3b916acece..2dfe1f91f5 100644
--- a/contrib/ciabot/README
+++ b/contrib/ciabot/README
@@ -8,5 +8,4 @@ You probably want the Python version; it's faster, more capable, and
better documented. The shell version is maintained only as a fallback
for use on hosting sites that don't permit Python hook scripts.
-You will find installation instructions for each script in its comment
-header.
+See the file INSTALL for installation instructions.
diff --git a/contrib/ciabot/ciabot.py b/contrib/ciabot/ciabot.py
index 9775dffb5d..bd24395d4c 100755
--- a/contrib/ciabot/ciabot.py
+++ b/contrib/ciabot/ciabot.py
@@ -10,44 +10,45 @@
# usage: ciabot.py [-V] [-n] [-p projectname] [refname [commits...]]
#
# This script is meant to be run either in a post-commit hook or in an
-# update hook. If there's nothing unusual about your hosting setup,
-# you can specify the project name with a -p option and avoid having
-# to modify this script. Try it with -n to see the notification mail
-# dumped to stdout and verify that it looks sane. With -V it dumps its
-# version and exits.
+# update hook. Try it with -n to see the notification mail dumped to
+# stdout and verify that it looks sane. With -V it dumps its version
+# and exits.
#
-# In post-commit, run it without arguments (other than possibly a -p
-# option). It will query for current HEAD and the latest commit ID to
-# get the information it needs.
+# In post-commit, run it without arguments. It will query for
+# current HEAD and the latest commit ID to get the information it
+# needs.
#
# In update, call it with a refname followed by a list of commits:
-# You want to reverse the order git rev-list emits becxause it lists
+# You want to reverse the order git rev-list emits because it lists
# from most recent to oldest.
#
# /path/to/ciabot.py ${refname} $(git rev-list ${oldhead}..${newhead} | tac)
#
-# Note: this script uses mail, not XML-RPC, in order to avoid stalling
-# until timeout when the CIA XML-RPC server is down.
+# Configuration variables affecting this script:
#
-
+# ciabot.project = name of the project
+# ciabot.repo = name of the project repo for gitweb/cgit purposes
+# ciabot.xmlrpc = if true (default), ship notifications via XML-RPC
+# ciabot.revformat = format in which the revision is shown
#
-# The project as known to CIA. You will either want to change this
-# or invoke the script with a -p option to set it.
+# ciabot.project defaults to the directory name of the repository toplevel.
+# ciabot.repo defaults to ciabot.project lowercased.
#
-project=None
-
+# This means that in the normal case you need not do any configuration at all,
+# but setting the project name will speed it up slightly.
#
-# You may not need to change these:
+# The revformat variable may have the following values
+# raw -> full hex ID of commit
+# short -> first 12 chars of hex ID
+# describe = -> describe relative to last tag, falling back to short
+# The default is 'describe'.
+#
+# Note: the CIA project now says only XML-RPC is reliable, so
+# we default to that.
#
-import os, sys, commands, socket, urllib
-
-# Name of the repository.
-# You can hardwire this to make the script faster.
-repo = os.path.basename(os.getcwd())
-# Fully-qualified domain name of this host.
-# You can hardwire this to make the script faster.
-host = socket.getfqdn()
+import os, sys, commands, socket, urllib
+from xml.sax.saxutils import escape
# Changeset URL prefix for your repo: when the commit ID is appended
# to this, it should point at a CGI that will display the commit
@@ -72,7 +73,7 @@ xml = '''\
<message>
<generator>
<name>CIA Python client for Git</name>
- <version>%(gitver)s</version>
+ <version>%(version)s</version>
<url>%(generator)s</url>
</generator>
<source>
@@ -98,19 +99,18 @@ xml = '''\
# No user-serviceable parts below this line:
#
-# Addresses for the e-mail. The from address is a dummy, since CIA
-# will never reply to this mail.
-fromaddr = "CIABOT-NOREPLY@" + host
-toaddr = "cia@cia.navi.cx"
+# Where to ship e-mail notifications.
+toaddr = "cia@cia.vc"
# Identify the generator script.
# Should only change when the script itself gets a new home and maintainer.
-generator="http://www.catb.org/~esr/ciabot.py"
+generator = "http://www.catb.org/~esr/ciabot.py"
+version = "3.6"
def do(command):
return commands.getstatusoutput(command)[1]
-def report(refname, merged):
+def report(refname, merged, xmlrpc=True):
"Generate a commit notification to be reported to CIA"
# Try to tinyfy a reference to a web view for this commit.
@@ -121,32 +121,27 @@ def report(refname, merged):
branch = os.path.basename(refname)
- # Compute a shortnane for the revision
- rev = do("git describe '"+ merged +"' 2>/dev/null") or merged[:12]
-
- # Extract the neta-information for the commit
- rawcommit = do("git cat-file commit " + merged)
+ # Compute a description for the revision
+ if revformat == 'raw':
+ rev = merged
+ elif revformat == 'short':
+ rev = ''
+ else: # revformat == 'describe'
+ rev = do("git describe %s 2>/dev/null" % merged)
+ if not rev:
+ rev = merged[:12]
+
+ # Extract the meta-information for the commit
files=do("git diff-tree -r --name-only '"+ merged +"' | sed -e '1d' -e 's-.*-<file>&</file>-'")
- inheader = True
- headers = {}
- logmsg = ""
- for line in rawcommit.split("\n"):
- if inheader:
- if line:
- fields = line.split()
- headers[fields[0]] = " ".join(fields[1:])
- else:
- inheader = False
- else:
- logmsg = line
- break
- (author, ts) = headers["author"].split(">")
+ metainfo = do("git log -1 '--pretty=format:%an <%ae>%n%at%n%s' " + merged)
+ (author, ts, logmsg) = metainfo.split("\n")
+ logmsg = escape(logmsg)
- # This discards the part of the authors addrsss after @.
- # Might be bnicece to ship the full email address, if not
+ # This discards the part of the author's address after @.
+ # Might be be nice to ship the full email address, if not
# for spammers' address harvesters - getting this wrong
# would make the freenode #commits channel into harvester heaven.
- author = author.replace("<", "").split("@")[0].split()[-1]
+ author = escape(author.replace("<", "").split("@")[0].split()[-1])
# This ignores the timezone. Not clear what to do with it...
ts = ts.strip().split()[0]
@@ -155,8 +150,7 @@ def report(refname, merged):
context.update(globals())
out = xml % context
-
- message = '''\
+ mail = '''\
Message-ID: <%(merged)s.%(author)s@%(project)s>
From: %(fromaddr)s
To: %(toaddr)s
@@ -165,34 +159,56 @@ Subject: DeliverXML
%(out)s''' % locals()
- return message
+ if xmlrpc:
+ return out
+ else:
+ return mail
if __name__ == "__main__":
import getopt
+ # Get all config variables
+ revformat = do("git config --get ciabot.revformat")
+ project = do("git config --get ciabot.project")
+ repo = do("git config --get ciabot.repo")
+ xmlrpc = do("git config --get ciabot.xmlrpc")
+ xmlrpc = not (xmlrpc and xmlrpc == "false")
+
+ host = socket.getfqdn()
+ fromaddr = "CIABOT-NOREPLY@" + host
+
try:
- (options, arguments) = getopt.getopt(sys.argv[1:], "np:V")
+ (options, arguments) = getopt.getopt(sys.argv[1:], "np:xV")
except getopt.GetoptError, msg:
print "ciabot.py: " + str(msg)
raise SystemExit, 1
- mailit = True
+ notify = True
for (switch, val) in options:
if switch == '-p':
project = val
elif switch == '-n':
- mailit = False
+ notify = False
+ elif switch == '-x':
+ xmlrpc = True
elif switch == '-V':
- print "ciabot.py: version 3.2"
+ print "ciabot.py: version", version
sys.exit(0)
- # Cough and die if user has not specified a project
+ # The project variable defaults to the name of the repository toplevel.
if not project:
- sys.stderr.write("ciabot.py: no project specified, bailing out.\n")
- sys.exit(1)
-
- # We'll need the git version number.
- gitver = do("git --version").split()[0]
+ here = os.getcwd()
+ while True:
+ if os.path.exists(os.path.join(here, ".git")):
+ project = os.path.basename(here)
+ break
+ elif here == '/':
+ sys.stderr.write("ciabot.py: no .git below root!\n")
+ sys.exit(1)
+ here = os.path.dirname(here)
+
+ if not repo:
+ repo = project.lower()
urlprefix = urlprefix % globals()
@@ -205,18 +221,29 @@ if __name__ == "__main__":
refname = arguments[0]
merges = arguments[1:]
- if mailit:
- import smtplib
- server = smtplib.SMTP('localhost')
+ if notify:
+ if xmlrpc:
+ import xmlrpclib
+ server = xmlrpclib.Server('http://cia.vc/RPC2');
+ else:
+ import smtplib
+ server = smtplib.SMTP('localhost')
for merged in merges:
- message = report(refname, merged)
- if mailit:
- server.sendmail(fromaddr, [toaddr], message)
- else:
+ message = report(refname, merged, xmlrpc)
+ if not notify:
print message
+ elif xmlrpc:
+ try:
+ # RPC server is flaky, this can fail due to timeout.
+ server.hub.deliver(message)
+ except socket.error, e:
+ sys.stderr.write("%s\n" % e)
+ else:
+ server.sendmail(fromaddr, [toaddr], message)
- if mailit:
- server.quit()
+ if notify:
+ if not xmlrpc:
+ server.quit()
#End
diff --git a/contrib/ciabot/ciabot.sh b/contrib/ciabot/ciabot.sh
index eb87bba38e..3fbbc534ae 100755
--- a/contrib/ciabot/ciabot.sh
+++ b/contrib/ciabot/ciabot.sh
@@ -3,6 +3,8 @@
# Copyright (c) 2006 Fernando J. Pereda <ferdy@gentoo.org>
# Copyright (c) 2008 Natanael Copa <natanael.copa@gmail.com>
# Copyright (c) 2010 Eric S. Raymond <esr@thyrsus.com>
+# Assistance and review by Petr Baudis, author of ciabot.pl,
+# is gratefully acknowledged.
#
# This is a version 3.x of ciabot.sh; use -V to find the exact
# version. Versions 1 and 2 were shipped in 2006 and 2008 and are not
@@ -11,6 +13,7 @@
# Note: This script should be considered obsolete.
# There is a faster, better-documented rewrite in Python: find it as ciabot.py
# Use this only if your hosting site forbids Python hooks.
+# It requires: git(1), hostname(1), cut(1), sendmail(1), and wget(1).
#
# Originally based on Git ciabot.pl by Petr Baudis.
# This script contains porcelain and porcelain byproducts.
@@ -18,15 +21,13 @@
# usage: ciabot.sh [-V] [-n] [-p projectname] [refname commit]
#
# This script is meant to be run either in a post-commit hook or in an
-# update hook. If there's nothing unusual about your hosting setup,
-# you can specify the project name with a -p option and avoid having
-# to modify this script. Try it with -n first to see the notification
-# mail dumped to stdout and verify that it looks sane. Use -V to dump
-# the version and exit.
+# update hook. Try it with -n to see the notification mail dumped to
+# stdout and verify that it looks sane. With -V it dumps its version
+# and exits.
#
-# In post-commit, run it without arguments (other than possibly a -p
-# option). It will query for current HEAD and the latest commit ID to
-# get the information it needs.
+# In post-commit, run it without arguments. It will query for
+# current HEAD and the latest commit ID to get the information it
+# needs.
#
# In update, you have to call it once per merged commit:
#
@@ -34,33 +35,76 @@
# oldhead=$2
# newhead=$3
# for merged in $(git rev-list ${oldhead}..${newhead} | tac) ; do
-# /path/to/ciabot.bash ${refname} ${merged}
+# /path/to/ciabot.sh ${refname} ${merged}
# done
#
-# The reason for the tac call ids that git rev-list emits commits from
+# The reason for the tac call is that git rev-list emits commits from
# most recent to least - better to ship notifactions from oldest to newest.
#
-# Note: this script uses mail, not XML-RPC, in order to avoid stalling
-# until timeout when the CIA XML-RPC server is down.
+# Configuration variables affecting this script:
#
-
+# ciabot.project = name of the project
+# ciabot.repo = name of the project repo for gitweb/cgit purposes
+# ciabot.revformat = format in which the revision is shown
#
-# The project as known to CIA. You will either want to change this
-# or set the project name with a -p option.
+# ciabot.project defaults to the directory name of the repository toplevel.
+# ciabot.repo defaults to ciabot.project lowercased.
#
-project=
-
+# This means that in the normal case you need not do any configuration at all,
+# but setting the project name will speed it up slightly.
#
-# You may not need to change these:
+# The revformat variable may have the following values
+# raw -> full hex ID of commit
+# short -> first 12 chars of hex ID
+# describe = -> describe relative to last tag, falling back to short
+# The default is 'describe'.
#
+# Note: the shell ancestors of this script used mail, not XML-RPC, in
+# order to avoid stalling until timeout when the CIA XML-RPC server is
+# down. It is unknown whether this is still an issue in 2010, but
+# XML-RPC would be annoying to do from sh in any case. (XML-RPC does
+# have the advantage that it guarantees notification of multiple commits
+# shpped from an update in their actual order.)
+#
+
+# The project as known to CIA. You can set this with a -p option,
+# or let it default to the directory name of the repo toplevel.
+project=$(git config --get ciabot.project)
+
+if [ -z $project ]
+then
+ here=`pwd`;
+ while :; do
+ if [ -d $here/.git ]
+ then
+ project=`basename $here`
+ break
+ elif [ $here = '/' ]
+ then
+ echo "ciabot.sh: no .git below root!"
+ exit 1
+ fi
+ here=`dirname $here`
+ done
+fi
-# Name of the repository.
-# You can hardwire this to make the script faster.
-repo="`basename ${PWD}`"
+# Name of the repo for gitweb/cgit purposes
+repo=$(git config --get ciabot.repo)
+[ -z $repo] && repo=$(echo "${project}" | tr '[A-Z]' '[a-z]')
-# Fully qualified domain name of the repo host.
-# You can hardwire this to make the script faster.
-host=`hostname --fqdn`
+# What revision format do we want in the summary?
+revformat=$(git config --get ciabot.revformat)
+
+# Fully qualified domain name of the repo host. You can hardwire this
+# to make the script faster. The -f option works under Linux and FreeBSD,
+# but not OpenBSD and NetBSD. But under OpenBSD and NetBSD,
+# hostname without options gives the FQDN.
+if hostname -f >/dev/null 2>&1
+then
+ hostname=`hostname -f`
+else
+ hostname=`hostname`
+fi
# Changeset URL prefix for your repo: when the commit ID is appended
# to this, it should point at a CGI that will display the commit
@@ -73,13 +117,14 @@ urlprefix="http://${host}/cgi-bin/cgit.cgi/${repo}/commit/?id="
# You probably will not need to change the following:
#
-# Identify the script. Should change only when the script itself
-# gets a new home and maintainer.
+# Identify the script. The 'generator' variable should change only
+# when the script itself gets a new home and maintainer.
generator="http://www.catb.org/~esr/ciabot/ciabot.sh"
+version=3.5
# Addresses for the e-mail
-from="CIABOT-NOREPLY@${host}"
-to="cia@cia.navi.cx"
+from="CIABOT-NOREPLY@${hostname}"
+to="cia@cia.vc"
# SMTP client to use - may need to edit the absolute pathname for your system
sendmail="sendmail -t -f ${from}"
@@ -97,7 +142,7 @@ do
case $opt in
p) project=$2; shift ; shift ;;
n) mode=dumpit; shift ;;
- V) echo "ciabot.sh: version 3.2"; exit 0; shift ;;
+ V) echo "ciabot.sh: version $version"; exit 0; shift ;;
esac
done
@@ -128,33 +173,29 @@ fi
refname=${refname##refs/heads/}
-gitver=$(git --version)
-gitver=${gitver##* }
-
-rev=$(git describe ${merged} 2>/dev/null)
-# ${merged:0:12} was the only bashism left in the 2008 version of this
-# script, according to checkbashisms. Replace it with ${merged} here
-# because it was just a fallback anyway, and it's worth accepting a
-# longer fallback for faster execution and removing the bash
-# dependency.
-[ -z ${rev} ] && rev=${merged}
+case $revformat in
+raw) rev=$merged ;;
+short) rev='' ;;
+*) rev=$(git describe ${merged} 2>/dev/null) ;;
+esac
+[ -z ${rev} ] && rev=$(echo "$merged" | cut -c 1-12)
-# This discards the part of the author's address after @.
+# We discard the part of the author's address after @.
# Might be nice to ship the full email address, if not
# for spammers' address harvesters - getting this wrong
# would make the freenode #commits channel into harvester heaven.
-rawcommit=$(git cat-file commit ${merged})
-author=$(echo "$rawcommit" | sed -n -e '/^author .*<\([^@]*\).*$/s--\1-p')
-logmessage=$(echo "$rawcommit" | sed -e '1,/^$/d' | head -n 1)
-logmessage=$(echo "$logmessage" | sed 's/\&/&amp\;/g; s/</&lt\;/g; s/>/&gt\;/g')
-ts=$(echo "$rawcommit" | sed -n -e '/^author .*> \([0-9]\+\).*$/s--\1-p')
+author=$(git log -1 '--pretty=format:%an <%ae>' $merged)
+author=$(echo "$author" | sed -n -e '/^.*<\([^@]*\).*$/s--\1-p')
+
+logmessage=$(git log -1 '--pretty=format:%s' $merged)
+ts=$(git log -1 '--pretty=format:%at' $merged)
files=$(git diff-tree -r --name-only ${merged} | sed -e '1d' -e 's-.*-<file>&</file>-')
out="
<message>
<generator>
<name>CIA Shell client for Git</name>
- <version>${gitver}</version>
+ <version>${version}</version>
<url>${generator}</url>
</generator>
<source>
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 0acbdda3b8..222b804ce4 100755..100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -20,46 +20,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.
-#
+# 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
@@ -74,9 +36,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,232 +56,16 @@ __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=""
-
- # 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
- "<"*) let ++behind
- ;;
- *) let ++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 requires 2 arguments
__gitcomp_1 ()
{
- local c IFS=' '$'\t'$'\n'
+ local c IFS=$' \t\n'
for c in $1; do
- case "$c$2" in
- --*=*) printf %s$'\n' "$c$2" ;;
- *.) printf %s$'\n' "$c$2" ;;
- *) printf %s$'\n' "$c$2 " ;;
+ c="$c$2"
+ case $c in
+ --*=*|*.) ;;
+ *) c="$c " ;;
esac
+ printf '%s\n' "$c"
done
}
@@ -677,9 +428,7 @@ __git_complete_revlist_file ()
*) pfx="$ref:$pfx" ;;
esac
- local IFS=$'\n'
- COMPREPLY=($(compgen -P "$pfx" \
- -W "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \
+ __gitcomp_nl "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \
| sed '/^100... blob /{
s,^.* ,,
s,$, ,
@@ -693,7 +442,7 @@ __git_complete_revlist_file ()
s,$,/,
}
s/^.* //')" \
- -- "$cur_"))
+ "$pfx" "$cur_" ""
;;
*...*)
pfx="${cur_%...*}..."
@@ -726,6 +475,9 @@ __git_complete_remote_or_refspec ()
{
local cur_="$cur" cmd="${words[1]}"
local i c=2 remote="" pfx="" lhs=1 no_complete_refspec=0
+ if [ "$cmd" = "remote" ]; then
+ ((c++))
+ fi
while [ $c -lt $cword ]; do
i="${words[c]}"
case "$i" in
@@ -743,7 +495,7 @@ __git_complete_remote_or_refspec ()
-*) ;;
*) remote="$i"; break ;;
esac
- c=$((++c))
+ ((c++))
done
if [ -z "$remote" ]; then
__gitcomp_nl "$(__git_remotes)"
@@ -776,7 +528,7 @@ __git_complete_remote_or_refspec ()
__gitcomp_nl "$(__git_refs)" "$pfx" "$cur_"
fi
;;
- pull)
+ pull|remote)
if [ $lhs = 1 ]; then
__gitcomp_nl "$(__git_refs "$remote")" "$pfx" "$cur_"
else
@@ -846,6 +598,8 @@ __git_list_porcelain_commands ()
checkout-index) : plumbing;;
commit-tree) : plumbing;;
count-objects) : infrequent;;
+ credential-cache) : credentials helper;;
+ credential-store) : credentials helper;;
cvsexportcommit) : export;;
cvsimport) : import;;
cvsserver) : daemon;;
@@ -983,7 +737,7 @@ __git_find_on_cmdline ()
return
fi
done
- c=$((++c))
+ ((c++))
done
}
@@ -994,7 +748,7 @@ __git_has_doubledash ()
if [ "--" = "${words[c]}" ]; then
return 0
fi
- c=$((++c))
+ ((c++))
done
return 1
}
@@ -1117,7 +871,7 @@ _git_branch ()
-d|-m) only_local_ref="y" ;;
-r) has_r="y" ;;
esac
- c=$((++c))
+ ((c++))
done
case "$cur" in
@@ -1317,7 +1071,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 ()
@@ -1656,7 +1410,7 @@ _git_notes ()
__gitcomp '--ref'
;;
,*)
- case "${words[cword-1]}" in
+ case "$prev" in
--ref)
__gitcomp_nl "$(__git_refs)"
;;
@@ -1682,7 +1436,7 @@ _git_notes ()
prune,*)
;;
*)
- case "${words[cword-1]}" in
+ case "$prev" in
-m|-F)
;;
*)
@@ -2091,6 +1845,7 @@ _git_config ()
core.whitespace
core.worktree
diff.autorefreshindex
+ diff.statGraphWidth
diff.external
diff.ignoreSubmodules
diff.mnemonicprefix
@@ -2277,7 +2032,7 @@ _git_config ()
_git_remote ()
{
- local subcommands="add rename rm show prune update set-head"
+ local subcommands="add rename rm set-head set-branches set-url show prune update"
local subcommand="$(__git_find_on_cmdline "$subcommands")"
if [ -z "$subcommand" ]; then
__gitcomp "$subcommands"
@@ -2285,9 +2040,12 @@ _git_remote ()
fi
case "$subcommand" in
- rename|rm|show|prune)
+ rename|rm|set-url|show|prune)
__gitcomp_nl "$(__git_remotes)"
;;
+ set-head|set-branches)
+ __git_complete_remote_or_refspec
+ ;;
update)
local i c='' IFS=$'\n'
for i in $(git --git-dir="$(__gitdir)" config --get-regexp "remotes\..*" 2>/dev/null); do
@@ -2500,7 +2258,7 @@ _git_svn ()
__gitcomp "
--merge --strategy= --verbose --dry-run
--fetch-all --no-rebase --commit-url
- --revision $cmt_opts $fc_opts
+ --revision --interactive $cmt_opts $fc_opts
"
;;
set-tree,--*)
@@ -2568,7 +2326,7 @@ _git_tag ()
f=1
;;
esac
- c=$((++c))
+ ((c++))
done
case "$prev" in
@@ -2593,35 +2351,21 @@ _git_whatchanged ()
_git_log
}
-_git ()
+__git_main ()
{
local i c=1 command __git_dir
- if [[ -n ${ZSH_VERSION-} ]]; then
- emulate -L bash
- setopt KSH_TYPESET
-
- # workaround zsh's bug that leaves 'words' as a special
- # variable in versions < 4.3.12
- typeset -h words
-
- # workaround zsh's bug that quotes spaces in the COMPREPLY
- # array if IFS doesn't contain spaces.
- typeset -h IFS
- fi
-
- local cur words cword prev
- _get_comp_words_by_ref -n =: cur words cword prev
while [ $c -lt $cword ]; do
i="${words[c]}"
case "$i" in
--git-dir=*) __git_dir="${i#--git-dir=}" ;;
--bare) __git_dir="." ;;
- --version|-p|--paginate) ;;
--help) command="help"; break ;;
+ -c) c=$((++c)) ;;
+ -*) ;;
*) command="$i"; break ;;
esac
- c=$((++c))
+ ((c++))
done
if [ -z "$command" ]; then
@@ -2633,9 +2377,12 @@ _git ()
--bare
--version
--exec-path
+ --exec-path=
--html-path
+ --info-path
--work-tree=
--namespace=
+ --no-replace-objects
--help
"
;;
@@ -2655,24 +2402,8 @@ _git ()
fi
}
-_gitk ()
+__gitk_main ()
{
- if [[ -n ${ZSH_VERSION-} ]]; then
- emulate -L bash
- setopt KSH_TYPESET
-
- # workaround zsh's bug that leaves 'words' as a special
- # variable in versions < 4.3.12
- typeset -h words
-
- # workaround zsh's bug that quotes spaces in the COMPREPLY
- # array if IFS doesn't contain spaces.
- typeset -h IFS
- fi
-
- local cur words cword prev
- _get_comp_words_by_ref -n =: cur words cword prev
-
__git_has_doubledash && return
local g="$(__gitdir)"
@@ -2693,16 +2424,55 @@ _gitk ()
__git_complete_revlist
}
-complete -o bashdefault -o default -o nospace -F _git git 2>/dev/null \
- || complete -o default -o nospace -F _git git
-complete -o bashdefault -o default -o nospace -F _gitk gitk 2>/dev/null \
- || complete -o default -o nospace -F _gitk gitk
+__git_func_wrap ()
+{
+ if [[ -n ${ZSH_VERSION-} ]]; then
+ emulate -L bash
+ setopt KSH_TYPESET
+
+ # workaround zsh's bug that leaves 'words' as a special
+ # variable in versions < 4.3.12
+ typeset -h words
+
+ # workaround zsh's bug that quotes spaces in the COMPREPLY
+ # array if IFS doesn't contain spaces.
+ typeset -h IFS
+ fi
+ local cur words cword prev
+ _get_comp_words_by_ref -n =: cur words cword prev
+ $1
+}
+
+# Setup completion for certain functions defined above by setting common
+# variables and workarounds.
+# This is NOT a public function; use at your own risk.
+__git_complete ()
+{
+ local wrapper="__git_wrap${2}"
+ eval "$wrapper () { __git_func_wrap $2 ; }"
+ complete -o bashdefault -o default -o nospace -F $wrapper $1 2>/dev/null \
+ || complete -o default -o nospace -F $wrapper $1
+}
+
+# wrapper for backwards compatibility
+_git ()
+{
+ __git_wrap__git_main
+}
+
+# wrapper for backwards compatibility
+_gitk ()
+{
+ __git_wrap__gitk_main
+}
+
+__git_complete git __git_main
+__git_complete gitk __gitk_main
# The following are necessary only for Cygwin, and only are needed
# when the user has tab-completed the executable name and consequently
# included the '.exe' suffix.
#
if [ Cygwin = "$(uname -o 2>/dev/null)" ]; then
-complete -o bashdefault -o default -o nospace -F _git git.exe 2>/dev/null \
- || complete -o default -o nospace -F _git git.exe
+__git_complete git.exe __git_main
fi
diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh
new file mode 100644
index 0000000000..29b1ec9eb1
--- /dev/null
+++ b/contrib/completion/git-prompt.sh
@@ -0,0 +1,289 @@
+# 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
+# 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)")]\$ '
+#
+# 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.
+
+# __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)
+# 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
+}
diff --git a/contrib/credential/osxkeychain/Makefile b/contrib/credential/osxkeychain/Makefile
index 75c07f8be4..4b3a08a2ba 100644
--- a/contrib/credential/osxkeychain/Makefile
+++ b/contrib/credential/osxkeychain/Makefile
@@ -2,10 +2,13 @@ all:: git-credential-osxkeychain
CC = gcc
RM = rm -f
-CFLAGS = -g -Wall
+CFLAGS = -g -O2 -Wall
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
git-credential-osxkeychain: git-credential-osxkeychain.o
- $(CC) -o $@ $< -Wl,-framework -Wl,Security
+ $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) -Wl,-framework -Wl,Security
git-credential-osxkeychain.o: git-credential-osxkeychain.c
$(CC) -c $(CFLAGS) $<
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/diff-highlight/README b/contrib/diff-highlight/README
index 1b7b6df8eb..502e03b305 100644
--- a/contrib/diff-highlight/README
+++ b/contrib/diff-highlight/README
@@ -14,13 +14,15 @@ Instead, this script post-processes the line-oriented diff, finds pairs
of lines, and highlights the differing segments. It's currently very
simple and stupid about doing these tasks. In particular:
- 1. It will only highlight a pair of lines if they are the only two
- lines in a hunk. It could instead try to match up "before" and
- "after" lines for a given hunk into pairs of similar lines.
- However, this may end up visually distracting, as the paired
- lines would have other highlighted lines in between them. And in
- practice, the lines which most need attention called to their
- small, hard-to-see changes are touching only a single line.
+ 1. It will only highlight hunks in which the number of removed and
+ added lines is the same, and it will pair lines within the hunk by
+ position (so the first removed line is compared to the first added
+ line, and so forth). This is simple and tends to work well in
+ practice. More complex changes don't highlight well, so we tend to
+ exclude them due to the "same number of removed and added lines"
+ restriction. Or even if we do try to highlight them, they end up
+ not highlighting because of our "don't highlight if the whole line
+ would be highlighted" rule.
2. It will find the common prefix and suffix of two lines, and
consider everything in the middle to be "different". It could
@@ -55,3 +57,96 @@ following in your git configuration:
show = diff-highlight | less
diff = diff-highlight | less
---------------------------------------------
+
+Bugs
+----
+
+Because diff-highlight relies on heuristics to guess which parts of
+changes are important, there are some cases where the highlighting is
+more distracting than useful. Fortunately, these cases are rare in
+practice, and when they do occur, the worst case is simply a little
+extra highlighting. This section documents some cases known to be
+sub-optimal, in case somebody feels like working on improving the
+heuristics.
+
+1. Two changes on the same line get highlighted in a blob. For example,
+ highlighting:
+
+----------------------------------------------
+-foo(buf, size);
++foo(obj->buf, obj->size);
+----------------------------------------------
+
+ yields (where the inside of "+{}" would be highlighted):
+
+----------------------------------------------
+-foo(buf, size);
++foo(+{obj->buf, obj->}size);
+----------------------------------------------
+
+ whereas a more semantically meaningful output would be:
+
+----------------------------------------------
+-foo(buf, size);
++foo(+{obj->}buf, +{obj->}size);
+----------------------------------------------
+
+ Note that doing this right would probably involve a set of
+ content-specific boundary patterns, similar to word-diff. Otherwise
+ you get junk like:
+
+-----------------------------------------------------
+-this line has some -{i}nt-{ere}sti-{ng} text on it
++this line has some +{fa}nt+{a}sti+{c} text on it
+-----------------------------------------------------
+
+ which is less readable than the current output.
+
+2. The multi-line matching assumes that lines in the pre- and post-image
+ match by position. This is often the case, but can be fooled when a
+ line is removed from the top and a new one added at the bottom (or
+ vice versa). Unless the lines in the middle are also changed, diffs
+ will show this as two hunks, and it will not get highlighted at all
+ (which is good). But if the lines in the middle are changed, the
+ highlighting can be misleading. Here's a pathological case:
+
+-----------------------------------------------------
+-one
+-two
+-three
+-four
++two 2
++three 3
++four 4
++five 5
+-----------------------------------------------------
+
+ which gets highlighted as:
+
+-----------------------------------------------------
+-one
+-t-{wo}
+-three
+-f-{our}
++two 2
++t+{hree 3}
++four 4
++f+{ive 5}
+-----------------------------------------------------
+
+ because it matches "two" to "three 3", and so forth. It would be
+ nicer as:
+
+-----------------------------------------------------
+-one
+-two
+-three
+-four
++two +{2}
++three +{3}
++four +{4}
++five 5
+-----------------------------------------------------
+
+ which would probably involve pre-matching the lines into pairs
+ according to some heuristic.
diff --git a/contrib/diff-highlight/diff-highlight b/contrib/diff-highlight/diff-highlight
index d8938982e4..c4404d49c9 100755
--- a/contrib/diff-highlight/diff-highlight
+++ b/contrib/diff-highlight/diff-highlight
@@ -1,28 +1,37 @@
#!/usr/bin/perl
+use warnings FATAL => 'all';
+use strict;
+
# Highlight by reversing foreground and background. You could do
# other things like bold or underline if you prefer.
my $HIGHLIGHT = "\x1b[7m";
my $UNHIGHLIGHT = "\x1b[27m";
my $COLOR = qr/\x1b\[[0-9;]*m/;
+my $BORING = qr/$COLOR|\s/;
-my @window;
+my @removed;
+my @added;
+my $in_hunk;
while (<>) {
- # We highlight only single-line changes, so we need
- # a 4-line window to make a decision on whether
- # to highlight.
- push @window, $_;
- next if @window < 4;
- if ($window[0] =~ /^$COLOR*(\@| )/ &&
- $window[1] =~ /^$COLOR*-/ &&
- $window[2] =~ /^$COLOR*\+/ &&
- $window[3] !~ /^$COLOR*\+/) {
- print shift @window;
- show_pair(shift @window, shift @window);
+ if (!$in_hunk) {
+ print;
+ $in_hunk = /^$COLOR*\@/;
+ }
+ elsif (/^$COLOR*-/) {
+ push @removed, $_;
+ }
+ elsif (/^$COLOR*\+/) {
+ push @added, $_;
}
else {
- print shift @window;
+ show_hunk(\@removed, \@added);
+ @removed = ();
+ @added = ();
+
+ print;
+ $in_hunk = /^$COLOR*[\@ ]/;
}
# Most of the time there is enough output to keep things streaming,
@@ -38,23 +47,40 @@ while (<>) {
}
}
-# Special case a single-line hunk at the end of file.
-if (@window == 3 &&
- $window[0] =~ /^$COLOR*(\@| )/ &&
- $window[1] =~ /^$COLOR*-/ &&
- $window[2] =~ /^$COLOR*\+/) {
- print shift @window;
- show_pair(shift @window, shift @window);
-}
-
-# And then flush any remaining lines.
-while (@window) {
- print shift @window;
-}
+# Flush any queued hunk (this can happen when there is no trailing context in
+# the final diff of the input).
+show_hunk(\@removed, \@added);
exit 0;
-sub show_pair {
+sub show_hunk {
+ my ($a, $b) = @_;
+
+ # If one side is empty, then there is nothing to compare or highlight.
+ if (!@$a || !@$b) {
+ print @$a, @$b;
+ return;
+ }
+
+ # If we have mismatched numbers of lines on each side, we could try to
+ # be clever and match up similar lines. But for now we are simple and
+ # stupid, and only handle multi-line hunks that remove and add the same
+ # number of lines.
+ if (@$a != @$b) {
+ print @$a, @$b;
+ return;
+ }
+
+ my @queue;
+ for (my $i = 0; $i < @$a; $i++) {
+ my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]);
+ print $rm;
+ push @queue, $add;
+ }
+ print @queue;
+}
+
+sub highlight_pair {
my @a = split_line(shift);
my @b = split_line(shift);
@@ -101,8 +127,14 @@ sub show_pair {
}
}
- print highlight(\@a, $pa, $sa);
- print highlight(\@b, $pb, $sb);
+ if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) {
+ return highlight_line(\@a, $pa, $sa),
+ highlight_line(\@b, $pb, $sb);
+ }
+ else {
+ return join('', @a),
+ join('', @b);
+ }
}
sub split_line {
@@ -111,7 +143,7 @@ sub split_line {
split /($COLOR*)/;
}
-sub highlight {
+sub highlight_line {
my ($line, $prefix, $suffix) = @_;
return join('',
@@ -122,3 +154,20 @@ sub highlight {
@{$line}[($suffix+1)..$#$line]
);
}
+
+# Pairs are interesting to highlight only if we are going to end up
+# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting
+# is just useless noise. We can detect this by finding either a matching prefix
+# or suffix (disregarding boring bits like whitespace and colorization).
+sub is_pair_interesting {
+ my ($a, $pa, $sa, $b, $pb, $sb) = @_;
+ my $prefix_a = join('', @$a[0..($pa-1)]);
+ my $prefix_b = join('', @$b[0..($pb-1)]);
+ my $suffix_a = join('', @$a[($sa+1)..$#$a]);
+ my $suffix_b = join('', @$b[($sb+1)..$#$b]);
+
+ return $prefix_a !~ /^$COLOR*-$BORING*$/ ||
+ $prefix_b !~ /^$COLOR*\+$BORING*$/ ||
+ $suffix_a !~ /^$BORING*$/ ||
+ $suffix_b !~ /^$BORING*$/;
+}
diff --git a/contrib/diffall/README b/contrib/diffall/README
new file mode 100644
index 0000000000..507f17dcd6
--- /dev/null
+++ b/contrib/diffall/README
@@ -0,0 +1,31 @@
+The git-diffall script provides a directory based diff mechanism
+for git.
+
+To determine what diff viewer is used, the script requires either
+the 'diff.tool' or 'merge.tool' configuration option to be set.
+
+This script is compatible with most common forms used to specify a
+range of revisions to diff:
+
+ 1. git diffall: shows diff between working tree and staged changes
+ 2. git diffall --cached [<commit>]: shows diff between staged
+ changes and HEAD (or other named commit)
+ 3. git diffall <commit>: shows diff between working tree and named
+ commit
+ 4. git diffall <commit> <commit>: show diff between two named commits
+ 5. git diffall <commit>..<commit>: same as above
+ 6. git diffall <commit>...<commit>: show the changes on the branch
+ containing and up to the second, starting at a common ancestor
+ of both <commit>
+
+Note: all forms take an optional path limiter [-- <path>*]
+
+The '--extcmd=<command>' option allows the user to specify a custom
+command for viewing diffs. When given, configured defaults are
+ignored and the script runs $command $LOCAL $REMOTE. Additionally,
+$BASE is set in the environment.
+
+This script is based on an example provided by Thomas Rast on the
+Git list [1]:
+
+[1] http://thread.gmane.org/gmane.comp.version-control.git/124807
diff --git a/contrib/diffall/git-diffall b/contrib/diffall/git-diffall
new file mode 100755
index 0000000000..84f2b654d7
--- /dev/null
+++ b/contrib/diffall/git-diffall
@@ -0,0 +1,257 @@
+#!/bin/sh
+# Copyright 2010 - 2012, Tim Henigan <tim.henigan@gmail.com>
+#
+# Perform a directory diff between commits in the repository using
+# the external diff or merge tool specified in the user's config.
+
+USAGE='[--cached] [--copy-back] [-x|--extcmd=<command>] <commit>{0,2} [-- <path>*]
+
+ --cached Compare to the index rather than the working tree.
+
+ --copy-back Copy files back to the working tree when the diff
+ tool exits (in case they were modified by the
+ user). This option is only valid if the diff
+ compared with the working tree.
+
+ -x=<command>
+ --extcmd=<command> Specify a custom command for viewing diffs.
+ git-diffall ignores the configured defaults and
+ runs $command $LOCAL $REMOTE when this option is
+ specified. Additionally, $BASE is set in the
+ environment.
+'
+
+SUBDIRECTORY_OK=1
+. "$(git --exec-path)/git-sh-setup"
+
+TOOL_MODE=diff
+. "$(git --exec-path)/git-mergetool--lib"
+
+merge_tool="$(get_merge_tool)"
+if test -z "$merge_tool"
+then
+ echo "Error: Either the 'diff.tool' or 'merge.tool' option must be set."
+ usage
+fi
+
+start_dir=$(pwd)
+
+# All the file paths returned by the diff command are relative to the root
+# of the working copy. So if the script is called from a subdirectory, it
+# must switch to the root of working copy before trying to use those paths.
+cdup=$(git rev-parse --show-cdup) &&
+cd "$cdup" || {
+ echo >&2 "Cannot chdir to $cdup, the toplevel of the working tree"
+ exit 1
+}
+
+# set up temp dir
+tmp=$(perl -e 'use File::Temp qw(tempdir);
+ $t=tempdir("/tmp/git-diffall.XXXXX") or exit(1);
+ print $t') || exit 1
+trap 'rm -rf "$tmp"' EXIT
+
+left=
+right=
+paths=
+dashdash_seen=
+compare_staged=
+merge_base=
+left_dir=
+right_dir=
+diff_tool=
+copy_back=
+
+while test $# != 0
+do
+ case "$1" in
+ -h|--h|--he|--hel|--help)
+ usage
+ ;;
+ --cached)
+ compare_staged=1
+ ;;
+ --copy-back)
+ copy_back=1
+ ;;
+ -x|--e|--ex|--ext|--extc|--extcm|--extcmd)
+ if test $# = 1
+ then
+ echo You must specify the tool for use with --extcmd
+ usage
+ else
+ diff_tool=$2
+ shift
+ fi
+ ;;
+ --)
+ dashdash_seen=1
+ ;;
+ -*)
+ echo Invalid option: "$1"
+ usage
+ ;;
+ *)
+ # could be commit, commit range or path limiter
+ case "$1" in
+ *...*)
+ left=${1%...*}
+ right=${1#*...}
+ merge_base=1
+ ;;
+ *..*)
+ left=${1%..*}
+ right=${1#*..}
+ ;;
+ *)
+ if test -n "$dashdash_seen"
+ then
+ paths="$paths$1 "
+ elif test -z "$left"
+ then
+ left=$1
+ elif test -z "$right"
+ then
+ right=$1
+ else
+ paths="$paths$1 "
+ fi
+ ;;
+ esac
+ ;;
+ esac
+ shift
+done
+
+# Determine the set of files which changed
+if test -n "$left" && test -n "$right"
+then
+ left_dir="cmt-$(git rev-parse --short $left)"
+ right_dir="cmt-$(git rev-parse --short $right)"
+
+ if test -n "$compare_staged"
+ then
+ usage
+ elif test -n "$merge_base"
+ then
+ git diff --name-only "$left"..."$right" -- $paths >"$tmp/filelist"
+ else
+ git diff --name-only "$left" "$right" -- $paths >"$tmp/filelist"
+ fi
+elif test -n "$left"
+then
+ left_dir="cmt-$(git rev-parse --short $left)"
+
+ if test -n "$compare_staged"
+ then
+ right_dir="staged"
+ git diff --name-only --cached "$left" -- $paths >"$tmp/filelist"
+ else
+ right_dir="working_tree"
+ git diff --name-only "$left" -- $paths >"$tmp/filelist"
+ fi
+else
+ left_dir="HEAD"
+
+ if test -n "$compare_staged"
+ then
+ right_dir="staged"
+ git diff --name-only --cached -- $paths >"$tmp/filelist"
+ else
+ right_dir="working_tree"
+ git diff --name-only -- $paths >"$tmp/filelist"
+ fi
+fi
+
+# Exit immediately if there are no diffs
+if test ! -s "$tmp/filelist"
+then
+ exit 0
+fi
+
+if test -n "$copy_back" && test "$right_dir" != "working_tree"
+then
+ echo "--copy-back is only valid when diff includes the working tree."
+ exit 1
+fi
+
+# Create the named tmp directories that will hold the files to be compared
+mkdir -p "$tmp/$left_dir" "$tmp/$right_dir"
+
+# Populate the tmp/right_dir directory with the files to be compared
+while read name
+do
+ if test -n "$right"
+ then
+ ls_list=$(git ls-tree $right "$name")
+ if test -n "$ls_list"
+ then
+ mkdir -p "$tmp/$right_dir/$(dirname "$name")"
+ git show "$right":"$name" >"$tmp/$right_dir/$name" || true
+ fi
+ elif test -n "$compare_staged"
+ then
+ ls_list=$(git ls-files -- "$name")
+ if test -n "$ls_list"
+ then
+ mkdir -p "$tmp/$right_dir/$(dirname "$name")"
+ git show :"$name" >"$tmp/$right_dir/$name"
+ fi
+ else
+ if test -e "$name"
+ then
+ mkdir -p "$tmp/$right_dir/$(dirname "$name")"
+ cp "$name" "$tmp/$right_dir/$name"
+ fi
+ fi
+done < "$tmp/filelist"
+
+# Populate the tmp/left_dir directory with the files to be compared
+while read name
+do
+ if test -n "$left"
+ then
+ ls_list=$(git ls-tree $left "$name")
+ if test -n "$ls_list"
+ then
+ mkdir -p "$tmp/$left_dir/$(dirname "$name")"
+ git show "$left":"$name" >"$tmp/$left_dir/$name" || true
+ fi
+ else
+ if test -n "$compare_staged"
+ then
+ ls_list=$(git ls-tree HEAD "$name")
+ if test -n "$ls_list"
+ then
+ mkdir -p "$tmp/$left_dir/$(dirname "$name")"
+ git show HEAD:"$name" >"$tmp/$left_dir/$name"
+ fi
+ else
+ mkdir -p "$tmp/$left_dir/$(dirname "$name")"
+ git show :"$name" >"$tmp/$left_dir/$name"
+ fi
+ fi
+done < "$tmp/filelist"
+
+LOCAL="$tmp/$left_dir"
+REMOTE="$tmp/$right_dir"
+
+if test -n "$diff_tool"
+then
+ export BASE
+ eval $diff_tool '"$LOCAL"' '"$REMOTE"'
+else
+ run_merge_tool "$merge_tool" false
+fi
+
+# Copy files back to the working dir, if requested
+if test -n "$copy_back" && test "$right_dir" = "working_tree"
+then
+ cd "$start_dir"
+ git_top_dir=$(git rev-parse --show-toplevel)
+ find "$tmp/$right_dir" -type f |
+ while read file
+ do
+ cp "$file" "$git_top_dir/${file#$tmp/$right_dir/}"
+ done
+fi
diff --git a/contrib/emacs/git-blame.el b/contrib/emacs/git-blame.el
index d351cfb6e7..e671f6c1c6 100644
--- a/contrib/emacs/git-blame.el
+++ b/contrib/emacs/git-blame.el
@@ -304,7 +304,7 @@ See also function `git-blame-mode'."
(defun git-blame-cleanup ()
"Remove all blame properties"
- (mapcar 'delete-overlay git-blame-overlays)
+ (mapc 'delete-overlay git-blame-overlays)
(setq git-blame-overlays nil)
(remove-git-blame-text-properties (point-min) (point-max)))
@@ -337,16 +337,16 @@ See also function `git-blame-mode'."
(defvar in-blame-filter nil)
(defun git-blame-filter (proc str)
- (save-excursion
- (set-buffer (process-buffer proc))
- (goto-char (process-mark proc))
- (insert-before-markers str)
- (goto-char 0)
- (unless in-blame-filter
- (let ((more t)
- (in-blame-filter t))
- (while more
- (setq more (git-blame-parse)))))))
+ (with-current-buffer (process-buffer proc)
+ (save-excursion
+ (goto-char (process-mark proc))
+ (insert-before-markers str)
+ (goto-char (point-min))
+ (unless in-blame-filter
+ (let ((more t)
+ (in-blame-filter t))
+ (while more
+ (setq more (git-blame-parse))))))))
(defun git-blame-parse ()
(cond ((looking-at "\\([0-9a-f]\\{40\\}\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\)\n")
@@ -385,32 +385,33 @@ See also function `git-blame-mode'."
info))))
(defun git-blame-create-overlay (info start-line num-lines)
- (save-excursion
- (set-buffer git-blame-file)
- (let ((inhibit-point-motion-hooks t)
- (inhibit-modification-hooks t))
- (goto-line start-line)
- (let* ((start (point))
- (end (progn (forward-line num-lines) (point)))
- (ovl (make-overlay start end))
- (hash (car info))
- (spec `((?h . ,(substring hash 0 6))
- (?H . ,hash)
- (?a . ,(git-blame-get-info info 'author))
- (?A . ,(git-blame-get-info info 'author-mail))
- (?c . ,(git-blame-get-info info 'committer))
- (?C . ,(git-blame-get-info info 'committer-mail))
- (?s . ,(git-blame-get-info info 'summary)))))
- (push ovl git-blame-overlays)
- (overlay-put ovl 'git-blame info)
- (overlay-put ovl 'help-echo
- (format-spec git-blame-mouseover-format spec))
- (if git-blame-use-colors
- (overlay-put ovl 'face (list :background
- (cdr (assq 'color (cdr info))))))
- (overlay-put ovl 'line-prefix
- (propertize (format-spec git-blame-prefix-format spec)
- 'face 'git-blame-prefix-face))))))
+ (with-current-buffer git-blame-file
+ (save-excursion
+ (let ((inhibit-point-motion-hooks t)
+ (inhibit-modification-hooks t))
+ (goto-char (point-min))
+ (forward-line (1- start-line))
+ (let* ((start (point))
+ (end (progn (forward-line num-lines) (point)))
+ (ovl (make-overlay start end))
+ (hash (car info))
+ (spec `((?h . ,(substring hash 0 6))
+ (?H . ,hash)
+ (?a . ,(git-blame-get-info info 'author))
+ (?A . ,(git-blame-get-info info 'author-mail))
+ (?c . ,(git-blame-get-info info 'committer))
+ (?C . ,(git-blame-get-info info 'committer-mail))
+ (?s . ,(git-blame-get-info info 'summary)))))
+ (push ovl git-blame-overlays)
+ (overlay-put ovl 'git-blame info)
+ (overlay-put ovl 'help-echo
+ (format-spec git-blame-mouseover-format spec))
+ (if git-blame-use-colors
+ (overlay-put ovl 'face (list :background
+ (cdr (assq 'color (cdr info))))))
+ (overlay-put ovl 'line-prefix
+ (propertize (format-spec git-blame-prefix-format spec)
+ 'face 'git-blame-prefix-face)))))))
(defun git-blame-add-info (info key value)
(nconc info (list (cons (intern key) value))))
diff --git a/contrib/examples/builtin-fetch--tool.c b/contrib/examples/builtin-fetch--tool.c
index 3140e405fa..0d54aa7061 100644
--- a/contrib/examples/builtin-fetch--tool.c
+++ b/contrib/examples/builtin-fetch--tool.c
@@ -518,7 +518,7 @@ int cmd_fetch__tool(int argc, const char **argv, const char *prefix)
filename = git_path("FETCH_HEAD");
fp = fopen(filename, "a");
if (!fp)
- return error("cannot open %s: %s\n", filename, strerror(errno));
+ return error("cannot open %s: %s", filename, strerror(errno));
result = append_fetch_head(fp, argv[2], argv[3],
argv[4], argv[5],
argv[6], !!argv[7][0],
@@ -536,7 +536,7 @@ int cmd_fetch__tool(int argc, const char **argv, const char *prefix)
filename = git_path("FETCH_HEAD");
fp = fopen(filename, "a");
if (!fp)
- return error("cannot open %s: %s\n", filename, strerror(errno));
+ return error("cannot open %s: %s", filename, strerror(errno));
result = fetch_native_store(fp, argv[2], argv[3], argv[4],
verbose, force);
fclose(fp);
diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4
deleted file mode 100755
index 9ccc87b205..0000000000
--- a/contrib/fast-import/git-p4
+++ /dev/null
@@ -1,2606 +0,0 @@
-#!/usr/bin/env python
-#
-# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
-#
-# Author: Simon Hausmann <simon@lst.de>
-# Copyright: 2007 Simon Hausmann <simon@lst.de>
-# 2007 Trolltech ASA
-# License: MIT <http://www.opensource.org/licenses/mit-license.php>
-#
-
-import optparse, sys, os, marshal, subprocess, shelve
-import tempfile, getopt, os.path, time, platform
-import re
-
-verbose = False
-
-
-def p4_build_cmd(cmd):
- """Build a suitable p4 command line.
-
- This consolidates building and returning a p4 command line into one
- location. It means that hooking into the environment, or other configuration
- can be done more easily.
- """
- real_cmd = ["p4"]
-
- user = gitConfig("git-p4.user")
- if len(user) > 0:
- real_cmd += ["-u",user]
-
- password = gitConfig("git-p4.password")
- if len(password) > 0:
- real_cmd += ["-P", password]
-
- port = gitConfig("git-p4.port")
- if len(port) > 0:
- real_cmd += ["-p", port]
-
- host = gitConfig("git-p4.host")
- if len(host) > 0:
- real_cmd += ["-h", host]
-
- client = gitConfig("git-p4.client")
- if len(client) > 0:
- real_cmd += ["-c", client]
-
-
- if isinstance(cmd,basestring):
- real_cmd = ' '.join(real_cmd) + ' ' + cmd
- else:
- real_cmd += cmd
- return real_cmd
-
-def chdir(dir):
- # P4 uses the PWD environment variable rather than getcwd(). Since we're
- # not using the shell, we have to set it ourselves. This path could
- # be relative, so go there first, then figure out where we ended up.
- os.chdir(dir)
- os.environ['PWD'] = os.getcwd()
-
-def die(msg):
- if verbose:
- raise Exception(msg)
- else:
- sys.stderr.write(msg + "\n")
- sys.exit(1)
-
-def write_pipe(c, stdin):
- if verbose:
- sys.stderr.write('Writing pipe: %s\n' % str(c))
-
- expand = isinstance(c,basestring)
- p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
- pipe = p.stdin
- val = pipe.write(stdin)
- pipe.close()
- if p.wait():
- die('Command failed: %s' % str(c))
-
- return val
-
-def p4_write_pipe(c, stdin):
- real_cmd = p4_build_cmd(c)
- return write_pipe(real_cmd, stdin)
-
-def read_pipe(c, ignore_error=False):
- if verbose:
- sys.stderr.write('Reading pipe: %s\n' % str(c))
-
- expand = isinstance(c,basestring)
- p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
- pipe = p.stdout
- val = pipe.read()
- if p.wait() and not ignore_error:
- die('Command failed: %s' % str(c))
-
- return val
-
-def p4_read_pipe(c, ignore_error=False):
- real_cmd = p4_build_cmd(c)
- return read_pipe(real_cmd, ignore_error)
-
-def read_pipe_lines(c):
- if verbose:
- sys.stderr.write('Reading pipe: %s\n' % str(c))
-
- expand = isinstance(c, basestring)
- p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
- pipe = p.stdout
- val = pipe.readlines()
- if pipe.close() or p.wait():
- die('Command failed: %s' % str(c))
-
- return val
-
-def p4_read_pipe_lines(c):
- """Specifically invoke p4 on the command supplied. """
- real_cmd = p4_build_cmd(c)
- return read_pipe_lines(real_cmd)
-
-def system(cmd):
- expand = isinstance(cmd,basestring)
- if verbose:
- sys.stderr.write("executing %s\n" % str(cmd))
- subprocess.check_call(cmd, shell=expand)
-
-def p4_system(cmd):
- """Specifically invoke p4 as the system command. """
- real_cmd = p4_build_cmd(cmd)
- expand = isinstance(real_cmd, basestring)
- subprocess.check_call(real_cmd, shell=expand)
-
-def p4_integrate(src, dest):
- p4_system(["integrate", "-Dt", src, dest])
-
-def p4_sync(path):
- p4_system(["sync", path])
-
-def p4_add(f):
- p4_system(["add", f])
-
-def p4_delete(f):
- p4_system(["delete", f])
-
-def p4_edit(f):
- p4_system(["edit", f])
-
-def p4_revert(f):
- p4_system(["revert", f])
-
-def p4_reopen(type, file):
- p4_system(["reopen", "-t", type, file])
-
-#
-# Canonicalize the p4 type and return a tuple of the
-# base type, plus any modifiers. See "p4 help filetypes"
-# for a list and explanation.
-#
-def split_p4_type(p4type):
-
- p4_filetypes_historical = {
- "ctempobj": "binary+Sw",
- "ctext": "text+C",
- "cxtext": "text+Cx",
- "ktext": "text+k",
- "kxtext": "text+kx",
- "ltext": "text+F",
- "tempobj": "binary+FSw",
- "ubinary": "binary+F",
- "uresource": "resource+F",
- "uxbinary": "binary+Fx",
- "xbinary": "binary+x",
- "xltext": "text+Fx",
- "xtempobj": "binary+Swx",
- "xtext": "text+x",
- "xunicode": "unicode+x",
- "xutf16": "utf16+x",
- }
- if p4type in p4_filetypes_historical:
- p4type = p4_filetypes_historical[p4type]
- mods = ""
- s = p4type.split("+")
- base = s[0]
- mods = ""
- if len(s) > 1:
- mods = s[1]
- return (base, mods)
-
-
-def setP4ExecBit(file, mode):
- # Reopens an already open file and changes the execute bit to match
- # the execute bit setting in the passed in mode.
-
- p4Type = "+x"
-
- if not isModeExec(mode):
- p4Type = getP4OpenedType(file)
- p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
- p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
- if p4Type[-1] == "+":
- p4Type = p4Type[0:-1]
-
- p4_reopen(p4Type, file)
-
-def getP4OpenedType(file):
- # Returns the perforce file type for the given file.
-
- result = p4_read_pipe(["opened", file])
- match = re.match(".*\((.+)\)\r?$", result)
- if match:
- return match.group(1)
- else:
- die("Could not determine file type for %s (result: '%s')" % (file, result))
-
-def diffTreePattern():
- # This is a simple generator for the diff tree regex pattern. This could be
- # a class variable if this and parseDiffTreeEntry were a part of a class.
- pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
- while True:
- yield pattern
-
-def parseDiffTreeEntry(entry):
- """Parses a single diff tree entry into its component elements.
-
- See git-diff-tree(1) manpage for details about the format of the diff
- output. This method returns a dictionary with the following elements:
-
- src_mode - The mode of the source file
- dst_mode - The mode of the destination file
- src_sha1 - The sha1 for the source file
- dst_sha1 - The sha1 fr the destination file
- status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
- status_score - The score for the status (applicable for 'C' and 'R'
- statuses). This is None if there is no score.
- src - The path for the source file.
- dst - The path for the destination file. This is only present for
- copy or renames. If it is not present, this is None.
-
- If the pattern is not matched, None is returned."""
-
- match = diffTreePattern().next().match(entry)
- if match:
- return {
- 'src_mode': match.group(1),
- 'dst_mode': match.group(2),
- 'src_sha1': match.group(3),
- 'dst_sha1': match.group(4),
- 'status': match.group(5),
- 'status_score': match.group(6),
- 'src': match.group(7),
- 'dst': match.group(10)
- }
- return None
-
-def isModeExec(mode):
- # Returns True if the given git mode represents an executable file,
- # otherwise False.
- return mode[-3:] == "755"
-
-def isModeExecChanged(src_mode, dst_mode):
- return isModeExec(src_mode) != isModeExec(dst_mode)
-
-def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
-
- if isinstance(cmd,basestring):
- cmd = "-G " + cmd
- expand = True
- else:
- cmd = ["-G"] + cmd
- expand = False
-
- cmd = p4_build_cmd(cmd)
- if verbose:
- sys.stderr.write("Opening pipe: %s\n" % str(cmd))
-
- # Use a temporary file to avoid deadlocks without
- # subprocess.communicate(), which would put another copy
- # of stdout into memory.
- stdin_file = None
- if stdin is not None:
- stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
- if isinstance(stdin,basestring):
- stdin_file.write(stdin)
- else:
- for i in stdin:
- stdin_file.write(i + '\n')
- stdin_file.flush()
- stdin_file.seek(0)
-
- p4 = subprocess.Popen(cmd,
- shell=expand,
- stdin=stdin_file,
- stdout=subprocess.PIPE)
-
- result = []
- try:
- while True:
- entry = marshal.load(p4.stdout)
- if cb is not None:
- cb(entry)
- else:
- result.append(entry)
- except EOFError:
- pass
- exitCode = p4.wait()
- if exitCode != 0:
- entry = {}
- entry["p4ExitCode"] = exitCode
- result.append(entry)
-
- return result
-
-def p4Cmd(cmd):
- list = p4CmdList(cmd)
- result = {}
- for entry in list:
- result.update(entry)
- return result;
-
-def p4Where(depotPath):
- if not depotPath.endswith("/"):
- depotPath += "/"
- depotPath = depotPath + "..."
- outputList = p4CmdList(["where", depotPath])
- output = None
- for entry in outputList:
- if "depotFile" in entry:
- if entry["depotFile"] == depotPath:
- output = entry
- break
- elif "data" in entry:
- data = entry.get("data")
- space = data.find(" ")
- if data[:space] == depotPath:
- output = entry
- break
- if output == None:
- return ""
- if output["code"] == "error":
- return ""
- clientPath = ""
- if "path" in output:
- clientPath = output.get("path")
- elif "data" in output:
- data = output.get("data")
- lastSpace = data.rfind(" ")
- clientPath = data[lastSpace + 1:]
-
- if clientPath.endswith("..."):
- clientPath = clientPath[:-3]
- return clientPath
-
-def currentGitBranch():
- return read_pipe("git name-rev HEAD").split(" ")[1].strip()
-
-def isValidGitDir(path):
- if (os.path.exists(path + "/HEAD")
- and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
- return True;
- return False
-
-def parseRevision(ref):
- return read_pipe("git rev-parse %s" % ref).strip()
-
-def branchExists(ref):
- rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
- ignore_error=True)
- return len(rev) > 0
-
-def extractLogMessageFromGitCommit(commit):
- logMessage = ""
-
- ## fixme: title is first line of commit, not 1st paragraph.
- foundTitle = False
- for log in read_pipe_lines("git cat-file commit %s" % commit):
- if not foundTitle:
- if len(log) == 1:
- foundTitle = True
- continue
-
- logMessage += log
- return logMessage
-
-def extractSettingsGitLog(log):
- values = {}
- for line in log.split("\n"):
- line = line.strip()
- m = re.search (r"^ *\[git-p4: (.*)\]$", line)
- if not m:
- continue
-
- assignments = m.group(1).split (':')
- for a in assignments:
- vals = a.split ('=')
- key = vals[0].strip()
- val = ('='.join (vals[1:])).strip()
- if val.endswith ('\"') and val.startswith('"'):
- val = val[1:-1]
-
- values[key] = val
-
- paths = values.get("depot-paths")
- if not paths:
- paths = values.get("depot-path")
- if paths:
- values['depot-paths'] = paths.split(',')
- return values
-
-def gitBranchExists(branch):
- proc = subprocess.Popen(["git", "rev-parse", branch],
- stderr=subprocess.PIPE, stdout=subprocess.PIPE);
- return proc.wait() == 0;
-
-_gitConfig = {}
-def gitConfig(key, args = None): # set args to "--bool", for instance
- if not _gitConfig.has_key(key):
- argsFilter = ""
- if args != None:
- argsFilter = "%s " % args
- cmd = "git config %s%s" % (argsFilter, key)
- _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
- return _gitConfig[key]
-
-def gitConfigList(key):
- if not _gitConfig.has_key(key):
- _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep)
- return _gitConfig[key]
-
-def p4BranchesInGit(branchesAreInRemotes = True):
- branches = {}
-
- cmdline = "git rev-parse --symbolic "
- if branchesAreInRemotes:
- cmdline += " --remotes"
- else:
- cmdline += " --branches"
-
- for line in read_pipe_lines(cmdline):
- line = line.strip()
-
- ## only import to p4/
- if not line.startswith('p4/') or line == "p4/HEAD":
- continue
- branch = line
-
- # strip off p4
- branch = re.sub ("^p4/", "", line)
-
- branches[branch] = parseRevision(line)
- return branches
-
-def findUpstreamBranchPoint(head = "HEAD"):
- branches = p4BranchesInGit()
- # map from depot-path to branch name
- branchByDepotPath = {}
- for branch in branches.keys():
- tip = branches[branch]
- log = extractLogMessageFromGitCommit(tip)
- settings = extractSettingsGitLog(log)
- if settings.has_key("depot-paths"):
- paths = ",".join(settings["depot-paths"])
- branchByDepotPath[paths] = "remotes/p4/" + branch
-
- settings = None
- parent = 0
- while parent < 65535:
- commit = head + "~%s" % parent
- log = extractLogMessageFromGitCommit(commit)
- settings = extractSettingsGitLog(log)
- if settings.has_key("depot-paths"):
- paths = ",".join(settings["depot-paths"])
- if branchByDepotPath.has_key(paths):
- return [branchByDepotPath[paths], settings]
-
- parent = parent + 1
-
- return ["", settings]
-
-def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
- if not silent:
- print ("Creating/updating branch(es) in %s based on origin branch(es)"
- % localRefPrefix)
-
- originPrefix = "origin/p4/"
-
- for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
- line = line.strip()
- if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
- continue
-
- headName = line[len(originPrefix):]
- remoteHead = localRefPrefix + headName
- originHead = line
-
- original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
- if (not original.has_key('depot-paths')
- or not original.has_key('change')):
- continue
-
- update = False
- if not gitBranchExists(remoteHead):
- if verbose:
- print "creating %s" % remoteHead
- update = True
- else:
- settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
- if settings.has_key('change') > 0:
- if settings['depot-paths'] == original['depot-paths']:
- originP4Change = int(original['change'])
- p4Change = int(settings['change'])
- if originP4Change > p4Change:
- print ("%s (%s) is newer than %s (%s). "
- "Updating p4 branch from origin."
- % (originHead, originP4Change,
- remoteHead, p4Change))
- update = True
- else:
- print ("Ignoring: %s was imported from %s while "
- "%s was imported from %s"
- % (originHead, ','.join(original['depot-paths']),
- remoteHead, ','.join(settings['depot-paths'])))
-
- if update:
- system("git update-ref %s %s" % (remoteHead, originHead))
-
-def originP4BranchesExist():
- return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
-
-def p4ChangesForPaths(depotPaths, changeRange):
- assert depotPaths
- cmd = ['changes']
- for p in depotPaths:
- cmd += ["%s...%s" % (p, changeRange)]
- output = p4_read_pipe_lines(cmd)
-
- changes = {}
- for line in output:
- changeNum = int(line.split(" ")[1])
- changes[changeNum] = True
-
- changelist = changes.keys()
- changelist.sort()
- return changelist
-
-def p4PathStartsWith(path, prefix):
- # This method tries to remedy a potential mixed-case issue:
- #
- # If UserA adds //depot/DirA/file1
- # and UserB adds //depot/dira/file2
- #
- # we may or may not have a problem. If you have core.ignorecase=true,
- # we treat DirA and dira as the same directory
- ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
- if ignorecase:
- return path.lower().startswith(prefix.lower())
- return path.startswith(prefix)
-
-def getClientSpec():
- """Look at the p4 client spec, create a View() object that contains
- all the mappings, and return it."""
-
- specList = p4CmdList("client -o")
- if len(specList) != 1:
- die('Output from "client -o" is %d lines, expecting 1' %
- len(specList))
-
- # dictionary of all client parameters
- entry = specList[0]
-
- # just the keys that start with "View"
- view_keys = [ k for k in entry.keys() if k.startswith("View") ]
-
- # hold this new View
- view = View()
-
- # append the lines, in order, to the view
- for view_num in range(len(view_keys)):
- k = "View%d" % view_num
- if k not in view_keys:
- die("Expected view key %s missing" % k)
- view.append(entry[k])
-
- return view
-
-def getClientRoot():
- """Grab the client directory."""
-
- output = p4CmdList("client -o")
- if len(output) != 1:
- die('Output from "client -o" is %d lines, expecting 1' % len(output))
-
- entry = output[0]
- if "Root" not in entry:
- die('Client has no "Root"')
-
- return entry["Root"]
-
-class Command:
- def __init__(self):
- self.usage = "usage: %prog [options]"
- self.needsGit = True
-
-class P4UserMap:
- def __init__(self):
- self.userMapFromPerforceServer = False
-
- def getUserCacheFilename(self):
- home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
- return home + "/.gitp4-usercache.txt"
-
- def getUserMapFromPerforceServer(self):
- if self.userMapFromPerforceServer:
- return
- self.users = {}
- self.emails = {}
-
- for output in p4CmdList("users"):
- if not output.has_key("User"):
- continue
- self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
- self.emails[output["Email"]] = output["User"]
-
-
- s = ''
- for (key, val) in self.users.items():
- s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
-
- open(self.getUserCacheFilename(), "wb").write(s)
- self.userMapFromPerforceServer = True
-
- def loadUserMapFromCache(self):
- self.users = {}
- self.userMapFromPerforceServer = False
- try:
- cache = open(self.getUserCacheFilename(), "rb")
- lines = cache.readlines()
- cache.close()
- for line in lines:
- entry = line.strip().split("\t")
- self.users[entry[0]] = entry[1]
- except IOError:
- self.getUserMapFromPerforceServer()
-
-class P4Debug(Command):
- def __init__(self):
- Command.__init__(self)
- self.options = [
- optparse.make_option("--verbose", dest="verbose", action="store_true",
- default=False),
- ]
- self.description = "A tool to debug the output of p4 -G."
- self.needsGit = False
- self.verbose = False
-
- def run(self, args):
- j = 0
- for output in p4CmdList(args):
- print 'Element: %d' % j
- j += 1
- print output
- return True
-
-class P4RollBack(Command):
- def __init__(self):
- Command.__init__(self)
- self.options = [
- optparse.make_option("--verbose", dest="verbose", action="store_true"),
- optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
- ]
- self.description = "A tool to debug the multi-branch import. Don't use :)"
- self.verbose = False
- self.rollbackLocalBranches = False
-
- def run(self, args):
- if len(args) != 1:
- return False
- maxChange = int(args[0])
-
- if "p4ExitCode" in p4Cmd("changes -m 1"):
- die("Problems executing p4");
-
- if self.rollbackLocalBranches:
- refPrefix = "refs/heads/"
- lines = read_pipe_lines("git rev-parse --symbolic --branches")
- else:
- refPrefix = "refs/remotes/"
- lines = read_pipe_lines("git rev-parse --symbolic --remotes")
-
- for line in lines:
- if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
- line = line.strip()
- ref = refPrefix + line
- log = extractLogMessageFromGitCommit(ref)
- settings = extractSettingsGitLog(log)
-
- depotPaths = settings['depot-paths']
- change = settings['change']
-
- changed = False
-
- if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
- for p in depotPaths]))) == 0:
- print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
- system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
- continue
-
- while change and int(change) > maxChange:
- changed = True
- if self.verbose:
- print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
- system("git update-ref %s \"%s^\"" % (ref, ref))
- log = extractLogMessageFromGitCommit(ref)
- settings = extractSettingsGitLog(log)
-
-
- depotPaths = settings['depot-paths']
- change = settings['change']
-
- if changed:
- print "%s rewound to %s" % (ref, change)
-
- return True
-
-class P4Submit(Command, P4UserMap):
- def __init__(self):
- Command.__init__(self)
- P4UserMap.__init__(self)
- self.options = [
- optparse.make_option("--verbose", dest="verbose", action="store_true"),
- optparse.make_option("--origin", dest="origin"),
- optparse.make_option("-M", dest="detectRenames", action="store_true"),
- # preserve the user, requires relevant p4 permissions
- optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
- ]
- self.description = "Submit changes from git to the perforce depot."
- self.usage += " [name of git branch to submit into perforce depot]"
- self.interactive = True
- self.origin = ""
- self.detectRenames = False
- self.verbose = False
- self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
- self.isWindows = (platform.system() == "Windows")
- self.myP4UserId = None
-
- def check(self):
- if len(p4CmdList("opened ...")) > 0:
- die("You have files opened with perforce! Close them before starting the sync.")
-
- # replaces everything between 'Description:' and the next P4 submit template field with the
- # commit message
- def prepareLogMessage(self, template, message):
- result = ""
-
- inDescriptionSection = False
-
- for line in template.split("\n"):
- if line.startswith("#"):
- result += line + "\n"
- continue
-
- if inDescriptionSection:
- if line.startswith("Files:") or line.startswith("Jobs:"):
- inDescriptionSection = False
- else:
- continue
- else:
- if line.startswith("Description:"):
- inDescriptionSection = True
- line += "\n"
- for messageLine in message.split("\n"):
- line += "\t" + messageLine + "\n"
-
- result += line + "\n"
-
- return result
-
- def p4UserForCommit(self,id):
- # Return the tuple (perforce user,git email) for a given git commit id
- self.getUserMapFromPerforceServer()
- gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
- gitEmail = gitEmail.strip()
- if not self.emails.has_key(gitEmail):
- return (None,gitEmail)
- else:
- return (self.emails[gitEmail],gitEmail)
-
- def checkValidP4Users(self,commits):
- # check if any git authors cannot be mapped to p4 users
- for id in commits:
- (user,email) = self.p4UserForCommit(id)
- if not user:
- msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
- if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
- print "%s" % msg
- else:
- die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
-
- def lastP4Changelist(self):
- # Get back the last changelist number submitted in this client spec. This
- # then gets used to patch up the username in the change. If the same
- # client spec is being used by multiple processes then this might go
- # wrong.
- results = p4CmdList("client -o") # find the current client
- client = None
- for r in results:
- if r.has_key('Client'):
- client = r['Client']
- break
- if not client:
- die("could not get client spec")
- results = p4CmdList(["changes", "-c", client, "-m", "1"])
- for r in results:
- if r.has_key('change'):
- return r['change']
- die("Could not get changelist number for last submit - cannot patch up user details")
-
- def modifyChangelistUser(self, changelist, newUser):
- # fixup the user field of a changelist after it has been submitted.
- changes = p4CmdList("change -o %s" % changelist)
- if len(changes) != 1:
- die("Bad output from p4 change modifying %s to user %s" %
- (changelist, newUser))
-
- c = changes[0]
- if c['User'] == newUser: return # nothing to do
- c['User'] = newUser
- input = marshal.dumps(c)
-
- result = p4CmdList("change -f -i", stdin=input)
- for r in result:
- if r.has_key('code'):
- if r['code'] == 'error':
- die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
- if r.has_key('data'):
- print("Updated user field for changelist %s to %s" % (changelist, newUser))
- return
- die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
-
- def canChangeChangelists(self):
- # check to see if we have p4 admin or super-user permissions, either of
- # which are required to modify changelists.
- results = p4CmdList("protects %s" % self.depotPath)
- for r in results:
- if r.has_key('perm'):
- if r['perm'] == 'admin':
- return 1
- if r['perm'] == 'super':
- return 1
- return 0
-
- def p4UserId(self):
- if self.myP4UserId:
- return self.myP4UserId
-
- results = p4CmdList("user -o")
- for r in results:
- if r.has_key('User'):
- self.myP4UserId = r['User']
- return r['User']
- die("Could not find your p4 user id")
-
- def p4UserIsMe(self, p4User):
- # return True if the given p4 user is actually me
- me = self.p4UserId()
- if not p4User or p4User != me:
- return False
- else:
- return True
-
- def prepareSubmitTemplate(self):
- # remove lines in the Files section that show changes to files outside the depot path we're committing into
- template = ""
- inFilesSection = False
- for line in p4_read_pipe_lines(['change', '-o']):
- if line.endswith("\r\n"):
- line = line[:-2] + "\n"
- if inFilesSection:
- if line.startswith("\t"):
- # path starts and ends with a tab
- path = line[1:]
- lastTab = path.rfind("\t")
- if lastTab != -1:
- path = path[:lastTab]
- if not p4PathStartsWith(path, self.depotPath):
- continue
- else:
- inFilesSection = False
- else:
- if line.startswith("Files:"):
- inFilesSection = True
-
- template += line
-
- return template
-
- def edit_template(self, template_file):
- """Invoke the editor to let the user change the submission
- message. Return true if okay to continue with the submit."""
-
- # if configured to skip the editing part, just submit
- if gitConfig("git-p4.skipSubmitEdit") == "true":
- return True
-
- # look at the modification time, to check later if the user saved
- # the file
- mtime = os.stat(template_file).st_mtime
-
- # invoke the editor
- if os.environ.has_key("P4EDITOR"):
- editor = os.environ.get("P4EDITOR")
- else:
- editor = read_pipe("git var GIT_EDITOR").strip()
- system(editor + " " + template_file)
-
- # If the file was not saved, prompt to see if this patch should
- # be skipped. But skip this verification step if configured so.
- if gitConfig("git-p4.skipSubmitEditCheck") == "true":
- return True
-
- # modification time updated means user saved the file
- if os.stat(template_file).st_mtime > mtime:
- return True
-
- while True:
- response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
- if response == 'y':
- return True
- if response == 'n':
- return False
-
- def applyCommit(self, id):
- print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
-
- (p4User, gitEmail) = self.p4UserForCommit(id)
-
- if not self.detectRenames:
- # If not explicitly set check the config variable
- self.detectRenames = gitConfig("git-p4.detectRenames")
-
- if self.detectRenames.lower() == "false" or self.detectRenames == "":
- diffOpts = ""
- elif self.detectRenames.lower() == "true":
- diffOpts = "-M"
- else:
- diffOpts = "-M%s" % self.detectRenames
-
- detectCopies = gitConfig("git-p4.detectCopies")
- if detectCopies.lower() == "true":
- diffOpts += " -C"
- elif detectCopies != "" and detectCopies.lower() != "false":
- diffOpts += " -C%s" % detectCopies
-
- if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
- diffOpts += " --find-copies-harder"
-
- diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
- filesToAdd = set()
- filesToDelete = set()
- editedFiles = set()
- filesToChangeExecBit = {}
- for line in diff:
- diff = parseDiffTreeEntry(line)
- modifier = diff['status']
- path = diff['src']
- if modifier == "M":
- p4_edit(path)
- if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
- filesToChangeExecBit[path] = diff['dst_mode']
- editedFiles.add(path)
- elif modifier == "A":
- filesToAdd.add(path)
- filesToChangeExecBit[path] = diff['dst_mode']
- if path in filesToDelete:
- filesToDelete.remove(path)
- elif modifier == "D":
- filesToDelete.add(path)
- if path in filesToAdd:
- filesToAdd.remove(path)
- elif modifier == "C":
- src, dest = diff['src'], diff['dst']
- p4_integrate(src, dest)
- if diff['src_sha1'] != diff['dst_sha1']:
- p4_edit(dest)
- if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
- p4_edit(dest)
- filesToChangeExecBit[dest] = diff['dst_mode']
- os.unlink(dest)
- editedFiles.add(dest)
- elif modifier == "R":
- src, dest = diff['src'], diff['dst']
- p4_integrate(src, dest)
- if diff['src_sha1'] != diff['dst_sha1']:
- p4_edit(dest)
- if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
- p4_edit(dest)
- filesToChangeExecBit[dest] = diff['dst_mode']
- os.unlink(dest)
- editedFiles.add(dest)
- filesToDelete.add(src)
- else:
- die("unknown modifier %s for %s" % (modifier, path))
-
- diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
- patchcmd = diffcmd + " | git apply "
- tryPatchCmd = patchcmd + "--check -"
- applyPatchCmd = patchcmd + "--check --apply -"
-
- if os.system(tryPatchCmd) != 0:
- print "Unfortunately applying the change failed!"
- print "What do you want to do?"
- response = "x"
- while response != "s" and response != "a" and response != "w":
- response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
- "and with .rej files / [w]rite the patch to a file (patch.txt) ")
- if response == "s":
- print "Skipping! Good luck with the next patches..."
- for f in editedFiles:
- p4_revert(f)
- for f in filesToAdd:
- os.remove(f)
- return
- elif response == "a":
- os.system(applyPatchCmd)
- if len(filesToAdd) > 0:
- print "You may also want to call p4 add on the following files:"
- print " ".join(filesToAdd)
- if len(filesToDelete):
- print "The following files should be scheduled for deletion with p4 delete:"
- print " ".join(filesToDelete)
- die("Please resolve and submit the conflict manually and "
- + "continue afterwards with git-p4 submit --continue")
- elif response == "w":
- system(diffcmd + " > patch.txt")
- print "Patch saved to patch.txt in %s !" % self.clientPath
- die("Please resolve and submit the conflict manually and "
- "continue afterwards with git-p4 submit --continue")
-
- system(applyPatchCmd)
-
- for f in filesToAdd:
- p4_add(f)
- for f in filesToDelete:
- p4_revert(f)
- p4_delete(f)
-
- # Set/clear executable bits
- for f in filesToChangeExecBit.keys():
- mode = filesToChangeExecBit[f]
- setP4ExecBit(f, mode)
-
- logMessage = extractLogMessageFromGitCommit(id)
- logMessage = logMessage.strip()
-
- template = self.prepareSubmitTemplate()
-
- if self.interactive:
- submitTemplate = self.prepareLogMessage(template, logMessage)
-
- if self.preserveUser:
- submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
-
- if os.environ.has_key("P4DIFF"):
- del(os.environ["P4DIFF"])
- diff = ""
- for editedFile in editedFiles:
- diff += p4_read_pipe(['diff', '-du', editedFile])
-
- newdiff = ""
- for newFile in filesToAdd:
- newdiff += "==== new file ====\n"
- newdiff += "--- /dev/null\n"
- newdiff += "+++ %s\n" % newFile
- f = open(newFile, "r")
- for line in f.readlines():
- newdiff += "+" + line
- f.close()
-
- if self.checkAuthorship and not self.p4UserIsMe(p4User):
- submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
- submitTemplate += "######## Use git-p4 option --preserve-user to modify authorship\n"
- submitTemplate += "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"
-
- separatorLine = "######## everything below this line is just the diff #######\n"
-
- (handle, fileName) = tempfile.mkstemp()
- tmpFile = os.fdopen(handle, "w+")
- if self.isWindows:
- submitTemplate = submitTemplate.replace("\n", "\r\n")
- separatorLine = separatorLine.replace("\n", "\r\n")
- newdiff = newdiff.replace("\n", "\r\n")
- tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
- tmpFile.close()
-
- if self.edit_template(fileName):
- # read the edited message and submit
- tmpFile = open(fileName, "rb")
- message = tmpFile.read()
- tmpFile.close()
- submitTemplate = message[:message.index(separatorLine)]
- if self.isWindows:
- submitTemplate = submitTemplate.replace("\r\n", "\n")
- p4_write_pipe(['submit', '-i'], submitTemplate)
-
- if self.preserveUser:
- if p4User:
- # Get last changelist number. Cannot easily get it from
- # the submit command output as the output is
- # unmarshalled.
- changelist = self.lastP4Changelist()
- self.modifyChangelistUser(changelist, p4User)
- else:
- # skip this patch
- print "Submission cancelled, undoing p4 changes."
- for f in editedFiles:
- p4_revert(f)
- for f in filesToAdd:
- p4_revert(f)
- os.remove(f)
-
- os.remove(fileName)
- else:
- fileName = "submit.txt"
- file = open(fileName, "w+")
- file.write(self.prepareLogMessage(template, logMessage))
- file.close()
- print ("Perforce submit template written as %s. "
- + "Please review/edit and then use p4 submit -i < %s to submit directly!"
- % (fileName, fileName))
-
- def run(self, args):
- if len(args) == 0:
- self.master = currentGitBranch()
- if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
- die("Detecting current git branch failed!")
- elif len(args) == 1:
- self.master = args[0]
- if not branchExists(self.master):
- die("Branch %s does not exist" % self.master)
- else:
- return False
-
- allowSubmit = gitConfig("git-p4.allowSubmit")
- if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
- die("%s is not in git-p4.allowSubmit" % self.master)
-
- [upstream, settings] = findUpstreamBranchPoint()
- self.depotPath = settings['depot-paths'][0]
- if len(self.origin) == 0:
- self.origin = upstream
-
- if self.preserveUser:
- if not self.canChangeChangelists():
- die("Cannot preserve user names without p4 super-user or admin permissions")
-
- if self.verbose:
- print "Origin branch is " + self.origin
-
- if len(self.depotPath) == 0:
- print "Internal error: cannot locate perforce depot path from existing branches"
- sys.exit(128)
-
- self.useClientSpec = False
- if gitConfig("git-p4.useclientspec", "--bool") == "true":
- self.useClientSpec = True
- if self.useClientSpec:
- self.clientSpecDirs = getClientSpec()
-
- if self.useClientSpec:
- # all files are relative to the client spec
- self.clientPath = getClientRoot()
- else:
- self.clientPath = p4Where(self.depotPath)
-
- if self.clientPath == "":
- die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
-
- print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
- self.oldWorkingDirectory = os.getcwd()
-
- # ensure the clientPath exists
- if not os.path.exists(self.clientPath):
- os.makedirs(self.clientPath)
-
- chdir(self.clientPath)
- print "Synchronizing p4 checkout..."
- p4_sync("...")
- self.check()
-
- commits = []
- for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
- commits.append(line.strip())
- commits.reverse()
-
- if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
- self.checkAuthorship = False
- else:
- self.checkAuthorship = True
-
- if self.preserveUser:
- self.checkValidP4Users(commits)
-
- while len(commits) > 0:
- commit = commits[0]
- commits = commits[1:]
- self.applyCommit(commit)
- if not self.interactive:
- break
-
- if len(commits) == 0:
- print "All changes applied!"
- chdir(self.oldWorkingDirectory)
-
- sync = P4Sync()
- sync.run([])
-
- rebase = P4Rebase()
- rebase.rebase()
-
- return True
-
-class View(object):
- """Represent a p4 view ("p4 help views"), and map files in a
- repo according to the view."""
-
- class Path(object):
- """A depot or client path, possibly containing wildcards.
- The only one supported is ... at the end, currently.
- Initialize with the full path, with //depot or //client."""
-
- def __init__(self, path, is_depot):
- self.path = path
- self.is_depot = is_depot
- self.find_wildcards()
- # remember the prefix bit, useful for relative mappings
- m = re.match("(//[^/]+/)", self.path)
- if not m:
- die("Path %s does not start with //prefix/" % self.path)
- prefix = m.group(1)
- if not self.is_depot:
- # strip //client/ on client paths
- self.path = self.path[len(prefix):]
-
- def find_wildcards(self):
- """Make sure wildcards are valid, and set up internal
- variables."""
-
- self.ends_triple_dot = False
- # There are three wildcards allowed in p4 views
- # (see "p4 help views"). This code knows how to
- # handle "..." (only at the end), but cannot deal with
- # "%%n" or "*". Only check the depot_side, as p4 should
- # validate that the client_side matches too.
- if re.search(r'%%[1-9]', self.path):
- die("Can't handle %%n wildcards in view: %s" % self.path)
- if self.path.find("*") >= 0:
- die("Can't handle * wildcards in view: %s" % self.path)
- triple_dot_index = self.path.find("...")
- if triple_dot_index >= 0:
- if not self.path.endswith("..."):
- die("Can handle ... wildcard only at end of path: %s" %
- self.path)
- self.ends_triple_dot = True
-
- def ensure_compatible(self, other_path):
- """Make sure the wildcards agree."""
- if self.ends_triple_dot != other_path.ends_triple_dot:
- die("Both paths must end with ... if either does;\n" +
- "paths: %s %s" % (self.path, other_path.path))
-
- def match_wildcards(self, test_path):
- """See if this test_path matches us, and fill in the value
- of the wildcards if so. Returns a tuple of
- (True|False, wildcards[]). For now, only the ... at end
- is supported, so at most one wildcard."""
- if self.ends_triple_dot:
- dotless = self.path[:-3]
- if test_path.startswith(dotless):
- wildcard = test_path[len(dotless):]
- return (True, [ wildcard ])
- else:
- if test_path == self.path:
- return (True, [])
- return (False, [])
-
- def match(self, test_path):
- """Just return if it matches; don't bother with the wildcards."""
- b, _ = self.match_wildcards(test_path)
- return b
-
- def fill_in_wildcards(self, wildcards):
- """Return the relative path, with the wildcards filled in
- if there are any."""
- if self.ends_triple_dot:
- return self.path[:-3] + wildcards[0]
- else:
- return self.path
-
- class Mapping(object):
- def __init__(self, depot_side, client_side, overlay, exclude):
- # depot_side is without the trailing /... if it had one
- self.depot_side = View.Path(depot_side, is_depot=True)
- self.client_side = View.Path(client_side, is_depot=False)
- self.overlay = overlay # started with "+"
- self.exclude = exclude # started with "-"
- assert not (self.overlay and self.exclude)
- self.depot_side.ensure_compatible(self.client_side)
-
- def __str__(self):
- c = " "
- if self.overlay:
- c = "+"
- if self.exclude:
- c = "-"
- return "View.Mapping: %s%s -> %s" % \
- (c, self.depot_side, self.client_side)
-
- def map_depot_to_client(self, depot_path):
- """Calculate the client path if using this mapping on the
- given depot path; does not consider the effect of other
- mappings in a view. Even excluded mappings are returned."""
- matches, wildcards = self.depot_side.match_wildcards(depot_path)
- if not matches:
- return ""
- client_path = self.client_side.fill_in_wildcards(wildcards)
- return client_path
-
- #
- # View methods
- #
- def __init__(self):
- self.mappings = []
-
- def append(self, view_line):
- """Parse a view line, splitting it into depot and client
- sides. Append to self.mappings, preserving order."""
-
- # Split the view line into exactly two words. P4 enforces
- # structure on these lines that simplifies this quite a bit.
- #
- # Either or both words may be double-quoted.
- # Single quotes do not matter.
- # Double-quote marks cannot occur inside the words.
- # A + or - prefix is also inside the quotes.
- # There are no quotes unless they contain a space.
- # The line is already white-space stripped.
- # The two words are separated by a single space.
- #
- if view_line[0] == '"':
- # First word is double quoted. Find its end.
- close_quote_index = view_line.find('"', 1)
- if close_quote_index <= 0:
- die("No first-word closing quote found: %s" % view_line)
- depot_side = view_line[1:close_quote_index]
- # skip closing quote and space
- rhs_index = close_quote_index + 1 + 1
- else:
- space_index = view_line.find(" ")
- if space_index <= 0:
- die("No word-splitting space found: %s" % view_line)
- depot_side = view_line[0:space_index]
- rhs_index = space_index + 1
-
- if view_line[rhs_index] == '"':
- # Second word is double quoted. Make sure there is a
- # double quote at the end too.
- if not view_line.endswith('"'):
- die("View line with rhs quote should end with one: %s" %
- view_line)
- # skip the quotes
- client_side = view_line[rhs_index+1:-1]
- else:
- client_side = view_line[rhs_index:]
-
- # prefix + means overlay on previous mapping
- overlay = False
- if depot_side.startswith("+"):
- overlay = True
- depot_side = depot_side[1:]
-
- # prefix - means exclude this path
- exclude = False
- if depot_side.startswith("-"):
- exclude = True
- depot_side = depot_side[1:]
-
- m = View.Mapping(depot_side, client_side, overlay, exclude)
- self.mappings.append(m)
-
- def map_in_client(self, depot_path):
- """Return the relative location in the client where this
- depot file should live. Returns "" if the file should
- not be mapped in the client."""
-
- paths_filled = []
- client_path = ""
-
- # look at later entries first
- for m in self.mappings[::-1]:
-
- # see where will this path end up in the client
- p = m.map_depot_to_client(depot_path)
-
- if p == "":
- # Depot path does not belong in client. Must remember
- # this, as previous items should not cause files to
- # exist in this path either. Remember that the list is
- # being walked from the end, which has higher precedence.
- # Overlap mappings do not exclude previous mappings.
- if not m.overlay:
- paths_filled.append(m.client_side)
-
- else:
- # This mapping matched; no need to search any further.
- # But, the mapping could be rejected if the client path
- # has already been claimed by an earlier mapping.
- already_mapped_in_client = False
- for f in paths_filled:
- # this is View.Path.match
- if f.match(p):
- already_mapped_in_client = True
- break
- if not already_mapped_in_client:
- # Include this file, unless it is from a line that
- # explicitly said to exclude it.
- if not m.exclude:
- client_path = p
-
- # a match, even if rejected, always stops the search
- break
-
- return client_path
-
-class P4Sync(Command, P4UserMap):
- delete_actions = ( "delete", "move/delete", "purge" )
-
- def __init__(self):
- Command.__init__(self)
- P4UserMap.__init__(self)
- self.options = [
- optparse.make_option("--branch", dest="branch"),
- optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
- optparse.make_option("--changesfile", dest="changesFile"),
- optparse.make_option("--silent", dest="silent", action="store_true"),
- optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
- optparse.make_option("--verbose", dest="verbose", action="store_true"),
- optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
- help="Import into refs/heads/ , not refs/remotes"),
- optparse.make_option("--max-changes", dest="maxChanges"),
- optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
- help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
- optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
- help="Only sync files that are included in the Perforce Client Spec")
- ]
- self.description = """Imports from Perforce into a git repository.\n
- example:
- //depot/my/project/ -- to import the current head
- //depot/my/project/@all -- to import everything
- //depot/my/project/@1,6 -- to import only from revision 1 to 6
-
- (a ... is not needed in the path p4 specification, it's added implicitly)"""
-
- self.usage += " //depot/path[@revRange]"
- self.silent = False
- self.createdBranches = set()
- self.committedChanges = set()
- self.branch = ""
- self.detectBranches = False
- self.detectLabels = False
- self.changesFile = ""
- self.syncWithOrigin = True
- self.verbose = False
- self.importIntoRemotes = True
- self.maxChanges = ""
- self.isWindows = (platform.system() == "Windows")
- self.keepRepoPath = False
- self.depotPaths = None
- self.p4BranchesInGit = []
- self.cloneExclude = []
- self.useClientSpec = False
- self.useClientSpec_from_options = False
- self.clientSpecDirs = None
-
- if gitConfig("git-p4.syncFromOrigin") == "false":
- self.syncWithOrigin = False
-
- #
- # P4 wildcards are not allowed in filenames. P4 complains
- # if you simply add them, but you can force it with "-f", in
- # which case it translates them into %xx encoding internally.
- # Search for and fix just these four characters. Do % last so
- # that fixing it does not inadvertently create new %-escapes.
- #
- def wildcard_decode(self, path):
- # Cannot have * in a filename in windows; untested as to
- # what p4 would do in such a case.
- if not self.isWindows:
- path = path.replace("%2A", "*")
- path = path.replace("%23", "#") \
- .replace("%40", "@") \
- .replace("%25", "%")
- return path
-
- def extractFilesFromCommit(self, commit):
- self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
- for path in self.cloneExclude]
- files = []
- fnum = 0
- while commit.has_key("depotFile%s" % fnum):
- path = commit["depotFile%s" % fnum]
-
- if [p for p in self.cloneExclude
- if p4PathStartsWith(path, p)]:
- found = False
- else:
- found = [p for p in self.depotPaths
- if p4PathStartsWith(path, p)]
- if not found:
- fnum = fnum + 1
- continue
-
- file = {}
- file["path"] = path
- file["rev"] = commit["rev%s" % fnum]
- file["action"] = commit["action%s" % fnum]
- file["type"] = commit["type%s" % fnum]
- files.append(file)
- fnum = fnum + 1
- return files
-
- def stripRepoPath(self, path, prefixes):
- if self.useClientSpec:
- return self.clientSpecDirs.map_in_client(path)
-
- if self.keepRepoPath:
- prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
-
- for p in prefixes:
- if p4PathStartsWith(path, p):
- path = path[len(p):]
-
- return path
-
- def splitFilesIntoBranches(self, commit):
- branches = {}
- fnum = 0
- while commit.has_key("depotFile%s" % fnum):
- path = commit["depotFile%s" % fnum]
- found = [p for p in self.depotPaths
- if p4PathStartsWith(path, p)]
- if not found:
- fnum = fnum + 1
- continue
-
- file = {}
- file["path"] = path
- file["rev"] = commit["rev%s" % fnum]
- file["action"] = commit["action%s" % fnum]
- file["type"] = commit["type%s" % fnum]
- fnum = fnum + 1
-
- relPath = self.stripRepoPath(path, self.depotPaths)
-
- for branch in self.knownBranches.keys():
-
- # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
- if relPath.startswith(branch + "/"):
- if branch not in branches:
- branches[branch] = []
- branches[branch].append(file)
- break
-
- return branches
-
- # output one file from the P4 stream
- # - helper for streamP4Files
-
- def streamOneP4File(self, file, contents):
- relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
- relPath = self.wildcard_decode(relPath)
- if verbose:
- sys.stderr.write("%s\n" % relPath)
-
- (type_base, type_mods) = split_p4_type(file["type"])
-
- git_mode = "100644"
- if "x" in type_mods:
- git_mode = "100755"
- if type_base == "symlink":
- git_mode = "120000"
- # p4 print on a symlink contains "target\n"; remove the newline
- data = ''.join(contents)
- contents = [data[:-1]]
-
- if type_base == "utf16":
- # p4 delivers different text in the python output to -G
- # than it does when using "print -o", or normal p4 client
- # operations. utf16 is converted to ascii or utf8, perhaps.
- # But ascii text saved as -t utf16 is completely mangled.
- # Invoke print -o to get the real contents.
- text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
- contents = [ text ]
-
- if type_base == "apple":
- # Apple filetype files will be streamed as a concatenation of
- # its appledouble header and the contents. This is useless
- # on both macs and non-macs. If using "print -q -o xx", it
- # will create "xx" with the data, and "%xx" with the header.
- # This is also not very useful.
- #
- # Ideally, someday, this script can learn how to generate
- # appledouble files directly and import those to git, but
- # non-mac machines can never find a use for apple filetype.
- print "\nIgnoring apple filetype file %s" % file['depotFile']
- return
-
- # Perhaps windows wants unicode, utf16 newlines translated too;
- # but this is not doing it.
- if self.isWindows and type_base == "text":
- mangled = []
- for data in contents:
- data = data.replace("\r\n", "\n")
- mangled.append(data)
- contents = mangled
-
- # Note that we do not try to de-mangle keywords on utf16 files,
- # even though in theory somebody may want that.
- if type_base in ("text", "unicode", "binary"):
- if "ko" in type_mods:
- text = ''.join(contents)
- text = re.sub(r'\$(Id|Header):[^$]*\$', r'$\1$', text)
- contents = [ text ]
- elif "k" in type_mods:
- text = ''.join(contents)
- text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$', r'$\1$', text)
- contents = [ text ]
-
- self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
-
- # total length...
- length = 0
- for d in contents:
- length = length + len(d)
-
- self.gitStream.write("data %d\n" % length)
- for d in contents:
- self.gitStream.write(d)
- self.gitStream.write("\n")
-
- def streamOneP4Deletion(self, file):
- relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
- if verbose:
- sys.stderr.write("delete %s\n" % relPath)
- self.gitStream.write("D %s\n" % relPath)
-
- # handle another chunk of streaming data
- def streamP4FilesCb(self, marshalled):
-
- if marshalled.has_key('depotFile') and self.stream_have_file_info:
- # start of a new file - output the old one first
- self.streamOneP4File(self.stream_file, self.stream_contents)
- self.stream_file = {}
- self.stream_contents = []
- self.stream_have_file_info = False
-
- # pick up the new file information... for the
- # 'data' field we need to append to our array
- for k in marshalled.keys():
- if k == 'data':
- self.stream_contents.append(marshalled['data'])
- else:
- self.stream_file[k] = marshalled[k]
-
- self.stream_have_file_info = True
-
- # Stream directly from "p4 files" into "git fast-import"
- def streamP4Files(self, files):
- filesForCommit = []
- filesToRead = []
- filesToDelete = []
-
- for f in files:
- # if using a client spec, only add the files that have
- # a path in the client
- if self.clientSpecDirs:
- if self.clientSpecDirs.map_in_client(f['path']) == "":
- continue
-
- filesForCommit.append(f)
- if f['action'] in self.delete_actions:
- filesToDelete.append(f)
- else:
- filesToRead.append(f)
-
- # deleted files...
- for f in filesToDelete:
- self.streamOneP4Deletion(f)
-
- if len(filesToRead) > 0:
- self.stream_file = {}
- self.stream_contents = []
- self.stream_have_file_info = False
-
- # curry self argument
- def streamP4FilesCbSelf(entry):
- self.streamP4FilesCb(entry)
-
- fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
-
- p4CmdList(["-x", "-", "print"],
- stdin=fileArgs,
- cb=streamP4FilesCbSelf)
-
- # do the last chunk
- if self.stream_file.has_key('depotFile'):
- self.streamOneP4File(self.stream_file, self.stream_contents)
-
- def commit(self, details, files, branch, branchPrefixes, parent = ""):
- epoch = details["time"]
- author = details["user"]
- self.branchPrefixes = branchPrefixes
-
- if self.verbose:
- print "commit into %s" % branch
-
- # start with reading files; if that fails, we should not
- # create a commit.
- new_files = []
- for f in files:
- if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
- new_files.append (f)
- else:
- sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
-
- self.gitStream.write("commit %s\n" % branch)
-# gitStream.write("mark :%s\n" % details["change"])
- self.committedChanges.add(int(details["change"]))
- committer = ""
- if author not in self.users:
- self.getUserMapFromPerforceServer()
- if author in self.users:
- committer = "%s %s %s" % (self.users[author], epoch, self.tz)
- else:
- committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
-
- self.gitStream.write("committer %s\n" % committer)
-
- self.gitStream.write("data <<EOT\n")
- self.gitStream.write(details["desc"])
- self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
- % (','.join (branchPrefixes), details["change"]))
- if len(details['options']) > 0:
- self.gitStream.write(": options = %s" % details['options'])
- self.gitStream.write("]\nEOT\n\n")
-
- if len(parent) > 0:
- if self.verbose:
- print "parent %s" % parent
- self.gitStream.write("from %s\n" % parent)
-
- self.streamP4Files(new_files)
- self.gitStream.write("\n")
-
- change = int(details["change"])
-
- if self.labels.has_key(change):
- label = self.labels[change]
- labelDetails = label[0]
- labelRevisions = label[1]
- if self.verbose:
- print "Change %s is labelled %s" % (change, labelDetails)
-
- files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
- for p in branchPrefixes])
-
- if len(files) == len(labelRevisions):
-
- cleanedFiles = {}
- for info in files:
- if info["action"] in self.delete_actions:
- continue
- cleanedFiles[info["depotFile"]] = info["rev"]
-
- if cleanedFiles == labelRevisions:
- self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
- self.gitStream.write("from %s\n" % branch)
-
- owner = labelDetails["Owner"]
- tagger = ""
- if author in self.users:
- tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
- else:
- tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
- self.gitStream.write("tagger %s\n" % tagger)
- self.gitStream.write("data <<EOT\n")
- self.gitStream.write(labelDetails["Description"])
- self.gitStream.write("EOT\n\n")
-
- else:
- if not self.silent:
- print ("Tag %s does not match with change %s: files do not match."
- % (labelDetails["label"], change))
-
- else:
- if not self.silent:
- print ("Tag %s does not match with change %s: file count is different."
- % (labelDetails["label"], change))
-
- def getLabels(self):
- self.labels = {}
-
- l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
- if len(l) > 0 and not self.silent:
- print "Finding files belonging to labels in %s" % `self.depotPaths`
-
- for output in l:
- label = output["label"]
- revisions = {}
- newestChange = 0
- if self.verbose:
- print "Querying files for label %s" % label
- for file in p4CmdList(["files"] +
- ["%s...@%s" % (p, label)
- for p in self.depotPaths]):
- revisions[file["depotFile"]] = file["rev"]
- change = int(file["change"])
- if change > newestChange:
- newestChange = change
-
- self.labels[newestChange] = [output, revisions]
-
- if self.verbose:
- print "Label changes: %s" % self.labels.keys()
-
- def guessProjectName(self):
- for p in self.depotPaths:
- if p.endswith("/"):
- p = p[:-1]
- p = p[p.strip().rfind("/") + 1:]
- if not p.endswith("/"):
- p += "/"
- return p
-
- def getBranchMapping(self):
- lostAndFoundBranches = set()
-
- user = gitConfig("git-p4.branchUser")
- if len(user) > 0:
- command = "branches -u %s" % user
- else:
- command = "branches"
-
- for info in p4CmdList(command):
- details = p4Cmd("branch -o %s" % info["branch"])
- viewIdx = 0
- while details.has_key("View%s" % viewIdx):
- paths = details["View%s" % viewIdx].split(" ")
- viewIdx = viewIdx + 1
- # require standard //depot/foo/... //depot/bar/... mapping
- if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
- continue
- source = paths[0]
- destination = paths[1]
- ## HACK
- if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
- source = source[len(self.depotPaths[0]):-4]
- destination = destination[len(self.depotPaths[0]):-4]
-
- if destination in self.knownBranches:
- if not self.silent:
- print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
- print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
- continue
-
- self.knownBranches[destination] = source
-
- lostAndFoundBranches.discard(destination)
-
- if source not in self.knownBranches:
- lostAndFoundBranches.add(source)
-
- # Perforce does not strictly require branches to be defined, so we also
- # check git config for a branch list.
- #
- # Example of branch definition in git config file:
- # [git-p4]
- # branchList=main:branchA
- # branchList=main:branchB
- # branchList=branchA:branchC
- configBranches = gitConfigList("git-p4.branchList")
- for branch in configBranches:
- if branch:
- (source, destination) = branch.split(":")
- self.knownBranches[destination] = source
-
- lostAndFoundBranches.discard(destination)
-
- if source not in self.knownBranches:
- lostAndFoundBranches.add(source)
-
-
- for branch in lostAndFoundBranches:
- self.knownBranches[branch] = branch
-
- def getBranchMappingFromGitBranches(self):
- branches = p4BranchesInGit(self.importIntoRemotes)
- for branch in branches.keys():
- if branch == "master":
- branch = "main"
- else:
- branch = branch[len(self.projectName):]
- self.knownBranches[branch] = branch
-
- def listExistingP4GitBranches(self):
- # branches holds mapping from name to commit
- branches = p4BranchesInGit(self.importIntoRemotes)
- self.p4BranchesInGit = branches.keys()
- for branch in branches.keys():
- self.initialParents[self.refPrefix + branch] = branches[branch]
-
- def updateOptionDict(self, d):
- option_keys = {}
- if self.keepRepoPath:
- option_keys['keepRepoPath'] = 1
-
- d["options"] = ' '.join(sorted(option_keys.keys()))
-
- def readOptions(self, d):
- self.keepRepoPath = (d.has_key('options')
- and ('keepRepoPath' in d['options']))
-
- def gitRefForBranch(self, branch):
- if branch == "main":
- return self.refPrefix + "master"
-
- if len(branch) <= 0:
- return branch
-
- return self.refPrefix + self.projectName + branch
-
- def gitCommitByP4Change(self, ref, change):
- if self.verbose:
- print "looking in ref " + ref + " for change %s using bisect..." % change
-
- earliestCommit = ""
- latestCommit = parseRevision(ref)
-
- while True:
- if self.verbose:
- print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
- next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
- if len(next) == 0:
- if self.verbose:
- print "argh"
- return ""
- log = extractLogMessageFromGitCommit(next)
- settings = extractSettingsGitLog(log)
- currentChange = int(settings['change'])
- if self.verbose:
- print "current change %s" % currentChange
-
- if currentChange == change:
- if self.verbose:
- print "found %s" % next
- return next
-
- if currentChange < change:
- earliestCommit = "^%s" % next
- else:
- latestCommit = "%s" % next
-
- return ""
-
- def importNewBranch(self, branch, maxChange):
- # make fast-import flush all changes to disk and update the refs using the checkpoint
- # command so that we can try to find the branch parent in the git history
- self.gitStream.write("checkpoint\n\n");
- self.gitStream.flush();
- branchPrefix = self.depotPaths[0] + branch + "/"
- range = "@1,%s" % maxChange
- #print "prefix" + branchPrefix
- changes = p4ChangesForPaths([branchPrefix], range)
- if len(changes) <= 0:
- return False
- firstChange = changes[0]
- #print "first change in branch: %s" % firstChange
- sourceBranch = self.knownBranches[branch]
- sourceDepotPath = self.depotPaths[0] + sourceBranch
- sourceRef = self.gitRefForBranch(sourceBranch)
- #print "source " + sourceBranch
-
- branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
- #print "branch parent: %s" % branchParentChange
- gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
- if len(gitParent) > 0:
- self.initialParents[self.gitRefForBranch(branch)] = gitParent
- #print "parent git commit: %s" % gitParent
-
- self.importChanges(changes)
- return True
-
- def importChanges(self, changes):
- cnt = 1
- for change in changes:
- description = p4Cmd("describe %s" % change)
- self.updateOptionDict(description)
-
- if not self.silent:
- sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
- sys.stdout.flush()
- cnt = cnt + 1
-
- try:
- if self.detectBranches:
- branches = self.splitFilesIntoBranches(description)
- for branch in branches.keys():
- ## HACK --hwn
- branchPrefix = self.depotPaths[0] + branch + "/"
-
- parent = ""
-
- filesForCommit = branches[branch]
-
- if self.verbose:
- print "branch is %s" % branch
-
- self.updatedBranches.add(branch)
-
- if branch not in self.createdBranches:
- self.createdBranches.add(branch)
- parent = self.knownBranches[branch]
- if parent == branch:
- parent = ""
- else:
- fullBranch = self.projectName + branch
- if fullBranch not in self.p4BranchesInGit:
- if not self.silent:
- print("\n Importing new branch %s" % fullBranch);
- if self.importNewBranch(branch, change - 1):
- parent = ""
- self.p4BranchesInGit.append(fullBranch)
- if not self.silent:
- print("\n Resuming with change %s" % change);
-
- if self.verbose:
- print "parent determined through known branches: %s" % parent
-
- branch = self.gitRefForBranch(branch)
- parent = self.gitRefForBranch(parent)
-
- if self.verbose:
- print "looking for initial parent for %s; current parent is %s" % (branch, parent)
-
- if len(parent) == 0 and branch in self.initialParents:
- parent = self.initialParents[branch]
- del self.initialParents[branch]
-
- self.commit(description, filesForCommit, branch, [branchPrefix], parent)
- else:
- files = self.extractFilesFromCommit(description)
- self.commit(description, files, self.branch, self.depotPaths,
- self.initialParent)
- self.initialParent = ""
- except IOError:
- print self.gitError.read()
- sys.exit(1)
-
- def importHeadRevision(self, revision):
- print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
-
- details = {}
- details["user"] = "git perforce import user"
- details["desc"] = ("Initial import of %s from the state at revision %s\n"
- % (' '.join(self.depotPaths), revision))
- details["change"] = revision
- newestRevision = 0
-
- fileCnt = 0
- fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
-
- for info in p4CmdList(["files"] + fileArgs):
-
- if 'code' in info and info['code'] == 'error':
- sys.stderr.write("p4 returned an error: %s\n"
- % info['data'])
- if info['data'].find("must refer to client") >= 0:
- sys.stderr.write("This particular p4 error is misleading.\n")
- sys.stderr.write("Perhaps the depot path was misspelled.\n");
- sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
- sys.exit(1)
- if 'p4ExitCode' in info:
- sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
- sys.exit(1)
-
-
- change = int(info["change"])
- if change > newestRevision:
- newestRevision = change
-
- if info["action"] in self.delete_actions:
- # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
- #fileCnt = fileCnt + 1
- continue
-
- for prop in ["depotFile", "rev", "action", "type" ]:
- details["%s%s" % (prop, fileCnt)] = info[prop]
-
- fileCnt = fileCnt + 1
-
- details["change"] = newestRevision
-
- # Use time from top-most change so that all git-p4 clones of
- # the same p4 repo have the same commit SHA1s.
- res = p4CmdList("describe -s %d" % newestRevision)
- newestTime = None
- for r in res:
- if r.has_key('time'):
- newestTime = int(r['time'])
- if newestTime is None:
- die("\"describe -s\" on newest change %d did not give a time")
- details["time"] = newestTime
-
- self.updateOptionDict(details)
- try:
- self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
- except IOError:
- print "IO error with git fast-import. Is your git version recent enough?"
- print self.gitError.read()
-
-
- def run(self, args):
- self.depotPaths = []
- self.changeRange = ""
- self.initialParent = ""
- self.previousDepotPaths = []
-
- # map from branch depot path to parent branch
- self.knownBranches = {}
- self.initialParents = {}
- self.hasOrigin = originP4BranchesExist()
- if not self.syncWithOrigin:
- self.hasOrigin = False
-
- if self.importIntoRemotes:
- self.refPrefix = "refs/remotes/p4/"
- else:
- self.refPrefix = "refs/heads/p4/"
-
- if self.syncWithOrigin and self.hasOrigin:
- if not self.silent:
- print "Syncing with origin first by calling git fetch origin"
- system("git fetch origin")
-
- if len(self.branch) == 0:
- self.branch = self.refPrefix + "master"
- if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
- system("git update-ref %s refs/heads/p4" % self.branch)
- system("git branch -D p4");
- # create it /after/ importing, when master exists
- if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
- system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
-
- # accept either the command-line option, or the configuration variable
- if self.useClientSpec:
- # will use this after clone to set the variable
- self.useClientSpec_from_options = True
- else:
- if gitConfig("git-p4.useclientspec", "--bool") == "true":
- self.useClientSpec = True
- if self.useClientSpec:
- self.clientSpecDirs = getClientSpec()
-
- # TODO: should always look at previous commits,
- # merge with previous imports, if possible.
- if args == []:
- if self.hasOrigin:
- createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
- self.listExistingP4GitBranches()
-
- if len(self.p4BranchesInGit) > 1:
- if not self.silent:
- print "Importing from/into multiple branches"
- self.detectBranches = True
-
- if self.verbose:
- print "branches: %s" % self.p4BranchesInGit
-
- p4Change = 0
- for branch in self.p4BranchesInGit:
- logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
-
- settings = extractSettingsGitLog(logMsg)
-
- self.readOptions(settings)
- if (settings.has_key('depot-paths')
- and settings.has_key ('change')):
- change = int(settings['change']) + 1
- p4Change = max(p4Change, change)
-
- depotPaths = sorted(settings['depot-paths'])
- if self.previousDepotPaths == []:
- self.previousDepotPaths = depotPaths
- else:
- paths = []
- for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
- prev_list = prev.split("/")
- cur_list = cur.split("/")
- for i in range(0, min(len(cur_list), len(prev_list))):
- if cur_list[i] <> prev_list[i]:
- i = i - 1
- break
-
- paths.append ("/".join(cur_list[:i + 1]))
-
- self.previousDepotPaths = paths
-
- if p4Change > 0:
- self.depotPaths = sorted(self.previousDepotPaths)
- self.changeRange = "@%s,#head" % p4Change
- if not self.detectBranches:
- self.initialParent = parseRevision(self.branch)
- if not self.silent and not self.detectBranches:
- print "Performing incremental import into %s git branch" % self.branch
-
- if not self.branch.startswith("refs/"):
- self.branch = "refs/heads/" + self.branch
-
- if len(args) == 0 and self.depotPaths:
- if not self.silent:
- print "Depot paths: %s" % ' '.join(self.depotPaths)
- else:
- if self.depotPaths and self.depotPaths != args:
- print ("previous import used depot path %s and now %s was specified. "
- "This doesn't work!" % (' '.join (self.depotPaths),
- ' '.join (args)))
- sys.exit(1)
-
- self.depotPaths = sorted(args)
-
- revision = ""
- self.users = {}
-
- # Make sure no revision specifiers are used when --changesfile
- # is specified.
- bad_changesfile = False
- if len(self.changesFile) > 0:
- for p in self.depotPaths:
- if p.find("@") >= 0 or p.find("#") >= 0:
- bad_changesfile = True
- break
- if bad_changesfile:
- die("Option --changesfile is incompatible with revision specifiers")
-
- newPaths = []
- for p in self.depotPaths:
- if p.find("@") != -1:
- atIdx = p.index("@")
- self.changeRange = p[atIdx:]
- if self.changeRange == "@all":
- self.changeRange = ""
- elif ',' not in self.changeRange:
- revision = self.changeRange
- self.changeRange = ""
- p = p[:atIdx]
- elif p.find("#") != -1:
- hashIdx = p.index("#")
- revision = p[hashIdx:]
- p = p[:hashIdx]
- elif self.previousDepotPaths == []:
- # pay attention to changesfile, if given, else import
- # the entire p4 tree at the head revision
- if len(self.changesFile) == 0:
- revision = "#head"
-
- p = re.sub ("\.\.\.$", "", p)
- if not p.endswith("/"):
- p += "/"
-
- newPaths.append(p)
-
- self.depotPaths = newPaths
-
-
- self.loadUserMapFromCache()
- self.labels = {}
- if self.detectLabels:
- self.getLabels();
-
- if self.detectBranches:
- ## FIXME - what's a P4 projectName ?
- self.projectName = self.guessProjectName()
-
- if self.hasOrigin:
- self.getBranchMappingFromGitBranches()
- else:
- self.getBranchMapping()
- if self.verbose:
- print "p4-git branches: %s" % self.p4BranchesInGit
- print "initial parents: %s" % self.initialParents
- for b in self.p4BranchesInGit:
- if b != "master":
-
- ## FIXME
- b = b[len(self.projectName):]
- self.createdBranches.add(b)
-
- self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
-
- importProcess = subprocess.Popen(["git", "fast-import"],
- stdin=subprocess.PIPE, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE);
- self.gitOutput = importProcess.stdout
- self.gitStream = importProcess.stdin
- self.gitError = importProcess.stderr
-
- if revision:
- self.importHeadRevision(revision)
- else:
- changes = []
-
- if len(self.changesFile) > 0:
- output = open(self.changesFile).readlines()
- changeSet = set()
- for line in output:
- changeSet.add(int(line))
-
- for change in changeSet:
- changes.append(change)
-
- changes.sort()
- else:
- # catch "git-p4 sync" with no new branches, in a repo that
- # does not have any existing git-p4 branches
- if len(args) == 0 and not self.p4BranchesInGit:
- die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
- if self.verbose:
- print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
- self.changeRange)
- changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
-
- if len(self.maxChanges) > 0:
- changes = changes[:min(int(self.maxChanges), len(changes))]
-
- if len(changes) == 0:
- if not self.silent:
- print "No changes to import!"
- return True
-
- if not self.silent and not self.detectBranches:
- print "Import destination: %s" % self.branch
-
- self.updatedBranches = set()
-
- self.importChanges(changes)
-
- if not self.silent:
- print ""
- if len(self.updatedBranches) > 0:
- sys.stdout.write("Updated branches: ")
- for b in self.updatedBranches:
- sys.stdout.write("%s " % b)
- sys.stdout.write("\n")
-
- self.gitStream.close()
- if importProcess.wait() != 0:
- die("fast-import failed: %s" % self.gitError.read())
- self.gitOutput.close()
- self.gitError.close()
-
- return True
-
-class P4Rebase(Command):
- def __init__(self):
- Command.__init__(self)
- self.options = [ ]
- self.description = ("Fetches the latest revision from perforce and "
- + "rebases the current work (branch) against it")
- self.verbose = False
-
- def run(self, args):
- sync = P4Sync()
- sync.run([])
-
- return self.rebase()
-
- def rebase(self):
- if os.system("git update-index --refresh") != 0:
- die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up-to-date or stash away all your changes with git stash.");
- if len(read_pipe("git diff-index HEAD --")) > 0:
- die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
-
- [upstream, settings] = findUpstreamBranchPoint()
- if len(upstream) == 0:
- die("Cannot find upstream branchpoint for rebase")
-
- # the branchpoint may be p4/foo~3, so strip off the parent
- upstream = re.sub("~[0-9]+$", "", upstream)
-
- print "Rebasing the current branch onto %s" % upstream
- oldHead = read_pipe("git rev-parse HEAD").strip()
- system("git rebase %s" % upstream)
- system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
- return True
-
-class P4Clone(P4Sync):
- def __init__(self):
- P4Sync.__init__(self)
- self.description = "Creates a new git repository and imports from Perforce into it"
- self.usage = "usage: %prog [options] //depot/path[@revRange]"
- self.options += [
- optparse.make_option("--destination", dest="cloneDestination",
- action='store', default=None,
- help="where to leave result of the clone"),
- optparse.make_option("-/", dest="cloneExclude",
- action="append", type="string",
- help="exclude depot path"),
- optparse.make_option("--bare", dest="cloneBare",
- action="store_true", default=False),
- ]
- self.cloneDestination = None
- self.needsGit = False
- self.cloneBare = False
-
- # This is required for the "append" cloneExclude action
- def ensure_value(self, attr, value):
- if not hasattr(self, attr) or getattr(self, attr) is None:
- setattr(self, attr, value)
- return getattr(self, attr)
-
- def defaultDestination(self, args):
- ## TODO: use common prefix of args?
- depotPath = args[0]
- depotDir = re.sub("(@[^@]*)$", "", depotPath)
- depotDir = re.sub("(#[^#]*)$", "", depotDir)
- depotDir = re.sub(r"\.\.\.$", "", depotDir)
- depotDir = re.sub(r"/$", "", depotDir)
- return os.path.split(depotDir)[1]
-
- def run(self, args):
- if len(args) < 1:
- return False
-
- if self.keepRepoPath and not self.cloneDestination:
- sys.stderr.write("Must specify destination for --keep-path\n")
- sys.exit(1)
-
- depotPaths = args
-
- if not self.cloneDestination and len(depotPaths) > 1:
- self.cloneDestination = depotPaths[-1]
- depotPaths = depotPaths[:-1]
-
- self.cloneExclude = ["/"+p for p in self.cloneExclude]
- for p in depotPaths:
- if not p.startswith("//"):
- return False
-
- if not self.cloneDestination:
- self.cloneDestination = self.defaultDestination(args)
-
- print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
-
- if not os.path.exists(self.cloneDestination):
- os.makedirs(self.cloneDestination)
- chdir(self.cloneDestination)
-
- init_cmd = [ "git", "init" ]
- if self.cloneBare:
- init_cmd.append("--bare")
- subprocess.check_call(init_cmd)
-
- if not P4Sync.run(self, depotPaths):
- return False
- if self.branch != "master":
- if self.importIntoRemotes:
- masterbranch = "refs/remotes/p4/master"
- else:
- masterbranch = "refs/heads/p4/master"
- if gitBranchExists(masterbranch):
- system("git branch master %s" % masterbranch)
- if not self.cloneBare:
- system("git checkout -f")
- else:
- print "Could not detect main branch. No checkout/master branch created."
-
- # auto-set this variable if invoked with --use-client-spec
- if self.useClientSpec_from_options:
- system("git config --bool git-p4.useclientspec true")
-
- return True
-
-class P4Branches(Command):
- def __init__(self):
- Command.__init__(self)
- self.options = [ ]
- self.description = ("Shows the git branches that hold imports and their "
- + "corresponding perforce depot paths")
- self.verbose = False
-
- def run(self, args):
- if originP4BranchesExist():
- createOrUpdateBranchesFromOrigin()
-
- cmdline = "git rev-parse --symbolic "
- cmdline += " --remotes"
-
- for line in read_pipe_lines(cmdline):
- line = line.strip()
-
- if not line.startswith('p4/') or line == "p4/HEAD":
- continue
- branch = line
-
- log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
- settings = extractSettingsGitLog(log)
-
- print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
- return True
-
-class HelpFormatter(optparse.IndentedHelpFormatter):
- def __init__(self):
- optparse.IndentedHelpFormatter.__init__(self)
-
- def format_description(self, description):
- if description:
- return description + "\n"
- else:
- return ""
-
-def printUsage(commands):
- print "usage: %s <command> [options]" % sys.argv[0]
- print ""
- print "valid commands: %s" % ", ".join(commands)
- print ""
- print "Try %s <command> --help for command specific help." % sys.argv[0]
- print ""
-
-commands = {
- "debug" : P4Debug,
- "submit" : P4Submit,
- "commit" : P4Submit,
- "sync" : P4Sync,
- "rebase" : P4Rebase,
- "clone" : P4Clone,
- "rollback" : P4RollBack,
- "branches" : P4Branches
-}
-
-
-def main():
- if len(sys.argv[1:]) == 0:
- printUsage(commands.keys())
- sys.exit(2)
-
- cmd = ""
- cmdName = sys.argv[1]
- try:
- klass = commands[cmdName]
- cmd = klass()
- except KeyError:
- print "unknown command %s" % cmdName
- print ""
- printUsage(commands.keys())
- sys.exit(2)
-
- options = cmd.options
- cmd.gitdir = os.environ.get("GIT_DIR", None)
-
- args = sys.argv[2:]
-
- if len(options) > 0:
- if cmd.needsGit:
- options.append(optparse.make_option("--git-dir", dest="gitdir"))
-
- parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
- options,
- description = cmd.description,
- formatter = HelpFormatter())
-
- (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
- global verbose
- verbose = cmd.verbose
- if cmd.needsGit:
- if cmd.gitdir == None:
- cmd.gitdir = os.path.abspath(".git")
- if not isValidGitDir(cmd.gitdir):
- cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
- if os.path.exists(cmd.gitdir):
- cdup = read_pipe("git rev-parse --show-cdup").strip()
- if len(cdup) > 0:
- chdir(cdup);
-
- if not isValidGitDir(cmd.gitdir):
- if isValidGitDir(cmd.gitdir + "/.git"):
- cmd.gitdir += "/.git"
- else:
- die("fatal: cannot locate git repository at %s" % cmd.gitdir)
-
- os.environ["GIT_DIR"] = cmd.gitdir
-
- if not cmd.run(args):
- parser.print_help()
- sys.exit(2)
-
-
-if __name__ == '__main__':
- main()
diff --git a/contrib/fast-import/git-p4.README b/contrib/fast-import/git-p4.README
new file mode 100644
index 0000000000..cec5ecfa7c
--- /dev/null
+++ b/contrib/fast-import/git-p4.README
@@ -0,0 +1,12 @@
+The git-p4 script moved to the top-level of the git source directory.
+
+Invoke it as any other git command, like "git p4 clone", for instance.
+
+Note that the top-level git-p4.py script is now the source. It is
+built using make to git-p4, which will be installed.
+
+Windows users can copy the git-p4.py source script directly, possibly
+invoking it through a batch file called "git-p4.bat" in the same folder.
+It should contain just one line:
+
+ @python "%~d0%~p0git-p4.py" %*
diff --git a/contrib/fast-import/git-p4.bat b/contrib/fast-import/git-p4.bat
deleted file mode 100644
index 9f97e884f5..0000000000
--- a/contrib/fast-import/git-p4.bat
+++ /dev/null
@@ -1 +0,0 @@
-@python "%~d0%~p0git-p4" %*
diff --git a/contrib/mw-to-git/Makefile b/contrib/mw-to-git/Makefile
new file mode 100644
index 0000000000..3ed728b0ef
--- /dev/null
+++ b/contrib/mw-to-git/Makefile
@@ -0,0 +1,47 @@
+#
+# 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>
+#
+## Build git-remote-mediawiki
+
+-include ../../config.mak.autogen
+-include ../../config.mak
+
+ifndef PERL_PATH
+ PERL_PATH = /usr/bin/perl
+endif
+ifndef gitexecdir
+ gitexecdir = $(shell git --exec-path)
+endif
+
+PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
+gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
+SCRIPT = git-remote-mediawiki
+
+.PHONY: install help doc test clean
+
+help:
+ @echo 'This is the help target of the Makefile. Current configuration:'
+ @echo ' gitexecdir = $(gitexecdir_SQ)'
+ @echo ' PERL_PATH = $(PERL_PATH_SQ)'
+ @echo 'Run "$(MAKE) install" to install $(SCRIPT) in gitexecdir'
+ @echo 'Run "$(MAKE) test" to run the testsuite'
+
+install:
+ sed -e '1s|#!.*/perl|#!$(PERL_PATH_SQ)|' $(SCRIPT) \
+ > '$(gitexecdir_SQ)/$(SCRIPT)'
+ chmod +x '$(gitexecdir)/$(SCRIPT)'
+
+doc:
+ @echo 'Sorry, "make doc" is not implemented yet for $(SCRIPT)'
+
+test:
+ $(MAKE) -C t/ test
+
+clean:
+ $(RM) '$(gitexecdir)/$(SCRIPT)'
+ $(MAKE) -C t/ clean
diff --git a/contrib/mw-to-git/git-remote-mediawiki b/contrib/mw-to-git/git-remote-mediawiki
index c18bfa1f15..68555d4265 100755
--- a/contrib/mw-to-git/git-remote-mediawiki
+++ b/contrib/mw-to-git/git-remote-mediawiki
@@ -9,40 +9,19 @@
# 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
@@ -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
@@ -466,6 +716,11 @@ sub import_file_revision {
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};
@@ -485,6 +740,10 @@ sub import_file_revision {
if ($content ne DELETED_CONTENT) {
print STDOUT "M 644 inline $title.mw\n";
literal_data($content);
+ if (%mediafile) {
+ print STDOUT "M 644 inline $mediafile{title}\n";
+ literal_data_raw($mediafile{content});
+ }
print STDOUT "\n\n";
} else {
print STDOUT "D $title.mw\n";
@@ -547,8 +806,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 +814,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 refering 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 +928,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 +962,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 +1031,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,9 +1044,16 @@ 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
@@ -664,7 +1071,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 +1083,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 +1093,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 +1172,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 +1213,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 +1239,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 +1247,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/t/.gitignore b/contrib/mw-to-git/t/.gitignore
new file mode 100644
index 0000000000..a7a40b4964
--- /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 0000000000..f422203fa0
--- /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 0000000000..96e97390cf
--- /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 priviledge (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 beggining 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
+begining 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 0000000000..c6d6fa3aef
--- /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 0000000000..b5a2a4408c
--- /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 0000000000..29f125116b
--- /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 publically 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 0000000000..0f3f4e018a
--- /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 0000000000..9da2dc5ff0
--- /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 0000000000..811a90c9ae
--- /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 0000000000..9ea201459b
--- /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 0000000000..246d47d8fb
--- /dev/null
+++ b/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh
@@ -0,0 +1,321 @@
+#!/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 begining 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 begining 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 formating 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 formating for file name begining 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 "commiting 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 formating 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 "commiting 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_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 0000000000..5a0373935f
--- /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 0000000000..5c22457a0b
--- /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 0000000000..3b2cfacf51
--- /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 0000000000..0ff76259fa
--- /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 0000000000..958b37b4a7
--- /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/persistent-https/LICENSE b/contrib/persistent-https/LICENSE
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/contrib/persistent-https/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/contrib/persistent-https/Makefile b/contrib/persistent-https/Makefile
new file mode 100644
index 0000000000..92baa3beee
--- /dev/null
+++ b/contrib/persistent-https/Makefile
@@ -0,0 +1,38 @@
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+BUILD_LABEL=$(shell date +"%s")
+TAR_OUT=$(shell go env GOOS)_$(shell go env GOARCH).tar.gz
+
+all: git-remote-persistent-https git-remote-persistent-https--proxy \
+ git-remote-persistent-http
+
+git-remote-persistent-https--proxy: git-remote-persistent-https
+ ln -f -s git-remote-persistent-https git-remote-persistent-https--proxy
+
+git-remote-persistent-http: git-remote-persistent-https
+ ln -f -s git-remote-persistent-https git-remote-persistent-http
+
+git-remote-persistent-https:
+ go build -o git-remote-persistent-https \
+ -ldflags "-X main._BUILD_EMBED_LABEL $(BUILD_LABEL)"
+
+clean:
+ rm -f git-remote-persistent-http* *.tar.gz
+
+tar: clean all
+ @chmod 555 git-remote-persistent-https
+ @tar -czf $(TAR_OUT) git-remote-persistent-http* README LICENSE
+ @echo
+ @echo "Created $(TAR_OUT)"
diff --git a/contrib/persistent-https/README b/contrib/persistent-https/README
new file mode 100644
index 0000000000..f784dd2e66
--- /dev/null
+++ b/contrib/persistent-https/README
@@ -0,0 +1,62 @@
+git-remote-persistent-https
+
+The git-remote-persistent-https binary speeds up SSL operations
+by running a daemon job (git-remote-persistent-https--proxy) that
+keeps a connection open to a server.
+
+
+PRE-BUILT BINARIES
+
+Darwin amd64:
+https://commondatastorage.googleapis.com/git-remote-persistent-https/darwin_amd64.tar.gz
+
+Linux amd64:
+https://commondatastorage.googleapis.com/git-remote-persistent-https/linux_amd64.tar.gz
+
+
+INSTALLING
+
+Move all of the git-remote-persistent-http* binaries to a directory
+in PATH.
+
+
+USAGE
+
+HTTPS requests can be delegated to the proxy by using the
+"persistent-https" scheme, e.g.
+
+git clone persistent-https://kernel.googlesource.com/pub/scm/git/git
+
+Likewise, .gitconfig can be updated as follows to rewrite https urls
+to use persistent-https:
+
+[url "persistent-https"]
+ insteadof = https
+[url "persistent-http"]
+ insteadof = http
+
+
+#####################################################################
+# BUILDING FROM SOURCE
+#####################################################################
+
+LOCATION
+
+The source is available in the contrib/persistent-https directory of
+the Git source repository. The Git source repository is available at
+git://git.kernel.org/pub/scm/git/git.git/
+https://kernel.googlesource.com/pub/scm/git/git
+
+
+PREREQUISITES
+
+The code is written in Go (http://golang.org/) and the Go compiler is
+required. Currently, the compiler must be built and installed from tip
+of source, in order to include a fix in the reverse http proxy:
+http://code.google.com/p/go/source/detail?r=a615b796570a2cd8591884767a7d67ede74f6648
+
+
+BUILDING
+
+Run "make" to build the binaries. See the section on
+INSTALLING above.
diff --git a/contrib/persistent-https/client.go b/contrib/persistent-https/client.go
new file mode 100644
index 0000000000..71125b5832
--- /dev/null
+++ b/contrib/persistent-https/client.go
@@ -0,0 +1,189 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "net"
+ "net/url"
+ "os"
+ "os/exec"
+ "strings"
+ "syscall"
+ "time"
+)
+
+type Client struct {
+ ProxyBin string
+ Args []string
+
+ insecure bool
+}
+
+func (c *Client) Run() error {
+ if err := c.resolveArgs(); err != nil {
+ return fmt.Errorf("resolveArgs() got error: %v", err)
+ }
+
+ // Connect to the proxy.
+ uconn, hconn, addr, err := c.connect()
+ if err != nil {
+ return fmt.Errorf("connect() got error: %v", err)
+ }
+ // Keep the unix socket connection open for the duration of the request.
+ defer uconn.Close()
+ // Keep a connection to the HTTP server open, so no other user can
+ // bind on the same address so long as the process is running.
+ defer hconn.Close()
+
+ // Start the git-remote-http subprocess.
+ cargs := []string{"-c", fmt.Sprintf("http.proxy=%v", addr), "remote-http"}
+ cargs = append(cargs, c.Args...)
+ cmd := exec.Command("git", cargs...)
+
+ for _, v := range os.Environ() {
+ if !strings.HasPrefix(v, "GIT_PERSISTENT_HTTPS_SECURE=") {
+ cmd.Env = append(cmd.Env, v)
+ }
+ }
+ // Set the GIT_PERSISTENT_HTTPS_SECURE environment variable when
+ // the proxy is using a SSL connection. This allows credential helpers
+ // to identify secure proxy connections, despite being passed an HTTP
+ // scheme.
+ if !c.insecure {
+ cmd.Env = append(cmd.Env, "GIT_PERSISTENT_HTTPS_SECURE=1")
+ }
+
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ if eerr, ok := err.(*exec.ExitError); ok {
+ if stat, ok := eerr.ProcessState.Sys().(syscall.WaitStatus); ok && stat.ExitStatus() != 0 {
+ os.Exit(stat.ExitStatus())
+ }
+ }
+ return fmt.Errorf("git-remote-http subprocess got error: %v", err)
+ }
+ return nil
+}
+
+func (c *Client) connect() (uconn net.Conn, hconn net.Conn, addr string, err error) {
+ uconn, err = DefaultSocket.Dial()
+ if err != nil {
+ if e, ok := err.(*net.OpError); ok && (os.IsNotExist(e.Err) || e.Err == syscall.ECONNREFUSED) {
+ if err = c.startProxy(); err == nil {
+ uconn, err = DefaultSocket.Dial()
+ }
+ }
+ if err != nil {
+ return
+ }
+ }
+
+ if addr, err = c.readAddr(uconn); err != nil {
+ return
+ }
+
+ // Open a tcp connection to the proxy.
+ if hconn, err = net.Dial("tcp", addr); err != nil {
+ return
+ }
+
+ // Verify the address hasn't changed ownership.
+ var addr2 string
+ if addr2, err = c.readAddr(uconn); err != nil {
+ return
+ } else if addr != addr2 {
+ err = fmt.Errorf("address changed after connect. got %q, want %q", addr2, addr)
+ return
+ }
+ return
+}
+
+func (c *Client) readAddr(conn net.Conn) (string, error) {
+ conn.SetDeadline(time.Now().Add(5 * time.Second))
+ data := make([]byte, 100)
+ n, err := conn.Read(data)
+ if err != nil {
+ return "", fmt.Errorf("error reading unix socket: %v", err)
+ } else if n == 0 {
+ return "", errors.New("empty data response")
+ }
+ conn.Write([]byte{1}) // Ack
+
+ var addr string
+ if addrs := strings.Split(string(data[:n]), "\n"); len(addrs) != 2 {
+ return "", fmt.Errorf("got %q, wanted 2 addresses", data[:n])
+ } else if c.insecure {
+ addr = addrs[1]
+ } else {
+ addr = addrs[0]
+ }
+ return addr, nil
+}
+
+func (c *Client) startProxy() error {
+ cmd := exec.Command(c.ProxyBin)
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return err
+ }
+ defer stdout.Close()
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ result := make(chan error)
+ go func() {
+ bytes, _, err := bufio.NewReader(stdout).ReadLine()
+ if line := string(bytes); err == nil && line != "OK" {
+ err = fmt.Errorf("proxy returned %q, want \"OK\"", line)
+ }
+ result <- err
+ }()
+ select {
+ case err := <-result:
+ return err
+ case <-time.After(5 * time.Second):
+ return errors.New("timeout waiting for proxy to start")
+ }
+ panic("not reachable")
+}
+
+func (c *Client) resolveArgs() error {
+ if nargs := len(c.Args); nargs == 0 {
+ return errors.New("remote needed")
+ } else if nargs > 2 {
+ return fmt.Errorf("want at most 2 args, got %v", c.Args)
+ }
+
+ // Rewrite the url scheme to be http.
+ idx := len(c.Args) - 1
+ rawurl := c.Args[idx]
+ rurl, err := url.Parse(rawurl)
+ if err != nil {
+ return fmt.Errorf("invalid remote: %v", err)
+ }
+ c.insecure = rurl.Scheme == "persistent-http"
+ rurl.Scheme = "http"
+ c.Args[idx] = rurl.String()
+ if idx != 0 && c.Args[0] == rawurl {
+ c.Args[0] = c.Args[idx]
+ }
+ return nil
+}
diff --git a/contrib/persistent-https/main.go b/contrib/persistent-https/main.go
new file mode 100644
index 0000000000..fd1b107743
--- /dev/null
+++ b/contrib/persistent-https/main.go
@@ -0,0 +1,82 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The git-remote-persistent-https binary speeds up SSL operations by running
+// a daemon job that keeps a connection open to a Git server. This ensures the
+// git-remote-persistent-https--proxy is running and delegating execution
+// to the git-remote-http binary with the http_proxy set to the daemon job.
+// A unix socket is used to authenticate the proxy and discover the
+// HTTP address. Note, both the client and proxy are included in the same
+// binary.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "time"
+)
+
+var (
+ forceProxy = flag.Bool("proxy", false, "Whether to start the binary in proxy mode")
+ proxyBin = flag.String("proxy_bin", "git-remote-persistent-https--proxy", "Path to the proxy binary")
+ printLabel = flag.Bool("print_label", false, "Prints the build label for the binary")
+
+ // Variable that should be defined through the -X linker flag.
+ _BUILD_EMBED_LABEL string
+)
+
+const (
+ defaultMaxIdleDuration = 24 * time.Hour
+ defaultPollUpdateInterval = 15 * time.Minute
+)
+
+func main() {
+ flag.Parse()
+ if *printLabel {
+ // Short circuit execution to print the build label
+ fmt.Println(buildLabel())
+ return
+ }
+
+ var err error
+ if *forceProxy || strings.HasSuffix(os.Args[0], "--proxy") {
+ log.SetPrefix("git-remote-persistent-https--proxy: ")
+ proxy := &Proxy{
+ BuildLabel: buildLabel(),
+ MaxIdleDuration: defaultMaxIdleDuration,
+ PollUpdateInterval: defaultPollUpdateInterval,
+ }
+ err = proxy.Run()
+ } else {
+ log.SetPrefix("git-remote-persistent-https: ")
+ client := &Client{
+ ProxyBin: *proxyBin,
+ Args: flag.Args(),
+ }
+ err = client.Run()
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+}
+
+func buildLabel() string {
+ if _BUILD_EMBED_LABEL == "" {
+ log.Println(`unlabeled build; build with "make" to label`)
+ }
+ return _BUILD_EMBED_LABEL
+}
diff --git a/contrib/persistent-https/proxy.go b/contrib/persistent-https/proxy.go
new file mode 100644
index 0000000000..bb0cdba386
--- /dev/null
+++ b/contrib/persistent-https/proxy.go
@@ -0,0 +1,190 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "os"
+ "os/exec"
+ "os/signal"
+ "sync"
+ "syscall"
+ "time"
+)
+
+type Proxy struct {
+ BuildLabel string
+ MaxIdleDuration time.Duration
+ PollUpdateInterval time.Duration
+
+ ul net.Listener
+ httpAddr string
+ httpsAddr string
+}
+
+func (p *Proxy) Run() error {
+ hl, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ return fmt.Errorf("http listen failed: %v", err)
+ }
+ defer hl.Close()
+
+ hsl, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ return fmt.Errorf("https listen failed: %v", err)
+ }
+ defer hsl.Close()
+
+ p.ul, err = DefaultSocket.Listen()
+ if err != nil {
+ c, derr := DefaultSocket.Dial()
+ if derr == nil {
+ c.Close()
+ fmt.Println("OK\nA proxy is already running... exiting")
+ return nil
+ } else if e, ok := derr.(*net.OpError); ok && e.Err == syscall.ECONNREFUSED {
+ // Nothing is listening on the socket, unlink it and try again.
+ syscall.Unlink(DefaultSocket.Path())
+ p.ul, err = DefaultSocket.Listen()
+ }
+ if err != nil {
+ return fmt.Errorf("unix listen failed on %v: %v", DefaultSocket.Path(), err)
+ }
+ }
+ defer p.ul.Close()
+ go p.closeOnSignal()
+ go p.closeOnUpdate()
+
+ p.httpAddr = hl.Addr().String()
+ p.httpsAddr = hsl.Addr().String()
+ fmt.Printf("OK\nListening on unix socket=%v http=%v https=%v\n",
+ p.ul.Addr(), p.httpAddr, p.httpsAddr)
+
+ result := make(chan error, 2)
+ go p.serveUnix(result)
+ go func() {
+ result <- http.Serve(hl, &httputil.ReverseProxy{
+ FlushInterval: 500 * time.Millisecond,
+ Director: func(r *http.Request) {},
+ })
+ }()
+ go func() {
+ result <- http.Serve(hsl, &httputil.ReverseProxy{
+ FlushInterval: 500 * time.Millisecond,
+ Director: func(r *http.Request) {
+ r.URL.Scheme = "https"
+ },
+ })
+ }()
+ return <-result
+}
+
+type socketContext struct {
+ sync.WaitGroup
+ mutex sync.Mutex
+ last time.Time
+}
+
+func (sc *socketContext) Done() {
+ sc.mutex.Lock()
+ defer sc.mutex.Unlock()
+ sc.last = time.Now()
+ sc.WaitGroup.Done()
+}
+
+func (p *Proxy) serveUnix(result chan<- error) {
+ sockCtx := &socketContext{}
+ go p.closeOnIdle(sockCtx)
+
+ var err error
+ for {
+ var uconn net.Conn
+ uconn, err = p.ul.Accept()
+ if err != nil {
+ err = fmt.Errorf("accept failed: %v", err)
+ break
+ }
+ sockCtx.Add(1)
+ go p.handleUnixConn(sockCtx, uconn)
+ }
+ sockCtx.Wait()
+ result <- err
+}
+
+func (p *Proxy) handleUnixConn(sockCtx *socketContext, uconn net.Conn) {
+ defer sockCtx.Done()
+ defer uconn.Close()
+ data := []byte(fmt.Sprintf("%v\n%v", p.httpsAddr, p.httpAddr))
+ uconn.SetDeadline(time.Now().Add(5 * time.Second))
+ for i := 0; i < 2; i++ {
+ if n, err := uconn.Write(data); err != nil {
+ log.Printf("error sending http addresses: %+v\n", err)
+ return
+ } else if n != len(data) {
+ log.Printf("sent %d data bytes, wanted %d\n", n, len(data))
+ return
+ }
+ if _, err := uconn.Read([]byte{0, 0, 0, 0}); err != nil {
+ log.Printf("error waiting for Ack: %+v\n", err)
+ return
+ }
+ }
+ // Wait without a deadline for the client to finish via EOF
+ uconn.SetDeadline(time.Time{})
+ uconn.Read([]byte{0, 0, 0, 0})
+}
+
+func (p *Proxy) closeOnIdle(sockCtx *socketContext) {
+ for d := p.MaxIdleDuration; d > 0; {
+ time.Sleep(d)
+ sockCtx.Wait()
+ sockCtx.mutex.Lock()
+ if d = sockCtx.last.Add(p.MaxIdleDuration).Sub(time.Now()); d <= 0 {
+ log.Println("graceful shutdown from idle timeout")
+ p.ul.Close()
+ }
+ sockCtx.mutex.Unlock()
+ }
+}
+
+func (p *Proxy) closeOnUpdate() {
+ for {
+ time.Sleep(p.PollUpdateInterval)
+ if out, err := exec.Command(os.Args[0], "--print_label").Output(); err != nil {
+ log.Printf("error polling for updated binary: %v\n", err)
+ } else if s := string(out[:len(out)-1]); p.BuildLabel != s {
+ log.Printf("graceful shutdown from updated binary: %q --> %q\n", p.BuildLabel, s)
+ p.ul.Close()
+ break
+ }
+ }
+}
+
+func (p *Proxy) closeOnSignal() {
+ ch := make(chan os.Signal, 10)
+ signal.Notify(ch, os.Interrupt, os.Kill, os.Signal(syscall.SIGTERM), os.Signal(syscall.SIGHUP))
+ sig := <-ch
+ p.ul.Close()
+ switch sig {
+ case os.Signal(syscall.SIGHUP):
+ log.Printf("graceful shutdown from signal: %v\n", sig)
+ default:
+ log.Fatalf("exiting from signal: %v\n", sig)
+ }
+}
diff --git a/contrib/persistent-https/socket.go b/contrib/persistent-https/socket.go
new file mode 100644
index 0000000000..193b911dd1
--- /dev/null
+++ b/contrib/persistent-https/socket.go
@@ -0,0 +1,97 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "net"
+ "os"
+ "path/filepath"
+ "syscall"
+)
+
+// A Socket is a wrapper around a Unix socket that verifies directory
+// permissions.
+type Socket struct {
+ Dir string
+}
+
+func defaultDir() string {
+ sockPath := ".git-credential-cache"
+ if home := os.Getenv("HOME"); home != "" {
+ return filepath.Join(home, sockPath)
+ }
+ log.Printf("socket: cannot find HOME path. using relative directory %q for socket", sockPath)
+ return sockPath
+}
+
+// DefaultSocket is a Socket in the $HOME/.git-credential-cache directory.
+var DefaultSocket = Socket{Dir: defaultDir()}
+
+// Listen announces the local network address of the unix socket. The
+// permissions on the socket directory are verified before attempting
+// the actual listen.
+func (s Socket) Listen() (net.Listener, error) {
+ network, addr := "unix", s.Path()
+ if err := s.mkdir(); err != nil {
+ return nil, &net.OpError{Op: "listen", Net: network, Addr: &net.UnixAddr{Name: addr, Net: network}, Err: err}
+ }
+ return net.Listen(network, addr)
+}
+
+// Dial connects to the unix socket. The permissions on the socket directory
+// are verified before attempting the actual dial.
+func (s Socket) Dial() (net.Conn, error) {
+ network, addr := "unix", s.Path()
+ if err := s.checkPermissions(); err != nil {
+ return nil, &net.OpError{Op: "dial", Net: network, Addr: &net.UnixAddr{Name: addr, Net: network}, Err: err}
+ }
+ return net.Dial(network, addr)
+}
+
+// Path returns the fully specified file name of the unix socket.
+func (s Socket) Path() string {
+ return filepath.Join(s.Dir, "persistent-https-proxy-socket")
+}
+
+func (s Socket) mkdir() error {
+ if err := s.checkPermissions(); err == nil {
+ return nil
+ } else if !os.IsNotExist(err) {
+ return err
+ }
+ if err := os.MkdirAll(s.Dir, 0700); err != nil {
+ return err
+ }
+ return s.checkPermissions()
+}
+
+func (s Socket) checkPermissions() error {
+ fi, err := os.Stat(s.Dir)
+ if err != nil {
+ return err
+ }
+ if !fi.IsDir() {
+ return fmt.Errorf("socket: got file, want directory for %q", s.Dir)
+ }
+ if fi.Mode().Perm() != 0700 {
+ return fmt.Errorf("socket: got perm %o, want 700 for %q", fi.Mode().Perm(), s.Dir)
+ }
+ if st := fi.Sys().(*syscall.Stat_t); int(st.Uid) != os.Getuid() {
+ return fmt.Errorf("socket: got uid %d, want %d for %q", st.Uid, os.Getuid(), s.Dir)
+ }
+ return nil
+}
diff --git a/contrib/rerere-train.sh b/contrib/rerere-train.sh
index 2cfe1b936b..36b6feebe0 100755
--- a/contrib/rerere-train.sh
+++ b/contrib/rerere-train.sh
@@ -7,7 +7,7 @@ USAGE="$me rev-list-args"
SUBDIRECTORY_OK=Yes
OPTIONS_SPEC=
-. git-sh-setup
+. $(git --exec-path)/git-sh-setup
require_work_tree
cd_to_toplevel
diff --git a/contrib/subtree/.gitignore b/contrib/subtree/.gitignore
new file mode 100644
index 0000000000..7e77c9d022
--- /dev/null
+++ b/contrib/subtree/.gitignore
@@ -0,0 +1,5 @@
+*~
+git-subtree.xml
+git-subtree.1
+mainline
+subproj
diff --git a/contrib/subtree/COPYING b/contrib/subtree/COPYING
new file mode 100644
index 0000000000..d511905c16
--- /dev/null
+++ b/contrib/subtree/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/contrib/subtree/INSTALL b/contrib/subtree/INSTALL
new file mode 100644
index 0000000000..7ab0cf4509
--- /dev/null
+++ b/contrib/subtree/INSTALL
@@ -0,0 +1,28 @@
+HOW TO INSTALL git-subtree
+==========================
+
+First, build from the top source directory.
+
+Then, in contrib/subtree, run:
+
+ make
+ make install
+ make install-doc
+
+If you used configure to do the main build the git-subtree build will
+pick up those settings. If not, you will likely have to provide a
+value for prefix:
+
+ make prefix=<some dir>
+ make prefix=<some dir> install
+ make prefix=<some dir> install-doc
+
+To run tests first copy git-subtree to the main build area so the
+newly-built git can find it:
+
+ cp git-subtree ../..
+
+Then:
+
+ make test
+
diff --git a/contrib/subtree/Makefile b/contrib/subtree/Makefile
new file mode 100644
index 0000000000..05cdd5c9b2
--- /dev/null
+++ b/contrib/subtree/Makefile
@@ -0,0 +1,52 @@
+-include ../../config.mak.autogen
+-include ../../config.mak
+
+prefix ?= /usr/local
+mandir ?= $(prefix)/share/man
+libexecdir ?= $(prefix)/libexec/git-core
+gitdir ?= $(shell git --exec-path)
+man1dir ?= $(mandir)/man1
+
+gitver ?= $(word 3,$(shell git --version))
+
+# this should be set to a 'standard' bsd-type install program
+INSTALL ?= install
+
+ASCIIDOC_CONF = ../../Documentation/asciidoc.conf
+MANPAGE_NORMAL_XSL = ../../Documentation/manpage-normal.xsl
+
+GIT_SUBTREE_SH := git-subtree.sh
+GIT_SUBTREE := git-subtree
+
+GIT_SUBTREE_DOC := git-subtree.1
+GIT_SUBTREE_XML := git-subtree.xml
+GIT_SUBTREE_TXT := git-subtree.txt
+
+all: $(GIT_SUBTREE)
+
+$(GIT_SUBTREE): $(GIT_SUBTREE_SH)
+ cp $< $@ && chmod +x $@
+
+doc: $(GIT_SUBTREE_DOC)
+
+install: $(GIT_SUBTREE)
+ $(INSTALL) -m 755 $(GIT_SUBTREE) $(libexecdir)
+
+install-doc: install-man
+
+install-man: $(GIT_SUBTREE_DOC)
+ $(INSTALL) -m 644 $^ $(man1dir)
+
+$(GIT_SUBTREE_DOC): $(GIT_SUBTREE_XML)
+ xmlto -m $(MANPAGE_NORMAL_XSL) man $^
+
+$(GIT_SUBTREE_XML): $(GIT_SUBTREE_TXT)
+ asciidoc -b docbook -d manpage -f $(ASCIIDOC_CONF) \
+ -agit_version=$(gitver) $^
+
+test:
+ $(MAKE) -C t/ test
+
+clean:
+ rm -f *~ *.xml *.html *.1
+ rm -rf subproj mainline
diff --git a/contrib/subtree/README b/contrib/subtree/README
new file mode 100644
index 0000000000..c686b4a69b
--- /dev/null
+++ b/contrib/subtree/README
@@ -0,0 +1,8 @@
+
+Please read git-subtree.txt for documentation.
+
+Please don't contact me using github mail; it's slow, ugly, and worst of
+all, redundant. Email me instead at apenwarr@gmail.com and I'll be happy to
+help.
+
+Avery
diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh
new file mode 100755
index 0000000000..920c664bb7
--- /dev/null
+++ b/contrib/subtree/git-subtree.sh
@@ -0,0 +1,712 @@
+#!/bin/bash
+#
+# git-subtree.sh: split/join git repositories in subdirectories of this one
+#
+# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
+#
+if [ $# -eq 0 ]; then
+ set -- -h
+fi
+OPTS_SPEC="\
+git subtree add --prefix=<prefix> <commit>
+git subtree merge --prefix=<prefix> <commit>
+git subtree pull --prefix=<prefix> <repository> <refspec...>
+git subtree push --prefix=<prefix> <repository> <refspec...>
+git subtree split --prefix=<prefix> <commit...>
+--
+h,help show the help
+q quiet
+d show debug messages
+P,prefix= the name of the subdir to split out
+m,message= use the given message as the commit message for the merge commit
+ options for 'split'
+annotate= add a prefix to commit message of new commits
+b,branch= create a new branch from the split subtree
+ignore-joins ignore prior --rejoin commits
+onto= try connecting new tree to an existing one
+rejoin merge the new branch back into HEAD
+ options for 'add', 'merge', 'pull' and 'push'
+squash merge subtree changes as a single commit
+"
+eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
+
+PATH=$PATH:$(git --exec-path)
+. git-sh-setup
+
+require_work_tree
+
+quiet=
+branch=
+debug=
+command=
+onto=
+rejoin=
+ignore_joins=
+annotate=
+squash=
+message=
+
+debug()
+{
+ if [ -n "$debug" ]; then
+ echo "$@" >&2
+ fi
+}
+
+say()
+{
+ if [ -z "$quiet" ]; then
+ echo "$@" >&2
+ fi
+}
+
+assert()
+{
+ if "$@"; then
+ :
+ else
+ die "assertion failed: " "$@"
+ fi
+}
+
+
+#echo "Options: $*"
+
+while [ $# -gt 0 ]; do
+ opt="$1"
+ shift
+ case "$opt" in
+ -q) quiet=1 ;;
+ -d) debug=1 ;;
+ --annotate) annotate="$1"; shift ;;
+ --no-annotate) annotate= ;;
+ -b) branch="$1"; shift ;;
+ -P) prefix="$1"; shift ;;
+ -m) message="$1"; shift ;;
+ --no-prefix) prefix= ;;
+ --onto) onto="$1"; shift ;;
+ --no-onto) onto= ;;
+ --rejoin) rejoin=1 ;;
+ --no-rejoin) rejoin= ;;
+ --ignore-joins) ignore_joins=1 ;;
+ --no-ignore-joins) ignore_joins= ;;
+ --squash) squash=1 ;;
+ --no-squash) squash= ;;
+ --) break ;;
+ *) die "Unexpected option: $opt" ;;
+ esac
+done
+
+command="$1"
+shift
+case "$command" in
+ add|merge|pull) default= ;;
+ split|push) default="--default HEAD" ;;
+ *) die "Unknown command '$command'" ;;
+esac
+
+if [ -z "$prefix" ]; then
+ die "You must provide the --prefix option."
+fi
+
+case "$command" in
+ add) [ -e "$prefix" ] &&
+ die "prefix '$prefix' already exists." ;;
+ *) [ -e "$prefix" ] ||
+ die "'$prefix' does not exist; use 'git subtree add'" ;;
+esac
+
+dir="$(dirname "$prefix/.")"
+
+if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
+ revs=$(git rev-parse $default --revs-only "$@") || exit $?
+ dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
+ if [ -n "$dirs" ]; then
+ die "Error: Use --prefix instead of bare filenames."
+ fi
+fi
+
+debug "command: {$command}"
+debug "quiet: {$quiet}"
+debug "revs: {$revs}"
+debug "dir: {$dir}"
+debug "opts: {$*}"
+debug
+
+cache_setup()
+{
+ cachedir="$GIT_DIR/subtree-cache/$$"
+ rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
+ mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
+ mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree"
+ debug "Using cachedir: $cachedir" >&2
+}
+
+cache_get()
+{
+ for oldrev in $*; do
+ if [ -r "$cachedir/$oldrev" ]; then
+ read newrev <"$cachedir/$oldrev"
+ echo $newrev
+ fi
+ done
+}
+
+cache_miss()
+{
+ for oldrev in $*; do
+ if [ ! -r "$cachedir/$oldrev" ]; then
+ echo $oldrev
+ fi
+ done
+}
+
+check_parents()
+{
+ missed=$(cache_miss $*)
+ for miss in $missed; do
+ if [ ! -r "$cachedir/notree/$miss" ]; then
+ debug " incorrect order: $miss"
+ fi
+ done
+}
+
+set_notree()
+{
+ echo "1" > "$cachedir/notree/$1"
+}
+
+cache_set()
+{
+ oldrev="$1"
+ newrev="$2"
+ if [ "$oldrev" != "latest_old" \
+ -a "$oldrev" != "latest_new" \
+ -a -e "$cachedir/$oldrev" ]; then
+ die "cache for $oldrev already exists!"
+ fi
+ echo "$newrev" >"$cachedir/$oldrev"
+}
+
+rev_exists()
+{
+ if git rev-parse "$1" >/dev/null 2>&1; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+rev_is_descendant_of_branch()
+{
+ newrev="$1"
+ branch="$2"
+ branch_hash=$(git rev-parse $branch)
+ match=$(git rev-list -1 $branch_hash ^$newrev)
+
+ if [ -z "$match" ]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# if a commit doesn't have a parent, this might not work. But we only want
+# to remove the parent from the rev-list, and since it doesn't exist, it won't
+# be there anyway, so do nothing in that case.
+try_remove_previous()
+{
+ if rev_exists "$1^"; then
+ echo "^$1^"
+ fi
+}
+
+find_latest_squash()
+{
+ debug "Looking for latest squash ($dir)..."
+ dir="$1"
+ sq=
+ main=
+ sub=
+ git log --grep="^git-subtree-dir: $dir/*\$" \
+ --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
+ while read a b junk; do
+ debug "$a $b $junk"
+ debug "{{$sq/$main/$sub}}"
+ case "$a" in
+ START) sq="$b" ;;
+ git-subtree-mainline:) main="$b" ;;
+ git-subtree-split:) sub="$b" ;;
+ END)
+ if [ -n "$sub" ]; then
+ if [ -n "$main" ]; then
+ # a rejoin commit?
+ # Pretend its sub was a squash.
+ sq="$sub"
+ fi
+ debug "Squash found: $sq $sub"
+ echo "$sq" "$sub"
+ break
+ fi
+ sq=
+ main=
+ sub=
+ ;;
+ esac
+ done
+}
+
+find_existing_splits()
+{
+ debug "Looking for prior splits..."
+ dir="$1"
+ revs="$2"
+ main=
+ sub=
+ git log --grep="^git-subtree-dir: $dir/*\$" \
+ --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
+ while read a b junk; do
+ case "$a" in
+ START) sq="$b" ;;
+ git-subtree-mainline:) main="$b" ;;
+ git-subtree-split:) sub="$b" ;;
+ END)
+ debug " Main is: '$main'"
+ if [ -z "$main" -a -n "$sub" ]; then
+ # squash commits refer to a subtree
+ debug " Squash: $sq from $sub"
+ cache_set "$sq" "$sub"
+ fi
+ if [ -n "$main" -a -n "$sub" ]; then
+ debug " Prior: $main -> $sub"
+ cache_set $main $sub
+ cache_set $sub $sub
+ try_remove_previous "$main"
+ try_remove_previous "$sub"
+ fi
+ main=
+ sub=
+ ;;
+ esac
+ done
+}
+
+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" |
+ (
+ read GIT_AUTHOR_NAME
+ read GIT_AUTHOR_EMAIL
+ read GIT_AUTHOR_DATE
+ read GIT_COMMITTER_NAME
+ read GIT_COMMITTER_EMAIL
+ read GIT_COMMITTER_DATE
+ export GIT_AUTHOR_NAME \
+ GIT_AUTHOR_EMAIL \
+ GIT_AUTHOR_DATE \
+ GIT_COMMITTER_NAME \
+ GIT_COMMITTER_EMAIL \
+ GIT_COMMITTER_DATE
+ (echo -n "$annotate"; cat ) |
+ git commit-tree "$2" $3 # reads the rest of stdin
+ ) || die "Can't copy commit $1"
+}
+
+add_msg()
+{
+ dir="$1"
+ latest_old="$2"
+ latest_new="$3"
+ if [ -n "$message" ]; then
+ commit_message="$message"
+ else
+ commit_message="Add '$dir/' from commit '$latest_new'"
+ fi
+ cat <<-EOF
+ $commit_message
+
+ git-subtree-dir: $dir
+ git-subtree-mainline: $latest_old
+ git-subtree-split: $latest_new
+ EOF
+}
+
+add_squashed_msg()
+{
+ if [ -n "$message" ]; then
+ echo "$message"
+ else
+ echo "Merge commit '$1' as '$2'"
+ fi
+}
+
+rejoin_msg()
+{
+ dir="$1"
+ latest_old="$2"
+ latest_new="$3"
+ if [ -n "$message" ]; then
+ commit_message="$message"
+ else
+ commit_message="Split '$dir/' into commit '$latest_new'"
+ fi
+ cat <<-EOF
+ $commit_message
+
+ git-subtree-dir: $dir
+ git-subtree-mainline: $latest_old
+ git-subtree-split: $latest_new
+ EOF
+}
+
+squash_msg()
+{
+ dir="$1"
+ oldsub="$2"
+ newsub="$3"
+ newsub_short=$(git rev-parse --short "$newsub")
+
+ if [ -n "$oldsub" ]; then
+ oldsub_short=$(git rev-parse --short "$oldsub")
+ echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
+ echo
+ git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
+ git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
+ else
+ echo "Squashed '$dir/' content from commit $newsub_short"
+ fi
+
+ echo
+ echo "git-subtree-dir: $dir"
+ echo "git-subtree-split: $newsub"
+}
+
+toptree_for_commit()
+{
+ commit="$1"
+ git log -1 --pretty=format:'%T' "$commit" -- || exit $?
+}
+
+subtree_for_commit()
+{
+ commit="$1"
+ dir="$2"
+ git ls-tree "$commit" -- "$dir" |
+ while read mode type tree name; do
+ assert [ "$name" = "$dir" ]
+ assert [ "$type" = "tree" -o "$type" = "commit" ]
+ [ "$type" = "commit" ] && continue # ignore submodules
+ echo $tree
+ break
+ done
+}
+
+tree_changed()
+{
+ tree=$1
+ shift
+ if [ $# -ne 1 ]; then
+ return 0 # weird parents, consider it changed
+ else
+ ptree=$(toptree_for_commit $1)
+ if [ "$ptree" != "$tree" ]; then
+ return 0 # changed
+ else
+ return 1 # not changed
+ fi
+ fi
+}
+
+new_squash_commit()
+{
+ old="$1"
+ oldsub="$2"
+ newsub="$3"
+ tree=$(toptree_for_commit $newsub) || exit $?
+ if [ -n "$old" ]; then
+ squash_msg "$dir" "$oldsub" "$newsub" |
+ git commit-tree "$tree" -p "$old" || exit $?
+ else
+ squash_msg "$dir" "" "$newsub" |
+ git commit-tree "$tree" || exit $?
+ fi
+}
+
+copy_or_skip()
+{
+ rev="$1"
+ tree="$2"
+ newparents="$3"
+ assert [ -n "$tree" ]
+
+ identical=
+ nonidentical=
+ p=
+ gotparents=
+ for parent in $newparents; do
+ ptree=$(toptree_for_commit $parent) || exit $?
+ [ -z "$ptree" ] && continue
+ if [ "$ptree" = "$tree" ]; then
+ # an identical parent could be used in place of this rev.
+ identical="$parent"
+ else
+ nonidentical="$parent"
+ fi
+
+ # sometimes both old parents map to the same newparent;
+ # eliminate duplicates
+ is_new=1
+ for gp in $gotparents; do
+ if [ "$gp" = "$parent" ]; then
+ is_new=
+ break
+ fi
+ done
+ if [ -n "$is_new" ]; then
+ gotparents="$gotparents $parent"
+ p="$p -p $parent"
+ fi
+ done
+
+ if [ -n "$identical" ]; then
+ echo $identical
+ else
+ copy_commit $rev $tree "$p" || exit $?
+ fi
+}
+
+ensure_clean()
+{
+ if ! git diff-index HEAD --exit-code --quiet 2>&1; then
+ die "Working tree has modifications. Cannot add."
+ fi
+ if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
+ die "Index has modifications. Cannot add."
+ fi
+}
+
+cmd_add()
+{
+ if [ -e "$dir" ]; then
+ die "'$dir' already exists. Cannot add."
+ fi
+
+ ensure_clean
+
+ if [ $# -eq 1 ]; then
+ "cmd_add_commit" "$@"
+ elif [ $# -eq 2 ]; then
+ "cmd_add_repository" "$@"
+ else
+ say "error: parameters were '$@'"
+ die "Provide either a refspec or a repository and refspec."
+ fi
+}
+
+cmd_add_repository()
+{
+ echo "git fetch" "$@"
+ repository=$1
+ refspec=$2
+ git fetch "$@" || exit $?
+ revs=FETCH_HEAD
+ set -- $revs
+ cmd_add_commit "$@"
+}
+
+cmd_add_commit()
+{
+ revs=$(git rev-parse $default --revs-only "$@") || exit $?
+ set -- $revs
+ rev="$1"
+
+ debug "Adding $dir as '$rev'..."
+ git read-tree --prefix="$dir" $rev || exit $?
+ git checkout -- "$dir" || exit $?
+ tree=$(git write-tree) || exit $?
+
+ headrev=$(git rev-parse HEAD) || exit $?
+ if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
+ headp="-p $headrev"
+ else
+ headp=
+ fi
+
+ if [ -n "$squash" ]; then
+ rev=$(new_squash_commit "" "" "$rev") || exit $?
+ commit=$(add_squashed_msg "$rev" "$dir" |
+ git commit-tree $tree $headp -p "$rev") || exit $?
+ else
+ commit=$(add_msg "$dir" "$headrev" "$rev" |
+ git commit-tree $tree $headp -p "$rev") || exit $?
+ fi
+ git reset "$commit" || exit $?
+
+ say "Added dir '$dir'"
+}
+
+cmd_split()
+{
+ debug "Splitting $dir..."
+ cache_setup || exit $?
+
+ if [ -n "$onto" ]; then
+ debug "Reading history for --onto=$onto..."
+ git rev-list $onto |
+ while read rev; do
+ # the 'onto' history is already just the subdir, so
+ # any parent we find there can be used verbatim
+ debug " cache: $rev"
+ cache_set $rev $rev
+ done
+ fi
+
+ if [ -n "$ignore_joins" ]; then
+ unrevs=
+ else
+ unrevs="$(find_existing_splits "$dir" "$revs")"
+ fi
+
+ # We can't restrict rev-list to only $dir here, because some of our
+ # parents have the $dir contents the root, and those won't match.
+ # (and rev-list --follow doesn't seem to solve this)
+ grl='git rev-list --topo-order --reverse --parents $revs $unrevs'
+ revmax=$(eval "$grl" | wc -l)
+ revcount=0
+ createcount=0
+ eval "$grl" |
+ while read rev parents; do
+ revcount=$(($revcount + 1))
+ say -n "$revcount/$revmax ($createcount) "
+ debug "Processing commit: $rev"
+ exists=$(cache_get $rev)
+ if [ -n "$exists" ]; then
+ debug " prior: $exists"
+ continue
+ fi
+ createcount=$(($createcount + 1))
+ debug " parents: $parents"
+ newparents=$(cache_get $parents)
+ debug " newparents: $newparents"
+
+ tree=$(subtree_for_commit $rev "$dir")
+ debug " tree is: $tree"
+
+ check_parents $parents
+
+ # ugly. is there no better way to tell if this is a subtree
+ # vs. a mainline commit? Does it matter?
+ if [ -z $tree ]; then
+ set_notree $rev
+ if [ -n "$newparents" ]; then
+ cache_set $rev $rev
+ fi
+ continue
+ fi
+
+ newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
+ debug " newrev is: $newrev"
+ cache_set $rev $newrev
+ cache_set latest_new $newrev
+ cache_set latest_old $rev
+ done || exit $?
+ latest_new=$(cache_get latest_new)
+ if [ -z "$latest_new" ]; then
+ die "No new revisions were found"
+ fi
+
+ if [ -n "$rejoin" ]; then
+ debug "Merging split branch into HEAD..."
+ latest_old=$(cache_get latest_old)
+ git merge -s ours \
+ -m "$(rejoin_msg $dir $latest_old $latest_new)" \
+ $latest_new >&2 || exit $?
+ fi
+ if [ -n "$branch" ]; then
+ if rev_exists "refs/heads/$branch"; then
+ if ! rev_is_descendant_of_branch $latest_new $branch; then
+ die "Branch '$branch' is not an ancestor of commit '$latest_new'."
+ fi
+ action='Updated'
+ else
+ action='Created'
+ fi
+ git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
+ say "$action branch '$branch'"
+ fi
+ echo $latest_new
+ exit 0
+}
+
+cmd_merge()
+{
+ revs=$(git rev-parse $default --revs-only "$@") || exit $?
+ ensure_clean
+
+ set -- $revs
+ if [ $# -ne 1 ]; then
+ die "You must provide exactly one revision. Got: '$revs'"
+ fi
+ rev="$1"
+
+ if [ -n "$squash" ]; then
+ first_split="$(find_latest_squash "$dir")"
+ if [ -z "$first_split" ]; then
+ die "Can't squash-merge: '$dir' was never added."
+ fi
+ set $first_split
+ old=$1
+ sub=$2
+ if [ "$sub" = "$rev" ]; then
+ say "Subtree is already at commit $rev."
+ exit 0
+ fi
+ new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
+ debug "New squash commit: $new"
+ rev="$new"
+ fi
+
+ version=$(git version)
+ if [ "$version" \< "git version 1.7" ]; then
+ if [ -n "$message" ]; then
+ git merge -s subtree --message="$message" $rev
+ else
+ git merge -s subtree $rev
+ fi
+ else
+ if [ -n "$message" ]; then
+ git merge -Xsubtree="$prefix" --message="$message" $rev
+ else
+ git merge -Xsubtree="$prefix" $rev
+ fi
+ fi
+}
+
+cmd_pull()
+{
+ ensure_clean
+ git fetch "$@" || exit $?
+ revs=FETCH_HEAD
+ set -- $revs
+ cmd_merge "$@"
+}
+
+cmd_push()
+{
+ if [ $# -ne 2 ]; then
+ die "You must provide <repository> <refspec>"
+ fi
+ if [ -e "$dir" ]; then
+ repository=$1
+ refspec=$2
+ echo "git push using: " $repository $refspec
+ git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
+ else
+ die "'$dir' must already exist. Try 'git subtree add'."
+ fi
+}
+
+"cmd_$command" "$@"
diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt
new file mode 100644
index 0000000000..0c44fda011
--- /dev/null
+++ b/contrib/subtree/git-subtree.txt
@@ -0,0 +1,366 @@
+git-subtree(1)
+==============
+
+NAME
+----
+git-subtree - Merge subtrees together and split repository into subtrees
+
+
+SYNOPSIS
+--------
+[verse]
+'git subtree' add -P <prefix> <commit>
+'git subtree' pull -P <prefix> <repository> <refspec...>
+'git subtree' push -P <prefix> <repository> <refspec...>
+'git subtree' merge -P <prefix> <commit>
+'git subtree' split -P <prefix> [OPTIONS] [<commit>]
+
+
+DESCRIPTION
+-----------
+Subtrees allow subprojects to be included within a subdirectory
+of the main project, optionally including the subproject's
+entire history.
+
+For example, you could include the source code for a library
+as a subdirectory of your application.
+
+Subtrees are not to be confused with submodules, which are meant for
+the same task. Unlike submodules, subtrees do not need any special
+constructions (like .gitmodule files or gitlinks) be present in
+your repository, and do not force end-users of your
+repository to do anything special or to understand how subtrees
+work. A subtree is just a subdirectory that can be
+committed to, branched, and merged along with your project in
+any way you want.
+
+They are also not to be confused with using the subtree merge
+strategy. The main difference is that, besides merging
+the other project as a subdirectory, you can also extract the
+entire history of a subdirectory from your project and make it
+into a standalone project. Unlike the subtree merge strategy
+you can alternate back and forth between these
+two operations. If the standalone library gets updated, you can
+automatically merge the changes into your project; if you
+update the library inside your project, you can "split" the
+changes back out again and merge them back into the library
+project.
+
+For example, if a library you made for one application ends up being
+useful elsewhere, you can extract its entire history and publish
+that as its own git repository, without accidentally
+intermingling the history of your application project.
+
+[TIP]
+In order to keep your commit messages clean, we recommend that
+people split their commits between the subtrees and the main
+project as much as possible. That is, if you make a change that
+affects both the library and the main application, commit it in
+two pieces. That way, when you split the library commits out
+later, their descriptions will still make sense. But if this
+isn't important to you, it's not *necessary*. git subtree will
+simply leave out the non-library-related parts of the commit
+when it splits it out into the subproject later.
+
+
+COMMANDS
+--------
+add::
+ Create the <prefix> subtree by importing its contents
+ from the given <refspec> or <repository> and remote <refspec>.
+ A new commit is created automatically, joining the imported
+ project's history with your own. With '--squash', imports
+ only a single commit from the subproject, rather than its
+ entire history.
+
+merge::
+ Merge recent changes up to <commit> into the <prefix>
+ subtree. As with normal 'git merge', this doesn't
+ remove your own local changes; it just merges those
+ changes into the latest <commit>. With '--squash',
+ creates only one commit that contains all the changes,
+ rather than merging in the entire history.
+
+ If you use '--squash', the merge direction doesn't
+ always have to be forward; you can use this command to
+ go back in time from v2.5 to v2.4, for example. If your
+ merge introduces a conflict, you can resolve it in the
+ usual ways.
+
+pull::
+ Exactly like 'merge', but parallels 'git pull' in that
+ it fetches the given commit from the specified remote
+ repository.
+
+push::
+ Does a 'split' (see above) 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.
+
+split::
+ Extract a new, synthetic project history from the
+ history of the <prefix> subtree. The new history
+ includes only the commits (including merges) that
+ affected <prefix>, and each of those commits now has the
+ contents of <prefix> at the root of the project instead
+ of in a subdirectory. Thus, the newly created history
+ is suitable for export as a separate git repository.
+
+ After splitting successfully, a single commit id is
+ printed to stdout. This corresponds to the HEAD of the
+ newly created tree, which you can manipulate however you
+ want.
+
+ Repeated splits of exactly the same history are
+ guaranteed to be identical (ie. to produce the same
+ commit ids). Because of this, if you add new commits
+ and then re-split, the new commits will be attached as
+ commits on top of the history you generated last time,
+ so 'git merge' and friends will work as expected.
+
+ Note that if you use '--squash' when you merge, you
+ should usually not just '--rejoin' when you split.
+
+
+OPTIONS
+-------
+-q::
+--quiet::
+ Suppress unnecessary output messages on stderr.
+
+-d::
+--debug::
+ Produce even more unnecessary output messages on stderr.
+
+-P <prefix>::
+--prefix=<prefix>::
+ Specify the path in the repository to the subtree you
+ want to manipulate. This option is mandatory
+ for all commands.
+
+-m <message>::
+--message=<message>::
+ This option is only valid for add, merge and pull (unsure).
+ Specify <message> as the commit message for the merge commit.
+
+
+OPTIONS FOR add, merge, push, pull
+----------------------------------
+--squash::
+ This option is only valid for add, merge, push and pull
+ commands.
+
+ Instead of merging the entire history from the subtree
+ project, produce only a single commit that contains all
+ the differences you want to merge, and then merge that
+ new commit into your project.
+
+ Using this option helps to reduce log clutter. People
+ rarely want to see every change that happened between
+ v1.0 and v1.1 of the library they're using, since none of the
+ interim versions were ever included in their application.
+
+ Using '--squash' also helps avoid problems when the same
+ subproject is included multiple times in the same
+ project, or is removed and then re-added. In such a
+ case, it doesn't make sense to combine the histories
+ anyway, since it's unclear which part of the history
+ belongs to which subtree.
+
+ Furthermore, with '--squash', you can switch back and
+ forth between different versions of a subtree, rather
+ than strictly forward. 'git subtree merge --squash'
+ always adjusts the subtree to match the exactly
+ specified commit, even if getting to that commit would
+ require undoing some changes that were added earlier.
+
+ Whether or not you use '--squash', changes made in your
+ local repository remain intact and can be later split
+ and send upstream to the subproject.
+
+
+OPTIONS FOR split
+-----------------
+--annotate=<annotation>::
+ This option is only valid for the split command.
+
+ When generating synthetic history, add <annotation> as a
+ prefix to each commit message. Since we're creating new
+ commits with the same commit message, but possibly
+ different content, from the original commits, this can help
+ to differentiate them and avoid confusion.
+
+ Whenever you split, you need to use the same
+ <annotation>, or else you don't have a guarantee that
+ the new re-created history will be identical to the old
+ one. That will prevent merging from working correctly.
+ git subtree tries to make it work anyway, particularly
+ if you use --rejoin, but it may not always be effective.
+
+-b <branch>::
+--branch=<branch>::
+ This option is only valid for the split command.
+
+ After generating the synthetic history, create a new
+ branch called <branch> that contains the new history.
+ This is suitable for immediate pushing upstream.
+ <branch> must not already exist.
+
+--ignore-joins::
+ This option is only valid for the split command.
+
+ If you use '--rejoin', git subtree attempts to optimize
+ its history reconstruction to generate only the new
+ commits since the last '--rejoin'. '--ignore-join'
+ disables this behaviour, forcing it to regenerate the
+ entire history. In a large project, this can take a
+ long time.
+
+--onto=<onto>::
+ This option is only valid for the split command.
+
+ If your subtree was originally imported using something
+ other than git subtree, its history may not match what
+ git subtree is expecting. In that case, you can specify
+ the commit id <onto> that corresponds to the first
+ revision of the subproject's history that was imported
+ into your project, and git subtree will attempt to build
+ its history from there.
+
+ If you used 'git subtree add', you should never need
+ this option.
+
+--rejoin::
+ This option is only valid for the split command.
+
+ After splitting, merge the newly created synthetic
+ history back into your main project. That way, future
+ splits can search only the part of history that has
+ been added since the most recent --rejoin.
+
+ If your split commits end up merged into the upstream
+ subproject, and then you want to get the latest upstream
+ version, this will allow git's merge algorithm to more
+ intelligently avoid conflicts (since it knows these
+ synthetic commits are already part of the upstream
+ repository).
+
+ Unfortunately, using this option results in 'git log'
+ showing an extra copy of every new commit that was
+ created (the original, and the synthetic one).
+
+ If you do all your merges with '--squash', don't use
+ '--rejoin' when you split, because you don't want the
+ subproject's history to be part of your project anyway.
+
+
+EXAMPLE 1. Add command
+----------------------
+Let's assume that you have a local repository that you would like
+to add an external vendor library to. In this case we will add the
+git-subtree repository as a subdirectory of your already existing
+git-extensions repository in ~/git-extensions/:
+
+ $ git subtree add --prefix=git-subtree --squash \
+ git://github.com/apenwarr/git-subtree.git master
+
+'master' needs to be a valid remote ref and can be a different branch
+name
+
+You can omit the --squash flag, but doing so will increase the number
+of commits that are incldued in your local repository.
+
+We now have a ~/git-extensions/git-subtree directory containing code
+from the master branch of git://github.com/apenwarr/git-subtree.git
+in our git-extensions repository.
+
+EXAMPLE 2. Extract a subtree using commit, merge and pull
+---------------------------------------------------------
+Let's use the repository for the git source code as an example.
+First, get your own copy of the git.git repository:
+
+ $ git clone git://git.kernel.org/pub/scm/git/git.git test-git
+ $ cd test-git
+
+gitweb (commit 1130ef3) was merged into git as of commit
+0a8f4f0, after which it was no longer maintained separately.
+But imagine it had been maintained separately, and we wanted to
+extract git's changes to gitweb since that time, to share with
+the upstream. You could do this:
+
+ $ git subtree split --prefix=gitweb --annotate='(split) ' \
+ 0a8f4f0^.. --onto=1130ef3 --rejoin \
+ --branch gitweb-latest
+ $ gitk gitweb-latest
+ $ git push git@github.com:whatever/gitweb.git gitweb-latest:master
+
+(We use '0a8f4f0^..' because that means "all the changes from
+0a8f4f0 to the current version, including 0a8f4f0 itself.")
+
+If gitweb had originally been merged using 'git subtree add' (or
+a previous split had already been done with --rejoin specified)
+then you can do all your splits without having to remember any
+weird commit ids:
+
+ $ git subtree split --prefix=gitweb --annotate='(split) ' --rejoin \
+ --branch gitweb-latest2
+
+And you can merge changes back in from the upstream project just
+as easily:
+
+ $ git subtree pull --prefix=gitweb \
+ git@github.com:whatever/gitweb.git master
+
+Or, using '--squash', you can actually rewind to an earlier
+version of gitweb:
+
+ $ git subtree merge --prefix=gitweb --squash gitweb-latest~10
+
+Then make some changes:
+
+ $ date >gitweb/myfile
+ $ git add gitweb/myfile
+ $ git commit -m 'created myfile'
+
+And fast forward again:
+
+ $ git subtree merge --prefix=gitweb --squash gitweb-latest
+
+And notice that your change is still intact:
+
+ $ ls -l gitweb/myfile
+
+And you can split it out and look at your changes versus
+the standard gitweb:
+
+ git log gitweb-latest..$(git subtree split --prefix=gitweb)
+
+EXAMPLE 3. Extract a subtree using branch
+-----------------------------------------
+Suppose you have a source directory with many files and
+subdirectories, and you want to extract the lib directory to its own
+git project. Here's a short way to do it:
+
+First, make the new repository wherever you want:
+
+ $ <go to the new location>
+ $ git init --bare
+
+Back in your original directory:
+
+ $ git subtree split --prefix=lib --annotate="(split)" -b split
+
+Then push the new branch onto the new empty repository:
+
+ $ git push <new-repo> split:master
+
+
+AUTHOR
+------
+Written by Avery Pennarun <apenwarr@gmail.com>
+
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/contrib/subtree/t/Makefile b/contrib/subtree/t/Makefile
new file mode 100644
index 0000000000..c864810389
--- /dev/null
+++ b/contrib/subtree/t/Makefile
@@ -0,0 +1,69 @@
+# Run tests
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+#GIT_TEST_OPTS=--verbose --debug
+SHELL_PATH ?= $(SHELL)
+PERL_PATH ?= /usr/bin/perl
+TAR ?= $(TAR)
+RM ?= rm -f
+PROVE ?= prove
+DEFAULT_TEST_TARGET ?= test
+
+# Shell quote;
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+
+T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
+
+all: $(DEFAULT_TEST_TARGET)
+
+test: pre-clean $(TEST_LINT)
+ $(MAKE) aggregate-results-and-cleanup
+
+prove: pre-clean $(TEST_LINT)
+ @echo "*** prove ***"; GIT_CONFIG=.git/config $(PROVE) --exec '$(SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS)
+ $(MAKE) clean
+
+$(T):
+ @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS)
+
+pre-clean:
+ $(RM) -r test-results
+
+clean:
+ $(RM) -r 'trash directory'.* test-results
+ $(RM) -r valgrind/bin
+ $(RM) .prove
+
+test-lint: test-lint-duplicates test-lint-executable
+
+test-lint-duplicates:
+ @dups=`echo $(T) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \
+ test -z "$$dups" || { \
+ echo >&2 "duplicate test numbers:" $$dups; exit 1; }
+
+test-lint-executable:
+ @bad=`for i in $(T); do test -x "$$i" || echo $$i; done` && \
+ test -z "$$bad" || { \
+ echo >&2 "non-executable tests:" $$bad; exit 1; }
+
+aggregate-results-and-cleanup: $(T)
+ $(MAKE) aggregate-results
+ $(MAKE) clean
+
+aggregate-results:
+ for f in ../../../t/test-results/t*-*.counts; do \
+ echo "$$f"; \
+ done | '$(SHELL_PATH_SQ)' ../../../t/aggregate-results.sh
+
+valgrind:
+ $(MAKE) GIT_TEST_OPTS="$(GIT_TEST_OPTS) --valgrind"
+
+test-results:
+ mkdir -p test-results
+
+.PHONY: pre-clean $(T) aggregate-results clean valgrind
diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh
new file mode 100755
index 0000000000..bc2eeb0944
--- /dev/null
+++ b/contrib/subtree/t/t7900-subtree.sh
@@ -0,0 +1,508 @@
+#!/bin/sh
+#
+# Copyright (c) 2012 Avery Pennaraum
+#
+test_description='Basic porcelain support for subtrees
+
+This test verifies the basic operation of the merge, pull, add
+and split subcommands of git subtree.
+'
+
+export TEST_DIRECTORY=$(pwd)/../../../t
+
+. ../../../t/test-lib.sh
+
+create()
+{
+ echo "$1" >"$1"
+ git add "$1"
+}
+
+
+check_equal()
+{
+ test_debug 'echo'
+ test_debug "echo \"check a:\" \"{$1}\""
+ test_debug "echo \" b:\" \"{$2}\""
+ if [ "$1" = "$2" ]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+fixnl()
+{
+ t=""
+ while read x; do
+ t="$t$x "
+ done
+ echo $t
+}
+
+multiline()
+{
+ while read x; do
+ set -- $x
+ for d in "$@"; do
+ echo "$d"
+ done
+ done
+}
+
+undo()
+{
+ git reset --hard HEAD~
+}
+
+last_commit_message()
+{
+ git log --pretty=format:%s -1
+}
+
+# 1
+test_expect_success 'init subproj' '
+ test_create_repo subproj
+'
+
+# To the subproject!
+cd subproj
+
+# 2
+test_expect_success 'add sub1' '
+ create sub1 &&
+ git commit -m "sub1" &&
+ git branch sub1 &&
+ git branch -m master subproj
+'
+
+# 3
+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" &&
+ git branch sub3
+'
+
+# Back to mainline
+cd ..
+
+# 5
+test_expect_success 'add main4' '
+ create main4 &&
+ git commit -m "main4" &&
+ git branch -m master mainline &&
+ 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 &&
+ test_debug "echo -n expected: " &&
+ test_debug "cat expected" &&
+ test_debug "echo -n actual: " &&
+ test_debug "cat actual" &&
+ test_cmp expected actual &&
+ 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 &&
+ test_debug "echo -n expected: " &&
+ test_debug "cat expected" &&
+ test_debug "echo -n actual: " &&
+ test_debug "cat actual" &&
+ test_cmp expected actual
+# 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" &&
+ check_equal ''"$(last_commit_message)"'' "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"
+'
+
+# 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 &&
+ git branch splitbr2 sub1 &&
+ git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr2 &&
+ 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 &&
+ git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --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"
+'
+
+# 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"
+'
+
+# 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"
+'
+
+# To the subproject!
+cd ./subproj
+
+# 34
+test_expect_success 'merge split into subproj' '
+ git fetch .. spl3 &&
+ git branch spl3 FETCH_HEAD &&
+ git merge FETCH_HEAD &&
+ git branch subproj-merge-spl3
+'
+
+chkm="main4 main6"
+chkms="main-sub10 main-sub5 main-sub7 main-sub8"
+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"
+'
+
+# 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
+ # change, since they were *only* changed in the subtree branch.
+ allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' &&
+ check_equal "$allchanges" ''"$(echo $chkms $chkm $chks $chkms_sub | multiline | sort | fixnl)"''
+'
+
+# 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.*:")"'' ""
+'
+
+# prepare second pair of repositories
+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"
+'
+
+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"
+'
+
+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 &&
+ git subtree add --prefix subdir sub2
+'
+
+cd ../sub
+
+# 47
+test_expect_success 'add sub3' '
+ create sub3 &&
+ git commit -m "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
+'
+
+# at this point, the new commit parent should be sub3 if it is not,
+# something went wrong (the "newparent" of "master~" commit should
+# 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
+ git subtree split --prefix subdir2 --branch mainsub5 &&
+ check_equal ''"$(git log --pretty=format:%P -1 mainsub5)"'' ""
+'
+
+# make sure no patch changes more than one file. The original set of commits
+# changed only one file each. A multi-file change would imply that we pruned
+# commits too aggressively.
+joincommits()
+{
+ commit=
+ all=
+ while read x y; do
+ #echo "{$x}" >&2
+ if [ -z "$x" ]; then
+ continue
+ elif [ "$x" = "commit:" ]; then
+ if [ -n "$commit" ]; then
+ echo "$commit $all"
+ all=
+ fi
+ commit="$y"
+ else
+ all="$all $y"
+ fi
+ done
+ echo "$commit $all"
+}
+
+# 54
+test_expect_success 'verify one file change per commit' '
+ x= &&
+ list=''"$(git log --pretty=format:'"'commit: %H'"' | joincommits)"'' &&
+# test_debug "echo HERE" &&
+# test_debug "echo ''"$list"''" &&
+ (git log --pretty=format:'"'commit: %H'"' | joincommits |
+ ( while read commit a b; do
+ test_debug "echo Verifying commit "''"$commit"''
+ test_debug "echo a: "''"$a"''
+ test_debug "echo b: "''"$b"''
+ check_equal "$b" ""
+ x=1
+ done
+ check_equal "$x" 1
+ ))
+'
+
+test_done
diff --git a/contrib/subtree/todo b/contrib/subtree/todo
new file mode 100644
index 0000000000..7e44b0024f
--- /dev/null
+++ b/contrib/subtree/todo
@@ -0,0 +1,50 @@
+
+ delete tempdir
+
+ 'git subtree rejoin' option to do the same as --rejoin, eg. after a
+ rebase
+
+ --prefix doesn't force the subtree correctly in merge/pull:
+ "-s subtree" should be given an explicit subtree option?
+ There doesn't seem to be a way to do this. We'd have to
+ patch git-merge-subtree. Ugh.
+ (but we could avoid this problem by generating squashes with
+ exactly the right subtree structure, rather than using
+ subtree merge...)
+
+ add a 'push' subcommand to parallel 'pull'
+
+ add a 'log' subcommand to see what's new in a subtree?
+
+ add to-submodule and from-submodule commands
+
+ automated tests for --squash stuff
+
+ "add" command non-obviously requires a commitid; would be easier if
+ it had a "pull" sort of mode instead
+
+ "pull" and "merge" commands should fail if you've never merged
+ that --prefix before
+
+ docs should provide an example of "add"
+
+ note that the initial split doesn't *have* to have a commitid
+ specified... that's just an optimization
+
+ if you try to add (or maybe merge?) with an invalid commitid, you
+ get a misleading "prefix must end with /" message from
+ one of the other git tools that git-subtree calls. Should
+ detect this situation and print the *real* problem.
+
+ "pull --squash" should do fetch-synthesize-merge, but instead just
+ does "pull" directly, which doesn't work at all.
+
+ make a 'force-update' that does what 'add' does even if the subtree
+ already exists. That way we can help people who imported
+ subtrees "incorrectly" (eg. by just copying in the files) in
+ the past.
+
+ guess --prefix automatically if possible based on pwd
+
+ make a 'git subtree grafts' that automatically expands --squash'd
+ commits so you can see the full history if you want it.
diff --git a/contrib/svn-fe/svn-fe.txt b/contrib/svn-fe/svn-fe.txt
index 72ffea0b3a..1128ab2ce4 100644
--- a/contrib/svn-fe/svn-fe.txt
+++ b/contrib/svn-fe/svn-fe.txt
@@ -8,7 +8,10 @@ svn-fe - convert an SVN "dumpfile" to a fast-import stream
SYNOPSIS
--------
[verse]
-svnadmin dump --incremental REPO | svn-fe [url] | git fast-import
+mkfifo backchannel &&
+svnadmin dump --deltas REPO |
+ svn-fe [url] 3<backchannel |
+ git fast-import --cat-blob-fd=3 3>backchannel
DESCRIPTION
-----------
@@ -29,9 +32,6 @@ Subversion's repository dump format is documented in full in
Files in this format can be generated using the 'svnadmin dump' or
'svk admin dump' command.
-Dumps produced with 'svnadmin dump --deltas' (dumpfile format v3)
-are not supported.
-
OUTPUT FORMAT
-------------
The fast-import format is documented by the git-fast-import(1)
@@ -51,7 +51,7 @@ as committer, where 'user' is the value of the `svn:author` property
and 'UUID' the repository's identifier.
To support incremental imports, 'svn-fe' puts a `git-svn-id` line at
-the end of each commit log message if passed an url on the command
+the end of each commit log message if passed a URL on the command
line. This line has the form `git-svn-id: URL@REVNO UUID`.
The resulting repository will generally require further processing