Handle revisions with no revprops
[svn2git:mgedmin-svn2git.git] / src / svn.cpp
1 /*
2  *  Copyright (C) 2007  Thiago Macieira <thiago@kde.org>
3  *
4  *  This program is free software: you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation, either version 3 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 /*
19  * Based on svn-fast-export by Chris Lee <clee@kde.org>
20  * License: MIT <http://www.opensource.org/licenses/mit-license.php>
21  * URL: git://repo.or.cz/fast-import.git http://repo.or.cz/w/fast-export.git
22  */
23
24 #define _XOPEN_SOURCE
25 #define _LARGEFILE_SUPPORT
26 #define _LARGEFILE64_SUPPORT
27
28 #include "svn.h"
29 #include "CommandLineParser.h"
30
31 #include <unistd.h>
32 #include <string.h>
33 #include <stdio.h>
34 #include <time.h>
35 #include <unistd.h>
36
37 #include <apr_lib.h>
38 #include <apr_getopt.h>
39 #include <apr_general.h>
40
41 #include <svn_fs.h>
42 #include <svn_pools.h>
43 #include <svn_repos.h>
44 #include <svn_types.h>
45
46 #include <QFile>
47 #include <QDebug>
48
49 #include "repository.h"
50
51 #undef SVN_ERR
52 #define SVN_ERR(expr) SVN_INT_ERR(expr)
53
54 typedef QList<Rules::Match> MatchRuleList;
55 typedef QHash<QString, Repository *> RepositoryHash;
56 typedef QHash<QByteArray, QByteArray> IdentityHash;
57
58 class AprAutoPool
59 {
60     apr_pool_t *pool;
61     AprAutoPool(const AprAutoPool &);
62     AprAutoPool &operator=(const AprAutoPool &);
63 public:
64     inline AprAutoPool(apr_pool_t *parent = NULL)
65         { pool = svn_pool_create(parent); }
66     inline ~AprAutoPool()
67         { svn_pool_destroy(pool); }
68
69     inline void clear() { svn_pool_clear(pool); }
70     inline apr_pool_t *data() const { return pool; }
71     inline operator apr_pool_t *() const { return pool; }
72 };
73
74 class SvnPrivate
75 {
76 public:
77     QList<MatchRuleList> allMatchRules;
78     RepositoryHash repositories;
79     IdentityHash identities;
80     QString userdomain;
81
82     SvnPrivate(const QString &pathToRepository);
83     ~SvnPrivate();
84     int youngestRevision();
85     int exportRevision(int revnum);
86
87     int openRepository(const QString &pathToRepository);
88
89 private:
90     AprAutoPool global_pool;
91     svn_fs_t *fs;
92     svn_revnum_t youngest_rev;
93 };
94
95 void Svn::initialize()
96 {
97     // initialize APR or exit
98     if (apr_initialize() != APR_SUCCESS) {
99         fprintf(stderr, "You lose at apr_initialize().\n");
100         exit(1);
101     }
102
103     // static destructor
104     static struct Destructor { ~Destructor() { apr_terminate(); } } destructor;
105 }
106
107 Svn::Svn(const QString &pathToRepository)
108     : d(new SvnPrivate(pathToRepository))
109 {
110 }
111
112 Svn::~Svn()
113 {
114     delete d;
115 }
116
117 void Svn::setMatchRules(const QList<MatchRuleList> &allMatchRules)
118 {
119     d->allMatchRules = allMatchRules;
120 }
121
122 void Svn::setRepositories(const RepositoryHash &repositories)
123 {
124     d->repositories = repositories;
125 }
126
127 void Svn::setIdentityMap(const IdentityHash &identityMap)
128 {
129     d->identities = identityMap;
130 }
131
132 void Svn::setIdentityDomain(const QString &identityDomain)
133 {
134     d->userdomain = identityDomain;
135 }
136
137 int Svn::youngestRevision()
138 {
139     return d->youngestRevision();
140 }
141
142 bool Svn::exportRevision(int revnum)
143 {
144     return d->exportRevision(revnum) == EXIT_SUCCESS;
145 }
146
147 SvnPrivate::SvnPrivate(const QString &pathToRepository)
148     : global_pool(NULL)
149 {
150     if( openRepository(pathToRepository) != EXIT_SUCCESS) {
151         qCritical() << "Failed to open repository";
152         exit(1);
153     }
154
155     // get the youngest revision
156     svn_fs_youngest_rev(&youngest_rev, fs, global_pool);
157 }
158
159 SvnPrivate::~SvnPrivate()
160 {
161     svn_pool_destroy(global_pool);
162 }
163
164 int SvnPrivate::youngestRevision()
165 {
166     return youngest_rev;
167 }
168
169 int SvnPrivate::openRepository(const QString &pathToRepository)
170 {
171     svn_repos_t *repos;
172     QString path = pathToRepository;
173     while (path.endsWith('/')) // no trailing slash allowed
174         path = path.mid(0, path.length()-1);
175     SVN_ERR(svn_repos_open(&repos, QFile::encodeName(path), global_pool));
176     fs = svn_repos_fs(repos);
177
178     return EXIT_SUCCESS;
179 }
180
181 enum RuleType { AnyRule = 0, NoIgnoreRule = 0x01, NoRecurseRule = 0x02 };
182
183 static MatchRuleList::ConstIterator
184 findMatchRule(const MatchRuleList &matchRules, int revnum, const QString &current,
185               int ruleMask = AnyRule)
186 {
187     MatchRuleList::ConstIterator it = matchRules.constBegin(),
188                                 end = matchRules.constEnd();
189     for ( ; it != end; ++it) {
190         if (it->minRevision > revnum)
191             continue;
192         if (it->maxRevision != -1 && it->maxRevision < revnum)
193             continue;
194         if (it->action == Rules::Match::Ignore && ruleMask & NoIgnoreRule)
195             continue;
196         if (it->action == Rules::Match::Recurse && ruleMask & NoRecurseRule)
197             continue;
198         if (it->rx.indexIn(current) == 0) {
199             Stats::instance()->ruleMatched(*it, revnum);
200             return it;
201         }
202     }
203
204     // no match
205     return end;
206 }
207
208 static void splitPathName(const Rules::Match &rule, const QString &pathName, QString *svnprefix_p,
209                           QString *repository_p, QString *branch_p, QString *path_p)
210 {
211     QString svnprefix = pathName;
212     svnprefix.truncate(rule.rx.matchedLength());
213
214     if (svnprefix_p) {
215         *svnprefix_p = svnprefix;
216     }
217
218     if (repository_p) {
219         *repository_p = svnprefix;
220         repository_p->replace(rule.rx, rule.repository);
221         foreach (Rules::Match::Substitution subst, rule.repo_substs) {
222             subst.apply(*repository_p);
223         }
224     }
225
226     if (branch_p) {
227         *branch_p = svnprefix;
228         branch_p->replace(rule.rx, rule.branch);
229         foreach (Rules::Match::Substitution subst, rule.branch_substs) {
230             subst.apply(*branch_p);
231         }
232     }
233
234     if (path_p) {
235         QString prefix = svnprefix;
236         prefix.replace(rule.rx, rule.prefix);
237         *path_p = prefix + pathName.mid(svnprefix.length());
238     }
239 }
240
241 static int pathMode(svn_fs_root_t *fs_root, const char *pathname, apr_pool_t *pool)
242 {
243     svn_string_t *propvalue;
244     SVN_ERR(svn_fs_node_prop(&propvalue, fs_root, pathname, "svn:executable", pool));
245     int mode = 0100644;
246     if (propvalue)
247         mode = 0100755;
248
249     return mode;
250 }
251
252 svn_error_t *QIODevice_write(void *baton, const char *data, apr_size_t *len)
253 {
254     QIODevice *device = reinterpret_cast<QIODevice *>(baton);
255     device->write(data, *len);
256
257     while (device->bytesToWrite() > 32*1024) {
258         if (!device->waitForBytesWritten(-1)) {
259             qFatal("Failed to write to process: %s", qPrintable(device->errorString()));
260             return svn_error_createf(APR_EOF, SVN_NO_ERROR, "Failed to write to process: %s",
261                                      qPrintable(device->errorString()));
262         }
263     }
264     return SVN_NO_ERROR;
265 }
266
267 static svn_stream_t *streamForDevice(QIODevice *device, apr_pool_t *pool)
268 {
269     svn_stream_t *stream = svn_stream_create(device, pool);
270     svn_stream_set_write(stream, QIODevice_write);
271
272     return stream;
273 }
274
275 static int dumpBlob(Repository::Transaction *txn, svn_fs_root_t *fs_root,
276                     const char *pathname, const QString &finalPathName, apr_pool_t *pool)
277 {
278     AprAutoPool dumppool(pool);
279     // what type is it?
280     int mode = pathMode(fs_root, pathname, dumppool);
281
282     svn_filesize_t stream_length;
283
284     SVN_ERR(svn_fs_file_length(&stream_length, fs_root, pathname, dumppool));
285
286     svn_stream_t *in_stream, *out_stream;
287     if (!CommandLineParser::instance()->contains("dry-run")) {
288         // open the file
289         SVN_ERR(svn_fs_file_contents(&in_stream, fs_root, pathname, dumppool));
290     }
291
292     // maybe it's a symlink?
293     svn_string_t *propvalue;
294     SVN_ERR(svn_fs_node_prop(&propvalue, fs_root, pathname, "svn:special", dumppool));
295     if (propvalue) {
296         apr_size_t len = strlen("link ");
297         if (!CommandLineParser::instance()->contains("dry-run")) {
298             QByteArray buf;
299             buf.reserve(len);
300             SVN_ERR(svn_stream_read(in_stream, buf.data(), &len));
301             if (len == strlen("link ") && strncmp(buf, "link ", len) == 0) {
302                 mode = 0120000;
303                 stream_length -= len;
304             } else {
305                 //this can happen if a link changed into a file in one commit
306                 qWarning("file %s is svn:special but not a symlink", pathname);
307                 // re-open the file as we tried to read "link "
308                 svn_stream_close(in_stream);
309                 SVN_ERR(svn_fs_file_contents(&in_stream, fs_root, pathname, dumppool));
310             }
311         }
312     }
313
314     QIODevice *io = txn->addFile(finalPathName, mode, stream_length);
315
316     if (!CommandLineParser::instance()->contains("dry-run")) {
317         // open a generic svn_stream_t for the QIODevice
318         out_stream = streamForDevice(io, dumppool);
319         SVN_ERR(svn_stream_copy(in_stream, out_stream, dumppool));
320         svn_stream_close(out_stream);
321         svn_stream_close(in_stream);
322
323         // print an ending newline
324         io->putChar('\n');
325     }
326
327     return EXIT_SUCCESS;
328 }
329
330 static int recursiveDumpDir(Repository::Transaction *txn, svn_fs_root_t *fs_root,
331                             const QByteArray &pathname, const QString &finalPathName,
332                             apr_pool_t *pool)
333 {
334     // get the dir listing
335     apr_hash_t *entries;
336     SVN_ERR(svn_fs_dir_entries(&entries, fs_root, pathname, pool));
337     AprAutoPool dirpool(pool);
338
339     // While we get a hash, put it in a map for sorted lookup, so we can
340     // repeat the conversions and get the same git commit hashes.
341     QMap<QByteArray, svn_node_kind_t> map;
342     for (apr_hash_index_t *i = apr_hash_first(pool, entries); i; i = apr_hash_next(i)) {
343         const void *vkey;
344         void *value;
345         apr_hash_this(i, &vkey, NULL, &value);
346         svn_fs_dirent_t *dirent = reinterpret_cast<svn_fs_dirent_t *>(value);
347         map.insertMulti(QByteArray(dirent->name), dirent->kind);
348     }
349
350     QMapIterator<QByteArray, svn_node_kind_t> i(map);
351     while (i.hasNext()) {
352         dirpool.clear();
353         i.next();
354         QByteArray entryName = pathname + '/' + i.key();
355         QString entryFinalName = finalPathName + QString::fromUtf8(i.key());
356
357         if (i.value() == svn_node_dir) {
358             entryFinalName += '/';
359             if (recursiveDumpDir(txn, fs_root, entryName, entryFinalName, dirpool) == EXIT_FAILURE)
360                 return EXIT_FAILURE;
361         } else if (i.value() == svn_node_file) {
362             printf("+");
363             fflush(stdout);
364             if (dumpBlob(txn, fs_root, entryName, entryFinalName, dirpool) == EXIT_FAILURE)
365                 return EXIT_FAILURE;
366         }
367     }
368
369     return EXIT_SUCCESS;
370 }
371
372 static bool wasDir(svn_fs_t *fs, int revnum, const char *pathname, apr_pool_t *pool)
373 {
374     AprAutoPool subpool(pool);
375     svn_fs_root_t *fs_root;
376     if (svn_fs_revision_root(&fs_root, fs, revnum, subpool) != SVN_NO_ERROR)
377         return false;
378
379     svn_boolean_t is_dir;
380     if (svn_fs_is_dir(&is_dir, fs_root, pathname, subpool) != SVN_NO_ERROR)
381         return false;
382
383     return is_dir;
384 }
385
386 time_t get_epoch(const char* svn_date)
387 {
388     struct tm tm;
389     memset(&tm, 0, sizeof tm);
390     QByteArray date(svn_date, strlen(svn_date) - 8);
391     strptime(date, "%Y-%m-%dT%H:%M:%S", &tm);
392     return timegm(&tm);
393 }
394
395 class SvnRevision
396 {
397 public:
398     AprAutoPool pool;
399     QHash<QString, Repository::Transaction *> transactions;
400     QList<MatchRuleList> allMatchRules;
401     RepositoryHash repositories;
402     IdentityHash identities;
403     QString userdomain;
404
405     svn_fs_t *fs;
406     svn_fs_root_t *fs_root;
407     int revnum;
408
409     // must call fetchRevProps first:
410     QByteArray authorident;
411     QByteArray log;
412     uint epoch;
413     bool ruledebug;
414     bool propsFetched;
415     bool needCommit;
416
417     SvnRevision(int revision, svn_fs_t *f, apr_pool_t *parent_pool)
418         : pool(parent_pool), fs(f), fs_root(0), revnum(revision), propsFetched(false)
419     {
420         ruledebug = CommandLineParser::instance()->contains( QLatin1String("debug-rules"));
421     }
422
423     int open()
424     {
425         SVN_ERR(svn_fs_revision_root(&fs_root, fs, revnum, pool));
426         return EXIT_SUCCESS;
427     }
428
429     int prepareTransactions();
430     int fetchRevProps();
431     int commit();
432
433     int exportEntry(const char *path, const svn_fs_path_change_t *change, apr_hash_t *changes);
434     int exportDispatch(const char *path, const svn_fs_path_change_t *change,
435                        const char *path_from, svn_revnum_t rev_from,
436                        apr_hash_t *changes, const QString &current, const Rules::Match &rule,
437                        const MatchRuleList &matchRules, apr_pool_t *pool);
438     int exportInternal(const char *path, const svn_fs_path_change_t *change,
439                        const char *path_from, svn_revnum_t rev_from,
440                        const QString &current, const Rules::Match &rule, const MatchRuleList &matchRules);
441     int recurse(const char *path, const svn_fs_path_change_t *change,
442                 const char *path_from, const MatchRuleList &matchRules, svn_revnum_t rev_from,
443                 apr_hash_t *changes, apr_pool_t *pool);
444 };
445
446 int SvnPrivate::exportRevision(int revnum)
447 {
448     SvnRevision rev(revnum, fs, global_pool);
449     rev.allMatchRules = allMatchRules;
450     rev.repositories = repositories;
451     rev.identities = identities;
452     rev.userdomain = userdomain;
453
454     // open this revision:
455     printf("Exporting revision %d ", revnum);
456     fflush(stdout);
457
458     if (rev.open() == EXIT_FAILURE)
459         return EXIT_FAILURE;
460
461     if (rev.prepareTransactions() == EXIT_FAILURE)
462         return EXIT_FAILURE;
463
464     if (!rev.needCommit) {
465         printf(" nothing to do\n");
466         return EXIT_SUCCESS;    // no changes?
467     }
468
469     if (rev.commit() == EXIT_FAILURE)
470         return EXIT_FAILURE;
471
472     printf(" done\n");
473     return EXIT_SUCCESS;
474 }
475
476 int SvnRevision::prepareTransactions()
477 {
478     // find out what was changed in this revision:
479     apr_hash_t *changes;
480     SVN_ERR(svn_fs_paths_changed(&changes, fs_root, pool));
481
482     QMap<QByteArray, svn_fs_path_change_t*> map;
483     for (apr_hash_index_t *i = apr_hash_first(pool, changes); i; i = apr_hash_next(i)) {
484         const void *vkey;
485         void *value;
486         apr_hash_this(i, &vkey, NULL, &value);
487         const char *key = reinterpret_cast<const char *>(vkey);
488         svn_fs_path_change_t *change = reinterpret_cast<svn_fs_path_change_t *>(value);
489         // If we mix path deletions with path adds/replaces we might erase a
490         // branch after that it has been reset -> history truncated
491         if (map.contains(QByteArray(key))) {
492             // If the same path is deleted and added, we need to put the
493             // deletions into the map first, then the addition.
494             if (change->change_kind == svn_fs_path_change_delete) {
495                 // XXX
496             }
497             fprintf(stderr, "\nDuplicate key found in rev %d: %s\n", revnum, key);
498             fprintf(stderr, "This needs more code to be handled, file a bug report\n");
499             fflush(stderr);
500             exit(1);
501         }
502         map.insertMulti(QByteArray(key), change);
503     }
504
505     QMapIterator<QByteArray, svn_fs_path_change_t*> i(map);
506     while (i.hasNext()) {
507         i.next();
508         if (exportEntry(i.key(), i.value(), changes) == EXIT_FAILURE)
509             return EXIT_FAILURE;
510     }
511
512     return EXIT_SUCCESS;
513 }
514
515 int SvnRevision::fetchRevProps()
516 {
517     if( propsFetched )
518         return EXIT_SUCCESS;
519
520     apr_hash_t *revprops;
521     SVN_ERR(svn_fs_revision_proplist(&revprops, fs, revnum, pool));
522     svn_string_t *svnauthor = (svn_string_t*)apr_hash_get(revprops, "svn:author", APR_HASH_KEY_STRING);
523     svn_string_t *svndate = (svn_string_t*)apr_hash_get(revprops, "svn:date", APR_HASH_KEY_STRING);
524     svn_string_t *svnlog = (svn_string_t*)apr_hash_get(revprops, "svn:log", APR_HASH_KEY_STRING);
525
526     log = svnlog ? svnlog->data : 0;
527     authorident = svnauthor ? identities.value(svnauthor->data) : QByteArray();
528     epoch = svndate ? get_epoch(svndate->data) : 0;
529     if (authorident.isEmpty()) {
530         if (!svnauthor || svn_string_isempty(svnauthor))
531             authorident = "nobody <nobody@localhost>";
532         else
533             authorident = svnauthor->data + QByteArray(" <") + svnauthor->data +
534                 QByteArray("@") + userdomain.toUtf8() + QByteArray(">");
535     }
536     propsFetched = true;
537     return EXIT_SUCCESS;
538 }
539
540 int SvnRevision::commit()
541 {
542     // now create the commit
543     if (fetchRevProps() != EXIT_SUCCESS)
544         return EXIT_FAILURE;
545     foreach (Repository *repo, repositories.values()) {
546         repo->commit();
547     }
548
549     foreach (Repository::Transaction *txn, transactions) {
550         txn->setAuthor(authorident);
551         txn->setDateTime(epoch);
552         txn->setLog(log);
553
554         txn->commit();
555         delete txn;
556     }
557
558     return EXIT_SUCCESS;
559 }
560
561 int SvnRevision::exportEntry(const char *key, const svn_fs_path_change_t *change,
562                              apr_hash_t *changes)
563 {
564     AprAutoPool revpool(pool.data());
565     QString current = QString::fromUtf8(key);
566
567     // was this copied from somewhere?
568     svn_revnum_t rev_from;
569     const char *path_from;
570     SVN_ERR(svn_fs_copied_from(&rev_from, &path_from, fs_root, key, revpool));
571
572     // is this a directory?
573     svn_boolean_t is_dir;
574     SVN_ERR(svn_fs_is_dir(&is_dir, fs_root, key, revpool));
575     if (is_dir) {
576         if (change->change_kind == svn_fs_path_change_modify ||
577             change->change_kind == svn_fs_path_change_add) {
578             if (path_from == NULL) {
579                 // freshly added directory, or modified properties
580                 // Git doesn't handle directories, so we don't either
581                 //qDebug() << "   mkdir ignored:" << key;
582                 return EXIT_SUCCESS;
583             }
584
585             qDebug() << "   " << key << "was copied from" << path_from << "rev" << rev_from;
586         } else if (change->change_kind == svn_fs_path_change_replace) {
587             if (path_from == NULL)
588                 qDebug() << "   " << key << "was replaced";
589             else
590                 qDebug() << "   " << key << "was replaced from" << path_from << "rev" << rev_from;
591         } else if (change->change_kind == svn_fs_path_change_reset) {
592             qCritical() << "   " << key << "was reset, panic!";
593             return EXIT_FAILURE;
594         } else {
595             // if change_kind == delete, it shouldn't come into this arm of the 'is_dir' test
596             qCritical() << "   " << key << "has unhandled change kind " << change->change_kind << ", panic!";
597             return EXIT_FAILURE;
598         }
599     } else if (change->change_kind == svn_fs_path_change_delete) {
600         is_dir = wasDir(fs, revnum - 1, key, revpool);
601     }
602
603     if (is_dir)
604         current += '/';
605
606     //MultiRule: loop start
607     //Replace all returns with continue,
608     bool isHandled = false;
609     foreach ( const MatchRuleList matchRules, allMatchRules ) {
610         // find the first rule that matches this pathname
611         MatchRuleList::ConstIterator match = findMatchRule(matchRules, revnum, current);
612         if (match != matchRules.constEnd()) {
613             const Rules::Match &rule = *match;
614             if ( exportDispatch(key, change, path_from, rev_from, changes, current, rule, matchRules, revpool) == EXIT_FAILURE )
615                 return EXIT_FAILURE;
616             isHandled = true;
617         } else if (is_dir && path_from != NULL) {
618             qDebug() << current << "is a copy-with-history, auto-recursing";
619             if ( recurse(key, change, path_from, matchRules, rev_from, changes, revpool) == EXIT_FAILURE )
620                 return EXIT_FAILURE;
621             isHandled = true;
622         } else if (is_dir && change->change_kind == svn_fs_path_change_delete) {
623             qDebug() << current << "deleted, auto-recursing";
624             if ( recurse(key, change, path_from, matchRules, rev_from, changes, revpool) == EXIT_FAILURE )
625                 return EXIT_FAILURE;
626             isHandled = true;
627         }
628     }
629     if ( isHandled ) {
630         return EXIT_SUCCESS;
631     }
632     if (wasDir(fs, revnum - 1, key, revpool)) {
633         qDebug() << current << "was a directory; ignoring";
634     } else if (change->change_kind == svn_fs_path_change_delete) {
635         qDebug() << current << "is being deleted but I don't know anything about it; ignoring";
636     } else {
637         qCritical() << current << "did not match any rules; cannot continue";
638         return EXIT_FAILURE;
639     }
640     return EXIT_SUCCESS;
641 }
642
643 int SvnRevision::exportDispatch(const char *key, const svn_fs_path_change_t *change,
644                                 const char *path_from, svn_revnum_t rev_from,
645                                 apr_hash_t *changes, const QString &current,
646                                 const Rules::Match &rule, const MatchRuleList &matchRules, apr_pool_t *pool)
647 {
648     //if(ruledebug)
649     //  qDebug() << "rev" << revnum << qPrintable(current) << "matched rule:" << rule.lineNumber << "(" << rule.rx.pattern() << ")";
650     switch (rule.action) {
651     case Rules::Match::Ignore:
652         //if(ruledebug)
653         //    qDebug() << "  " << "ignoring.";
654         return EXIT_SUCCESS;
655
656     case Rules::Match::Recurse:
657         if(ruledebug)
658             qDebug() << "rev" << revnum << qPrintable(current) << "matched rule:" << rule.info() << "  " << "recursing.";
659         return recurse(key, change, path_from, matchRules, rev_from, changes, pool);
660
661     case Rules::Match::Export:
662         if(ruledebug)
663             qDebug() << "rev" << revnum << qPrintable(current) << "matched rule:" << rule.info() << "  " << "exporting.";
664         if (exportInternal(key, change, path_from, rev_from, current, rule, matchRules) == EXIT_SUCCESS)
665             return EXIT_SUCCESS;
666         if (change->change_kind != svn_fs_path_change_delete) {
667             if(ruledebug)
668                 qDebug() << "rev" << revnum << qPrintable(current) << "matched rule:" << rule.info() << "  " << "Unable to export non path removal.";
669             return EXIT_FAILURE;
670         }
671         // we know that the default action inside recurse is to recurse further or to ignore,
672         // either of which is reasonably safe for deletion
673         qWarning() << "WARN: deleting unknown path" << current << "; auto-recursing";
674         return recurse(key, change, path_from, matchRules, rev_from, changes, pool);
675     }
676
677     // never reached
678     return EXIT_FAILURE;
679 }
680
681 int SvnRevision::exportInternal(const char *key, const svn_fs_path_change_t *change,
682                                 const char *path_from, svn_revnum_t rev_from,
683                                 const QString &current, const Rules::Match &rule, const MatchRuleList &matchRules)
684 {
685     needCommit = true;
686     QString svnprefix, repository, branch, path;
687     splitPathName(rule, current, &svnprefix, &repository, &branch, &path);
688
689     Repository *repo = repositories.value(repository, 0);
690     if (!repo) {
691         if (change->change_kind != svn_fs_path_change_delete)
692             qCritical() << "Rule" << rule
693                         << "references unknown repository" << repository;
694         return EXIT_FAILURE;
695     }
696
697     printf(".");
698     fflush(stdout);
699 //                qDebug() << "   " << qPrintable(current) << "rev" << revnum << "->"
700 //                         << qPrintable(repository) << qPrintable(branch) << qPrintable(path);
701
702     if (change->change_kind == svn_fs_path_change_delete && current == svnprefix && path.isEmpty()) {
703         if(ruledebug)
704             qDebug() << "repository" << repository << "branch" << branch << "deleted";
705         return repo->deleteBranch(branch, revnum);
706     }
707
708     QString previous;
709     QString prevsvnprefix, prevrepository, prevbranch, prevpath;
710
711     if (path_from != NULL) {
712         previous = QString::fromUtf8(path_from);
713         if (wasDir(fs, rev_from, path_from, pool.data())) {
714             previous += '/';
715         }
716         MatchRuleList::ConstIterator prevmatch =
717             findMatchRule(matchRules, rev_from, previous, NoIgnoreRule);
718         if (prevmatch != matchRules.constEnd()) {
719             splitPathName(*prevmatch, previous, &prevsvnprefix, &prevrepository,
720                           &prevbranch, &prevpath);
721
722         } else {
723             qWarning() << "WARN: SVN reports a \"copy from\" @" << revnum << "from" << path_from << "@" << rev_from << "but no matching rules found! Ignoring copy, treating as a modification";
724             path_from = NULL;
725         }
726     }
727
728     // current == svnprefix => we're dealing with the contents of the whole branch here
729     if (path_from != NULL && current == svnprefix && path.isEmpty()) {
730         if (previous != prevsvnprefix) {
731             // source is not the whole of its branch
732             qDebug() << qPrintable(current) << "is a partial branch of repository"
733                      << qPrintable(prevrepository) << "branch"
734                      << qPrintable(prevbranch) << "subdir"
735                      << qPrintable(prevpath);
736         } else if (prevrepository != repository) {
737             qWarning() << "WARN:" << qPrintable(current) << "rev" << revnum
738                        << "is a cross-repository copy (from repository"
739                        << qPrintable(prevrepository) << "branch"
740                        << qPrintable(prevbranch) << "path"
741                        << qPrintable(prevpath) << "rev" << rev_from << ")";
742         } else if (path != prevpath) {
743             qDebug() << qPrintable(current)
744                      << "is a branch copy which renames base directory of all contents"
745                      << qPrintable(prevpath) << "to" << qPrintable(path);
746             // FIXME: Handle with fast-import 'file rename' facility
747             //        ??? Might need special handling when path == / or prevpath == /
748         } else {
749             if (prevbranch == branch) {
750                 // same branch and same repository
751                 qDebug() << qPrintable(current) << "rev" << revnum
752                          << "is reseating branch" << qPrintable(branch)
753                          << "to an earlier revision"
754                          << qPrintable(previous) << "rev" << rev_from;
755             } else {
756                 // same repository but not same branch
757                 // this means this is a plain branch
758                 qDebug() << qPrintable(repository) << ": branch"
759                          << qPrintable(branch) << "is branching from"
760                          << qPrintable(prevbranch);
761             }
762
763             if (repo->createBranch(branch, revnum, prevbranch, rev_from) == EXIT_FAILURE)
764                 return EXIT_FAILURE;
765
766             if(CommandLineParser::instance()->contains("svn-branches")) {
767                 Repository::Transaction *txn = transactions.value(repository + branch, 0);
768                 if (!txn) {
769                     txn = repo->newTransaction(branch, svnprefix, revnum);
770                     if (!txn)
771                         return EXIT_FAILURE;
772
773                     transactions.insert(repository + branch, txn);
774                 }
775                 if(ruledebug)
776                     qDebug() << "Create a true SVN copy of branch (" << key << "->" << branch << path << ")";
777                 txn->deleteFile(path);
778                 recursiveDumpDir(txn, fs_root, key, path, pool);
779             }
780             if (rule.annotate) {
781                 // create an annotated tag
782                 fetchRevProps();
783                 repo->createAnnotatedTag(branch, svnprefix, revnum, authorident,
784                                          epoch, log);
785             }
786             return EXIT_SUCCESS;
787         }
788     }
789     Repository::Transaction *txn = transactions.value(repository + branch, 0);
790     if (!txn) {
791         txn = repo->newTransaction(branch, svnprefix, revnum);
792         if (!txn)
793             return EXIT_FAILURE;
794
795         transactions.insert(repository + branch, txn);
796     }
797
798     //
799     // If this path was copied from elsewhere, use it to infer _some_
800     // merge points.  This heuristic is fairly useful for tracking
801     // changes across directory re-organizations and wholesale branch
802     // imports.
803     //
804     if (path_from != NULL && prevrepository == repository && prevbranch != branch) {
805         if(ruledebug)
806             qDebug() << "copy from branch" << prevbranch << "to branch" << branch << "@rev" << rev_from;
807         txn->noteCopyFromBranch (prevbranch, rev_from);
808     }
809
810     if (change->change_kind == svn_fs_path_change_replace && path_from == NULL) {
811         if(ruledebug)
812             qDebug() << "replaced with empty path (" << branch << path << ")";
813         txn->deleteFile(path);
814     }
815     if (change->change_kind == svn_fs_path_change_delete) {
816         if(ruledebug)
817             qDebug() << "delete (" << branch << path << ")";
818         txn->deleteFile(path);
819     } else if (!current.endsWith('/')) {
820         if(ruledebug)
821             qDebug() << "add/change file (" << key << "->" << branch << path << ")";
822         dumpBlob(txn, fs_root, key, path, pool);
823     } else {
824         if(ruledebug)
825             qDebug() << "add/change dir (" << key << "->" << branch << path << ")";
826         txn->deleteFile(path);
827         recursiveDumpDir(txn, fs_root, key, path, pool);
828     }
829
830     return EXIT_SUCCESS;
831 }
832
833 int SvnRevision::recurse(const char *path, const svn_fs_path_change_t *change,
834                          const char *path_from, const MatchRuleList &matchRules, svn_revnum_t rev_from,
835                          apr_hash_t *changes, apr_pool_t *pool)
836 {
837     svn_fs_root_t *fs_root = this->fs_root;
838     if (change->change_kind == svn_fs_path_change_delete)
839         SVN_ERR(svn_fs_revision_root(&fs_root, fs, revnum - 1, pool));
840
841     // get the dir listing
842     svn_node_kind_t kind;
843     SVN_ERR(svn_fs_check_path(&kind, fs_root, path, pool));
844     if(kind == svn_node_none) {
845         qWarning() << "WARN: Trying to recurse using a nonexistant path" << path << ", ignoring";
846         return EXIT_SUCCESS;
847     } else if(kind != svn_node_dir) {
848         qWarning() << "WARN: Trying to recurse using a non-directory path" << path << ", ignoring";
849         return EXIT_SUCCESS;
850     }
851
852     apr_hash_t *entries;
853     SVN_ERR(svn_fs_dir_entries(&entries, fs_root, path, pool));
854     AprAutoPool dirpool(pool);
855
856     // While we get a hash, put it in a map for sorted lookup, so we can
857     // repeat the conversions and get the same git commit hashes.
858     QMap<QByteArray, svn_node_kind_t> map;
859     for (apr_hash_index_t *i = apr_hash_first(pool, entries); i; i = apr_hash_next(i)) {
860         dirpool.clear();
861         const void *vkey;
862         void *value;
863         apr_hash_this(i, &vkey, NULL, &value);
864         svn_fs_dirent_t *dirent = reinterpret_cast<svn_fs_dirent_t *>(value);
865         if (dirent->kind != svn_node_dir)
866             continue;           // not a directory, so can't recurse; skip
867         map.insertMulti(QByteArray(dirent->name), dirent->kind);
868     }
869
870     QMapIterator<QByteArray, svn_node_kind_t> i(map);
871     while (i.hasNext()) {
872         dirpool.clear();
873         i.next();
874         QByteArray entry = path + QByteArray("/") + i.key();
875         QByteArray entryFrom;
876         if (path_from)
877             entryFrom = path_from + QByteArray("/") + i.key();
878
879         // check if this entry is in the changelist for this revision already
880         svn_fs_path_change_t *otherchange =
881             (svn_fs_path_change_t*)apr_hash_get(changes, entry.constData(), APR_HASH_KEY_STRING);
882         if (otherchange && otherchange->change_kind == svn_fs_path_change_add) {
883             qDebug() << entry << "rev" << revnum
884                      << "is in the change-list, deferring to that one";
885             continue;
886         }
887
888         QString current = QString::fromUtf8(entry);
889         if (i.value() == svn_node_dir)
890             current += '/';
891
892         // find the first rule that matches this pathname
893         MatchRuleList::ConstIterator match = findMatchRule(matchRules, revnum, current);
894         if (match != matchRules.constEnd()) {
895             if (exportDispatch(entry, change, entryFrom.isNull() ? 0 : entryFrom.constData(),
896                                rev_from, changes, current, *match, matchRules, dirpool) == EXIT_FAILURE)
897                 return EXIT_FAILURE;
898         } else {
899             if (i.value() == svn_node_dir) {
900                 qDebug() << current << "rev" << revnum
901                          << "did not match any rules; auto-recursing";
902                 if (recurse(entry, change, entryFrom.isNull() ? 0 : entryFrom.constData(),
903                             matchRules, rev_from, changes, dirpool) == EXIT_FAILURE)
904                     return EXIT_FAILURE;
905             }
906         }
907     }
908
909     return EXIT_SUCCESS;
910 }