Merge commit 'refs/merge-requests/14' of gitorious.org:svn2git/svn2git into merge...
[svn2git:uqs-svn2git.git] / src / repository.cpp
1 /*
2  *  Copyright (C) 2007  Thiago Macieira <thiago@kde.org>
3  *  Copyright (C) 2009 Thomas Zander <zander@kde.org>
4  *
5  *  This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 3 of the License, or
8  *  (at your option) any later version.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 #include "repository.h"
20 #include "CommandLineParser.h"
21 #include <QTextStream>
22 #include <QDebug>
23 #include <QDir>
24 #include <QFile>
25 #include <QLinkedList>
26
27 static const int maxSimultaneousProcesses = 100;
28
29 static const int maxMark = (1 << 20) - 1; // some versions of git-fast-import are buggy for larger values of maxMark
30
31 class ProcessCache: QLinkedList<Repository *>
32 {
33 public:
34     void touch(Repository *repo)
35     {
36         remove(repo);
37
38         // if the cache is too big, remove from the front
39         while (size() >= maxSimultaneousProcesses)
40             takeFirst()->closeFastImport();
41
42         // append to the end
43         append(repo);
44     }
45
46     inline void remove(Repository *repo)
47     {
48 #if QT_VERSION >= 0x040400
49         removeOne(repo);
50 #else
51         removeAll(repo);
52 #endif
53     }
54 };
55 static ProcessCache processCache;
56
57 static QString marksFileName(QString name)
58 {
59     name.replace('/', '_');
60     name.prepend("marks-");
61     return name;
62 }
63
64 Repository::Repository(const Rules::Repository &rule)
65     : name(rule.name), prefix(rule.forwardTo), fastImport(name), commitCount(0), outstandingTransactions(0),
66       last_commit_mark(0), next_file_mark(maxMark), processHasStarted(false)
67 {
68     foreach (Rules::Repository::Branch branchRule, rule.branches) {
69         Branch branch;
70         branch.created = 0;     // not created
71
72         branches.insert(branchRule.name, branch);
73     }
74
75     // create the default branch
76     branches["master"].created = 1;
77
78     fastImport.setWorkingDirectory(name);
79     if (!CommandLineParser::instance()->contains("dry-run")) {
80         if (!QDir(name).exists()) { // repo doesn't exist yet.
81             qDebug() << "Creating new repository" << name;
82             QDir::current().mkpath(name);
83             QProcess init;
84             init.setWorkingDirectory(name);
85             init.start("git", QStringList() << "--bare" << "init");
86             init.waitForFinished(-1);
87             // Write description
88             if (!rule.description.isEmpty()) {
89                 QFile fDesc(QDir(name).filePath("description"));
90                 if (fDesc.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
91                     fDesc.write(rule.description.toUtf8());
92                     fDesc.putChar('\n');
93                     fDesc.close();
94                 }
95             }
96             {
97                 QFile marks(name + "/" + marksFileName(name));
98                 marks.open(QIODevice::WriteOnly);
99                 marks.close();
100             }
101         }
102     }
103 }
104
105 static QString logFileName(QString name)
106 {
107     name.replace('/', '_');
108     name.prepend("log-");
109     return name;
110 }
111
112 static int lastValidMark(QString name)
113 {
114     QFile marksfile(name + "/" + marksFileName(name));
115     if (!marksfile.open(QIODevice::ReadOnly))
116         return 0;
117
118     int prev_mark = 0;
119
120     int lineno = 0;
121     while (!marksfile.atEnd()) {
122         QString line = marksfile.readLine();
123         ++lineno;
124         if (line.isEmpty())
125             continue;
126
127         int mark = 0;
128         if (line[0] == ':') {
129             int sp = line.indexOf(' ');
130             if (sp != -1) {
131                 QString m = line.mid(1, sp-1);
132                 mark = m.toInt();
133             }
134         }
135
136         if (!mark) {
137             qCritical() << marksfile.fileName() << "line" << lineno << "marks file corrupt?";
138             return 0;
139         }
140
141         if (mark == prev_mark) {
142             qCritical() << marksfile.fileName() << "line" << lineno << "marks file has duplicates";
143             return 0;
144         }
145
146         if (mark < prev_mark) {
147             qCritical() << marksfile.fileName() << "line" << lineno << "marks file not sorted";
148             return 0;
149         }
150
151         if (mark > prev_mark + 1)
152             break;
153
154         prev_mark = mark;
155     }
156
157     return prev_mark;
158 }
159
160 int Repository::setupIncremental(int &cutoff)
161 {
162     QFile logfile(logFileName(name));
163     if (!logfile.exists())
164         return 1;
165
166     logfile.open(QIODevice::ReadWrite);
167
168     QRegExp progress("progress SVN r(\\d+) branch (.*) = :(\\d+)");
169
170     int last_valid_mark = lastValidMark(name);
171
172     int last_revnum = 0;
173     qint64 pos = 0;
174     int retval = 0;
175     QString bkup = logfile.fileName() + ".old";
176
177     while (!logfile.atEnd()) {
178         pos = logfile.pos();
179         QByteArray line = logfile.readLine();
180         int hash = line.indexOf('#');
181         if (hash != -1)
182             line.truncate(hash);
183         line = line.trimmed();
184         if (line.isEmpty())
185             continue;
186         if (!progress.exactMatch(line))
187             continue;
188
189         int revnum = progress.cap(1).toInt();
190         QString branch = progress.cap(2);
191         int mark = progress.cap(3).toInt();
192
193         if (revnum >= cutoff)
194             goto beyond_cutoff;
195
196         if (revnum < last_revnum)
197             qWarning() << "WARN:" << name << "revision numbers are not monotonic: "
198                        << "got" << QString::number(last_revnum)
199                        << "and then" << QString::number(revnum);
200
201         if (mark > last_valid_mark) {
202             qWarning() << "WARN:" << name << "unknown commit mark found: rewinding -- did you hit Ctrl-C?";
203             cutoff = revnum;
204             goto beyond_cutoff;
205         }
206
207         last_revnum = revnum;
208
209         if (last_commit_mark < mark)
210             last_commit_mark = mark;
211
212         Branch &br = branches[branch];
213         if (!br.created || !mark || br.marks.isEmpty() || !br.marks.last())
214             br.created = revnum;
215         br.commits.append(revnum);
216         br.marks.append(mark);
217     }
218
219     retval = last_revnum + 1;
220     if (retval == cutoff)
221         /*
222          * If a stale backup file exists already, remove it, so that
223          * we don't confuse ourselves in 'restoreLog()'
224          */
225         QFile::remove(bkup);
226
227     return retval;
228
229   beyond_cutoff:
230     // backup file, since we'll truncate
231     QFile::remove(bkup);
232     logfile.copy(bkup);
233
234     // truncate, so that we ignore the rest of the revisions
235     qDebug() << name << "truncating history to revision" << cutoff;
236     logfile.resize(pos);
237     return cutoff;
238 }
239
240 void Repository::restoreLog()
241 {
242     QString file = logFileName(name);
243     QString bkup = file + ".old";
244     if (!QFile::exists(bkup))
245         return;
246     QFile::remove(file);
247     QFile::rename(bkup, file);
248 }
249
250 Repository::~Repository()
251 {
252     Q_ASSERT(outstandingTransactions == 0);
253     closeFastImport();
254 }
255
256 void Repository::closeFastImport()
257 {
258     if (fastImport.state() != QProcess::NotRunning) {
259         fastImport.write("checkpoint\n");
260         fastImport.waitForBytesWritten(-1);
261         fastImport.closeWriteChannel();
262         if (!fastImport.waitForFinished()) {
263             fastImport.terminate();
264             if (!fastImport.waitForFinished(200))
265                 qWarning() << "WARN: git-fast-import for repository" << name << "did not die";
266         }
267     }
268     processHasStarted = false;
269     processCache.remove(this);
270 }
271
272 void Repository::reloadBranches()
273 {
274     foreach (QString branch, branches.keys()) {
275         Branch &br = branches[branch];
276
277         if (br.marks.isEmpty() || !br.marks.last())
278             continue;
279
280         QByteArray branchRef = branch.toUtf8();
281         if (!branchRef.startsWith("refs/"))
282             branchRef.prepend("refs/heads/");
283
284         fastImport.write("reset " + branchRef +
285                         "\nfrom :" + QByteArray::number(br.marks.last()) + "\n\n"
286                         "progress Branch " + branchRef + " reloaded\n");
287     }
288 }
289
290 int Repository::markFrom(const QString &branchFrom, int branchRevNum, QByteArray &branchFromDesc)
291 {
292     Branch &brFrom = branches[branchFrom];
293     if (!brFrom.created)
294         return -1;
295
296     if (brFrom.commits.isEmpty()) {
297         return -1;
298     }
299     if (branchRevNum == brFrom.commits.last()) {
300         return brFrom.marks.last();
301     }
302
303     QVector<int>::const_iterator it = qUpperBound(brFrom.commits, branchRevNum);
304     if (it == brFrom.commits.begin()) {
305         return 0;
306     }
307
308     int closestCommit = *--it;
309
310     if (!branchFromDesc.isEmpty()) {
311         branchFromDesc += " at r" + QByteArray::number(branchRevNum);
312         if (closestCommit != branchRevNum) {
313             branchFromDesc += " => r" + QByteArray::number(closestCommit);
314         }
315     }
316
317     return brFrom.marks[it - brFrom.commits.begin()];
318 }
319
320 int Repository::createBranch(const QString &branch, int revnum,
321                                      const QString &branchFrom, int branchRevNum)
322 {
323     QByteArray branchFromDesc = "from branch " + branchFrom.toUtf8();
324     int mark = markFrom(branchFrom, branchRevNum, branchFromDesc);
325
326     if (mark == -1) {
327         qCritical() << branch << "in repository" << name
328                     << "is branching from branch" << branchFrom
329                     << "but the latter doesn't exist. Can't continue.";
330         return EXIT_FAILURE;
331     }
332
333     QByteArray branchFromRef = ":" + QByteArray::number(mark);
334     if (!mark) {
335         qWarning() << "WARN:" << branch << "in repository" << name << "is branching but no exported commits exist in repository"
336                 << "creating an empty branch.";
337         branchFromRef = branchFrom.toUtf8();
338         if (!branchFromRef.startsWith("refs/"))
339             branchFromRef.prepend("refs/heads/");
340         branchFromDesc += ", deleted/unknown";
341     }
342
343     qDebug() << "Creating branch:" << branch << "from" << branchFrom << "(" << branchRevNum << branchFromDesc << ")";
344
345     // Preserve note
346     branches[branch].note = branches.value(branchFrom).note;
347
348     return resetBranch(branch, revnum, mark, branchFromRef, branchFromDesc);
349 }
350
351 int Repository::deleteBranch(const QString &branch, int revnum)
352 {
353     static QByteArray null_sha(40, '0');
354     return resetBranch(branch, revnum, 0, null_sha, "delete");
355 }
356
357 int Repository::resetBranch(const QString &branch, int revnum, int mark, const QByteArray &resetTo, const QByteArray &comment)
358 {
359     QByteArray branchRef = branch.toUtf8();
360     if (!branchRef.startsWith("refs/"))
361         branchRef.prepend("refs/heads/");
362
363     Branch &br = branches[branch];
364     QByteArray backupCmd;
365     if (br.created && br.created != revnum && !br.marks.isEmpty() && br.marks.last()) {
366         QByteArray backupBranch;
367         if ((comment == "delete") && branchRef.startsWith("refs/heads/"))
368             backupBranch = "refs/tags/backups/" + branchRef.mid(11) + "@" + QByteArray::number(revnum);
369         else
370             backupBranch = "refs/backups/r" + QByteArray::number(revnum) + branchRef.mid(4);
371         qWarning() << "WARN: backing up branch" << branch << "to" << backupBranch;
372
373         backupCmd = "reset " + backupBranch + "\nfrom " + branchRef + "\n\n";
374     }
375
376     br.created = revnum;
377     br.commits.append(revnum);
378     br.marks.append(mark);
379
380     QByteArray cmd = "reset " + branchRef + "\nfrom " + resetTo + "\n\n"
381                      "progress SVN r" + QByteArray::number(revnum)
382                      + " branch " + branch.toUtf8() + " = :" + QByteArray::number(mark)
383                      + " # " + comment + "\n\n";
384     if(comment == "delete")
385         deletedBranches.append(backupCmd).append(cmd);
386     else
387         resetBranches.append(backupCmd).append(cmd);
388
389     return EXIT_SUCCESS;
390 }
391
392 void Repository::commit()
393 {
394     if (deletedBranches.isEmpty() && resetBranches.isEmpty()) {
395         return;
396     }
397     startFastImport();
398     fastImport.write(deletedBranches);
399     fastImport.write(resetBranches);
400     deletedBranches.clear();
401     resetBranches.clear();
402 }
403
404 Repository::Transaction *Repository::newTransaction(const QString &branch, const QString &svnprefix,
405                                                     int revnum)
406 {
407     if (!branches.contains(branch)) {
408         qWarning() << "WARN: Transaction:" << branch << "is not a known branch in repository" << name << endl
409                    << "Going to create it automatically";
410     }
411
412     Transaction *txn = new Transaction;
413     txn->repository = this;
414     txn->branch = branch.toUtf8();
415     txn->svnprefix = svnprefix.toUtf8();
416     txn->datetime = 0;
417     txn->revnum = revnum;
418
419     if ((++commitCount % CommandLineParser::instance()->optionArgument(QLatin1String("commit-interval"), QLatin1String("10000")).toInt()) == 0) {
420         startFastImport();
421         // write everything to disk every 10000 commits
422         fastImport.write("checkpoint\n");
423         qDebug() << "checkpoint!, marks file trunkated";
424     }
425     outstandingTransactions++;
426     return txn;
427 }
428
429 void Repository::forgetTransaction(Transaction *)
430 {
431     if (!--outstandingTransactions)
432         next_file_mark = maxMark;
433 }
434
435 void Repository::createAnnotatedTag(const QString &ref, const QString &svnprefix,
436                                     int revnum,
437                                     const QByteArray &author, uint dt,
438                                     const QByteArray &log)
439 {
440     QString tagName = ref;
441     if (tagName.startsWith("refs/tags/"))
442         tagName.remove(0, 10);
443
444     if (!annotatedTags.contains(tagName))
445         printf("Creating annotated tag %s (%s)\n", qPrintable(tagName), qPrintable(ref));
446     else
447         printf("Re-creating annotated tag %s\n", qPrintable(tagName));
448
449     AnnotatedTag &tag = annotatedTags[tagName];
450     tag.supportingRef = ref;
451     tag.svnprefix = svnprefix.toUtf8();
452     tag.revnum = revnum;
453     tag.author = author;
454     tag.log = log;
455     tag.dt = dt;
456 }
457
458 void Repository::finalizeTags()
459 {
460     if (annotatedTags.isEmpty())
461         return;
462
463     printf("Finalising tags for %s...", qPrintable(name));
464     startFastImport();
465
466     QHash<QString, AnnotatedTag>::ConstIterator it = annotatedTags.constBegin();
467     for ( ; it != annotatedTags.constEnd(); ++it) {
468         const QString &tagName = it.key();
469         const AnnotatedTag &tag = it.value();
470
471         QByteArray message = tag.log;
472         if (!message.endsWith('\n'))
473             message += '\n';
474         if (CommandLineParser::instance()->contains("add-metadata"))
475             message += "\n" + formatMetadataMessage(tag.svnprefix, tag.revnum, tagName.toUtf8());
476
477         {
478             QByteArray branchRef = tag.supportingRef.toUtf8();
479             if (!branchRef.startsWith("refs/"))
480                 branchRef.prepend("refs/heads/");
481
482             QByteArray s = "progress Creating annotated tag " + tagName.toUtf8() + " from ref " + branchRef + "\n"
483               + "tag " + tagName.toUtf8() + "\n"
484               + "from " + branchRef + "\n"
485               + "tagger " + tag.author + ' ' + QByteArray::number(tag.dt) + " +0000" + "\n"
486               + "data " + QByteArray::number( message.length() ) + "\n";
487             fastImport.write(s);
488         }
489
490         fastImport.write(message);
491         fastImport.putChar('\n');
492         if (!fastImport.waitForBytesWritten(-1))
493             qFatal("Failed to write to process: %s", qPrintable(fastImport.errorString()));
494
495         // Append note to the tip commit of the supporting ref. There is no
496         // easy way to attach a note to the tag itself with fast-import.
497         if (CommandLineParser::instance()->contains("add-metadata-notes")) {
498             Repository::Transaction *txn = newTransaction(tag.supportingRef, tag.svnprefix, tag.revnum);
499             txn->setAuthor(tag.author);
500             txn->setDateTime(tag.dt);
501             txn->commitNote(formatMetadataMessage(tag.svnprefix, tag.revnum, tagName.toUtf8()), true);
502             delete txn;
503
504             if (!fastImport.waitForBytesWritten(-1))
505                 qFatal("Failed to write to process: %s", qPrintable(fastImport.errorString()));
506         }
507
508         printf(" %s", qPrintable(tagName));
509         fflush(stdout);
510     }
511
512     while (fastImport.bytesToWrite())
513         if (!fastImport.waitForBytesWritten(-1))
514             qFatal("Failed to write to process: %s", qPrintable(fastImport.errorString()));
515     printf("\n");
516 }
517
518 void Repository::startFastImport()
519 {
520     processCache.touch(this);
521
522     if (fastImport.state() == QProcess::NotRunning) {
523         if (processHasStarted)
524             qFatal("git-fast-import has been started once and crashed?");
525         processHasStarted = true;
526
527         // start the process
528         QString marksFile = marksFileName(name);
529         QStringList marksOptions;
530         marksOptions << "--import-marks=" + marksFile;
531         marksOptions << "--export-marks=" + marksFile;
532         marksOptions << "--force";
533
534         fastImport.setStandardOutputFile(logFileName(name), QIODevice::Append);
535         fastImport.setProcessChannelMode(QProcess::MergedChannels);
536
537         if (!CommandLineParser::instance()->contains("dry-run")) {
538             fastImport.start("git", QStringList() << "fast-import" << marksOptions);
539         } else {
540             fastImport.start("/bin/cat", QStringList());
541         }
542         fastImport.waitForStarted(-1);
543
544         reloadBranches();
545     }
546 }
547
548 QByteArray Repository::formatMetadataMessage(const QByteArray &svnprefix, int revnum, const QByteArray &tag)
549 {
550     QByteArray msg = "svn path=" + svnprefix + "; revision=" + QByteArray::number(revnum);
551     if (!tag.isEmpty())
552         msg += "; tag=" + tag;
553     msg += "\n";
554     return msg;
555 }
556
557 bool Repository::branchExists(const QString& branch) const\
558 {
559     return branches.contains(branch);
560 }
561
562 const QByteArray Repository::branchNote(const QString& branch) const
563 {
564     return branches.value(branch).note;
565 }
566
567 void Repository::setBranchNote(const QString& branch, const QByteArray& noteText)
568 {
569     if (branches.contains(branch))
570         branches[branch].note = noteText;
571 }
572
573 Repository::Transaction::~Transaction()
574 {
575     repository->forgetTransaction(this);
576 }
577
578 void Repository::Transaction::setAuthor(const QByteArray &a)
579 {
580     author = a;
581 }
582
583 void Repository::Transaction::setDateTime(uint dt)
584 {
585     datetime = dt;
586 }
587
588 void Repository::Transaction::setLog(const QByteArray &l)
589 {
590     log = l;
591 }
592
593 void Repository::Transaction::noteCopyFromBranch(const QString &branchFrom, int branchRevNum)
594 {
595     if(branch == branchFrom) {
596         qWarning() << "WARN: Cannot merge inside a branch";
597         return;
598     }
599     static QByteArray dummy;
600     int mark = repository->markFrom(branchFrom, branchRevNum, dummy);
601     Q_ASSERT(dummy.isEmpty());
602
603     if (mark == -1) {
604         qWarning() << "WARN:" << branch << "is copying from branch" << branchFrom
605                     << "but the latter doesn't exist.  Continuing, assuming the files exist.";
606     } else if (mark == 0) {
607     qWarning() << "WARN: Unknown revision r" << QByteArray::number(branchRevNum)
608                << ".  Continuing, assuming the files exist.";
609     } else {
610         qWarning() << "WARN: repository " + repository->name + " branch " + branch + " has some files copied from " + branchFrom + "@" + QByteArray::number(branchRevNum);
611
612         if (!merges.contains(mark)) {
613             merges.append(mark);
614             qDebug() << "adding" << branchFrom + "@" + QByteArray::number(branchRevNum) << ":" << mark << "as a merge point";
615         } else {
616             qDebug() << "merge point already recorded";
617         }
618     }
619 }
620
621 void Repository::Transaction::deleteFile(const QString &path)
622 {
623     QString pathNoSlash = repository->prefix + path;
624     if(pathNoSlash.endsWith('/'))
625         pathNoSlash.chop(1);
626     deletedFiles.append(pathNoSlash);
627 }
628
629 QIODevice *Repository::Transaction::addFile(const QString &path, int mode, qint64 length)
630 {
631     int mark = repository->next_file_mark--;
632
633     // in case the two mark allocations meet, we might as well just abort
634     Q_ASSERT(mark > repository->last_commit_mark + 1);
635
636     if (modifiedFiles.capacity() == 0)
637         modifiedFiles.reserve(2048);
638     modifiedFiles.append("M ");
639     modifiedFiles.append(QByteArray::number(mode, 8));
640     modifiedFiles.append(" :");
641     modifiedFiles.append(QByteArray::number(mark));
642     modifiedFiles.append(' ');
643     modifiedFiles.append(repository->prefix + path.toUtf8());
644     modifiedFiles.append("\n");
645
646     if (!CommandLineParser::instance()->contains("dry-run")) {
647         repository->startFastImport();
648         repository->fastImport.writeNoLog("blob\nmark :");
649         repository->fastImport.writeNoLog(QByteArray::number(mark));
650         repository->fastImport.writeNoLog("\ndata ");
651         repository->fastImport.writeNoLog(QByteArray::number(length));
652         repository->fastImport.writeNoLog("\n", 1);
653     }
654
655     return &repository->fastImport;
656 }
657
658 void Repository::Transaction::commitNote(const QByteArray &noteText, bool append, const QByteArray &commit)
659 {
660     QByteArray branchRef = branch;
661     if (!branchRef.startsWith("refs/"))
662         branchRef.prepend("refs/heads/");
663     const QByteArray &commitRef = commit.isNull() ? branchRef : commit;
664     QByteArray message = "Adding Git note for current " + commitRef + "\n";
665     QByteArray text = noteText;
666
667     if (append && commit.isNull() &&
668         repository->branchExists(branch) &&
669         !repository->branchNote(branch).isEmpty())
670     {
671         text = repository->branchNote(branch) + text;
672         message = "Appending Git note for current " + commitRef + "\n";
673     }
674
675     QTextStream s(&repository->fastImport);
676     s << "commit refs/notes/commits" << endl
677       << "committer " << QString::fromUtf8(author) << ' ' << datetime << " -0000" << endl
678       << "data " << message.length() << endl
679       << message << endl
680       << "N inline " << commitRef << endl
681       <<  "data " << text.length() << endl
682       << text << endl;
683
684     if (commit.isNull()) {
685         repository->setBranchNote(QString::fromUtf8(branch), text);
686     }
687 }
688
689 void Repository::Transaction::commit()
690 {
691     repository->startFastImport();
692
693     // We might be tempted to use the SVN revision number as the fast-import commit mark.
694     // However, a single SVN revision can modify multple branches, and thus lead to multiple
695     // commits in the same repo.  So, we need to maintain a separate commit mark counter.
696     int mark = ++repository->last_commit_mark;
697
698     // in case the two mark allocations meet, we might as well just abort
699     Q_ASSERT(mark < repository->next_file_mark - 1);
700
701     // create the commit message
702     QByteArray message = log;
703     if (!message.endsWith('\n'))
704         message += '\n';
705     if (CommandLineParser::instance()->contains("add-metadata"))
706         message += "\n" + Repository::formatMetadataMessage(svnprefix, revnum);
707
708     int parentmark = 0;
709     Branch &br = repository->branches[branch];
710     if (br.created && !br.marks.isEmpty() && br.marks.last()) {
711         parentmark = br.marks.last();
712     } else {
713         qWarning() << "WARN: Branch" << branch << "in repository" << repository->name << "doesn't exist at revision"
714                    << revnum << "-- did you resume from the wrong revision?";
715         br.created = revnum;
716     }
717     br.commits.append(revnum);
718     br.marks.append(mark);
719
720     {
721         QByteArray branchRef = branch;
722         if (!branchRef.startsWith("refs/"))
723             branchRef.prepend("refs/heads/");
724         QTextStream s(&repository->fastImport);
725         s.setCodec("UTF-8");
726         s << "commit " << branchRef << endl;
727         s << "mark :" << QByteArray::number(mark) << endl;
728         s << "committer " << QString::fromUtf8(author) << ' ' << datetime << " +0000" << endl;
729
730         s << "data " << message.length() << endl;
731     }
732
733     repository->fastImport.write(message);
734     repository->fastImport.putChar('\n');
735
736     // note some of the inferred merges
737     QByteArray desc = "";
738     int i = !!parentmark;       // if parentmark != 0, there's at least one parent
739
740     if(log.contains("This commit was manufactured by cvs2svn") && merges.count() > 1) {
741         qSort(merges);
742         repository->fastImport.write("merge :" + QByteArray::number(merges.last()) + "\n");
743         merges.pop_back();
744         qWarning() << "WARN: Discarding all but the highest merge point as a workaround for cvs2svn created branch/tag"
745                       << "Discarded marks:" << merges;
746     } else {
747         foreach (const int merge, merges) {
748             if (merge == parentmark) {
749                 qDebug() << "Skipping marking" << merge << "as a merge point as it matches the parent";
750                 continue;
751             }
752
753             if (++i > 16) {
754                 // FIXME: options:
755                 //   (1) ignore the 16 parent limit
756                 //   (2) don't emit more than 16 parents
757                 //   (3) create another commit on branch to soak up additional parents
758                 // we've chosen option (2) for now, since only artificial commits
759                 // created by cvs2svn seem to have this issue
760                 qWarning() << "WARN: too many merge parents";
761                 break;
762             }
763
764             QByteArray m = " :" + QByteArray::number(merge);
765             desc += m;
766             repository->fastImport.write("merge" + m + "\n");
767         }
768     }
769     // write the file deletions
770     if (deletedFiles.contains(""))
771         repository->fastImport.write("deleteall\n");
772     else
773         foreach (QString df, deletedFiles)
774             repository->fastImport.write("D " + df.toUtf8() + "\n");
775
776     // write the file modifications
777     repository->fastImport.write(modifiedFiles);
778
779     repository->fastImport.write("\nprogress SVN r" + QByteArray::number(revnum)
780                                  + " branch " + branch + " = :" + QByteArray::number(mark)
781                                  + (desc.isEmpty() ? "" : " # merge from") + desc
782                                  + "\n\n");
783     printf(" %d modifications from SVN %s to %s/%s",
784            deletedFiles.count() + modifiedFiles.count('\n'), svnprefix.data(),
785            qPrintable(repository->name), branch.data());
786
787     // Commit metadata note if requested
788     if (CommandLineParser::instance()->contains("add-metadata-notes"))
789         commitNote(Repository::formatMetadataMessage(svnprefix, revnum), false);
790
791     while (repository->fastImport.bytesToWrite())
792         if (!repository->fastImport.waitForBytesWritten(-1))
793             qFatal("Failed to write to process: %s for repository %s", qPrintable(repository->fastImport.errorString()), qPrintable(repository->name));
794 }