{"thread":{"id":"31281","subject":"[RFC 1/2] add svn-fetch/push","startedAt":"2012-08-18T17:39:14Z","lastAt":"2012-08-22T02:30:43Z","messageCount":6,"participants":["James R. McKaskill","Marco Schulze","Michael Haggerty","James McKaskill"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"197250","messageId":"1345311556-70767-1-git-send-email-james@foobar.co.nz","threadId":"31281","inReplyTo":null,"subject":"[RFC 0/2] svn-fetch|push - an alternate approach","fromName":"James R. McKaskill","fromEmail":"james@foobar.co.nz","sentAt":"2012-08-18T17:39:14Z","receivedAt":"2012-08-18T17:39:14Z","isPatch":false,"sender":{"key":"james@foobar.co.nz","avatar":"https://gravatar.com/avatar/7a9da4c40caed181c908ea4990fb2a534f561581df163bedaeaa5d6ab0885870?d=mp&s=160"},"body":"This is sort of an RFC/ANN. There has been a whole bunch of traffic of\nlate on git-svn, vcs-svn, the remote helper, etc. I would like to\npresent an alternate solution that I've been playing with for a couple\nof weeks.\n\nI work in a team that uses a mixture of git-svn and svn proper. Whilst\nit works remarkably well and I largely don't have to deal with svn on a\nday to day basis, git-svn is still quite limiting compared to git\nproper. Namely any merges created by myself or another either can't be\npushed into svn or information is lost when done so. Switching wholesale\nto git is problematic due to the disruption this causes. There are also\nnumerous non-core users who want a really simple update and occasionally\ncheckin a single file/folder UI. Tortoise SVN fits their needs perfectly\nand even with a proper git-svn bridge in place I have no plan on\ndropping svn support.\n\nPossible Solutions\n------------------\n\nIn general there are a couple of ways of attacking this:\n\n1. Have git-svn push extraneous git context into subversion and pull it\n   back down on the other side.\n2. Create a dummy svn server ala git-cvsserver\n3. Use both a git server and svn server and have the git server push to\n   the svn server.\n4. Same as #3 but have the svn server push to the git server.\n\n#1 is frought with issues due to how exacting git is wrt to recreating\ncommits. The recreated commits would have to have the same hash to be\nable to use git's distributed nature.\n\n#2 is possible but a lot of work. SVN clients expect a substantial\namount of metadata. This introduces considerable complexity of where to\nstore the metadata and what metadata to generate on the fly. Not all can\nbe generated on the fly as SVN presents a general purpose key value\nstore to clients, which many use and expect to work.\n\n#3 and #4 have divergence issues in that you have two servers through\nwhich commits can be pushed. If conflicting commits are pushed into both\nservers at the same time, there has to be some method for conflict\nresolution. Requiring that an admin go in and fix things manually is\nunpalatable. The easiest way around this is to delegate one of the two\nservers as the master and require that commits pushed to the other\nupdate the master before suceeding. In this mode #3 has the SVN server\nas master, and #4 has the git server.\n\nThere is an existing commercial system that came up a couple of weeks\nago that transfers commits asynchronously. I am unsure of how this\navoids conflicts if at all.\n\nI've been working on a solution using approach #3. The code follows\n(hopefully) and is currently running an internal beta with 3 or 4 users\nusing it and works very well.\n\nIn my setup, the SVN server is the master node. The git server has a\npre-receive hook which pushes commits onto the SVN server. The hook\nchecks for any SVN errors (SVN hooks, file conflicts, etc), and that no\nSVN commits are intermingled with the pushed git commits (either before\nor interspersed). If this fails at all, the pre-receive hook fails,\nwhich fails the git push. The git user then pulls or rebases and tries\nto push again.\n\nKerberos Auth (Off-Topic)\n-------------------------\n\nI am also using a kerberos auth http frontend\n(http://github.com/jmckaskill/krb-httpd). This checks authentication\nagainst active directory and uses the remote user for the --user\nargument of git svn-push. Most clients are windows users. In this case I\nhave built a replacement version of libcurl-4.dll which enables kerberos\nnegotiate (as an aside the msysgit build should enable this by default).\nI've then added the following to users' global git config:\n\ncredential.http://domain.name.username=dummy\ncredential.http://domain.name.helper=!echo password=dummy\n\nWith this in place, git uses the user's domain login and never asks for\na password. There is also some equivalent tweaks to get domain logins\nworking in browsers for cgit.\n\nHow it Works\n------------\n\nThe attached patch adds two commands to git: svn-fetch and svn-push. The\nnames are temporary. My plan is to refactor these into git-remote-svn so\nthat git push/pull/fetch work as expected. svn-fetch fetches svn commits\nfor all branches and creates git commits, branches, tags, etc as it goes\nalong. svn-push takes the remote ref name, from and to sha1 (or a list\nof these on stdin for use as a pre-receive hook) and pushes the changes\nto svn, creating/deleting branches/tags as neccessary and failing if\nthere are any intermingled svn commits in the pushed to folders.\n\nMetadata is tracked by creating an extra git commit for each svn commit\nin each branch/tag that is stored in refs/svn/heads|tags as well as\nrefs/svn/latest for the latest fetched commit. These commits look\nlike the following:\n\ntree <svn tree>\nparent <underlying git commit>\nparent <previous svn commit for this svn folder>\nauthor <svn author + time>\ncommitter <svn author + time>\nrevision <svn revision>\npath <svn path relative to svn.url>\n\nUsing commits to track the SVN metadata has proven really really handy.\nThe code gets to use all the built-in locking primitives, packing, etc.\ngit push --mirror nicely mirrors all of the metadata as well as the\ngit commits. As an anecdote I had a bug the other day where my\ncheck_for_svn_commits wasn't working and so had missed a svn commit.\nAfter fixing the bug, I needed to rerun the fetch to grab the missing\ncommit. This was a simple matter of: ran git log --pretty=raw\nrefs/svn/heads/trunk to get the svn/git commit sha1s, updated the refs,\nand reran git svn-fetch.\n\nAs a twist the code does not use the svn library, but rather talks the\nsvn protocol directly. I actually found it much easier to go this route\nthen trying to bend everything to how the svn library understands\nthings. It also has the advantage of not depending on libsvn. A number\nof distributions currently distribute the svn specific parts of git\nseperately to avoid this dependency.\n\nTrees\n-----\n\nFor each commit I track the svn and git trees seperately. The svn tree\ntracks exactly what SVN returns byte for byte. This is required so that\nfetched SVN diffs can be processed correctly. I can then have slight\ntweaks between the two trees. Currently I have three differences between\nthe two trees.\n\nIf svn.eol is set then eol conversion is done between the two trees\n(controlled by info/attributes). This way all of my imported git trees\nhave unix line endings, whilst the svn trees have a mixture. In my case\nthey should be windows line endings, but some third party libraries we\nimported as unix line endings. Files updated in git are renormalized to\nthe eol given as svn.eol.\n\nThe second tweak is that changes to .git* pushed from the git side are\nnot pushed through to svn. This way git users can manage .gitattributes\nand .gitignore without pushing these through to svn.\n\nThe third change is that svn-fetch creates a .gitempty file in any\nempty directory on the svn side. This forces git to create the directory\nand gives the git user a clean way of removing the directory.\n\nBranches\n--------\n\nBranch names in SVN are a bit more leniant then ref names in git. For\nexample SVN allows spaces whilst git does not. Thus svn-fetch converts\nall disallowed characters to '_'. The svn commit stores the original\nbranch name so it can be pushed back to. svn-fetch does not currently\nhandle conflicts where two different SVN branches collide with a single\ngit name.\n\nWhen updating a branch svn-push tries to find a path from the previous\ncommit to the target. When another branch has been merged into the svn\nbranch, then the pushed commits look very similar to the svn equivalent.\nNamely just the merge commit with a large diff. In the case that pushed\ncommits branch and merge back together, then svn-push simply tries to\nfind any path that gets it from the previous commit to the target. For\nnew branches it creates an svn copy from the newest svn commit that is a\nancestor of the target. In the case a forced push it will do an svn\nreplace with the newest ancestor in svn. The code for this is in\nfind_copy_source and if (!has_parent) check in do_push.\n\nTags\n----\n\nTags are pushed to SVN as SVN tags (ie folder copies). Both annonated\nand simple tags can be used. For annontated tags, the messages is used\nfor the commit message. For simple tags a hard-coded commit message of\nCreate <tag folder relative to svn.url> is used. svn-fetch creates\nannotated tags for tags created/updated in SVN. If a tag is updated in\nSVN, svn-fetch will create a git commit for the change, a new annotated\ntag and overwrite the existing tag. Standard SVN practise is not to\ncommit to tag folders, but it does occasionally happen. This is thus\ntreated in the same way as the need in git to occasionally overwrite a\ntag.\n\nSVN Auth\n--------\n\nThe current authentication is temporary. Currently I'm using a git-svn\nstyle authors file hard-coded to <git-dir>/svn-authors. svn-push then\nrequires the user be of the form user:pass. As you push commits it will\nthen switch to that user by killing the connection and reopening. There\nare also a couple of operations which require an svn user but don't have\na git commit to look the email up from. For these both svn-fetch and\nsvn-push require a --user argument. These operations are: all of\nsvn-fetch and get-latest-rev, log, and deleting branch/tags for\nsvn-push.\n\nPipelining\n----------\n\nsvn-fetch has a -c option which lets you increase the number of\nconnections used to download commits. The svn protocol is annoying in\nthat it doesn't let you pipeline requests. svnserve will let you send\nsome content out of order, but once you overflow its rx buffer it will\nkill the connection. So instead svn-fetch opens n+1 connections, sends\nthe requests ahead of time and then cycle back round to process the\nreply. When initially importing one of my work projects setting this to\n15 increased the fetch speed by a factor of 10.\n\nTests\n-----\n\nI've also added some tests for svn-fetch, svn-push. These currently\nrequire svn 1.7 or newer. svn 1.6 doesn't understand branch\nreplacement and I haven't gotten around to disabling those tests.\n\nConfig\n------\n\nsvn-fetch and svn-push use a number of config items:\n\n- svn.trunk - path under svn.url of the trunk branch\n\n- svn.branches - path under svn.url of the branches folder, branches are\n    then folders inside this folder\n\n- svn.tags - path under svn.url of the tags folders, tags are then\n    folders inside this folder\n\n- svn.user - default svn user for fetch, push without a git commit\n\n- svn.remote - remote name to use for fetch tags/branches\n\n- svn.trunkref - name to use for the git trunk ref (defaults to master)\n\n- svn.eol - eol to convert to for files pushed to svn\n\nMost of these are temporary.\n\nTODO\n----\n\nThere is a whole bunch more work to be done. My big ticket items are:\n\n- svn over HTTP support. I've had an initial look into this and looks\n    fairly straight forward. svn over HTTP is largely the same svn\ncommands converted to XML. Is there any recomendation on what XML\nlibrary to use or should I write my own limited version?\n\n- fixing up auth to use credentials\n\n- refactor svn-fetch svn-push into git-remote-svn\n\n- adding a cfg item for the authors file and using svn-user@repo UUID if\n    none is provided like git-svn\n\n- documentation - for the moment the documentation is this email\n\n- svn:externals - none of the repos have to deal with have this and I'm\n    not sure yet how to deal with it\n\n- style cleanup\n\nCode\n----\n\nThe code is also available at https://github.com/jmckaskill/git, which I\nwill keep uptodate as I flush out the todo list.\n"},{"id":"197249","messageId":"1345311556-70767-2-git-send-email-james@foobar.co.nz","threadId":"31281","inReplyTo":"1345311556-70767-1-git-send-email-james@foobar.co.nz","subject":"[RFC 1/2] add svn-fetch/push","fromName":"James R. McKaskill","fromEmail":"james@foobar.co.nz","sentAt":"2012-08-18T17:39:15Z","receivedAt":"2012-08-18T17:39:15Z","isPatch":false,"sender":{"key":"james@foobar.co.nz","avatar":"https://gravatar.com/avatar/7a9da4c40caed181c908ea4990fb2a534f561581df163bedaeaa5d6ab0885870?d=mp&s=160"},"body":"From: \"James R. McKaskill\" <james@foobar.co.nz>\n\n\nSigned-off-by: James R. McKaskill <james@foobar.co.nz>\n---\n .gitignore          |    3 +\n Makefile            |    2 +\n builtin.h           |    2 +\n builtin/svn-fetch.c | 3257 +++++++++++++++++++++++++++++++++++++++++++++++++++\n command-list.txt    |    2 +\n git.c               |    2 +\n svn-sync.sh         |  150 +++\n 7 files changed, 3418 insertions(+)\n create mode 100644 builtin/svn-fetch.c\n create mode 100755 svn-sync.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex bb5c91e..8e9cd4b 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -153,6 +153,8 @@\n /git-stripspace\n /git-submodule\n /git-svn\n+/git-svn-fetch\n+/git-svn-push\n /git-symbolic-ref\n /git-tag\n /git-tar-tree\n@@ -233,3 +235,4 @@\n *.pdb\n /Debug/\n /Release/\n+*.swp\ndiff --git a/Makefile b/Makefile\nindex 6b0c961..e28e224 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -523,6 +523,7 @@ BUILT_INS += git-show$X\n BUILT_INS += git-stage$X\n BUILT_INS += git-status$X\n BUILT_INS += git-whatchanged$X\n+BUILT_INS += git-svn-push$X\n \n # what 'all' will build and 'install' will install in gitexecdir,\n # excluding programs for built-in commands\n@@ -894,6 +895,7 @@ BUILTIN_OBJS += builtin/shortlog.o\n BUILTIN_OBJS += builtin/show-branch.o\n BUILTIN_OBJS += builtin/show-ref.o\n BUILTIN_OBJS += builtin/stripspace.o\n+BUILTIN_OBJS += builtin/svn-fetch.o\n BUILTIN_OBJS += builtin/symbolic-ref.o\n BUILTIN_OBJS += builtin/tag.o\n BUILTIN_OBJS += builtin/tar-tree.o\ndiff --git a/builtin.h b/builtin.h\nindex ba6626b..71fe517 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -131,6 +131,8 @@ extern int cmd_show(int argc, const char **argv, const char *prefix);\n extern int cmd_show_branch(int argc, const char **argv, const char *prefix);\n extern int cmd_status(int argc, const char **argv, const char *prefix);\n extern int cmd_stripspace(int argc, const char **argv, const char *prefix);\n+extern int cmd_svn_fetch(int argc, const char **argv, const char* prefix);\n+extern int cmd_svn_push(int argc, const char **argv, const char* prefix);\n extern int cmd_symbolic_ref(int argc, const char **argv, const char *prefix);\n extern int cmd_tag(int argc, const char **argv, const char *prefix);\n extern int cmd_tar_tree(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/svn-fetch.c b/builtin/svn-fetch.c\nnew file mode 100644\nindex 0000000..05ef85f\n--- /dev/null\n+++ b/builtin/svn-fetch.c\n@@ -0,0 +1,3257 @@\n+#include \"git-compat-util.h\"\n+#include \"parse-options.h\"\n+#include \"gettext.h\"\n+#include \"cache.h\"\n+#include \"cache-tree.h\"\n+#include \"refs.h\"\n+#include \"unpack-trees.h\"\n+#include \"commit.h\"\n+#include \"tag.h\"\n+#include \"diff.h\"\n+#include \"revision.h\"\n+#include \"diffcore.h\"\n+#include \"run-command.h\"\n+\n+#include <openssl/hmac.h>\n+#include <openssl/evp.h>\n+#include <openssl/md5.h>\n+#include <string.h>\n+#include <stdio.h>\n+#include <stdarg.h>\n+\n+static const char *svnuser;\n+static const char *trunk;\n+static const char *branches;\n+static const char *tags;\n+static const char *remotedir;\n+static const char *remoteheads;\n+static const char *remotetags;\n+static const char *trunkref = \"master\";\n+static int last_revision = INT_MAX;\n+static int verbose;\n+static int push_from_stdin;\n+static int svnfdc = 1;\n+static int leave_remote;\n+static int svnfd;\n+static int inner;\n+static int pause_between_commits;\n+static const char* url;\n+static enum eol svn_eol = EOL_UNSET;\n+\n+#define FETCH_AT_ONCE 1000\n+\n+static const char* const builtin_svn_fetch_usage[] = {\n+\t\"git svn-fetch [options]\",\n+\tNULL,\n+};\n+\n+static struct option builtin_svn_fetch_options[] = {\n+\tOPT_STRING(0, \"user\", &svnuser, \"user\", \"svn username\"),\n+\tOPT_BOOLEAN('v', \"verbose\", &verbose, \"verbose logging of all svn traffic\"),\n+\tOPT_INTEGER('r', \"revision\", &last_revision, \"revisions to fetch up to\"),\n+\tOPT_INTEGER('c', \"connections\", &svnfdc, \"number of concurrent connections\"),\n+\tOPT_BOOLEAN(0, \"inner\", &inner, \"internal\"),\n+\tOPT_END()\n+};\n+\n+static const char* const builtin_svn_push_usage[] = {\n+\t\"git svn-push [options] <ref> <from commit> <to commit>\",\n+\t\"git svn-push [options] --stdin\",\n+\tNULL,\n+};\n+\n+static struct option builtin_svn_push_options[] = {\n+\tOPT_STRING(0, \"user\", &svnuser, \"user\", \"default svn username\"),\n+\tOPT_BOOLEAN('v', \"verbose\", &verbose, \"verbose logging of all svn traffic\"),\n+\tOPT_BOOLEAN(0, \"stdin\", &push_from_stdin, \"read refs to update from stdin\"),\n+\tOPT_END()\n+};\n+\n+static int config(const char *var, const char *value, void *dummy) {\n+\tif (!strcmp(var, \"svn.trunk\")) {\n+\t\treturn git_config_string(&trunk, var, value);\n+\t}\n+\tif (!strcmp(var, \"svn.branches\")) {\n+\t\treturn git_config_string(&branches, var, value);\n+\t}\n+\tif (!strcmp(var, \"svn.tags\")) {\n+\t\treturn git_config_string(&tags, var, value);\n+\t}\n+\tif (!strcmp(var, \"svn.user\")) {\n+\t\treturn git_config_string(&svnuser, var, value);\n+\t}\n+\tif (!strcmp(var, \"svn.url\")) {\n+\t\treturn git_config_string(&url, var, value);\n+\t}\n+\tif (!strcmp(var, \"svn.remote\")) {\n+\t\treturn git_config_string(&remotedir, var, value);\n+\t}\n+\tif (!strcmp(var, \"svn.trunkref\")) {\n+\t\treturn git_config_string(&trunkref, var, value);\n+\t}\n+\tif (!strcmp(var, \"svn.eol\")) {\n+\t\tif (value && !strcasecmp(value, \"lf\"))\n+\t\t\tsvn_eol = EOL_LF;\n+\t\telse if (value && !strcasecmp(value, \"crlf\"))\n+\t\t\tsvn_eol = EOL_CRLF;\n+\t\telse if (value && !strcasecmp(value, \"native\"))\n+\t\t\tsvn_eol = EOL_NATIVE;\n+\t\telse\n+\t\t\tsvn_eol = EOL_UNSET;\n+\t\treturn 0;\n+\t}\n+\treturn git_default_config(var, value, dummy);\n+}\n+\n+#ifndef min\n+#define min(a,b) ((a) < (b) ? (a) : (b))\n+#endif\n+\n+#ifndef max\n+#define max(a,b) ((a) < (b) ? (b) : (a))\n+#endif\n+\n+struct inbuffer {\n+\tchar buf[4096];\n+\tint b, e;\n+};\n+static struct inbuffer* inbuf;\n+\n+static int readc() {\n+\tif (inbuf->b == inbuf->e) {\n+\t\tinbuf->b = 0;\n+\t\tinbuf->e = xread(svnfd, inbuf->buf, sizeof(inbuf->buf));\n+\t\tif (inbuf->e <= 0) return EOF;\n+\t}\n+\n+\treturn inbuf->buf[inbuf->b++];\n+}\n+\n+static void unreadc() {\n+\tinbuf->b--;\n+}\n+\n+static ssize_t read_svn(void* p, size_t n) {\n+\t/* big reads we may as well read directly into the target */\n+\tif (inbuf->e == inbuf->b && n >= sizeof(inbuf->buf) / 2) {\n+\t\treturn xread(svnfd, p, n);\n+\n+\t} else if (inbuf->e == inbuf->b) {\n+\t\tinbuf->b = 0;\n+\t\tinbuf->e = xread(svnfd, inbuf->buf, sizeof(inbuf->buf));\n+\t\tif (inbuf->e <= 0) return inbuf->e;\n+\t}\n+\n+\tn = min(n, inbuf->e - inbuf->b);\n+\tmemcpy(p, inbuf->buf + inbuf->b, n);\n+\tinbuf->b += n;\n+\treturn n;\n+}\n+\n+static const char hex[] = \"0123456789abcdef\";\n+#define MAX_PRINT_LEN 64\n+\n+static int print_ascii(const void* p, int n, int maxoutput) {\n+\tint i;\n+\tint printed = 0;\n+\tconst unsigned char* v = p;\n+\n+\tfor (i = 0; i < n && printed < maxoutput; i++) {\n+\t\tint ch = v[i];\n+\n+\t\tif (' ' <= ch && ch < 0x7F && ch != '\\\\') {\n+\t\t\tputc(v[i], stderr);\n+\t\t\tprinted++;\n+\n+\t\t} else if (ch == '\\n') {\n+\t\t\tputc('\\\\', stderr);\n+\t\t\tputc('n', stderr);\n+\t\t\tprinted += 2;\n+\n+\t\t} else if (ch == '\\r') {\n+\t\t\tputc('\\\\', stderr);\n+\t\t\tputc('r', stderr);\n+\t\t\tprinted += 2;\n+\n+\t\t} else if (ch == '\\t') {\n+\t\t\tputc('\\\\', stderr);\n+\t\t\tputc('t', stderr);\n+\t\t\tprinted += 2;\n+\n+\t\t} else if (ch == '\\\\') {\n+\t\t\tputc('\\\\', stderr);\n+\t\t\tputc('\\\\', stderr);\n+\t\t\tprinted += 2;\n+\n+\t\t} else {\n+\t\t\tputc('\\\\', stderr);\n+\t\t\tputc('x', stderr);\n+\t\t\tputc(hex[ch >> 4], stderr);\n+\t\t\tputc(hex[ch & 0x0F], stderr);\n+\t\t\tprinted += 4;\n+\t\t}\n+\t}\n+\n+\tif (printed >= maxoutput) {\n+\t\tfprintf(stderr, \"...\");\n+\t}\n+\n+\treturn printed;\n+}\n+\n+static int get_md5_hex(const char *hex, unsigned char *sha1)\n+{\n+\tint i;\n+\tfor (i = 0; i < 16; i++) {\n+\t\tunsigned int val;\n+\t\t/*\n+\t\t * hex[1]=='\\0' is caught when val is checked below,\n+\t\t * but if hex[0] is NUL we have to avoid reading\n+\t\t * past the end of the string:\n+\t\t */\n+\t\tif (!hex[0])\n+\t\t\treturn -1;\n+\t\tval = (hexval(hex[0]) << 4) | hexval(hex[1]);\n+\t\tif (val & ~0xff)\n+\t\t\treturn -1;\n+\t\t*sha1++ = val;\n+\t\thex += 2;\n+\t}\n+\treturn 0;\n+}\n+\n+static const char* md5_to_hex(const unsigned char* md5) {\n+\tstatic int bufno;\n+\tstatic char hexbuffer[4][50];\n+\tchar *buffer = hexbuffer[3 & ++bufno], *buf = buffer;\n+\tint i;\n+\n+\tfor (i = 0; i < 16; i++) {\n+\t\tunsigned int val = *md5++;\n+\t\t*buf++ = hex[val >> 4];\n+\t\t*buf++ = hex[val & 0xf];\n+\t}\n+\t*buf = '\\0';\n+\n+\treturn buffer;\n+}\n+\n+__attribute__((format (printf,1,2)))\n+static void sendf(const char* fmt, ...);\n+\n+static int verbosetxnl = 1;\n+\n+static void sendf(const char* fmt, ...) {\n+\tstatic struct strbuf out = STRBUF_INIT;\n+\tva_list ap;\n+\tva_start(ap, fmt);\n+\tstrbuf_reset(&out);\n+\tstrbuf_vaddf(&out, fmt, ap);\n+\n+\tif (verbose) {\n+\t\tif (verbosetxnl) {\n+\t\t\tfputc('+', stderr);\n+\t\t}\n+\t\tverbosetxnl = out.len && out.buf[out.len-1] == '\\n';\n+\t\tprint_ascii(out.buf, out.len - verbosetxnl, INT_MAX);\n+\t\tif (verbosetxnl) {\n+\t\t\tfputc('\\n', stderr);\n+\t\t}\n+\t}\n+\n+\tif (write_in_full(svnfd, out.buf, out.len) != out.len) {\n+\t\tdie_errno(\"write\");\n+\t}\n+}\n+\n+/* returns -1 if it can't find a number */\n+static ssize_t read_number() {\n+\tssize_t v;\n+\n+\tfor (;;) {\n+\t\tint ch = readc();\n+\t\tif ('0' <= ch && ch <= '9') {\n+\t\t\tv = ch - '0';\n+\t\t\tbreak;\n+\t\t} else if (ch != ' ' && ch != '\\n') {\n+\t\t\tunreadc();\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n+\tfor (;;) {\n+\t\tint ch = readc();\n+\t\tif (ch < '0' || ch > '9') {\n+\t\t\tunreadc();\n+\t\t\tif (verbose) fprintf(stderr, \" %d\", (int) v);\n+\t\t\treturn v;\n+\t\t}\n+\n+\t\tif (v > INT64_MAX/10) {\n+\t\t\tdie(_(\"number too big\"));\n+\t\t} else {\n+\t\t\tv = 10*v + (ch - '0');\n+\t\t}\n+\t}\n+}\n+\n+/* returns -1 if it can't find a list */\n+static int read_list() {\n+\tfor (;;) {\n+\t\tint ch = readc();\n+\t\tif (ch == '(') {\n+\t\t\tif (verbose) fprintf(stderr, \" (\");\n+\t\t\treturn 0;\n+\t\t} else if (ch != ' ' && ch != '\\n') {\n+\t\t\tunreadc();\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+}\n+\n+/* returns 0 if the list is missing or empty (and skips over it), 1 if\n+ * its present and has values */\n+static int have_optional() {\n+\tif (read_list()) return 0;\n+\tfor (;;) {\n+\t\tint ch = readc();\n+\t\tif (ch == ')') {\n+\t\t\tif (verbose) fprintf(stderr, \" )\");\n+\t\t\treturn 0;\n+\t\t} else if (ch != ' ' && ch != '\\n') {\n+\t\t\tunreadc();\n+\t\t\treturn 1;\n+\t\t}\n+\t}\n+}\n+\n+/* returns NULL if it can't find an atom, string only valid until next\n+ * call to read_word, not thread-safe */\n+static const char *read_word() {\n+\tstatic char buf[256];\n+\tint bufsz = 0;\n+\tint ch;\n+\n+\tfor (;;) {\n+\t\tch = readc();\n+\t\tif (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')) {\n+\t\t\tbreak;\n+\t\t} else if (ch != ' ' && ch != '\\n') {\n+\t\t\tunreadc();\n+\t\t\treturn NULL;\n+\t\t}\n+\t}\n+\n+\twhile (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')\n+\t\t\t|| ('0' <= ch && ch <= '9')\n+\t\t\t|| ch == '-') {\n+\t\tif (bufsz >= sizeof(buf))\n+\t\t\tdie(_(\"atom too long\"));\n+\n+\t\tbuf[bufsz++] = ch;\n+\t\tch = readc();\n+\t}\n+\n+\tunreadc();\n+\tbuf[bufsz] = '\\0';\n+\tif (verbose) fprintf(stderr, \" %s\", buf);\n+\treturn bufsz ? buf : NULL;\n+}\n+\n+/* returns -1 if no string or an invalid string */\n+static int read_string(struct strbuf* s) {\n+\tsize_t i;\n+\tssize_t n = read_number();\n+\tif (n < 0 || unsigned_add_overflows(s->len, (size_t) n))\n+\t\treturn -1;\n+\tif (readc() != ':')\n+\t\tdie(_(\"malformed string\"));\n+\tif (verbose)\n+\t\tfprintf(stderr, \":\");\n+\n+\tstrbuf_grow(s, s->len + n);\n+\n+\ti = 0;\n+\twhile (i < n) {\n+\t\tssize_t r = read_svn(s->buf + s->len, n-i);\n+\t\tif (r < 0)\n+\t\t\tdie_errno(\"read error\");\n+\t\tif (r == 0)\n+\t\t\tdie(\"short read\");\n+\t\tstrbuf_setlen(s, s->len + r);\n+\t\ti += r;\n+\t}\n+\n+\tif (verbose) print_ascii(s->buf + s->len - n, n, MAX_PRINT_LEN);\n+\n+\treturn 0;\n+}\n+\n+static int skip_string() {\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tint r = read_string(&buf);\n+\tstrbuf_release(&buf);\n+\treturn r;\n+}\n+\n+static void read_end() {\n+\tint parens = 1;\n+\twhile (parens > 0) {\n+\t\tint ch = readc();\n+\t\tif (ch == EOF)\n+\t\t\tdie(_(\"socket close whilst looking for list close\"));\n+\n+\t\tif (ch == '(') {\n+\t\t\tif (verbose) fprintf(stderr, \" (\");\n+\t\t\tparens++;\n+\t\t} else if (ch == ')') {\n+\t\t\tif (verbose) fprintf(stderr, \" )\");\n+\t\t\tparens--;\n+\t\t} else if (ch == ' ' || ch == '\\n') {\n+\t\t\t/* whitespace */\n+\t\t} else if ('0' <= ch && ch <= '9') {\n+\t\t\t/* number or string */\n+\t\t\tsize_t n;\n+\t\t\tchar buf[4096];\n+\t\t\tint toprint = verbose ? MAX_PRINT_LEN : 0;\n+\n+\t\t\tunreadc();\n+\t\t\tn = read_number();\n+\n+\t\t\tch = readc();\n+\t\t\tif (ch != ':') {\n+\t\t\t\t/* number */\n+\t\t\t\tunreadc();\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* string */\n+\t\t\tif (verbose) fputc(':', stderr);\n+\t\t\twhile (n) {\n+\t\t\t\tssize_t r = read_svn(buf, min(n, sizeof(buf)));\n+\t\t\t\tif (r <= 0) die_errno(\"read\");\n+\t\t\t\tif (toprint > 0) {\n+\t\t\t\t\ttoprint -= print_ascii(buf, r, toprint);\n+\t\t\t\t}\n+\t\t\t\tn -= r;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tunreadc();\n+\t\t\tif (!read_word())\n+\t\t\t\tdie(_(\"unexpected character %c\"), ch);\n+\t\t}\n+\t}\n+}\n+\n+static const char* read_command() {\n+\tconst char *cmd;\n+\n+\tif (read_list()) goto err;\n+\n+\tcmd = read_word();\n+\tif (!cmd) goto err;\n+\tif (read_list()) goto err;\n+\n+\treturn cmd;\n+err:\n+\tdie(_(\"malformed response\"));\n+}\n+\n+static void read_command_end() {\n+\tread_end();\n+\tread_end();\n+\tif (verbose) fprintf(stderr, \"\\n\");\n+}\n+\n+static void read_done() {\n+\tconst char* s = read_word();\n+\tif (!s || strcmp(s, \"done\"))\n+\t\tdie(\"unexpected failure\");\n+\tif (verbose) fputc('\\n', stderr);\n+}\n+\n+static void read_success() {\n+\tconst char* s = read_command();\n+\tif (strcmp(s, \"success\")) {\n+\t\tverbose = 1;\n+\t\tread_end();\n+\t\tdie(\"unexpected failure\");\n+\t}\n+\tread_command_end();\n+}\n+\n+static void cram_md5(const char* user, const char* pass) {\n+\tconst char *s;\n+\tunsigned char hash[16];\n+\tstruct strbuf chlg = STRBUF_INIT;\n+\tHMAC_CTX hmac;\n+\n+\ts = read_command();\n+\tif (strcmp(s, \"step\")) goto error;\n+\tif (read_string(&chlg)) goto error;\n+\n+\tread_command_end();\n+\n+\tHMAC_Init(&hmac, (unsigned char*) pass, strlen(pass), EVP_md5());\n+\tHMAC_Update(&hmac, (unsigned char*) chlg.buf, chlg.len);\n+\tHMAC_Final(&hmac, hash, NULL);\n+\tHMAC_CTX_cleanup(&hmac);\n+\n+\tsendf(\"%d:%s %s\\n\", (int) (strlen(user) + 1 + 32), user, md5_to_hex(hash));\n+\n+\tstrbuf_release(&chlg);\n+\treturn;\n+\n+error:\n+\tdie(_(\"auth failed\"));\n+}\n+\n+static void read_name(struct strbuf* name) {\n+\tstrbuf_reset(name);\n+\tif (read_string(name)) goto err;\n+\tif (name->buf[0] == '/') strbuf_remove(name, 0, 1);\n+\tif (memchr(name->buf, '\\0', name->len)) goto err;\n+\tif (strstr(name->buf, \"//\")) goto err;\n+\tif (!strcmp(name->buf, \"..\")) goto err;\n+\tif (!strcmp(name->buf, \".\")) goto err;\n+\tif (!prefixcmp(name->buf, \"../\")) goto err;\n+\tif (!prefixcmp(name->buf, \"./\")) goto err;\n+\tif (strstr(name->buf, \"/../\")) goto err;\n+\tif (strstr(name->buf, \"/./\")) goto err;\n+\tif (!suffixcmp(name->buf, \"/..\")) goto err;\n+\tif (!suffixcmp(name->buf, \"/.\")) goto err;\n+\n+\treturn;\n+err:\n+\tdie(\"invalid path name %s\", name->buf);\n+}\n+\n+static const char* cmt_to_hex(struct commit* c) {\n+\treturn sha1_to_hex(c ? c->object.sha1 : null_sha1);\n+}\n+\n+static const unsigned char* cmt_sha1(struct commit* c) {\n+\treturn c ? c->object.sha1 : null_sha1;\n+}\n+\n+static int parse_svnrev(struct commit* c) {\n+\tchar* p = strstr(c->buffer, \"\\nrevision \");\n+\tif (!p) die(\"invalid svn commit %s\", cmt_to_hex(c));\n+\tp += strlen(\"\\nrevision \");\n+\treturn atoi(p);\n+}\n+\n+static void parse_svnpath(struct commit* c, struct strbuf* buf) {\n+\tchar* e;\n+\tchar* p = strstr(c->buffer, \"\\npath \");\n+\tif (!p) die(\"invalid svn commit %s\", cmt_to_hex(c));\n+\tp += strlen(\"\\npath \");\n+\te = strchr(p, '\\n');\n+\tif (!e) die(\"invalid svn commit %s\", cmt_to_hex(c));\n+\tstrbuf_add(buf, p, e-p);\n+}\n+\n+static struct commit* svn_commit(struct commit* c) {\n+\tif (parse_commit(c) || !c->parents || !c->parents->item)\n+\t\tdie(\"invalid svn commit %s\", cmt_to_hex(c));\n+\t/* In the case of no git commit, but we have a previous svn\n+\t * commit, the svn parent is repeated twice. That way we can\n+\t * distinguish that case from a git commit but no svn commit */\n+\tif (c->parents->next && c->parents->item == c->parents->next->item) {\n+\t\treturn NULL;\n+\t}\n+\treturn c->parents->item;\n+}\n+\n+static struct commit* svn_parent(struct commit* c) {\n+\tif (parse_commit(c) || !c->parents)\n+\t\tdie(\"invalid svn commit %s\", cmt_to_hex(c));\n+\treturn c->parents->next ? c->parents->next->item : NULL;\n+}\n+\n+struct svnref {\n+\tstruct strbuf svn; /* svn root */\n+\tstruct strbuf ref; /* svn ref path */\n+\tstruct strbuf remote; /* remote ref path */\n+\n+\tstruct index_state git_index, svn_index;\n+\tstruct tree *git_tree, *svn_tree;\n+\n+\tunsigned int delete : 1;\n+\tunsigned int istag : 1;\n+\n+\tstruct commit* svncmt; /* current value of svn ref */\n+\tstruct object* gitobj; /* current value of remote ref, may be tag or commit */\n+\tstruct commit* parent; /* parent git commit */\n+};\n+\n+static struct svnref** refs;\n+static size_t refn, refalloc;\n+\n+static int is_in_dir(char* file, const char* dir, char** rel) {\n+\tsize_t sz = strlen(dir);\n+\tif (strncmp(file, dir, sz)) return 0;\n+\tif (file[sz] && file[sz] != '/') return 0;\n+\tif (rel) *rel = file[sz] ? &file[sz+1] : &file[sz];\n+\treturn 1;\n+}\n+\n+#define TRUNK_REF 0\n+#define BRANCH_REF 1\n+#define TAG_REF 2\n+\n+static void add_refname(struct strbuf* buf, const char* name) {\n+\twhile (*name) {\n+\t\tint ch = *(name++);\n+\t\tif (ch <= ' '\n+\t\t\t|| ch == 0x7F\n+\t\t\t|| ch == '~'\n+\t\t\t|| ch == '^'\n+\t\t\t|| ch == ':'\n+\t\t\t|| ch == '\\\\'\n+\t\t\t|| ch == '*'\n+\t\t\t|| ch == '?'\n+\t\t\t|| ch == '[') {\n+\t\t\tstrbuf_addch(buf, '_');\n+\t\t} else {\n+\t\t\tstrbuf_addch(buf, ch);\n+\t\t}\n+\t}\n+}\n+\n+static void checkout_tree(struct index_state* idx, struct tree* t) {\n+\tstruct tree_desc desc;\n+\tstruct unpack_trees_options op;\n+\n+\tif (!t) return;\n+\n+\tif (parse_tree(t))\n+\t\tdie(\"failed to checkout %s\", sha1_to_hex(t->object.sha1));\n+\n+\tinit_tree_desc(&desc, t->buffer, t->size);\n+\n+\tmemset(&op, 0, sizeof(op));\n+\top.src_index = idx;\n+\top.dst_index = idx;\n+\top.index_only = 1;\n+\n+\tif (unpack_trees(1, &desc, &op))\n+\t\tdie(\"failed to checkout %s\", sha1_to_hex(t->object.sha1));\n+}\n+\n+static struct index_state* git_index(struct svnref* r) {\n+\tcheckout_tree(&r->git_index, r->git_tree);\n+\tr->git_tree = NULL;\n+\treturn &r->git_index;\n+}\n+\n+static struct index_state* svn_index(struct svnref* r) {\n+\tcheckout_tree(&r->svn_index, r->svn_tree);\n+\tr->svn_tree = NULL;\n+\treturn &r->svn_index;\n+}\n+\n+static const unsigned char *idx_sha1(struct index_state* idx) {\n+\tif (!idx->cache_tree)\n+\t\tidx->cache_tree = cache_tree();\n+\tif (cache_tree_update(idx->cache_tree, idx->cache, idx->cache_nr, 0))\n+\t\tdie(\"failed to update cache tree\");\n+\n+\treturn idx->cache_tree->sha1;\n+}\n+\n+static void checkout_svncmt(struct svnref* r, struct commit* svncmt) {\n+\tstruct commit* gitcmt;\n+\n+\t/* R (replace) log entries may already have content that\n+\t * we need to clear first */\n+\n+\tdiscard_index(&r->git_index);\n+\tdiscard_index(&r->svn_index);\n+\tr->git_tree = NULL;\n+\tr->svn_tree = NULL;\n+\n+\t/* Note r->svncmt may not equal c if this creates a branch or\n+\t * replaces an existing one. c will be the new source whereas\n+\t * svncmt will continue to point to the old svn commit */\n+\n+\tif (svncmt && parse_commit(svncmt))\n+\t\tdie(\"invalid object %s\", cmt_to_hex(svncmt));\n+\n+\tr->svn_tree = svncmt ? svncmt->tree : NULL;\n+\tgitcmt = svncmt ? svn_commit(svncmt) : NULL;\n+\n+\tif (gitcmt && parse_commit(gitcmt))\n+\t\tdie(\"invalid object %s\", cmt_to_hex(gitcmt));\n+\n+\tr->git_tree = gitcmt ? gitcmt->tree : NULL;\n+\tr->parent = gitcmt;\n+}\n+\n+static struct svnref* create_ref(int type, const char* name) {\n+\tstruct svnref* r = NULL;\n+\tunsigned char sha1[20];\n+\n+\tswitch (type) {\n+\tcase TRUNK_REF:\n+\t\tr = xcalloc(1, sizeof(*r));\n+\t\tstrbuf_addstr(&r->svn, trunk ? trunk : \"\");\n+\t\tstrbuf_addstr(&r->ref, \"refs/svn/heads/trunk\");\n+\t\tstrbuf_addstr(&r->remote, remoteheads);\n+\t\tstrbuf_addstr(&r->remote, trunkref);\n+\t\tbreak;\n+\n+\tcase BRANCH_REF:\n+\t\tr = xcalloc(1, sizeof(*r));\n+\n+\t\tstrbuf_addstr(&r->svn, branches);\n+\t\tstrbuf_addch(&r->svn, '/');\n+\t\tstrbuf_addstr(&r->svn, name);\n+\n+\t\tstrbuf_addstr(&r->ref, \"refs/svn/heads/\");\n+\t\tadd_refname(&r->ref, name);\n+\n+\t\tstrbuf_addstr(&r->remote, remoteheads);\n+\t\tadd_refname(&r->remote, name);\n+\t\tbreak;\n+\n+\tcase TAG_REF:\n+\t\tr = xcalloc(1, sizeof(*r));\n+\t\tr->istag = 1;\n+\n+\t\tstrbuf_addstr(&r->svn, tags);\n+\t\tstrbuf_addch(&r->svn, '/');\n+\t\tstrbuf_addstr(&r->svn, name);\n+\n+\t\tstrbuf_addstr(&r->ref, \"refs/svn/tags/\");\n+\t\tadd_refname(&r->ref, name);\n+\n+\t\tstrbuf_addstr(&r->remote, remotetags);\n+\t\tadd_refname(&r->remote, name);\n+\t\tbreak;\n+\t}\n+\n+\tif (!read_ref(r->remote.buf, sha1) && !is_null_sha1(sha1)) {\n+\t\tr->gitobj = parse_object(sha1);\n+\t\tif (!r->gitobj)\n+\t\t\tdie(\"invalid ref %s\", name);\n+\t}\n+\n+\tif (!read_ref(r->ref.buf, sha1) && !is_null_sha1(sha1)) {\n+\t\tr->svncmt = lookup_commit(sha1);\n+\t\tif (parse_commit(r->svncmt))\n+\t\t\tdie(\"invalid ref %s\", name);\n+\t\tcheckout_svncmt(r, r->svncmt);\n+\t}\n+\n+\tALLOC_GROW(refs, refn + 1, refalloc);\n+\trefs[refn++] = r;\n+\treturn r;\n+}\n+\n+static struct svnref* find_svnref_by_path(struct strbuf* name) {\n+\tint i;\n+\tstruct svnref* r;\n+\tchar *a, *b, *c, *d;\n+\n+\tif (!trunk && !branches && !tags && refn) {\n+\t\treturn refs[0];\n+\t}\n+\n+\tfor (i = 0; i < refn; i++) {\n+\t\tr = refs[i];\n+\t\tif (prefixcmp(name->buf, r->svn.buf)) {\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tswitch (name->buf[r->svn.len]) {\n+\t\tcase '\\0':\n+\t\t\tstrbuf_setlen(name, 0);\n+\t\t\treturn r;\n+\t\tcase '/':\n+\t\t\tstrbuf_remove(name, 0, r->svn.len + 1);\n+\t\t\treturn r;\n+\t\t}\n+\t}\n+\n+\t/* names are of the form\n+\t * branches/foo/...\n+\t * a        b  c   d\n+\t */\n+\ta = name->buf;\n+\td = name->buf + name->len;\n+\n+\tif (!trunk && !branches && !tags) {\n+\t\treturn create_ref(TRUNK_REF, NULL);\n+\n+\t} else if (trunk && is_in_dir(a, trunk, &b)) {\n+\t\tstrbuf_remove(name, 0, b - a);\n+\t\treturn create_ref(TRUNK_REF, NULL);\n+\n+\n+\t} else if (branches && is_in_dir(a, branches, &b) && *b) {\n+\t\tc = memchr(b, '/', d - b);\n+\t\tif (c) {\n+\t\t\t*c = '\\0';\n+\t\t\tr = create_ref(BRANCH_REF, b);\n+\t\t\tstrbuf_remove(name, 0, c+1 - a);\n+\t\t} else {\n+\t\t\tr = create_ref(BRANCH_REF, b);\n+\t\t\tstrbuf_reset(name);\n+\t\t}\n+\t\treturn r;\n+\n+\t} else if (tags && is_in_dir(a, tags, &b) && *b) {\n+\t\tc = memchr(b, '/', d - b);\n+\t\tif (c) {\n+\t\t\t*c = '\\0';\n+\t\t\tr = create_ref(TAG_REF, b);\n+\t\t\tstrbuf_remove(name, 0, c+1 - a);\n+\t\t} else {\n+\t\t\tr = create_ref(TAG_REF, b);\n+\t\t\tstrbuf_reset(name);\n+\t\t}\n+\t\treturn r;\n+\n+\t} else {\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static struct svnref* find_svnref_by_refname(const char* name) {\n+\tint i;\n+\tunsigned char sha1[20];\n+\n+\tif (prefixcmp(name, \"refs/\")) {\n+\t\tchar* real_ref;\n+\t\tint refcount = dwim_ref(name, strlen(name), sha1, &real_ref);\n+\n+\t\tif (refcount > 1) {\n+\t\t\tdie(\"ambiguous ref '%s'\", name);\n+\t\t} else if (!refcount) {\n+\t\t\tdie(\"can not find ref '%s'\", name);\n+\t\t}\n+\n+\t\tname = real_ref;\n+\t}\n+\n+\tfor (i = 0; i < refn; i++) {\n+\t\tstruct svnref* r = refs[i];\n+\t\tif (!strcmp(r->remote.buf, name)) {\n+\t\t\treturn r;\n+\t\t}\n+\t}\n+\n+\tif (!prefixcmp(name, remoteheads)) {\n+\t\tname += strlen(remoteheads);\n+\n+\t\tif (!strcmp(name, trunkref)) {\n+\t\t\treturn create_ref(TRUNK_REF, NULL);\n+\t\t} else if (!branches) {\n+\t\t\tdie(\"in order to push a branch, svn.branches must be set\");\n+\t\t} else {\n+\t\t\treturn create_ref(BRANCH_REF, name);\n+\t\t}\n+\n+\t} else if (!prefixcmp(name, remotetags)) {\n+\t\tname += strlen(remotetags);\n+\t\tif (!tags)\n+\t\t\tdie(\"in order to push a tag, svn.tags must be set\");\n+\n+\t\treturn create_ref(TAG_REF, name);\n+\t}\n+\n+\tdie(\"can not find ref '%s'\", name);\n+}\n+\n+static struct commit* find_svncmt(struct svnref* r, int rev) {\n+\tstruct commit* c = r->svncmt;\n+\n+\twhile (c && parse_svnrev(c) > rev) {\n+\t\tc = svn_parent(c);\n+\t\tif (c && parse_commit(c)) {\n+\t\t\tdie(\"invalid commit %s\", cmt_to_hex(c));\n+\t\t}\n+\t}\n+\n+\treturn c;\n+}\n+\n+/* reads a path, revision pair */\n+static struct svnref* read_copy_source(struct strbuf* name, int* rev) {\n+\tint64_t srev;\n+\tstruct svnref* sref;\n+\n+\t/* copy-path */\n+\tread_name(name);\n+\tsref = find_svnref_by_path(name);\n+\tif (!sref) return NULL;\n+\n+\t/* copy-rev */\n+\tsrev = read_number();\n+\tif (srev < 0 || srev > INT_MAX) goto err;\n+\t*rev = srev;\n+\n+\treturn sref;\n+err:\n+\tdie(\"invalid copy source\");\n+}\n+\n+static int create_ref_cb(const char* refname, const unsigned char* sha1, int flags, void* cb_data) {\n+\tint i;\n+\tfor (i = 0; i < refn; i++) {\n+\t\tconst char *s = refs[i]->ref.buf;\n+\t\tif (!prefixcmp(s, \"refs/svn/heads/\") && !strcmp(s + strlen(\"refs/svn/heads/\"), refname)) {\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\tif (!strcmp(refname, \"trunk\")) {\n+\t\tcreate_ref(TRUNK_REF, \"\");\n+\t} else if (branches) {\n+\t\tcreate_ref(BRANCH_REF, refname);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+#define SEEN_FROM_OBJ 1\n+#define SEEN_FROM_SVN 2\n+#define SEEN_FROM_BOTH (SEEN_FROM_OBJ | SEEN_FROM_SVN)\n+#define SVNCMT 4\n+\n+static void insert_commit(struct commit *c, struct commit_list **cmts) {\n+\tif (parse_commit(c)) {\n+\t\tdie(\"invalid commit %s\", sha1_to_hex(c->object.sha1));\n+\t}\n+\tcommit_list_insert_by_date(c, cmts);\n+}\n+\n+static void insert_svncmt(struct commit *sc, struct commit_list **cmts) {\n+\tstruct commit *gc;\n+\tstruct commit *sc2;\n+\tif (!sc) return;\n+\n+\tsc->object.flags = SVNCMT;\n+\tinsert_commit(sc, cmts);\n+\n+\t/* If two or more svn commits point to the same git commit, then\n+\t * we use the newest one. This is in line with using the newest\n+\t * svn revision we can find. */\n+\tgc = svn_commit(sc);\n+\tif (!gc) return;\n+\n+\tsc2 = (struct commit*) ((gc->object.flags & SEEN_FROM_SVN) ? gc->util : NULL);\n+\tif (!sc2 || sc->date > sc2->date) {\n+\t\tgc->object.flags |= SEEN_FROM_SVN;\n+\t\tgc->util = sc;\n+\t\tinsert_commit(gc, cmts);\n+\t}\n+}\n+\n+static void add_roots(struct commit_list **cmts, struct object *obj) {\n+\tstatic int all_refs_added;\n+\n+\tint i;\n+\tstruct commit *c = (struct commit*) deref_tag(obj, NULL, 0);\n+\tif (!c || c->object.type != OBJ_COMMIT) {\n+\t\tdie(\"invalid object %s\", sha1_to_hex(obj->sha1));\n+\t}\n+\n+\tc->object.flags = SEEN_FROM_OBJ;\n+\tinsert_commit(c, cmts);\n+\n+\tif (!all_refs_added) {\n+\t\tfor_each_ref_in(\"refs/svn/heads/\", &create_ref_cb, NULL);\n+\t\tall_refs_added = 1;\n+\t}\n+\n+\tfor (i = 0; i < refn; i++) {\n+\t\tinsert_svncmt(refs[i]->svncmt, cmts);\n+\t}\n+}\n+\n+/* Searches back from the new object. Looking for the previous svn\n+ * commit or failing that the newest svn commit.\n+ */\n+static struct commit* find_copy_source(struct svnref* r, struct object* obj) {\n+\tstruct commit_list *cmts = NULL;\n+\tstruct commit *end = r->svncmt ? svn_commit(r->svncmt) : NULL;\n+\tstruct commit *ret = NULL;\n+\n+\tadd_roots(&cmts, obj);\n+\twhile (cmts) {\n+\t\tstruct commit *c = pop_commit(&cmts);\n+\t\tint both = (c->object.flags & SEEN_FROM_BOTH) == SEEN_FROM_BOTH;\n+\n+\t\tif (both && c == end) {\n+\t\t\tret = (struct commit*) c->util;\n+\t\t\tbreak;\n+\n+\t\t} else if (c == end) {\n+\t\t\tend = NULL;\n+\t\t\tif (ret) break;\n+\n+\t\t} else if (both && !ret) {\n+\t\t\t/* commits are processed newest first hence !ret */\n+\t\t\tret = (struct commit*) c->util;\n+\t\t\tif (end == NULL) break;\n+\n+\t\t} else if (c->object.flags & SVNCMT) {\n+\t\t\tinsert_svncmt(svn_parent(c), &cmts);\n+\n+\t\t} else if (c->object.flags & SEEN_FROM_OBJ) {\n+\t\t\tstruct commit_list *p = c->parents;\n+\n+\t\t\twhile (p) {\n+\t\t\t\tc = p->item;\n+\t\t\t\tc->object.flags |= SEEN_FROM_OBJ;\n+\t\t\t\tinsert_commit(c, &cmts);\n+\t\t\t\tp = p->next;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfree_commit_list(cmts);\n+\tclear_object_flags(SEEN_FROM_BOTH | SVNCMT);\n+\n+\treturn ret;\n+}\n+\n+static void read_add_dir(struct svnref* r, int rev) {\n+\t/* path, parent-token, child-token, [copy-path, copy-rev] */\n+\n+\tstruct strbuf name = STRBUF_INIT;\n+\tstruct strbuf srcname = STRBUF_INIT;\n+\tstruct cache_entry* ce;\n+\tchar* p;\n+\tsize_t dlen;\n+\tint files = 0;\n+\n+\tread_name(&name);\n+\tfind_svnref_by_path(&name);\n+\tfprintf(stderr, \"A %s\\n\", name.buf);\n+\n+\tif (name.len) strbuf_addch(&name, '/');\n+\tdlen = name.len;\n+\n+\tstrbuf_setlen(&name, dlen);\n+\n+\t/* empty folder - add ./.gitempty */\n+\tif (files == 0 && dlen) {\n+\t\tunsigned char sha1[20];\n+\t\tif (write_sha1_file(NULL, 0, \"blob\", sha1))\n+\t\t\tdie(\"failed to write .gitempty object\");\n+\t\tstrbuf_addstr(&name, \".gitempty\");\n+\t\tce = make_cache_entry(create_ce_mode(0644), sha1, name.buf, 0, 0);\n+\t\tadd_index_entry(git_index(r), ce, ADD_CACHE_OK_TO_ADD);\n+\t}\n+\n+\t/* remove ../.gitempty */\n+\tif (dlen) {\n+\t\tstrbuf_setlen(&name, dlen - 1);\n+\t\tp = strrchr(name.buf, '/');\n+\t\tif (p) {\n+\t\t\tstrbuf_setlen(&name, p - name.buf);\n+\t\t\tstrbuf_addstr(&name, \"/.gitempty\");\n+\t\t\tremove_file_from_index(git_index(r), name.buf);\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&srcname);\n+\tstrbuf_release(&name);\n+}\n+\n+static void read_add_file(struct svnref* r, int rev, struct strbuf* name, void** srcp, size_t* srcsz) {\n+\t/* name, dir-token, file-token, [copy-path, copy-rev] */\n+\tstruct strbuf srcname = STRBUF_INIT;\n+\tchar* p;\n+\n+\tread_name(name);\n+\tfind_svnref_by_path(name);\n+\tfprintf(stderr, \"A %s\\n\", name->buf);\n+\n+\t/* remove ./.gitempty */\n+\tp = strrchr(name->buf, '/');\n+\tif (p) {\n+\t\tstruct strbuf empty = STRBUF_INIT;\n+\t\tstrbuf_add(&empty, name->buf, p - name->buf);\n+\t\tstrbuf_addstr(&empty, \"/.gitempty\");\n+\t\tremove_file_from_index(git_index(r), empty.buf);\n+\t\tstrbuf_release(&empty);\n+\t}\n+\n+\tstrbuf_release(&srcname);\n+}\n+\n+static void read_open_file(struct svnref* r, int rev, struct strbuf* name, void** srcp, size_t* srcsz) {\n+\t/* name, dir-token, file-token, rev */\n+\tenum object_type type;\n+\tstruct cache_entry* ce;\n+\tunsigned long srcn;\n+\n+\tread_name(name);\n+\tfind_svnref_by_path(name);\n+\tfprintf(stderr, \"M %s\\n\", name->buf);\n+\n+\tce = index_name_exists(svn_index(r), name->buf, name->len, 0);\n+\tif (!ce) goto err;\n+\n+\t*srcp = read_sha1_file(ce->sha1, &type, &srcn);\n+\tif (!srcp || type != OBJ_BLOB) goto err;\n+\t*srcsz = srcn;\n+\n+\treturn;\n+err:\n+\tdie(\"malformed update\");\n+}\n+\n+static void read_close_file(struct svnref* r, const char* name, const void* data, size_t sz) {\n+\t/* file-token, [text-checksum] */\n+\tstruct cache_entry* ce;\n+\tunsigned char sha1[20];\n+\tstruct strbuf buf = STRBUF_INIT;\n+\n+\tif (skip_string()) goto err; /* file-token */\n+\n+\tif (write_sha1_file(data, sz, \"blob\", sha1))\n+\t\tdie_errno(\"write blob\");\n+\n+\tif (have_optional()) {\n+\t\tunsigned char h1[16], h2[16];\n+\t\tMD5_CTX ctx;\n+\n+\t\tstrbuf_reset(&buf);\n+\t\tif (read_string(&buf)) goto err;\n+\t\tif (get_md5_hex(buf.buf, h1)) goto err;\n+\n+\t\tMD5_Init(&ctx);\n+\t\tMD5_Update(&ctx, data, sz);\n+\t\tMD5_Final(h2, &ctx);\n+\n+\t\tif (memcmp(h1, h2, sizeof(h1))) {\n+\t\t\tce = index_name_exists(svn_index(r), name, strlen(name), 0);\n+\t\t\tdie(\"hash mismatch for '%s', expected md5 %s, got md5 %s, old sha1 %s, new sha1 %s\",\n+\t\t\t\t\tname,\n+\t\t\t\t\tmd5_to_hex(h2),\n+\t\t\t\t\tmd5_to_hex(h1),\n+\t\t\t\t\tsha1_to_hex(ce ? ce->sha1 : null_sha1),\n+\t\t\t\t\tsha1_to_hex(sha1));\n+\t\t}\n+\n+\t\tread_end();\n+\t}\n+\n+\tce = make_cache_entry(0644, sha1, name, 0, 0);\n+\tif (!ce) die(\"make_cache_entry failed for path '%s'\", name);\n+\tadd_index_entry(svn_index(r), ce, ADD_CACHE_OK_TO_ADD);\n+\n+\tstrbuf_reset(&buf);\n+\tif (convert_to_git(name, data, sz, &buf, SAFE_CRLF_FALSE)) {\n+\t\tif (write_sha1_file(buf.buf, buf.len, \"blob\", sha1)) {\n+\t\t\tdie_errno(\"write blob\");\n+\t\t}\n+\t}\n+\tce = make_cache_entry(0644, sha1, name, 0, 0);\n+\tif (!ce) die(\"make_cache_entry failed for path '%s'\", name);\n+\tadd_index_entry(git_index(r), ce, ADD_CACHE_OK_TO_ADD);\n+\n+\tstrbuf_release(&buf);\n+\treturn;\n+\n+err:\n+\tdie(\"malformed update\");\n+}\n+\n+/* returns number of entries removed */\n+static int remove_index_path(struct index_state* idx, struct strbuf* name) {\n+\tint ret = 0;\n+\tint i = index_name_pos(idx, name->buf, name->len);\n+\n+\tif (i >= 0) {\n+\t\t/* file */\n+\t\tcache_tree_invalidate_path(idx->cache_tree, name->buf);\n+\t\tremove_index_entry_at(idx, i);\n+\t\treturn 1;\n+\t}\n+\n+\t/* we've got to re-lookup the path as a < a.c < a/c */\n+\tstrbuf_addch(name, '/');\n+\ti = -index_name_pos(idx, name->buf, name->len) - 1;\n+\n+\t/* directory, index_name_pos returns -first-1\n+\t * where first is the position the entry would\n+\t * be added at, and the cache is sorted */\n+\twhile (i < idx->cache_nr) {\n+\t\tstruct cache_entry* ce = idx->cache[i];\n+\t\tif (ce_namelen(ce) < name->len) break;\n+\t\tif (memcmp(ce->name, name->buf, name->len)) break;\n+\n+\t\tce->ce_flags |= CE_REMOVE;\n+\t\ti++;\n+\t\tret++;\n+\t}\n+\n+\tstrbuf_setlen(name, name->len - 1);\n+\n+\tif (ret) {\n+\t\tcache_tree_invalidate_path(idx->cache_tree, name->buf);\n+\t\tremove_marked_cache_entries(idx);\n+\t}\n+\n+\treturn ret;\n+}\n+\n+static void read_delete_entry(struct svnref* r, int rev) {\n+\t/* name, [revno], dir-token */\n+\tstruct strbuf name = STRBUF_INIT;\n+\n+\tread_name(&name);\n+\tfind_svnref_by_path(&name);\n+\tfprintf(stderr, \"D %s\\n\", name.buf);\n+\n+\tremove_index_path(svn_index(r), &name);\n+\tremove_index_path(git_index(r), &name);\n+\tif (!name.len) {\n+\t\tr->delete = 1;\n+\t}\n+\tstrbuf_release(&name);\n+\treturn;\n+}\n+\n+static void read_text_delta(struct strbuf *delta) {\n+\tfor (;;) {\n+\t\tconst char* s;\n+\n+\t\t/* finish off the previous textdelta-chunk or\n+\t\t * apply-textdelta */\n+\t\tread_command_end();\n+\n+\t\ts = read_command();\n+\n+\t\tif (!strcmp(s, \"textdelta-end\")) {\n+\t\t\t/* leave textdelta-end opened for read_update to\n+\t\t\t * close */\n+\t\t\treturn;\n+\t\t}\n+\n+\t\t/* if we get some other command we just loop around\n+\t\t * again */\n+\t\tif (strcmp(s, \"textdelta-chunk\")) {\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t/* file-token, chunk */\n+\t\tif (skip_string() || read_string(delta))\n+\t\t\tdie(\"invalid textdelta command\");\n+\t}\n+}\n+\n+#define MAX_VARINT_LEN 9\n+\n+static unsigned char* parse_varint(unsigned char *p, unsigned char *e, size_t *v) {\n+\t*v = 0;\n+\tfor (;;) {\n+\t\tif (p == e || *v > (INT64_MAX >> 7))\n+\t\t\tdie(\"invalid svndiff\");\n+\n+\t\t*v = (*v << 7) | (*p & 0x7F);\n+\n+\t\tif (!(*(p++) & 0x80))\n+\t\t\treturn p;\n+\t}\n+}\n+\n+static unsigned char* encode_varint(unsigned char* p, size_t n) {\n+\tif (n < 0) die(\"int too large\");\n+\tif (n >= (INT64_C(1) << 56)) *(p++) = ((n >> 56) & 0x7F) | 0x80;\n+\tif (n >= (INT64_C(1) << 49)) *(p++) = ((n >> 49) & 0x7F) | 0x80;\n+\tif (n >= (INT64_C(1) << 42)) *(p++) = ((n >> 42) & 0x7F) | 0x80;\n+\tif (n >= (INT64_C(1) << 35)) *(p++) = ((n >> 35) & 0x7F) | 0x80;\n+\tif (n >= (INT64_C(1) << 28)) *(p++) = ((n >> 28) & 0x7F) | 0x80;\n+\tif (n >= (INT64_C(1) << 21)) *(p++) = ((n >> 21) & 0x7F) | 0x80;\n+\tif (n >= (INT64_C(1) << 14)) *(p++) = ((n >> 14) & 0x7F) | 0x80;\n+\tif (n >= (INT64_C(1) << 7)) *(p++) = ((n >> 7) & 0x7F) | 0x80;\n+\t*(p++) = n & 0x7F;\n+\treturn p;\n+}\n+\n+static size_t encoded_length(size_t n) {\n+\tunsigned char b[MAX_VARINT_LEN];\n+\treturn encode_varint(b, n) - b;\n+}\n+\n+#define FROM_SOURCE (0 << 6)\n+#define FROM_TARGET (1 << 6)\n+#define FROM_NEW    (2 << 6)\n+\n+static unsigned char* parse_instruction(unsigned char *p, unsigned char *e, int* ins, size_t* off, size_t* len) {\n+\tint hdr;\n+\n+\tif (p >= e) die(\"invalid svndiff\");\n+\thdr = *p++;\n+\n+\t*len = hdr & 0x3F;\n+\tif (*len == 0) {\n+\t\tp = parse_varint(p, e, len);\n+\t}\n+\n+\t*ins = hdr & 0xC0;\n+\t*off = 0;\n+\tif (*ins == FROM_SOURCE || *ins == FROM_TARGET) {\n+\t\tp = parse_varint(p, e, off);\n+\t}\n+\n+\treturn p;\n+}\n+\n+#define MAX_INS_LEN (1 + 2 * MAX_VARINT_LEN)\n+\n+static unsigned char* encode_instruction(unsigned char* p, int ins, size_t off, size_t len) {\n+\tif (len < 0x3F) {\n+\t\t*(p++) = ins | len;\n+\t} else {\n+\t\t*(p++) = ins;\n+\t\tp = encode_varint(p, len);\n+\t}\n+\n+\tif (ins == FROM_SOURCE || ins == FROM_TARGET) {\n+\t\tp = encode_varint(p, off);\n+\t}\n+\n+\treturn p;\n+}\n+\n+static unsigned char *parse_svndiff_chunk(unsigned char *p, size_t *sz, struct strbuf *buf, int ver) {\n+\tunsigned char *e = p + *sz;\n+\tsize_t inflated = *sz;\n+\tz_stream z;\n+\n+\tif (ver > 0) {\n+\t\tp = parse_varint(p, e, &inflated);\n+\t}\n+\n+\t*sz = inflated;\n+\tif (p + inflated == e)\n+\t\treturn p;\n+\n+\tmemset(&z, 0, sizeof(z));\n+\tinflateInit(&z);\n+\n+\tstrbuf_grow(buf, inflated);\n+\n+\tz.next_in = p;\n+\tz.avail_in = e - p;\n+\tz.next_out = (unsigned char*) buf->buf;\n+\tz.avail_out = inflated;\n+\n+\tif (inflate(&z, Z_FINISH) != Z_STREAM_END) {\n+\t\tdie(\"zlib error\");\n+\t}\n+\tstrbuf_setlen(buf, inflated - z.avail_out);\n+\tinflateEnd(&z);\n+\n+\treturn (unsigned char*) buf->buf;\n+}\n+\n+static unsigned char* apply_svndiff_win(struct strbuf *tgt, const void *src, size_t sz, unsigned char *d, unsigned char *e, int ver) {\n+\tstruct strbuf insbuf = STRBUF_INIT;\n+\tstruct strbuf databuf = STRBUF_INIT;\n+\tunsigned char *insp, *inse, *datap, *datae;\n+\tsize_t srco, srcl, tgtl, insl, datal, w = 0;\n+\n+\td = parse_varint(d, e, &srco);\n+\td = parse_varint(d, e, &srcl);\n+\td = parse_varint(d, e, &tgtl);\n+\td = parse_varint(d, e, &insl);\n+\td = parse_varint(d, e, &datal);\n+\n+\tif (unsigned_add_overflows(srco, srcl) || srco + srcl > sz)\n+\t\tgoto err;\n+\n+\tif (unsigned_add_overflows(insl, datal) || insl + datal > e - d)\n+\t\tgoto err;\n+\n+\tinsp = d;\n+\tdatap = insp + insl;\n+\td = datap + datal;\n+\n+\tinsp = parse_svndiff_chunk(insp, &insl, &insbuf, ver);\n+\tdatap = parse_svndiff_chunk(datap, &datal, &databuf, ver);\n+\n+\tinse = insp + insl;\n+\tdatae = datap + datal;\n+\n+\tstrbuf_grow(tgt, tgt->len + tgtl);\n+\n+\twhile (insp < inse) {\n+\t\tsize_t off, len;\n+\t\tssize_t tgtr;\n+\t\tint ins;\n+\n+\t\tinsp = parse_instruction(insp, inse, &ins, &off, &len);\n+\n+\t\tswitch (ins) {\n+\t\tcase FROM_SOURCE:\n+\t\t\tif (off > srcl || len > srcl - off) goto err;\n+\t\t\tstrbuf_add(tgt, (char*) src + srco + off, len);\n+\t\t\tbreak;\n+\n+\t\tcase FROM_TARGET:\n+\t\t\ttgtr = min(w - off, len);\n+\t\t\tif (tgtr <= 0) goto err;\n+\n+\t\t\toff = tgt->len - w + off;\n+\n+\t\t\t/* len may be greater than tgtr. In this case we\n+\t\t\t * just repeat [tgto,tgto+tgtr]\n+\t\t\t */\n+\t\t\twhile (len) {\n+\t\t\t\tint n = min(len, tgtr);\n+\t\t\t\tstrbuf_add(tgt, tgt->buf + off, n);\n+\t\t\t\tlen -= n;\n+\t\t\t}\n+\t\t\tbreak;\n+\n+\t\tcase FROM_NEW:\n+\t\t\tif (datae - datap < len) goto err;\n+\t\t\tstrbuf_add(tgt, datap, len);\n+\t\t\tdatap += len;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tgoto err;\n+\t\t}\n+\n+\t\tw += len;\n+\t}\n+\n+\tif (w != tgtl || datap != datae) goto err;\n+\n+\tstrbuf_release(&insbuf);\n+\tstrbuf_release(&databuf);\n+\treturn d;\n+err:\n+\tdie(\"invalid svndiff\");\n+}\n+\n+static void apply_svndiff(struct strbuf *tgt, const void *src, size_t sz, const void *delta, size_t dsz) {\n+\tunsigned char *d = (unsigned char*) delta;\n+\tunsigned char *e = d + dsz;\n+\tint ver;\n+\n+\tif (dsz < 4 || memcmp(d, \"SVN\", 3))\n+\t\tgoto err;\n+\n+\tver = d[3];\n+\tif (ver > 1)\n+\t\tgoto err;\n+\n+\td += 4;\n+\n+\twhile (d < e) {\n+\t\td = apply_svndiff_win(tgt, src, sz, d, e, ver);\n+\t}\n+\n+\treturn;\n+\n+err:\n+\tdie(_(\"invalid svndiff\"));\n+}\n+\n+static void read_update(struct svnref* r, int rev) {\n+\tstruct strbuf name = STRBUF_INIT;\n+\tstruct strbuf tgt = STRBUF_INIT;\n+\tvoid* src = NULL;\n+\tsize_t srcsz = 0;\n+\tint filedirty = 0;\n+\n+\tread_success(); /* update */\n+\tread_success(); /* report */\n+\n+\tfor (;;) {\n+\t\tconst char *s = read_command();\n+\n+\t\tif (!strcmp(s, \"close-edit\")) {\n+\t\t\tif (name.len) goto err;\n+\t\t\tread_command_end();\n+\t\t\tbreak;\n+\n+\t\t} else if (!strcmp(s, \"abort-edit\")) {\n+\t\t\tdie(\"update aborted\");\n+\n+\t\t} else if (!strcmp(s, \"open-root\")) {\n+\t\t\tif (name.len) goto err;\n+\n+\t\t} else if (!strcmp(s, \"add-dir\")) {\n+\t\t\tif (name.len) goto err;\n+\t\t\tread_add_dir(r, rev);\n+\n+\t\t} else if (!strcmp(s, \"open-file\")) {\n+\t\t\tif (name.len) goto err;\n+\t\t\tread_open_file(r, rev, &name, &src, &srcsz);\n+\n+\t\t} else if (!strcmp(s, \"add-file\")) {\n+\t\t\tif (name.len) goto err;\n+\t\t\tread_add_file(r, rev, &name, &src, &srcsz);\n+\n+\t\t} else if (!strcmp(s, \"close-file\")) {\n+\t\t\tif (!name.len) goto err;\n+\n+\t\t\tif (filedirty) {\n+\t\t\t\tread_close_file(r, name.buf, tgt.buf, tgt.len);\n+\t\t\t}\n+\n+\t\t\tstrbuf_release(&tgt);\n+\t\t\tstrbuf_reset(&name);\n+\t\t\tfree(src);\n+\t\t\tsrc = NULL;\n+\t\t\tsrcsz = 0;\n+\t\t\tfiledirty = 0;\n+\n+\t\t} else if (!strcmp(s, \"delete-entry\")) {\n+\t\t\tif (name.len) goto err;\n+\t\t\tread_delete_entry(r, rev);\n+\n+\t\t} else if (!strcmp(s, \"apply-textdelta\")) {\n+\t\t\tstruct strbuf delta = STRBUF_INIT;\n+\n+\t\t\t/* file-token, [base-checksum] */\n+\t\t\tif (!name.len) goto err;\n+\n+\t\t\tread_text_delta(&delta);\n+\t\t\tfiledirty = 1;\n+\t\t\tif (delta.len) {\n+\t\t\t\tapply_svndiff(&tgt, src, srcsz, delta.buf, delta.len);\n+\t\t\t}\n+\t\t\tstrbuf_release(&delta);\n+\t\t}\n+\n+\t\tread_command_end();\n+\t}\n+\n+\tread_success();\n+\n+\tfree(src);\n+\tstrbuf_release(&name);\n+\tstrbuf_release(&tgt);\n+\treturn;\n+\n+err:\n+\tdie(\"malformed update\");\n+}\n+\n+struct author {\n+\tchar* user;\n+\tchar* pass;\n+\tchar* name;\n+\tchar* mail;\n+};\n+\n+struct author* authors;\n+size_t authorn, authoralloc;\n+struct author* defauthor;\n+\n+static char* strip_space(char* p) {\n+\tchar* e = p + strlen(p);\n+\n+\twhile (*p == ' ' || *p == '\\t') {\n+\t\tp++;\n+\t}\n+\n+\twhile (e > p && (e[-1] == ' ' || e[-1] == '\\t')) {\n+\t\t*(--e) = '\\0';\n+\t}\n+\n+\treturn p;\n+}\n+\n+static void parse_authors() {\n+\tchar* p;\n+\tstruct stat st;\n+\tint fd = open(git_path(\"svn-authors\"), O_RDONLY);\n+\tif (fd < 0 || fstat(fd, &st)) return;\n+\n+\tp = xmalloc(st.st_size + 1);\n+\tif (xread(fd, p, st.st_size) != st.st_size)\n+\t\tdie(\"read failed on authors\");\n+\n+\tp[st.st_size] = '\\0';\n+\n+\twhile (p && *p) {\n+\t\tstruct author a;\n+\t\tchar* line = strchr(p, '\\n');\n+\t\tif (line) *(line++) = '\\0';\n+\n+\t\ta.user = p;\n+\n+\t\tp = strchr(p, '=');\n+\t\tif (!p) goto nextline; /* empty line */\n+\t\t*(p++) = '\\0';\n+\t\ta.name = p;\n+\n+\t\tp = strchr(p, '<');\n+\t\tif (!p) die(\"invalid author entry for %s\", a.user);\n+\t\t*(p++) = '\\0';\n+\t\ta.mail = p;\n+\n+\t\tp = strchr(p, '>');\n+\t\tif (!p) die(\"invalid author entry for %s\", a.user);\n+\t\t*(p++) = '\\0';\n+\t\ta.pass = p;\n+\n+\t\ta.user = strip_space(a.user);\n+\t\ta.name = strip_space(a.name);\n+\t\ta.mail = strip_space(a.mail);\n+\n+\t\tp = strchr(a.user, ':');\n+\t\tif (p) {\n+\t\t\t*p = '\\0';\n+\t\t\ta.pass = p+1;\n+\t\t} else {\n+\t\t\ta.pass = NULL;\n+\t\t}\n+\n+\t\tif (*a.user == '#') {\n+\t\t\t/* comment */\n+\t\t} else {\n+\t\t\tALLOC_GROW(authors, authorn + 1, authoralloc);\n+\t\t\tauthors[authorn++] = a;\n+\t\t}\n+\n+nextline:\n+\t\tp = line;\n+\t}\n+\n+\tclose(fd);\n+}\n+\n+static void svn_author_to_git(struct strbuf* author) {\n+\tint i;\n+\n+\tfor (i = 0; i < authorn; i++) {\n+\t\tstruct author* a = &authors[i];\n+\t\tif (!strcasecmp(author->buf, a->user)) {\n+\t\t\tstrbuf_reset(author);\n+\t\t\tstrbuf_addf(author, \"%s <%s>\", a->name, a->mail);\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tdie(\"could not find username '%s' in %s\\n\"\n+\t\t\t\"Add a line of the form:\\n\"\n+\t\t\t\"%s = Full Name <email@example.com>\\n\",\n+\t\t\tauthor->buf,\n+\t\t\tgit_path(\"svn-authors\"),\n+\t\t\tauthor->buf);\n+}\n+\n+static struct author* get_object_author(struct object* obj) {\n+\tconst char *lb, *le, *mb, *me;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct author* ret = NULL;\n+\tchar* data = NULL;\n+\tint i;\n+\n+\tif (obj->type == OBJ_COMMIT) {\n+\t\tstruct commit* cmt = (struct commit*) obj;\n+\t\tparse_commit(cmt);\n+\t\tlb = strstr(cmt->buffer, \"\\ncommitter \");\n+\t\tif (!lb) lb = strstr(cmt->buffer, \"\\nauthor \");\n+\t} else if (obj->type == OBJ_TAG) {\n+\t\tenum object_type type;\n+\t\tunsigned long size;\n+\t\tdata = read_sha1_file(obj->sha1, &type, &size);\n+\t\tif (!data || type != OBJ_TAG) goto err;\n+\t\tlb = strstr(data, \"\\ntagger \");\n+\t} else {\n+\t\tdie(\"invalid commit object\");\n+\t}\n+\n+\tif (!lb) goto err;\n+\tle = strchr(lb+1, '\\n');\n+\tif (!le) goto err;\n+\tmb = memchr(lb, '<', le - lb);\n+\tif (!mb) goto err;\n+\tme = memchr(mb, '>', le - mb);\n+\tif (!me) goto err;\n+\n+\tstrbuf_add(&buf, mb+1, me - (mb+1));\n+\n+\tfor (i = 0; i < authorn; i++) {\n+\t\tstruct author* a = &authors[i];\n+\t\tif (strcasecmp(buf.buf, a->mail)) continue;\n+\t\tif (!a->pass) {\n+\t\t\tdie(\"need password for user '%s' in %s\\n\"\n+\t\t\t\t\"Add a line of the form:\\n\"\n+\t\t\t\t\"%s:password = Full Name <%s>\\n\",\n+\t\t\t\ta->user,\n+\t\t\t\tgit_path(\"svn-authors\"),\n+\t\t\t\ta->user,\n+\t\t\t\ta->mail);\n+\t\t}\n+\n+\t\tret = a;\n+\t\tbreak;\n+\t}\n+\n+\tif (!ret) {\n+\t\tdie(\"could not find username/password for %s in %s\\n\"\n+\t\t\t\t\"Add a line of the form:\\n\"\n+\t\t\t\t\"username:password = Full Name <%s>\\n\",\n+\t\t\t\tbuf.buf,\n+\t\t\t\tgit_path(\"svn-authors\"),\n+\t\t\t\tbuf.buf);\n+\t}\n+\n+\tstrbuf_release(&buf);\n+\tfree(data);\n+\treturn ret;\n+\n+err:\n+\tdie(\"can not find author in %s\", sha1_to_hex(obj->sha1));\n+}\n+\n+static struct commit* latest_fetch_svncmt;\n+\n+static void init_latest_fetch(void) {\n+\tunsigned char sha1[20];\n+\tstruct commit* cmt;\n+\n+\tlatest_fetch_svncmt = NULL;\n+\tif (read_ref(\"refs/svn/latest\", sha1) || is_null_sha1(sha1))\n+\t\treturn;\n+\n+\tcmt = lookup_commit(sha1);\n+\tif (!cmt || parse_commit(cmt)) {\n+\t\tdie(\"invalid latest ref %s\", sha1_to_hex(sha1));\n+\t}\n+\n+\tlatest_fetch_svncmt = cmt;\n+}\n+\n+static int latest_fetch_rev(void) {\n+\treturn latest_fetch_svncmt ? parse_svnrev(latest_fetch_svncmt) : 0;\n+}\n+\n+static int set_latest_fetch(struct commit* cmt) {\n+\tstruct ref_lock* lk;\n+\n+\tif (cmt == latest_fetch_svncmt) {\n+\t\treturn 0;\n+\t}\n+\n+\tlk = lock_ref_sha1(\"svn/latest\", cmt_sha1(latest_fetch_svncmt));\n+\tif (!lk || write_ref_sha1(lk, cmt_sha1(cmt), \"svn-fetch\")) {\n+\t\terror(\"failed to update latest ref\");\n+\t\treturn 1;\n+\t}\n+\n+\tlatest_fetch_svncmt = cmt;\n+\treturn 0;\n+}\n+\n+static int svn_time_to_git(struct strbuf* time) {\n+\tstruct tm tm;\n+\tmemset(&tm, 0, sizeof(tm));\n+\tif (!strptime(time->buf, \"%Y-%m-%dT%H:%M:%S\", &tm)) return -1;\n+\tstrbuf_reset(time);\n+\tstrbuf_addf(time, \"%\"PRId64, (int64_t) mktime(&tm));\n+\treturn 0;\n+}\n+\n+static struct commit* create_fetched_commit(struct svnref* r, int rev, const char* author, const char* time, const char* log, int created) {\n+\tstatic struct strbuf buf = STRBUF_INIT;\n+\tunsigned char sha1[20];\n+\n+\tstruct object *gitobj;\n+\tstruct commit *gitcmt, *svncmt;\n+\tstruct ref_lock* lk = NULL;\n+\n+\t/* Create the commit object.\n+\t *\n+\t * SVN can't create tags and branches without a commit,\n+\t * but git can. In the cases where new refs are just\n+\t * created without any changes to the tree, we don't add\n+\t * a commit. This way git commits pushed to svn and\n+\t * pulled back again look roughly the same.\n+\t */\n+\tif (r->delete) {\n+\t\tgitcmt = NULL;\n+\n+\t} else if ((r->istag || created)\n+\t\t&& r->parent\n+\t\t&& !hashcmp(idx_sha1(git_index(r)), r->parent->tree->object.sha1)\n+\t\t  ) {\n+\t\t/* branch/tag has been created/replaced, but the tree hasn't\n+\t\t * been changed */\n+\t\tgitcmt = r->parent;\n+\n+\t} else {\n+\t\tstrbuf_reset(&buf);\n+\t\tstrbuf_addf(&buf, \"tree %s\\n\", sha1_to_hex(idx_sha1(git_index(r))));\n+\n+\t\tif (r->parent) {\n+\t\t\tstrbuf_addf(&buf, \"parent %s\\n\", cmt_to_hex(r->parent));\n+\t\t}\n+\n+\t\tstrbuf_addf(&buf, \"author %s %s +0000\\n\", author, time);\n+\t\tstrbuf_addf(&buf, \"committer %s %s +0000\\n\", author, time);\n+\n+\t\tstrbuf_addch(&buf, '\\n');\n+\t\tstrbuf_addstr(&buf, log);\n+\n+\t\tif (write_sha1_file(buf.buf, buf.len, \"commit\", sha1))\n+\t\t\tdie(\"failed to create commit\");\n+\n+\t\tgitcmt = lookup_commit(sha1);\n+\t\tif (!gitcmt || parse_commit(gitcmt))\n+\t\t\tdie(\"failed to parse created commit\");\n+\t}\n+\n+\t/* Create the tag object.\n+\t *\n+\t * Now we create an annotated tag wrapped around either\n+\t * the commit the tag was branched from or the wrapper.\n+\t * Where a tag is later updated, we either recreate this\n+\t * tag with a new time (no tree change) or create a new\n+\t * dummy commit whose parent is the old dummy.\n+\t */\n+\tif (r->delete) {\n+\t\tgitobj = NULL;\n+\n+\t} else if (r->istag) {\n+\t\tstrbuf_reset(&buf);\n+\t\tstrbuf_addf(&buf, \"object %s\\n\", cmt_to_hex(gitcmt));\n+\t\tstrbuf_addf(&buf, \"type commit\\n\");\n+\t\tstrbuf_addf(&buf, \"tag %s\\n\", r->remote.buf + strlen(\"refs/tags/\"));\n+\t\tstrbuf_addf(&buf, \"tagger %s %s +0000\\n\", author, time);\n+\t\tstrbuf_addch(&buf, '\\n');\n+\t\tstrbuf_addstr(&buf, log);\n+\n+\t\tif (write_sha1_file(buf.buf, buf.len, tag_type, sha1))\n+\t\t\tdie(\"failed to create tag\");\n+\n+\t\tgitobj = parse_object(sha1);\n+\t\tif (!gitobj)\n+\t\t\tdie(\"failed to parse created tag\");\n+\n+\t} else {\n+\t\tgitobj = &gitcmt->object;\n+\t}\n+\n+\t/* Create the svn commit */\n+\tstrbuf_reset(&buf);\n+\tstrbuf_addf(&buf, \"tree %s\\n\", sha1_to_hex(idx_sha1(svn_index(r))));\n+\n+\tif (gitcmt || r->svncmt) {\n+\t\tstrbuf_addf(&buf, \"parent %s\\n\", cmt_to_hex(gitcmt ? gitcmt : r->svncmt));\n+\t}\n+\n+\tif (r->svncmt) {\n+\t\tstrbuf_addf(&buf, \"parent %s\\n\", cmt_to_hex(r->svncmt));\n+\t}\n+\n+\tstrbuf_addf(&buf, \"author %s %s +0000\\n\", author, time);\n+\tstrbuf_addf(&buf, \"committer %s %s +0000\\n\", author, time);\n+\tstrbuf_addf(&buf, \"revision %d\\n\", rev);\n+\tstrbuf_addf(&buf, \"path %s\\n\", r->svn.buf);\n+\tstrbuf_addch(&buf, '\\n');\n+\n+\tif (write_sha1_file(buf.buf, buf.len, \"commit\", sha1))\n+\t\tdie(\"failed to create svn object\");\n+\n+\tsvncmt = lookup_commit(sha1);\n+\tif (!svncmt || parse_commit(svncmt))\n+\t\tdie(\"failed to parse created svn commit\");\n+\n+\t/* update the ref */\n+\n+\tlk = lock_ref_sha1(r->ref.buf + strlen(\"refs/\"), cmt_sha1(r->svncmt));\n+\tif (!lk || write_ref_sha1(lk, cmt_sha1(svncmt), \"svn-fetch\")) {\n+\t\tdie(\"failed to update ref %s\", r->ref.buf);\n+\t}\n+\n+\t/* update the remote or tag ref */\n+\n+\tif (r->gitobj && !gitobj) {\n+\t\tif (delete_ref(r->remote.buf, r->gitobj->sha1, 0)) {\n+\t\t\terror(\"failed to delete ref %s\", r->remote.buf);\n+\t\t\tgoto rollback;\n+\t\t}\n+\t} else if (gitobj) {\n+\t\tlk = lock_ref_sha1(r->remote.buf + strlen(\"refs/\"),\n+\t\t\t\tr->gitobj ? r->gitobj->sha1 : null_sha1);\n+\n+\t\tif (!lk || write_ref_sha1(lk, gitobj->sha1, \"svn-fetch\")) {\n+\t\t\terror(\"failed to update ref %s\", r->remote.buf);\n+\t\t\tgoto rollback;\n+\t\t}\n+\t}\n+\n+\tr->delete = 0;\n+\tr->gitobj = gitobj;\n+\tr->svncmt = svncmt;\n+\tr->parent = gitcmt;\n+\n+\tfprintf(stderr, \"fetched %d %s %s\\n\", rev, r->ref.buf, sha1_to_hex(r->svncmt->object.sha1));\n+\treturn svncmt;\n+\n+rollback:\n+\tif (r->svncmt) {\n+\t\tlk = lock_ref_sha1(r->ref.buf + strlen(\"refs/\"), cmt_sha1(svncmt));\n+\t\tif (!lk || write_ref_sha1(lk, cmt_sha1(r->svncmt), \"svn-fetch rollback\"))\n+\t\t\tgoto rollback_failed;\n+\t} else if (svncmt) {\n+\t\tif (delete_ref(r->ref.buf, cmt_sha1(svncmt), 0))\n+\t\t\tgoto rollback_failed;\n+\t}\n+\n+\texit(128);\n+\n+rollback_failed:\n+\tdie(\"failed to rollback %s\", r->ref.buf);\n+}\n+\n+static void request_commit(struct svnref* r, int rev, struct svnref* copysrc, int copyrev) {\n+\tfprintf(stderr, \"request commit %d\\n\", rev);\n+\n+\tif (!copysrc) {\n+\t\tcopysrc = r;\n+\t\tcopyrev = rev - 1;\n+\t}\n+\n+\t/* [rev] target recurse target-url */\n+\tsendf(\"( switch ( ( %d ) %d:%s true %d:%s/%s ) )\\n\",\n+\t\t\trev,\n+\t\t\t(int) copysrc->svn.len,\n+\t\t\tcopysrc->svn.buf,\n+\t\t\t(int) (strlen(url) + 1 + r->svn.len),\n+\t\t\turl,\n+\t\t\tr->svn.buf);\n+\n+\t/* path rev start-empty */\n+\tsendf(\"( set-path ( 0: %d false ) )\\n\", copyrev);\n+\tsendf(\"( finish-report ( ) )\\n\");\n+\tsendf(\"( success ( ) )\\n\");\n+}\n+\n+static void request_log(int from, int to) {\n+\tstruct strbuf paths = STRBUF_INIT;\n+\tif (!trunk && !branches && !tags) {\n+\t\tstrbuf_addstr(&paths, \"0: \");\n+\t}\n+\tif (trunk) {\n+\t\tstrbuf_addf(&paths, \"%d:%s \", (int) strlen(trunk), trunk);\n+\t}\n+\tif (branches) {\n+\t\tstrbuf_addf(&paths, \"%d:%s \", (int) strlen(branches), branches);\n+\t}\n+\tif (tags) {\n+\t\tstrbuf_addf(&paths, \"%d:%s \", (int) strlen(tags), tags);\n+\t}\n+\n+\tsendf(\"( log ( ( %s) \" /* (path...) */\n+\t\t\"( %d ) ( %d ) \" /* start/end revno */\n+\t\t\"true false \" /* changed-paths strict-node */\n+\t\t\"0 \" /* limit */\n+\t\t\"false \" /* include-merged-revisions */\n+\t\t\"revprops ( 10:svn:author 8:svn:date 7:svn:log ) \"\n+\t\t\") )\\n\",\n+\t\tpaths.buf,\n+\t\tfrom, /* log start */\n+\t\tto /* log end */\n+\t     );\n+\n+\tstrbuf_release(&paths);\n+}\n+\n+struct pending {\n+\tchar *buf, *msg, *author, *time;\n+\tstruct svnref *ref, *copysrc;\n+\tint rev, copyrev;\n+};\n+\n+static int have_next_commit(struct pending* retp) {\n+\tstatic struct pending *nextv;\n+\tstatic int nextc, nexta;\n+\tstatic int64_t rev;\n+\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tstruct strbuf author = STRBUF_INIT;\n+\tstruct strbuf time = STRBUF_INIT;\n+\tstruct strbuf name = STRBUF_INIT;\n+\n+\t/* log reply is of the form\n+\t * ( ( ( n:changed-path A|D|R|M ( n:copy-path copy-rev ) ) ... ) rev n:author n:date n:message )\n+\t * ....\n+\t * done\n+\t * ( success ( ) )\n+\t */\n+\n+\twhile (!nextc) {\n+\t\tstruct pending* p;\n+\n+\t\t/* start of log entry */\n+\t\tif (read_list()) {\n+\t\t\tread_done();\n+\t\t\tread_success();\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\t/* start changed path entries */\n+\t\tif (read_list()) goto err;\n+\n+\t\twhile (!read_list()) {\n+\t\t\tconst char* s;\n+\t\t\tstruct svnref *to;\n+\t\t\tint i;\n+\n+\t\t\t/* path A|D|R|M [copy-path copy-rev] */\n+\t\t\tstrbuf_reset(&name);\n+\t\t\tread_name(&name);\n+\t\t\tto = find_svnref_by_path(&name);\n+\t\t\ts = read_word();\n+\t\t\tif (!s) goto err;\n+\n+\t\t\tp = NULL;\n+\t\t\tfor (i = 0; i < nextc; i++) {\n+\t\t\t\tif (nextv[i].ref == to) {\n+\t\t\t\t\tp = &nextv[i];\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (to && !name.len && (!strcmp(s, \"A\") || !strcmp(s, \"R\")) && have_optional()) {\n+\t\t\t\tint copyrev;\n+\t\t\t\tstruct svnref* copysrc;\n+\n+\t\t\t\tstrbuf_reset(&name);\n+\t\t\t\tcopysrc = read_copy_source(&name, &copyrev);\n+\t\t\t\tif (copysrc && name.len) {\n+\t\t\t\t\twarning(\"copy from non-root path\");\n+\t\t\t\t\tcopysrc = NULL;\n+\t\t\t\t\tcopyrev = 0;\n+\t\t\t\t}\n+\n+\t\t\t\tif (p == NULL) {\n+\t\t\t\t\tALLOC_GROW(nextv, nextc+1, nexta);\n+\t\t\t\t\tp = &nextv[nextc++];\n+\t\t\t\t}\n+\n+\t\t\t\tp->copysrc = copysrc;\n+\t\t\t\tp->copyrev = copyrev;\n+\t\t\t\tp->ref = to;\n+\n+\t\t\t\tread_end();\n+\n+\t\t\t} else if (to && p == NULL) {\n+\t\t\t\tALLOC_GROW(nextv, nextc+1, nexta);\n+\t\t\t\tp = &nextv[nextc++];\n+\t\t\t\tp->copysrc = NULL;\n+\t\t\t\tp->ref = to;\n+\t\t\t}\n+\n+\t\t\tread_end();\n+\t\t}\n+\n+\t\t/* end of changed path entries */\n+\t\tread_end();\n+\n+\t\t/* rev number */\n+\t\trev = read_number();\n+\t\tif (rev < 0) goto err;\n+\n+\t\t/* author */\n+\t\tif (read_list()) goto err;\n+\t\tstrbuf_reset(&author);\n+\t\tif (read_string(&author)) goto err;\n+\t\tsvn_author_to_git(&author);\n+\t\tread_end();\n+\n+\t\t/* timestamp */\n+\t\tif (read_list()) goto err;\n+\t\tstrbuf_reset(&time);\n+\t\tif (read_string(&time)) goto err;\n+\t\tif (svn_time_to_git(&time)) goto err;\n+\t\tread_end();\n+\n+\t\t/* log message */\n+\t\tstrbuf_reset(&msg);\n+\t\tif (have_optional()) {\n+\t\t\tif (read_string(&msg)) goto err;\n+\t\t\tstrbuf_complete_line(&msg);\n+\t\t\tread_end();\n+\t\t}\n+\n+\t\t/* end of log entry */\n+\t\tread_end();\n+\t\tif (verbose) fputc('\\n', stderr);\n+\n+\t\t/* remove entries where we've already downloaded the\n+\t\t * commit */\n+\t\tp = nextv;\n+\t\twhile (p < nextv+nextc) {\n+\t\t\tif (p->ref->svncmt && parse_svnrev(p->ref->svncmt) >= rev) {\n+\t\t\t\tmemmove(p, p+1, sizeof(*p) * ((nextv+nextc) - (p+1)));\n+\t\t\t\tnextc--;\n+\t\t\t} else {\n+\t\t\t\tp->buf = xmalloc(msg.len + 1 + author.len + 1 + time.len + 1);\n+\n+\t\t\t\tp->rev = rev;\n+\t\t\t\tp->msg = p->buf;\n+\t\t\t\tp->author = p->msg + msg.len + 1;\n+\t\t\t\tp->time = p->author + author.len + 1;\n+\n+\t\t\t\tmemcpy(p->msg, msg.buf, msg.len + 1);\n+\t\t\t\tmemcpy(p->author, author.buf, author.len + 1);\n+\t\t\t\tmemcpy(p->time, time.buf, time.len + 1);\n+\n+\t\t\t\tp++;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\t*retp = nextv[0];\n+\n+\tmemmove(nextv, nextv+1, sizeof(nextv[0])*(nextc-1));\n+\tnextc--;\n+\n+\tstrbuf_release(&name);\n+\tstrbuf_release(&msg);\n+\tstrbuf_release(&author);\n+\tstrbuf_release(&time);\n+\treturn 1;\n+\n+err:\n+\tdie(\"malformed log\");\n+}\n+\n+static char* clean_path(char* p) {\n+\tchar* e;\n+\tif (*p == '/') p++;\n+\te = p + strlen(p);\n+\tif (e > p && e[-1] == '/') e[-1] = '\\0';\n+\treturn p;\n+}\n+\n+static struct author** connection_authors;\n+static int *svnfdv;\n+static struct inbuffer *inbufv;\n+\n+static void setup_globals() {\n+\tint i;\n+\n+\tsetenv(\"TZ\", \"\", 1);\n+\n+\tcore_eol = svn_eol;\n+\n+\tif (getenv(\"GIT_SVN_PUSH_PAUSE\")) {\n+\t\tpause_between_commits = atoi(getenv(\"GIT_SVN_PUSH_PAUSE\"));\n+\t}\n+\n+\tif (remotedir) {\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tremotedir = clean_path((char*) remotedir);\n+\n+\t\tstrbuf_addstr(&buf, \"refs/remotes/\");\n+\t\tstrbuf_addstr(&buf, remotedir);\n+\t\tstrbuf_addch(&buf, '/');\n+\t\tremoteheads = strbuf_detach(&buf, NULL);\n+\n+\t\tstrbuf_addstr(&buf, \"refs/tags/\");\n+\t\tstrbuf_addstr(&buf, remotedir);\n+\t\tstrbuf_addch(&buf, '/');\n+\t\tremotetags = strbuf_detach(&buf, NULL);\n+\t} else {\n+\t\tremoteheads = \"refs/heads/\";\n+\t\tremotetags = \"refs/tags/\";\n+\t\tleave_remote = 1;\n+\t}\n+\n+\tif (svnfdc < 1) die(\"invalid number of connections\");\n+\n+\tconnection_authors = xcalloc(svnfdc+1, sizeof(connection_authors[0]));\n+\tsvnfdv = xmalloc((svnfdc+1) * sizeof(svnfdv[0]));\n+\tinbufv = xmalloc((svnfdc+1) * sizeof(inbufv[0]));\n+\tfor (i = 0; i <= svnfdc; i++) {\n+\t\tsvnfdv[i] = -1;\n+\t\tinbufv[i].b = inbufv[i].e = 0;\n+\t}\n+\n+\tparse_authors();\n+\n+\tfor (i = 0; svnuser && i < authorn; i++) {\n+\t\tstruct author* a = &authors[i];\n+\t\tif (!strcasecmp(a->user, svnuser)) {\n+\t\t\tdefauthor = a;\n+\t\t\tif (!a->pass) {\n+\t\t\t\tdie(\"user specified with --user needs a password\");\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tif (!defauthor) die(\"need to specify default user with --user\");\n+\tif (!url) die(\"need to specify a url with --url\");\n+\n+\tif (trunk) trunk = clean_path((char*) trunk);\n+\tif (branches) branches = clean_path((char*) branches);\n+\tif (tags) tags = clean_path((char*) tags);\n+\n+\tinit_latest_fetch();\n+}\n+\n+static void close_connection(int cidx) {\n+\tif (svnfdv[cidx] >= 0) {\n+\t\tclose(svnfdv[cidx]);\n+\t}\n+\tsvnfdv[cidx] = -1;\n+\tconnection_authors[cidx] = NULL;\n+\tinbufv[cidx].b = inbufv[cidx].e = 0;\n+}\n+\n+static void change_connection(int cidx, struct author* a) {\n+\tchar pathsep;\n+\tchar *host, *port, *path;\n+\tconst char *s;\n+\tstruct addrinfo hints, *res, *ai;\n+\tint err;\n+\tint fd = -1;\n+\n+\tsvnfd = svnfdv[cidx];\n+\tinbuf = &inbufv[cidx];\n+\tif (svnfd >= 0 && connection_authors[cidx] == a) {\n+\t\treturn;\n+\t}\n+\n+\tclose_connection(cidx);\n+\n+\tif (prefixcmp(url, \"svn://\"))\n+\t\tdie(_(\"only svn repositories are supported\"));\n+\n+\tif (!a->pass)\n+\t\tdie(\"need a password for user %s\", a->user);\n+\n+\thost = (char*) url + strlen(\"svn://\");\n+\n+\tpath = strchr(host, '/');\n+\tif (!path) path = host + strlen(host);\n+\tpathsep = *path;\n+\t*path = '\\0';\n+\n+\tport = strchr(host, ':');\n+\tif (port) *(port++) = '\\0';\n+\n+\tmemset(&hints, 0, sizeof(hints));\n+\thints.ai_socktype = SOCK_STREAM;\n+\n+\terr = getaddrinfo(host, port ? port : \"3690\", &hints, &res);\n+\t*path = pathsep;\n+\tif (port) port[-1] = ':';\n+\n+\tif (err)\n+\t\tdie_errno(\"failed to connect to %s\", url);\n+\n+\tfor (ai = res; ai != NULL; ai = ai->ai_next) {\n+\t\tfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);\n+\t\tif (fd < 0) continue;\n+\n+\t\tif (connect(fd, ai->ai_addr, ai->ai_addrlen)) {\n+\t\t\tint err = errno;\n+\t\t\tclose(fd);\n+\t\t\terrno = err;\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tbreak;\n+\t}\n+\n+\tif (fd < 0)\n+\t\tdie_errno(\"failed to connect to %s\", url);\n+\n+\tsvnfd = svnfdv[cidx] = fd;\n+\tinbuf = &inbufv[cidx];\n+\tverbosetxnl = 1;\n+\n+\t/* TODO: client software version and client capabilities */\n+\tsendf(\"( 2 ( edit-pipeline svndiff1 ) %d:%s )\\n\", (int) strlen(url), url);\n+\tsendf(\"( CRAM-MD5 ( ) )\\n\");\n+\n+\t/* TODO: we don't care about capabilities/versions right now */\n+\ts = read_command();\n+\tif (strcmp(s, \"success\"))\n+\t\tdie(\"server error\");\n+\n+\t/* minver then maxver */\n+\tif (read_number() > 2 || read_number() < 2)\n+\t\tdie(_(\"version mismatch\"));\n+\n+\tread_command_end();\n+\n+\t/* TODO: read the mech lists et all */\n+\tread_success();\n+\n+\tcram_md5(a->user, a->pass);\n+\n+\tsendf(\"( reparent ( %d:%s ) )\\n\", (int) strlen(url), url);\n+\n+\tread_success(); /* auth */\n+\tread_success(); /* repo info */\n+\tread_success(); /* reparent */\n+\tread_success(); /* reparent again */\n+\n+\tconnection_authors[cidx] = a;\n+}\n+\n+static int run_gc_auto() {\n+\tconst char *args[] = {\"gc\", \"--auto\", NULL};\n+\treturn run_command_v_opt(args, RUN_GIT_CMD);\n+}\n+\n+static const char* print_arg(struct strbuf* sb, const char* fmt, ...) {\n+\tva_list ap;\n+\tva_start(ap, fmt);\n+\tstrbuf_reset(sb);\n+\tstrbuf_vaddf(sb, fmt, ap);\n+\treturn sb->buf;\n+}\n+\n+int cmd_svn_fetch(int argc, const char **argv, const char *prefix) {\n+\tint64_t n;\n+\tint from, to, i, finished;\n+\tstruct pending *pending;\n+\tstruct commit* svncmt = NULL;\n+\n+\tgit_config(&config, NULL);\n+\n+\targc = parse_options(argc, argv, prefix, builtin_svn_fetch_options,\n+\t\t\tbuiltin_svn_fetch_usage, 0);\n+\n+\tif (argc)\n+\t\tusage_msg_opt(_(\"Too many arguments.\"),\n+\t\t\tbuiltin_svn_fetch_usage, builtin_svn_fetch_options);\n+\n+\tsetup_globals();\n+\n+\tif (getenv(\"GIT_SVN_FETCH_REPORT_LATEST\")) {\n+\t\tprintf(\"%d\\n\", latest_fetch_rev());\n+\t\treturn 0;\n+\t}\n+\n+\tfrom = latest_fetch_rev();\n+\tpending = xcalloc(svnfdc, sizeof(pending[0]));\n+\n+\tchange_connection(svnfdc, defauthor);\n+\tsendf(\"( get-latest-rev ( ) )\\n\");\n+\n+\tread_success(); /* latest rev */\n+\tread_command(); /* latest rev again */\n+\tn = read_number();\n+\tif (n < 0 || n > INT_MAX) die(\"latest-rev failed\");\n+\tread_command_end();\n+\n+\tto = min(last_revision, (int) n);\n+\n+\tfprintf(stderr, \"request log for %d %d\\n\", from, to);\n+\n+\t/* gc --auto invalidates the object cache. Thus we have to run\n+\t * it last. For when we want to run the update in multiple\n+\t * bunches then we spawn off a sub command for each chunk.\n+\t */\n+\tif (!inner && to > from + FETCH_AT_ONCE) {\n+\t\tstruct strbuf revs = STRBUF_INIT;\n+\t\tstruct strbuf conns = STRBUF_INIT;\n+\n+\t\twhile (from < to) {\n+\t\t\tint ret;\n+\t\t\tint cmdto = min(from + FETCH_AT_ONCE, to);\n+\t\t\tconst char* args[] = {\n+\t\t\t\t\"svn-fetch\",\n+\t\t\t\t\"-c\", print_arg(&conns, \"%d\", svnfdc),\n+\t\t\t\t\"-r\", print_arg(&revs, \"%d\", cmdto),\n+\t\t\t\t\"--user\", svnuser,\n+\t\t\t\t\"--inner\",\n+\t\t\t\tverbose ? \"-v\" : NULL,\n+\t\t\t\tNULL\n+\t\t\t};\n+\n+\t\t\tret = run_command_v_opt(args, RUN_GIT_CMD);\n+\t\t\tif (ret) return ret;\n+\n+\t\t\tfrom = cmdto;\n+\t\t}\n+\n+\t\treturn 0;\n+\t}\n+\n+\tif (from >= to) {\n+\t\treturn 0;\n+\t}\n+\n+\tchange_connection(svnfdc, defauthor);\n+\trequest_log(from+1, to);\n+\tread_success();\n+\n+\t/* start requesting commits until we've filled out pending\n+\t * commits or run out of commits */\n+\ti = 0;\n+\tfinished = 0;\n+\twhile (i < svnfdc) {\n+\t\tstruct pending* p = &pending[i];\n+\n+\t\tchange_connection(svnfdc, defauthor);\n+\t\tif (!have_next_commit(p)) {\n+\t\t\tfinished = 1;\n+\t\t\tbreak;\n+\t\t}\n+\t\tchange_connection(i, defauthor);\n+\t\trequest_commit(p->ref, p->rev, p->copysrc, p->copyrev);\n+\n+\t\ti++;\n+\t}\n+\n+\ti = 0;\n+\tfor (;;) {\n+\t\tstruct pending* p = &pending[i];\n+\n+\t\t/* process a commit */\n+\t\tif (!p->ref) break;\n+\n+\t\t/* Only update the latest when we've moved onto a new\n+\t\t * revision. That way if we fail after the first of two\n+\t\t * branch updates in a revision we replay the whole\n+\t\t * revision next time. */\n+\t\tif (svncmt && p->rev > latest_fetch_rev()) {\n+\t\t\tset_latest_fetch(svncmt);\n+\t\t}\n+\n+\t\tif (p->copysrc) {\n+\t\t\tstruct commit* c = find_svncmt(p->copysrc, p->copyrev);\n+\t\t\tcheckout_svncmt(p->ref, c);\n+\t\t}\n+\n+\t\tchange_connection(i, defauthor);\n+\t\tread_update(p->ref, p->rev);\n+\t\tsvncmt = create_fetched_commit(p->ref, p->rev, p->author, p->time, p->msg, p->copysrc != NULL);\n+\t\tfree(p->buf);\n+\n+\t\t/* then request a new one on that connection */\n+\t\tchange_connection(svnfdc, defauthor);\n+\t\tif (!finished && have_next_commit(p)) {\n+\t\t\tchange_connection(i, defauthor);\n+\t\t\trequest_commit(p->ref, p->rev, p->copysrc, p->copyrev);\n+\t\t} else {\n+\t\t\tfinished = 1;\n+\t\t\tp->ref = NULL;\n+\t\t}\n+\n+\t\ti = (i+1) % svnfdc;\n+\t}\n+\n+\tif (svncmt) {\n+\t\tset_latest_fetch(svncmt);\n+\t}\n+\n+\treturn run_gc_auto();\n+}\n+\n+static const char* dtoken(int dir) {\n+\tstatic int bufnum;\n+\tstatic char bufs[4][32];\n+\tchar* buf1 = bufs[bufnum++ & 3];\n+\tchar* buf2 = bufs[bufnum++ & 3];\n+\tsprintf(buf1, \"d%d\", dir);\n+\tsprintf(buf2, \"%d:%s\", (int) strlen(buf1), buf1);\n+\treturn buf2;\n+}\n+\n+static int fcount;\n+static const char* ftoken() {\n+\tstatic char buf[32];\n+\tsprintf(buf, \"f%d\", ++fcount);\n+\tsprintf(buf, \"%d:f%d\", (int) strlen(buf), fcount);\n+\treturn buf;\n+}\n+\n+/* check that no commits have been inserted on our branch between from\n+ * (the previous revision at which we saw a change) and to (the revision\n+ * we just commited) */\n+static void check_for_svn_commits(struct svnref* r, int from, int to) {\n+\tif (from + 1 >= to) {\n+\t\treturn;\n+\t}\n+\n+\tsendf(\"( log ( ( %d:%s ) \" /* (path...) */\n+\t\t\t\"( %d ) ( %d ) \" /* start/end revno */\n+\t\t\t\"false false \" /* changed-paths strict-node */\n+\t\t\t\"0 false \" /* limit include-merged-revisions */\n+\t\t\t\"revprops ( ) ) )\\n\",\n+\t\t(int) r->svn.len,\n+\t\tr->svn.buf,\n+\t\tfrom + 1,\n+\t\tto - 1);\n+\n+\tread_success();\n+\tif (!read_list()) {\n+\t\tdie(\"commits inserted during push\");\n+\t}\n+\n+\tread_done();\n+\tread_success();\n+}\n+\n+static size_t common_directory(const char* a, const char* b, int* depth) {\n+\tint off;\n+\tconst char* ab = a;\n+\n+\toff = 0;\n+\twhile (*a && *b && *a == *b) {\n+\t\tif (*a == '/') {\n+\t\t\t(*depth)++;\n+\t\t\toff = a + 1 - ab;\n+\t\t}\n+\t\ta++;\n+\t\tb++;\n+\t}\n+\n+\treturn off;\n+}\n+\n+static struct strbuf cpath = STRBUF_INIT;\n+static int cdepth;\n+\n+static int change_dir(const char* path) {\n+\tconst char *p, *d;\n+\tint off, depth = 0;\n+\n+\toff = common_directory(path, cpath.buf, &depth);\n+\n+\t/* cd .. to the common root */\n+\twhile (cdepth > depth) {\n+\t\tsendf(\"( close-dir ( %s ) )\\n\", dtoken(cdepth));\n+\t\tcdepth--;\n+\t}\n+\n+\tstrbuf_setlen(&cpath, off);\n+\n+\t/* cd down to the new path */\n+\td = p = path + off;\n+\tfor (;;) {\n+\t\tchar* d = strchr(p, '/');\n+\t\tif (!d) break;\n+\n+\t\tsendf(\"( open-dir ( %d:%.*s %s %s ( ) ) )\\n\",\n+\t\t\t(int) (d - path), (int) (d - path), path,\n+\t\t\tdtoken(cdepth),\n+\t\t\tdtoken(cdepth+1));\n+\n+\t\t/* include the / at the end */\n+\t\td++;\n+\t\tstrbuf_add(&cpath, p, d - p);\n+\t\tp = d;\n+\t\tcdepth++;\n+\t}\n+\n+\treturn cdepth;\n+}\n+\n+static void dir_changed(int dir, const char* path) {\n+\tstrbuf_reset(&cpath);\n+\tstrbuf_addstr(&cpath, path);\n+\tif (*path) strbuf_addch(&cpath, '/');\n+\tcdepth = dir;\n+}\n+\n+static void send_delta_chunk(const char* tok, const void* data, size_t sz) {\n+\tsendf(\"( textdelta-chunk ( %s %d:\", tok, (int) sz);\n+\n+\tprint_ascii(data, sz, MAX_PRINT_LEN);\n+\n+\tif (write_in_full(svnfd, data, sz) != sz) {\n+\t\tdie_errno(\"write\");\n+\t}\n+\n+\tsendf(\" ) )\\n\");\n+}\n+\n+#define MAX_DELTA_SIZE (32*1024)\n+\n+static void send_file(const char* tok, char *data, size_t sz) {\n+\tstruct strbuf dataz = STRBUF_INIT;\n+\tz_stream z;\n+\n+\tmemset(&z, 0, sizeof(z));\n+\n+\tsendf(\"( apply-textdelta ( %s ( ) ) )\\n\", tok);\n+\n+\tsend_delta_chunk(tok, \"SVN\\1\", 4);\n+\n+\twhile (sz > 0) {\n+\t\tunsigned char hdr[7*MAX_VARINT_LEN+MAX_INS_LEN], *hp = hdr;\n+\t\tunsigned char ins[MAX_INS_LEN], *inp;\n+\t\tsize_t d = min(MAX_DELTA_SIZE, sz);\n+\t\tint ret = Z_OK;\n+\n+\t\tz.next_in = (unsigned char*) data;\n+\t\tz.avail_in = d;\n+\t\tstrbuf_reset(&dataz);\n+\n+\t\tdeflateInit(&z, Z_DEFAULT_COMPRESSION);\n+\n+\t\twhile (ret == Z_OK) {\n+\t\t\tstrbuf_grow(&dataz, dataz.len + MAX_DELTA_SIZE);\n+\t\t\tz.next_out = (unsigned char*) dataz.buf + dataz.len;\n+\t\t\tz.avail_out = MAX_DELTA_SIZE;\n+\t\t\tret = deflate(&z, Z_FINISH);\n+\t\t\tstrbuf_setlen(&dataz, MAX_DELTA_SIZE - z.avail_out);\n+\t\t}\n+\n+\t\tdeflateEnd(&z);\n+\n+\t\tinp = encode_instruction(ins, FROM_NEW, 0, d);\n+\n+\t\thp = encode_varint(hp, 0); /* source off */\n+\t\thp = encode_varint(hp, 0); /* source len */\n+\t\thp = encode_varint(hp, d); /* target len */\n+\t\thp = encode_varint(hp, inp - ins + encoded_length(inp - ins)); /* ins compressed len */\n+\t\thp = encode_varint(hp, dataz.len + encoded_length(d)); /* compressed data len */\n+\t\thp = encode_varint(hp, inp - ins); /* ins decompressed len */\n+\t\tmemcpy(hp, ins, inp - ins); /* instructions */\n+\t\thp += inp - ins;\n+\t\thp = encode_varint(hp, d); /* decompressed data len */\n+\n+\t\tsend_delta_chunk(tok, hdr, hp - hdr);\n+\t\tsend_delta_chunk(tok, dataz.buf, dataz.len);\n+\n+\t\tdata += d;\n+\t\tsz -= d;\n+\n+\t}\n+\n+\tsendf(\"( textdelta-end ( %s ) )\\n\", tok);\n+\n+\tstrbuf_release(&dataz);\n+}\n+\n+static void change(struct diff_options* op,\n+\t\tunsigned omode,\n+\t\tunsigned nmode,\n+\t\tconst unsigned char* osha1,\n+\t\tconst unsigned char* nsha1,\n+\t\tconst char* svnpath,\n+\t\tunsigned odsubmodule,\n+\t\tunsigned ndsubmodule)\n+{\n+\tstruct svnref* r = op->format_callback_data;\n+\tint svnlen = (int) strlen(svnpath);\n+\tconst char *gitpath = r->svn.len ? svnpath + r->svn.len : svnpath;\n+\tint gitlen = svnpath + svnlen - gitpath;\n+\tstruct cache_entry* ce;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tenum object_type type;\n+\tconst char* tok;\n+\tchar* data;\n+\tint dir;\n+\tsize_t sz;\n+\n+\tif (verbose) fprintf(stderr, \"change mode %x/%x sha1 %s/%s path %s\\n\",\n+\t\t\tomode, nmode, sha1_to_hex(osha1), sha1_to_hex(nsha1), svnpath);\n+\n+\t/* dont care about changed directories */\n+\tif (!S_ISREG(nmode)) return;\n+\n+\tdir = change_dir(svnpath);\n+\n+\tce = index_name_exists(svn_index(r), gitpath, gitlen, 0);\n+\tif (!ce) {\n+\t\t/* file exists in git but not in svn */\n+\t\treturn;\n+\t}\n+\n+\t/* TODO make this actually use diffcore */\n+\n+\tdata = read_sha1_file(nsha1, &type, &sz);\n+\tif (type != OBJ_BLOB)\n+\t\tdie(\"unexpected object type for %s\", sha1_to_hex(nsha1));\n+\n+\tif (convert_to_working_tree(gitpath, data, sz, &buf)) {\n+\t\tunsigned char sha1[20];\n+\t\tfree(data);\n+\t\tdata = strbuf_detach(&buf, &sz);\n+\n+\t\tif (write_sha1_file(data, sz, \"blob\", sha1)) {\n+\t\t\tdie_errno(\"blob write\");\n+\t\t}\n+\n+\t\tce = make_cache_entry(0644, sha1, gitpath, 0, 0);\n+\t\tadd_index_entry(svn_index(r), ce, ADD_CACHE_OK_TO_REPLACE);\n+\t}\n+\n+\ttok = ftoken();\n+\tsendf(\"( open-file ( %d:%s %s %s ( ) ) )\\n\",\n+\t\tsvnlen, svnpath, dtoken(dir), tok);\n+\n+\tsend_file(tok, data, sz);\n+\n+\tsendf(\"( close-file ( %s ( ) ) )\\n\", tok);\n+\n+\tdiff_change(op, omode, nmode, osha1, nsha1, gitpath, odsubmodule, ndsubmodule);\n+\n+\tfree(data);\n+}\n+\n+static void addremove(struct diff_options* op,\n+\t\tint addrm,\n+\t\tunsigned mode,\n+\t\tconst unsigned char* sha1,\n+\t\tconst char* svnpath,\n+\t\tunsigned dsubmodule)\n+{\n+\tstatic struct strbuf buf = STRBUF_INIT;\n+\tstruct svnref* r = op->format_callback_data;\n+\tint svnlen = strlen(svnpath);\n+\tconst char *gitpath = r->svn.len ? svnpath + r->svn.len : svnpath;\n+\tint gitlen = svnpath + svnlen - gitpath;\n+\n+\tif (verbose) fprintf(stderr, \"addrm %c mode %x sha1 %s path %s\\n\",\n+\t\t\taddrm, mode, sha1_to_hex(sha1), svnpath);\n+\n+\tif (addrm == '-' && S_ISDIR(mode)) {\n+\t\tstrbuf_reset(&buf);\n+\t\tstrbuf_add(&buf, gitpath, gitlen);\n+\t\tif (remove_index_path(svn_index(r), &buf) > 0) {\n+\t\t\tint dir = change_dir(svnpath);\n+\t\t\tsendf(\"( delete-entry ( %d:%s ( ) %s ) )\\n\",\n+\t\t\t\tsvnlen, svnpath, dtoken(dir));\n+\t\t}\n+\n+\t} else if (addrm == '+' && S_ISDIR(mode)) {\n+\t\tint dir = change_dir(svnpath);\n+\t\tsendf(\"( add-dir ( %d:%s %s %s ( ) ) )\\n\",\n+\t\t\tsvnlen, svnpath, dtoken(dir), dtoken(dir+1));\n+\n+\t\tdir_changed(++dir, svnpath);\n+\n+\t} else if (addrm == '-' && S_ISREG(mode)) {\n+\t\tstrbuf_reset(&buf);\n+\t\tstrbuf_add(&buf, gitpath, gitlen);\n+\t\tif (remove_index_path(svn_index(r), &buf) > 0) {\n+\t\t\tint dir = change_dir(svnpath);\n+\t\t\tsendf(\"( delete-entry ( %d:%s ( ) %s ) )\\n\",\n+\t\t\t\tsvnlen, svnpath, dtoken(dir));\n+\t\t}\n+\n+\t} else if (addrm == '+' && S_ISREG(mode)) {\n+\t\tstruct cache_entry* ce;\n+\t\tunsigned char nsha1[20];\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tenum object_type type;\n+\t\tconst char* tok;\n+\t\tvoid* data;\n+\t\tsize_t sz;\n+\t\tint dir;\n+\n+\t\t/* files beginning with .git eg .gitempty,\n+\t\t * .gitattributes, etc are filtered from svn\n+\t\t */\n+\t\tconst char* p = strrchr(gitpath, '/');\n+\t\tp = p ? p+1 : gitpath;\n+\t\tif (!prefixcmp(p, \".git\")) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\thashcpy(nsha1, sha1);\n+\t\tdata = read_sha1_file(nsha1, &type, &sz);\n+\t\tif (!data || type != OBJ_BLOB)\n+\t\t\tdie(\"unexpected object type for %s\", sha1_to_hex(sha1));\n+\n+\t\tif (convert_to_working_tree(gitpath, data, sz, &buf)) {\n+\t\t\tfree(data);\n+\t\t\tdata = strbuf_detach(&buf, &sz);\n+\n+\t\t\tif (write_sha1_file(data, sz, \"blob\", nsha1)) {\n+\t\t\t\tdie_errno(\"blob write\");\n+\t\t\t}\n+\t\t}\n+\n+\t\tce = make_cache_entry(0644, nsha1, gitpath, 0, 0);\n+\t\tadd_index_entry(svn_index(r), ce, ADD_CACHE_OK_TO_ADD);\n+\n+\t\t/* TODO: use diffcore to find copies */\n+\n+\t\tdir = change_dir(svnpath);\n+\t\ttok = ftoken();\n+\t\tsendf(\"( add-file ( %d:%s %s %s ( ) ) )\\n\",\n+\t\t\tsvnlen, svnpath, dtoken(dir), tok);\n+\n+\t\tsend_file(tok, data, sz);\n+\n+\t\tsendf(\"( close-file ( %s ( ) ) )\\n\", tok);\n+\n+\t\tfree(data);\n+\t}\n+\n+\tdiff_addremove(op, addrm, mode, sha1, svnpath, dsubmodule);\n+}\n+\n+static int read_commit_revno(struct strbuf* time) {\n+\tint64_t n;\n+\n+\tread_success();\n+\tread_success();\n+\n+\t/* commit-info */\n+\tif (read_list()) goto err;\n+\tn = read_number();\n+\tif (n < 0 || n > INT_MAX) goto err;\n+\tif (have_optional() && time) {\n+\t\tread_string(time);\n+\t\tsvn_time_to_git(time);\n+\t\tread_end();\n+\t}\n+\tread_end();\n+\tif (verbose) fputc('\\n', stderr);\n+\n+\treturn (int) n;\n+\n+err:\n+\tdie(\"commit failed\");\n+}\n+\n+/* returns the rev number */\n+static int send_commit(struct svnref* r, struct commit* cmt, struct commit* copysrc, const char* log, struct strbuf* time) {\n+\tstruct diff_options op;\n+\tint dir;\n+\n+\t/* If we are creating a new ref that we have never seen before\n+\t * in SVN, then the target is just above the last fetch, as that\n+\t * is the last time we checked the branches/tags folder for new\n+\t * refs. Otherwise its just above the last time we pushed/pulled\n+\t * this ref.\n+\t */\n+\tint tgtrev = (r->svncmt ? parse_svnrev(r->svncmt) : latest_fetch_rev()) + 1;\n+\n+\tsendf(\"( commit ( %d:%s ) )\\n\", (int) strlen(log), log);\n+\tsendf(\"( target-rev ( %d ) )\\n\", tgtrev);\n+\tsendf(\"( open-root ( ( ) %s ) )\\n\", dtoken(0));\n+\n+\tread_success();\n+\tread_success();\n+\n+\tdir = change_dir(r->svn.buf);\n+\n+\tif (!cmt) {\n+\t\tsendf(\"( delete-entry ( %d:%s ( ) %s ) )\\n\",\n+\t\t\t\t(int) r->svn.len,\n+\t\t\t\tr->svn.buf,\n+\t\t\t\tdtoken(dir));\n+\t} else {\n+\t\tif (copysrc) {\n+\t\t\tstruct strbuf path = STRBUF_INIT;\n+\t\t\tparse_svnpath(copysrc, &path);\n+\n+\t\t\tif (r->gitobj) {\n+\t\t\t\tsendf(\"( delete-entry ( %d:%s ( ) %s ) )\\n\",\n+\t\t\t\t\t\t(int) r->svn.len,\n+\t\t\t\t\t\tr->svn.buf,\n+\t\t\t\t\t\tdtoken(dir));\n+\t\t\t}\n+\n+\t\t\tsendf(\"( add-dir ( %d:%s %s %s ( %d:%s/%s %d ) ) )\\n\",\n+\t\t\t\t\t(int) r->svn.len,\n+\t\t\t\t\tr->svn.buf,\n+\t\t\t\t\tdtoken(dir),\n+\t\t\t\t\tdtoken(dir+1),\n+\t\t\t\t\t(int) (strlen(url) + 1 + path.len),\n+\t\t\t\t\turl,\n+\t\t\t\t\tpath.buf,\n+\t\t\t\t\tparse_svnrev(copysrc));\n+\n+\t\t\tstrbuf_release(&path);\n+\t\t} else {\n+\t\t\t/* We never have to create the root */\n+\t\t\tsendf(\"( %s ( %d:%s %s %s ( ) ) )\\n\",\n+\t\t\t\t(!r->gitobj && r->svn.len) ? \"add-dir\" : \"open-dir\",\n+\t\t\t\t(int) r->svn.len,\n+\t\t\t\tr->svn.buf,\n+\t\t\t\tdtoken(dir),\n+\t\t\t\tdtoken(dir+1));\n+\t\t}\n+\n+\t\tdir_changed(++dir, r->svn.buf);\n+\n+\t\tdiff_setup(&op);\n+\t\top.output_format = DIFF_FORMAT_NO_OUTPUT;\n+\t\top.change = &change;\n+\t\top.add_remove = &addremove;\n+\t\top.format_callback_data = r;\n+\t\tDIFF_OPT_SET(&op, RECURSIVE);\n+\t\tDIFF_OPT_SET(&op, IGNORE_SUBMODULES);\n+\t\tDIFF_OPT_SET(&op, TREE_IN_RECURSIVE);\n+\n+\t\tif (r->svn.len) {\n+\t\t\tstrbuf_addch(&r->svn, '/');\n+\t\t}\n+\n+\t\tif (r->parent) {\n+\t\t\tif (diff_tree_sha1(cmt_sha1(r->parent), cmt_sha1(cmt), r->svn.buf, &op))\n+\t\t\t\tdie(\"diff tree failed\");\n+\t\t} else {\n+\t\t\tif (diff_root_tree_sha1(cmt_sha1(cmt), r->svn.buf, &op))\n+\t\t\t\tdie(\"diff tree failed\");\n+\t\t}\n+\n+\t\tif (r->svn.len) {\n+\t\t\tstrbuf_setlen(&r->svn, r->svn.len - 1);\n+\t\t}\n+\n+\t\tdiffcore_std(&op);\n+\t\tdiff_flush(&op);\n+\t}\n+\n+\tchange_dir(\"\");\n+\tsendf(\"( close-dir ( %s ) )\\n\", dtoken(0));\n+\tsendf(\"( close-edit ( ) )\\n\");\n+\n+\treturn read_commit_revno(time);\n+}\n+\n+struct push {\n+\tstruct push* next;\n+\tstruct object* old;\n+\tstruct object* new;\n+\tstruct svnref* ref;\n+\tstruct commit* copysrc;\n+};\n+\n+/* logobj is used for the log message and author, gitcmt is used for the\n+ * tree. gitcmt is non NULL on branch mod and creation, NULL on\n+ * deletion. logobj will be NULL for branch deletion and branch creation\n+ * where gitcmt is for another branch in svn.\n+ */\n+static void push_commit(struct push* p, struct object* logobj, struct commit* gitcmt) {\n+\tstatic struct strbuf buf = STRBUF_INIT;\n+\tstatic struct strbuf time = STRBUF_INIT;\n+\tstatic struct strbuf logbuf = STRBUF_INIT;\n+\n+\tint rev;\n+\tunsigned char sha1[20];\n+\tstruct ref_lock* lk;\n+\tconst char* log;\n+\tstruct svnref *r = p->ref;\n+\tstruct author *auth = logobj ? get_object_author(logobj) : defauthor;\n+\tstruct commit *svncmt;\n+\n+\tfprintf(stderr, \"push commit %s\\n\", cmt_to_hex(gitcmt));\n+\n+\tchange_connection(0, auth);\n+\n+\tif (!logobj) {\n+\t\tstrbuf_reset(&logbuf);\n+\t\tstrbuf_addf(&logbuf, \"%s %s\\n\",\n+\t\t\tr->parent ? \"Create\" : \"Remove\",\n+\t\t\tr->svn.buf);\n+\t\tlog = logbuf.buf;\n+\n+\t} else if (logobj->type == OBJ_COMMIT) {\n+\t\tfind_commit_subject(((struct commit*) logobj)->buffer, &log);\n+\n+\t} else if (logobj->type == OBJ_TAG) {\n+\t\tunsigned long size;\n+\t\tenum object_type type;\n+\t\tchar* data = read_sha1_file(logobj->sha1, &type, &size);\n+\t\tfind_commit_subject(data, &log);\n+\t\tstrbuf_reset(&logbuf);\n+\t\tstrbuf_add(&logbuf, log, parse_signature(log, data + size - log));\n+\t\tfree(data);\n+\t\tlog = logbuf.buf;\n+\n+\t} else {\n+\t\tdie(\"unexpected type\");\n+\t}\n+\n+\tstrbuf_reset(&time);\n+\trev = send_commit(r, gitcmt, p->copysrc, log, &time);\n+\n+\t/* If we find any intermediate commits, we die. They will be\n+\t * picked up the next time the user does a pull.  If we have\n+\t * just created a branch then the svn server will check this for\n+\t * us by failing on the add-dir. If we have just replaced a\n+\t * branch then we don't really care as the git history for this\n+\t * branch wouldn't have referenced those commits anyways (they\n+\t * will be picked up on the next fetch though in case they are\n+\t * copied on the svn side later).\n+\t */\n+\tif (!p->copysrc && r->svncmt) {\n+\t\tcheck_for_svn_commits(r, parse_svnrev(r->svncmt), rev);\n+\t}\n+\n+\t/* create the svn object */\n+\n+\tstrbuf_reset(&buf);\n+\tstrbuf_addf(&buf, \"tree %s\\n\", sha1_to_hex(idx_sha1(svn_index(r))));\n+\n+\tif (gitcmt || r->svncmt) {\n+\t\tstrbuf_addf(&buf, \"parent %s\\n\", cmt_to_hex(gitcmt ? gitcmt : r->svncmt));\n+\t}\n+\n+\tif (r->svncmt) {\n+\t\tstrbuf_addf(&buf, \"parent %s\\n\", cmt_to_hex(r->svncmt));\n+\t}\n+\n+\tstrbuf_addf(&buf, \"author %s <%s> %s +0000\\n\", auth->name, auth->mail, time.buf);\n+\tstrbuf_addf(&buf, \"committer %s <%s> %s +0000\\n\", auth->name, auth->mail, time.buf);\n+\tstrbuf_addf(&buf, \"revision %d\\n\", rev);\n+\tstrbuf_addf(&buf, \"path %s\\n\", r->svn.buf);\n+\tstrbuf_addch(&buf, '\\n');\n+\n+\tif (write_sha1_file(buf.buf, buf.len, \"commit\", sha1))\n+\t\tdie(\"failed to create svn commit\");\n+\n+\tsvncmt = lookup_commit(sha1);\n+\tif (!svncmt || parse_commit(svncmt))\n+\t\tdie(\"failed to parse created svn commit\");\n+\n+\t/* update the ref */\n+\n+\tlk = lock_ref_sha1(r->ref.buf + strlen(\"refs/\"), cmt_sha1(r->svncmt));\n+\tif (!lk || write_ref_sha1(lk, cmt_sha1(svncmt), \"svn-push\"))\n+\t\tdie(\"failed to grab ref lock for %s\", r->ref.buf);\n+\n+\t/* update the remote */\n+\n+\tif (leave_remote) {\n+\t\t/* we don't update 'remotes' that are not stored in a\n+\t\t * remote directory (e.g. refs/tags/ or refs/heads/ when\n+\t\t * svn.remote is not set)\n+\t\t */\n+\n+\t} else if (gitcmt) {\n+\t\tlk = lock_ref_sha1(r->remote.buf + strlen(\"refs/\"), r->gitobj ? r->gitobj->sha1 : null_sha1);\n+\t\tif (!lk || write_ref_sha1(lk, cmt_sha1(gitcmt), \"svn-push\"))\n+\t\t\tdie(\"failed to update ref %s\", r->remote.buf);\n+\n+\t} else if (r->gitobj) {\n+\t\tif (delete_ref(r->remote.buf, r->gitobj->sha1, 0))\n+\t\t\tdie(\"failed to delete ref %s\", r->remote.buf);\n+\n+\t}\n+\n+\tr->svncmt = svncmt;\n+\tr->parent = gitcmt;\n+\tp->copysrc = NULL;\n+\n+\tif (logobj) {\n+\t\tr->gitobj = logobj;\n+\t} else if (gitcmt) {\n+\t\tr->gitobj = &gitcmt->object;\n+\t} else {\n+\t\tr->gitobj = NULL;\n+\t}\n+\n+\tif (pause_between_commits) {\n+\t\tint fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);\n+\t\tstruct sockaddr_in addr;\n+\t\taddr.sin_addr.s_addr = ntohl(INADDR_LOOPBACK);\n+\t\taddr.sin_port = htons(pause_between_commits);\n+\t\tbind(fd, (struct sockaddr*) &addr, sizeof(addr));\n+\t\tlisten(fd, SOMAXCONN);\n+\t\tclose(accept(fd, NULL, NULL));\n+\t\tclose(accept(fd, NULL, NULL));\n+\t\tclose(fd);\n+\t\tpause_between_commits = 0;\n+\t}\n+}\n+\n+static int has_parent(struct commit *c, struct commit *parent) {\n+\tstruct commit_list *p = c->parents;\n+\n+\t/* null parents are only parents of root commits */\n+\tif (!parent) {\n+\t\treturn p == NULL;\n+\t}\n+\n+\twhile (p) {\n+\t\tif (p->item == parent) {\n+\t\t\treturn 1;\n+\t\t}\n+\t\tp = p->next;\n+\t}\n+\treturn 0;\n+}\n+\n+static void do_push(struct push* p) {\n+\tstruct svnref* r = p->ref;\n+\tstruct commit* cmt;\n+\tstruct rev_info walk;\n+\n+\tif (p->old != r->gitobj) {\n+\t\tdie(\"non fast-forward for %s\", r->ref.buf);\n+\t}\n+\n+\tif (p->old && !p->new) {\n+\t\t/* deleting a ref */\n+\t\tpush_commit(p, NULL, NULL);\n+\n+\t} else if (p->new != p->old) {\n+\t\t/* add/modify/replace a ref */\n+\t\tint have_commits = 0;\n+\t\tstruct commit *new = (struct commit*) deref_tag(p->new, NULL, 0);\n+\n+\t\tif (!new || new->object.type != OBJ_COMMIT)\n+\t\t\tdie(\"invalid tag %s\", sha1_to_hex(p->new->sha1));\n+\n+\t\tinit_revisions(&walk, NULL);\n+\t\tadd_pending_object(&walk, &new->object, \"to\");\n+\t\twalk.reverse = 1;\n+\n+\t\tif (p->old) {\n+\t\t\tstruct object *old = deref_tag(p->old, NULL, 0);\n+\t\t\tif (!old || old->type != OBJ_COMMIT)\n+\t\t\t\tdie(\"invalid tag %s\", sha1_to_hex(p->old->sha1));\n+\n+\t\t\told->flags |= UNINTERESTING;\n+\t\t\tadd_pending_object(&walk, old, \"from\");\n+\t\t}\n+\n+\t\tif (p->copysrc) {\n+\t\t\tstruct object* obj = &svn_commit(p->copysrc)->object;\n+\t\t\tobj->flags |= UNINTERESTING;\n+\t\t\tadd_pending_object(&walk, obj, \"from\");\n+\t\t\t/* sets r->parent for the has_parent check below */\n+\t\t\tcheckout_svncmt(r, p->copysrc);\n+\t\t}\n+\n+\t\tif (prepare_revision_walk(&walk))\n+\t\t\tdie(\"prepare rev walk failed\");\n+\n+\t\twhile ((cmt = get_revision(&walk)) != NULL) {\n+\t\t\t/* The revwalk gives us all paths that go from\n+\t\t\t * copysrc or p->old to cmt. We can work with\n+\t\t\t * any of these so pick one arbitrarily by using\n+\t\t\t * r->parent. r->parent is set by\n+\t\t\t * checkout_svncmt above for add/replace.\n+\t\t\t */\n+\t\t\tif (!has_parent(cmt, r->parent)) continue;\n+\n+\t\t\tif (cmt == new) {\n+\t\t\t\t/* use the tag object in p->new for the\n+\t\t\t\t * log message */\n+\t\t\t\tpush_commit(p, p->new, cmt);\n+\t\t\t} else {\n+\t\t\t\tpush_commit(p, &cmt->object, cmt);\n+\t\t\t}\n+\n+\t\t\thave_commits = 1;\n+\t\t}\n+\n+\t\t/* if there were no commits we have to force through a\n+\t\t * commit to create/replace the branch/tag in svn. */\n+\n+\t\tif (r->gitobj != p->new) {\n+\t\t\t/* If we have an annotated tag, we can use that\n+\t\t\t * for a log statement. Otherwise we have to\n+\t\t\t * create a message.\n+\t\t\t */\n+\t\t\tstruct object *logobj = (p->new != &new->object) ? p->new : NULL;\n+\t\t\tpush_commit(p, logobj, new);\n+\t\t}\n+\n+\t\treset_revision_walk();\n+\t}\n+}\n+\n+static void new_push(struct push** list, const char* ref, const char* oldref, const char* newref) {\n+\tunsigned char old[20], new[20];\n+\tstruct push *p = xcalloc(1, sizeof(*p));\n+\n+\tif (get_sha1(oldref, old))\n+\t\tdie(\"invalid ref %s\", oldref);\n+\n+\tif (get_sha1(newref, new))\n+\t\tdie(\"invalid ref %s\", newref);\n+\n+\tif (!is_null_sha1(old)) {\n+\t\tp->old = parse_object(old);\n+\t\tif (!p->old)\n+\t\t\tdie(\"invalid ref %s\", oldref);\n+\t}\n+\n+\tif (!is_null_sha1(new)) {\n+\t\tp->new = parse_object(new);\n+\t\tif (!p->new)\n+\t\t\tdie(\"invalid ref %s\", newref);\n+\t}\n+\n+\tp->ref = find_svnref_by_refname(ref);\n+\tp->next = *list;\n+\t*list = p;\n+}\n+\n+int cmd_svn_push(int argc, const char **argv, const char *prefix) {\n+\tstruct push *updates = NULL, *p;\n+\tchar buf[256];\n+\n+\tgit_config(&config, NULL);\n+\n+\targc = parse_options(argc, argv, prefix, builtin_svn_push_options,\n+\t\t\tbuiltin_svn_push_usage, 0);\n+\n+\tsetup_globals();\n+\n+\t/* get the list of references to push */\n+\tif (push_from_stdin) {\n+\t\tif (argc)\n+\t\t\tusage_msg_opt( _(\"Too many arguments.\"),\n+\t\t\t\tbuiltin_svn_push_usage, builtin_svn_push_options);\n+\n+\n+\t\twhile (fgets(buf, sizeof(buf), stdin)) {\n+\t\t\tsize_t sz = strlen(buf);\n+\t\t\tif (sz <= 82) continue;\n+\n+\t\t\tif (buf[sz-1] == '\\n') {\n+\t\t\t\tbuf[--sz] = '\\0';\n+\t\t\t}\n+\t\t\tif (buf[sz-1] == '\\r') {\n+\t\t\t\tbuf[--sz] = '\\0';\n+\t\t\t}\n+\n+\t\t\tbuf[40] = '\\0';\n+\t\t\tbuf[81] = '\\0';\n+\t\t\tnew_push(&updates, &buf[82], &buf[0], &buf[41]);\n+\t\t}\n+\t} else {\n+\t\tif (argc != 3)\n+\t\t\tusage_msg_opt(argc > 3 ? _(\"Too many arguments.\") : _(\"Too few arguments\"),\n+\t\t\t\tbuiltin_svn_push_usage, builtin_svn_push_options);\n+\n+\t\tnew_push(&updates, argv[0], argv[1], argv[2]);\n+\t}\n+\n+\t/* modify/delete refs */\n+\tfor (p = updates; p != NULL; p = p->next) {\n+\t\tif (p->new) {\n+\t\t\tp->copysrc = find_copy_source(p->ref, p->new);\n+\t\t}\n+\t\tif (p->old && (!p->copysrc || deref_tag(p->old, NULL, 0) == &svn_commit(p->copysrc)->object)) {\n+\t\t\tp->copysrc = NULL;\n+\t\t\tdo_push(p);\n+\t\t\tp->ref = NULL;\n+\t\t}\n+\t}\n+\n+\t/* add/replace refs - do this last so we can find copy bases in\n+\t * the modified refs. Note if two added refs have a common\n+\t * commit branching off of svn then those common commits will be\n+\t * assigned to whichever ref comes first (i.e. unspecified). */\n+\tfor (p = updates; p != NULL; p = p->next) {\n+\t\tif (p->ref) {\n+\t\t\tp->copysrc = find_copy_source(p->ref, p->new);\n+\t\t\tdo_push(p);\n+\t\t}\n+\t}\n+\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex ec64cac..64f6d6d 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -120,6 +120,8 @@ git-status                              mainporcelain common\n git-stripspace                          purehelpers\n git-submodule                           mainporcelain\n git-svn                                 foreignscminterface\n+git-svn-fetch\t\t\t\tforeignscminterface\n+git-svn-push\t\t\t\tforeignscminterface\n git-symbolic-ref                        plumbingmanipulators\n git-tag                                 mainporcelain common\n git-tar-tree                            plumbinginterrogators\tdeprecated\ndiff --git a/git.c b/git.c\nindex 8788b32..c861b5a 100644\n--- a/git.c\n+++ b/git.c\n@@ -425,6 +425,8 @@ static void handle_internal_command(int argc, const char **argv)\n \t\t{ \"stage\", cmd_add, RUN_SETUP | NEED_WORK_TREE },\n \t\t{ \"status\", cmd_status, RUN_SETUP | NEED_WORK_TREE },\n \t\t{ \"stripspace\", cmd_stripspace },\n+\t\t{ \"svn-fetch\", cmd_svn_fetch, RUN_SETUP },\n+\t\t{ \"svn-push\", cmd_svn_push, RUN_SETUP },\n \t\t{ \"symbolic-ref\", cmd_symbolic_ref, RUN_SETUP },\n \t\t{ \"tag\", cmd_tag, RUN_SETUP },\n \t\t{ \"tar-tree\", cmd_tar_tree },\ndiff --git a/svn-sync.sh b/svn-sync.sh\nnew file mode 100755\nindex 0000000..af8a66e\n--- /dev/null\n+++ b/svn-sync.sh\n@@ -0,0 +1,150 @@\n+#!/bin/sh\n+\n+make\n+\n+rm -rf svn\n+mkdir svn\n+cd svn\n+\n+#Setup the test database\n+svnadmin create db\n+\n+cat > db/conf/passwd <<!\n+[users]\n+user = pass\n+!\n+\n+cat > db/conf/svnserve.conf <<!\n+[general]\n+anon-access = none\n+auth-access = write\n+password-db = passwd\n+realm = Test Repository\n+!\n+\n+# Setup the subversion repo\n+killall svnserve\n+killall lt-svnserve\n+svnserve --daemon --log-file svnlog --root db\n+\n+svn co --username user --password pass svn://localhost co\n+cd co\n+svn mkdir trunk\n+cd trunk\n+cat > file.txt <<!\n+Some file contents\n+Some more\n+\n+!\n+svn add file.txt\n+svn ci -m 'add file.txt'\n+\n+svn mkdir --parents a/b/c/d/e/f\n+cat > a/b/c/d/e/f/deep.txt <<!\n+Some deep contents\n+.....\n+!\n+svn add a/b/c/d/e/f/deep.txt\n+svn ci -m 'add deep.txt'\n+svn up\n+\n+svn rm file.txt\n+svn ci -m 'remove file.txt'\n+svn up\n+\n+svn mkdir svn://localhost/tags -m 'create tags folder'\n+\n+svn rm a/b\n+svn ci -m 'remove folder a/b'\n+svn up\n+\n+cat > a/foo.txt <<!\n+Some other contents for foo.txt\n+!\n+svn add a/foo.txt\n+svn ci -m 'add foo.txt'\n+svn up\n+\n+svn mv a b\n+svn ci -m 'move folder'\n+svn up\n+\n+svn mv b/foo.txt b/foo2.txt\n+svn ci -m 'move file'\n+svn up\n+\n+cd ..\n+#echo \"some new text\" >> trunk/b/foo2.txt\n+#echo \"some new file\" > trunk/b/foo.txt\n+#svn add trunk/b/foo.txt\n+#svn mkdir fake\n+#svn ci -m 'git commit add/mod'\n+#exit\n+\n+svn mkdir branches\n+svn ci -m 'create branches folder'\n+svn up\n+\n+svn cp svn://localhost/trunk svn://localhost/branches/foobranch -m 'create branch'\n+svn cp svn://localhost/trunk@4 svn://localhost/tags/footag -m 'create tag'\n+\n+cd ..\n+git init\n+\n+cat > .git/svn-authors <<!\n+# Some comment\n+\n+user:pass = James M <james@example.com>\n+!\n+\n+../git-svn-fetch -v -r 3 -t trunk -b branches -T tags --user user --pass pass svn://localhost\n+../git-svn-fetch -v -t trunk -b branches -T tags --user user --pass pass svn://localhost\n+\n+git config user.name 'James M'\n+git config user.email 'james@example.com'\n+\n+\n+git checkout -b master svn/master\n+\n+echo \"some new text\" >> b/foo2.txt\n+echo \"some new file\" > b/foo.txt\n+git add b/foo.txt b/foo2.txt\n+git commit -m 'git commit add/mod'\n+\n+mkdir -p b/c\n+echo \"some more text\" >> b/c/foo.txt\n+git add b/c/foo.txt\n+git commit -m 'git commit 2'\n+\n+git rm -rf b\n+git commit -m 'some removals'\n+\n+\n+git checkout -b foobranch svn/foobranch\n+\n+echo \"some branch file\" >> b/foo2.txt\n+git add b/foo2.txt\n+git commit -m 'git branch commit'\n+\n+../git svn-push -v -t trunk -b branches -T tags svn://localhost heads/master svn/master master\n+../git svn-push -v -t trunk -b branches -T tags svn://localhost heads/foobranch svn/foobranch foobranch\n+\n+cat > .git/hooks/pre-receive <<!\n+#!/bin/sh\n+exec $PWD/../git-svn-push -v -t trunk -b branches -T tags --pre-receive svn://localhost\n+!\n+\n+chmod +x .git/hooks/pre-receive\n+git config receive.denyCurrentBranch ignore\n+\n+../git clone -- . gitco\n+cd gitco\n+\n+git config user.name 'James M'\n+git config user.email 'james@example.com'\n+\n+echo \"some mod\" >> b/foo2.txt\n+../../git add b/foo2.txt\n+../../git commit -m 'git clone commit'\n+../../git push\n+\n-- \n1.7.11.3\n"},{"id":"197251","messageId":"1345311556-70767-3-git-send-email-james@foobar.co.nz","threadId":"31281","inReplyTo":"1345311556-70767-1-git-send-email-james@foobar.co.nz","subject":"[RFC 2/2] add tests for git svn-fetch|push","fromName":"James R. McKaskill","fromEmail":"james@foobar.co.nz","sentAt":"2012-08-18T17:39:16Z","receivedAt":"2012-08-18T17:39:16Z","isPatch":false,"sender":{"key":"james@foobar.co.nz","avatar":"https://gravatar.com/avatar/7a9da4c40caed181c908ea4990fb2a534f561581df163bedaeaa5d6ab0885870?d=mp&s=160"},"body":"From: \"James R. McKaskill\" <james@foobar.co.nz>\n\n\nSigned-off-by: James R. McKaskill <james@foobar.co.nz>\n---\n t/lib-git-svn-fetch.sh          | 131 +++++++++++\n t/t9050-git-svn-fetch.sh        |  85 +++++++\n t/t9051-git-svn-fetch-branch.sh | 245 ++++++++++++++++++++\n t/t9052-git-svn-push.sh         | 140 ++++++++++++\n t/t9053-git-svn-push-branch.sh  | 478 ++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 1079 insertions(+)\n create mode 100644 t/lib-git-svn-fetch.sh\n create mode 100755 t/t9050-git-svn-fetch.sh\n create mode 100755 t/t9051-git-svn-fetch-branch.sh\n create mode 100755 t/t9052-git-svn-push.sh\n create mode 100755 t/t9053-git-svn-push-branch.sh\n\ndiff --git a/t/lib-git-svn-fetch.sh b/t/lib-git-svn-fetch.sh\nnew file mode 100644\nindex 0000000..16326a9\n--- /dev/null\n+++ b/t/lib-git-svn-fetch.sh\n@@ -0,0 +1,131 @@\n+#!/bin/sh\n+\n+. ./test-lib.sh\n+\n+if test -z \"$SVNSERVE_PORT\"\n+then\n+\tskip_all='skipping svn-(fetch|push) test. (set $SVNSERVE_PORT to enable)'\n+\ttest_done\n+fi\n+\n+svn --version | grep \"version 1.7\" &> /dev/null\n+if [ ! $? -eq 0 ]\n+then\n+\tskip_all='skipping svn-(fetch|push) test. (need svn 1.7 or newer)'\n+\ttest_done\n+fi\n+\n+svnrepo=$PWD/svnrepo\n+svnconf=$PWD/svnconf\n+svnurl=\"svn://localhost:$SVNSERVE_PORT\"\n+null_sha1=0000000000000000000000000000000000000000\n+\n+# We need this, because we should pass empty configuration directory to\n+# the 'svn commit' to avoid automated property changes and other stuff\n+# that could be set from user's configuration files in ~/.subversion.\n+svn_cmd () {\n+\t[ -d \"$svnconf\" ] || mkdir \"$svnconf\"\n+\tcat > \"$svnconf/servers\" <<!\n+[global]\n+store-plaintext-passwords = yes\n+!\n+\torig_svncmd=\"$1\"; shift\n+\tif [ -z \"$orig_svncmd\" ]; then\n+\t\tsvn\n+\t\treturn\n+\tfi\n+\techo svn $orig_svncmd $@\n+\tsvn \"$orig_svncmd\" --username committer --password pass --no-auth-cache --non-interactive --config-dir \"$svnconf\" \"$@\"\n+}\n+\n+function test_file() {\n+\tfile_contents=\"`cat $1`\"\n+\ttest_contents=\"$2\"\n+\ttest \"$file_contents\" == \"$test_contents\"\n+}\n+\n+function svn_date() {\n+\trevision=\"$1\"\n+\tdirectory=\"$2\"\n+\tsvn_cmd log -r \"$revision\" --xml -l 1 \"$directory\" | grep \"<date>\" | sed -re 's#^<date>([^\\.Z]*)\\.[0-9]+Z</date>#\\1Z#g'\n+}\n+\n+function test_git_subject() {\n+\tcommit=\"$1\"\n+\tsubject=\"$2\"\n+\tcommit_subject=\"`git log -1 --pretty=format:%s $commit`\"\n+\techo test_git_subject \"$commit_subject\" \"$subject\"\n+\ttest \"$commit_subject\" == \"$subject\"\n+}\n+\n+function test_git_author() {\n+\tcommit=\"$1\"\n+\tauthor=\"$2\"\n+\tcommit_author=\"`git log -1 --pretty=format:'%an <%ae>' $commit`\"\n+\techo test_git_author \"$commit_author\" \"$author\"\n+\ttest \"$commit_author\" == \"$author\"\n+}\n+\n+function test_git_date() {\n+\tcommit=\"$1\"\n+\tdate=\"$2\"\n+\tcommit_date=\"`git log -1 --pretty=format:%ai $commit | sed -re 's#^([^ ]*) ([^ ]*) \\+0000$#\\1T\\2Z#g'`\"\n+\techo test_git_date \"$commit_date\" \"$date\"\n+\ttest \"$commit_date\" == \"$date\"\n+}\n+\n+function test_svn_subject() {\n+\tsubject=\"$1\"\n+\trevision=`test -n \"$2\" && echo \"-r $2\"`\n+\tcommit_subject=\"`svn log -l 1 --xml $revision | grep '<msg>' | sed -re 's#<msg>(.*)#\\1#g' | sed -re 's#(.*)</msg>#\\1#g'`\"\n+\techo test_svn_subject \"$commit_subject\" \"$subject\"\n+\ttest \"$commit_subject\" == \"$subject\"\n+}\n+\n+function test_svn_author() {\n+\tauthor=\"$1\"\n+\trevision=`test -n \"$2\" && echo \"-r $2\"`\n+\tcommit_author=\"`svn log -l 1 --xml $revision | grep '<author>' | sed -re 's#<author>(.*)</author>#\\1#g'`\"\n+\techo test_svn_author \"$commit_author\" \"$author\"\n+\ttest \"$commit_author\" == \"$author\"\n+}\n+\n+show_ref() {\n+\t(git show-ref --head $1 | head -n 1 | cut -d ' ' -f 1) || echo $1\n+}\n+\n+show_tag() {\n+\tshow_ref refs/tags/$1 | git cat-file --batch | grep object | cut -f 2 -d ' '\n+}\n+\n+merge_base() {\n+\tgit merge-base `show_ref $1` `show_ref $2`\n+}\n+\n+test_expect_success 'start svnserve' '\n+\tkillall svnserve &> /dev/null\n+\tkillall lt-svnserve &> /dev/null\n+\trm -rf \"$svnrepo\" &&\n+\tmkdir -p \"$svnrepo\" &&\n+\tsvnadmin create \"$svnrepo\" &&\n+\tcat > \"$svnrepo/conf/svnserve.conf\" <<!\n+[general]\n+auth-access = write\n+password-db = passwd\n+!\n+\tcat > \"$svnrepo/conf/passwd\" <<!\n+[users]\n+committer = pass\n+!\n+\tcat > .git/svn-authors <<!\n+committer:pass = C O Mitter <committer@example.com>\n+!\n+\tsvnserve --daemon \\\n+\t\t--listen-port $SVNSERVE_PORT \\\n+\t\t--root \"$svnrepo\" \\\n+\t\t--listen-host localhost &&\n+\tgit config svn.user committer &&\n+\tgit config svn.url $svnurl &&\n+\tgit config svn.remote svn &&\n+\tsvn_cmd co $svnurl svnco\n+'\ndiff --git a/t/t9050-git-svn-fetch.sh b/t/t9050-git-svn-fetch.sh\nnew file mode 100755\nindex 0000000..7ac3643\n--- /dev/null\n+++ b/t/t9050-git-svn-fetch.sh\n@@ -0,0 +1,85 @@\n+#!/bin/sh\n+\n+test_description='git svn-fetch non trunk'\n+. ./lib-git-svn-fetch.sh\n+\n+test_expect_success 'fetch empty' '\n+\tgit svn-fetch -v &&\n+\ttest_must_fail test -x .git/refs/svn/latest &&\n+\ttest_must_fail git checkout svn/master &&\n+\ttest_must_fail git checkout svn/trunk\n+'\n+\n+test_expect_success 'init repo' '\n+\tcd svnco &&\n+\tsvn_cmd mkdir empty-dir &&\n+\techo \"some contents\" > file.txt &&\n+\tsvn_cmd add file.txt &&\n+\tsvn_cmd ci -m \"some commit\" &&\n+\tcd ..\n+'\n+\n+date=`svn_date HEAD svnco`\n+\n+test_expect_success 'fetch repo' '\n+\tgit svn-fetch -v &&\n+\ttest $(GIT_SVN_FETCH_REPORT_LATEST=1 git svn-fetch) -eq 1 &&\n+\tgit checkout svn/master &&\n+\ttest -d empty-dir &&\n+\ttest -e empty-dir/.gitempty &&\n+\ttest_file file.txt \"some contents\" &&\n+\ttest_git_subject HEAD \"some commit\" &&\n+\ttest_git_author HEAD \"C O Mitter <committer@example.com>\" &&\n+\ttest_git_date HEAD $date\n+'\n+\n+test_expect_success 'auto crlf' '\n+\tcd svnco &&\n+\techo \"666f6f0d0a6261720d0a\" | xxd -r -p > crlf.txt &&\n+\tsvn_cmd add crlf.txt &&\n+\tsvn_cmd ci -m \"crlf\" &&\n+\tcd .. &&\n+\techo \"* text=auto\" > .git/info/attributes &&\n+\tgit config svn.eol crlf &&\n+\tgit config core.eol lf &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/master &&\n+\techo \"666f6f0a6261720a\" | xxd -r -p > crlf_test.txt &&\n+\ttest \"$(cat crlf.txt)\" = \"$(cat crlf_test.txt)\"\n+'\n+\n+test_expect_success 'move file' '\n+\tcd svnco &&\n+\tseq 1 1000 > somefile &&\n+\tsvn_cmd add somefile &&\n+\tsvn_cmd ci -m \"adding some file\" &&\n+\tsvn_cmd mv somefile somefile2 &&\n+\tsvn_cmd ci -m \"moving file\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout -f svn/master &&\n+\tcmp somefile2 svnco/somefile2\n+'\n+\n+test_expect_success 'move folder' '\n+\tcd svnco &&\n+\tsvn_cmd mkdir folder &&\n+\tcd folder &&\n+\tseq 1 1000 > file1 &&\n+\tseq 1000 2000 > file2 &&\n+\tseq 3000 50000 > file3 &&\n+\tsvn_cmd add file1 file2 file3 &&\n+\tcd .. &&\n+\tsvn_cmd ci -m \"add some folder\" &&\n+\tsvn_cmd mv folder folder2 &&\n+\tsvn_cmd ci -m \"move folder\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout -f svn/master &&\n+\tcmp folder2/file1 svnco/folder2/file1 &&\n+\tcmp folder2/file2 svnco/folder2/file2 &&\n+\tcmp folder2/file3 svnco/folder2/file3\n+'\n+\n+test_done\n+\ndiff --git a/t/t9051-git-svn-fetch-branch.sh b/t/t9051-git-svn-fetch-branch.sh\nnew file mode 100755\nindex 0000000..277dcd3\n--- /dev/null\n+++ b/t/t9051-git-svn-fetch-branch.sh\n@@ -0,0 +1,245 @@\n+#!/bin/sh\n+\n+test_description='git svn-fetch branch'\n+. ./lib-git-svn-fetch.sh\n+\n+test_expect_success 'setup branches' '\n+\tcd svnco &&\n+\tsvn_cmd mkdir Trunk &&\n+\tsvn_cmd mkdir Branches &&\n+\tsvn_cmd mkdir Tags &&\n+\ttouch Trunk/file.txt &&\n+\tsvn_cmd add Trunk/file.txt &&\n+\tsvn_cmd ci -m \"init\" &&\n+\tsvn_cmd up &&\n+\techo \"other\" >> Trunk/file.txt &&\n+\tsvn_cmd ci -m \"trunk file\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit config svn.trunk Trunk &&\n+\tgit config svn.branches Branches &&\n+\tgit config svn.tags Tags &&\n+\tgit config svn.trunkref trunk &&\n+\tgit svn-fetch -v\n+'\n+\n+test_expect_success 'copied branch' '\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Branches/Branch &&\n+\tsvn_cmd ci -m \"create branch\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest `show_ref svn/trunk` == `show_ref svn/Branch`\n+'\n+\n+test_expect_success 'copied and edited branch' '\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Branches/CopiedBranch &&\n+\techo \"more\" >> Branches/CopiedBranch/file2.txt &&\n+\tsvn_cmd add Branches/CopiedBranch/file2.txt &&\n+\tsvn_cmd ci -m \"create copied branch\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/CopiedBranch &&\n+\ttest_file file.txt \"other\" &&\n+\ttest_file file2.txt \"more\" &&\n+\ttest_git_subject HEAD \"create copied branch\" &&\n+\ttest_git_subject HEAD~1 \"trunk file\" &&\n+\ttest_git_subject HEAD~2 \"init\" &&\n+\ttest `git log --pretty=oneline svn/trunk..svn/CopiedBranch | wc -l` -eq 1 &&\n+\tmerge_base svn/trunk svn/CopiedBranch\n+'\n+\n+test_expect_success 'edited and copied branch' '\n+\tcd svnco &&\n+\techo \"more\" >> Trunk/file2.txt &&\n+\tsvn_cmd add Trunk/file2.txt &&\n+\tsvn_cmd copy Trunk Branches/EditCopyBranch &&\n+\tsvn_cmd ci -m \"create edit copy branch\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/EditCopyBranch &&\n+\ttest_file file.txt \"other\" &&\n+\ttest_file file2.txt \"more\" &&\n+\ttest_git_subject HEAD \"create edit copy branch\" &&\n+\ttest `show_ref svn/trunk` == `show_ref svn/EditCopyBranch`\n+'\n+\n+# the copy commits shouldn't create git commits\n+\n+test_expect_success 'copy, copy, copy' '\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Branches/FastCopy2 &&\n+\tsvn_cmd copy Branches/FastCopy2 Branches/FastCopy1 &&\n+\tsvn_cmd copy Branches/FastCopy2 Branches/FastCopy3 &&\n+\tsvn_cmd ci -m \"fast copy\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest `show_ref svn/FastCopy1` == `show_ref svn/FastCopy2` &&\n+\ttest `show_ref svn/FastCopy1` == `show_ref svn/FastCopy3` &&\n+\ttest `show_ref svn/FastCopy1` == `show_ref svn/trunk` &&\n+\ttest_must_fail test_git_subject svn/FastCopy1 \"fast copy\"\n+'\n+\n+# 'edit copy delete 1' shouldn't create a git commit\n+\n+test_expect_success 'edit, copy, and delete' '\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Branches/EditCopyDelete &&\n+\tsvn_cmd ci -m \"edit copy delete 1\" &&\n+\tsvn_cmd up &&\n+\techo \"edit copy delete\" >> Branches/EditCopyDelete/file3.txt &&\n+\tsvn_cmd add Branches/EditCopyDelete/file3.txt &&\n+\tsvn_cmd copy Branches/EditCopyDelete Branches/EditCopyDelete2 &&\n+\tsvn_cmd rm --force Branches/EditCopyDelete &&\n+\tsvn_cmd ci -m \"edit copy delete 2\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest_must_fail git checkout svn/EditCopyDelete &&\n+\tgit checkout svn/EditCopyDelete2 &&\n+\ttest_git_subject HEAD \"edit copy delete 2\" &&\n+\ttest_must_fail test_git_subject HEAD~1 \"edit copy delete 1\" &&\n+\ttest_file file3.txt \"edit copy delete\"\n+'\n+\n+test_expect_success 'copy, edit, copy, and delete' '\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Branches/CopyEditCopyDelete &&\n+\techo \"copy edit copy delete\" >> Branches/CopyEditCopyDelete/file4.txt &&\n+\tsvn_cmd add Branches/CopyEditCopyDelete/file4.txt &&\n+\tsvn_cmd copy Branches/CopyEditCopyDelete Branches/CopyEditCopyDelete2 &&\n+\tsvn_cmd rm --force Branches/CopyEditCopyDelete &&\n+\tsvn_cmd ci -m \"copy edit copy delete\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest_must_fail git checkout svn/CopyEditCopyDelete &&\n+\tgit checkout svn/CopyEditCopyDelete2 &&\n+\ttest_git_subject HEAD \"copy edit copy delete\" &&\n+\ttest_file file4.txt \"copy edit copy delete\"\n+'\n+\n+test_expect_success 'non copied branch' '\n+\tcd svnco &&\n+\tsvn_cmd mkdir Branches/NonCopiedBranch &&\n+\techo \"non copied\" >> Branches/NonCopiedBranch/file.txt &&\n+\tsvn_cmd add Branches/NonCopiedBranch/file.txt &&\n+\tsvn_cmd ci -m \"create non-copied branch\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/NonCopiedBranch &&\n+\ttest_file file.txt \"non copied\" &&\n+\ttest ! -e file2.txt &&\n+\ttest_git_subject HEAD \"create non-copied branch\" &&\n+\ttest `git log --pretty=oneline | wc -l` -eq 1 &&\n+\ttest_must_fail merge_base svn/trunk svn/NonCopiedBranch\n+'\n+\n+test_expect_success 'removed branch' '\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Branches/RemovedBranch &&\n+\tsvn_cmd ci -m \"create branch\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest `show_ref svn/RemovedBranch` == `show_ref svn/trunk` &&\n+\trev=$(GIT_SVN_FETCH_REPORT_LATEST=1 git svn-fetch) &&\n+\tcd svnco &&\n+\tsvn_cmd rm Branches/RemovedBranch &&\n+\tsvn_cmd ci -m \"remove branch\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest_must_fail git checkout svn/RemovedBranch &&\n+\tcd svnco &&\n+\techo $rev &&\n+\tsvn_cmd copy Branches/RemovedBranch@$rev Branches/RemovedBranch2 &&\n+\tsvn_cmd ci -m \"copy branch\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest `show_ref svn/RemovedBranch2` == `show_ref svn/trunk` &&\n+\tgit checkout svn/RemovedBranch2 &&\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Branches/RemovedBranch &&\n+\techo \"foo\" > Branches/RemovedBranch/newfile.txt &&\n+\tsvn_cmd add Branches/RemovedBranch/newfile.txt &&\n+\tsvn_cmd ci -m \"create branch again\" &&\n+\tsvn_cmd up &&\n+\tsvn_cmd rm Branches/RemovedBranch2 &&\n+\tsvn_cmd copy Branches/RemovedBranch@$rev Branches/RemovedBranch2 &&\n+\tsvn_cmd ci -m \"copy branch again\" &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/RemovedBranch &&\n+\tgit checkout svn/RemovedBranch2 &&\n+\ttest `show_ref svn/RemovedBranch` != `show_ref svn/trunk` &&\n+\ttest `show_ref svn/RemovedBranch2` == `show_ref svn/trunk`\n+'\n+\n+test_expect_success 'move branch' '\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Branches/MovedBranch &&\n+\tsvn_cmd ci -m \"create branch\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/MovedBranch &&\n+\tcd svnco &&\n+\tsvn_cmd mv Branches/MovedBranch Branches/MovedBranch2 &&\n+\tsvn_cmd ci -m \"move branch\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest_must_fail git checkout svn/MovedBranch &&\n+\tgit checkout svn/MovedBranch2 &&\n+\ttest `show_ref svn/MovedBranch2` == `show_ref svn/trunk`\n+'\n+\n+test_expect_success 'tag' '\n+\tcd svnco &&\n+\tsvn_cmd copy Trunk Tags/Tag &&\n+\tsvn_cmd ci -m \"create tag\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/Tag &&\n+\ttest `show_tag svn/Tag` == `show_ref svn/trunk` &&\n+\trev=$(GIT_SVN_FETCH_REPORT_LATEST=1 git svn-fetch) &&\n+\tcd svnco &&\n+\tsvn_cmd rm Tags/Tag &&\n+\tsvn_cmd ci -m \"remove tag\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest_must_fail git checkout svn/Tag &&\n+\tcd svnco &&\n+\tsvn_cmd copy Tags/Tag@$rev Branches/CopiedTag &&\n+\tsvn_cmd ci -m \"copy tag\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/CopiedTag &&\n+\ttest `show_ref svn/trunk` == `show_ref refs/remotes/svn/CopiedTag` &&\n+\tcd svnco &&\n+\tsvn_cmd copy Branches/CopiedTag Tags/Tag &&\n+\tsvn_cmd ci -m \"create tag again\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit checkout svn/Tag &&\n+\ttest `show_tag svn/Tag` == `show_ref svn/trunk` &&\n+\tcd svnco &&\n+\tsvn_cmd copy Tags/Tag Tags/Tag2 &&\n+\tsvn_cmd ci -m \"create 2nd tag\" &&\n+\tsvn_cmd rm Tags/Tag &&\n+\tsvn_cmd copy Tags/Tag2 Tags/Tag &&\n+\tsvn_cmd ci -m \"recreate tag from 2nd\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\ttest `show_tag svn/Tag` == `show_tag svn/Tag2` &&\n+\ttest `show_tag svn/Tag` == `show_ref svn/trunk`\n+\n+'\n+\n+test_done\n+\ndiff --git a/t/t9052-git-svn-push.sh b/t/t9052-git-svn-push.sh\nnew file mode 100755\nindex 0000000..1a8cf1e\n--- /dev/null\n+++ b/t/t9052-git-svn-push.sh\n@@ -0,0 +1,140 @@\n+#!/bin/sh\n+\n+test_description='git svn-push'\n+. ./lib-git-svn-fetch.sh\n+\n+test_expect_success 'init push' '\n+\techo \"foo\" > file.txt &&\n+\tgit add file.txt &&\n+\tgit commit -a -m \"initial commit\" &&\n+\tgit svn-push -v refs/remotes/svn/master $null_sha1 master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest_svn_subject \"initial commit\" &&\n+\ttest_svn_author committer &&\n+\ttest_file file.txt \"foo\" &&\n+\tcd ..\n+'\n+\n+test_expect_success 'multiple commits' '\n+\techo \"bar\" >> file.txt &&\n+\tgit commit -a -m \"second commit\" &&\n+\tmkdir a &&\n+\techo \"fefifofum\" >> a/test &&\n+\tgit add a/test &&\n+\tgit commit -a -m \"third commit\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\tsvn_cmd up -r 2 &&\n+\ttest_svn_subject \"second commit\" &&\n+\ttest_svn_author committer &&\n+\techo foo > file_test.txt &&\n+\techo bar >> file_test.txt &&\n+\ttest_file file.txt \"$(cat file_test.txt)\" &&\n+\ttest ! -e a &&\n+\tsvn_cmd up -r 3 &&\n+\ttest_svn_subject \"third commit\" &&\n+\ttest_svn_author committer &&\n+\ttest_file a/test \"fefifofum\" &&\n+\tcd ..\n+'\n+\n+test_expect_success 'remove git empty directories' '\n+\tmkdir -p b/c/d &&\n+\ttouch b/c/d/foo.txt &&\n+\tgit add b/c/d/foo.txt &&\n+\tgit commit -a -m \"add dir\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest -e b/c/d/foo.txt &&\n+\tcd .. &&\n+\trm -rf b &&\n+\tgit commit -a -m \"rm dir\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest ! -e b &&\n+\tcd ..\n+'\n+\n+test_expect_success 'remove file' '\n+\ttouch foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -a -m \"add file\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest -e foo.txt &&\n+\tcd .. &&\n+\trm foo.txt &&\n+\tgit commit -a -m \"rm file\" &&\n+\tgit svn-push -v svn/master svn/master master\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest ! -e foo.txt &&\n+\tcd ..\n+'\n+\n+test_expect_success 'remove svn empty directories' '\n+\tcd svnco &&\n+\tsvn_cmd mkdir empty &&\n+\tsvn_cmd commit -m \"make empty\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tgit reset --hard svn/master &&\n+\ttest -e empty/.gitempty &&\n+\trm empty/.gitempty &&\n+\tgit commit -a -m \"remove empty\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\ttest \"$(git clean -n -d | grep empty)\" = \"Would remove empty/\" &&\n+\tcd ..\n+'\n+\n+test_expect_success '.git files' '\n+\tmkdir h &&\n+\ttouch h/.githidden &&\n+\tgit add h/.githidden &&\n+\tgit commit -a -m \"add h/.githidden\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest -e h &&\n+\ttest ! -e h/.githidden &&\n+\tcd ..\n+'\n+\n+test_expect_success 'modify file' '\n+\techo \"foo\" > file.txt &&\n+\tgit add file.txt &&\n+\tgit commit -a -m \"edit1\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest_svn_subject \"edit1\" &&\n+\ttest_file file.txt \"foo\" &&\n+\tcd .. &&\n+\techo \"bar\" > file.txt &&\n+\tgit commit -a -m \"edit2\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest_svn_subject \"edit2\" &&\n+\ttest_file file.txt \"bar\" &&\n+\tcd ..\n+'\n+\n+test_expect_success 'big file' '\n+\tseq 1 100000 > file.txt &&\n+\tgit add file.txt &&\n+\tgit commit -a -m \"big file\" &&\n+\tgit svn-push -v svn/master svn/master master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcmp file.txt ../file.txt &&\n+\tcd ..\n+'\n+\n+test_done\n+\ndiff --git a/t/t9053-git-svn-push-branch.sh b/t/t9053-git-svn-push-branch.sh\nnew file mode 100755\nindex 0000000..501ab3c\n--- /dev/null\n+++ b/t/t9053-git-svn-push-branch.sh\n@@ -0,0 +1,478 @@\n+#!/bin/sh\n+\n+test_description='git svn-push branch'\n+. ./lib-git-svn-fetch.sh\n+\n+function check_branched() {\n+\tcopyfrom_path=\"$1\"\n+\tcopyfrom_rev=\"$2\"\n+\techo check_branched $1 $2\n+\tsvn_cmd log --stop-on-copy -v --xml | grep copyfrom-path=\\\"/$copyfrom_path\\\" &&\n+\tsvn_cmd log --stop-on-copy -v --xml | grep copyfrom-rev=\\\"$copyfrom_rev\\\"\n+}\n+\n+test_expect_success 'setup branches' '\n+\tgit config svn.trunk Trunk &&\n+\tgit config svn.branches Branches &&\n+\tgit config svn.tags Tags &&\n+\tgit config svn.trunkref trunk &&\n+\tcd svnco &&\n+\tsvn_cmd mkdir Branches &&\n+\tsvn_cmd mkdir Tags &&\n+\tsvn_cmd ci -m \"svn init\" &&\n+\tcd ..\n+'\n+\n+test_expect_success 'init trunk' '\n+\techo \"foo\" > file.txt &&\n+\tgit add file.txt &&\n+\tgit commit -a -m \"init trunk\" &&\n+\tgit svn-push -v refs/remotes/svn/trunk $null_sha1 master &&\n+\ttest `show_ref svn/trunk` == `show_ref master` &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest -d Trunk &&\n+\ttest_svn_subject \"init trunk\" &&\n+\ttest_svn_author committer &&\n+\ttest_file Trunk/file.txt \"foo\" &&\n+\tcd ..\n+'\n+\n+test_expect_success 'modify file' '\n+\tcd svnco/Trunk &&\n+\techo \"bar23\" > file.txt &&\n+\tsvn_cmd ci -m \"svn edit\" &&\n+\tcd ../.. &&\n+\tgit svn-fetch -v &&\n+\tgit reset --hard svn/trunk &&\n+\ttest_file file.txt \"bar23\" &&\n+\techo \"foo\" > file.txt &&\n+\tgit commit -a -m \"git edit\" &&\n+\tgit svn-push -v svn/trunk svn/trunk HEAD &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Trunk &&\n+\ttest_svn_subject \"git edit\" &&\n+\ttest_svn_subject \"svn edit\" PREV &&\n+\ttest_file file.txt \"foo\" &&\n+\tcd ../..\n+'\n+\n+test_expect_success 'modify file2' '\n+\tcd svnco/Trunk &&\n+\techo \"bar\" > svn.txt &&\n+\tsvn_cmd add svn.txt &&\n+\tsvn_cmd ci -m \"svn edit\" &&\n+\tcd ../.. &&\n+\tgit svn-fetch -v &&\n+\tgit reset --hard svn/trunk &&\n+\ttest_file svn.txt \"bar\" &&\n+\techo \"foo\" > svn.txt &&\n+\tgit commit -a -m \"git edit\" &&\n+\tgit svn-push -v svn/trunk svn/trunk HEAD &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Trunk &&\n+\ttest_svn_subject \"git edit\" &&\n+\ttest_svn_subject \"svn edit\" PREV &&\n+\ttest_file svn.txt \"foo\" &&\n+\tcd ../..\n+'\n+\n+function svn_head() {\n+\twd=`pwd` &&\n+\tcd svnco &&\n+\tcd \"$1\" &&\n+\tsvn info | grep Revision | sed -e 's/Revision: *//g' &&\n+\tcd \"$wd\"\n+}\n+\n+init_trunk_rev=`svn_head Trunk`\n+init_trunk_path=Trunk\n+\n+test_expect_success 'create standalone branch' '\n+\tgit symbolic-ref HEAD refs/heads/standalone &&\n+\tgit rm -r --cached . &&\n+\techo \"bar\" > file.txt &&\n+\tgit add file.txt &&\n+\trm svn.txt &&\n+\tgit commit -a -m \"init standalone\" &&\n+\tgit svn-push -v refs/remotes/svn/standalone $null_sha1 standalone &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/standalone &&\n+\ttest_file file.txt \"bar\" &&\n+\ttest_svn_subject \"init standalone\" &&\n+\ttest_must_fail svn_cmd log PREV &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'create branch' '\n+\tgit checkout -b CreateBranch master &&\n+\tgit svn-push -v refs/remotes/svn/CreateBranch $null_sha1 CreateBranch &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/CreateBranch &&\n+\ttest_file file.txt \"foo\" &&\n+\ttest_svn_subject \"Create Branches/CreateBranch\" &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../..\n+'\n+\n+init_trunk_rev=`svn_head Branches/CreateBranch`\n+init_trunk_path=Branches/CreateBranch\n+\n+test_expect_success 'create and edit branch' '\n+\tgit checkout -b CreateEditBranch master &&\n+\techo \"foo2\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -a -m \"create/edit branch\" &&\n+\tgit svn-push -v refs/remotes/svn/CreateEditBranch $null_sha1 CreateEditBranch &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/CreateEditBranch &&\n+\ttest_file file.txt \"foo\" &&\n+\ttest_file file2.txt \"foo2\" &&\n+\ttest_svn_subject \"create/edit branch\" &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'create tag' '\n+\tgit checkout master &&\n+\tgit tag SimpleTag &&\n+\tgit svn-push -v refs/tags/svn/SimpleTag $null_sha1 HEAD &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Tags/SimpleTag &&\n+\ttest_file file.txt \"foo\" &&\n+\ttest_svn_subject \"Create Tags/SimpleTag\" &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'create annotated tag' '\n+\tgit checkout master &&\n+\tgit tag -m \"annotate tag\" AnnotatedTag &&\n+\tgit svn-push -v refs/tags/svn/AnnotatedTag $null_sha1 AnnotatedTag &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Tags/AnnotatedTag &&\n+\ttest_file file.txt \"foo\" &&\n+\ttest_svn_subject \"annotate tag\" &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'replace branch' '\n+\tgit checkout -b ReplaceBranch master &&\n+\techo \"before replace\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -a -m \"before replace\" &&\n+\tbefore_sha1=`show_ref HEAD` &&\n+\tgit svn-push -v refs/remotes/svn/ReplaceBranch $null_sha1 ReplaceBranch &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/ReplaceBranch &&\n+\ttest_file file2.txt \"before replace\" &&\n+\ttest_svn_subject \"before replace\" &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../.. &&\n+\tgit checkout master &&\n+\tgit branch -D ReplaceBranch &&\n+\tgit checkout -b ReplaceBranch master &&\n+\techo \"after replace\" > file3.txt &&\n+\tgit add file3.txt &&\n+\tgit commit -a -m \"after replace\" &&\n+\tgit svn-push -v svn/ReplaceBranch $before_sha1 ReplaceBranch &&\n+\tcd svnco/Branches/ReplaceBranch &&\n+\tsvn_cmd up &&\n+\ttest ! -e file2.txt &&\n+\ttest_file file3.txt \"after replace\" &&\n+\ttest_svn_subject \"after replace\" &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'replace tag' '\n+\tgit checkout -b temp master &&\n+\techo \"foo\" > file3.txt &&\n+\tgit add file3.txt &&\n+\tgit commit -a -m \"before replace tag\" &&\n+\told_sha1=`show_ref HEAD` &&\n+\tgit tag ReplaceTag &&\n+\tgit svn-push -v refs/tags/svn/ReplaceTag $null_sha1 ReplaceTag &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Tags/ReplaceTag &&\n+\ttest_svn_subject \"before replace tag\" &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../.. &&\n+\tgit reset --hard master &&\n+\techo \"bar\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -a -m \"after replace tag\" &&\n+\tgit tag -f ReplaceTag &&\n+\tgit svn-push -v svn/ReplaceTag $old_sha1 ReplaceTag &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Tags/ReplaceTag &&\n+\ttest_svn_subject \"after replace tag\" &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../.. &&\n+\techo \"bar2\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -a -m \"dummy commit\" &&\n+\tgit tag -f -m \"create replace tag\" ReplaceTag &&\n+\tgit svn-push -v svn/ReplaceTag HEAD~ ReplaceTag &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Tags/ReplaceTag &&\n+\ttest_svn_subject \"create replace tag\" &&\n+\ttest_svn_subject \"after replace tag\" PREV &&\n+\tcd ../../.. &&\n+\tgit checkout master &&\n+\tgit branch -D temp\n+'\n+\n+test_expect_success 'delete branch' '\n+\tgit checkout -b DeleteBranch master &&\n+\tgit svn-push -v refs/remotes/svn/DeleteBranch $null_sha1 DeleteBranch &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/DeleteBranch &&\n+\ttest_svn_subject \"Create Branches/DeleteBranch\" &&\n+\tcd ../../.. &&\n+\tgit checkout master &&\n+\tgit svn-push -v svn/DeleteBranch DeleteBranch $null_sha1 &&\n+\tgit branch -D DeleteBranch &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest ! -e Branches/DeleteBranch &&\n+\tcd ..\n+'\n+\n+test_expect_success 'delete tag' '\n+\tgit checkout master &&\n+\tgit tag DeleteTag &&\n+\tgit svn-push -v refs/tags/svn/DeleteTag $null_sha1 DeleteTag &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Tags/DeleteTag &&\n+\ttest_svn_subject \"Create Tags/DeleteTag\" &&\n+\tcd ../../.. &&\n+\tgit svn-push -v svn/DeleteTag DeleteTag $null_sha1 &&\n+\tgit tag -d DeleteTag &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest ! -e Tags/DeleteTag &&\n+\tcd ..\n+'\n+\n+test_expect_success 'modify and create branch' '\n+\tgit checkout -b MCBranch1 master &&\n+\tgit svn-push -v refs/remotes/svn/MCBranch1 $null_sha1 MCBranch1 &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\tinit_trunk_rev=`svn_head Branches/MCBranch1`\n+\tinit_trunk_path=Branches/MCBranch1 &&\n+\techo \"bar\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -a -m \"some modification on MCBranch1\" &&\n+\techo \"$null_sha1 `show_ref HEAD` refs/remotes/svn/MCBranch2\" >> cmds.txt &&\n+\techo \"`show_ref master` `show_ref HEAD` refs/remotes/svn/MCBranch1\" >> cmds.txt &&\n+\tgit svn-push -v --stdin < cmds.txt &&\n+\trm -f cmds.txt &&\n+\tbefore_rev=`svn_head Branches/MCBranch1` &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/MCBranch1 &&\n+\ttest_svn_subject \"some modification on MCBranch1\" &&\n+\tcd ../MCBranch2 &&\n+\ttest_svn_subject \"Create Branches/MCBranch2\" &&\n+\tcheck_branched Branches/MCBranch1 $(($before_rev+1)) &&\n+\tcd ../../..\n+'\n+test_expect_success 'modify and replace branch' '\n+\tgit checkout -b MRBranch1 master &&\n+\techo \"change\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -a -m \"some modification on MRBranch1\" &&\n+\tgit svn-push -v refs/remotes/svn/MRBranch1 $null_sha1 MRBranch1 &&\n+\tgit svn-push -v refs/remotes/svn/MRBranch2 $null_sha1 master &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/MRBranch1 &&\n+\ttest_svn_subject \"some modification on MRBranch1\" &&\n+\tcd ../MRBranch2 &&\n+\ttest_svn_subject \"Create Branches/MRBranch2\" &&\n+\tcd ../../.. &&\n+\tbefore_rev=`svn_head Branches/MRBranch2` &&\n+\tgit checkout -b MRBranch2 master &&\n+\techo \"bar\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -a -m \"some modification on MRBranch2\" &&\n+\techo \"`show_ref MRBranch1` `show_ref MRBranch2` svn/MRBranch1\" > cmds.txt &&\n+\techo \"`show_ref master` `show_ref MRBranch2` svn/MRBranch2\" >> cmds.txt &&\n+\tgit svn-push -v --stdin < cmds.txt &&\n+\trm -f cmds.txt &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/MRBranch1 &&\n+\ttest_svn_subject \"Create Branches/MRBranch1\" &&\n+\tcheck_branched Branches/MRBranch2 $(($before_rev+1)) &&\n+\tcd ../MRBranch2 &&\n+\ttest_svn_subject \"some modification on MRBranch2\" &&\n+\ttest_svn_subject \"Create Branches/MRBranch2\" PREV &&\n+\tcheck_branched $init_trunk_path $init_trunk_rev &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'tag deleted branch' '\n+\tgit checkout -b DeleteBranch master &&\n+\techo \"foo\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -a -m \"commit on deleted branch\" &&\n+\tgit svn-push -v refs/remotes/svn/DeleteBranch $null_sha1 HEAD &&\n+\tgit tag TagOnDeleteBranch &&\n+\tgit checkout master &&\n+\tgit svn-push -v svn/DeleteBranch DeleteBranch $null_sha1 &&\n+\tgit svn-push -v refs/tags/svn/TagOnDeleteBranch $null_sha1 TagOnDeleteBranch &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\ttest ! -e Branches/DeleteBranch &&\n+\tcd Tags/TagOnDeleteBranch &&\n+\ttest_svn_subject \"Create Tags/TagOnDeleteBranch\" &&\n+\ttest_must_fail svn_cmd log -r PREV --stop-on-copy &&\n+\tcd ../../.. &&\n+\tgit branch -D DeleteBranch\n+'\n+\n+test_expect_success 'push left merge' '\n+\tgit checkout -b LeftLeft master &&\n+\techo \"left\" > left.txt &&\n+\tgit add left.txt &&\n+\tgit commit -m \"left\" &&\n+\tgit svn-push -v refs/remotes/svn/LeftMerged $null_sha1 HEAD &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/LeftMerged &&\n+\tprev_path=`svn_cmd log --stop-on-copy -v --xml | grep copyfrom-path | sed -re \"s#.*copyfrom-path=\\\"([^\\\"]*)\\\".*#copyfrom-path=\\\"\\1\\\"#g\"` &&\n+\tprev_rev=`svn_cmd log --stop-on-copy -v --xml | grep copyfrom-rev | sed -re \"s#.*copyfrom-rev=\\\"([^\\\"]*)\\\".*#copyfrom-rev=\\\"\\1\\\"#g\"` &&\n+\techo prev_path $prev_path &&\n+\techo prev_rev $prev_rev &&\n+\tcd ../../.. &&\n+\tgit checkout -b LeftRight master &&\n+\techo \"right\" > right.txt &&\n+\tgit add right.txt &&\n+\tgit commit -m \"right\" &&\n+\tgit checkout LeftLeft &&\n+\tgit merge --no-ff \"merge commit\" HEAD LeftRight &&\n+\tgit svn-push -v svn/LeftMerged svn/LeftMerged LeftLeft &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/LeftMerged &&\n+\ttest_file left.txt \"left\" &&\n+\ttest_file right.txt \"right\" &&\n+\ttest_svn_subject \"merge commit\" &&\n+\ttest_svn_subject \"left\" PREV &&\n+\tsvn_cmd log --stop-on-copy -v --xml | grep $prev_path &&\n+\tsvn_cmd log --stop-on-copy -v --xml | grep $prev_rev &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'push right merge' '\n+\tgit checkout -b RightLeft master &&\n+\techo \"left\" > left.txt &&\n+\tgit add left.txt &&\n+\tgit commit -m \"left\" &&\n+\tgit svn-push -v refs/remotes/svn/RightMerged $null_sha1 HEAD &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/RightMerged &&\n+\tprev_path=`svn_cmd log --stop-on-copy -v --xml | grep copyfrom-path | sed -re \"s#.*copyfrom-path=\\\"([^\\\"]*)\\\".*#copyfrom-path=\\\"\\1\\\"#g\"` &&\n+\tprev_rev=`svn_cmd log --stop-on-copy -v --xml | grep copyfrom-rev | sed -re \"s#.*copyfrom-rev=\\\"([^\\\"]*)\\\".*#copyfrom-rev=\\\"\\1\\\"#g\"` &&\n+\techo prev_path $prev_path &&\n+\techo prev_rev $prev_rev &&\n+\tcd ../../.. &&\n+\tgit checkout -b RightRight master &&\n+\techo \"right\" > right.txt &&\n+\tgit add right.txt &&\n+\tgit commit -m \"right\" &&\n+\tgit merge --no-ff \"merge commit\" HEAD RightLeft &&\n+\tgit svn-push -v svn/RightMerged svn/RightMerged RightRight &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/RightMerged &&\n+\ttest_svn_subject \"merge commit\" &&\n+\ttest_svn_subject \"left\" PREV &&\n+\tsvn_cmd log --stop-on-copy -v --xml | grep $prev_path &&\n+\tsvn_cmd log --stop-on-copy -v --xml | grep $prev_rev &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'unseen new commit in svn' '\n+\tcd svnco &&\n+\tsvn_cmd cp Trunk Branches/Unseen &&\n+\tsvn_cmd ci -m \"make branch\" &&\n+\tcd .. &&\n+\tgit svn-fetch -v &&\n+\tcd svnco/Branches/Unseen &&\n+\techo \"foo\" > unseen.txt &&\n+\tsvn_cmd add unseen.txt &&\n+\tsvn_cmd ci -m \"unseen file\" &&\n+\tcd ../../.. &&\n+\tgit checkout -b unseen svn/Unseen &&\n+\techo \"bar\" > seen.txt &&\n+\tgit add seen.txt &&\n+\tgit commit -m \"seen file\" &&\n+\t# This should push the commit and then fail, so after the fetch\n+\t# and rebase no push should be required\n+\ttest_must_fail git svn-push -v svn/Unseen unseen~1 unseen &&\n+\tgit svn-fetch -v &&\n+\tgit rebase svn/Unseen &&\n+\ttest `show_ref svn/Unseen` == `show_ref unseen` &&\n+\tcd svnco/Branches/Unseen &&\n+\tsvn_cmd up &&\n+\ttest_file unseen.txt \"foo\" &&\n+\ttest_file seen.txt \"bar\" &&\n+\tcd ../../..\n+'\n+\n+test_expect_success 'intermingled commits' '\n+\tgit checkout -b intermingled svn/trunk &&\n+\techo \"bar\" > file1.txt &&\n+\tgit add file1.txt &&\n+\tgit commit -m \"commit 1\" &&\n+\techo \"foo\" > file2.txt &&\n+\tgit add file2.txt &&\n+\tgit commit -m \"commit 2\" &&\n+\tport=$(($SVNSERVE_PORT+1)) || exit 1\n+\tGIT_SVN_PUSH_PAUSE=$port git svn-push -v refs/remotes/svn/intermingled $null_sha1 HEAD &\n+\tpush_pid=$! &&\n+\tuntil nc -z localhost $port; do sleep 1; done &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd Branches/intermingled &&\n+\techo \"foobar\" > file3.txt &&\n+\tsvn_cmd add file3.txt &&\n+\tsvn_cmd ci -m \"svn commit\" &&\n+\tcd ../../.. &&\n+\tnc -z localhost $port &&\n+\ttest_must_fail wait $push_pid &&\n+\tgit svn-fetch -v &&\n+\tgit rebase svn/intermingled &&\n+\tgit svn-push -v svn/intermingled svn/intermingled HEAD &&\n+\tcd svnco &&\n+\tsvn_cmd up &&\n+\tcd .. &&\n+\trev=`svn_head Branches/intermingled` &&\n+\tcd svnco/Branches/intermingled &&\n+\ttest_svn_subject \"commit 2\" $rev &&\n+\ttest_svn_subject \"svn commit\" $(($rev-1)) &&\n+\ttest_svn_subject \"commit 1\" $(($rev-2)) &&\n+\tcd ../../..\n+'\n+\n+test_done\n-- \n1.7.11.3\n"},{"id":"197281","messageId":"5030BE33.9000007@gmail.com","threadId":"31281","inReplyTo":"1345311556-70767-1-git-send-email-james@foobar.co.nz","subject":"Re: [RFC 0/2] svn-fetch|push - an alternate approach","fromName":"Marco Schulze","fromEmail":"marco.c.schulze@gmail.com","sentAt":"2012-08-19T10:21:39Z","receivedAt":"2012-08-19T10:21:39Z","isPatch":false,"sender":{"key":"marco.c.schulze@gmail.com","avatar":null},"body":"\nOn 18-08-2012 14:39, James R. McKaskill wrote:\n> As a twist the code does not use the svn library, but rather talks the\n> svn protocol directly. I actually found it much easier to go this route\n> then trying to bend everything to how the svn library understands\n> things. It also has the advantage of not depending on libsvn. A number\n> of distributions currently distribute the svn specific parts of git\n> seperately to avoid this dependency.\nIt is a bit of a pain to work with libsvn, but I think it is worth using \nit, not only due to protocol support, but also due to ready-to-use \nsupport for SVN deltas and dumps.  Pipelining could be implemented by \nmaintaining a set of connections (svn_ra_session_t structures) and \nmanually distributing work between them.\n"},{"id":"197513","messageId":"5033336C.4050906@alum.mit.edu","threadId":"31281","inReplyTo":"5030BE33.9000007@gmail.com","subject":"Re: [RFC 0/2] svn-fetch|push - an alternate approach","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2012-08-21T07:06:20Z","receivedAt":"2012-08-21T07:06:20Z","isPatch":false,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"On 08/19/2012 12:21 PM, Marco Schulze wrote:\n> \n> On 18-08-2012 14:39, James R. McKaskill wrote:\n>> As a twist the code does not use the svn library, but rather talks the\n>> svn protocol directly. I actually found it much easier to go this route\n>> then trying to bend everything to how the svn library understands\n>> things. It also has the advantage of not depending on libsvn. A number\n>> of distributions currently distribute the svn specific parts of git\n>> seperately to avoid this dependency.\n> It is a bit of a pain to work with libsvn, but I think it is worth using \n> it, not only due to protocol support, but also due to ready-to-use \n> support for SVN deltas and dumps.  Pipelining could be implemented by \n> maintaining a set of connections (svn_ra_session_t structures) and \n> manually distributing work between them.\n\nUsing libsvn would also have the benefit of letting the Subversion\nproject worry about forwards and backwards compatibility if the protocol\never changes.  (They tend to be quite diligent about that.)\n\nMichael\n\n-- \nMichael Haggerty\nmhagger@alum.mit.edu\nhttp://softwareswirl.blogspot.com/\n"},{"id":"197570","messageId":"CAGwq8_4nKKN5jjR8Z7qvDzuoTjYhjYiudhnPHD-JLgnASAdCKQ@mail.gmail.com","threadId":"31281","inReplyTo":"5033336C.4050906@alum.mit.edu","subject":"Re: [RFC 0/2] svn-fetch|push - an alternate approach","fromName":"James McKaskill","fromEmail":"james@foobar.co.nz","sentAt":"2012-08-22T02:30:43Z","receivedAt":"2012-08-22T02:30:43Z","isPatch":false,"sender":{"key":"james@foobar.co.nz","avatar":"https://gravatar.com/avatar/7a9da4c40caed181c908ea4990fb2a534f561581df163bedaeaa5d6ab0885870?d=mp&s=160"},"body":"On Tue, Aug 21, 2012 at 3:06 AM, Michael Haggerty <mhagger@alum.mit.edu> wrote:\n> On 08/19/2012 12:21 PM, Marco Schulze wrote:\n>> It is a bit of a pain to work with libsvn, but I think it is worth using\n>> it, not only due to protocol support, but also due to ready-to-use\n>> support for SVN deltas and dumps.  Pipelining could be implemented by\n>> maintaining a set of connections (svn_ra_session_t structures) and\n>> manually distributing work between them.\n>\n> Using libsvn would also have the benefit of letting the Subversion\n> project worry about forwards and backwards compatibility if the protocol\n> ever changes.  (They tend to be quite diligent about that.)\n\nSo my goal with the svn support is not so much to support pushing to\nsvn, but to support it natively. What I mean by that is that it will\nuse the git configuration for http, auth, ssl, etc. For the svn\nprotocol there is little to none of this, but frankly the svn protocol\nis a one to one mapping to the svn editor interface and very simple to\nboot. So working off the interface vs working off of the protocol\nthere isn't much different in the complexity or amount of code. For\nHTTP though there is a huge benefit to working outside libsvn. Namely\nreusing the http support in git means all of the credential, http, ssl\nconfig works. Also these days again there isn't much difference\nbetween the http interface and the svn editor interface.\n\nAs for diffs and dumps ... Well svn diff parsing and generation is\nrelatively trivial (200-300 lines currently). I have no real plan\ncurrently to deal with dumps.\n\nI've since added support for HTTP. What I'll do is add a third\ninterface in the same manner that uses libsvn. If it turns out simple\nenough then that would be the way to go for dumps and svn, but I'm\nvery skeptical about using that for HTTP+svn (can't be fully sure\nwithout trying I suppose).\n\nTLDR: You've convinced me to at least give it a try.\n\n-- James\n"}]}