[RFC 1/2] add svn-fetch/push

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



From: "James R. McKaskill" <james@xxxxxxxxxxxx>


Signed-off-by: James R. McKaskill <james@xxxxxxxxxxxx>
---
 .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@xxxxxxxxxxx>\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@xxxxxxxxxxx>
+!
+
+../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@xxxxxxxxxxx'
+
+
+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@xxxxxxxxxxx'
+
+echo "some mod" >> b/foo2.txt
+../../git add b/foo2.txt
+../../git commit -m 'git clone commit'
+../../git push
+
-- 
1.7.11.3

--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html


[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]