Add support for non-latin1 filenames
[vng.git] / src / patches / Commit.cpp
blob7defca26c2cf6f3698a43dbf79adf3fbd58089da
1 /*
2 * This file is part of the vng project
3 * Copyright (C) 2008 Thomas Zander <tzander@trolltech.com>
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.
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.
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/>.
19 #include "Commit.h"
20 #include "Commit_p.h"
21 #include "../GitRunner.h"
22 #include "../Logger.h"
24 #include <QProcess>
25 #include <QDebug>
27 static QDateTime timeFromSecondsSinceEpoch(const QString &string) {
28 const int split = string.lastIndexOf('>');
29 const int tz = string.lastIndexOf(' ');
30 if (split > 0 && tz > split)
31 return QDateTime::fromTime_t(string.mid(split + 1, tz - split).toLong());
32 return QDateTime();
35 Commit::Commit(CommitPrivate *priv)
36 : d(priv)
40 Commit::Commit()
41 : d(0)
45 Commit::Commit(const QString &treeIsm, const Commit &nextCommit)
46 : d(0)
48 fillFromTreeIsm(treeIsm);
49 if (d)
50 d->child = nextCommit;
53 Commit::Commit(const Commit &other)
54 : d(other.d)
56 if (d)
57 d->ref++;
60 Commit::~Commit()
62 if (d && --d->ref == 0)
63 delete d;
66 QList<Commit> Commit::previous()
68 if (d->previousCommits.count() != d->parentTreeisms.count()) {
69 foreach(QString id, d->parentTreeisms)
70 d->previousCommits.append(Commit(id, *this));
72 return d->previousCommits;
75 int Commit::previousCommitsCount() const
77 return d->parentTreeisms.count();
80 QString Commit::author() const
82 int split = d->author.lastIndexOf('>');
83 if (split > 0)
84 return d->author.left(split + 1);
85 return d->author;
88 QString Commit::committer() const
90 int split = d->committer.lastIndexOf('>');
91 if (split > 0)
92 return d->committer.left(split + 1);
93 return d->committer;
96 QDateTime Commit::commitTime() const
98 return timeFromSecondsSinceEpoch(d->committer);
101 QDateTime Commit::authorTime() const
103 return timeFromSecondsSinceEpoch(d->author);
106 QByteArray Commit::logMessage() const
108 return d->logMessage;
111 QString Commit::tree() const
113 return d->tree;
116 void Commit::clearParents()
118 d->previousCommits.clear();
121 Commit &Commit::operator=(const Commit &other)
123 if (other.d)
124 other.d->ref++;
125 if (d && --d->ref == 0)
126 delete d;
127 d = other.d;
128 return *this;
131 bool Commit::operator==(const Commit &other)
133 return other.d == d || (other.d && d && other.d->tree == d->tree);
136 Vng::Acceptance Commit::acceptance() const
138 return d->acceptance;
141 void Commit::setAcceptance(Vng::Acceptance accepted)
143 d->acceptance = accepted;
146 bool Commit::isValid() const
148 return d && !d->tree.isEmpty();
151 QString Commit::commitTreeIsm() const
153 return d->treeIsm;
156 QString Commit::commitTreeIsmSha1() const
158 if (! d->resolvedTreeIsm.isEmpty())
159 return d->resolvedTreeIsm;
161 QProcess git;
162 QStringList arguments;
163 if (d->treeIsm == "HEAD") // grmbl; git requires us to special case this...
164 arguments << "show-ref" << "--hash" << "--head";
165 else
166 arguments << "show-ref" << "--hash" << d->treeIsm;
167 GitRunner runner(git, arguments);
168 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
169 if (rc)
170 return d->treeIsm;
172 char buf[50];
173 while(true) {
174 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
175 if (lineLength == -1)
176 break;
177 d->resolvedTreeIsm += QString::fromLatin1(buf, lineLength);
178 if (d->resolvedTreeIsm.length() > 40)
179 break;
181 d->resolvedTreeIsm = d->resolvedTreeIsm.trimmed();
182 if (d->resolvedTreeIsm.length() != 40) {
183 Logger::warn() << "Commit::commitTreeIsmSha1: show-ref gave unexpected; '" << d->resolvedTreeIsm << "`\n";
184 d->resolvedTreeIsm.clear();
186 return d->resolvedTreeIsm;
189 Commit Commit::next()
191 return d->child;
194 Commit Commit::firstCommitInBranch()
196 Q_ASSERT(isValid());
197 int max = 50;
198 Q_ASSERT(isValid());
199 if (previousCommitsCount() == 0) {
200 Logger::debug() << "Commit::firstCommitInBranch: No parent commit\n";
201 return Commit(); // this is the first commit in the repo
204 Commit currentBranch;
206 QList<Commit> otherBranches;
208 QDir heads(".git/refs/heads");
209 foreach (QString head, heads.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
210 Commit branch(head);
211 if (operator==(branch)) { // this only works if this commit is the curent HEAD...
212 currentBranch = (*this);
213 continue;
215 otherBranches.append(branch);
217 if (!currentBranch.isValid()) { // we are not on a branch...
218 Logger::debug() << "Commit::firstCommitInBranch: we are not on a branch\n";
219 return Commit();
222 currentBranch = currentBranch.previous()[0];
223 while(true) {
224 QDateTime time = currentBranch.commitTime();
225 QList<Commit> list;
226 foreach (Commit c, otherBranches) {
227 Q_ASSERT(c.previousCommitsCount());
228 while(c.commitTime() > time)
229 c = c.previous()[0];
230 if (c == currentBranch && currentBranch.previousCommitsCount() == 1)
231 return currentBranch;
232 list.append(c);
234 otherBranches = list;
235 if (currentBranch.previousCommitsCount() == 0)
236 return currentBranch;
237 currentBranch = currentBranch.previous()[0];
238 if (--max <= 0)
239 return Commit();
243 ChangeSet Commit::changeSet() const
245 return d->changeSet;
248 void Commit::fillFromTreeIsm(const QString &treeIsm)
250 QProcess git;
251 QStringList arguments;
252 arguments << "cat-file" << "commit" << treeIsm;
253 GitRunner runner(git, arguments);
254 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
255 if (rc) {
256 Logger::info() << "Invalid treeIsm passed " << treeIsm << endl;
257 delete d;
258 d = 0;
259 return;
261 Commit c = createFromStream(&git, d);
262 d = c.d;
263 d->treeIsm = treeIsm;
264 if (d)
265 d->ref++;
267 if (d->treeIsm.length() == 40) // more checks?
268 d->resolvedTreeIsm = d->treeIsm;
271 // static
272 Commit Commit::createFromStream(QIODevice *device)
274 return Commit::createFromStream(device, 0);
277 // static
278 Commit Commit::createFromStream(QIODevice *device, CommitPrivate *priv)
280 if (priv == 0)
281 priv = new CommitPrivate();
282 char buf[1024];
284 enum State { Init, Header, Comment, Files, Done };
285 State state = Init;
286 while (state != Done) {
287 qint64 lineLength = Vng::readLine(device, buf, sizeof(buf));
288 if (lineLength == -1)
289 break;
290 QString line = QString::fromLocal8Bit(buf, lineLength);
291 if (state == Init || state == Header) {
292 state = Header;
293 if (line.length() == 1) {
294 state = Comment;
295 continue;
297 else if (line.startsWith("commit "))
298 priv->treeIsm = line.mid(7).trimmed();
299 else if (line.startsWith("parent "))
300 priv->parentTreeisms.append(line.mid(7).trimmed());
301 else if (line.startsWith("author "))
302 priv->author = line.mid(7).trimmed();
303 else if (line.startsWith("committer "))
304 priv->committer = line.mid(10).trimmed();
305 else if (line.startsWith("tree "))
306 priv->tree = line.mid(5).trimmed();
307 else
308 state = Init;
310 if ((state == Init || state == Files) && line.length() > 0) {
311 if (line[0] == ':')
312 state = Files;
313 if (line.length() > 100) {
314 if (priv->parentTreeisms.count() <= 1) { // skip file listings for merges
315 File file;
316 file.setOldProtection(line.mid(1, 6));
317 file.setProtection(line.mid(8, 6));
318 file.setOldSha1(line.mid(15, 40));
319 file.setSha1(line.mid(56, 40));
320 file.setFileName(File::escapeGitFilename(QByteArray(buf + 99, lineLength - 100))); // immediately cut off the linefeed
321 switch (buf[97]) {
322 case 'M': file.setOldFileName(file.fileName()); break;
323 case 'D': file.setOldFileName(file.fileName()); file.setFileName(QByteArray()); break;
324 default:
325 break;
327 priv->changeSet.addFile(file);
330 else {
331 device->waitForReadyRead(-1);
332 lineLength = device->peek(buf, 2);
333 if (lineLength == -1 || buf[0] == 'c') { // exit, the next line is not part of this commit no more.
334 state = Done;
335 break;
339 if (state == Comment) {
340 if (line.length() == 1)
341 state = Init; // can be part of the comment, or we will start the Files section soon.
342 else {
343 if (state == Init && ! priv->logMessage.isEmpty())
344 priv->logMessage.append('\n');
345 priv->logMessage += line;
347 continue;
351 Commit answer;
352 Q_ASSERT(answer.d == 0); // make sure there will be no mem-leak
353 answer.d = priv;
354 return answer;
357 // static
358 QList<File> Commit::allFiles(const QString &commitTreeIsm)
360 QList<File> files;
362 QProcess git;
363 QStringList arguments;
364 arguments << "ls-tree" << "-r" << commitTreeIsm;
365 GitRunner runner(git, arguments);
366 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
367 if (rc != AbstractCommand::Ok)
368 return files;
369 char buf[4000];
370 while(true) {
371 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
372 if (lineLength == -1)
373 break;
374 Q_ASSERT(lineLength > 53);
375 buf[lineLength - 1] = 0; // remove the linefeed
376 File file;
377 file.setFileName(QByteArray(buf + 53, lineLength - 54));
378 file.setProtection(QString::fromLatin1(buf, 6));
379 file.setSha1(QString::fromLatin1(buf + 12, 40));
380 files << file;
382 git.waitForFinished();
383 return files;