Cache results to avoid calling git twice
[vng.git] / src / patches / Commit.cpp
blobd590d247a75ef88eb1e35bd1692f9fcb666bd470
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, GitRunner::FailureAccepted);
169 if (rc) {
170 d->resolvedTreeIsm = d->treeIsm;
171 return d->treeIsm;
174 char buf[50];
175 while(true) {
176 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
177 if (lineLength == -1)
178 break;
179 d->resolvedTreeIsm += QString::fromLatin1(buf, lineLength);
180 if (d->resolvedTreeIsm.length() > 40)
181 break;
183 d->resolvedTreeIsm = d->resolvedTreeIsm.trimmed();
184 if (d->resolvedTreeIsm.length() != 40) {
185 Logger::warn() << "Commit::commitTreeIsmSha1: show-ref gave unexpected; '" << d->resolvedTreeIsm << "`\n";
186 d->resolvedTreeIsm.clear();
188 return d->resolvedTreeIsm;
191 Commit Commit::next()
193 return d->child;
196 Commit Commit::firstCommitInBranch()
198 Q_ASSERT(isValid());
199 int max = 50;
200 Q_ASSERT(isValid());
201 if (previousCommitsCount() == 0) {
202 Logger::debug() << "Commit::firstCommitInBranch: No parent commit\n";
203 return Commit(); // this is the first commit in the repo
206 Commit currentBranch;
208 QList<Commit> otherBranches;
210 QDir heads(".git/refs/heads");
211 foreach (QString head, heads.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
212 Commit branch(head);
213 if (operator==(branch)) { // this only works if this commit is the curent HEAD...
214 currentBranch = (*this);
215 continue;
217 otherBranches.append(branch);
219 if (!currentBranch.isValid()) { // we are not on a branch...
220 Logger::debug() << "Commit::firstCommitInBranch: we are not on a branch\n";
221 return Commit();
224 currentBranch = currentBranch.previous()[0];
225 while(true) {
226 QDateTime time = currentBranch.commitTime();
227 QList<Commit> list;
228 foreach (Commit c, otherBranches) {
229 Q_ASSERT(c.previousCommitsCount());
230 while(c.commitTime() > time)
231 c = c.previous()[0];
232 if (c == currentBranch && currentBranch.previousCommitsCount() == 1)
233 return currentBranch;
234 list.append(c);
236 otherBranches = list;
237 if (currentBranch.previousCommitsCount() == 0)
238 return currentBranch;
239 currentBranch = currentBranch.previous()[0];
240 if (--max <= 0)
241 return Commit();
245 ChangeSet Commit::changeSet() const
247 return d->changeSet;
250 void Commit::fillFromTreeIsm(const QString &treeIsm)
252 QProcess git;
253 QStringList arguments;
254 arguments << "cat-file" << "commit" << treeIsm;
255 GitRunner runner(git, arguments);
256 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
257 if (rc) {
258 Logger::info() << "Invalid treeIsm passed " << treeIsm << endl;
259 delete d;
260 d = 0;
261 return;
263 Commit c = createFromStream(&git, d);
264 d = c.d;
265 d->treeIsm = treeIsm;
266 if (d)
267 d->ref++;
269 if (d->treeIsm.length() == 40) // more checks?
270 d->resolvedTreeIsm = d->treeIsm;
273 // static
274 Commit Commit::createFromStream(QIODevice *device)
276 return Commit::createFromStream(device, 0);
279 // static
280 Commit Commit::createFromStream(QIODevice *device, CommitPrivate *priv)
282 if (priv == 0)
283 priv = new CommitPrivate();
284 char buf[1024];
286 enum State { Init, Header, Comment, Files, Done };
287 State state = Init;
288 while (state != Done) {
289 qint64 lineLength = Vng::readLine(device, buf, sizeof(buf));
290 if (lineLength == -1)
291 break;
292 QString line = QString::fromLocal8Bit(buf, lineLength);
293 if (state == Init || state == Header) {
294 state = Header;
295 if (line.length() == 1) {
296 state = Comment;
297 continue;
299 else if (line.startsWith("commit "))
300 priv->treeIsm = line.mid(7).trimmed();
301 else if (line.startsWith("parent "))
302 priv->parentTreeisms.append(line.mid(7).trimmed());
303 else if (line.startsWith("author "))
304 priv->author = line.mid(7).trimmed();
305 else if (line.startsWith("committer "))
306 priv->committer = line.mid(10).trimmed();
307 else if (line.startsWith("tree "))
308 priv->tree = line.mid(5).trimmed();
309 else
310 state = Init;
312 if ((state == Init || state == Files) && line.length() > 0) {
313 if (line[0] == ':')
314 state = Files;
315 if (line.length() > 100) {
316 if (priv->parentTreeisms.count() <= 1) { // skip file listings for merges
317 File file;
318 file.setOldProtection(line.mid(1, 6));
319 file.setProtection(line.mid(8, 6));
320 file.setOldSha1(line.mid(15, 40));
321 file.setSha1(line.mid(56, 40));
322 file.setFileName(File::escapeGitFilename(QByteArray(buf + 99, lineLength - 100))); // immediately cut off the linefeed
323 switch (buf[97]) {
324 case 'M': file.setOldFileName(file.fileName()); break;
325 case 'D': file.setOldFileName(file.fileName()); file.setFileName(QByteArray()); break;
326 default:
327 break;
329 priv->changeSet.addFile(file);
332 else {
333 device->waitForReadyRead(-1);
334 lineLength = device->peek(buf, 2);
335 if (lineLength == -1 || buf[0] == 'c') { // exit, the next line is not part of this commit no more.
336 state = Done;
337 break;
341 if (state == Comment) {
342 if (line.length() == 1)
343 state = Init; // can be part of the comment, or we will start the Files section soon.
344 else {
345 if (state == Init && ! priv->logMessage.isEmpty())
346 priv->logMessage.append('\n');
347 priv->logMessage += line.toLocal8Bit();
349 continue;
353 Commit answer;
354 Q_ASSERT(answer.d == 0); // make sure there will be no mem-leak
355 answer.d = priv;
356 return answer;
359 // static
360 QList<File> Commit::allFiles(const QString &commitTreeIsm)
362 QList<File> files;
364 QProcess git;
365 QStringList arguments;
366 arguments << "ls-tree" << "-r" << commitTreeIsm;
367 GitRunner runner(git, arguments);
368 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
369 if (rc != AbstractCommand::Ok)
370 return files;
371 char buf[4000];
372 while(true) {
373 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
374 if (lineLength == -1)
375 break;
376 Q_ASSERT(lineLength > 53);
377 buf[lineLength - 1] = 0; // remove the linefeed
378 File file;
379 file.setFileName(QByteArray(buf + 53, lineLength - 54));
380 file.setProtection(QString::fromLatin1(buf, 6));
381 file.setSha1(QString::fromLatin1(buf + 12, 40));
382 files << file;
384 git.waitForFinished();
385 return files;