git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [RFC 0/2] svn-fetch|push - an alternate approach
@ 2012-08-18 17:39 James R. McKaskill
  2012-08-18 17:39 ` [RFC 1/2] add svn-fetch/push James R. McKaskill
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: James R. McKaskill @ 2012-08-18 17:39 UTC (permalink / raw)
  To: git

This is sort of an RFC/ANN. There has been a whole bunch of traffic of
late on git-svn, vcs-svn, the remote helper, etc. I would like to
present an alternate solution that I've been playing with for a couple
of weeks.

I work in a team that uses a mixture of git-svn and svn proper. Whilst
it works remarkably well and I largely don't have to deal with svn on a
day to day basis, git-svn is still quite limiting compared to git
proper. Namely any merges created by myself or another either can't be
pushed into svn or information is lost when done so. Switching wholesale
to git is problematic due to the disruption this causes. There are also
numerous non-core users who want a really simple update and occasionally
checkin a single file/folder UI. Tortoise SVN fits their needs perfectly
and even with a proper git-svn bridge in place I have no plan on
dropping svn support.

Possible Solutions
------------------

In general there are a couple of ways of attacking this:

1. Have git-svn push extraneous git context into subversion and pull it
   back down on the other side.
2. Create a dummy svn server ala git-cvsserver
3. Use both a git server and svn server and have the git server push to
   the svn server.
4. Same as #3 but have the svn server push to the git server.

#1 is frought with issues due to how exacting git is wrt to recreating
commits. The recreated commits would have to have the same hash to be
able to use git's distributed nature.

#2 is possible but a lot of work. SVN clients expect a substantial
amount of metadata. This introduces considerable complexity of where to
store the metadata and what metadata to generate on the fly. Not all can
be generated on the fly as SVN presents a general purpose key value
store to clients, which many use and expect to work.

#3 and #4 have divergence issues in that you have two servers through
which commits can be pushed. If conflicting commits are pushed into both
servers at the same time, there has to be some method for conflict
resolution. Requiring that an admin go in and fix things manually is
unpalatable. The easiest way around this is to delegate one of the two
servers as the master and require that commits pushed to the other
update the master before suceeding. In this mode #3 has the SVN server
as master, and #4 has the git server.

There is an existing commercial system that came up a couple of weeks
ago that transfers commits asynchronously. I am unsure of how this
avoids conflicts if at all.

I've been working on a solution using approach #3. The code follows
(hopefully) and is currently running an internal beta with 3 or 4 users
using it and works very well.

In my setup, the SVN server is the master node. The git server has a
pre-receive hook which pushes commits onto the SVN server. The hook
checks for any SVN errors (SVN hooks, file conflicts, etc), and that no
SVN commits are intermingled with the pushed git commits (either before
or interspersed). If this fails at all, the pre-receive hook fails,
which fails the git push. The git user then pulls or rebases and tries
to push again.

Kerberos Auth (Off-Topic)
-------------------------

I am also using a kerberos auth http frontend
(http://github.com/jmckaskill/krb-httpd). This checks authentication
against active directory and uses the remote user for the --user
argument of git svn-push. Most clients are windows users. In this case I
have built a replacement version of libcurl-4.dll which enables kerberos
negotiate (as an aside the msysgit build should enable this by default).
I've then added the following to users' global git config:

credential.http://domain.name.username=dummy
credential.http://domain.name.helper=!echo password=dummy

With this in place, git uses the user's domain login and never asks for
a password. There is also some equivalent tweaks to get domain logins
working in browsers for cgit.

How it Works
------------

The attached patch adds two commands to git: svn-fetch and svn-push. The
names are temporary. My plan is to refactor these into git-remote-svn so
that git push/pull/fetch work as expected. svn-fetch fetches svn commits
for all branches and creates git commits, branches, tags, etc as it goes
along. svn-push takes the remote ref name, from and to sha1 (or a list
of these on stdin for use as a pre-receive hook) and pushes the changes
to svn, creating/deleting branches/tags as neccessary and failing if
there are any intermingled svn commits in the pushed to folders.

Metadata is tracked by creating an extra git commit for each svn commit
in each branch/tag that is stored in refs/svn/heads|tags as well as
refs/svn/latest for the latest fetched commit. These commits look
like the following:

tree <svn tree>
parent <underlying git commit>
parent <previous svn commit for this svn folder>
author <svn author + time>
committer <svn author + time>
revision <svn revision>
path <svn path relative to svn.url>

Using commits to track the SVN metadata has proven really really handy.
The code gets to use all the built-in locking primitives, packing, etc.
git push --mirror nicely mirrors all of the metadata as well as the
git commits. As an anecdote I had a bug the other day where my
check_for_svn_commits wasn't working and so had missed a svn commit.
After fixing the bug, I needed to rerun the fetch to grab the missing
commit. This was a simple matter of: ran git log --pretty=raw
refs/svn/heads/trunk to get the svn/git commit sha1s, updated the refs,
and reran git svn-fetch.

As a twist the code does not use the svn library, but rather talks the
svn protocol directly. I actually found it much easier to go this route
then trying to bend everything to how the svn library understands
things. It also has the advantage of not depending on libsvn. A number
of distributions currently distribute the svn specific parts of git
seperately to avoid this dependency.

Trees
-----

For each commit I track the svn and git trees seperately. The svn tree
tracks exactly what SVN returns byte for byte. This is required so that
fetched SVN diffs can be processed correctly. I can then have slight
tweaks between the two trees. Currently I have three differences between
the two trees.

If svn.eol is set then eol conversion is done between the two trees
(controlled by info/attributes). This way all of my imported git trees
have unix line endings, whilst the svn trees have a mixture. In my case
they should be windows line endings, but some third party libraries we
imported as unix line endings. Files updated in git are renormalized to
the eol given as svn.eol.

The second tweak is that changes to .git* pushed from the git side are
not pushed through to svn. This way git users can manage .gitattributes
and .gitignore without pushing these through to svn.

The third change is that svn-fetch creates a .gitempty file in any
empty directory on the svn side. This forces git to create the directory
and gives the git user a clean way of removing the directory.

Branches
--------

Branch names in SVN are a bit more leniant then ref names in git. For
example SVN allows spaces whilst git does not. Thus svn-fetch converts
all disallowed characters to '_'. The svn commit stores the original
branch name so it can be pushed back to. svn-fetch does not currently
handle conflicts where two different SVN branches collide with a single
git name.

When updating a branch svn-push tries to find a path from the previous
commit to the target. When another branch has been merged into the svn
branch, then the pushed commits look very similar to the svn equivalent.
Namely just the merge commit with a large diff. In the case that pushed
commits branch and merge back together, then svn-push simply tries to
find any path that gets it from the previous commit to the target. For
new branches it creates an svn copy from the newest svn commit that is a
ancestor of the target. In the case a forced push it will do an svn
replace with the newest ancestor in svn. The code for this is in
find_copy_source and if (!has_parent) check in do_push.

Tags
----

Tags are pushed to SVN as SVN tags (ie folder copies). Both annonated
and simple tags can be used. For annontated tags, the messages is used
for the commit message. For simple tags a hard-coded commit message of
Create <tag folder relative to svn.url> is used. svn-fetch creates
annotated tags for tags created/updated in SVN. If a tag is updated in
SVN, svn-fetch will create a git commit for the change, a new annotated
tag and overwrite the existing tag. Standard SVN practise is not to
commit to tag folders, but it does occasionally happen. This is thus
treated in the same way as the need in git to occasionally overwrite a
tag.

SVN Auth
--------

The current authentication is temporary. Currently I'm using a git-svn
style authors file hard-coded to <git-dir>/svn-authors. svn-push then
requires the user be of the form user:pass. As you push commits it will
then switch to that user by killing the connection and reopening. There
are also a couple of operations which require an svn user but don't have
a git commit to look the email up from. For these both svn-fetch and
svn-push require a --user argument. These operations are: all of
svn-fetch and get-latest-rev, log, and deleting branch/tags for
svn-push.

Pipelining
----------

svn-fetch has a -c option which lets you increase the number of
connections used to download commits. The svn protocol is annoying in
that it doesn't let you pipeline requests. svnserve will let you send
some content out of order, but once you overflow its rx buffer it will
kill the connection. So instead svn-fetch opens n+1 connections, sends
the requests ahead of time and then cycle back round to process the
reply. When initially importing one of my work projects setting this to
15 increased the fetch speed by a factor of 10.

Tests
-----

I've also added some tests for svn-fetch, svn-push. These currently
require svn 1.7 or newer. svn 1.6 doesn't understand branch
replacement and I haven't gotten around to disabling those tests.

Config
------

svn-fetch and svn-push use a number of config items:

- svn.trunk - path under svn.url of the trunk branch

- svn.branches - path under svn.url of the branches folder, branches are
    then folders inside this folder

- svn.tags - path under svn.url of the tags folders, tags are then
    folders inside this folder

- svn.user - default svn user for fetch, push without a git commit

- svn.remote - remote name to use for fetch tags/branches

- svn.trunkref - name to use for the git trunk ref (defaults to master)

- svn.eol - eol to convert to for files pushed to svn

Most of these are temporary.

TODO
----

There is a whole bunch more work to be done. My big ticket items are:

- svn over HTTP support. I've had an initial look into this and looks
    fairly straight forward. svn over HTTP is largely the same svn
commands converted to XML. Is there any recomendation on what XML
library to use or should I write my own limited version?

- fixing up auth to use credentials

- refactor svn-fetch svn-push into git-remote-svn

- adding a cfg item for the authors file and using svn-user@repo UUID if
    none is provided like git-svn

- documentation - for the moment the documentation is this email

- svn:externals - none of the repos have to deal with have this and I'm
    not sure yet how to deal with it

- style cleanup

Code
----

The code is also available at https://github.com/jmckaskill/git, which I
will keep uptodate as I flush out the todo list.

^ permalink raw reply	[flat|nested] 6+ messages in thread

* [RFC 1/2] add svn-fetch/push
  2012-08-18 17:39 [RFC 0/2] svn-fetch|push - an alternate approach James R. McKaskill
@ 2012-08-18 17:39 ` James R. McKaskill
  2012-08-18 17:39 ` [RFC 2/2] add tests for git svn-fetch|push James R. McKaskill
  2012-08-19 10:21 ` [RFC 0/2] svn-fetch|push - an alternate approach Marco Schulze
  2 siblings, 0 replies; 6+ messages in thread
From: James R. McKaskill @ 2012-08-18 17:39 UTC (permalink / raw)
  To: git; +Cc: James R. McKaskill

From: "James R. McKaskill" <james@foobar.co.nz>


Signed-off-by: James R. McKaskill <james@foobar.co.nz>
---
 .gitignore          |    3 +
 Makefile            |    2 +
 builtin.h           |    2 +
 builtin/svn-fetch.c | 3257 +++++++++++++++++++++++++++++++++++++++++++++++++++
 command-list.txt    |    2 +
 git.c               |    2 +
 svn-sync.sh         |  150 +++
 7 files changed, 3418 insertions(+)
 create mode 100644 builtin/svn-fetch.c
 create mode 100755 svn-sync.sh

diff --git a/.gitignore b/.gitignore
index bb5c91e..8e9cd4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -153,6 +153,8 @@
 /git-stripspace
 /git-submodule
 /git-svn
+/git-svn-fetch
+/git-svn-push
 /git-symbolic-ref
 /git-tag
 /git-tar-tree
@@ -233,3 +235,4 @@
 *.pdb
 /Debug/
 /Release/
+*.swp
diff --git a/Makefile b/Makefile
index 6b0c961..e28e224 100644
--- a/Makefile
+++ b/Makefile
@@ -523,6 +523,7 @@ BUILT_INS += git-show$X
 BUILT_INS += git-stage$X
 BUILT_INS += git-status$X
 BUILT_INS += git-whatchanged$X
+BUILT_INS += git-svn-push$X
 
 # what 'all' will build and 'install' will install in gitexecdir,
 # excluding programs for built-in commands
@@ -894,6 +895,7 @@ BUILTIN_OBJS += builtin/shortlog.o
 BUILTIN_OBJS += builtin/show-branch.o
 BUILTIN_OBJS += builtin/show-ref.o
 BUILTIN_OBJS += builtin/stripspace.o
+BUILTIN_OBJS += builtin/svn-fetch.o
 BUILTIN_OBJS += builtin/symbolic-ref.o
 BUILTIN_OBJS += builtin/tag.o
 BUILTIN_OBJS += builtin/tar-tree.o
diff --git a/builtin.h b/builtin.h
index ba6626b..71fe517 100644
--- a/builtin.h
+++ b/builtin.h
@@ -131,6 +131,8 @@ extern int cmd_show(int argc, const char **argv, const char *prefix);
 extern int cmd_show_branch(int argc, const char **argv, const char *prefix);
 extern int cmd_status(int argc, const char **argv, const char *prefix);
 extern int cmd_stripspace(int argc, const char **argv, const char *prefix);
+extern int cmd_svn_fetch(int argc, const char **argv, const char* prefix);
+extern int cmd_svn_push(int argc, const char **argv, const char* prefix);
 extern int cmd_symbolic_ref(int argc, const char **argv, const char *prefix);
 extern int cmd_tag(int argc, const char **argv, const char *prefix);
 extern int cmd_tar_tree(int argc, const char **argv, const char *prefix);
diff --git a/builtin/svn-fetch.c b/builtin/svn-fetch.c
new file mode 100644
index 0000000..05ef85f
--- /dev/null
+++ b/builtin/svn-fetch.c
@@ -0,0 +1,3257 @@
+#include "git-compat-util.h"
+#include "parse-options.h"
+#include "gettext.h"
+#include "cache.h"
+#include "cache-tree.h"
+#include "refs.h"
+#include "unpack-trees.h"
+#include "commit.h"
+#include "tag.h"
+#include "diff.h"
+#include "revision.h"
+#include "diffcore.h"
+#include "run-command.h"
+
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+#include <openssl/md5.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdarg.h>
+
+static const char *svnuser;
+static const char *trunk;
+static const char *branches;
+static const char *tags;
+static const char *remotedir;
+static const char *remoteheads;
+static const char *remotetags;
+static const char *trunkref = "master";
+static int last_revision = INT_MAX;
+static int verbose;
+static int push_from_stdin;
+static int svnfdc = 1;
+static int leave_remote;
+static int svnfd;
+static int inner;
+static int pause_between_commits;
+static const char* url;
+static enum eol svn_eol = EOL_UNSET;
+
+#define FETCH_AT_ONCE 1000
+
+static const char* const builtin_svn_fetch_usage[] = {
+	"git svn-fetch [options]",
+	NULL,
+};
+
+static struct option builtin_svn_fetch_options[] = {
+	OPT_STRING(0, "user", &svnuser, "user", "svn username"),
+	OPT_BOOLEAN('v', "verbose", &verbose, "verbose logging of all svn traffic"),
+	OPT_INTEGER('r', "revision", &last_revision, "revisions to fetch up to"),
+	OPT_INTEGER('c', "connections", &svnfdc, "number of concurrent connections"),
+	OPT_BOOLEAN(0, "inner", &inner, "internal"),
+	OPT_END()
+};
+
+static const char* const builtin_svn_push_usage[] = {
+	"git svn-push [options] <ref> <from commit> <to commit>",
+	"git svn-push [options] --stdin",
+	NULL,
+};
+
+static struct option builtin_svn_push_options[] = {
+	OPT_STRING(0, "user", &svnuser, "user", "default svn username"),
+	OPT_BOOLEAN('v', "verbose", &verbose, "verbose logging of all svn traffic"),
+	OPT_BOOLEAN(0, "stdin", &push_from_stdin, "read refs to update from stdin"),
+	OPT_END()
+};
+
+static int config(const char *var, const char *value, void *dummy) {
+	if (!strcmp(var, "svn.trunk")) {
+		return git_config_string(&trunk, var, value);
+	}
+	if (!strcmp(var, "svn.branches")) {
+		return git_config_string(&branches, var, value);
+	}
+	if (!strcmp(var, "svn.tags")) {
+		return git_config_string(&tags, var, value);
+	}
+	if (!strcmp(var, "svn.user")) {
+		return git_config_string(&svnuser, var, value);
+	}
+	if (!strcmp(var, "svn.url")) {
+		return git_config_string(&url, var, value);
+	}
+	if (!strcmp(var, "svn.remote")) {
+		return git_config_string(&remotedir, var, value);
+	}
+	if (!strcmp(var, "svn.trunkref")) {
+		return git_config_string(&trunkref, var, value);
+	}
+	if (!strcmp(var, "svn.eol")) {
+		if (value && !strcasecmp(value, "lf"))
+			svn_eol = EOL_LF;
+		else if (value && !strcasecmp(value, "crlf"))
+			svn_eol = EOL_CRLF;
+		else if (value && !strcasecmp(value, "native"))
+			svn_eol = EOL_NATIVE;
+		else
+			svn_eol = EOL_UNSET;
+		return 0;
+	}
+	return git_default_config(var, value, dummy);
+}
+
+#ifndef min
+#define min(a,b) ((a) < (b) ? (a) : (b))
+#endif
+
+#ifndef max
+#define max(a,b) ((a) < (b) ? (b) : (a))
+#endif
+
+struct inbuffer {
+	char buf[4096];
+	int b, e;
+};
+static struct inbuffer* inbuf;
+
+static int readc() {
+	if (inbuf->b == inbuf->e) {
+		inbuf->b = 0;
+		inbuf->e = xread(svnfd, inbuf->buf, sizeof(inbuf->buf));
+		if (inbuf->e <= 0) return EOF;
+	}
+
+	return inbuf->buf[inbuf->b++];
+}
+
+static void unreadc() {
+	inbuf->b--;
+}
+
+static ssize_t read_svn(void* p, size_t n) {
+	/* big reads we may as well read directly into the target */
+	if (inbuf->e == inbuf->b && n >= sizeof(inbuf->buf) / 2) {
+		return xread(svnfd, p, n);
+
+	} else if (inbuf->e == inbuf->b) {
+		inbuf->b = 0;
+		inbuf->e = xread(svnfd, inbuf->buf, sizeof(inbuf->buf));
+		if (inbuf->e <= 0) return inbuf->e;
+	}
+
+	n = min(n, inbuf->e - inbuf->b);
+	memcpy(p, inbuf->buf + inbuf->b, n);
+	inbuf->b += n;
+	return n;
+}
+
+static const char hex[] = "0123456789abcdef";
+#define MAX_PRINT_LEN 64
+
+static int print_ascii(const void* p, int n, int maxoutput) {
+	int i;
+	int printed = 0;
+	const unsigned char* v = p;
+
+	for (i = 0; i < n && printed < maxoutput; i++) {
+		int ch = v[i];
+
+		if (' ' <= ch && ch < 0x7F && ch != '\\') {
+			putc(v[i], stderr);
+			printed++;
+
+		} else if (ch == '\n') {
+			putc('\\', stderr);
+			putc('n', stderr);
+			printed += 2;
+
+		} else if (ch == '\r') {
+			putc('\\', stderr);
+			putc('r', stderr);
+			printed += 2;
+
+		} else if (ch == '\t') {
+			putc('\\', stderr);
+			putc('t', stderr);
+			printed += 2;
+
+		} else if (ch == '\\') {
+			putc('\\', stderr);
+			putc('\\', stderr);
+			printed += 2;
+
+		} else {
+			putc('\\', stderr);
+			putc('x', stderr);
+			putc(hex[ch >> 4], stderr);
+			putc(hex[ch & 0x0F], stderr);
+			printed += 4;
+		}
+	}
+
+	if (printed >= maxoutput) {
+		fprintf(stderr, "...");
+	}
+
+	return printed;
+}
+
+static int get_md5_hex(const char *hex, unsigned char *sha1)
+{
+	int i;
+	for (i = 0; i < 16; i++) {
+		unsigned int val;
+		/*
+		 * hex[1]=='\0' is caught when val is checked below,
+		 * but if hex[0] is NUL we have to avoid reading
+		 * past the end of the string:
+		 */
+		if (!hex[0])
+			return -1;
+		val = (hexval(hex[0]) << 4) | hexval(hex[1]);
+		if (val & ~0xff)
+			return -1;
+		*sha1++ = val;
+		hex += 2;
+	}
+	return 0;
+}
+
+static const char* md5_to_hex(const unsigned char* md5) {
+	static int bufno;
+	static char hexbuffer[4][50];
+	char *buffer = hexbuffer[3 & ++bufno], *buf = buffer;
+	int i;
+
+	for (i = 0; i < 16; i++) {
+		unsigned int val = *md5++;
+		*buf++ = hex[val >> 4];
+		*buf++ = hex[val & 0xf];
+	}
+	*buf = '\0';
+
+	return buffer;
+}
+
+__attribute__((format (printf,1,2)))
+static void sendf(const char* fmt, ...);
+
+static int verbosetxnl = 1;
+
+static void sendf(const char* fmt, ...) {
+	static struct strbuf out = STRBUF_INIT;
+	va_list ap;
+	va_start(ap, fmt);
+	strbuf_reset(&out);
+	strbuf_vaddf(&out, fmt, ap);
+
+	if (verbose) {
+		if (verbosetxnl) {
+			fputc('+', stderr);
+		}
+		verbosetxnl = out.len && out.buf[out.len-1] == '\n';
+		print_ascii(out.buf, out.len - verbosetxnl, INT_MAX);
+		if (verbosetxnl) {
+			fputc('\n', stderr);
+		}
+	}
+
+	if (write_in_full(svnfd, out.buf, out.len) != out.len) {
+		die_errno("write");
+	}
+}
+
+/* returns -1 if it can't find a number */
+static ssize_t read_number() {
+	ssize_t v;
+
+	for (;;) {
+		int ch = readc();
+		if ('0' <= ch && ch <= '9') {
+			v = ch - '0';
+			break;
+		} else if (ch != ' ' && ch != '\n') {
+			unreadc();
+			return -1;
+		}
+	}
+
+	for (;;) {
+		int ch = readc();
+		if (ch < '0' || ch > '9') {
+			unreadc();
+			if (verbose) fprintf(stderr, " %d", (int) v);
+			return v;
+		}
+
+		if (v > INT64_MAX/10) {
+			die(_("number too big"));
+		} else {
+			v = 10*v + (ch - '0');
+		}
+	}
+}
+
+/* returns -1 if it can't find a list */
+static int read_list() {
+	for (;;) {
+		int ch = readc();
+		if (ch == '(') {
+			if (verbose) fprintf(stderr, " (");
+			return 0;
+		} else if (ch != ' ' && ch != '\n') {
+			unreadc();
+			return -1;
+		}
+	}
+}
+
+/* returns 0 if the list is missing or empty (and skips over it), 1 if
+ * its present and has values */
+static int have_optional() {
+	if (read_list()) return 0;
+	for (;;) {
+		int ch = readc();
+		if (ch == ')') {
+			if (verbose) fprintf(stderr, " )");
+			return 0;
+		} else if (ch != ' ' && ch != '\n') {
+			unreadc();
+			return 1;
+		}
+	}
+}
+
+/* returns NULL if it can't find an atom, string only valid until next
+ * call to read_word, not thread-safe */
+static const char *read_word() {
+	static char buf[256];
+	int bufsz = 0;
+	int ch;
+
+	for (;;) {
+		ch = readc();
+		if (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')) {
+			break;
+		} else if (ch != ' ' && ch != '\n') {
+			unreadc();
+			return NULL;
+		}
+	}
+
+	while (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')
+			|| ('0' <= ch && ch <= '9')
+			|| ch == '-') {
+		if (bufsz >= sizeof(buf))
+			die(_("atom too long"));
+
+		buf[bufsz++] = ch;
+		ch = readc();
+	}
+
+	unreadc();
+	buf[bufsz] = '\0';
+	if (verbose) fprintf(stderr, " %s", buf);
+	return bufsz ? buf : NULL;
+}
+
+/* returns -1 if no string or an invalid string */
+static int read_string(struct strbuf* s) {
+	size_t i;
+	ssize_t n = read_number();
+	if (n < 0 || unsigned_add_overflows(s->len, (size_t) n))
+		return -1;
+	if (readc() != ':')
+		die(_("malformed string"));
+	if (verbose)
+		fprintf(stderr, ":");
+
+	strbuf_grow(s, s->len + n);
+
+	i = 0;
+	while (i < n) {
+		ssize_t r = read_svn(s->buf + s->len, n-i);
+		if (r < 0)
+			die_errno("read error");
+		if (r == 0)
+			die("short read");
+		strbuf_setlen(s, s->len + r);
+		i += r;
+	}
+
+	if (verbose) print_ascii(s->buf + s->len - n, n, MAX_PRINT_LEN);
+
+	return 0;
+}
+
+static int skip_string() {
+	struct strbuf buf = STRBUF_INIT;
+	int r = read_string(&buf);
+	strbuf_release(&buf);
+	return r;
+}
+
+static void read_end() {
+	int parens = 1;
+	while (parens > 0) {
+		int ch = readc();
+		if (ch == EOF)
+			die(_("socket close whilst looking for list close"));
+
+		if (ch == '(') {
+			if (verbose) fprintf(stderr, " (");
+			parens++;
+		} else if (ch == ')') {
+			if (verbose) fprintf(stderr, " )");
+			parens--;
+		} else if (ch == ' ' || ch == '\n') {
+			/* whitespace */
+		} else if ('0' <= ch && ch <= '9') {
+			/* number or string */
+			size_t n;
+			char buf[4096];
+			int toprint = verbose ? MAX_PRINT_LEN : 0;
+
+			unreadc();
+			n = read_number();
+
+			ch = readc();
+			if (ch != ':') {
+				/* number */
+				unreadc();
+				continue;
+			}
+
+			/* string */
+			if (verbose) fputc(':', stderr);
+			while (n) {
+				ssize_t r = read_svn(buf, min(n, sizeof(buf)));
+				if (r <= 0) die_errno("read");
+				if (toprint > 0) {
+					toprint -= print_ascii(buf, r, toprint);
+				}
+				n -= r;
+			}
+		} else {
+			unreadc();
+			if (!read_word())
+				die(_("unexpected character %c"), ch);
+		}
+	}
+}
+
+static const char* read_command() {
+	const char *cmd;
+
+	if (read_list()) goto err;
+
+	cmd = read_word();
+	if (!cmd) goto err;
+	if (read_list()) goto err;
+
+	return cmd;
+err:
+	die(_("malformed response"));
+}
+
+static void read_command_end() {
+	read_end();
+	read_end();
+	if (verbose) fprintf(stderr, "\n");
+}
+
+static void read_done() {
+	const char* s = read_word();
+	if (!s || strcmp(s, "done"))
+		die("unexpected failure");
+	if (verbose) fputc('\n', stderr);
+}
+
+static void read_success() {
+	const char* s = read_command();
+	if (strcmp(s, "success")) {
+		verbose = 1;
+		read_end();
+		die("unexpected failure");
+	}
+	read_command_end();
+}
+
+static void cram_md5(const char* user, const char* pass) {
+	const char *s;
+	unsigned char hash[16];
+	struct strbuf chlg = STRBUF_INIT;
+	HMAC_CTX hmac;
+
+	s = read_command();
+	if (strcmp(s, "step")) goto error;
+	if (read_string(&chlg)) goto error;
+
+	read_command_end();
+
+	HMAC_Init(&hmac, (unsigned char*) pass, strlen(pass), EVP_md5());
+	HMAC_Update(&hmac, (unsigned char*) chlg.buf, chlg.len);
+	HMAC_Final(&hmac, hash, NULL);
+	HMAC_CTX_cleanup(&hmac);
+
+	sendf("%d:%s %s\n", (int) (strlen(user) + 1 + 32), user, md5_to_hex(hash));
+
+	strbuf_release(&chlg);
+	return;
+
+error:
+	die(_("auth failed"));
+}
+
+static void read_name(struct strbuf* name) {
+	strbuf_reset(name);
+	if (read_string(name)) goto err;
+	if (name->buf[0] == '/') strbuf_remove(name, 0, 1);
+	if (memchr(name->buf, '\0', name->len)) goto err;
+	if (strstr(name->buf, "//")) goto err;
+	if (!strcmp(name->buf, "..")) goto err;
+	if (!strcmp(name->buf, ".")) goto err;
+	if (!prefixcmp(name->buf, "../")) goto err;
+	if (!prefixcmp(name->buf, "./")) goto err;
+	if (strstr(name->buf, "/../")) goto err;
+	if (strstr(name->buf, "/./")) goto err;
+	if (!suffixcmp(name->buf, "/..")) goto err;
+	if (!suffixcmp(name->buf, "/.")) goto err;
+
+	return;
+err:
+	die("invalid path name %s", name->buf);
+}
+
+static const char* cmt_to_hex(struct commit* c) {
+	return sha1_to_hex(c ? c->object.sha1 : null_sha1);
+}
+
+static const unsigned char* cmt_sha1(struct commit* c) {
+	return c ? c->object.sha1 : null_sha1;
+}
+
+static int parse_svnrev(struct commit* c) {
+	char* p = strstr(c->buffer, "\nrevision ");
+	if (!p) die("invalid svn commit %s", cmt_to_hex(c));
+	p += strlen("\nrevision ");
+	return atoi(p);
+}
+
+static void parse_svnpath(struct commit* c, struct strbuf* buf) {
+	char* e;
+	char* p = strstr(c->buffer, "\npath ");
+	if (!p) die("invalid svn commit %s", cmt_to_hex(c));
+	p += strlen("\npath ");
+	e = strchr(p, '\n');
+	if (!e) die("invalid svn commit %s", cmt_to_hex(c));
+	strbuf_add(buf, p, e-p);
+}
+
+static struct commit* svn_commit(struct commit* c) {
+	if (parse_commit(c) || !c->parents || !c->parents->item)
+		die("invalid svn commit %s", cmt_to_hex(c));
+	/* In the case of no git commit, but we have a previous svn
+	 * commit, the svn parent is repeated twice. That way we can
+	 * distinguish that case from a git commit but no svn commit */
+	if (c->parents->next && c->parents->item == c->parents->next->item) {
+		return NULL;
+	}
+	return c->parents->item;
+}
+
+static struct commit* svn_parent(struct commit* c) {
+	if (parse_commit(c) || !c->parents)
+		die("invalid svn commit %s", cmt_to_hex(c));
+	return c->parents->next ? c->parents->next->item : NULL;
+}
+
+struct svnref {
+	struct strbuf svn; /* svn root */
+	struct strbuf ref; /* svn ref path */
+	struct strbuf remote; /* remote ref path */
+
+	struct index_state git_index, svn_index;
+	struct tree *git_tree, *svn_tree;
+
+	unsigned int delete : 1;
+	unsigned int istag : 1;
+
+	struct commit* svncmt; /* current value of svn ref */
+	struct object* gitobj; /* current value of remote ref, may be tag or commit */
+	struct commit* parent; /* parent git commit */
+};
+
+static struct svnref** refs;
+static size_t refn, refalloc;
+
+static int is_in_dir(char* file, const char* dir, char** rel) {
+	size_t sz = strlen(dir);
+	if (strncmp(file, dir, sz)) return 0;
+	if (file[sz] && file[sz] != '/') return 0;
+	if (rel) *rel = file[sz] ? &file[sz+1] : &file[sz];
+	return 1;
+}
+
+#define TRUNK_REF 0
+#define BRANCH_REF 1
+#define TAG_REF 2
+
+static void add_refname(struct strbuf* buf, const char* name) {
+	while (*name) {
+		int ch = *(name++);
+		if (ch <= ' '
+			|| ch == 0x7F
+			|| ch == '~'
+			|| ch == '^'
+			|| ch == ':'
+			|| ch == '\\'
+			|| ch == '*'
+			|| ch == '?'
+			|| ch == '[') {
+			strbuf_addch(buf, '_');
+		} else {
+			strbuf_addch(buf, ch);
+		}
+	}
+}
+
+static void checkout_tree(struct index_state* idx, struct tree* t) {
+	struct tree_desc desc;
+	struct unpack_trees_options op;
+
+	if (!t) return;
+
+	if (parse_tree(t))
+		die("failed to checkout %s", sha1_to_hex(t->object.sha1));
+
+	init_tree_desc(&desc, t->buffer, t->size);
+
+	memset(&op, 0, sizeof(op));
+	op.src_index = idx;
+	op.dst_index = idx;
+	op.index_only = 1;
+
+	if (unpack_trees(1, &desc, &op))
+		die("failed to checkout %s", sha1_to_hex(t->object.sha1));
+}
+
+static struct index_state* git_index(struct svnref* r) {
+	checkout_tree(&r->git_index, r->git_tree);
+	r->git_tree = NULL;
+	return &r->git_index;
+}
+
+static struct index_state* svn_index(struct svnref* r) {
+	checkout_tree(&r->svn_index, r->svn_tree);
+	r->svn_tree = NULL;
+	return &r->svn_index;
+}
+
+static const unsigned char *idx_sha1(struct index_state* idx) {
+	if (!idx->cache_tree)
+		idx->cache_tree = cache_tree();
+	if (cache_tree_update(idx->cache_tree, idx->cache, idx->cache_nr, 0))
+		die("failed to update cache tree");
+
+	return idx->cache_tree->sha1;
+}
+
+static void checkout_svncmt(struct svnref* r, struct commit* svncmt) {
+	struct commit* gitcmt;
+
+	/* R (replace) log entries may already have content that
+	 * we need to clear first */
+
+	discard_index(&r->git_index);
+	discard_index(&r->svn_index);
+	r->git_tree = NULL;
+	r->svn_tree = NULL;
+
+	/* Note r->svncmt may not equal c if this creates a branch or
+	 * replaces an existing one. c will be the new source whereas
+	 * svncmt will continue to point to the old svn commit */
+
+	if (svncmt && parse_commit(svncmt))
+		die("invalid object %s", cmt_to_hex(svncmt));
+
+	r->svn_tree = svncmt ? svncmt->tree : NULL;
+	gitcmt = svncmt ? svn_commit(svncmt) : NULL;
+
+	if (gitcmt && parse_commit(gitcmt))
+		die("invalid object %s", cmt_to_hex(gitcmt));
+
+	r->git_tree = gitcmt ? gitcmt->tree : NULL;
+	r->parent = gitcmt;
+}
+
+static struct svnref* create_ref(int type, const char* name) {
+	struct svnref* r = NULL;
+	unsigned char sha1[20];
+
+	switch (type) {
+	case TRUNK_REF:
+		r = xcalloc(1, sizeof(*r));
+		strbuf_addstr(&r->svn, trunk ? trunk : "");
+		strbuf_addstr(&r->ref, "refs/svn/heads/trunk");
+		strbuf_addstr(&r->remote, remoteheads);
+		strbuf_addstr(&r->remote, trunkref);
+		break;
+
+	case BRANCH_REF:
+		r = xcalloc(1, sizeof(*r));
+
+		strbuf_addstr(&r->svn, branches);
+		strbuf_addch(&r->svn, '/');
+		strbuf_addstr(&r->svn, name);
+
+		strbuf_addstr(&r->ref, "refs/svn/heads/");
+		add_refname(&r->ref, name);
+
+		strbuf_addstr(&r->remote, remoteheads);
+		add_refname(&r->remote, name);
+		break;
+
+	case TAG_REF:
+		r = xcalloc(1, sizeof(*r));
+		r->istag = 1;
+
+		strbuf_addstr(&r->svn, tags);
+		strbuf_addch(&r->svn, '/');
+		strbuf_addstr(&r->svn, name);
+
+		strbuf_addstr(&r->ref, "refs/svn/tags/");
+		add_refname(&r->ref, name);
+
+		strbuf_addstr(&r->remote, remotetags);
+		add_refname(&r->remote, name);
+		break;
+	}
+
+	if (!read_ref(r->remote.buf, sha1) && !is_null_sha1(sha1)) {
+		r->gitobj = parse_object(sha1);
+		if (!r->gitobj)
+			die("invalid ref %s", name);
+	}
+
+	if (!read_ref(r->ref.buf, sha1) && !is_null_sha1(sha1)) {
+		r->svncmt = lookup_commit(sha1);
+		if (parse_commit(r->svncmt))
+			die("invalid ref %s", name);
+		checkout_svncmt(r, r->svncmt);
+	}
+
+	ALLOC_GROW(refs, refn + 1, refalloc);
+	refs[refn++] = r;
+	return r;
+}
+
+static struct svnref* find_svnref_by_path(struct strbuf* name) {
+	int i;
+	struct svnref* r;
+	char *a, *b, *c, *d;
+
+	if (!trunk && !branches && !tags && refn) {
+		return refs[0];
+	}
+
+	for (i = 0; i < refn; i++) {
+		r = refs[i];
+		if (prefixcmp(name->buf, r->svn.buf)) {
+			continue;
+		}
+
+		switch (name->buf[r->svn.len]) {
+		case '\0':
+			strbuf_setlen(name, 0);
+			return r;
+		case '/':
+			strbuf_remove(name, 0, r->svn.len + 1);
+			return r;
+		}
+	}
+
+	/* names are of the form
+	 * branches/foo/...
+	 * a        b  c   d
+	 */
+	a = name->buf;
+	d = name->buf + name->len;
+
+	if (!trunk && !branches && !tags) {
+		return create_ref(TRUNK_REF, NULL);
+
+	} else if (trunk && is_in_dir(a, trunk, &b)) {
+		strbuf_remove(name, 0, b - a);
+		return create_ref(TRUNK_REF, NULL);
+
+
+	} else if (branches && is_in_dir(a, branches, &b) && *b) {
+		c = memchr(b, '/', d - b);
+		if (c) {
+			*c = '\0';
+			r = create_ref(BRANCH_REF, b);
+			strbuf_remove(name, 0, c+1 - a);
+		} else {
+			r = create_ref(BRANCH_REF, b);
+			strbuf_reset(name);
+		}
+		return r;
+
+	} else if (tags && is_in_dir(a, tags, &b) && *b) {
+		c = memchr(b, '/', d - b);
+		if (c) {
+			*c = '\0';
+			r = create_ref(TAG_REF, b);
+			strbuf_remove(name, 0, c+1 - a);
+		} else {
+			r = create_ref(TAG_REF, b);
+			strbuf_reset(name);
+		}
+		return r;
+
+	} else {
+		return NULL;
+	}
+}
+
+static struct svnref* find_svnref_by_refname(const char* name) {
+	int i;
+	unsigned char sha1[20];
+
+	if (prefixcmp(name, "refs/")) {
+		char* real_ref;
+		int refcount = dwim_ref(name, strlen(name), sha1, &real_ref);
+
+		if (refcount > 1) {
+			die("ambiguous ref '%s'", name);
+		} else if (!refcount) {
+			die("can not find ref '%s'", name);
+		}
+
+		name = real_ref;
+	}
+
+	for (i = 0; i < refn; i++) {
+		struct svnref* r = refs[i];
+		if (!strcmp(r->remote.buf, name)) {
+			return r;
+		}
+	}
+
+	if (!prefixcmp(name, remoteheads)) {
+		name += strlen(remoteheads);
+
+		if (!strcmp(name, trunkref)) {
+			return create_ref(TRUNK_REF, NULL);
+		} else if (!branches) {
+			die("in order to push a branch, svn.branches must be set");
+		} else {
+			return create_ref(BRANCH_REF, name);
+		}
+
+	} else if (!prefixcmp(name, remotetags)) {
+		name += strlen(remotetags);
+		if (!tags)
+			die("in order to push a tag, svn.tags must be set");
+
+		return create_ref(TAG_REF, name);
+	}
+
+	die("can not find ref '%s'", name);
+}
+
+static struct commit* find_svncmt(struct svnref* r, int rev) {
+	struct commit* c = r->svncmt;
+
+	while (c && parse_svnrev(c) > rev) {
+		c = svn_parent(c);
+		if (c && parse_commit(c)) {
+			die("invalid commit %s", cmt_to_hex(c));
+		}
+	}
+
+	return c;
+}
+
+/* reads a path, revision pair */
+static struct svnref* read_copy_source(struct strbuf* name, int* rev) {
+	int64_t srev;
+	struct svnref* sref;
+
+	/* copy-path */
+	read_name(name);
+	sref = find_svnref_by_path(name);
+	if (!sref) return NULL;
+
+	/* copy-rev */
+	srev = read_number();
+	if (srev < 0 || srev > INT_MAX) goto err;
+	*rev = srev;
+
+	return sref;
+err:
+	die("invalid copy source");
+}
+
+static int create_ref_cb(const char* refname, const unsigned char* sha1, int flags, void* cb_data) {
+	int i;
+	for (i = 0; i < refn; i++) {
+		const char *s = refs[i]->ref.buf;
+		if (!prefixcmp(s, "refs/svn/heads/") && !strcmp(s + strlen("refs/svn/heads/"), refname)) {
+			return 0;
+		}
+	}
+
+	if (!strcmp(refname, "trunk")) {
+		create_ref(TRUNK_REF, "");
+	} else if (branches) {
+		create_ref(BRANCH_REF, refname);
+	}
+
+	return 0;
+}
+
+#define SEEN_FROM_OBJ 1
+#define SEEN_FROM_SVN 2
+#define SEEN_FROM_BOTH (SEEN_FROM_OBJ | SEEN_FROM_SVN)
+#define SVNCMT 4
+
+static void insert_commit(struct commit *c, struct commit_list **cmts) {
+	if (parse_commit(c)) {
+		die("invalid commit %s", sha1_to_hex(c->object.sha1));
+	}
+	commit_list_insert_by_date(c, cmts);
+}
+
+static void insert_svncmt(struct commit *sc, struct commit_list **cmts) {
+	struct commit *gc;
+	struct commit *sc2;
+	if (!sc) return;
+
+	sc->object.flags = SVNCMT;
+	insert_commit(sc, cmts);
+
+	/* If two or more svn commits point to the same git commit, then
+	 * we use the newest one. This is in line with using the newest
+	 * svn revision we can find. */
+	gc = svn_commit(sc);
+	if (!gc) return;
+
+	sc2 = (struct commit*) ((gc->object.flags & SEEN_FROM_SVN) ? gc->util : NULL);
+	if (!sc2 || sc->date > sc2->date) {
+		gc->object.flags |= SEEN_FROM_SVN;
+		gc->util = sc;
+		insert_commit(gc, cmts);
+	}
+}
+
+static void add_roots(struct commit_list **cmts, struct object *obj) {
+	static int all_refs_added;
+
+	int i;
+	struct commit *c = (struct commit*) deref_tag(obj, NULL, 0);
+	if (!c || c->object.type != OBJ_COMMIT) {
+		die("invalid object %s", sha1_to_hex(obj->sha1));
+	}
+
+	c->object.flags = SEEN_FROM_OBJ;
+	insert_commit(c, cmts);
+
+	if (!all_refs_added) {
+		for_each_ref_in("refs/svn/heads/", &create_ref_cb, NULL);
+		all_refs_added = 1;
+	}
+
+	for (i = 0; i < refn; i++) {
+		insert_svncmt(refs[i]->svncmt, cmts);
+	}
+}
+
+/* Searches back from the new object. Looking for the previous svn
+ * commit or failing that the newest svn commit.
+ */
+static struct commit* find_copy_source(struct svnref* r, struct object* obj) {
+	struct commit_list *cmts = NULL;
+	struct commit *end = r->svncmt ? svn_commit(r->svncmt) : NULL;
+	struct commit *ret = NULL;
+
+	add_roots(&cmts, obj);
+	while (cmts) {
+		struct commit *c = pop_commit(&cmts);
+		int both = (c->object.flags & SEEN_FROM_BOTH) == SEEN_FROM_BOTH;
+
+		if (both && c == end) {
+			ret = (struct commit*) c->util;
+			break;
+
+		} else if (c == end) {
+			end = NULL;
+			if (ret) break;
+
+		} else if (both && !ret) {
+			/* commits are processed newest first hence !ret */
+			ret = (struct commit*) c->util;
+			if (end == NULL) break;
+
+		} else if (c->object.flags & SVNCMT) {
+			insert_svncmt(svn_parent(c), &cmts);
+
+		} else if (c->object.flags & SEEN_FROM_OBJ) {
+			struct commit_list *p = c->parents;
+
+			while (p) {
+				c = p->item;
+				c->object.flags |= SEEN_FROM_OBJ;
+				insert_commit(c, &cmts);
+				p = p->next;
+			}
+		}
+	}
+
+	free_commit_list(cmts);
+	clear_object_flags(SEEN_FROM_BOTH | SVNCMT);
+
+	return ret;
+}
+
+static void read_add_dir(struct svnref* r, int rev) {
+	/* path, parent-token, child-token, [copy-path, copy-rev] */
+
+	struct strbuf name = STRBUF_INIT;
+	struct strbuf srcname = STRBUF_INIT;
+	struct cache_entry* ce;
+	char* p;
+	size_t dlen;
+	int files = 0;
+
+	read_name(&name);
+	find_svnref_by_path(&name);
+	fprintf(stderr, "A %s\n", name.buf);
+
+	if (name.len) strbuf_addch(&name, '/');
+	dlen = name.len;
+
+	strbuf_setlen(&name, dlen);
+
+	/* empty folder - add ./.gitempty */
+	if (files == 0 && dlen) {
+		unsigned char sha1[20];
+		if (write_sha1_file(NULL, 0, "blob", sha1))
+			die("failed to write .gitempty object");
+		strbuf_addstr(&name, ".gitempty");
+		ce = make_cache_entry(create_ce_mode(0644), sha1, name.buf, 0, 0);
+		add_index_entry(git_index(r), ce, ADD_CACHE_OK_TO_ADD);
+	}
+
+	/* remove ../.gitempty */
+	if (dlen) {
+		strbuf_setlen(&name, dlen - 1);
+		p = strrchr(name.buf, '/');
+		if (p) {
+			strbuf_setlen(&name, p - name.buf);
+			strbuf_addstr(&name, "/.gitempty");
+			remove_file_from_index(git_index(r), name.buf);
+		}
+	}
+
+	strbuf_release(&srcname);
+	strbuf_release(&name);
+}
+
+static void read_add_file(struct svnref* r, int rev, struct strbuf* name, void** srcp, size_t* srcsz) {
+	/* name, dir-token, file-token, [copy-path, copy-rev] */
+	struct strbuf srcname = STRBUF_INIT;
+	char* p;
+
+	read_name(name);
+	find_svnref_by_path(name);
+	fprintf(stderr, "A %s\n", name->buf);
+
+	/* remove ./.gitempty */
+	p = strrchr(name->buf, '/');
+	if (p) {
+		struct strbuf empty = STRBUF_INIT;
+		strbuf_add(&empty, name->buf, p - name->buf);
+		strbuf_addstr(&empty, "/.gitempty");
+		remove_file_from_index(git_index(r), empty.buf);
+		strbuf_release(&empty);
+	}
+
+	strbuf_release(&srcname);
+}
+
+static void read_open_file(struct svnref* r, int rev, struct strbuf* name, void** srcp, size_t* srcsz) {
+	/* name, dir-token, file-token, rev */
+	enum object_type type;
+	struct cache_entry* ce;
+	unsigned long srcn;
+
+	read_name(name);
+	find_svnref_by_path(name);
+	fprintf(stderr, "M %s\n", name->buf);
+
+	ce = index_name_exists(svn_index(r), name->buf, name->len, 0);
+	if (!ce) goto err;
+
+	*srcp = read_sha1_file(ce->sha1, &type, &srcn);
+	if (!srcp || type != OBJ_BLOB) goto err;
+	*srcsz = srcn;
+
+	return;
+err:
+	die("malformed update");
+}
+
+static void read_close_file(struct svnref* r, const char* name, const void* data, size_t sz) {
+	/* file-token, [text-checksum] */
+	struct cache_entry* ce;
+	unsigned char sha1[20];
+	struct strbuf buf = STRBUF_INIT;
+
+	if (skip_string()) goto err; /* file-token */
+
+	if (write_sha1_file(data, sz, "blob", sha1))
+		die_errno("write blob");
+
+	if (have_optional()) {
+		unsigned char h1[16], h2[16];
+		MD5_CTX ctx;
+
+		strbuf_reset(&buf);
+		if (read_string(&buf)) goto err;
+		if (get_md5_hex(buf.buf, h1)) goto err;
+
+		MD5_Init(&ctx);
+		MD5_Update(&ctx, data, sz);
+		MD5_Final(h2, &ctx);
+
+		if (memcmp(h1, h2, sizeof(h1))) {
+			ce = index_name_exists(svn_index(r), name, strlen(name), 0);
+			die("hash mismatch for '%s', expected md5 %s, got md5 %s, old sha1 %s, new sha1 %s",
+					name,
+					md5_to_hex(h2),
+					md5_to_hex(h1),
+					sha1_to_hex(ce ? ce->sha1 : null_sha1),
+					sha1_to_hex(sha1));
+		}
+
+		read_end();
+	}
+
+	ce = make_cache_entry(0644, sha1, name, 0, 0);
+	if (!ce) die("make_cache_entry failed for path '%s'", name);
+	add_index_entry(svn_index(r), ce, ADD_CACHE_OK_TO_ADD);
+
+	strbuf_reset(&buf);
+	if (convert_to_git(name, data, sz, &buf, SAFE_CRLF_FALSE)) {
+		if (write_sha1_file(buf.buf, buf.len, "blob", sha1)) {
+			die_errno("write blob");
+		}
+	}
+	ce = make_cache_entry(0644, sha1, name, 0, 0);
+	if (!ce) die("make_cache_entry failed for path '%s'", name);
+	add_index_entry(git_index(r), ce, ADD_CACHE_OK_TO_ADD);
+
+	strbuf_release(&buf);
+	return;
+
+err:
+	die("malformed update");
+}
+
+/* returns number of entries removed */
+static int remove_index_path(struct index_state* idx, struct strbuf* name) {
+	int ret = 0;
+	int i = index_name_pos(idx, name->buf, name->len);
+
+	if (i >= 0) {
+		/* file */
+		cache_tree_invalidate_path(idx->cache_tree, name->buf);
+		remove_index_entry_at(idx, i);
+		return 1;
+	}
+
+	/* we've got to re-lookup the path as a < a.c < a/c */
+	strbuf_addch(name, '/');
+	i = -index_name_pos(idx, name->buf, name->len) - 1;
+
+	/* directory, index_name_pos returns -first-1
+	 * where first is the position the entry would
+	 * be added at, and the cache is sorted */
+	while (i < idx->cache_nr) {
+		struct cache_entry* ce = idx->cache[i];
+		if (ce_namelen(ce) < name->len) break;
+		if (memcmp(ce->name, name->buf, name->len)) break;
+
+		ce->ce_flags |= CE_REMOVE;
+		i++;
+		ret++;
+	}
+
+	strbuf_setlen(name, name->len - 1);
+
+	if (ret) {
+		cache_tree_invalidate_path(idx->cache_tree, name->buf);
+		remove_marked_cache_entries(idx);
+	}
+
+	return ret;
+}
+
+static void read_delete_entry(struct svnref* r, int rev) {
+	/* name, [revno], dir-token */
+	struct strbuf name = STRBUF_INIT;
+
+	read_name(&name);
+	find_svnref_by_path(&name);
+	fprintf(stderr, "D %s\n", name.buf);
+
+	remove_index_path(svn_index(r), &name);
+	remove_index_path(git_index(r), &name);
+	if (!name.len) {
+		r->delete = 1;
+	}
+	strbuf_release(&name);
+	return;
+}
+
+static void read_text_delta(struct strbuf *delta) {
+	for (;;) {
+		const char* s;
+
+		/* finish off the previous textdelta-chunk or
+		 * apply-textdelta */
+		read_command_end();
+
+		s = read_command();
+
+		if (!strcmp(s, "textdelta-end")) {
+			/* leave textdelta-end opened for read_update to
+			 * close */
+			return;
+		}
+
+		/* if we get some other command we just loop around
+		 * again */
+		if (strcmp(s, "textdelta-chunk")) {
+			continue;
+		}
+
+		/* file-token, chunk */
+		if (skip_string() || read_string(delta))
+			die("invalid textdelta command");
+	}
+}
+
+#define MAX_VARINT_LEN 9
+
+static unsigned char* parse_varint(unsigned char *p, unsigned char *e, size_t *v) {
+	*v = 0;
+	for (;;) {
+		if (p == e || *v > (INT64_MAX >> 7))
+			die("invalid svndiff");
+
+		*v = (*v << 7) | (*p & 0x7F);
+
+		if (!(*(p++) & 0x80))
+			return p;
+	}
+}
+
+static unsigned char* encode_varint(unsigned char* p, size_t n) {
+	if (n < 0) die("int too large");
+	if (n >= (INT64_C(1) << 56)) *(p++) = ((n >> 56) & 0x7F) | 0x80;
+	if (n >= (INT64_C(1) << 49)) *(p++) = ((n >> 49) & 0x7F) | 0x80;
+	if (n >= (INT64_C(1) << 42)) *(p++) = ((n >> 42) & 0x7F) | 0x80;
+	if (n >= (INT64_C(1) << 35)) *(p++) = ((n >> 35) & 0x7F) | 0x80;
+	if (n >= (INT64_C(1) << 28)) *(p++) = ((n >> 28) & 0x7F) | 0x80;
+	if (n >= (INT64_C(1) << 21)) *(p++) = ((n >> 21) & 0x7F) | 0x80;
+	if (n >= (INT64_C(1) << 14)) *(p++) = ((n >> 14) & 0x7F) | 0x80;
+	if (n >= (INT64_C(1) << 7)) *(p++) = ((n >> 7) & 0x7F) | 0x80;
+	*(p++) = n & 0x7F;
+	return p;
+}
+
+static size_t encoded_length(size_t n) {
+	unsigned char b[MAX_VARINT_LEN];
+	return encode_varint(b, n) - b;
+}
+
+#define FROM_SOURCE (0 << 6)
+#define FROM_TARGET (1 << 6)
+#define FROM_NEW    (2 << 6)
+
+static unsigned char* parse_instruction(unsigned char *p, unsigned char *e, int* ins, size_t* off, size_t* len) {
+	int hdr;
+
+	if (p >= e) die("invalid svndiff");
+	hdr = *p++;
+
+	*len = hdr & 0x3F;
+	if (*len == 0) {
+		p = parse_varint(p, e, len);
+	}
+
+	*ins = hdr & 0xC0;
+	*off = 0;
+	if (*ins == FROM_SOURCE || *ins == FROM_TARGET) {
+		p = parse_varint(p, e, off);
+	}
+
+	return p;
+}
+
+#define MAX_INS_LEN (1 + 2 * MAX_VARINT_LEN)
+
+static unsigned char* encode_instruction(unsigned char* p, int ins, size_t off, size_t len) {
+	if (len < 0x3F) {
+		*(p++) = ins | len;
+	} else {
+		*(p++) = ins;
+		p = encode_varint(p, len);
+	}
+
+	if (ins == FROM_SOURCE || ins == FROM_TARGET) {
+		p = encode_varint(p, off);
+	}
+
+	return p;
+}
+
+static unsigned char *parse_svndiff_chunk(unsigned char *p, size_t *sz, struct strbuf *buf, int ver) {
+	unsigned char *e = p + *sz;
+	size_t inflated = *sz;
+	z_stream z;
+
+	if (ver > 0) {
+		p = parse_varint(p, e, &inflated);
+	}
+
+	*sz = inflated;
+	if (p + inflated == e)
+		return p;
+
+	memset(&z, 0, sizeof(z));
+	inflateInit(&z);
+
+	strbuf_grow(buf, inflated);
+
+	z.next_in = p;
+	z.avail_in = e - p;
+	z.next_out = (unsigned char*) buf->buf;
+	z.avail_out = inflated;
+
+	if (inflate(&z, Z_FINISH) != Z_STREAM_END) {
+		die("zlib error");
+	}
+	strbuf_setlen(buf, inflated - z.avail_out);
+	inflateEnd(&z);
+
+	return (unsigned char*) buf->buf;
+}
+
+static unsigned char* apply_svndiff_win(struct strbuf *tgt, const void *src, size_t sz, unsigned char *d, unsigned char *e, int ver) {
+	struct strbuf insbuf = STRBUF_INIT;
+	struct strbuf databuf = STRBUF_INIT;
+	unsigned char *insp, *inse, *datap, *datae;
+	size_t srco, srcl, tgtl, insl, datal, w = 0;
+
+	d = parse_varint(d, e, &srco);
+	d = parse_varint(d, e, &srcl);
+	d = parse_varint(d, e, &tgtl);
+	d = parse_varint(d, e, &insl);
+	d = parse_varint(d, e, &datal);
+
+	if (unsigned_add_overflows(srco, srcl) || srco + srcl > sz)
+		goto err;
+
+	if (unsigned_add_overflows(insl, datal) || insl + datal > e - d)
+		goto err;
+
+	insp = d;
+	datap = insp + insl;
+	d = datap + datal;
+
+	insp = parse_svndiff_chunk(insp, &insl, &insbuf, ver);
+	datap = parse_svndiff_chunk(datap, &datal, &databuf, ver);
+
+	inse = insp + insl;
+	datae = datap + datal;
+
+	strbuf_grow(tgt, tgt->len + tgtl);
+
+	while (insp < inse) {
+		size_t off, len;
+		ssize_t tgtr;
+		int ins;
+
+		insp = parse_instruction(insp, inse, &ins, &off, &len);
+
+		switch (ins) {
+		case FROM_SOURCE:
+			if (off > srcl || len > srcl - off) goto err;
+			strbuf_add(tgt, (char*) src + srco + off, len);
+			break;
+
+		case FROM_TARGET:
+			tgtr = min(w - off, len);
+			if (tgtr <= 0) goto err;
+
+			off = tgt->len - w + off;
+
+			/* len may be greater than tgtr. In this case we
+			 * just repeat [tgto,tgto+tgtr]
+			 */
+			while (len) {
+				int n = min(len, tgtr);
+				strbuf_add(tgt, tgt->buf + off, n);
+				len -= n;
+			}
+			break;
+
+		case FROM_NEW:
+			if (datae - datap < len) goto err;
+			strbuf_add(tgt, datap, len);
+			datap += len;
+			break;
+
+		default:
+			goto err;
+		}
+
+		w += len;
+	}
+
+	if (w != tgtl || datap != datae) goto err;
+
+	strbuf_release(&insbuf);
+	strbuf_release(&databuf);
+	return d;
+err:
+	die("invalid svndiff");
+}
+
+static void apply_svndiff(struct strbuf *tgt, const void *src, size_t sz, const void *delta, size_t dsz) {
+	unsigned char *d = (unsigned char*) delta;
+	unsigned char *e = d + dsz;
+	int ver;
+
+	if (dsz < 4 || memcmp(d, "SVN", 3))
+		goto err;
+
+	ver = d[3];
+	if (ver > 1)
+		goto err;
+
+	d += 4;
+
+	while (d < e) {
+		d = apply_svndiff_win(tgt, src, sz, d, e, ver);
+	}
+
+	return;
+
+err:
+	die(_("invalid svndiff"));
+}
+
+static void read_update(struct svnref* r, int rev) {
+	struct strbuf name = STRBUF_INIT;
+	struct strbuf tgt = STRBUF_INIT;
+	void* src = NULL;
+	size_t srcsz = 0;
+	int filedirty = 0;
+
+	read_success(); /* update */
+	read_success(); /* report */
+
+	for (;;) {
+		const char *s = read_command();
+
+		if (!strcmp(s, "close-edit")) {
+			if (name.len) goto err;
+			read_command_end();
+			break;
+
+		} else if (!strcmp(s, "abort-edit")) {
+			die("update aborted");
+
+		} else if (!strcmp(s, "open-root")) {
+			if (name.len) goto err;
+
+		} else if (!strcmp(s, "add-dir")) {
+			if (name.len) goto err;
+			read_add_dir(r, rev);
+
+		} else if (!strcmp(s, "open-file")) {
+			if (name.len) goto err;
+			read_open_file(r, rev, &name, &src, &srcsz);
+
+		} else if (!strcmp(s, "add-file")) {
+			if (name.len) goto err;
+			read_add_file(r, rev, &name, &src, &srcsz);
+
+		} else if (!strcmp(s, "close-file")) {
+			if (!name.len) goto err;
+
+			if (filedirty) {
+				read_close_file(r, name.buf, tgt.buf, tgt.len);
+			}
+
+			strbuf_release(&tgt);
+			strbuf_reset(&name);
+			free(src);
+			src = NULL;
+			srcsz = 0;
+			filedirty = 0;
+
+		} else if (!strcmp(s, "delete-entry")) {
+			if (name.len) goto err;
+			read_delete_entry(r, rev);
+
+		} else if (!strcmp(s, "apply-textdelta")) {
+			struct strbuf delta = STRBUF_INIT;
+
+			/* file-token, [base-checksum] */
+			if (!name.len) goto err;
+
+			read_text_delta(&delta);
+			filedirty = 1;
+			if (delta.len) {
+				apply_svndiff(&tgt, src, srcsz, delta.buf, delta.len);
+			}
+			strbuf_release(&delta);
+		}
+
+		read_command_end();
+	}
+
+	read_success();
+
+	free(src);
+	strbuf_release(&name);
+	strbuf_release(&tgt);
+	return;
+
+err:
+	die("malformed update");
+}
+
+struct author {
+	char* user;
+	char* pass;
+	char* name;
+	char* mail;
+};
+
+struct author* authors;
+size_t authorn, authoralloc;
+struct author* defauthor;
+
+static char* strip_space(char* p) {
+	char* e = p + strlen(p);
+
+	while (*p == ' ' || *p == '\t') {
+		p++;
+	}
+
+	while (e > p && (e[-1] == ' ' || e[-1] == '\t')) {
+		*(--e) = '\0';
+	}
+
+	return p;
+}
+
+static void parse_authors() {
+	char* p;
+	struct stat st;
+	int fd = open(git_path("svn-authors"), O_RDONLY);
+	if (fd < 0 || fstat(fd, &st)) return;
+
+	p = xmalloc(st.st_size + 1);
+	if (xread(fd, p, st.st_size) != st.st_size)
+		die("read failed on authors");
+
+	p[st.st_size] = '\0';
+
+	while (p && *p) {
+		struct author a;
+		char* line = strchr(p, '\n');
+		if (line) *(line++) = '\0';
+
+		a.user = p;
+
+		p = strchr(p, '=');
+		if (!p) goto nextline; /* empty line */
+		*(p++) = '\0';
+		a.name = p;
+
+		p = strchr(p, '<');
+		if (!p) die("invalid author entry for %s", a.user);
+		*(p++) = '\0';
+		a.mail = p;
+
+		p = strchr(p, '>');
+		if (!p) die("invalid author entry for %s", a.user);
+		*(p++) = '\0';
+		a.pass = p;
+
+		a.user = strip_space(a.user);
+		a.name = strip_space(a.name);
+		a.mail = strip_space(a.mail);
+
+		p = strchr(a.user, ':');
+		if (p) {
+			*p = '\0';
+			a.pass = p+1;
+		} else {
+			a.pass = NULL;
+		}
+
+		if (*a.user == '#') {
+			/* comment */
+		} else {
+			ALLOC_GROW(authors, authorn + 1, authoralloc);
+			authors[authorn++] = a;
+		}
+
+nextline:
+		p = line;
+	}
+
+	close(fd);
+}
+
+static void svn_author_to_git(struct strbuf* author) {
+	int i;
+
+	for (i = 0; i < authorn; i++) {
+		struct author* a = &authors[i];
+		if (!strcasecmp(author->buf, a->user)) {
+			strbuf_reset(author);
+			strbuf_addf(author, "%s <%s>", a->name, a->mail);
+			return;
+		}
+	}
+
+	die("could not find username '%s' in %s\n"
+			"Add a line of the form:\n"
+			"%s = Full Name <email@example.com>\n",
+			author->buf,
+			git_path("svn-authors"),
+			author->buf);
+}
+
+static struct author* get_object_author(struct object* obj) {
+	const char *lb, *le, *mb, *me;
+	struct strbuf buf = STRBUF_INIT;
+	struct author* ret = NULL;
+	char* data = NULL;
+	int i;
+
+	if (obj->type == OBJ_COMMIT) {
+		struct commit* cmt = (struct commit*) obj;
+		parse_commit(cmt);
+		lb = strstr(cmt->buffer, "\ncommitter ");
+		if (!lb) lb = strstr(cmt->buffer, "\nauthor ");
+	} else if (obj->type == OBJ_TAG) {
+		enum object_type type;
+		unsigned long size;
+		data = read_sha1_file(obj->sha1, &type, &size);
+		if (!data || type != OBJ_TAG) goto err;
+		lb = strstr(data, "\ntagger ");
+	} else {
+		die("invalid commit object");
+	}
+
+	if (!lb) goto err;
+	le = strchr(lb+1, '\n');
+	if (!le) goto err;
+	mb = memchr(lb, '<', le - lb);
+	if (!mb) goto err;
+	me = memchr(mb, '>', le - mb);
+	if (!me) goto err;
+
+	strbuf_add(&buf, mb+1, me - (mb+1));
+
+	for (i = 0; i < authorn; i++) {
+		struct author* a = &authors[i];
+		if (strcasecmp(buf.buf, a->mail)) continue;
+		if (!a->pass) {
+			die("need password for user '%s' in %s\n"
+				"Add a line of the form:\n"
+				"%s:password = Full Name <%s>\n",
+				a->user,
+				git_path("svn-authors"),
+				a->user,
+				a->mail);
+		}
+
+		ret = a;
+		break;
+	}
+
+	if (!ret) {
+		die("could not find username/password for %s in %s\n"
+				"Add a line of the form:\n"
+				"username:password = Full Name <%s>\n",
+				buf.buf,
+				git_path("svn-authors"),
+				buf.buf);
+	}
+
+	strbuf_release(&buf);
+	free(data);
+	return ret;
+
+err:
+	die("can not find author in %s", sha1_to_hex(obj->sha1));
+}
+
+static struct commit* latest_fetch_svncmt;
+
+static void init_latest_fetch(void) {
+	unsigned char sha1[20];
+	struct commit* cmt;
+
+	latest_fetch_svncmt = NULL;
+	if (read_ref("refs/svn/latest", sha1) || is_null_sha1(sha1))
+		return;
+
+	cmt = lookup_commit(sha1);
+	if (!cmt || parse_commit(cmt)) {
+		die("invalid latest ref %s", sha1_to_hex(sha1));
+	}
+
+	latest_fetch_svncmt = cmt;
+}
+
+static int latest_fetch_rev(void) {
+	return latest_fetch_svncmt ? parse_svnrev(latest_fetch_svncmt) : 0;
+}
+
+static int set_latest_fetch(struct commit* cmt) {
+	struct ref_lock* lk;
+
+	if (cmt == latest_fetch_svncmt) {
+		return 0;
+	}
+
+	lk = lock_ref_sha1("svn/latest", cmt_sha1(latest_fetch_svncmt));
+	if (!lk || write_ref_sha1(lk, cmt_sha1(cmt), "svn-fetch")) {
+		error("failed to update latest ref");
+		return 1;
+	}
+
+	latest_fetch_svncmt = cmt;
+	return 0;
+}
+
+static int svn_time_to_git(struct strbuf* time) {
+	struct tm tm;
+	memset(&tm, 0, sizeof(tm));
+	if (!strptime(time->buf, "%Y-%m-%dT%H:%M:%S", &tm)) return -1;
+	strbuf_reset(time);
+	strbuf_addf(time, "%"PRId64, (int64_t) mktime(&tm));
+	return 0;
+}
+
+static struct commit* create_fetched_commit(struct svnref* r, int rev, const char* author, const char* time, const char* log, int created) {
+	static struct strbuf buf = STRBUF_INIT;
+	unsigned char sha1[20];
+
+	struct object *gitobj;
+	struct commit *gitcmt, *svncmt;
+	struct ref_lock* lk = NULL;
+
+	/* Create the commit object.
+	 *
+	 * SVN can't create tags and branches without a commit,
+	 * but git can. In the cases where new refs are just
+	 * created without any changes to the tree, we don't add
+	 * a commit. This way git commits pushed to svn and
+	 * pulled back again look roughly the same.
+	 */
+	if (r->delete) {
+		gitcmt = NULL;
+
+	} else if ((r->istag || created)
+		&& r->parent
+		&& !hashcmp(idx_sha1(git_index(r)), r->parent->tree->object.sha1)
+		  ) {
+		/* branch/tag has been created/replaced, but the tree hasn't
+		 * been changed */
+		gitcmt = r->parent;
+
+	} else {
+		strbuf_reset(&buf);
+		strbuf_addf(&buf, "tree %s\n", sha1_to_hex(idx_sha1(git_index(r))));
+
+		if (r->parent) {
+			strbuf_addf(&buf, "parent %s\n", cmt_to_hex(r->parent));
+		}
+
+		strbuf_addf(&buf, "author %s %s +0000\n", author, time);
+		strbuf_addf(&buf, "committer %s %s +0000\n", author, time);
+
+		strbuf_addch(&buf, '\n');
+		strbuf_addstr(&buf, log);
+
+		if (write_sha1_file(buf.buf, buf.len, "commit", sha1))
+			die("failed to create commit");
+
+		gitcmt = lookup_commit(sha1);
+		if (!gitcmt || parse_commit(gitcmt))
+			die("failed to parse created commit");
+	}
+
+	/* Create the tag object.
+	 *
+	 * Now we create an annotated tag wrapped around either
+	 * the commit the tag was branched from or the wrapper.
+	 * Where a tag is later updated, we either recreate this
+	 * tag with a new time (no tree change) or create a new
+	 * dummy commit whose parent is the old dummy.
+	 */
+	if (r->delete) {
+		gitobj = NULL;
+
+	} else if (r->istag) {
+		strbuf_reset(&buf);
+		strbuf_addf(&buf, "object %s\n", cmt_to_hex(gitcmt));
+		strbuf_addf(&buf, "type commit\n");
+		strbuf_addf(&buf, "tag %s\n", r->remote.buf + strlen("refs/tags/"));
+		strbuf_addf(&buf, "tagger %s %s +0000\n", author, time);
+		strbuf_addch(&buf, '\n');
+		strbuf_addstr(&buf, log);
+
+		if (write_sha1_file(buf.buf, buf.len, tag_type, sha1))
+			die("failed to create tag");
+
+		gitobj = parse_object(sha1);
+		if (!gitobj)
+			die("failed to parse created tag");
+
+	} else {
+		gitobj = &gitcmt->object;
+	}
+
+	/* Create the svn commit */
+	strbuf_reset(&buf);
+	strbuf_addf(&buf, "tree %s\n", sha1_to_hex(idx_sha1(svn_index(r))));
+
+	if (gitcmt || r->svncmt) {
+		strbuf_addf(&buf, "parent %s\n", cmt_to_hex(gitcmt ? gitcmt : r->svncmt));
+	}
+
+	if (r->svncmt) {
+		strbuf_addf(&buf, "parent %s\n", cmt_to_hex(r->svncmt));
+	}
+
+	strbuf_addf(&buf, "author %s %s +0000\n", author, time);
+	strbuf_addf(&buf, "committer %s %s +0000\n", author, time);
+	strbuf_addf(&buf, "revision %d\n", rev);
+	strbuf_addf(&buf, "path %s\n", r->svn.buf);
+	strbuf_addch(&buf, '\n');
+
+	if (write_sha1_file(buf.buf, buf.len, "commit", sha1))
+		die("failed to create svn object");
+
+	svncmt = lookup_commit(sha1);
+	if (!svncmt || parse_commit(svncmt))
+		die("failed to parse created svn commit");
+
+	/* update the ref */
+
+	lk = lock_ref_sha1(r->ref.buf + strlen("refs/"), cmt_sha1(r->svncmt));
+	if (!lk || write_ref_sha1(lk, cmt_sha1(svncmt), "svn-fetch")) {
+		die("failed to update ref %s", r->ref.buf);
+	}
+
+	/* update the remote or tag ref */
+
+	if (r->gitobj && !gitobj) {
+		if (delete_ref(r->remote.buf, r->gitobj->sha1, 0)) {
+			error("failed to delete ref %s", r->remote.buf);
+			goto rollback;
+		}
+	} else if (gitobj) {
+		lk = lock_ref_sha1(r->remote.buf + strlen("refs/"),
+				r->gitobj ? r->gitobj->sha1 : null_sha1);
+
+		if (!lk || write_ref_sha1(lk, gitobj->sha1, "svn-fetch")) {
+			error("failed to update ref %s", r->remote.buf);
+			goto rollback;
+		}
+	}
+
+	r->delete = 0;
+	r->gitobj = gitobj;
+	r->svncmt = svncmt;
+	r->parent = gitcmt;
+
+	fprintf(stderr, "fetched %d %s %s\n", rev, r->ref.buf, sha1_to_hex(r->svncmt->object.sha1));
+	return svncmt;
+
+rollback:
+	if (r->svncmt) {
+		lk = lock_ref_sha1(r->ref.buf + strlen("refs/"), cmt_sha1(svncmt));
+		if (!lk || write_ref_sha1(lk, cmt_sha1(r->svncmt), "svn-fetch rollback"))
+			goto rollback_failed;
+	} else if (svncmt) {
+		if (delete_ref(r->ref.buf, cmt_sha1(svncmt), 0))
+			goto rollback_failed;
+	}
+
+	exit(128);
+
+rollback_failed:
+	die("failed to rollback %s", r->ref.buf);
+}
+
+static void request_commit(struct svnref* r, int rev, struct svnref* copysrc, int copyrev) {
+	fprintf(stderr, "request commit %d\n", rev);
+
+	if (!copysrc) {
+		copysrc = r;
+		copyrev = rev - 1;
+	}
+
+	/* [rev] target recurse target-url */
+	sendf("( switch ( ( %d ) %d:%s true %d:%s/%s ) )\n",
+			rev,
+			(int) copysrc->svn.len,
+			copysrc->svn.buf,
+			(int) (strlen(url) + 1 + r->svn.len),
+			url,
+			r->svn.buf);
+
+	/* path rev start-empty */
+	sendf("( set-path ( 0: %d false ) )\n", copyrev);
+	sendf("( finish-report ( ) )\n");
+	sendf("( success ( ) )\n");
+}
+
+static void request_log(int from, int to) {
+	struct strbuf paths = STRBUF_INIT;
+	if (!trunk && !branches && !tags) {
+		strbuf_addstr(&paths, "0: ");
+	}
+	if (trunk) {
+		strbuf_addf(&paths, "%d:%s ", (int) strlen(trunk), trunk);
+	}
+	if (branches) {
+		strbuf_addf(&paths, "%d:%s ", (int) strlen(branches), branches);
+	}
+	if (tags) {
+		strbuf_addf(&paths, "%d:%s ", (int) strlen(tags), tags);
+	}
+
+	sendf("( log ( ( %s) " /* (path...) */
+		"( %d ) ( %d ) " /* start/end revno */
+		"true false " /* changed-paths strict-node */
+		"0 " /* limit */
+		"false " /* include-merged-revisions */
+		"revprops ( 10:svn:author 8:svn:date 7:svn:log ) "
+		") )\n",
+		paths.buf,
+		from, /* log start */
+		to /* log end */
+	     );
+
+	strbuf_release(&paths);
+}
+
+struct pending {
+	char *buf, *msg, *author, *time;
+	struct svnref *ref, *copysrc;
+	int rev, copyrev;
+};
+
+static int have_next_commit(struct pending* retp) {
+	static struct pending *nextv;
+	static int nextc, nexta;
+	static int64_t rev;
+
+	struct strbuf msg = STRBUF_INIT;
+	struct strbuf author = STRBUF_INIT;
+	struct strbuf time = STRBUF_INIT;
+	struct strbuf name = STRBUF_INIT;
+
+	/* log reply is of the form
+	 * ( ( ( n:changed-path A|D|R|M ( n:copy-path copy-rev ) ) ... ) rev n:author n:date n:message )
+	 * ....
+	 * done
+	 * ( success ( ) )
+	 */
+
+	while (!nextc) {
+		struct pending* p;
+
+		/* start of log entry */
+		if (read_list()) {
+			read_done();
+			read_success();
+			return 0;
+		}
+
+		/* start changed path entries */
+		if (read_list()) goto err;
+
+		while (!read_list()) {
+			const char* s;
+			struct svnref *to;
+			int i;
+
+			/* path A|D|R|M [copy-path copy-rev] */
+			strbuf_reset(&name);
+			read_name(&name);
+			to = find_svnref_by_path(&name);
+			s = read_word();
+			if (!s) goto err;
+
+			p = NULL;
+			for (i = 0; i < nextc; i++) {
+				if (nextv[i].ref == to) {
+					p = &nextv[i];
+					break;
+				}
+			}
+
+			if (to && !name.len && (!strcmp(s, "A") || !strcmp(s, "R")) && have_optional()) {
+				int copyrev;
+				struct svnref* copysrc;
+
+				strbuf_reset(&name);
+				copysrc = read_copy_source(&name, &copyrev);
+				if (copysrc && name.len) {
+					warning("copy from non-root path");
+					copysrc = NULL;
+					copyrev = 0;
+				}
+
+				if (p == NULL) {
+					ALLOC_GROW(nextv, nextc+1, nexta);
+					p = &nextv[nextc++];
+				}
+
+				p->copysrc = copysrc;
+				p->copyrev = copyrev;
+				p->ref = to;
+
+				read_end();
+
+			} else if (to && p == NULL) {
+				ALLOC_GROW(nextv, nextc+1, nexta);
+				p = &nextv[nextc++];
+				p->copysrc = NULL;
+				p->ref = to;
+			}
+
+			read_end();
+		}
+
+		/* end of changed path entries */
+		read_end();
+
+		/* rev number */
+		rev = read_number();
+		if (rev < 0) goto err;
+
+		/* author */
+		if (read_list()) goto err;
+		strbuf_reset(&author);
+		if (read_string(&author)) goto err;
+		svn_author_to_git(&author);
+		read_end();
+
+		/* timestamp */
+		if (read_list()) goto err;
+		strbuf_reset(&time);
+		if (read_string(&time)) goto err;
+		if (svn_time_to_git(&time)) goto err;
+		read_end();
+
+		/* log message */
+		strbuf_reset(&msg);
+		if (have_optional()) {
+			if (read_string(&msg)) goto err;
+			strbuf_complete_line(&msg);
+			read_end();
+		}
+
+		/* end of log entry */
+		read_end();
+		if (verbose) fputc('\n', stderr);
+
+		/* remove entries where we've already downloaded the
+		 * commit */
+		p = nextv;
+		while (p < nextv+nextc) {
+			if (p->ref->svncmt && parse_svnrev(p->ref->svncmt) >= rev) {
+				memmove(p, p+1, sizeof(*p) * ((nextv+nextc) - (p+1)));
+				nextc--;
+			} else {
+				p->buf = xmalloc(msg.len + 1 + author.len + 1 + time.len + 1);
+
+				p->rev = rev;
+				p->msg = p->buf;
+				p->author = p->msg + msg.len + 1;
+				p->time = p->author + author.len + 1;
+
+				memcpy(p->msg, msg.buf, msg.len + 1);
+				memcpy(p->author, author.buf, author.len + 1);
+				memcpy(p->time, time.buf, time.len + 1);
+
+				p++;
+			}
+		}
+	}
+
+	*retp = nextv[0];
+
+	memmove(nextv, nextv+1, sizeof(nextv[0])*(nextc-1));
+	nextc--;
+
+	strbuf_release(&name);
+	strbuf_release(&msg);
+	strbuf_release(&author);
+	strbuf_release(&time);
+	return 1;
+
+err:
+	die("malformed log");
+}
+
+static char* clean_path(char* p) {
+	char* e;
+	if (*p == '/') p++;
+	e = p + strlen(p);
+	if (e > p && e[-1] == '/') e[-1] = '\0';
+	return p;
+}
+
+static struct author** connection_authors;
+static int *svnfdv;
+static struct inbuffer *inbufv;
+
+static void setup_globals() {
+	int i;
+
+	setenv("TZ", "", 1);
+
+	core_eol = svn_eol;
+
+	if (getenv("GIT_SVN_PUSH_PAUSE")) {
+		pause_between_commits = atoi(getenv("GIT_SVN_PUSH_PAUSE"));
+	}
+
+	if (remotedir) {
+		struct strbuf buf = STRBUF_INIT;
+		remotedir = clean_path((char*) remotedir);
+
+		strbuf_addstr(&buf, "refs/remotes/");
+		strbuf_addstr(&buf, remotedir);
+		strbuf_addch(&buf, '/');
+		remoteheads = strbuf_detach(&buf, NULL);
+
+		strbuf_addstr(&buf, "refs/tags/");
+		strbuf_addstr(&buf, remotedir);
+		strbuf_addch(&buf, '/');
+		remotetags = strbuf_detach(&buf, NULL);
+	} else {
+		remoteheads = "refs/heads/";
+		remotetags = "refs/tags/";
+		leave_remote = 1;
+	}
+
+	if (svnfdc < 1) die("invalid number of connections");
+
+	connection_authors = xcalloc(svnfdc+1, sizeof(connection_authors[0]));
+	svnfdv = xmalloc((svnfdc+1) * sizeof(svnfdv[0]));
+	inbufv = xmalloc((svnfdc+1) * sizeof(inbufv[0]));
+	for (i = 0; i <= svnfdc; i++) {
+		svnfdv[i] = -1;
+		inbufv[i].b = inbufv[i].e = 0;
+	}
+
+	parse_authors();
+
+	for (i = 0; svnuser && i < authorn; i++) {
+		struct author* a = &authors[i];
+		if (!strcasecmp(a->user, svnuser)) {
+			defauthor = a;
+			if (!a->pass) {
+				die("user specified with --user needs a password");
+			}
+			break;
+		}
+	}
+
+	if (!defauthor) die("need to specify default user with --user");
+	if (!url) die("need to specify a url with --url");
+
+	if (trunk) trunk = clean_path((char*) trunk);
+	if (branches) branches = clean_path((char*) branches);
+	if (tags) tags = clean_path((char*) tags);
+
+	init_latest_fetch();
+}
+
+static void close_connection(int cidx) {
+	if (svnfdv[cidx] >= 0) {
+		close(svnfdv[cidx]);
+	}
+	svnfdv[cidx] = -1;
+	connection_authors[cidx] = NULL;
+	inbufv[cidx].b = inbufv[cidx].e = 0;
+}
+
+static void change_connection(int cidx, struct author* a) {
+	char pathsep;
+	char *host, *port, *path;
+	const char *s;
+	struct addrinfo hints, *res, *ai;
+	int err;
+	int fd = -1;
+
+	svnfd = svnfdv[cidx];
+	inbuf = &inbufv[cidx];
+	if (svnfd >= 0 && connection_authors[cidx] == a) {
+		return;
+	}
+
+	close_connection(cidx);
+
+	if (prefixcmp(url, "svn://"))
+		die(_("only svn repositories are supported"));
+
+	if (!a->pass)
+		die("need a password for user %s", a->user);
+
+	host = (char*) url + strlen("svn://");
+
+	path = strchr(host, '/');
+	if (!path) path = host + strlen(host);
+	pathsep = *path;
+	*path = '\0';
+
+	port = strchr(host, ':');
+	if (port) *(port++) = '\0';
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_socktype = SOCK_STREAM;
+
+	err = getaddrinfo(host, port ? port : "3690", &hints, &res);
+	*path = pathsep;
+	if (port) port[-1] = ':';
+
+	if (err)
+		die_errno("failed to connect to %s", url);
+
+	for (ai = res; ai != NULL; ai = ai->ai_next) {
+		fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+		if (fd < 0) continue;
+
+		if (connect(fd, ai->ai_addr, ai->ai_addrlen)) {
+			int err = errno;
+			close(fd);
+			errno = err;
+			continue;
+		}
+
+		break;
+	}
+
+	if (fd < 0)
+		die_errno("failed to connect to %s", url);
+
+	svnfd = svnfdv[cidx] = fd;
+	inbuf = &inbufv[cidx];
+	verbosetxnl = 1;
+
+	/* TODO: client software version and client capabilities */
+	sendf("( 2 ( edit-pipeline svndiff1 ) %d:%s )\n", (int) strlen(url), url);
+	sendf("( CRAM-MD5 ( ) )\n");
+
+	/* TODO: we don't care about capabilities/versions right now */
+	s = read_command();
+	if (strcmp(s, "success"))
+		die("server error");
+
+	/* minver then maxver */
+	if (read_number() > 2 || read_number() < 2)
+		die(_("version mismatch"));
+
+	read_command_end();
+
+	/* TODO: read the mech lists et all */
+	read_success();
+
+	cram_md5(a->user, a->pass);
+
+	sendf("( reparent ( %d:%s ) )\n", (int) strlen(url), url);
+
+	read_success(); /* auth */
+	read_success(); /* repo info */
+	read_success(); /* reparent */
+	read_success(); /* reparent again */
+
+	connection_authors[cidx] = a;
+}
+
+static int run_gc_auto() {
+	const char *args[] = {"gc", "--auto", NULL};
+	return run_command_v_opt(args, RUN_GIT_CMD);
+}
+
+static const char* print_arg(struct strbuf* sb, const char* fmt, ...) {
+	va_list ap;
+	va_start(ap, fmt);
+	strbuf_reset(sb);
+	strbuf_vaddf(sb, fmt, ap);
+	return sb->buf;
+}
+
+int cmd_svn_fetch(int argc, const char **argv, const char *prefix) {
+	int64_t n;
+	int from, to, i, finished;
+	struct pending *pending;
+	struct commit* svncmt = NULL;
+
+	git_config(&config, NULL);
+
+	argc = parse_options(argc, argv, prefix, builtin_svn_fetch_options,
+			builtin_svn_fetch_usage, 0);
+
+	if (argc)
+		usage_msg_opt(_("Too many arguments."),
+			builtin_svn_fetch_usage, builtin_svn_fetch_options);
+
+	setup_globals();
+
+	if (getenv("GIT_SVN_FETCH_REPORT_LATEST")) {
+		printf("%d\n", latest_fetch_rev());
+		return 0;
+	}
+
+	from = latest_fetch_rev();
+	pending = xcalloc(svnfdc, sizeof(pending[0]));
+
+	change_connection(svnfdc, defauthor);
+	sendf("( get-latest-rev ( ) )\n");
+
+	read_success(); /* latest rev */
+	read_command(); /* latest rev again */
+	n = read_number();
+	if (n < 0 || n > INT_MAX) die("latest-rev failed");
+	read_command_end();
+
+	to = min(last_revision, (int) n);
+
+	fprintf(stderr, "request log for %d %d\n", from, to);
+
+	/* gc --auto invalidates the object cache. Thus we have to run
+	 * it last. For when we want to run the update in multiple
+	 * bunches then we spawn off a sub command for each chunk.
+	 */
+	if (!inner && to > from + FETCH_AT_ONCE) {
+		struct strbuf revs = STRBUF_INIT;
+		struct strbuf conns = STRBUF_INIT;
+
+		while (from < to) {
+			int ret;
+			int cmdto = min(from + FETCH_AT_ONCE, to);
+			const char* args[] = {
+				"svn-fetch",
+				"-c", print_arg(&conns, "%d", svnfdc),
+				"-r", print_arg(&revs, "%d", cmdto),
+				"--user", svnuser,
+				"--inner",
+				verbose ? "-v" : NULL,
+				NULL
+			};
+
+			ret = run_command_v_opt(args, RUN_GIT_CMD);
+			if (ret) return ret;
+
+			from = cmdto;
+		}
+
+		return 0;
+	}
+
+	if (from >= to) {
+		return 0;
+	}
+
+	change_connection(svnfdc, defauthor);
+	request_log(from+1, to);
+	read_success();
+
+	/* start requesting commits until we've filled out pending
+	 * commits or run out of commits */
+	i = 0;
+	finished = 0;
+	while (i < svnfdc) {
+		struct pending* p = &pending[i];
+
+		change_connection(svnfdc, defauthor);
+		if (!have_next_commit(p)) {
+			finished = 1;
+			break;
+		}
+		change_connection(i, defauthor);
+		request_commit(p->ref, p->rev, p->copysrc, p->copyrev);
+
+		i++;
+	}
+
+	i = 0;
+	for (;;) {
+		struct pending* p = &pending[i];
+
+		/* process a commit */
+		if (!p->ref) break;
+
+		/* Only update the latest when we've moved onto a new
+		 * revision. That way if we fail after the first of two
+		 * branch updates in a revision we replay the whole
+		 * revision next time. */
+		if (svncmt && p->rev > latest_fetch_rev()) {
+			set_latest_fetch(svncmt);
+		}
+
+		if (p->copysrc) {
+			struct commit* c = find_svncmt(p->copysrc, p->copyrev);
+			checkout_svncmt(p->ref, c);
+		}
+
+		change_connection(i, defauthor);
+		read_update(p->ref, p->rev);
+		svncmt = create_fetched_commit(p->ref, p->rev, p->author, p->time, p->msg, p->copysrc != NULL);
+		free(p->buf);
+
+		/* then request a new one on that connection */
+		change_connection(svnfdc, defauthor);
+		if (!finished && have_next_commit(p)) {
+			change_connection(i, defauthor);
+			request_commit(p->ref, p->rev, p->copysrc, p->copyrev);
+		} else {
+			finished = 1;
+			p->ref = NULL;
+		}
+
+		i = (i+1) % svnfdc;
+	}
+
+	if (svncmt) {
+		set_latest_fetch(svncmt);
+	}
+
+	return run_gc_auto();
+}
+
+static const char* dtoken(int dir) {
+	static int bufnum;
+	static char bufs[4][32];
+	char* buf1 = bufs[bufnum++ & 3];
+	char* buf2 = bufs[bufnum++ & 3];
+	sprintf(buf1, "d%d", dir);
+	sprintf(buf2, "%d:%s", (int) strlen(buf1), buf1);
+	return buf2;
+}
+
+static int fcount;
+static const char* ftoken() {
+	static char buf[32];
+	sprintf(buf, "f%d", ++fcount);
+	sprintf(buf, "%d:f%d", (int) strlen(buf), fcount);
+	return buf;
+}
+
+/* check that no commits have been inserted on our branch between from
+ * (the previous revision at which we saw a change) and to (the revision
+ * we just commited) */
+static void check_for_svn_commits(struct svnref* r, int from, int to) {
+	if (from + 1 >= to) {
+		return;
+	}
+
+	sendf("( log ( ( %d:%s ) " /* (path...) */
+			"( %d ) ( %d ) " /* start/end revno */
+			"false false " /* changed-paths strict-node */
+			"0 false " /* limit include-merged-revisions */
+			"revprops ( ) ) )\n",
+		(int) r->svn.len,
+		r->svn.buf,
+		from + 1,
+		to - 1);
+
+	read_success();
+	if (!read_list()) {
+		die("commits inserted during push");
+	}
+
+	read_done();
+	read_success();
+}
+
+static size_t common_directory(const char* a, const char* b, int* depth) {
+	int off;
+	const char* ab = a;
+
+	off = 0;
+	while (*a && *b && *a == *b) {
+		if (*a == '/') {
+			(*depth)++;
+			off = a + 1 - ab;
+		}
+		a++;
+		b++;
+	}
+
+	return off;
+}
+
+static struct strbuf cpath = STRBUF_INIT;
+static int cdepth;
+
+static int change_dir(const char* path) {
+	const char *p, *d;
+	int off, depth = 0;
+
+	off = common_directory(path, cpath.buf, &depth);
+
+	/* cd .. to the common root */
+	while (cdepth > depth) {
+		sendf("( close-dir ( %s ) )\n", dtoken(cdepth));
+		cdepth--;
+	}
+
+	strbuf_setlen(&cpath, off);
+
+	/* cd down to the new path */
+	d = p = path + off;
+	for (;;) {
+		char* d = strchr(p, '/');
+		if (!d) break;
+
+		sendf("( open-dir ( %d:%.*s %s %s ( ) ) )\n",
+			(int) (d - path), (int) (d - path), path,
+			dtoken(cdepth),
+			dtoken(cdepth+1));
+
+		/* include the / at the end */
+		d++;
+		strbuf_add(&cpath, p, d - p);
+		p = d;
+		cdepth++;
+	}
+
+	return cdepth;
+}
+
+static void dir_changed(int dir, const char* path) {
+	strbuf_reset(&cpath);
+	strbuf_addstr(&cpath, path);
+	if (*path) strbuf_addch(&cpath, '/');
+	cdepth = dir;
+}
+
+static void send_delta_chunk(const char* tok, const void* data, size_t sz) {
+	sendf("( textdelta-chunk ( %s %d:", tok, (int) sz);
+
+	print_ascii(data, sz, MAX_PRINT_LEN);
+
+	if (write_in_full(svnfd, data, sz) != sz) {
+		die_errno("write");
+	}
+
+	sendf(" ) )\n");
+}
+
+#define MAX_DELTA_SIZE (32*1024)
+
+static void send_file(const char* tok, char *data, size_t sz) {
+	struct strbuf dataz = STRBUF_INIT;
+	z_stream z;
+
+	memset(&z, 0, sizeof(z));
+
+	sendf("( apply-textdelta ( %s ( ) ) )\n", tok);
+
+	send_delta_chunk(tok, "SVN\1", 4);
+
+	while (sz > 0) {
+		unsigned char hdr[7*MAX_VARINT_LEN+MAX_INS_LEN], *hp = hdr;
+		unsigned char ins[MAX_INS_LEN], *inp;
+		size_t d = min(MAX_DELTA_SIZE, sz);
+		int ret = Z_OK;
+
+		z.next_in = (unsigned char*) data;
+		z.avail_in = d;
+		strbuf_reset(&dataz);
+
+		deflateInit(&z, Z_DEFAULT_COMPRESSION);
+
+		while (ret == Z_OK) {
+			strbuf_grow(&dataz, dataz.len + MAX_DELTA_SIZE);
+			z.next_out = (unsigned char*) dataz.buf + dataz.len;
+			z.avail_out = MAX_DELTA_SIZE;
+			ret = deflate(&z, Z_FINISH);
+			strbuf_setlen(&dataz, MAX_DELTA_SIZE - z.avail_out);
+		}
+
+		deflateEnd(&z);
+
+		inp = encode_instruction(ins, FROM_NEW, 0, d);
+
+		hp = encode_varint(hp, 0); /* source off */
+		hp = encode_varint(hp, 0); /* source len */
+		hp = encode_varint(hp, d); /* target len */
+		hp = encode_varint(hp, inp - ins + encoded_length(inp - ins)); /* ins compressed len */
+		hp = encode_varint(hp, dataz.len + encoded_length(d)); /* compressed data len */
+		hp = encode_varint(hp, inp - ins); /* ins decompressed len */
+		memcpy(hp, ins, inp - ins); /* instructions */
+		hp += inp - ins;
+		hp = encode_varint(hp, d); /* decompressed data len */
+
+		send_delta_chunk(tok, hdr, hp - hdr);
+		send_delta_chunk(tok, dataz.buf, dataz.len);
+
+		data += d;
+		sz -= d;
+
+	}
+
+	sendf("( textdelta-end ( %s ) )\n", tok);
+
+	strbuf_release(&dataz);
+}
+
+static void change(struct diff_options* op,
+		unsigned omode,
+		unsigned nmode,
+		const unsigned char* osha1,
+		const unsigned char* nsha1,
+		const char* svnpath,
+		unsigned odsubmodule,
+		unsigned ndsubmodule)
+{
+	struct svnref* r = op->format_callback_data;
+	int svnlen = (int) strlen(svnpath);
+	const char *gitpath = r->svn.len ? svnpath + r->svn.len : svnpath;
+	int gitlen = svnpath + svnlen - gitpath;
+	struct cache_entry* ce;
+	struct strbuf buf = STRBUF_INIT;
+	enum object_type type;
+	const char* tok;
+	char* data;
+	int dir;
+	size_t sz;
+
+	if (verbose) fprintf(stderr, "change mode %x/%x sha1 %s/%s path %s\n",
+			omode, nmode, sha1_to_hex(osha1), sha1_to_hex(nsha1), svnpath);
+
+	/* dont care about changed directories */
+	if (!S_ISREG(nmode)) return;
+
+	dir = change_dir(svnpath);
+
+	ce = index_name_exists(svn_index(r), gitpath, gitlen, 0);
+	if (!ce) {
+		/* file exists in git but not in svn */
+		return;
+	}
+
+	/* TODO make this actually use diffcore */
+
+	data = read_sha1_file(nsha1, &type, &sz);
+	if (type != OBJ_BLOB)
+		die("unexpected object type for %s", sha1_to_hex(nsha1));
+
+	if (convert_to_working_tree(gitpath, data, sz, &buf)) {
+		unsigned char sha1[20];
+		free(data);
+		data = strbuf_detach(&buf, &sz);
+
+		if (write_sha1_file(data, sz, "blob", sha1)) {
+			die_errno("blob write");
+		}
+
+		ce = make_cache_entry(0644, sha1, gitpath, 0, 0);
+		add_index_entry(svn_index(r), ce, ADD_CACHE_OK_TO_REPLACE);
+	}
+
+	tok = ftoken();
+	sendf("( open-file ( %d:%s %s %s ( ) ) )\n",
+		svnlen, svnpath, dtoken(dir), tok);
+
+	send_file(tok, data, sz);
+
+	sendf("( close-file ( %s ( ) ) )\n", tok);
+
+	diff_change(op, omode, nmode, osha1, nsha1, gitpath, odsubmodule, ndsubmodule);
+
+	free(data);
+}
+
+static void addremove(struct diff_options* op,
+		int addrm,
+		unsigned mode,
+		const unsigned char* sha1,
+		const char* svnpath,
+		unsigned dsubmodule)
+{
+	static struct strbuf buf = STRBUF_INIT;
+	struct svnref* r = op->format_callback_data;
+	int svnlen = strlen(svnpath);
+	const char *gitpath = r->svn.len ? svnpath + r->svn.len : svnpath;
+	int gitlen = svnpath + svnlen - gitpath;
+
+	if (verbose) fprintf(stderr, "addrm %c mode %x sha1 %s path %s\n",
+			addrm, mode, sha1_to_hex(sha1), svnpath);
+
+	if (addrm == '-' && S_ISDIR(mode)) {
+		strbuf_reset(&buf);
+		strbuf_add(&buf, gitpath, gitlen);
+		if (remove_index_path(svn_index(r), &buf) > 0) {
+			int dir = change_dir(svnpath);
+			sendf("( delete-entry ( %d:%s ( ) %s ) )\n",
+				svnlen, svnpath, dtoken(dir));
+		}
+
+	} else if (addrm == '+' && S_ISDIR(mode)) {
+		int dir = change_dir(svnpath);
+		sendf("( add-dir ( %d:%s %s %s ( ) ) )\n",
+			svnlen, svnpath, dtoken(dir), dtoken(dir+1));
+
+		dir_changed(++dir, svnpath);
+
+	} else if (addrm == '-' && S_ISREG(mode)) {
+		strbuf_reset(&buf);
+		strbuf_add(&buf, gitpath, gitlen);
+		if (remove_index_path(svn_index(r), &buf) > 0) {
+			int dir = change_dir(svnpath);
+			sendf("( delete-entry ( %d:%s ( ) %s ) )\n",
+				svnlen, svnpath, dtoken(dir));
+		}
+
+	} else if (addrm == '+' && S_ISREG(mode)) {
+		struct cache_entry* ce;
+		unsigned char nsha1[20];
+		struct strbuf buf = STRBUF_INIT;
+		enum object_type type;
+		const char* tok;
+		void* data;
+		size_t sz;
+		int dir;
+
+		/* files beginning with .git eg .gitempty,
+		 * .gitattributes, etc are filtered from svn
+		 */
+		const char* p = strrchr(gitpath, '/');
+		p = p ? p+1 : gitpath;
+		if (!prefixcmp(p, ".git")) {
+			return;
+		}
+
+		hashcpy(nsha1, sha1);
+		data = read_sha1_file(nsha1, &type, &sz);
+		if (!data || type != OBJ_BLOB)
+			die("unexpected object type for %s", sha1_to_hex(sha1));
+
+		if (convert_to_working_tree(gitpath, data, sz, &buf)) {
+			free(data);
+			data = strbuf_detach(&buf, &sz);
+
+			if (write_sha1_file(data, sz, "blob", nsha1)) {
+				die_errno("blob write");
+			}
+		}
+
+		ce = make_cache_entry(0644, nsha1, gitpath, 0, 0);
+		add_index_entry(svn_index(r), ce, ADD_CACHE_OK_TO_ADD);
+
+		/* TODO: use diffcore to find copies */
+
+		dir = change_dir(svnpath);
+		tok = ftoken();
+		sendf("( add-file ( %d:%s %s %s ( ) ) )\n",
+			svnlen, svnpath, dtoken(dir), tok);
+
+		send_file(tok, data, sz);
+
+		sendf("( close-file ( %s ( ) ) )\n", tok);
+
+		free(data);
+	}
+
+	diff_addremove(op, addrm, mode, sha1, svnpath, dsubmodule);
+}
+
+static int read_commit_revno(struct strbuf* time) {
+	int64_t n;
+
+	read_success();
+	read_success();
+
+	/* commit-info */
+	if (read_list()) goto err;
+	n = read_number();
+	if (n < 0 || n > INT_MAX) goto err;
+	if (have_optional() && time) {
+		read_string(time);
+		svn_time_to_git(time);
+		read_end();
+	}
+	read_end();
+	if (verbose) fputc('\n', stderr);
+
+	return (int) n;
+
+err:
+	die("commit failed");
+}
+
+/* returns the rev number */
+static int send_commit(struct svnref* r, struct commit* cmt, struct commit* copysrc, const char* log, struct strbuf* time) {
+	struct diff_options op;
+	int dir;
+
+	/* If we are creating a new ref that we have never seen before
+	 * in SVN, then the target is just above the last fetch, as that
+	 * is the last time we checked the branches/tags folder for new
+	 * refs. Otherwise its just above the last time we pushed/pulled
+	 * this ref.
+	 */
+	int tgtrev = (r->svncmt ? parse_svnrev(r->svncmt) : latest_fetch_rev()) + 1;
+
+	sendf("( commit ( %d:%s ) )\n", (int) strlen(log), log);
+	sendf("( target-rev ( %d ) )\n", tgtrev);
+	sendf("( open-root ( ( ) %s ) )\n", dtoken(0));
+
+	read_success();
+	read_success();
+
+	dir = change_dir(r->svn.buf);
+
+	if (!cmt) {
+		sendf("( delete-entry ( %d:%s ( ) %s ) )\n",
+				(int) r->svn.len,
+				r->svn.buf,
+				dtoken(dir));
+	} else {
+		if (copysrc) {
+			struct strbuf path = STRBUF_INIT;
+			parse_svnpath(copysrc, &path);
+
+			if (r->gitobj) {
+				sendf("( delete-entry ( %d:%s ( ) %s ) )\n",
+						(int) r->svn.len,
+						r->svn.buf,
+						dtoken(dir));
+			}
+
+			sendf("( add-dir ( %d:%s %s %s ( %d:%s/%s %d ) ) )\n",
+					(int) r->svn.len,
+					r->svn.buf,
+					dtoken(dir),
+					dtoken(dir+1),
+					(int) (strlen(url) + 1 + path.len),
+					url,
+					path.buf,
+					parse_svnrev(copysrc));
+
+			strbuf_release(&path);
+		} else {
+			/* We never have to create the root */
+			sendf("( %s ( %d:%s %s %s ( ) ) )\n",
+				(!r->gitobj && r->svn.len) ? "add-dir" : "open-dir",
+				(int) r->svn.len,
+				r->svn.buf,
+				dtoken(dir),
+				dtoken(dir+1));
+		}
+
+		dir_changed(++dir, r->svn.buf);
+
+		diff_setup(&op);
+		op.output_format = DIFF_FORMAT_NO_OUTPUT;
+		op.change = &change;
+		op.add_remove = &addremove;
+		op.format_callback_data = r;
+		DIFF_OPT_SET(&op, RECURSIVE);
+		DIFF_OPT_SET(&op, IGNORE_SUBMODULES);
+		DIFF_OPT_SET(&op, TREE_IN_RECURSIVE);
+
+		if (r->svn.len) {
+			strbuf_addch(&r->svn, '/');
+		}
+
+		if (r->parent) {
+			if (diff_tree_sha1(cmt_sha1(r->parent), cmt_sha1(cmt), r->svn.buf, &op))
+				die("diff tree failed");
+		} else {
+			if (diff_root_tree_sha1(cmt_sha1(cmt), r->svn.buf, &op))
+				die("diff tree failed");
+		}
+
+		if (r->svn.len) {
+			strbuf_setlen(&r->svn, r->svn.len - 1);
+		}
+
+		diffcore_std(&op);
+		diff_flush(&op);
+	}
+
+	change_dir("");
+	sendf("( close-dir ( %s ) )\n", dtoken(0));
+	sendf("( close-edit ( ) )\n");
+
+	return read_commit_revno(time);
+}
+
+struct push {
+	struct push* next;
+	struct object* old;
+	struct object* new;
+	struct svnref* ref;
+	struct commit* copysrc;
+};
+
+/* logobj is used for the log message and author, gitcmt is used for the
+ * tree. gitcmt is non NULL on branch mod and creation, NULL on
+ * deletion. logobj will be NULL for branch deletion and branch creation
+ * where gitcmt is for another branch in svn.
+ */
+static void push_commit(struct push* p, struct object* logobj, struct commit* gitcmt) {
+	static struct strbuf buf = STRBUF_INIT;
+	static struct strbuf time = STRBUF_INIT;
+	static struct strbuf logbuf = STRBUF_INIT;
+
+	int rev;
+	unsigned char sha1[20];
+	struct ref_lock* lk;
+	const char* log;
+	struct svnref *r = p->ref;
+	struct author *auth = logobj ? get_object_author(logobj) : defauthor;
+	struct commit *svncmt;
+
+	fprintf(stderr, "push commit %s\n", cmt_to_hex(gitcmt));
+
+	change_connection(0, auth);
+
+	if (!logobj) {
+		strbuf_reset(&logbuf);
+		strbuf_addf(&logbuf, "%s %s\n",
+			r->parent ? "Create" : "Remove",
+			r->svn.buf);
+		log = logbuf.buf;
+
+	} else if (logobj->type == OBJ_COMMIT) {
+		find_commit_subject(((struct commit*) logobj)->buffer, &log);
+
+	} else if (logobj->type == OBJ_TAG) {
+		unsigned long size;
+		enum object_type type;
+		char* data = read_sha1_file(logobj->sha1, &type, &size);
+		find_commit_subject(data, &log);
+		strbuf_reset(&logbuf);
+		strbuf_add(&logbuf, log, parse_signature(log, data + size - log));
+		free(data);
+		log = logbuf.buf;
+
+	} else {
+		die("unexpected type");
+	}
+
+	strbuf_reset(&time);
+	rev = send_commit(r, gitcmt, p->copysrc, log, &time);
+
+	/* If we find any intermediate commits, we die. They will be
+	 * picked up the next time the user does a pull.  If we have
+	 * just created a branch then the svn server will check this for
+	 * us by failing on the add-dir. If we have just replaced a
+	 * branch then we don't really care as the git history for this
+	 * branch wouldn't have referenced those commits anyways (they
+	 * will be picked up on the next fetch though in case they are
+	 * copied on the svn side later).
+	 */
+	if (!p->copysrc && r->svncmt) {
+		check_for_svn_commits(r, parse_svnrev(r->svncmt), rev);
+	}
+
+	/* create the svn object */
+
+	strbuf_reset(&buf);
+	strbuf_addf(&buf, "tree %s\n", sha1_to_hex(idx_sha1(svn_index(r))));
+
+	if (gitcmt || r->svncmt) {
+		strbuf_addf(&buf, "parent %s\n", cmt_to_hex(gitcmt ? gitcmt : r->svncmt));
+	}
+
+	if (r->svncmt) {
+		strbuf_addf(&buf, "parent %s\n", cmt_to_hex(r->svncmt));
+	}
+
+	strbuf_addf(&buf, "author %s <%s> %s +0000\n", auth->name, auth->mail, time.buf);
+	strbuf_addf(&buf, "committer %s <%s> %s +0000\n", auth->name, auth->mail, time.buf);
+	strbuf_addf(&buf, "revision %d\n", rev);
+	strbuf_addf(&buf, "path %s\n", r->svn.buf);
+	strbuf_addch(&buf, '\n');
+
+	if (write_sha1_file(buf.buf, buf.len, "commit", sha1))
+		die("failed to create svn commit");
+
+	svncmt = lookup_commit(sha1);
+	if (!svncmt || parse_commit(svncmt))
+		die("failed to parse created svn commit");
+
+	/* update the ref */
+
+	lk = lock_ref_sha1(r->ref.buf + strlen("refs/"), cmt_sha1(r->svncmt));
+	if (!lk || write_ref_sha1(lk, cmt_sha1(svncmt), "svn-push"))
+		die("failed to grab ref lock for %s", r->ref.buf);
+
+	/* update the remote */
+
+	if (leave_remote) {
+		/* we don't update 'remotes' that are not stored in a
+		 * remote directory (e.g. refs/tags/ or refs/heads/ when
+		 * svn.remote is not set)
+		 */
+
+	} else if (gitcmt) {
+		lk = lock_ref_sha1(r->remote.buf + strlen("refs/"), r->gitobj ? r->gitobj->sha1 : null_sha1);
+		if (!lk || write_ref_sha1(lk, cmt_sha1(gitcmt), "svn-push"))
+			die("failed to update ref %s", r->remote.buf);
+
+	} else if (r->gitobj) {
+		if (delete_ref(r->remote.buf, r->gitobj->sha1, 0))
+			die("failed to delete ref %s", r->remote.buf);
+
+	}
+
+	r->svncmt = svncmt;
+	r->parent = gitcmt;
+	p->copysrc = NULL;
+
+	if (logobj) {
+		r->gitobj = logobj;
+	} else if (gitcmt) {
+		r->gitobj = &gitcmt->object;
+	} else {
+		r->gitobj = NULL;
+	}
+
+	if (pause_between_commits) {
+		int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+		struct sockaddr_in addr;
+		addr.sin_addr.s_addr = ntohl(INADDR_LOOPBACK);
+		addr.sin_port = htons(pause_between_commits);
+		bind(fd, (struct sockaddr*) &addr, sizeof(addr));
+		listen(fd, SOMAXCONN);
+		close(accept(fd, NULL, NULL));
+		close(accept(fd, NULL, NULL));
+		close(fd);
+		pause_between_commits = 0;
+	}
+}
+
+static int has_parent(struct commit *c, struct commit *parent) {
+	struct commit_list *p = c->parents;
+
+	/* null parents are only parents of root commits */
+	if (!parent) {
+		return p == NULL;
+	}
+
+	while (p) {
+		if (p->item == parent) {
+			return 1;
+		}
+		p = p->next;
+	}
+	return 0;
+}
+
+static void do_push(struct push* p) {
+	struct svnref* r = p->ref;
+	struct commit* cmt;
+	struct rev_info walk;
+
+	if (p->old != r->gitobj) {
+		die("non fast-forward for %s", r->ref.buf);
+	}
+
+	if (p->old && !p->new) {
+		/* deleting a ref */
+		push_commit(p, NULL, NULL);
+
+	} else if (p->new != p->old) {
+		/* add/modify/replace a ref */
+		int have_commits = 0;
+		struct commit *new = (struct commit*) deref_tag(p->new, NULL, 0);
+
+		if (!new || new->object.type != OBJ_COMMIT)
+			die("invalid tag %s", sha1_to_hex(p->new->sha1));
+
+		init_revisions(&walk, NULL);
+		add_pending_object(&walk, &new->object, "to");
+		walk.reverse = 1;
+
+		if (p->old) {
+			struct object *old = deref_tag(p->old, NULL, 0);
+			if (!old || old->type != OBJ_COMMIT)
+				die("invalid tag %s", sha1_to_hex(p->old->sha1));
+
+			old->flags |= UNINTERESTING;
+			add_pending_object(&walk, old, "from");
+		}
+
+		if (p->copysrc) {
+			struct object* obj = &svn_commit(p->copysrc)->object;
+			obj->flags |= UNINTERESTING;
+			add_pending_object(&walk, obj, "from");
+			/* sets r->parent for the has_parent check below */
+			checkout_svncmt(r, p->copysrc);
+		}
+
+		if (prepare_revision_walk(&walk))
+			die("prepare rev walk failed");
+
+		while ((cmt = get_revision(&walk)) != NULL) {
+			/* The revwalk gives us all paths that go from
+			 * copysrc or p->old to cmt. We can work with
+			 * any of these so pick one arbitrarily by using
+			 * r->parent. r->parent is set by
+			 * checkout_svncmt above for add/replace.
+			 */
+			if (!has_parent(cmt, r->parent)) continue;
+
+			if (cmt == new) {
+				/* use the tag object in p->new for the
+				 * log message */
+				push_commit(p, p->new, cmt);
+			} else {
+				push_commit(p, &cmt->object, cmt);
+			}
+
+			have_commits = 1;
+		}
+
+		/* if there were no commits we have to force through a
+		 * commit to create/replace the branch/tag in svn. */
+
+		if (r->gitobj != p->new) {
+			/* If we have an annotated tag, we can use that
+			 * for a log statement. Otherwise we have to
+			 * create a message.
+			 */
+			struct object *logobj = (p->new != &new->object) ? p->new : NULL;
+			push_commit(p, logobj, new);
+		}
+
+		reset_revision_walk();
+	}
+}
+
+static void new_push(struct push** list, const char* ref, const char* oldref, const char* newref) {
+	unsigned char old[20], new[20];
+	struct push *p = xcalloc(1, sizeof(*p));
+
+	if (get_sha1(oldref, old))
+		die("invalid ref %s", oldref);
+
+	if (get_sha1(newref, new))
+		die("invalid ref %s", newref);
+
+	if (!is_null_sha1(old)) {
+		p->old = parse_object(old);
+		if (!p->old)
+			die("invalid ref %s", oldref);
+	}
+
+	if (!is_null_sha1(new)) {
+		p->new = parse_object(new);
+		if (!p->new)
+			die("invalid ref %s", newref);
+	}
+
+	p->ref = find_svnref_by_refname(ref);
+	p->next = *list;
+	*list = p;
+}
+
+int cmd_svn_push(int argc, const char **argv, const char *prefix) {
+	struct push *updates = NULL, *p;
+	char buf[256];
+
+	git_config(&config, NULL);
+
+	argc = parse_options(argc, argv, prefix, builtin_svn_push_options,
+			builtin_svn_push_usage, 0);
+
+	setup_globals();
+
+	/* get the list of references to push */
+	if (push_from_stdin) {
+		if (argc)
+			usage_msg_opt( _("Too many arguments."),
+				builtin_svn_push_usage, builtin_svn_push_options);
+
+
+		while (fgets(buf, sizeof(buf), stdin)) {
+			size_t sz = strlen(buf);
+			if (sz <= 82) continue;
+
+			if (buf[sz-1] == '\n') {
+				buf[--sz] = '\0';
+			}
+			if (buf[sz-1] == '\r') {
+				buf[--sz] = '\0';
+			}
+
+			buf[40] = '\0';
+			buf[81] = '\0';
+			new_push(&updates, &buf[82], &buf[0], &buf[41]);
+		}
+	} else {
+		if (argc != 3)
+			usage_msg_opt(argc > 3 ? _("Too many arguments.") : _("Too few arguments"),
+				builtin_svn_push_usage, builtin_svn_push_options);
+
+		new_push(&updates, argv[0], argv[1], argv[2]);
+	}
+
+	/* modify/delete refs */
+	for (p = updates; p != NULL; p = p->next) {
+		if (p->new) {
+			p->copysrc = find_copy_source(p->ref, p->new);
+		}
+		if (p->old && (!p->copysrc || deref_tag(p->old, NULL, 0) == &svn_commit(p->copysrc)->object)) {
+			p->copysrc = NULL;
+			do_push(p);
+			p->ref = NULL;
+		}
+	}
+
+	/* add/replace refs - do this last so we can find copy bases in
+	 * the modified refs. Note if two added refs have a common
+	 * commit branching off of svn then those common commits will be
+	 * assigned to whichever ref comes first (i.e. unspecified). */
+	for (p = updates; p != NULL; p = p->next) {
+		if (p->ref) {
+			p->copysrc = find_copy_source(p->ref, p->new);
+			do_push(p);
+		}
+	}
+
+	return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index ec64cac..64f6d6d 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -120,6 +120,8 @@ git-status                              mainporcelain common
 git-stripspace                          purehelpers
 git-submodule                           mainporcelain
 git-svn                                 foreignscminterface
+git-svn-fetch				foreignscminterface
+git-svn-push				foreignscminterface
 git-symbolic-ref                        plumbingmanipulators
 git-tag                                 mainporcelain common
 git-tar-tree                            plumbinginterrogators	deprecated
diff --git a/git.c b/git.c
index 8788b32..c861b5a 100644
--- a/git.c
+++ b/git.c
@@ -425,6 +425,8 @@ static void handle_internal_command(int argc, const char **argv)
 		{ "stage", cmd_add, RUN_SETUP | NEED_WORK_TREE },
 		{ "status", cmd_status, RUN_SETUP | NEED_WORK_TREE },
 		{ "stripspace", cmd_stripspace },
+		{ "svn-fetch", cmd_svn_fetch, RUN_SETUP },
+		{ "svn-push", cmd_svn_push, RUN_SETUP },
 		{ "symbolic-ref", cmd_symbolic_ref, RUN_SETUP },
 		{ "tag", cmd_tag, RUN_SETUP },
 		{ "tar-tree", cmd_tar_tree },
diff --git a/svn-sync.sh b/svn-sync.sh
new file mode 100755
index 0000000..af8a66e
--- /dev/null
+++ b/svn-sync.sh
@@ -0,0 +1,150 @@
+#!/bin/sh
+
+make
+
+rm -rf svn
+mkdir svn
+cd svn
+
+#Setup the test database
+svnadmin create db
+
+cat > db/conf/passwd <<!
+[users]
+user = pass
+!
+
+cat > db/conf/svnserve.conf <<!
+[general]
+anon-access = none
+auth-access = write
+password-db = passwd
+realm = Test Repository
+!
+
+# Setup the subversion repo
+killall svnserve
+killall lt-svnserve
+svnserve --daemon --log-file svnlog --root db
+
+svn co --username user --password pass svn://localhost co
+cd co
+svn mkdir trunk
+cd trunk
+cat > file.txt <<!
+Some file contents
+Some more
+
+!
+svn add file.txt
+svn ci -m 'add file.txt'
+
+svn mkdir --parents a/b/c/d/e/f
+cat > a/b/c/d/e/f/deep.txt <<!
+Some deep contents
+.....
+!
+svn add a/b/c/d/e/f/deep.txt
+svn ci -m 'add deep.txt'
+svn up
+
+svn rm file.txt
+svn ci -m 'remove file.txt'
+svn up
+
+svn mkdir svn://localhost/tags -m 'create tags folder'
+
+svn rm a/b
+svn ci -m 'remove folder a/b'
+svn up
+
+cat > a/foo.txt <<!
+Some other contents for foo.txt
+!
+svn add a/foo.txt
+svn ci -m 'add foo.txt'
+svn up
+
+svn mv a b
+svn ci -m 'move folder'
+svn up
+
+svn mv b/foo.txt b/foo2.txt
+svn ci -m 'move file'
+svn up
+
+cd ..
+#echo "some new text" >> trunk/b/foo2.txt
+#echo "some new file" > trunk/b/foo.txt
+#svn add trunk/b/foo.txt
+#svn mkdir fake
+#svn ci -m 'git commit add/mod'
+#exit
+
+svn mkdir branches
+svn ci -m 'create branches folder'
+svn up
+
+svn cp svn://localhost/trunk svn://localhost/branches/foobranch -m 'create branch'
+svn cp svn://localhost/trunk@4 svn://localhost/tags/footag -m 'create tag'
+
+cd ..
+git init
+
+cat > .git/svn-authors <<!
+# Some comment
+
+user:pass = James M <james@example.com>
+!
+
+../git-svn-fetch -v -r 3 -t trunk -b branches -T tags --user user --pass pass svn://localhost
+../git-svn-fetch -v -t trunk -b branches -T tags --user user --pass pass svn://localhost
+
+git config user.name 'James M'
+git config user.email 'james@example.com'
+
+
+git checkout -b master svn/master
+
+echo "some new text" >> b/foo2.txt
+echo "some new file" > b/foo.txt
+git add b/foo.txt b/foo2.txt
+git commit -m 'git commit add/mod'
+
+mkdir -p b/c
+echo "some more text" >> b/c/foo.txt
+git add b/c/foo.txt
+git commit -m 'git commit 2'
+
+git rm -rf b
+git commit -m 'some removals'
+
+
+git checkout -b foobranch svn/foobranch
+
+echo "some branch file" >> b/foo2.txt
+git add b/foo2.txt
+git commit -m 'git branch commit'
+
+../git svn-push -v -t trunk -b branches -T tags svn://localhost heads/master svn/master master
+../git svn-push -v -t trunk -b branches -T tags svn://localhost heads/foobranch svn/foobranch foobranch
+
+cat > .git/hooks/pre-receive <<!
+#!/bin/sh
+exec $PWD/../git-svn-push -v -t trunk -b branches -T tags --pre-receive svn://localhost
+!
+
+chmod +x .git/hooks/pre-receive
+git config receive.denyCurrentBranch ignore
+
+../git clone -- . gitco
+cd gitco
+
+git config user.name 'James M'
+git config user.email 'james@example.com'
+
+echo "some mod" >> b/foo2.txt
+../../git add b/foo2.txt
+../../git commit -m 'git clone commit'
+../../git push
+
-- 
1.7.11.3

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [RFC 2/2] add tests for git svn-fetch|push
  2012-08-18 17:39 [RFC 0/2] svn-fetch|push - an alternate approach James R. McKaskill
  2012-08-18 17:39 ` [RFC 1/2] add svn-fetch/push James R. McKaskill
@ 2012-08-18 17:39 ` James R. McKaskill
  2012-08-19 10:21 ` [RFC 0/2] svn-fetch|push - an alternate approach Marco Schulze
  2 siblings, 0 replies; 6+ messages in thread
From: James R. McKaskill @ 2012-08-18 17:39 UTC (permalink / raw)
  To: git; +Cc: James R. McKaskill

From: "James R. McKaskill" <james@foobar.co.nz>


Signed-off-by: James R. McKaskill <james@foobar.co.nz>
---
 t/lib-git-svn-fetch.sh          | 131 +++++++++++
 t/t9050-git-svn-fetch.sh        |  85 +++++++
 t/t9051-git-svn-fetch-branch.sh | 245 ++++++++++++++++++++
 t/t9052-git-svn-push.sh         | 140 ++++++++++++
 t/t9053-git-svn-push-branch.sh  | 478 ++++++++++++++++++++++++++++++++++++++++
 5 files changed, 1079 insertions(+)
 create mode 100644 t/lib-git-svn-fetch.sh
 create mode 100755 t/t9050-git-svn-fetch.sh
 create mode 100755 t/t9051-git-svn-fetch-branch.sh
 create mode 100755 t/t9052-git-svn-push.sh
 create mode 100755 t/t9053-git-svn-push-branch.sh

diff --git a/t/lib-git-svn-fetch.sh b/t/lib-git-svn-fetch.sh
new file mode 100644
index 0000000..16326a9
--- /dev/null
+++ b/t/lib-git-svn-fetch.sh
@@ -0,0 +1,131 @@
+#!/bin/sh
+
+. ./test-lib.sh
+
+if test -z "$SVNSERVE_PORT"
+then
+	skip_all='skipping svn-(fetch|push) test. (set $SVNSERVE_PORT to enable)'
+	test_done
+fi
+
+svn --version | grep "version 1.7" &> /dev/null
+if [ ! $? -eq 0 ]
+then
+	skip_all='skipping svn-(fetch|push) test. (need svn 1.7 or newer)'
+	test_done
+fi
+
+svnrepo=$PWD/svnrepo
+svnconf=$PWD/svnconf
+svnurl="svn://localhost:$SVNSERVE_PORT"
+null_sha1=0000000000000000000000000000000000000000
+
+# We need this, because we should pass empty configuration directory to
+# the 'svn commit' to avoid automated property changes and other stuff
+# that could be set from user's configuration files in ~/.subversion.
+svn_cmd () {
+	[ -d "$svnconf" ] || mkdir "$svnconf"
+	cat > "$svnconf/servers" <<!
+[global]
+store-plaintext-passwords = yes
+!
+	orig_svncmd="$1"; shift
+	if [ -z "$orig_svncmd" ]; then
+		svn
+		return
+	fi
+	echo svn $orig_svncmd $@
+	svn "$orig_svncmd" --username committer --password pass --no-auth-cache --non-interactive --config-dir "$svnconf" "$@"
+}
+
+function test_file() {
+	file_contents="`cat $1`"
+	test_contents="$2"
+	test "$file_contents" == "$test_contents"
+}
+
+function svn_date() {
+	revision="$1"
+	directory="$2"
+	svn_cmd log -r "$revision" --xml -l 1 "$directory" | grep "<date>" | sed -re 's#^<date>([^\.Z]*)\.[0-9]+Z</date>#\1Z#g'
+}
+
+function test_git_subject() {
+	commit="$1"
+	subject="$2"
+	commit_subject="`git log -1 --pretty=format:%s $commit`"
+	echo test_git_subject "$commit_subject" "$subject"
+	test "$commit_subject" == "$subject"
+}
+
+function test_git_author() {
+	commit="$1"
+	author="$2"
+	commit_author="`git log -1 --pretty=format:'%an <%ae>' $commit`"
+	echo test_git_author "$commit_author" "$author"
+	test "$commit_author" == "$author"
+}
+
+function test_git_date() {
+	commit="$1"
+	date="$2"
+	commit_date="`git log -1 --pretty=format:%ai $commit | sed -re 's#^([^ ]*) ([^ ]*) \+0000$#\1T\2Z#g'`"
+	echo test_git_date "$commit_date" "$date"
+	test "$commit_date" == "$date"
+}
+
+function test_svn_subject() {
+	subject="$1"
+	revision=`test -n "$2" && echo "-r $2"`
+	commit_subject="`svn log -l 1 --xml $revision | grep '<msg>' | sed -re 's#<msg>(.*)#\1#g' | sed -re 's#(.*)</msg>#\1#g'`"
+	echo test_svn_subject "$commit_subject" "$subject"
+	test "$commit_subject" == "$subject"
+}
+
+function test_svn_author() {
+	author="$1"
+	revision=`test -n "$2" && echo "-r $2"`
+	commit_author="`svn log -l 1 --xml $revision | grep '<author>' | sed -re 's#<author>(.*)</author>#\1#g'`"
+	echo test_svn_author "$commit_author" "$author"
+	test "$commit_author" == "$author"
+}
+
+show_ref() {
+	(git show-ref --head $1 | head -n 1 | cut -d ' ' -f 1) || echo $1
+}
+
+show_tag() {
+	show_ref refs/tags/$1 | git cat-file --batch | grep object | cut -f 2 -d ' '
+}
+
+merge_base() {
+	git merge-base `show_ref $1` `show_ref $2`
+}
+
+test_expect_success 'start svnserve' '
+	killall svnserve &> /dev/null
+	killall lt-svnserve &> /dev/null
+	rm -rf "$svnrepo" &&
+	mkdir -p "$svnrepo" &&
+	svnadmin create "$svnrepo" &&
+	cat > "$svnrepo/conf/svnserve.conf" <<!
+[general]
+auth-access = write
+password-db = passwd
+!
+	cat > "$svnrepo/conf/passwd" <<!
+[users]
+committer = pass
+!
+	cat > .git/svn-authors <<!
+committer:pass = C O Mitter <committer@example.com>
+!
+	svnserve --daemon \
+		--listen-port $SVNSERVE_PORT \
+		--root "$svnrepo" \
+		--listen-host localhost &&
+	git config svn.user committer &&
+	git config svn.url $svnurl &&
+	git config svn.remote svn &&
+	svn_cmd co $svnurl svnco
+'
diff --git a/t/t9050-git-svn-fetch.sh b/t/t9050-git-svn-fetch.sh
new file mode 100755
index 0000000..7ac3643
--- /dev/null
+++ b/t/t9050-git-svn-fetch.sh
@@ -0,0 +1,85 @@
+#!/bin/sh
+
+test_description='git svn-fetch non trunk'
+. ./lib-git-svn-fetch.sh
+
+test_expect_success 'fetch empty' '
+	git svn-fetch -v &&
+	test_must_fail test -x .git/refs/svn/latest &&
+	test_must_fail git checkout svn/master &&
+	test_must_fail git checkout svn/trunk
+'
+
+test_expect_success 'init repo' '
+	cd svnco &&
+	svn_cmd mkdir empty-dir &&
+	echo "some contents" > file.txt &&
+	svn_cmd add file.txt &&
+	svn_cmd ci -m "some commit" &&
+	cd ..
+'
+
+date=`svn_date HEAD svnco`
+
+test_expect_success 'fetch repo' '
+	git svn-fetch -v &&
+	test $(GIT_SVN_FETCH_REPORT_LATEST=1 git svn-fetch) -eq 1 &&
+	git checkout svn/master &&
+	test -d empty-dir &&
+	test -e empty-dir/.gitempty &&
+	test_file file.txt "some contents" &&
+	test_git_subject HEAD "some commit" &&
+	test_git_author HEAD "C O Mitter <committer@example.com>" &&
+	test_git_date HEAD $date
+'
+
+test_expect_success 'auto crlf' '
+	cd svnco &&
+	echo "666f6f0d0a6261720d0a" | xxd -r -p > crlf.txt &&
+	svn_cmd add crlf.txt &&
+	svn_cmd ci -m "crlf" &&
+	cd .. &&
+	echo "* text=auto" > .git/info/attributes &&
+	git config svn.eol crlf &&
+	git config core.eol lf &&
+	git svn-fetch -v &&
+	git checkout svn/master &&
+	echo "666f6f0a6261720a" | xxd -r -p > crlf_test.txt &&
+	test "$(cat crlf.txt)" = "$(cat crlf_test.txt)"
+'
+
+test_expect_success 'move file' '
+	cd svnco &&
+	seq 1 1000 > somefile &&
+	svn_cmd add somefile &&
+	svn_cmd ci -m "adding some file" &&
+	svn_cmd mv somefile somefile2 &&
+	svn_cmd ci -m "moving file" &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout -f svn/master &&
+	cmp somefile2 svnco/somefile2
+'
+
+test_expect_success 'move folder' '
+	cd svnco &&
+	svn_cmd mkdir folder &&
+	cd folder &&
+	seq 1 1000 > file1 &&
+	seq 1000 2000 > file2 &&
+	seq 3000 50000 > file3 &&
+	svn_cmd add file1 file2 file3 &&
+	cd .. &&
+	svn_cmd ci -m "add some folder" &&
+	svn_cmd mv folder folder2 &&
+	svn_cmd ci -m "move folder" &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout -f svn/master &&
+	cmp folder2/file1 svnco/folder2/file1 &&
+	cmp folder2/file2 svnco/folder2/file2 &&
+	cmp folder2/file3 svnco/folder2/file3
+'
+
+test_done
+
diff --git a/t/t9051-git-svn-fetch-branch.sh b/t/t9051-git-svn-fetch-branch.sh
new file mode 100755
index 0000000..277dcd3
--- /dev/null
+++ b/t/t9051-git-svn-fetch-branch.sh
@@ -0,0 +1,245 @@
+#!/bin/sh
+
+test_description='git svn-fetch branch'
+. ./lib-git-svn-fetch.sh
+
+test_expect_success 'setup branches' '
+	cd svnco &&
+	svn_cmd mkdir Trunk &&
+	svn_cmd mkdir Branches &&
+	svn_cmd mkdir Tags &&
+	touch Trunk/file.txt &&
+	svn_cmd add Trunk/file.txt &&
+	svn_cmd ci -m "init" &&
+	svn_cmd up &&
+	echo "other" >> Trunk/file.txt &&
+	svn_cmd ci -m "trunk file" &&
+	svn_cmd up &&
+	cd .. &&
+	git config svn.trunk Trunk &&
+	git config svn.branches Branches &&
+	git config svn.tags Tags &&
+	git config svn.trunkref trunk &&
+	git svn-fetch -v
+'
+
+test_expect_success 'copied branch' '
+	cd svnco &&
+	svn_cmd copy Trunk Branches/Branch &&
+	svn_cmd ci -m "create branch" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	test `show_ref svn/trunk` == `show_ref svn/Branch`
+'
+
+test_expect_success 'copied and edited branch' '
+	cd svnco &&
+	svn_cmd copy Trunk Branches/CopiedBranch &&
+	echo "more" >> Branches/CopiedBranch/file2.txt &&
+	svn_cmd add Branches/CopiedBranch/file2.txt &&
+	svn_cmd ci -m "create copied branch" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout svn/CopiedBranch &&
+	test_file file.txt "other" &&
+	test_file file2.txt "more" &&
+	test_git_subject HEAD "create copied branch" &&
+	test_git_subject HEAD~1 "trunk file" &&
+	test_git_subject HEAD~2 "init" &&
+	test `git log --pretty=oneline svn/trunk..svn/CopiedBranch | wc -l` -eq 1 &&
+	merge_base svn/trunk svn/CopiedBranch
+'
+
+test_expect_success 'edited and copied branch' '
+	cd svnco &&
+	echo "more" >> Trunk/file2.txt &&
+	svn_cmd add Trunk/file2.txt &&
+	svn_cmd copy Trunk Branches/EditCopyBranch &&
+	svn_cmd ci -m "create edit copy branch" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout svn/EditCopyBranch &&
+	test_file file.txt "other" &&
+	test_file file2.txt "more" &&
+	test_git_subject HEAD "create edit copy branch" &&
+	test `show_ref svn/trunk` == `show_ref svn/EditCopyBranch`
+'
+
+# the copy commits shouldn't create git commits
+
+test_expect_success 'copy, copy, copy' '
+	cd svnco &&
+	svn_cmd copy Trunk Branches/FastCopy2 &&
+	svn_cmd copy Branches/FastCopy2 Branches/FastCopy1 &&
+	svn_cmd copy Branches/FastCopy2 Branches/FastCopy3 &&
+	svn_cmd ci -m "fast copy" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	test `show_ref svn/FastCopy1` == `show_ref svn/FastCopy2` &&
+	test `show_ref svn/FastCopy1` == `show_ref svn/FastCopy3` &&
+	test `show_ref svn/FastCopy1` == `show_ref svn/trunk` &&
+	test_must_fail test_git_subject svn/FastCopy1 "fast copy"
+'
+
+# 'edit copy delete 1' shouldn't create a git commit
+
+test_expect_success 'edit, copy, and delete' '
+	cd svnco &&
+	svn_cmd copy Trunk Branches/EditCopyDelete &&
+	svn_cmd ci -m "edit copy delete 1" &&
+	svn_cmd up &&
+	echo "edit copy delete" >> Branches/EditCopyDelete/file3.txt &&
+	svn_cmd add Branches/EditCopyDelete/file3.txt &&
+	svn_cmd copy Branches/EditCopyDelete Branches/EditCopyDelete2 &&
+	svn_cmd rm --force Branches/EditCopyDelete &&
+	svn_cmd ci -m "edit copy delete 2" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	test_must_fail git checkout svn/EditCopyDelete &&
+	git checkout svn/EditCopyDelete2 &&
+	test_git_subject HEAD "edit copy delete 2" &&
+	test_must_fail test_git_subject HEAD~1 "edit copy delete 1" &&
+	test_file file3.txt "edit copy delete"
+'
+
+test_expect_success 'copy, edit, copy, and delete' '
+	cd svnco &&
+	svn_cmd copy Trunk Branches/CopyEditCopyDelete &&
+	echo "copy edit copy delete" >> Branches/CopyEditCopyDelete/file4.txt &&
+	svn_cmd add Branches/CopyEditCopyDelete/file4.txt &&
+	svn_cmd copy Branches/CopyEditCopyDelete Branches/CopyEditCopyDelete2 &&
+	svn_cmd rm --force Branches/CopyEditCopyDelete &&
+	svn_cmd ci -m "copy edit copy delete" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	test_must_fail git checkout svn/CopyEditCopyDelete &&
+	git checkout svn/CopyEditCopyDelete2 &&
+	test_git_subject HEAD "copy edit copy delete" &&
+	test_file file4.txt "copy edit copy delete"
+'
+
+test_expect_success 'non copied branch' '
+	cd svnco &&
+	svn_cmd mkdir Branches/NonCopiedBranch &&
+	echo "non copied" >> Branches/NonCopiedBranch/file.txt &&
+	svn_cmd add Branches/NonCopiedBranch/file.txt &&
+	svn_cmd ci -m "create non-copied branch" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout svn/NonCopiedBranch &&
+	test_file file.txt "non copied" &&
+	test ! -e file2.txt &&
+	test_git_subject HEAD "create non-copied branch" &&
+	test `git log --pretty=oneline | wc -l` -eq 1 &&
+	test_must_fail merge_base svn/trunk svn/NonCopiedBranch
+'
+
+test_expect_success 'removed branch' '
+	cd svnco &&
+	svn_cmd copy Trunk Branches/RemovedBranch &&
+	svn_cmd ci -m "create branch" &&
+	cd .. &&
+	git svn-fetch -v &&
+	test `show_ref svn/RemovedBranch` == `show_ref svn/trunk` &&
+	rev=$(GIT_SVN_FETCH_REPORT_LATEST=1 git svn-fetch) &&
+	cd svnco &&
+	svn_cmd rm Branches/RemovedBranch &&
+	svn_cmd ci -m "remove branch" &&
+	cd .. &&
+	git svn-fetch -v &&
+	test_must_fail git checkout svn/RemovedBranch &&
+	cd svnco &&
+	echo $rev &&
+	svn_cmd copy Branches/RemovedBranch@$rev Branches/RemovedBranch2 &&
+	svn_cmd ci -m "copy branch" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	test `show_ref svn/RemovedBranch2` == `show_ref svn/trunk` &&
+	git checkout svn/RemovedBranch2 &&
+	cd svnco &&
+	svn_cmd copy Trunk Branches/RemovedBranch &&
+	echo "foo" > Branches/RemovedBranch/newfile.txt &&
+	svn_cmd add Branches/RemovedBranch/newfile.txt &&
+	svn_cmd ci -m "create branch again" &&
+	svn_cmd up &&
+	svn_cmd rm Branches/RemovedBranch2 &&
+	svn_cmd copy Branches/RemovedBranch@$rev Branches/RemovedBranch2 &&
+	svn_cmd ci -m "copy branch again" &&
+	svn_cmd up &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout svn/RemovedBranch &&
+	git checkout svn/RemovedBranch2 &&
+	test `show_ref svn/RemovedBranch` != `show_ref svn/trunk` &&
+	test `show_ref svn/RemovedBranch2` == `show_ref svn/trunk`
+'
+
+test_expect_success 'move branch' '
+	cd svnco &&
+	svn_cmd copy Trunk Branches/MovedBranch &&
+	svn_cmd ci -m "create branch" &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout svn/MovedBranch &&
+	cd svnco &&
+	svn_cmd mv Branches/MovedBranch Branches/MovedBranch2 &&
+	svn_cmd ci -m "move branch" &&
+	cd .. &&
+	git svn-fetch -v &&
+	test_must_fail git checkout svn/MovedBranch &&
+	git checkout svn/MovedBranch2 &&
+	test `show_ref svn/MovedBranch2` == `show_ref svn/trunk`
+'
+
+test_expect_success 'tag' '
+	cd svnco &&
+	svn_cmd copy Trunk Tags/Tag &&
+	svn_cmd ci -m "create tag" &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout svn/Tag &&
+	test `show_tag svn/Tag` == `show_ref svn/trunk` &&
+	rev=$(GIT_SVN_FETCH_REPORT_LATEST=1 git svn-fetch) &&
+	cd svnco &&
+	svn_cmd rm Tags/Tag &&
+	svn_cmd ci -m "remove tag" &&
+	cd .. &&
+	git svn-fetch -v &&
+	test_must_fail git checkout svn/Tag &&
+	cd svnco &&
+	svn_cmd copy Tags/Tag@$rev Branches/CopiedTag &&
+	svn_cmd ci -m "copy tag" &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout svn/CopiedTag &&
+	test `show_ref svn/trunk` == `show_ref refs/remotes/svn/CopiedTag` &&
+	cd svnco &&
+	svn_cmd copy Branches/CopiedTag Tags/Tag &&
+	svn_cmd ci -m "create tag again" &&
+	cd .. &&
+	git svn-fetch -v &&
+	git checkout svn/Tag &&
+	test `show_tag svn/Tag` == `show_ref svn/trunk` &&
+	cd svnco &&
+	svn_cmd copy Tags/Tag Tags/Tag2 &&
+	svn_cmd ci -m "create 2nd tag" &&
+	svn_cmd rm Tags/Tag &&
+	svn_cmd copy Tags/Tag2 Tags/Tag &&
+	svn_cmd ci -m "recreate tag from 2nd" &&
+	cd .. &&
+	git svn-fetch -v &&
+	test `show_tag svn/Tag` == `show_tag svn/Tag2` &&
+	test `show_tag svn/Tag` == `show_ref svn/trunk`
+
+'
+
+test_done
+
diff --git a/t/t9052-git-svn-push.sh b/t/t9052-git-svn-push.sh
new file mode 100755
index 0000000..1a8cf1e
--- /dev/null
+++ b/t/t9052-git-svn-push.sh
@@ -0,0 +1,140 @@
+#!/bin/sh
+
+test_description='git svn-push'
+. ./lib-git-svn-fetch.sh
+
+test_expect_success 'init push' '
+	echo "foo" > file.txt &&
+	git add file.txt &&
+	git commit -a -m "initial commit" &&
+	git svn-push -v refs/remotes/svn/master $null_sha1 master &&
+	cd svnco &&
+	svn_cmd up &&
+	test_svn_subject "initial commit" &&
+	test_svn_author committer &&
+	test_file file.txt "foo" &&
+	cd ..
+'
+
+test_expect_success 'multiple commits' '
+	echo "bar" >> file.txt &&
+	git commit -a -m "second commit" &&
+	mkdir a &&
+	echo "fefifofum" >> a/test &&
+	git add a/test &&
+	git commit -a -m "third commit" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	svn_cmd up -r 2 &&
+	test_svn_subject "second commit" &&
+	test_svn_author committer &&
+	echo foo > file_test.txt &&
+	echo bar >> file_test.txt &&
+	test_file file.txt "$(cat file_test.txt)" &&
+	test ! -e a &&
+	svn_cmd up -r 3 &&
+	test_svn_subject "third commit" &&
+	test_svn_author committer &&
+	test_file a/test "fefifofum" &&
+	cd ..
+'
+
+test_expect_success 'remove git empty directories' '
+	mkdir -p b/c/d &&
+	touch b/c/d/foo.txt &&
+	git add b/c/d/foo.txt &&
+	git commit -a -m "add dir" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	svn_cmd up &&
+	test -e b/c/d/foo.txt &&
+	cd .. &&
+	rm -rf b &&
+	git commit -a -m "rm dir" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	svn_cmd up &&
+	test ! -e b &&
+	cd ..
+'
+
+test_expect_success 'remove file' '
+	touch foo.txt &&
+	git add foo.txt &&
+	git commit -a -m "add file" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	svn_cmd up &&
+	test -e foo.txt &&
+	cd .. &&
+	rm foo.txt &&
+	git commit -a -m "rm file" &&
+	git svn-push -v svn/master svn/master master
+	cd svnco &&
+	svn_cmd up &&
+	test ! -e foo.txt &&
+	cd ..
+'
+
+test_expect_success 'remove svn empty directories' '
+	cd svnco &&
+	svn_cmd mkdir empty &&
+	svn_cmd commit -m "make empty" &&
+	cd .. &&
+	git svn-fetch -v &&
+	git reset --hard svn/master &&
+	test -e empty/.gitempty &&
+	rm empty/.gitempty &&
+	git commit -a -m "remove empty" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	test "$(git clean -n -d | grep empty)" = "Would remove empty/" &&
+	cd ..
+'
+
+test_expect_success '.git files' '
+	mkdir h &&
+	touch h/.githidden &&
+	git add h/.githidden &&
+	git commit -a -m "add h/.githidden" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	svn_cmd up &&
+	test -e h &&
+	test ! -e h/.githidden &&
+	cd ..
+'
+
+test_expect_success 'modify file' '
+	echo "foo" > file.txt &&
+	git add file.txt &&
+	git commit -a -m "edit1" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	svn_cmd up &&
+	test_svn_subject "edit1" &&
+	test_file file.txt "foo" &&
+	cd .. &&
+	echo "bar" > file.txt &&
+	git commit -a -m "edit2" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	svn_cmd up &&
+	test_svn_subject "edit2" &&
+	test_file file.txt "bar" &&
+	cd ..
+'
+
+test_expect_success 'big file' '
+	seq 1 100000 > file.txt &&
+	git add file.txt &&
+	git commit -a -m "big file" &&
+	git svn-push -v svn/master svn/master master &&
+	cd svnco &&
+	svn_cmd up &&
+	cmp file.txt ../file.txt &&
+	cd ..
+'
+
+test_done
+
diff --git a/t/t9053-git-svn-push-branch.sh b/t/t9053-git-svn-push-branch.sh
new file mode 100755
index 0000000..501ab3c
--- /dev/null
+++ b/t/t9053-git-svn-push-branch.sh
@@ -0,0 +1,478 @@
+#!/bin/sh
+
+test_description='git svn-push branch'
+. ./lib-git-svn-fetch.sh
+
+function check_branched() {
+	copyfrom_path="$1"
+	copyfrom_rev="$2"
+	echo check_branched $1 $2
+	svn_cmd log --stop-on-copy -v --xml | grep copyfrom-path=\"/$copyfrom_path\" &&
+	svn_cmd log --stop-on-copy -v --xml | grep copyfrom-rev=\"$copyfrom_rev\"
+}
+
+test_expect_success 'setup branches' '
+	git config svn.trunk Trunk &&
+	git config svn.branches Branches &&
+	git config svn.tags Tags &&
+	git config svn.trunkref trunk &&
+	cd svnco &&
+	svn_cmd mkdir Branches &&
+	svn_cmd mkdir Tags &&
+	svn_cmd ci -m "svn init" &&
+	cd ..
+'
+
+test_expect_success 'init trunk' '
+	echo "foo" > file.txt &&
+	git add file.txt &&
+	git commit -a -m "init trunk" &&
+	git svn-push -v refs/remotes/svn/trunk $null_sha1 master &&
+	test `show_ref svn/trunk` == `show_ref master` &&
+	cd svnco &&
+	svn_cmd up &&
+	test -d Trunk &&
+	test_svn_subject "init trunk" &&
+	test_svn_author committer &&
+	test_file Trunk/file.txt "foo" &&
+	cd ..
+'
+
+test_expect_success 'modify file' '
+	cd svnco/Trunk &&
+	echo "bar23" > file.txt &&
+	svn_cmd ci -m "svn edit" &&
+	cd ../.. &&
+	git svn-fetch -v &&
+	git reset --hard svn/trunk &&
+	test_file file.txt "bar23" &&
+	echo "foo" > file.txt &&
+	git commit -a -m "git edit" &&
+	git svn-push -v svn/trunk svn/trunk HEAD &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Trunk &&
+	test_svn_subject "git edit" &&
+	test_svn_subject "svn edit" PREV &&
+	test_file file.txt "foo" &&
+	cd ../..
+'
+
+test_expect_success 'modify file2' '
+	cd svnco/Trunk &&
+	echo "bar" > svn.txt &&
+	svn_cmd add svn.txt &&
+	svn_cmd ci -m "svn edit" &&
+	cd ../.. &&
+	git svn-fetch -v &&
+	git reset --hard svn/trunk &&
+	test_file svn.txt "bar" &&
+	echo "foo" > svn.txt &&
+	git commit -a -m "git edit" &&
+	git svn-push -v svn/trunk svn/trunk HEAD &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Trunk &&
+	test_svn_subject "git edit" &&
+	test_svn_subject "svn edit" PREV &&
+	test_file svn.txt "foo" &&
+	cd ../..
+'
+
+function svn_head() {
+	wd=`pwd` &&
+	cd svnco &&
+	cd "$1" &&
+	svn info | grep Revision | sed -e 's/Revision: *//g' &&
+	cd "$wd"
+}
+
+init_trunk_rev=`svn_head Trunk`
+init_trunk_path=Trunk
+
+test_expect_success 'create standalone branch' '
+	git symbolic-ref HEAD refs/heads/standalone &&
+	git rm -r --cached . &&
+	echo "bar" > file.txt &&
+	git add file.txt &&
+	rm svn.txt &&
+	git commit -a -m "init standalone" &&
+	git svn-push -v refs/remotes/svn/standalone $null_sha1 standalone &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/standalone &&
+	test_file file.txt "bar" &&
+	test_svn_subject "init standalone" &&
+	test_must_fail svn_cmd log PREV &&
+	cd ../../..
+'
+
+test_expect_success 'create branch' '
+	git checkout -b CreateBranch master &&
+	git svn-push -v refs/remotes/svn/CreateBranch $null_sha1 CreateBranch &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/CreateBranch &&
+	test_file file.txt "foo" &&
+	test_svn_subject "Create Branches/CreateBranch" &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../..
+'
+
+init_trunk_rev=`svn_head Branches/CreateBranch`
+init_trunk_path=Branches/CreateBranch
+
+test_expect_success 'create and edit branch' '
+	git checkout -b CreateEditBranch master &&
+	echo "foo2" > file2.txt &&
+	git add file2.txt &&
+	git commit -a -m "create/edit branch" &&
+	git svn-push -v refs/remotes/svn/CreateEditBranch $null_sha1 CreateEditBranch &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/CreateEditBranch &&
+	test_file file.txt "foo" &&
+	test_file file2.txt "foo2" &&
+	test_svn_subject "create/edit branch" &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../..
+'
+
+test_expect_success 'create tag' '
+	git checkout master &&
+	git tag SimpleTag &&
+	git svn-push -v refs/tags/svn/SimpleTag $null_sha1 HEAD &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Tags/SimpleTag &&
+	test_file file.txt "foo" &&
+	test_svn_subject "Create Tags/SimpleTag" &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../..
+'
+
+test_expect_success 'create annotated tag' '
+	git checkout master &&
+	git tag -m "annotate tag" AnnotatedTag &&
+	git svn-push -v refs/tags/svn/AnnotatedTag $null_sha1 AnnotatedTag &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Tags/AnnotatedTag &&
+	test_file file.txt "foo" &&
+	test_svn_subject "annotate tag" &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../..
+'
+
+test_expect_success 'replace branch' '
+	git checkout -b ReplaceBranch master &&
+	echo "before replace" > file2.txt &&
+	git add file2.txt &&
+	git commit -a -m "before replace" &&
+	before_sha1=`show_ref HEAD` &&
+	git svn-push -v refs/remotes/svn/ReplaceBranch $null_sha1 ReplaceBranch &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/ReplaceBranch &&
+	test_file file2.txt "before replace" &&
+	test_svn_subject "before replace" &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../.. &&
+	git checkout master &&
+	git branch -D ReplaceBranch &&
+	git checkout -b ReplaceBranch master &&
+	echo "after replace" > file3.txt &&
+	git add file3.txt &&
+	git commit -a -m "after replace" &&
+	git svn-push -v svn/ReplaceBranch $before_sha1 ReplaceBranch &&
+	cd svnco/Branches/ReplaceBranch &&
+	svn_cmd up &&
+	test ! -e file2.txt &&
+	test_file file3.txt "after replace" &&
+	test_svn_subject "after replace" &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../..
+'
+
+test_expect_success 'replace tag' '
+	git checkout -b temp master &&
+	echo "foo" > file3.txt &&
+	git add file3.txt &&
+	git commit -a -m "before replace tag" &&
+	old_sha1=`show_ref HEAD` &&
+	git tag ReplaceTag &&
+	git svn-push -v refs/tags/svn/ReplaceTag $null_sha1 ReplaceTag &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Tags/ReplaceTag &&
+	test_svn_subject "before replace tag" &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../.. &&
+	git reset --hard master &&
+	echo "bar" > file2.txt &&
+	git add file2.txt &&
+	git commit -a -m "after replace tag" &&
+	git tag -f ReplaceTag &&
+	git svn-push -v svn/ReplaceTag $old_sha1 ReplaceTag &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Tags/ReplaceTag &&
+	test_svn_subject "after replace tag" &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../.. &&
+	echo "bar2" > file2.txt &&
+	git add file2.txt &&
+	git commit -a -m "dummy commit" &&
+	git tag -f -m "create replace tag" ReplaceTag &&
+	git svn-push -v svn/ReplaceTag HEAD~ ReplaceTag &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Tags/ReplaceTag &&
+	test_svn_subject "create replace tag" &&
+	test_svn_subject "after replace tag" PREV &&
+	cd ../../.. &&
+	git checkout master &&
+	git branch -D temp
+'
+
+test_expect_success 'delete branch' '
+	git checkout -b DeleteBranch master &&
+	git svn-push -v refs/remotes/svn/DeleteBranch $null_sha1 DeleteBranch &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/DeleteBranch &&
+	test_svn_subject "Create Branches/DeleteBranch" &&
+	cd ../../.. &&
+	git checkout master &&
+	git svn-push -v svn/DeleteBranch DeleteBranch $null_sha1 &&
+	git branch -D DeleteBranch &&
+	cd svnco &&
+	svn_cmd up &&
+	test ! -e Branches/DeleteBranch &&
+	cd ..
+'
+
+test_expect_success 'delete tag' '
+	git checkout master &&
+	git tag DeleteTag &&
+	git svn-push -v refs/tags/svn/DeleteTag $null_sha1 DeleteTag &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Tags/DeleteTag &&
+	test_svn_subject "Create Tags/DeleteTag" &&
+	cd ../../.. &&
+	git svn-push -v svn/DeleteTag DeleteTag $null_sha1 &&
+	git tag -d DeleteTag &&
+	cd svnco &&
+	svn_cmd up &&
+	test ! -e Tags/DeleteTag &&
+	cd ..
+'
+
+test_expect_success 'modify and create branch' '
+	git checkout -b MCBranch1 master &&
+	git svn-push -v refs/remotes/svn/MCBranch1 $null_sha1 MCBranch1 &&
+	cd svnco &&
+	svn_cmd up &&
+	cd .. &&
+	init_trunk_rev=`svn_head Branches/MCBranch1`
+	init_trunk_path=Branches/MCBranch1 &&
+	echo "bar" > file2.txt &&
+	git add file2.txt &&
+	git commit -a -m "some modification on MCBranch1" &&
+	echo "$null_sha1 `show_ref HEAD` refs/remotes/svn/MCBranch2" >> cmds.txt &&
+	echo "`show_ref master` `show_ref HEAD` refs/remotes/svn/MCBranch1" >> cmds.txt &&
+	git svn-push -v --stdin < cmds.txt &&
+	rm -f cmds.txt &&
+	before_rev=`svn_head Branches/MCBranch1` &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/MCBranch1 &&
+	test_svn_subject "some modification on MCBranch1" &&
+	cd ../MCBranch2 &&
+	test_svn_subject "Create Branches/MCBranch2" &&
+	check_branched Branches/MCBranch1 $(($before_rev+1)) &&
+	cd ../../..
+'
+test_expect_success 'modify and replace branch' '
+	git checkout -b MRBranch1 master &&
+	echo "change" > file2.txt &&
+	git add file2.txt &&
+	git commit -a -m "some modification on MRBranch1" &&
+	git svn-push -v refs/remotes/svn/MRBranch1 $null_sha1 MRBranch1 &&
+	git svn-push -v refs/remotes/svn/MRBranch2 $null_sha1 master &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/MRBranch1 &&
+	test_svn_subject "some modification on MRBranch1" &&
+	cd ../MRBranch2 &&
+	test_svn_subject "Create Branches/MRBranch2" &&
+	cd ../../.. &&
+	before_rev=`svn_head Branches/MRBranch2` &&
+	git checkout -b MRBranch2 master &&
+	echo "bar" > file2.txt &&
+	git add file2.txt &&
+	git commit -a -m "some modification on MRBranch2" &&
+	echo "`show_ref MRBranch1` `show_ref MRBranch2` svn/MRBranch1" > cmds.txt &&
+	echo "`show_ref master` `show_ref MRBranch2` svn/MRBranch2" >> cmds.txt &&
+	git svn-push -v --stdin < cmds.txt &&
+	rm -f cmds.txt &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/MRBranch1 &&
+	test_svn_subject "Create Branches/MRBranch1" &&
+	check_branched Branches/MRBranch2 $(($before_rev+1)) &&
+	cd ../MRBranch2 &&
+	test_svn_subject "some modification on MRBranch2" &&
+	test_svn_subject "Create Branches/MRBranch2" PREV &&
+	check_branched $init_trunk_path $init_trunk_rev &&
+	cd ../../..
+'
+
+test_expect_success 'tag deleted branch' '
+	git checkout -b DeleteBranch master &&
+	echo "foo" > file2.txt &&
+	git add file2.txt &&
+	git commit -a -m "commit on deleted branch" &&
+	git svn-push -v refs/remotes/svn/DeleteBranch $null_sha1 HEAD &&
+	git tag TagOnDeleteBranch &&
+	git checkout master &&
+	git svn-push -v svn/DeleteBranch DeleteBranch $null_sha1 &&
+	git svn-push -v refs/tags/svn/TagOnDeleteBranch $null_sha1 TagOnDeleteBranch &&
+	cd svnco &&
+	svn_cmd up &&
+	test ! -e Branches/DeleteBranch &&
+	cd Tags/TagOnDeleteBranch &&
+	test_svn_subject "Create Tags/TagOnDeleteBranch" &&
+	test_must_fail svn_cmd log -r PREV --stop-on-copy &&
+	cd ../../.. &&
+	git branch -D DeleteBranch
+'
+
+test_expect_success 'push left merge' '
+	git checkout -b LeftLeft master &&
+	echo "left" > left.txt &&
+	git add left.txt &&
+	git commit -m "left" &&
+	git svn-push -v refs/remotes/svn/LeftMerged $null_sha1 HEAD &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/LeftMerged &&
+	prev_path=`svn_cmd log --stop-on-copy -v --xml | grep copyfrom-path | sed -re "s#.*copyfrom-path=\"([^\"]*)\".*#copyfrom-path=\"\1\"#g"` &&
+	prev_rev=`svn_cmd log --stop-on-copy -v --xml | grep copyfrom-rev | sed -re "s#.*copyfrom-rev=\"([^\"]*)\".*#copyfrom-rev=\"\1\"#g"` &&
+	echo prev_path $prev_path &&
+	echo prev_rev $prev_rev &&
+	cd ../../.. &&
+	git checkout -b LeftRight master &&
+	echo "right" > right.txt &&
+	git add right.txt &&
+	git commit -m "right" &&
+	git checkout LeftLeft &&
+	git merge --no-ff "merge commit" HEAD LeftRight &&
+	git svn-push -v svn/LeftMerged svn/LeftMerged LeftLeft &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/LeftMerged &&
+	test_file left.txt "left" &&
+	test_file right.txt "right" &&
+	test_svn_subject "merge commit" &&
+	test_svn_subject "left" PREV &&
+	svn_cmd log --stop-on-copy -v --xml | grep $prev_path &&
+	svn_cmd log --stop-on-copy -v --xml | grep $prev_rev &&
+	cd ../../..
+'
+
+test_expect_success 'push right merge' '
+	git checkout -b RightLeft master &&
+	echo "left" > left.txt &&
+	git add left.txt &&
+	git commit -m "left" &&
+	git svn-push -v refs/remotes/svn/RightMerged $null_sha1 HEAD &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/RightMerged &&
+	prev_path=`svn_cmd log --stop-on-copy -v --xml | grep copyfrom-path | sed -re "s#.*copyfrom-path=\"([^\"]*)\".*#copyfrom-path=\"\1\"#g"` &&
+	prev_rev=`svn_cmd log --stop-on-copy -v --xml | grep copyfrom-rev | sed -re "s#.*copyfrom-rev=\"([^\"]*)\".*#copyfrom-rev=\"\1\"#g"` &&
+	echo prev_path $prev_path &&
+	echo prev_rev $prev_rev &&
+	cd ../../.. &&
+	git checkout -b RightRight master &&
+	echo "right" > right.txt &&
+	git add right.txt &&
+	git commit -m "right" &&
+	git merge --no-ff "merge commit" HEAD RightLeft &&
+	git svn-push -v svn/RightMerged svn/RightMerged RightRight &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/RightMerged &&
+	test_svn_subject "merge commit" &&
+	test_svn_subject "left" PREV &&
+	svn_cmd log --stop-on-copy -v --xml | grep $prev_path &&
+	svn_cmd log --stop-on-copy -v --xml | grep $prev_rev &&
+	cd ../../..
+'
+
+test_expect_success 'unseen new commit in svn' '
+	cd svnco &&
+	svn_cmd cp Trunk Branches/Unseen &&
+	svn_cmd ci -m "make branch" &&
+	cd .. &&
+	git svn-fetch -v &&
+	cd svnco/Branches/Unseen &&
+	echo "foo" > unseen.txt &&
+	svn_cmd add unseen.txt &&
+	svn_cmd ci -m "unseen file" &&
+	cd ../../.. &&
+	git checkout -b unseen svn/Unseen &&
+	echo "bar" > seen.txt &&
+	git add seen.txt &&
+	git commit -m "seen file" &&
+	# This should push the commit and then fail, so after the fetch
+	# and rebase no push should be required
+	test_must_fail git svn-push -v svn/Unseen unseen~1 unseen &&
+	git svn-fetch -v &&
+	git rebase svn/Unseen &&
+	test `show_ref svn/Unseen` == `show_ref unseen` &&
+	cd svnco/Branches/Unseen &&
+	svn_cmd up &&
+	test_file unseen.txt "foo" &&
+	test_file seen.txt "bar" &&
+	cd ../../..
+'
+
+test_expect_success 'intermingled commits' '
+	git checkout -b intermingled svn/trunk &&
+	echo "bar" > file1.txt &&
+	git add file1.txt &&
+	git commit -m "commit 1" &&
+	echo "foo" > file2.txt &&
+	git add file2.txt &&
+	git commit -m "commit 2" &&
+	port=$(($SVNSERVE_PORT+1)) || exit 1
+	GIT_SVN_PUSH_PAUSE=$port git svn-push -v refs/remotes/svn/intermingled $null_sha1 HEAD &
+	push_pid=$! &&
+	until nc -z localhost $port; do sleep 1; done &&
+	cd svnco &&
+	svn_cmd up &&
+	cd Branches/intermingled &&
+	echo "foobar" > file3.txt &&
+	svn_cmd add file3.txt &&
+	svn_cmd ci -m "svn commit" &&
+	cd ../../.. &&
+	nc -z localhost $port &&
+	test_must_fail wait $push_pid &&
+	git svn-fetch -v &&
+	git rebase svn/intermingled &&
+	git svn-push -v svn/intermingled svn/intermingled HEAD &&
+	cd svnco &&
+	svn_cmd up &&
+	cd .. &&
+	rev=`svn_head Branches/intermingled` &&
+	cd svnco/Branches/intermingled &&
+	test_svn_subject "commit 2" $rev &&
+	test_svn_subject "svn commit" $(($rev-1)) &&
+	test_svn_subject "commit 1" $(($rev-2)) &&
+	cd ../../..
+'
+
+test_done
-- 
1.7.11.3

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [RFC 0/2] svn-fetch|push - an alternate approach
  2012-08-18 17:39 [RFC 0/2] svn-fetch|push - an alternate approach James R. McKaskill
  2012-08-18 17:39 ` [RFC 1/2] add svn-fetch/push James R. McKaskill
  2012-08-18 17:39 ` [RFC 2/2] add tests for git svn-fetch|push James R. McKaskill
@ 2012-08-19 10:21 ` Marco Schulze
  2012-08-21  7:06   ` Michael Haggerty
  2 siblings, 1 reply; 6+ messages in thread
From: Marco Schulze @ 2012-08-19 10:21 UTC (permalink / raw)
  To: James R. McKaskill; +Cc: git


On 18-08-2012 14:39, James R. McKaskill wrote:
> As a twist the code does not use the svn library, but rather talks the
> svn protocol directly. I actually found it much easier to go this route
> then trying to bend everything to how the svn library understands
> things. It also has the advantage of not depending on libsvn. A number
> of distributions currently distribute the svn specific parts of git
> seperately to avoid this dependency.
It is a bit of a pain to work with libsvn, but I think it is worth using 
it, not only due to protocol support, but also due to ready-to-use 
support for SVN deltas and dumps.  Pipelining could be implemented by 
maintaining a set of connections (svn_ra_session_t structures) and 
manually distributing work between them.

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [RFC 0/2] svn-fetch|push - an alternate approach
  2012-08-19 10:21 ` [RFC 0/2] svn-fetch|push - an alternate approach Marco Schulze
@ 2012-08-21  7:06   ` Michael Haggerty
  2012-08-22  2:30     ` James McKaskill
  0 siblings, 1 reply; 6+ messages in thread
From: Michael Haggerty @ 2012-08-21  7:06 UTC (permalink / raw)
  To: Marco Schulze; +Cc: James R. McKaskill, git

On 08/19/2012 12:21 PM, Marco Schulze wrote:
> 
> On 18-08-2012 14:39, James R. McKaskill wrote:
>> As a twist the code does not use the svn library, but rather talks the
>> svn protocol directly. I actually found it much easier to go this route
>> then trying to bend everything to how the svn library understands
>> things. It also has the advantage of not depending on libsvn. A number
>> of distributions currently distribute the svn specific parts of git
>> seperately to avoid this dependency.
> It is a bit of a pain to work with libsvn, but I think it is worth using 
> it, not only due to protocol support, but also due to ready-to-use 
> support for SVN deltas and dumps.  Pipelining could be implemented by 
> maintaining a set of connections (svn_ra_session_t structures) and 
> manually distributing work between them.

Using libsvn would also have the benefit of letting the Subversion
project worry about forwards and backwards compatibility if the protocol
ever changes.  (They tend to be quite diligent about that.)

Michael

-- 
Michael Haggerty
mhagger@alum.mit.edu
http://softwareswirl.blogspot.com/

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [RFC 0/2] svn-fetch|push - an alternate approach
  2012-08-21  7:06   ` Michael Haggerty
@ 2012-08-22  2:30     ` James McKaskill
  0 siblings, 0 replies; 6+ messages in thread
From: James McKaskill @ 2012-08-22  2:30 UTC (permalink / raw)
  To: Michael Haggerty; +Cc: Marco Schulze, git

On Tue, Aug 21, 2012 at 3:06 AM, Michael Haggerty <mhagger@alum.mit.edu> wrote:
> On 08/19/2012 12:21 PM, Marco Schulze wrote:
>> It is a bit of a pain to work with libsvn, but I think it is worth using
>> it, not only due to protocol support, but also due to ready-to-use
>> support for SVN deltas and dumps.  Pipelining could be implemented by
>> maintaining a set of connections (svn_ra_session_t structures) and
>> manually distributing work between them.
>
> Using libsvn would also have the benefit of letting the Subversion
> project worry about forwards and backwards compatibility if the protocol
> ever changes.  (They tend to be quite diligent about that.)

So my goal with the svn support is not so much to support pushing to
svn, but to support it natively. What I mean by that is that it will
use the git configuration for http, auth, ssl, etc. For the svn
protocol there is little to none of this, but frankly the svn protocol
is a one to one mapping to the svn editor interface and very simple to
boot. So working off the interface vs working off of the protocol
there isn't much different in the complexity or amount of code. For
HTTP though there is a huge benefit to working outside libsvn. Namely
reusing the http support in git means all of the credential, http, ssl
config works. Also these days again there isn't much difference
between the http interface and the svn editor interface.

As for diffs and dumps ... Well svn diff parsing and generation is
relatively trivial (200-300 lines currently). I have no real plan
currently to deal with dumps.

I've since added support for HTTP. What I'll do is add a third
interface in the same manner that uses libsvn. If it turns out simple
enough then that would be the way to go for dumps and svn, but I'm
very skeptical about using that for HTTP+svn (can't be fully sure
without trying I suppose).

TLDR: You've convinced me to at least give it a try.

-- James

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2012-08-22  2:30 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2012-08-18 17:39 [RFC 0/2] svn-fetch|push - an alternate approach James R. McKaskill
2012-08-18 17:39 ` [RFC 1/2] add svn-fetch/push James R. McKaskill
2012-08-18 17:39 ` [RFC 2/2] add tests for git svn-fetch|push James R. McKaskill
2012-08-19 10:21 ` [RFC 0/2] svn-fetch|push - an alternate approach Marco Schulze
2012-08-21  7:06   ` Michael Haggerty
2012-08-22  2:30     ` James McKaskill

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).