Fixed git-svn authors map compatiblity. SVN is able to use committer/author names...
[svn2git:uqs-svn2git.git] / src / main.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 #include <QCoreApplication>
19 #include <QFile>
20 #include <QStringList>
21 #include <QTextStream>
22 #include <QDebug>
23
24 #include <limits.h>
25 #include <stdio.h>
26
27 #include "CommandLineParser.h"
28 #include "ruleparser.h"
29 #include "repository.h"
30 #include "svn.h"
31
32 QHash<QByteArray, QByteArray> loadIdentityMapFile(const QString &fileName)
33 {
34     QHash<QByteArray, QByteArray> result;
35     if (fileName.isEmpty())
36         return result;
37
38     QFile file(fileName);
39     if (!file.open(QIODevice::ReadOnly)) {
40         fprintf(stderr, "Could not open file %s: %s",
41                 qPrintable(fileName), qPrintable(file.errorString()));
42         return result;
43     }
44
45     while (!file.atEnd()) {
46         QByteArray line = file.readLine();
47         int comment_pos = line.indexOf('#');
48         if (comment_pos != -1)
49             line.truncate(comment_pos);
50         line = line.trimmed();
51         int space = line.indexOf(' ');
52         if (space == -1)
53             continue;           // invalid line
54
55         // Support git-svn author files, too
56         // - svn2git native:  loginname Joe User <user@example.com>
57         // - git-svn:         loginname = Joe User <user@example.com>
58         int rightspace = line.indexOf(" = ");
59         int leftspace = space;
60         if (rightspace == -1) {
61             rightspace = space;
62         } else {
63           leftspace = rightspace;
64           rightspace += 2;
65         }
66
67         QByteArray realname = line.mid(rightspace).trimmed();
68         line.truncate(leftspace);
69
70         result.insert(line, realname);
71     };
72     file.close();
73
74     return result;
75 }
76
77 QSet<int> loadRevisionsFile( const QString &fileName, Svn &svn )
78 {
79     QRegExp revint("(\\d+)\\s*(?:-\\s*(\\d+|HEAD))?");
80     QSet<int> revisions;
81     if(fileName.isEmpty())
82         return revisions;
83
84     QFile file(fileName);
85     if( !file.open(QIODevice::ReadOnly)) {
86         fprintf(stderr, "Could not open file %s: %s\n", qPrintable(fileName), qPrintable(file.errorString()));
87         return revisions;
88     }
89
90     bool ok;
91     while(!file.atEnd()) {
92         QByteArray line = file.readLine().trimmed();
93         revint.indexIn(line);
94         if( revint.cap(2).isEmpty() ) {
95             int rev = revint.cap(1).toInt(&ok);
96             if(ok) {
97                 revisions.insert(rev);
98             } else {
99                 fprintf(stderr, "Unable to convert %s to int, skipping revision.\n", qPrintable(QString(line)));
100             }
101         } else if( revint.captureCount() == 2 ) {
102             int rev = revint.cap(1).toInt(&ok);
103             if(!ok) {
104                 fprintf(stderr, "Unable to convert %s (%s) to int, skipping revisions.\n", qPrintable(revint.cap(1)), qPrintable(QString(line)));
105                 continue;
106             }
107             int lastrev = 0;
108             if(revint.cap(2) == "HEAD") {
109                 lastrev = svn.youngestRevision();
110                 ok = true;
111             } else {
112                 lastrev = revint.cap(2).toInt(&ok);
113             }
114             if(!ok) {
115                 fprintf(stderr, "Unable to convert %s (%s) to int, skipping revisions.\n", qPrintable(revint.cap(2)), qPrintable(QString(line)));
116                 continue;
117             }
118             for(; rev <= lastrev; ++rev )
119                 revisions.insert(rev);
120         } else {
121             fprintf(stderr, "Unable to convert %s to int, skipping revision.\n", qPrintable(QString(line)));
122         }
123     }
124     file.close();
125     return revisions;
126 }
127
128 static const CommandLineOption options[] = {
129     {"--identity-map FILENAME", "provide map between svn username and email"},
130     {"--revisions-file FILENAME", "provide a file with revision number that should be processed"},
131     {"--rules FILENAME[,FILENAME]", "the rules file(s) that determines what goes where"},
132     {"--add-metadata", "if passed, each git commit will have svn commit info"},
133     {"--add-metadata-notes", "if passed, each git commit will have notes with svn commit info"},
134     {"--resume-from revision", "start importing at svn revision number"},
135     {"--max-rev revision", "stop importing at svn revision number"},
136     {"--dry-run", "don't actually write anything"},
137     {"--debug-rules", "print what rule is being used for each file"},
138     {"--commit-interval NUMBER", "if passed the cache will be flushed to git every NUMBER of commits"},
139     {"--stats", "after a run print some statistics about the rules"},
140     {"--svn-branches", "Use the contents of SVN when creating branches, Note: SVN tags are branches as well"},
141     {"-h, --help", "show help"},
142     {"-v, --version", "show version"},
143     CommandLineLastOption
144 };
145
146 int main(int argc, char **argv)
147 {
148     printf("Invoked as:'");
149     for(int i = 0; i < argc; ++i)
150         printf(" %s", argv[i]);
151     printf("'\n");
152     CommandLineParser::init(argc, argv);
153     CommandLineParser::addOptionDefinitions(options);
154     Stats::init();
155     CommandLineParser *args = CommandLineParser::instance();
156     if(args->contains(QLatin1String("version"))) {
157         printf("Git version: %s\n", VER);
158         return 0;
159     }
160     if (args->contains(QLatin1String("help")) || args->arguments().count() != 1) {
161         args->usage(QString(), "[Path to subversion repo]");
162         return 0;
163     }
164     if (args->undefinedOptions().count()) {
165         QTextStream out(stderr);
166         out << "svn-all-fast-export failed: ";
167         bool first = true;
168         foreach (QString option, args->undefinedOptions()) {
169             if (!first)
170                 out << "          : ";
171             out << "unrecognized option or missing argument for; `" << option << "'" << endl;
172             first = false;
173         }
174         return 10;
175     }
176     if (!args->contains("rules")) {
177         QTextStream out(stderr);
178         out << "svn-all-fast-export failed: please specify the rules using the 'rules' argument\n";
179         return 11;
180     }
181     if (!args->contains("identity-map")) {
182         QTextStream out(stderr);
183         out << "WARNING; no identity-map specified, all commits will be without email address\n\n";
184     }
185
186     QCoreApplication app(argc, argv);
187     // Load the configuration
188     RulesList rulesList(args->optionArgument(QLatin1String("rules")));
189     rulesList.load();
190
191     int resume_from = args->optionArgument(QLatin1String("resume-from")).toInt();
192     int max_rev = args->optionArgument(QLatin1String("max-rev")).toInt();
193
194     // create the repository list
195     QHash<QString, Repository *> repositories;
196
197     int cutoff = resume_from ? resume_from : INT_MAX;
198  retry:
199     int min_rev = 1;
200     foreach (Rules::Repository rule, rulesList.allRepositories()) {
201         Repository *repo = new Repository(rule);
202         if (!repo)
203             return EXIT_FAILURE;
204         repositories.insert(rule.name, repo);
205
206         int repo_next = repo->setupIncremental(cutoff);
207
208         /*
209   * cutoff < resume_from => error exit eventually
210   * repo_next == cutoff => probably truncated log
211   */
212         if (cutoff < resume_from && repo_next == cutoff)
213             /*
214       * Restore the log file so we fail the next time
215       * svn2git is invoked with the same arguments
216       */
217             repo->restoreLog();
218
219         if (cutoff < min_rev)
220             /*
221       * We've rewound before the last revision of some
222       * repository that we've already seen.  Start over
223       * from the beginning.  (since cutoff is decreasing,
224       * we're sure we'll make forward progress eventually)
225       */
226             goto retry;
227
228         if (min_rev < repo_next)
229             min_rev = repo_next;
230     }
231
232     if (cutoff < resume_from) {
233         qCritical() << "Cannot resume from" << resume_from
234                     << "as there are errors in revision" << cutoff;
235         return EXIT_FAILURE;
236     }
237
238     if (min_rev < resume_from)
239         qDebug() << "skipping revisions" << min_rev << "to" << resume_from - 1 << "as requested";
240
241     if (resume_from)
242         min_rev = resume_from;
243
244     Svn::initialize();
245     Svn svn(args->arguments().first());
246     svn.setMatchRules(rulesList.allMatchRules());
247     svn.setRepositories(repositories);
248     svn.setIdentityMap(loadIdentityMapFile(args->optionArgument("identity-map")));
249
250     if (max_rev < 1)
251         max_rev = svn.youngestRevision();
252
253     bool errors = false;
254     QSet<int> revisions = loadRevisionsFile(args->optionArgument(QLatin1String("revisions-file")), svn);
255     const bool filerRevisions = !revisions.isEmpty();
256     for (int i = min_rev; i <= max_rev; ++i) {
257         if(filerRevisions) {
258             if( !revisions.contains(i) ) {
259                 printf(".");
260                 continue;
261             } else {
262                 printf("\n");
263             }
264         }
265         if (!svn.exportRevision(i)) {
266             errors = true;
267             break;
268         }
269     }
270
271     foreach (Repository *repo, repositories) {
272         repo->finalizeTags();
273         delete repo;
274     }
275     Stats::instance()->printStats();
276     return errors ? EXIT_FAILURE : EXIT_SUCCESS;
277 }