2 * Copyright (C) 2007 Thiago Macieira <thiago@kde.org>
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 2 of the License, or
7 * (at your option) any later version.
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.
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/>.
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
25 #define _LARGEFILE_SUPPORT
26 #define _LARGEFILE64_SUPPORT
37 #include <apr_getopt.h>
38 #include <apr_general.h>
41 #include <svn_pools.h>
42 #include <svn_repos.h>
43 #include <svn_types.h>
48 #include "repository.h"
51 #define SVN_ERR(expr) SVN_INT_ERR(expr)
53 typedef QList
<Rules::Match
> MatchRuleList
;
54 typedef QHash
<QString
, Repository
*> RepositoryHash
;
55 typedef QHash
<QByteArray
, QByteArray
> IdentityHash
;
60 AprAutoPool(const AprAutoPool
&);
61 AprAutoPool
&operator=(const AprAutoPool
&);
63 inline AprAutoPool(apr_pool_t
*parent
= NULL
)
64 { pool
= svn_pool_create(parent
); }
66 { svn_pool_destroy(pool
); }
68 inline void clear() { svn_pool_clear(pool
); }
69 inline apr_pool_t
*data() const { return pool
; }
70 inline operator apr_pool_t
*() const { return pool
; }
76 MatchRuleList matchRules
;
77 RepositoryHash repositories
;
78 IdentityHash identities
;
80 SvnPrivate(const QString
&pathToRepository
);
82 int youngestRevision();
83 int exportRevision(int revnum
);
85 int openRepository(const QString
&pathToRepository
);
88 AprAutoPool global_pool
;
90 svn_revnum_t youngest_rev
;
93 void Svn::initialize()
95 // initialize APR or exit
96 if (apr_initialize() != APR_SUCCESS
) {
97 fprintf(stderr
, "You lose at apr_initialize().\n");
102 static struct Destructor
{ ~Destructor() { apr_terminate(); } } destructor
;
105 Svn::Svn(const QString
&pathToRepository
)
106 : d(new SvnPrivate(pathToRepository
))
115 void Svn::setMatchRules(const MatchRuleList
&matchRules
)
117 d
->matchRules
= matchRules
;
120 void Svn::setRepositories(const RepositoryHash
&repositories
)
122 d
->repositories
= repositories
;
125 int Svn::youngestRevision()
127 return d
->youngestRevision();
130 bool Svn::exportRevision(int revnum
)
132 return d
->exportRevision(revnum
) == EXIT_SUCCESS
;
135 SvnPrivate::SvnPrivate(const QString
&pathToRepository
)
138 openRepository(pathToRepository
);
140 // get the youngest revision
141 svn_fs_youngest_rev(&youngest_rev
, fs
, global_pool
);
144 SvnPrivate::~SvnPrivate()
146 svn_pool_destroy(global_pool
);
149 int SvnPrivate::youngestRevision()
154 int SvnPrivate::openRepository(const QString
&pathToRepository
)
157 SVN_ERR(svn_repos_open(&repos
, QFile::encodeName(pathToRepository
), global_pool
));
158 fs
= svn_repos_fs(repos
);
163 enum RuleType
{ AnyRule
= 0, NoIgnoreRule
= 0x01, NoRecurseRule
= 0x02 };
165 static MatchRuleList::ConstIterator
166 findMatchRule(const MatchRuleList
&matchRules
, int revnum
, const QString
¤t
,
167 int ruleMask
= AnyRule
)
169 MatchRuleList::ConstIterator it
= matchRules
.constBegin(),
170 end
= matchRules
.constEnd();
171 for ( ; it
!= end
; ++it
) {
172 if (it
->minRevision
> revnum
)
174 if (it
->maxRevision
!= -1 && it
->maxRevision
< revnum
)
176 if (it
->action
== Rules::Match::Ignore
&& ruleMask
& NoIgnoreRule
)
178 if (it
->action
== Rules::Match::Recurse
&& ruleMask
& NoRecurseRule
)
180 if (it
->rx
.indexIn(current
) == 0)
188 static void splitPathName(const Rules::Match
&rule
, const QString
&pathName
, QString
*svnprefix_p
,
189 QString
*repository_p
, QString
*branch_p
, QString
*path_p
)
191 QString svnprefix
= pathName
;
192 svnprefix
.truncate(rule
.rx
.matchedLength());
194 *svnprefix_p
= svnprefix
;
197 *repository_p
= svnprefix
;
198 repository_p
->replace(rule
.rx
, rule
.repository
);
202 *branch_p
= svnprefix
;
203 branch_p
->replace(rule
.rx
, rule
.branch
);
207 *path_p
= pathName
.mid(svnprefix
.length());
210 static int pathMode(svn_fs_root_t
*fs_root
, const char *pathname
, apr_pool_t
*pool
)
212 svn_string_t
*propvalue
;
213 SVN_ERR(svn_fs_node_prop(&propvalue
, fs_root
, pathname
, "svn:executable", pool
));
218 // maybe it's a symlink?
219 SVN_ERR(svn_fs_node_prop(&propvalue
, fs_root
, pathname
, "svn:special", pool
));
220 if (propvalue
&& strcmp(propvalue
->data
, "symlink") == 0)
226 svn_error_t
*QIODevice_write(void *baton
, const char *data
, apr_size_t
*len
)
228 QIODevice
*device
= reinterpret_cast<QIODevice
*>(baton
);
229 device
->write(data
, *len
);
231 if (device
->bytesToWrite() > 16384)
232 device
->waitForBytesWritten(0);
236 static svn_stream_t
*streamForDevice(QIODevice
*device
, apr_pool_t
*pool
)
238 svn_stream_t
*stream
= svn_stream_create(device
, pool
);
239 svn_stream_set_write(stream
, QIODevice_write
);
244 static int dumpBlob(Repository::Transaction
*txn
, svn_fs_root_t
*fs_root
,
245 const char *pathname
, const QString
&finalPathName
, apr_pool_t
*pool
)
247 AprAutoPool
dumppool(pool
);
249 int mode
= pathMode(fs_root
, pathname
, dumppool
);
251 svn_filesize_t stream_length
;
253 SVN_ERR(svn_fs_file_length(&stream_length
, fs_root
, pathname
, dumppool
));
254 QIODevice
*io
= txn
->addFile(finalPathName
, mode
, stream_length
);
258 svn_stream_t
*in_stream
, *out_stream
;
259 SVN_ERR(svn_fs_file_contents(&in_stream
, fs_root
, pathname
, dumppool
));
261 // open a generic svn_stream_t for the QIODevice
262 out_stream
= streamForDevice(io
, dumppool
);
263 SVN_ERR(svn_stream_copy(in_stream
, out_stream
, dumppool
));
265 // print an ending newline
272 static int recursiveDumpDir(Repository::Transaction
*txn
, svn_fs_root_t
*fs_root
,
273 const QByteArray
&pathname
, const QString
&finalPathName
,
276 // get the dir listing
278 SVN_ERR(svn_fs_dir_entries(&entries
, fs_root
, pathname
, pool
));
279 AprAutoPool
dirpool(pool
);
281 for (apr_hash_index_t
*i
= apr_hash_first(pool
, entries
); i
; i
= apr_hash_next(i
)) {
285 apr_hash_this(i
, &vkey
, NULL
, &value
);
287 svn_fs_dirent_t
*dirent
= reinterpret_cast<svn_fs_dirent_t
*>(value
);
288 QByteArray entryName
= pathname
+ '/' + dirent
->name
;
289 QString entryFinalName
;
290 if (finalPathName
.isEmpty())
291 entryFinalName
= dirent
->name
;
293 entryFinalName
= finalPathName
+ '/' + dirent
->name
;
295 if (dirent
->kind
== svn_node_dir
) {
296 if (recursiveDumpDir(txn
, fs_root
, entryName
, entryFinalName
, dirpool
) == EXIT_FAILURE
)
298 } else if (dirent
->kind
== svn_node_file
) {
301 if (dumpBlob(txn
, fs_root
, entryName
, entryFinalName
, dirpool
) == EXIT_FAILURE
)
309 static bool wasDir(svn_fs_t
*fs
, int revnum
, const char *pathname
, apr_pool_t
*pool
)
311 AprAutoPool
subpool(pool
);
312 svn_fs_root_t
*fs_root
;
313 if (svn_fs_revision_root(&fs_root
, fs
, revnum
, subpool
) != SVN_NO_ERROR
)
316 svn_boolean_t is_dir
;
317 if (svn_fs_is_dir(&is_dir
, fs_root
, pathname
, subpool
) != SVN_NO_ERROR
)
323 time_t get_epoch(char *svn_date
)
326 memset(&tm
, 0, sizeof tm
);
327 QByteArray
date(svn_date
, strlen(svn_date
) - 8);
328 strptime(date
, "%Y-%m-%dT%H:%M:%S", &tm
);
336 QHash
<QString
, Repository::Transaction
*> transactions
;
337 MatchRuleList matchRules
;
338 RepositoryHash repositories
;
339 IdentityHash identities
;
342 svn_fs_root_t
*fs_root
;
345 SvnRevision(int revision
, svn_fs_t
*f
, apr_pool_t
*parent_pool
)
346 : pool(parent_pool
), fs(f
), fs_root(0), revnum(revision
)
352 SVN_ERR(svn_fs_revision_root(&fs_root
, fs
, revnum
, pool
));
356 int prepareTransactions();
359 int exportEntry(const char *path
, const svn_fs_path_change_t
*change
);
360 int exportInternal(const char *path
, const svn_fs_path_change_t
*change
,
361 const char *path_from
, svn_revnum_t rev_from
,
362 const QString
¤t
, const Rules::Match
&rule
);
363 int recurse(const char *path
, const svn_fs_path_change_t
*change
,
364 const char *path_from
, svn_revnum_t rev_from
, apr_pool_t
*pool
);
367 int SvnPrivate::exportRevision(int revnum
)
369 SvnRevision
rev(revnum
, fs
, global_pool
);
370 rev
.matchRules
= matchRules
;
371 rev
.repositories
= repositories
;
372 rev
.identities
= identities
;
374 // open this revision:
375 printf("Exporting revision %d ", revnum
);
378 if (rev
.open() == EXIT_FAILURE
)
381 if (rev
.prepareTransactions() == EXIT_FAILURE
)
384 if (rev
.transactions
.isEmpty()) {
385 printf(" nothing to do\n");
386 return EXIT_SUCCESS
; // no changes?
389 if (rev
.commit() == EXIT_FAILURE
)
396 int SvnRevision::prepareTransactions()
398 // find out what was changed in this revision:
400 SVN_ERR(svn_fs_paths_changed(&changes
, fs_root
, pool
));
401 for (apr_hash_index_t
*i
= apr_hash_first(pool
, changes
); i
; i
= apr_hash_next(i
)) {
404 apr_hash_this(i
, &vkey
, NULL
, &value
);
405 const char *key
= reinterpret_cast<const char *>(vkey
);
406 svn_fs_path_change_t
*change
= reinterpret_cast<svn_fs_path_change_t
*>(value
);
408 if (exportEntry(key
, change
) == EXIT_FAILURE
)
415 int SvnRevision::commit()
417 // now create the commit
418 apr_hash_t
*revprops
;
419 SVN_ERR(svn_fs_revision_proplist(&revprops
, fs
, revnum
, pool
));
420 svn_string_t
*svnauthor
= (svn_string_t
*)apr_hash_get(revprops
, "svn:author", APR_HASH_KEY_STRING
);
421 svn_string_t
*svndate
= (svn_string_t
*)apr_hash_get(revprops
, "svn:date", APR_HASH_KEY_STRING
);
422 svn_string_t
*svnlog
= (svn_string_t
*)apr_hash_get(revprops
, "svn:log", APR_HASH_KEY_STRING
);
424 QByteArray log
= (char *)svnlog
->data
;
425 QByteArray authorident
= svnauthor
? identities
.value((char *)svnauthor
->data
) : QByteArray();
426 time_t epoch
= get_epoch((char*)svndate
->data
);
427 if (authorident
.isEmpty()) {
428 if (!svnauthor
|| svn_string_isempty(svnauthor
))
429 authorident
= "nobody <nobody@localhost>";
431 authorident
= svnauthor
->data
+ QByteArray(" <") +
432 svnauthor
->data
+ QByteArray("@localhost>");
435 foreach (Repository::Transaction
*txn
, transactions
) {
436 txn
->setAuthor(authorident
);
437 txn
->setDateTime(epoch
);
447 int SvnRevision::exportEntry(const char *key
, const svn_fs_path_change_t
*change
)
449 AprAutoPool
revpool(pool
.data());
450 QString current
= QString::fromUtf8(key
);
452 // was this copied from somewhere?
453 svn_revnum_t rev_from
;
454 const char *path_from
;
455 SVN_ERR(svn_fs_copied_from(&rev_from
, &path_from
, fs_root
, key
, revpool
));
457 // is this a directory?
458 svn_boolean_t is_dir
;
459 SVN_ERR(svn_fs_is_dir(&is_dir
, fs_root
, key
, revpool
));
461 if (path_from
== NULL
) {
462 // no, it's a new directory being added
463 // Git doesn't handle directories, so we don't either
464 //qDebug() << " mkdir ignored:" << key;
469 qDebug() << " " << key
<< "was copied from" << path_from
;
472 // find the first rule that matches this pathname
473 MatchRuleList::ConstIterator match
= findMatchRule(matchRules
, revnum
, current
);
474 if (match
!= matchRules
.constEnd()) {
475 const Rules::Match
&rule
= *match
;
476 switch (rule
.action
) {
477 case Rules::Match::Ignore
:
479 qDebug() << " " << qPrintable(current
) << "rev" << revnum
480 << "-> ignored (rule line" << rule
.lineNumber
<< ")";
483 case Rules::Match::Recurse
:
486 return recurse(key
, change
, path_from
, rev_from
, revpool
);
487 if (change
->change_kind
!= svn_fs_path_change_delete
)
488 qWarning() << " recurse rule " << rule
.rx
.pattern() << "line" << rule
.lineNumber
489 << "applied to non-directory:" << qPrintable(current
);
492 case Rules::Match::Export
:
493 return exportInternal(key
, change
, path_from
, rev_from
, current
, rule
);
497 if (is_dir
&& path_from
!= NULL
) {
498 qDebug() << current
<< "is a copy-with-history, auto-recursing";
499 return recurse(key
, change
, path_from
, rev_from
, revpool
);
500 } else if (wasDir(fs
, revnum
- 1, key
, revpool
)) {
501 qDebug() << current
<< "was a directory; ignoring";
502 } else if (change
->change_kind
== svn_fs_path_change_delete
) {
503 qDebug() << current
<< "is being deleted but I don't know anything about it; ignoring";
505 qCritical() << current
<< "did not match any rules; cannot continue";
512 int SvnRevision::exportInternal(const char *key
, const svn_fs_path_change_t
*change
,
513 const char *path_from
, svn_revnum_t rev_from
,
514 const QString
¤t
, const Rules::Match
&rule
)
516 QString svnprefix
, repository
, branch
, path
;
517 splitPathName(rule
, current
, &svnprefix
, &repository
, &branch
, &path
);
521 // qDebug() << " " << qPrintable(current) << "rev" << revnum << "->"
522 // << qPrintable(repository) << qPrintable(branch) << qPrintable(path);
524 if (path
.isEmpty() && path_from
!= NULL
) {
525 QString previous
= QString::fromUtf8(path_from
) + '/';
526 MatchRuleList::ConstIterator prevmatch
=
527 findMatchRule(matchRules
, rev_from
, previous
, NoRecurseRule
| NoIgnoreRule
);
528 if (prevmatch
!= matchRules
.constEnd()) {
529 QString prevsvnprefix
, prevrepository
, prevbranch
, prevpath
;
530 splitPathName(*prevmatch
, previous
, &prevsvnprefix
, &prevrepository
,
531 &prevbranch
, &prevpath
);
533 if (!prevpath
.isEmpty()) {
534 qDebug() << qPrintable(current
) << "is a partial branch of repository"
535 << qPrintable(prevrepository
) << "branch"
536 << qPrintable(prevbranch
) << "subdir"
537 << qPrintable(prevpath
);
538 } else if (prevrepository
!= repository
) {
539 qWarning() << qPrintable(current
) << "rev" << revnum
540 << "is a cross-repository copy (from repository"
541 << qPrintable(prevrepository
) << "branch"
542 << qPrintable(prevbranch
) << "path"
543 << qPrintable(prevpath
) << "rev" << rev_from
<< ")";
544 } else if (prevbranch
== branch
) {
545 // same branch and same repository
546 qDebug() << qPrintable(current
) << "rev" << revnum
547 << "is an SVN rename from"
548 << qPrintable(previous
) << "rev" << rev_from
;
551 // same repository but not same branch
552 // this means this is a plain branch
553 qDebug() << qPrintable(repository
) << ": branch"
554 << qPrintable(branch
) << "is branching from"
555 << qPrintable(prevbranch
);
557 Repository
*repo
= repositories
.value(repository
, 0);
559 qCritical() << "Rule" << rule
.rx
.pattern() << "line" << rule
.lineNumber
560 << "references unknown repository" << repository
;
564 repo
->createBranch(branch
, revnum
, prevbranch
, rev_from
);
569 Repository::Transaction
*txn
= transactions
.value(repository
, 0);
571 Repository
*repo
= repositories
.value(repository
, 0);
573 qCritical() << "Rule" << rule
.rx
.pattern() << "line" << rule
.lineNumber
574 << "references unknown repository" << repository
;
578 txn
= repo
->newTransaction(branch
, svnprefix
, revnum
);
582 transactions
.insert(repository
, txn
);
585 if (change
->change_kind
== svn_fs_path_change_delete
) {
586 txn
->deleteFile(path
);
587 } else if (!current
.endsWith('/')) {
588 dumpBlob(txn
, fs_root
, key
, path
, pool
);
590 txn
->deleteFile(path
);
591 recursiveDumpDir(txn
, fs_root
, key
, path
, pool
);
597 int SvnRevision::recurse(const char *path
, const svn_fs_path_change_t
*change
,
598 const char *path_from
, svn_revnum_t rev_from
,
601 // get the dir listing
603 SVN_ERR(svn_fs_dir_entries(&entries
, fs_root
, path
, pool
));
605 AprAutoPool
dirpool(pool
);
606 for (apr_hash_index_t
*i
= apr_hash_first(pool
, entries
); i
; i
= apr_hash_next(i
)) {
610 apr_hash_this(i
, &vkey
, NULL
, &value
);
612 svn_fs_dirent_t
*dirent
= reinterpret_cast<svn_fs_dirent_t
*>(value
);
613 QByteArray entry
= path
+ QByteArray("/") + dirent
->name
;
614 QByteArray entryFrom
;
616 entryFrom
= path_from
+ QByteArray("/") + dirent
->name
;
618 QString current
= QString::fromUtf8(entry
);
619 if (dirent
->kind
== svn_node_dir
)
622 // find the first rule that matches this pathname
623 MatchRuleList::ConstIterator match
= findMatchRule(matchRules
, revnum
, current
);
624 if (match
!= matchRules
.constEnd()) {
625 if (exportInternal(entry
, change
, entryFrom
.isNull() ? 0 : entryFrom
.constData(),
626 rev_from
, current
, *match
) == EXIT_FAILURE
)
629 qCritical() << current
<< "did not match any rules; cannot continue";