diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.cpp | 63 | ||||
-rw-r--r-- | src/repository.cpp | 621 | ||||
-rw-r--r-- | src/repository.h | 92 | ||||
-rw-r--r-- | src/ruleparser.cpp | 13 | ||||
-rw-r--r-- | src/ruleparser.h | 3 | ||||
-rw-r--r-- | src/svn.cpp | 162 |
6 files changed, 702 insertions, 252 deletions
diff --git a/src/main.cpp b/src/main.cpp index 0edf8f5..f2b189e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,7 +19,9 @@ #include <QFile> #include <QStringList> #include <QTextStream> +#include <QDebug> +#include <limits.h> #include <stdio.h> #include "CommandLineParser.h" @@ -115,18 +117,59 @@ int main(int argc, char **argv) Rules rules(args->optionArgument(QLatin1String("rules"))); rules.load(); - int min_rev = args->optionArgument(QLatin1String("resume-from")).toInt(); + int resume_from = args->optionArgument(QLatin1String("resume-from")).toInt(); int max_rev = args->optionArgument(QLatin1String("max-rev")).toInt(); - if (min_rev < 1) - min_rev = 1; // create the repository list QHash<QString, Repository *> repositories; + + int cutoff = resume_from ? resume_from : INT_MAX; + retry: + int min_rev = 1; foreach (Rules::Repository rule, rules.repositories()) { - Repository *repo = new Repository(rule); + Repository *repo = makeRepository(rule, repositories); + if (!repo) + return EXIT_FAILURE; repositories.insert(rule.name, repo); + + int repo_next = repo->setupIncremental(cutoff); + + /* + * cutoff < resume_from => error exit eventually + * repo_next == cutoff => probably truncated log + */ + if (cutoff < resume_from && repo_next == cutoff) + /* + * Restore the log file so we fail the next time + * svn2git is invoked with the same arguments + */ + repo->restoreLog(); + + if (cutoff < min_rev) + /* + * We've rewound before the last revision of some + * repository that we've already seen. Start over + * from the beginning. (since cutoff is decreasing, + * we're sure we'll make forward progress eventually) + */ + goto retry; + + if (min_rev < repo_next) + min_rev = repo_next; + } + + if (cutoff < resume_from) { + qCritical() << "Cannot resume from" << resume_from + << "as there are errors in revision" << cutoff; + return EXIT_FAILURE; } + if (min_rev < resume_from) + qDebug() << "skipping revisions" << min_rev << "to" << resume_from - 1 << "as requested"; + + if (resume_from) + min_rev = resume_from; + Svn::initialize(); Svn svn(args->arguments().first()); svn.setMatchRules(rules.matchRules()); @@ -135,15 +178,19 @@ int main(int argc, char **argv) if (max_rev < 1) max_rev = svn.youngestRevision(); - for (int i = min_rev; i <= max_rev; ++i) - if (!svn.exportRevision(i)) + + bool errors = false; + for (int i = min_rev; i <= max_rev; ++i) { + if (!svn.exportRevision(i)) { + errors = true; break; + } + } foreach (Repository *repo, repositories) { repo->finalizeTags(); delete repo; } - // success - return 0; + return errors ? EXIT_FAILURE : EXIT_SUCCESS; } diff --git a/src/repository.cpp b/src/repository.cpp index be73df6..5bcd8dd 100644 --- a/src/repository.cpp +++ b/src/repository.cpp @@ -25,10 +25,161 @@ static const int maxSimultaneousProcesses = 100; -class ProcessCache: QLinkedList<Repository *> +static const int maxMark = (1 << 20) - 1; // some versions of git-fast-import are buggy for larger values of maxMark + +class FastImportRepository : public Repository +{ +public: + class Transaction : public Repository::Transaction + { + Q_DISABLE_COPY(Transaction) + friend class FastImportRepository; + + FastImportRepository *repository; + QByteArray branch; + QByteArray svnprefix; + QByteArray author; + QByteArray log; + uint datetime; + int revnum; + + QVector<int> merges; + + QStringList deletedFiles; + QByteArray modifiedFiles; + + inline Transaction() {} + public: + ~Transaction(); + void commit(); + + void setAuthor(const QByteArray &author); + void setDateTime(uint dt); + void setLog(const QByteArray &log); + + void noteCopyFromBranch (const QString &prevbranch, int revFrom); + + void deleteFile(const QString &path); + QIODevice *addFile(const QString &path, int mode, qint64 length); + }; + FastImportRepository(const Rules::Repository &rule); + int setupIncremental(int &cutoff); + void restoreLog(); + ~FastImportRepository(); + + void reloadBranches(); + int createBranch(const QString &branch, int revnum, + const QString &branchFrom, int revFrom); + int deleteBranch(const QString &branch, int revnum); + Repository::Transaction *newTransaction(const QString &branch, const QString &svnprefix, int revnum); + + void createAnnotatedTag(const QString &name, const QString &svnprefix, int revnum, + const QByteArray &author, uint dt, + const QByteArray &log); + void finalizeTags(); + +private: + struct Branch + { + int created; + QVector<int> commits; + QVector<int> marks; + }; + struct AnnotatedTag + { + QString supportingRef; + QByteArray svnprefix; + QByteArray author; + QByteArray log; + uint dt; + int revnum; + }; + + QHash<QString, Branch> branches; + QHash<QString, AnnotatedTag> annotatedTags; + QString name; + QProcess fastImport; + int commitCount; + int outstandingTransactions; + + /* starts at 0, and counts up. */ + int last_commit_mark; + + /* starts at maxMark and counts down. Reset after each SVN revision */ + int next_file_mark; + + bool processHasStarted; + + void startFastImport(); + void closeFastImport(); + + // called when a transaction is deleted + void forgetTransaction(Transaction *t); + + int resetBranch(const QString &branch, int revnum, int mark, const QByteArray &resetTo, const QByteArray &comment); + int markFrom(const QString &branchFrom, int branchRevNum, QByteArray &desc); + + friend class ProcessCache; + Q_DISABLE_COPY(FastImportRepository) +}; + +class PrefixingRepository : public Repository +{ + Repository *repo; + QString prefix; +public: + class Transaction : public Repository::Transaction + { + Q_DISABLE_COPY(Transaction) + + Repository::Transaction *txn; + QString prefix; + public: + Transaction(Repository::Transaction *t, const QString &p) : txn(t), prefix(p) {} + ~Transaction() { delete txn; } + void commit() { txn->commit(); } + + void setAuthor(const QByteArray &author) { txn->setAuthor(author); } + void setDateTime(uint dt) { txn->setDateTime(dt); } + void setLog(const QByteArray &log) { txn->setLog(log); } + + void noteCopyFromBranch (const QString &prevbranch, int revFrom) + { txn->noteCopyFromBranch(prevbranch, revFrom); } + + void deleteFile(const QString &path) { txn->deleteFile(prefix + path); } + QIODevice *addFile(const QString &path, int mode, qint64 length) + { return txn->addFile(prefix + path, mode, length); } + }; + + PrefixingRepository(Repository *r, const QString &p) : repo(r), prefix(p) {} + + int setupIncremental(int &) { return 1; } + void restoreLog() {} + + int createBranch(const QString &branch, int revnum, + const QString &branchFrom, int revFrom) + { return repo->createBranch(branch, revnum, branchFrom, revFrom); } + + int deleteBranch(const QString &branch, int revnum) + { return repo->deleteBranch(branch, revnum); } + + Repository::Transaction *newTransaction(const QString &branch, const QString &svnprefix, int revnum) + { + Repository::Transaction *t = repo->newTransaction(branch, svnprefix, revnum); + return new Transaction(t, prefix); + } + + void createAnnotatedTag(const QString &name, const QString &svnprefix, int revnum, + const QByteArray &author, uint dt, + const QByteArray &log) + { repo->createAnnotatedTag(name, svnprefix, revnum, author, dt, log); } + void finalizeTags() { /* loop that called this will invoke it on 'repo' too */ } +}; + +class ProcessCache: QLinkedList<FastImportRepository *> { public: - void touch(Repository *repo) + void touch(FastImportRepository *repo) { remove(repo); @@ -40,7 +191,7 @@ public: append(repo); } - inline void remove(Repository *repo) + inline void remove(FastImportRepository *repo) { #if QT_VERSION >= 0x040400 removeOne(repo); @@ -51,8 +202,27 @@ public: }; static ProcessCache processCache; -Repository::Repository(const Rules::Repository &rule) - : name(rule.name), commitCount(0), outstandingTransactions(0), lastmark(0), processHasStarted(false) +Repository *makeRepository(const Rules::Repository &rule, const QHash<QString, Repository *> &repositories) +{ + if (rule.forwardTo.isEmpty()) + return new FastImportRepository(rule); + Repository *r = repositories[rule.forwardTo]; + if (!r) { + qCritical() << "no repository with name" << rule.forwardTo << "found at line" << rule.lineNumber; + return r; + } + return new PrefixingRepository(r, rule.prefix); +} + +static QString marksFileName(QString name) +{ + name.replace('/', '_'); + name.prepend("marks-"); + return name; +} + +FastImportRepository::FastImportRepository(const Rules::Repository &rule) + : name(rule.name), commitCount(0), outstandingTransactions(0), last_commit_mark(0), next_file_mark(maxMark), processHasStarted(false) { foreach (Rules::Repository::Branch branchRule, rule.branches) { Branch branch; @@ -73,17 +243,167 @@ Repository::Repository(const Rules::Repository &rule) init.setWorkingDirectory(name); init.start("git", QStringList() << "--bare" << "init"); init.waitForFinished(-1); + { + QFile marks(name + "/" + marksFileName(name)); + marks.open(QIODevice::WriteOnly); + marks.close(); + } } } } -Repository::~Repository() +static QString logFileName(QString name) +{ + name.replace('/', '_'); + name.prepend("log-"); + return name; +} + +static int lastValidMark(QString name) +{ + QFile marksfile(name + "/" + marksFileName(name)); + if (!marksfile.open(QIODevice::ReadOnly)) + return 0; + + int prev_mark = 0; + + int lineno = 0; + while (!marksfile.atEnd()) { + QString line = marksfile.readLine(); + ++lineno; + if (line.isEmpty()) + continue; + + int mark = 0; + if (line[0] == ':') { + int sp = line.indexOf(' '); + if (sp != -1) { + QString m = line.mid(1, sp-1); + mark = m.toInt(); + } + } + + if (!mark) { + qCritical() << marksfile.fileName() << "line" << lineno << "marks file corrupt?"; + return 0; + } + + if (mark == prev_mark) { + qCritical() << marksfile.fileName() << "line" << lineno << "marks file has duplicates"; + return 0; + } + + if (mark < prev_mark) { + qCritical() << marksfile.fileName() << "line" << lineno << "marks file not sorted"; + return 0; + } + + if (mark > prev_mark + 1) + break; + + prev_mark = mark; + } + + return prev_mark; +} + +int FastImportRepository::setupIncremental(int &cutoff) +{ + QFile logfile(logFileName(name)); + if (!logfile.exists()) + return 1; + + logfile.open(QIODevice::ReadWrite); + + QRegExp progress("progress SVN r(\\d+) branch (.*) = :(\\d+)"); + + int last_valid_mark = lastValidMark(name); + + int last_revnum = 0; + qint64 pos = 0; + int retval = 0; + QString bkup = logfile.fileName() + ".old"; + + while (!logfile.atEnd()) { + pos = logfile.pos(); + QByteArray line = logfile.readLine(); + int hash = line.indexOf('#'); + if (hash != -1) + line.truncate(hash); + line = line.trimmed(); + if (line.isEmpty()) + continue; + if (!progress.exactMatch(line)) + continue; + + int revnum = progress.cap(1).toInt(); + QString branch = progress.cap(2); + int mark = progress.cap(3).toInt(); + + if (revnum >= cutoff) + goto beyond_cutoff; + + if (revnum < last_revnum) + qWarning() << name << "revision numbers are not monotonic: " + << "got" << QString::number(last_revnum) + << "and then" << QString::number(revnum); + + if (mark > last_valid_mark) { + qWarning() << name << "unknown commit mark found: rewinding -- did you hit Ctrl-C?"; + cutoff = revnum; + goto beyond_cutoff; + } + + last_revnum = revnum; + + if (last_commit_mark < mark) + last_commit_mark = mark; + + Branch &br = branches[branch]; + if (!br.created || !mark || !br.marks.last()) + br.created = revnum; + br.commits.append(revnum); + br.marks.append(mark); + } + + retval = last_revnum + 1; + if (retval == cutoff) + /* + * If a stale backup file exists already, remove it, so that + * we don't confuse ourselves in 'restoreLog()' + */ + QFile::remove(bkup); + + return retval; + + beyond_cutoff: + // backup file, since we'll truncate + QFile::remove(bkup); + logfile.copy(bkup); + + // truncate, so that we ignore the rest of the revisions + qDebug() << name << "truncating history to revision" << cutoff; + logfile.resize(pos); + return cutoff; +} + +void FastImportRepository::restoreLog() +{ + QString file = logFileName(name); + QString bkup = file + ".old"; + if (!QFile::exists(bkup)) + return; + QFile::remove(file); + QFile::rename(bkup, file); +} + +FastImportRepository::~FastImportRepository() { Q_ASSERT(outstandingTransactions == 0); closeFastImport(); } -void Repository::closeFastImport() +void FastImportRepository::closeFastImport() { if (fastImport.state() != QProcess::NotRunning) { fastImport.write("checkpoint\n"); @@ -97,43 +417,52 @@ void Repository::closeFastImport() } processHasStarted = false; processCache.remove(this); - // Save the exported marks - QString revsFile = name; - revsFile.replace('/', '_'); - revsFile.prepend("revisions-"); - QFile exportedMarks(revsFile); - qDebug() << exportedMarks.open(QIODevice::Truncate | QIODevice::Text | QIODevice::WriteOnly); - - int mark; - foreach(mark, commitMarks.keys()) - { - exportedMarks.write(QString(":%2 r%1\n").arg(mark).arg(commitMarks.value(mark)).toLocal8Bit()); +} + +void FastImportRepository::reloadBranches() +{ + foreach (QString branch, branches.keys()) { + Branch &br = branches[branch]; + + if (!br.marks.count() || !br.marks.last()) + continue; + + QByteArray branchRef = branch.toUtf8(); + if (!branchRef.startsWith("refs/")) + branchRef.prepend("refs/heads/"); + + fastImport.write("reset " + branchRef + + "\nfrom :" + QByteArray::number(br.marks.last()) + "\n\n" + "progress Branch " + branchRef + " reloaded\n"); } - exportedMarks.close(); } -void Repository::reloadBranches() +int FastImportRepository::markFrom(const QString &branchFrom, int branchRevNum, QByteArray &branchFromDesc) { - QProcess revParse; - revParse.setWorkingDirectory(name); - revParse.start("git", QStringList() << "rev-parse" << "--symbolic" << "--branches"); - revParse.waitForFinished(-1); - - if (revParse.exitCode() == 0 && revParse.bytesAvailable()) { - while (revParse.canReadLine()) { - QByteArray branchName = revParse.readLine().trimmed(); - - //qDebug() << "Repo" << name << "reloaded branch" << branchName; - branches[branchName].created = 1; - fastImport.write("reset refs/heads/" + branchName + - "\nfrom refs/heads/" + branchName + "^0\n\n" - "progress Branch refs/heads/" + branchName + " reloaded\n"); - } + Branch &brFrom = branches[branchFrom]; + if (!brFrom.created) + return -1; + + if (branchRevNum == brFrom.commits.last()) + return brFrom.marks.last(); + + QVector<int>::const_iterator it = qUpperBound(brFrom.commits, branchRevNum); + if (it == brFrom.commits.begin()) + return 0; + + int closestCommit = *--it; + + if (!branchFromDesc.isEmpty()) { + branchFromDesc += " at r" + QByteArray::number(branchRevNum); + if (closestCommit != branchRevNum) + branchFromDesc += " => r" + QByteArray::number(closestCommit); } + + return brFrom.marks[it - brFrom.commits.begin()]; } -void Repository::createBranch(const QString &branch, int revnum, - const QString &branchFrom, int branchRevNum) +int FastImportRepository::createBranch(const QString &branch, int revnum, + const QString &branchFrom, int branchRevNum) { startFastImport(); if (!branches.contains(branch)) { @@ -141,64 +470,64 @@ void Repository::createBranch(const QString &branch, int revnum, << "Going to create it automatically"; } - QByteArray branchRef = branch.toUtf8(); - if (!branchRef.startsWith("refs/")) - branchRef.prepend("refs/heads/"); - - - Branch &br = branches[branch]; - if (br.created && br.created != revnum) { - QByteArray backupBranch = branchRef + '_' + QByteArray::number(revnum); - qWarning() << branch << "already exists; backing up to" << backupBranch; + QByteArray branchFromDesc = "from branch " + branchFrom.toUtf8(); + int mark = markFrom(branchFrom, branchRevNum, branchFromDesc); - fastImport.write("reset " + backupBranch + "\nfrom " + branchRef + "\n\n"); + if (mark == -1) { + qCritical() << branch << "in repository" << name + << "is branching from branch" << branchFrom + << "but the latter doesn't exist. Can't continue."; + return EXIT_FAILURE; } - // now create the branch - br.created = revnum; - QByteArray branchFromRef; - const int closestCommit = *qLowerBound(commitMarks.keys(), branchRevNum); - if(exportedCommits.contains(closestCommit)) - { - bool pathFound = false; - QString path; - foreach(path, exportedCommits[closestCommit]) { - if(path.contains(branchFrom)) { - pathFound = true; - break; - } - } - - if(pathFound) { - branchFromRef = ":" + QByteArray::number(commitMarks.value(closestCommit)); - qDebug() << branch << "in repository" << name << "is branching from" << closestCommit << "(svn reports r" << branchRevNum << ") git mark:" << branchFromRef; - } else { - qWarning() << branch << "in repository" << name << "is branching from a revision that doesn't touch the branch from path, branching from current revision"; - branchFromRef = branchFrom.toUtf8(); - if (!branchFromRef.startsWith("refs/")) - branchFromRef.prepend("refs/heads/"); - } - } else { + QByteArray branchFromRef = ":" + QByteArray::number(mark); + if (!mark) { qWarning() << branch << "in repository" << name << "is branching but no exported commits exist in repository" << "creating an empty branch."; branchFromRef = branchFrom.toUtf8(); if (!branchFromRef.startsWith("refs/")) branchFromRef.prepend("refs/heads/"); + branchFromDesc += ", deleted/unknown"; } - if (!branches.contains(branchFrom) || !branches.value(branchFrom).created) { - qCritical() << branch << "in repository" << name - << "is branching from branch" << branchFrom - << "but the latter doesn't exist. Can't continue."; - exit(1); + return resetBranch(branch, revnum, mark, branchFromRef, branchFromDesc); +} + +int FastImportRepository::deleteBranch(const QString &branch, int revnum) +{ + startFastImport(); + + static QByteArray null_sha(40, '0'); + return resetBranch(branch, revnum, 0, null_sha, "delete"); +} + +int FastImportRepository::resetBranch(const QString &branch, int revnum, int mark, const QByteArray &resetTo, const QByteArray &comment) +{ + QByteArray branchRef = branch.toUtf8(); + if (!branchRef.startsWith("refs/")) + branchRef.prepend("refs/heads/"); + + Branch &br = branches[branch]; + if (br.created && br.created != revnum && br.marks.last()) { + QByteArray backupBranch = "refs/backups/r" + QByteArray::number(revnum) + branchRef.mid(4); + qWarning() << "backing up branch" << branch << "to" << backupBranch; + + fastImport.write("reset " + backupBranch + "\nfrom " + branchRef + "\n\n"); } - fastImport.write("reset " + branchRef + "\nfrom " + branchFromRef + "\n\n" - "progress Branch " + branchRef + " created from " - + branchFromRef + " r" + QByteArray::number(branchRevNum) + "(at SVN" + QByteArray::number(revnum) + ")\n\n"); + br.created = revnum; + br.commits.append(revnum); + br.marks.append(mark); + + fastImport.write("reset " + branchRef + "\nfrom " + resetTo + "\n\n" + "progress SVN r" + QByteArray::number(revnum) + + " branch " + branch.toUtf8() + " = :" + QByteArray::number(mark) + + " # " + comment + "\n\n"); + + return EXIT_SUCCESS; } -Repository::Transaction *Repository::newTransaction(const QString &branch, const QString &svnprefix, +Repository::Transaction *FastImportRepository::newTransaction(const QString &branch, const QString &svnprefix, int revnum) { startFastImport(); @@ -221,7 +550,13 @@ Repository::Transaction *Repository::newTransaction(const QString &branch, const return txn; } -void Repository::createAnnotatedTag(const QString &ref, const QString &svnprefix, +void FastImportRepository::forgetTransaction(Transaction *) +{ + if (!--outstandingTransactions) + next_file_mark = maxMark; +} + +void FastImportRepository::createAnnotatedTag(const QString &ref, const QString &svnprefix, int revnum, const QByteArray &author, uint dt, const QByteArray &log) @@ -244,7 +579,7 @@ void Repository::createAnnotatedTag(const QString &ref, const QString &svnprefix tag.dt = dt; } -void Repository::finalizeTags() +void FastImportRepository::finalizeTags() { if (annotatedTags.isEmpty()) return; @@ -291,7 +626,7 @@ void Repository::finalizeTags() printf("\n"); } -void Repository::startFastImport() +void FastImportRepository::startFastImport() { if (fastImport.state() == QProcess::NotRunning) { if (processHasStarted) @@ -299,15 +634,13 @@ void Repository::startFastImport() processHasStarted = true; // start the process - QString marksFile = name; - marksFile.replace('/', '_'); - marksFile.prepend("marks-"); + QString marksFile = marksFileName(name); QStringList marksOptions; + marksOptions << "--import-marks=" + marksFile; marksOptions << "--export-marks=" + marksFile; - QString outputFile = name; - outputFile.replace('/', '_'); - outputFile.prepend("log-"); - fastImport.setStandardOutputFile(outputFile, QIODevice::Append); + marksOptions << "--force"; + + fastImport.setStandardOutputFile(logFileName(name), QIODevice::Append); fastImport.setProcessChannelMode(QProcess::MergedChannels); if (!CommandLineParser::instance()->contains("dry-run")) { @@ -320,38 +653,60 @@ void Repository::startFastImport() } } -Repository::Transaction::~Transaction() +FastImportRepository::Transaction::~Transaction() { - --repository->outstandingTransactions; + repository->forgetTransaction(this); } -void Repository::Transaction::setAuthor(const QByteArray &a) +void FastImportRepository::Transaction::setAuthor(const QByteArray &a) { author = a; } -void Repository::Transaction::setDateTime(uint dt) +void FastImportRepository::Transaction::setDateTime(uint dt) { datetime = dt; } -void Repository::Transaction::setLog(const QByteArray &l) +void FastImportRepository::Transaction::setLog(const QByteArray &l) { log = l; } -void Repository::Transaction::deleteFile(const QString &path) +void FastImportRepository::Transaction::noteCopyFromBranch(const QString &branchFrom, int branchRevNum) +{ + static QByteArray dummy; + int mark = repository->markFrom(branchFrom, branchRevNum, dummy); + Q_ASSERT(dummy.isEmpty()); + + if (mark == -1) { + qWarning() << branch << "is copying from branch" << branchFrom + << "but the latter doesn't exist. Continuing, assuming the files exist."; + } else if (mark == 0) { + qWarning() << "Unknown revision r" << QByteArray::number(branchRevNum) + << ". Continuing, assuming the files exist."; + } else { + qWarning() << "repository " + repository->name + " branch " + branch + " has some files copied from " + branchFrom + "@" + QByteArray::number(branchRevNum); + + if (!merges.contains(mark)) + merges.append(mark); + } +} + +void FastImportRepository::Transaction::deleteFile(const QString &path) { QString pathNoSlash = path; if(pathNoSlash.endsWith('/')) pathNoSlash.chop(1); deletedFiles.append(pathNoSlash); - modifiedPaths.append(path); } -QIODevice *Repository::Transaction::addFile(const QString &path, int mode, qint64 length) +QIODevice *FastImportRepository::Transaction::addFile(const QString &path, int mode, qint64 length) { - int mark = ++repository->lastmark; + int mark = repository->next_file_mark--; + + // in case the two mark allocations meet, we might as well just abort + Q_ASSERT(mark > repository->last_commit_mark + 1); if (modifiedFiles.capacity() == 0) modifiedFiles.reserve(2048); @@ -370,14 +725,22 @@ QIODevice *Repository::Transaction::addFile(const QString &path, int mode, qint6 repository->fastImport.write(QByteArray::number(length)); repository->fastImport.write("\n", 1); } - modifiedPaths.append(branch + "/" + path); + return &repository->fastImport; } -void Repository::Transaction::commit() +void FastImportRepository::Transaction::commit() { processCache.touch(repository); + // We might be tempted to use the SVN revision number as the fast-import commit mark. + // However, a single SVN revision can modify multple branches, and thus lead to multiple + // commits in the same repo. So, we need to maintain a separate commit mark counter. + int mark = ++repository->last_commit_mark; + + // in case the two mark allocations meet, we might as well just abort + Q_ASSERT(mark < repository->next_file_mark - 1); + // create the commit message QByteArray message = log; if (!message.endsWith('\n')) @@ -385,6 +748,18 @@ void Repository::Transaction::commit() if (CommandLineParser::instance()->contains("add-metadata")) message += "\nsvn path=" + svnprefix + "; revision=" + QByteArray::number(revnum) + "\n"; + int parentmark = 0; + Branch &br = repository->branches[branch]; + if (br.created) { + parentmark = br.marks.last(); + } else { + qWarning() << "Branch" << branch << "in repository" << repository->name << "doesn't exist at revision" + << revnum << "-- did you resume from the wrong revision?"; + br.created = revnum; + } + br.commits.append(revnum); + br.marks.append(mark); + { QByteArray branchRef = branch; if (!branchRef.startsWith("refs/")) @@ -392,24 +767,38 @@ void Repository::Transaction::commit() QTextStream s(&repository->fastImport); s << "commit " << branchRef << endl; - s << "mark :" << QByteArray::number(++repository->lastmark) << endl; - repository->commitMarks.insert(revnum, repository->lastmark); - repository->exportedCommits.insert(revnum, modifiedPaths); + s << "mark :" << QByteArray::number(mark) << endl; s << "committer " << QString::fromUtf8(author) << ' ' << datetime << " -0000" << endl; - Branch &br = repository->branches[branch]; - if (!br.created) { - qWarning() << "Branch" << branch << "in repository" << repository->name << "doesn't exist at revision" - << revnum << "-- did you resume from the wrong revision?"; - br.created = revnum; - } - s << "data " << message.length() << endl; } repository->fastImport.write(message); repository->fastImport.putChar('\n'); + // note some of the inferred merges + QByteArray desc = ""; + int i = !!parentmark; // if parentmark != 0, there's at least one parent + foreach (int merge, merges) { + if (merge == parentmark) + continue; + + if (++i > 16) { + // FIXME: options: + // (1) ignore the 16 parent limit + // (2) don't emit more than 16 parents + // (3) create another commit on branch to soak up additional parents + // we've chosen option (2) for now, since only artificial commits + // created by cvs2svn seem to have this issue + qWarning() << "too many merge parents"; + break; + } + + QByteArray m = " :" + QByteArray::number(merge); + desc += m; + repository->fastImport.write("merge" + m + "\n"); + } + // write the file deletions if (deletedFiles.contains("")) repository->fastImport.write("deleteall\n"); @@ -420,10 +809,10 @@ void Repository::Transaction::commit() // write the file modifications repository->fastImport.write(modifiedFiles); - repository->fastImport.write("\nprogress Commit #" + - QByteArray::number(repository->commitCount) + - " branch " + branch + - " = SVN r" + QByteArray::number(revnum) + "\n\n"); + repository->fastImport.write("\nprogress SVN r" + QByteArray::number(revnum) + + " branch " + branch + " = :" + QByteArray::number(mark) + + (desc.isEmpty() ? "" : " # merge from") + desc + + "\n\n"); printf(" %d modifications from SVN %s to %s/%s", deletedFiles.count() + modifiedFiles.count(), svnprefix.data(), qPrintable(repository->name), branch.data()); diff --git a/src/repository.h b/src/repository.h index d008996..e07d184 100644 --- a/src/repository.h +++ b/src/repository.h @@ -30,78 +30,36 @@ public: class Transaction { Q_DISABLE_COPY(Transaction) - friend class Repository; - - Repository *repository; - QByteArray branch; - QByteArray svnprefix; - QByteArray author; - QByteArray log; - uint datetime; - int revnum; - - QStringList deletedFiles; - QByteArray modifiedFiles; - QVector<QString> modifiedPaths; - - inline Transaction() {} + protected: + Transaction() {} public: - ~Transaction(); - void commit(); + virtual ~Transaction() {} + virtual void commit() = 0; - void setAuthor(const QByteArray &author); - void setDateTime(uint dt); - void setLog(const QByteArray &log); + virtual void setAuthor(const QByteArray &author) = 0; + virtual void setDateTime(uint dt) = 0; + virtual void setLog(const QByteArray &log) = 0; - void deleteFile(const QString &path); - QIODevice *addFile(const QString &path, int mode, qint64 length); - }; - Repository(const Rules::Repository &rule); - ~Repository(); + virtual void noteCopyFromBranch (const QString &prevbranch, int revFrom) = 0; - void reloadBranches(); - void createBranch(const QString &branch, int revnum, - const QString &branchFrom, int revFrom); - Transaction *newTransaction(const QString &branch, const QString &svnprefix, int revnum); - - void createAnnotatedTag(const QString &name, const QString &svnprefix, int revnum, - const QByteArray &author, uint dt, - const QByteArray &log); - void finalizeTags(); - -private: - struct Branch - { - int created; + virtual void deleteFile(const QString &path) = 0; + virtual QIODevice *addFile(const QString &path, int mode, qint64 length) = 0; }; - struct AnnotatedTag - { - QString supportingRef; - QByteArray svnprefix; - QByteArray author; - QByteArray log; - uint dt; - int revnum; - }; - - QHash<QString, Branch> branches; - QHash<QString, AnnotatedTag> annotatedTags; - // rXXXX, mark - QHash<int, int> commitMarks; - // rXXXX, [path, path, ...] - QHash<int, QVector<QString> > exportedCommits; - QString name; - QProcess fastImport; - int commitCount; - int outstandingTransactions; - int lastmark; - bool processHasStarted; - - void startFastImport(); - void closeFastImport(); - - friend class ProcessCache; - Q_DISABLE_COPY(Repository) + virtual int setupIncremental(int &cutoff) = 0; + virtual void restoreLog() = 0; + virtual ~Repository() {} + + virtual int createBranch(const QString &branch, int revnum, + const QString &branchFrom, int revFrom) = 0; + virtual int deleteBranch(const QString &branch, int revnum) = 0; + virtual Transaction *newTransaction(const QString &branch, const QString &svnprefix, int revnum) = 0; + + virtual void createAnnotatedTag(const QString &name, const QString &svnprefix, int revnum, + const QByteArray &author, uint dt, + const QByteArray &log) = 0; + virtual void finalizeTags() = 0; }; +Repository *makeRepository(const Rules::Repository &rule, const QHash<QString, Repository *> &repositories); + #endif diff --git a/src/ruleparser.cpp b/src/ruleparser.cpp index 9a83203..d6e634e 100644 --- a/src/ruleparser.cpp +++ b/src/ruleparser.cpp @@ -81,8 +81,19 @@ void Rules::load() repo.branches += branch; continue; - } else if (line == "end repository") { + } else if (matchRepoLine.exactMatch(line)) { + repo.forwardTo = matchRepoLine.cap(1); + continue; + } else if (matchPrefixLine.exactMatch(line)) { + repo.prefix = matchPrefixLine.cap(1); + continue; + } else if (line == "end repository") { m_repositories += repo; + { + // clear out 'repo' + Repository temp; + std::swap(repo, temp); + } state = ReadingNone; continue; } diff --git a/src/ruleparser.h b/src/ruleparser.h index 8d0fcb2..95a508f 100644 --- a/src/ruleparser.h +++ b/src/ruleparser.h @@ -36,6 +36,9 @@ public: QList<Branch> branches; int lineNumber; + QString forwardTo; + QString prefix; + Repository() : lineNumber(0) { } }; diff --git a/src/svn.cpp b/src/svn.cpp index 2a9ffcb..594a750 100644 --- a/src/svn.cpp +++ b/src/svn.cpp @@ -512,7 +512,6 @@ int SvnRevision::exportEntry(const char *key, const svn_fs_path_change_t *change svn_boolean_t is_dir; SVN_ERR(svn_fs_is_dir(&is_dir, fs_root, key, revpool)); if (is_dir) { - current += '/'; if (change->change_kind == svn_fs_path_change_modify || change->change_kind == svn_fs_path_change_add) { if (path_from == NULL) { @@ -523,8 +522,6 @@ int SvnRevision::exportEntry(const char *key, const svn_fs_path_change_t *change } qDebug() << " " << key << "was copied from" << path_from << "rev" << rev_from; - } else if (change->change_kind == svn_fs_path_change_delete) { - qDebug() << " " << key << "was deleted"; } else if (change->change_kind == svn_fs_path_change_replace) { if (path_from == NULL) qDebug() << " " << key << "was replaced"; @@ -534,11 +531,17 @@ int SvnRevision::exportEntry(const char *key, const svn_fs_path_change_t *change qCritical() << " " << key << "was reset, panic!"; return EXIT_FAILURE; } else { + // if change_kind == delete, it shouldn't come into this arm of the 'is_dir' test qCritical() << " " << key << "has unhandled change kind " << change->change_kind << ", panic!"; return EXIT_FAILURE; } + } else if (change->change_kind == svn_fs_path_change_delete) { + is_dir = wasDir(fs, revnum - 1, key, revpool); } + if (is_dir) + current += '/'; + // find the first rule that matches this pathname MatchRuleList::ConstIterator match = findMatchRule(matchRules, revnum, current); if (match != matchRules.constEnd()) { @@ -549,6 +552,9 @@ int SvnRevision::exportEntry(const char *key, const svn_fs_path_change_t *change if (is_dir && path_from != NULL) { qDebug() << current << "is a copy-with-history, auto-recursing"; return recurse(key, change, path_from, rev_from, changes, revpool); + } else if (is_dir && change->change_kind == svn_fs_path_change_delete) { + qDebug() << current << "deleted, auto-recursing"; + return recurse(key, change, path_from, rev_from, changes, revpool); } else if (wasDir(fs, revnum - 1, key, revpool)) { qDebug() << current << "was a directory; ignoring"; } else if (change->change_kind == svn_fs_path_change_delete) { @@ -579,7 +585,14 @@ int SvnRevision::exportDispatch(const char *key, const svn_fs_path_change_t *cha return recurse(key, change, path_from, rev_from, changes, pool); case Rules::Match::Export: - return exportInternal(key, change, path_from, rev_from, current, rule); + if (exportInternal(key, change, path_from, rev_from, current, rule) == EXIT_SUCCESS) + return EXIT_SUCCESS; + if (change->change_kind != svn_fs_path_change_delete) + return EXIT_FAILURE; + // we know that the default action inside recurse is to recurse further or to ignore, + // either of which is reasonably safe for deletion + qWarning() << "deleting unknown path" << current << "; auto-recursing"; + return recurse(key, change, path_from, rev_from, changes, pool); } // never reached @@ -593,72 +606,86 @@ int SvnRevision::exportInternal(const char *key, const svn_fs_path_change_t *cha QString svnprefix, repository, branch, path; splitPathName(rule, current, &svnprefix, &repository, &branch, &path); + Repository *repo = repositories.value(repository, 0); + if (!repo) { + if (change->change_kind != svn_fs_path_change_delete) + qCritical() << "Rule" << rule + << "references unknown repository" << repository; + return EXIT_FAILURE; + } + printf("."); fflush(stdout); // qDebug() << " " << qPrintable(current) << "rev" << revnum << "->" // << qPrintable(repository) << qPrintable(branch) << qPrintable(path); - if (path.isEmpty() && path_from != NULL) { - QString previous = QString::fromUtf8(path_from) + '/'; + if (change->change_kind == svn_fs_path_change_delete && current == svnprefix) { + qDebug() << "repository" << repository << "branch" << branch << "deleted"; + return repo->deleteBranch(branch, revnum); + } + + QString previous; + QString prevsvnprefix, prevrepository, prevbranch, prevpath; + + if (path_from != NULL) { + previous = QString::fromUtf8(path_from) + '/'; MatchRuleList::ConstIterator prevmatch = findMatchRule(matchRules, rev_from, previous, NoIgnoreRule); - if (prevmatch != matchRules.constEnd()) { - QString prevsvnprefix, prevrepository, prevbranch, prevpath; + if (prevmatch != matchRules.constEnd()) splitPathName(*prevmatch, previous, &prevsvnprefix, &prevrepository, &prevbranch, &prevpath); - - if (!prevpath.isEmpty()) { - qDebug() << qPrintable(current) << "is a partial branch of repository" - << qPrintable(prevrepository) << "branch" - << qPrintable(prevbranch) << "subdir" - << qPrintable(prevpath); - } else if (prevrepository != repository) { - qWarning() << qPrintable(current) << "rev" << revnum - << "is a cross-repository copy (from repository" - << qPrintable(prevrepository) << "branch" - << qPrintable(prevbranch) << "path" - << qPrintable(prevpath) << "rev" << rev_from << ")"; - } else if (prevbranch == branch) { - // same branch and same repository - qDebug() << qPrintable(current) << "rev" << revnum - << "is an SVN rename from" - << qPrintable(previous) << "rev" << rev_from; - return EXIT_SUCCESS; - } else { - // same repository but not same branch - // this means this is a plain branch - qDebug() << qPrintable(repository) << ": branch" - << qPrintable(branch) << "is branching from" - << qPrintable(prevbranch); - - Repository *repo = repositories.value(repository, 0); - if (!repo) { - qCritical() << "Rule" << rule - << "references unknown repository" << repository; - return EXIT_FAILURE; - } - - repo->createBranch(branch, revnum, prevbranch, rev_from); - if (rule.annotate) { - // create an annotated tag - fetchRevProps(); - repo->createAnnotatedTag(branch, svnprefix, revnum, authorident, - epoch, log); - } - return EXIT_SUCCESS; - } - } + else + path_from = NULL; } + // current == svnprefix => we're dealing with the contents of the whole branch here + if (path_from != NULL && current == svnprefix) { + if (previous != prevsvnprefix) { + // source is not the whole of its branch + qDebug() << qPrintable(current) << "is a partial branch of repository" + << qPrintable(prevrepository) << "branch" + << qPrintable(prevbranch) << "subdir" + << qPrintable(prevpath); + } else if (prevrepository != repository) { + qWarning() << qPrintable(current) << "rev" << revnum + << "is a cross-repository copy (from repository" + << qPrintable(prevrepository) << "branch" + << qPrintable(prevbranch) << "path" + << qPrintable(prevpath) << "rev" << rev_from << ")"; + } else if (path != prevpath) { + qDebug() << qPrintable(current) + << "is a branch copy which renames base directory of all contents" + << qPrintable(prevpath) << "to" << qPrintable(path); + // FIXME: Handle with fast-import 'file rename' facility + // ??? Might need special handling when path == / or prevpath == / + } else { + if (prevbranch == branch) { + // same branch and same repository + qDebug() << qPrintable(current) << "rev" << revnum + << "is reseating branch" << qPrintable(branch) + << "to an earlier revision" + << qPrintable(previous) << "rev" << rev_from; + } else { + // same repository but not same branch + // this means this is a plain branch + qDebug() << qPrintable(repository) << ": branch" + << qPrintable(branch) << "is branching from" + << qPrintable(prevbranch); + } + + if (repo->createBranch(branch, revnum, prevbranch, rev_from) == EXIT_FAILURE) + return EXIT_FAILURE; + if (rule.annotate) { + // create an annotated tag + fetchRevProps(); + repo->createAnnotatedTag(branch, svnprefix, revnum, authorident, + epoch, log); + } + return EXIT_SUCCESS; + } + } Repository::Transaction *txn = transactions.value(repository + branch, 0); if (!txn) { - Repository *repo = repositories.value(repository, 0); - if (!repo) { - qCritical() << "Rule" << rule - << "references unknown repository" << repository; - return EXIT_FAILURE; - } - txn = repo->newTransaction(branch, svnprefix, revnum); if (!txn) return EXIT_FAILURE; @@ -666,6 +693,15 @@ int SvnRevision::exportInternal(const char *key, const svn_fs_path_change_t *cha transactions.insert(repository + branch, txn); } + // + // If this path was copied from elsewhere, use it to infer _some_ + // merge points. This heuristic is fairly useful for tracking + // changes across directory re-organizations and wholesale branch + // imports. + // + if (path_from != NULL && prevrepository == repository) + txn->noteCopyFromBranch (prevbranch, rev_from); + if (change->change_kind == svn_fs_path_change_replace && path_from == NULL) txn->deleteFile(path); if (change->change_kind == svn_fs_path_change_delete) { @@ -684,6 +720,10 @@ int SvnRevision::recurse(const char *path, const svn_fs_path_change_t *change, const char *path_from, svn_revnum_t rev_from, apr_hash_t *changes, apr_pool_t *pool) { + svn_fs_root_t *fs_root = this->fs_root; + if (change->change_kind == svn_fs_path_change_delete) + SVN_ERR(svn_fs_revision_root(&fs_root, fs, revnum - 1, pool)); + // get the dir listing apr_hash_t *entries; SVN_ERR(svn_fs_dir_entries(&entries, fs_root, path, pool)); @@ -724,9 +764,11 @@ int SvnRevision::recurse(const char *path, const svn_fs_path_change_t *change, rev_from, changes, current, *match, dirpool) == EXIT_FAILURE) return EXIT_FAILURE; } else { - qCritical() << current << "rev" << revnum - << "did not match any rules; cannot continue"; - return EXIT_FAILURE; + qDebug() << current << "rev" << revnum + << "did not match any rules; auto-recursing"; + if (recurse(entry, change, entryFrom.isNull() ? 0 : entryFrom.constData(), + rev_from, changes, dirpool) == EXIT_FAILURE) + return EXIT_FAILURE; } } |