Design-wise, svnload resembles svndump. Include a Makefile rule to build it into vcs-svn/lib.a. Signed-off-by: Ramkumar Ramachandra <artagnon@xxxxxxxxx> --- I'm not very happy with the parse_data and parse_data_len API. It's probably too cryptic? See also: two skip_optional_lf() calls in the parse_data function. It seems to be necessary- is the fast-import documentation wrong about one optional lf? Makefile | 3 +- vcs-svn/dir_cache.c | 52 ++++++ vcs-svn/dir_cache.h | 11 + vcs-svn/dump_export.c | 164 +++++++++++++++++ vcs-svn/dump_export.h | 26 +++ vcs-svn/svnload.c | 485 +++++++++++++++++++++++++++++++++++++++++++++++++ vcs-svn/svnload.h | 19 ++ 7 files changed, 759 insertions(+), 1 deletions(-) create mode 100644 vcs-svn/dir_cache.c create mode 100644 vcs-svn/dir_cache.h create mode 100644 vcs-svn/dump_export.c create mode 100644 vcs-svn/dump_export.h create mode 100644 vcs-svn/svnload.c create mode 100644 vcs-svn/svnload.h diff --git a/Makefile b/Makefile index b0f155a..7206d35 100644 --- a/Makefile +++ b/Makefile @@ -1838,7 +1838,8 @@ endif XDIFF_OBJS = xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \ xdiff/xmerge.o xdiff/xpatience.o VCSSVN_OBJS = vcs-svn/string_pool.o vcs-svn/line_buffer.o \ - vcs-svn/repo_tree.o vcs-svn/fast_export.o vcs-svn/svndump.o + vcs-svn/repo_tree.o vcs-svn/fast_export.o vcs-svn/svndump.o \ + vcs-svn/svnload.o vcs-svn/dump_export.o vcs-svn/dir_cache.o VCSSVN_TEST_OBJS = test-obj-pool.o test-string-pool.o \ test-line-buffer.o test-treap.o OBJECTS := $(GIT_OBJS) $(XDIFF_OBJS) $(VCSSVN_OBJS) diff --git a/vcs-svn/dir_cache.c b/vcs-svn/dir_cache.c new file mode 100644 index 0000000..551558a --- /dev/null +++ b/vcs-svn/dir_cache.c @@ -0,0 +1,52 @@ +/* + * Licensed under a two-clause BSD-style license. + * See LICENSE for details. + */ + +#include "cache.h" +#include "git-compat-util.h" +#include "string-list.h" +#include "dir_cache.h" + +static struct string_list dirents = STRING_LIST_INIT_DUP; + +void dir_cache_add(const char *path, mode_t mode) +{ + struct string_list_item *dir; + dir = string_list_insert(&dirents, path); + dir->util = xmalloc(sizeof(uint16_t)); + *((mode_t *)(dir->util)) = mode; +} + +void dir_cache_remove(const char *path) +{ + struct string_list_item *dir; + dir = string_list_lookup(&dirents, path); + if (dir) + *((mode_t *)(dir->util)) = S_IFINVALID; +} + +mode_t dir_cache_lookup(const char *path) +{ + struct string_list_item *dir; + dir = string_list_lookup(&dirents, path); + if (dir) + return *((mode_t *)(dir->util)); + else + return S_IFINVALID; +} + +void dir_cache_mkdir_p(const char *path) { + char *t, *p; + + p = (char *) path; + while ((t = strchr(p, '/'))) { + *t = '\0'; + if (dir_cache_lookup(path) == S_IFINVALID) { + dir_cache_add(path, S_IFDIR); + dump_export_mkdir(path); + } + *t = '/'; /* Change it back */ + p = t + 1; + } +} diff --git a/vcs-svn/dir_cache.h b/vcs-svn/dir_cache.h new file mode 100644 index 0000000..e7e83fb --- /dev/null +++ b/vcs-svn/dir_cache.h @@ -0,0 +1,11 @@ +#ifndef DIR_CACHE_H_ +#define DIR_CACHE_H_ + +#include "dump_export.h" + +void dir_cache_add(const char *path, mode_t mode); +void dir_cache_remove(const char *path); +mode_t dir_cache_lookup(const char *path); +void dir_cache_mkdir_p(const char *path); + +#endif diff --git a/vcs-svn/dump_export.c b/vcs-svn/dump_export.c new file mode 100644 index 0000000..33ff852 --- /dev/null +++ b/vcs-svn/dump_export.c @@ -0,0 +1,164 @@ +/* + * Licensed under a two-clause BSD-style license. + * See LICENSE for details. + */ + +#include "cache.h" +#include "git-compat-util.h" +#include "dump_export.h" +#include "dir_cache.h" + +static struct strbuf props = STRBUF_INIT; +static unsigned long revn = 0; + +/* Fills props and tells if the mode represents a file */ +static int populate_props(mode_t mode) +{ + int is_file = 1; + + strbuf_reset(&props); + switch (mode) { + case S_IFINVALID: + break; + case 0644: + case S_IFREG | 0644: + break; + case 0755: + case S_IFREG | 0755: + strbuf_addf(&props, "K 14\nsvn:executable\nV 1\n*\n"); + break; + case S_IFLNK: + strbuf_addf(&props, "K 11\nsvn:special\nV 1\n*\n"); + break; + case S_IFGITLINK: + die("Gitlinks unsupported"); /* TODO */ + case S_IFDIR: + is_file = 0; + break; + default: + die("Corrupt mode: %d", mode); + } + strbuf_add(&props, "PROPS-END\n", 10); + return is_file; +} + +void dump_export_revision(struct strbuf *revprops) +{ + printf("Revision-number: %lu\n", ++ revn); + printf("Prop-content-length: %lu\n", revprops->len); + printf("Content-length: %lu\n\n", revprops->len); + printf("%s\n", revprops->buf); +} + +static void dump_export_action(enum node_action action) +{ + switch (action) { + case NODE_ACTION_CHANGE: + printf("Node-action: change\n"); + break; + case NODE_ACTION_ADD: + printf("Node-action: add\n"); + break; + case NODE_ACTION_DELETE: + printf("Node-action: delete\n"); + break; + case NODE_ACTION_REPLACE: + printf("Node-action: replace\n"); + break; + default: + die("Corrupt action: %d", action); + } +} + +void dump_export_node(const char *path, mode_t mode, + enum node_action action, size_t text_len, + const char *copyfrom_path) +{ + /* Node-path, Node-kind, and Node-action */ + printf("Node-path: %s\n", path); + printf("Node-kind: %s\n", populate_props(mode) ? "file" : "dir"); + dump_export_action(action); + + if (copyfrom_path) { + printf("Node-copyfrom-rev: %lu\n", revn - 1); + printf("Node-copyfrom-path: %s\n", copyfrom_path); + } + if (props.len) { + printf("Prop-delta: false\n"); + printf("Prop-content-length: %lu\n", props.len); + } + if (text_len) { + printf("Text-delta: false\n"); + printf("Text-content-length: %lu\n", text_len); + } + if (text_len || props.len) { + printf("Content-length: %lu\n\n", text_len + props.len); + printf("%s", props.buf); + } + + /* When no data is present, pad with two newlines */ + if (!text_len) + printf("\n\n"); +} + +void dump_export_mkdir(const char *path) +{ + dump_export_node(path, S_IFDIR, NODE_ACTION_ADD, 0, NULL); +} + +void dump_export_m(const char *path, mode_t mode, size_t text_len) +{ + enum node_action action = NODE_ACTION_CHANGE; + mode_t old_mode; + + old_mode = dir_cache_lookup(path); + + if (mode != old_mode) { + if (old_mode != S_IFINVALID) { + dump_export_d(path); + dir_cache_remove(path); + } + action = NODE_ACTION_ADD; + dir_cache_mkdir_p(path); + dir_cache_add(path, mode); + } + + dump_export_node(path, mode, action, text_len, NULL); +} + +void dump_export_d(const char *path) +{ + printf("Node-path: %s\n", path); + dump_export_action(NODE_ACTION_DELETE); + printf("\n\n"); + dir_cache_remove(path); +} + +void dump_export_cr(const char *path, const char *copyfrom_path, + int delete_old) +{ + enum node_action action = NODE_ACTION_REPLACE; + mode_t mode, old_mode; + + mode = dir_cache_lookup(path); + old_mode = dir_cache_lookup(copyfrom_path); + + if (old_mode == S_IFINVALID) + die("Can't copy from non-existant path: %s", copyfrom_path); + if (mode != old_mode) { + action = NODE_ACTION_ADD; + dir_cache_mkdir_p(path); + dir_cache_add(path, mode); + } + if (delete_old) { + dump_export_d(copyfrom_path); + dir_cache_remove(copyfrom_path); + } + + dump_export_node(path, old_mode, action, 0, copyfrom_path); +} + +void dump_export_init() +{ + printf("SVN-fs-dump-format-version: 3\n\n"); +} diff --git a/vcs-svn/dump_export.h b/vcs-svn/dump_export.h new file mode 100644 index 0000000..647701d --- /dev/null +++ b/vcs-svn/dump_export.h @@ -0,0 +1,26 @@ +#ifndef DUMP_EXPORT_H_ +#define DUMP_EXPORT_H_ + +#define MAX_GITSVN_LINE_LEN 4096 +#define SVN_INVALID_REV 0 + +enum node_action { + NODE_ACTION_CHANGE, + NODE_ACTION_ADD, + NODE_ACTION_DELETE, + NODE_ACTION_REPLACE, + NODE_ACTION_COUNT +}; + +void dump_export_revision(struct strbuf *revprops); +void dump_export_node(const char *path, mode_t mode, + enum node_action action, size_t text_len, + const char *copyfrom_path); +void dump_export_mkdir(const char *path); +void dump_export_m(const char *path, mode_t mode, size_t text_len); +void dump_export_d(const char *path); +void dump_export_cr(const char *path, const char *copyfrom_path, + int delete_old); +void dump_export_init(); + +#endif diff --git a/vcs-svn/svnload.c b/vcs-svn/svnload.c new file mode 100644 index 0000000..c07e475 --- /dev/null +++ b/vcs-svn/svnload.c @@ -0,0 +1,485 @@ +/* + * Produce a dumpfile v3 from a fast-import stream. + * Load the dump into the SVN repository with: + * svnrdump load <URL> <dumpfile + * + * Licensed under a two-clause BSD-style license. + * See LICENSE for details. + */ + +#include "cache.h" +#include "quote.h" +#include "svnload.h" +#include "dump_export.h" +#include "dir_cache.h" + +static FILE *infile; +static struct strbuf command_buf = STRBUF_INIT; +static struct strbuf path_d = STRBUF_INIT; + +static int read_next_command(void) +{ + strbuf_reset(&command_buf); + return strbuf_getline(&command_buf, infile, '\n'); +} + +static void populate_revprops(struct strbuf *revprops, + struct ident *svn_ident, struct strbuf *log) +{ + strbuf_reset(revprops); + strbuf_addf(revprops, "K 10\nsvn:author\nV %lu\n%s\n", + svn_ident->name.len, svn_ident->name.buf); + strbuf_addf(revprops, "K 7\nsvn:log\nV %lu\n%s\n", + log->len, log->buf); + if (svn_ident->date) + /* SVN doesn't like an empty svn:date value */ + strbuf_addf(revprops, "K 8\nsvn:date\nV %d\n%s\n", + SVN_DATE_LEN - 1, svn_ident->date); + strbuf_add(revprops, "PROPS-END\n", 10); +} + +static void parse_ident(const char *buf, struct ident *identp) +{ + char *original_buf, *t, *tz_off; + int tz_off_buf; + const struct tm *tm_time; + + /* John Doe <johndoe@xxxxxxxxx> 1170199019 +0530 */ + strbuf_reset(&(identp->name)); + strbuf_reset(&(identp->email)); + + original_buf = strdup(buf); + if (!(tz_off = strrchr(buf, ' '))) + goto error; + *tz_off++ = '\0'; + if (!(t = strrchr(buf, ' '))) + goto error; + *(t - 1) = '\0'; /* Ignore '>' from email */ + t++; + tz_off_buf = atoi(tz_off); + + /* UTC -1200 to UTC +1400 are valid */ + if (tz_off_buf > 1400 || tz_off_buf < -1200) + goto error; + tm_time = time_to_tm(strtoul(t, NULL, 10), tz_off_buf); + strftime(identp->date, SVN_DATE_LEN, SVN_DATE_FORMAT, tm_time); + if (!(t = strchr(buf, '<'))) + goto error; + *(t - 1) = '\0'; /* Ignore ' <' from email */ + t++; + + strbuf_add(&(identp->email), t, strlen(t)); + strbuf_add(&(identp->name), buf, strlen(buf)); + free(original_buf); + return; +error: + die("Malformed ident line: %s", original_buf); +} + +static void skip_optional_lf(void) +{ + int term_char = fgetc(infile); + if (term_char != '\n' && term_char != EOF) + ungetc(term_char, infile); +} + +/* Either sets term and returns terminator length or returns data + length after setting term to NULL */ +static size_t parse_data_len(char *term) +{ + uintmax_t length; + + term = NULL; + if (prefixcmp(command_buf.buf, "data ")) + die("Expected 'data n' command, found: %s", command_buf.buf); + + if (!prefixcmp(command_buf.buf + 5, "<<")) { + term = xstrdup(command_buf.buf + 5 + 2); + if (!(command_buf.len - 5 - 2)) + die("Missing delimeter after 'data <<' in: %s", command_buf.buf); + return (size_t) (command_buf.len - 5 - 2); + } + + length = strtoumax(command_buf.buf + 5, NULL, 10); + if ((size_t) length < length) + die("Data is too large to use in this context"); + + return (size_t) length; +} + +/* When term is filled in, nbytes refers to the size of the + terminator; otherwise, it refers to the size of the actual data. + The parsed data is written to dst and out, if they exist. */ +static void parse_data(char *term, size_t nbytes, struct strbuf *dst, FILE *out) +{ + size_t in; + size_t done = 0; + + if (term) { + /* Read line-by-line until terminator is encountered */ + while (1) { + if (read_next_command() == EOF) + die("Expected terminator '%s', found EOF", term); + + /* If the terminator is encountered, stop reading */ + if (nbytes == command_buf.len + && !memcmp(term, command_buf.buf, nbytes)) + break; + + if (dst) { + strbuf_addbuf(dst, &command_buf); + strbuf_addch(dst, '\n'); + } + if (out) { + strbuf_fwrite(&command_buf, command_buf.len, out); + fprintf(out, "\n"); + } + } + free(term); + goto END; + } + + /* Read nbytes bytes in chunks */ + while (done < nbytes && !feof(infile) && !ferror(infile)) { + in = (nbytes - done) < COPY_BUFFER_LEN ? + (nbytes - done) : COPY_BUFFER_LEN; + strbuf_reset(&command_buf); + in = strbuf_fread(&command_buf, in, infile); + done += in; + if (dst) + strbuf_addbuf(dst, &command_buf); + if (out) + strbuf_fwrite(&command_buf, command_buf.len, out); + } + if (done != nbytes) + die("Expected %lu bytes, read %lu bytes", nbytes, done); + + if (out) + fprintf(out, "\n"); /* Hack: Incase data is not terminated with lf */ +END: + skip_optional_lf(); + skip_optional_lf(); +} + +static const char *get_mode(const char *str, uint16_t *modep) +{ + unsigned char c; + uint16_t mode = 0; + + while ((c = *str++) != ' ') { + if (c < '0' || c > '7') + return NULL; + mode = (mode << 3) + (c - '0'); + } + *modep = mode; + return str; +} + +static void file_change_m(const char *p) +{ + struct strbuf dst = STRBUF_INIT; + const char *endp; + uint16_t mode; + size_t nbytes; + char *term; + + if (!p) + die("Missing mode after filemodify in: %s", command_buf.buf); + + if (!(p = get_mode(p, &mode))) + die("Corrupt mode: %s", command_buf.buf); + if (!prefixcmp(p, "inline ")) + p += 7; + else + die("Non-inlined data unsupported"); + + /* Parse out path into path_d */ + strbuf_reset(&path_d); + if (!unquote_c_style(&path_d, p, &endp)) { + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + } else + strbuf_addstr(&path_d, p); + + read_next_command(); + nbytes = parse_data_len(term); + if (term) { + strbuf_reset(&dst); + parse_data(term, nbytes, &dst, NULL); + dump_export_m(path_d.buf, mode, dst.len); + fwrite(&dst.buf, 1, dst.len, stdout); + return; + } + dump_export_m(path_d.buf, mode, nbytes); + parse_data(NULL, nbytes, NULL, stdout); +} + +static void file_change_d(const char *p) +{ + const char *endp; + + if (!p) + die("Missing path after filedelete in: %s", command_buf.buf); + + strbuf_reset(&path_d); + if (!unquote_c_style(&path_d, p, &endp)) { + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + } else + strbuf_addstr(&path_d, p); + dump_export_d(path_d.buf); +} + +static void file_change_cr(const char *p, int delete_old) +{ + struct strbuf path_s = STRBUF_INIT; + const char *endp; + + if (!p) + die("Missing source after %s in: %s", + delete_old ? "filerename" : "filecopy", command_buf.buf); + + strbuf_reset(&path_s); + if (!unquote_c_style(&path_s, p, &endp)) { + if (*endp != ' ') + die("Missing destination after source in: %s", command_buf.buf); + } else { + endp = strchr(p, ' '); + if (!endp) + die("Missing destination after source in: %s", command_buf.buf); + strbuf_add(&path_s, p, endp - p); + } + + endp++; + if (!*endp) + die("Missing destination in: %s", command_buf.buf); + + p = endp; + strbuf_reset(&path_d); + if (!unquote_c_style(&path_d, p, &endp)) { + if (*endp) + die("Garbage after destination in: %s", command_buf.buf); + } else + strbuf_addstr(&path_d, p); + + /* TODO: Check C "path/to/subdir" "" */ + dump_export_cr(path_d.buf, path_s.buf, delete_old); +} + +static void build_svn_ident(struct ident *svn_ident, + struct ident *author, struct ident *committer) +{ + char *t, *email; + + strbuf_reset(&(svn_ident->name)); + memcpy(svn_ident->date, committer->date, SVN_DATE_LEN); + email = author->email.len ? author->email.buf : committer->email.buf; + if ((t = strchr(email, '@'))) + strbuf_add(&(svn_ident->name), email, t - email); + else + strbuf_addstr(&(svn_ident->name), email); +} + +static void parse_ignore_notemodify(const char *p) +{ + char *term; + size_t nbytes; + + if (!p) + die("Missing dataref after notemodify in: %s", command_buf.buf); + if (!(p = strchr(p, ' '))) + die ("Missing committish after dataref in: %s", command_buf.buf); + + read_next_command(); + term = NULL; + nbytes = parse_data_len(term); + parse_data(term, nbytes, NULL, NULL); +} + +static void parse_commit(const char *p) +{ + static struct strbuf log = STRBUF_INIT; + static struct strbuf revprops = STRBUF_INIT; + static struct ident author = {STRBUF_INIT, STRBUF_INIT, ""}; + static struct ident committer = {STRBUF_INIT, STRBUF_INIT, ""}; + static struct ident svn_ident = {STRBUF_INIT, {0, 0, NULL}, ""}; + + char *ident_buf, *term; + size_t nbytes; + + /* TODO: Parse and use branch */ + if (!p) + die("Missing ref after commit in: %s", command_buf.buf); + read_next_command(); + + /* Parse and ignore mark line */ + if (!prefixcmp(command_buf.buf, "mark :")) + read_next_command(); + + if (!prefixcmp(command_buf.buf, "author ")) { + ident_buf = strbuf_detach(&command_buf, &command_buf.len); + parse_ident(ident_buf + 7, &author); + free(ident_buf); + read_next_command(); + } + if (!prefixcmp(command_buf.buf, "committer ")) { + ident_buf = strbuf_detach(&command_buf, &command_buf.len); + parse_ident(ident_buf + 10, &committer); + free(ident_buf); + read_next_command(); + } + if (!committer.name.len) + die("Missing committer line in stream"); + + /* Parse the log */ + strbuf_reset(&log); + term = NULL; + nbytes = parse_data_len(term); + parse_data(term, nbytes, &log, NULL); + read_next_command(); + + if (!prefixcmp(command_buf.buf, "from ")) + /* TODO: Support copyfrom */ + read_next_command(); + while (!prefixcmp(command_buf.buf, "merge ")) + /* TODO: Support merges */ + read_next_command(); + + /* Translation from Git metadata to SVN metadata */ + build_svn_ident(&svn_ident, &author, &committer); + populate_revprops(&revprops, &svn_ident, &log); + dump_export_revision(&revprops); + + do { + if (!prefixcmp(command_buf.buf, "M ")) + file_change_m(command_buf.buf + 2); + else if (!prefixcmp(command_buf.buf, "D ")) + file_change_d(command_buf.buf + 2); + else if (!prefixcmp(command_buf.buf, "R ")) + file_change_cr(command_buf.buf + 2, 1); + else if (!prefixcmp(command_buf.buf, "C ")) + file_change_cr(command_buf.buf + 2, 0); + else if (!prefixcmp(command_buf.buf, "N ")) + parse_ignore_notemodify(command_buf.buf + 2); + else if (!prefixcmp(command_buf.buf, "ls ")) + goto error; /* TODO */ + else if (!strcmp("deleteall", command_buf.buf)) + goto error; /* TODO */ + else + /* Unrecognized command is left on command_buf */ + break; + } while (read_next_command() != EOF); + + /* Eat up optional trailing lf */ + if (!command_buf.len) + read_next_command(); + return; +error: + die("Unsupported command: %s", command_buf.buf); +} + +static void parse_tag(const char *p) +{ + char *term; + size_t nbytes; + + if (!p) + die("Missing name after tag in: %s", command_buf.buf); + read_next_command(); + + if (prefixcmp(command_buf.buf, "from ")) + die("Expected 'from committish', found: %s", command_buf.buf); + p = command_buf.buf + 5; + if (!p) + die("Missing committish after from in: %s", command_buf.buf); + read_next_command(); + + if (prefixcmp(command_buf.buf, "tagger ")) + die("Expected 'tagger (name?) email when', found: %s", command_buf.buf); + p = command_buf.buf + 7; + if (*p != '<') + p = strchr(p, '<'); + if (!p) + die("Missing name or email after tagger in: %s", command_buf.buf); + if (!(p = strchr(p, '>'))) + die("Malformed email in: %s", command_buf.buf); + if (!(++ p)) + die("Missing when after email in: %s", command_buf.buf); + read_next_command(); + + term = NULL; + nbytes = parse_data_len(term); + parse_data(term, nbytes, NULL, NULL); + read_next_command(); +} + +void parse_reset_branch(const char *p) +{ + if (!p) + die("Missing ref after reset in: %s", command_buf.buf); + read_next_command(); + + if (!prefixcmp(command_buf.buf, "from ")) { + p = command_buf.buf + 5; + if (!p) + die("Missing committish after from in: %s", command_buf.buf); + read_next_command(); + } + + skip_optional_lf(); +} + +void svnload_read(void) +{ + read_next_command(); + + /* Every function in the loop keeps reading until it + encounteres EOF or an unrecognized command; the + unrecognized command is left on command_buf */ + while (!feof(infile)) { + if (!strcmp("blob", command_buf.buf)) + die("Non-inlined blobs unsupported"); + else if (!prefixcmp(command_buf.buf, "ls ")) + goto error; /* TODO */ + else if (!prefixcmp(command_buf.buf, "cat-blob ")) + goto error; /* TODO */ + else if (!prefixcmp(command_buf.buf, "commit ")) + parse_commit(command_buf.buf + 7); + else if (!prefixcmp(command_buf.buf, "tag ")) + /* TODO: No-op */ + parse_tag(command_buf.buf + 4); + else if (!prefixcmp(command_buf.buf, "reset ")) + /* TODO: No-op */ + parse_reset_branch(command_buf.buf + 6); + else if (!strcmp(command_buf.buf, "checkpoint") + || !prefixcmp(command_buf.buf, "progress ")) { + /* Ignored */ + read_next_command(); + skip_optional_lf(); + } + else if (!prefixcmp(command_buf.buf, "feature ") + || !prefixcmp(command_buf.buf, "option ")) + /* Ignored */ + read_next_command(); + else + goto error; + }; + return; +error: + die("Unsupported command: %s", command_buf.buf); +} + +int svnload_init(const char *filename) +{ + infile = filename ? fopen(filename, "r") : stdin; + if (!infile) + die("Cannot open %s: %s", filename, strerror(errno)); + dump_export_init(); + return 0; +} + +void svnload_deinit(void) +{ + strbuf_release(&command_buf); + strbuf_release(&path_d); +} diff --git a/vcs-svn/svnload.h b/vcs-svn/svnload.h new file mode 100644 index 0000000..12bb559 --- /dev/null +++ b/vcs-svn/svnload.h @@ -0,0 +1,19 @@ +#ifndef SVNLOAD_H_ +#define SVNLOAD_H_ + +#include "strbuf.h" + +#define SVN_DATE_FORMAT "%Y-%m-%dT%H:%M:%S.000000Z" +#define SVN_DATE_LEN 28 +#define COPY_BUFFER_LEN 4096 + +struct ident { + struct strbuf name, email; + char date[SVN_DATE_LEN]; +}; + +int svnload_init(const char *filename); +void svnload_deinit(void); +void svnload_read(void); + +#endif -- 1.7.4.rc1.7.g2cf08.dirty -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html