Make test more complete
[vng.git] / hunks / ChangeSet.cpp
blob58795689dc196284f5ec0f13b58a868aa4daaf1e
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 "ChangeSet.h"
20 #include "../GitRunner.h"
21 #include "../Logger.h" // for debugging level only
22 #include "../AbstractCommand.h"
23 #include "../Vng.h"
25 #include <QProcess>
26 #include <QThread>
27 #include <QMutexLocker>
28 #include <QMutex>
29 #include <QWaitCondition>
30 #include <QDebug>
32 class HunksFetcher : public QThread
34 public:
35 HunksFetcher(const QList<File> &files, ChangeSet &changeSet, bool changeSetOnIndex)
36 : m_files(files),
37 m_changeSet(changeSet),
38 m_changeSetOnIndex(changeSetOnIndex),
39 m_interrupted(false)
43 void run()
45 setPriority(QThread::LowPriority);
46 foreach(File file, m_files) {
47 if (m_interrupted)
48 break;
49 m_changeSet.lockFile(file);
51 file.fetchHunks(m_changeSetOnIndex);
53 if (!file.isBinary() && file.count() == 0 && !file.hasChanged()) { // No change in file at all.
54 Logger::debug() << "file: `" << file.oldFileName() << "' => `" << file.fileName() << "'\n";
55 Logger::debug() << " +- Unchanged file, skipping\n";
56 m_changeSet.removeFile(file);
57 QProcess git;
58 QStringList arguments;
59 arguments << "update-index" << "-q" << "--refresh" << file.fileName();
60 GitRunner runner(git, arguments);
61 runner.start(GitRunner::WaitUntilFinished);
62 continue;
64 if (file.fileName().isEmpty() || file.oldFileName().isEmpty())
65 file.setProtectionAcceptance(Vng::Accepted);
67 int i=0;
68 if (Logger::verbosity() >= Logger::Debug) {
69 Logger::debug() << "file: `" << file.oldFileName() << "' => `" << file.fileName() << "'\n";
70 if (file.isBinary())
71 Logger::debug() << " +- is a binary file" << endl;
72 Logger::debug() << " +- " << file.oldProtection() << " => " << file.protection() << endl;
73 foreach(Hunk h, file.hunks()) {
74 Logger::debug() << " +-(" << i++ << ") @" << h.lineNumber() << "; " << h.patch().size() << " bytes\n";
75 for(int i = 0; i < h.subHunkCount(); i++) {
76 Logger::debug() << " " << i <<"/"<< h.subHunkCount() <<"; "<< h.subHunk(i).size() <<" bytes\n";
81 m_changeSet.lockFile(File());
82 m_changeSet.allHunksFetched();
85 void interrupt()
87 m_interrupted = true;
90 private:
91 QList<File> m_files;
92 ChangeSet &m_changeSet;
93 bool m_changeSetOnIndex, m_interrupted;
96 class ChangeSet::Private
98 public:
99 Private() :
100 changeSetOnIndex(false),
101 hunksFetcher(0),
102 ref(1),
103 finishedOneHunk(true)
107 ~Private() {
108 if (hunksFetcher) {
109 hunksFetcher->interrupt();
110 hunksFetcher->wait();
111 delete hunksFetcher;
115 QList<File> files;
116 bool changeSetOnIndex; // the changesSet shows the changes of the working dir
117 QMutex fileAccessLock;
118 QWaitCondition fileAccessWaiter;
119 QWaitCondition cursorAccessWaiter;
120 QMutex cursorAccessLock;
121 File lockedFile;
122 HunksFetcher *hunksFetcher;
123 #if QT_VERSION >= 0x040400
124 QAtomicInt ref;
125 #else
126 int ref;
127 #endif
128 bool finishedOneHunk;
131 ChangeSet::ChangeSet()
132 : d(new Private())
136 ChangeSet::~ChangeSet()
138 #if QT_VERSION >= 0x040400
139 if (!d->ref.deref())
140 #else
141 if (--d->ref == 0)
142 #endif
143 delete d;
146 ChangeSet::ChangeSet(const ChangeSet &other)
147 : d(other.d)
149 #if QT_VERSION >= 0x040400
150 d->ref.ref();
151 #else
152 d->ref++;
153 #endif
156 AbstractCommand::ReturnCodes ChangeSet::fillFromDiffFile(QIODevice &file)
158 file.open(QIODevice::ReadOnly);
160 foreach (File f, readGitDiff(file))
161 addFile(f);
163 if (Logger::verbosity() >= Logger::Debug) {
164 foreach(File f, d->files) {
165 Logger::debug() << "changes in file: " << f.fileName() << endl;
166 int i=0;
167 foreach(Hunk h, f.hunks()) {
168 Logger::debug() << " +-(" << i++ << ") @" << h.lineNumber() << "; " << h.patch().size() << " bytes\n";
169 for(int i = 0; i < h.subHunkCount(); i++) {
170 Logger::debug() << " " << i <<"/"<< h.subHunkCount() <<"; "<< h.subHunk(i).size() <<" bytes\n";
174 Logger::debug().flush();
176 return AbstractCommand::Ok;
179 AbstractCommand::ReturnCodes ChangeSet::fillFromCurrentChanges(const QStringList &paths, bool doGenerateHunks )
181 d->changeSetOnIndex = true;
182 QDir refs(".git/refs/heads");
183 const bool emptyRepo = refs.count() == 2; // only '.' and '..'
184 if (emptyRepo) { // all files added are new, just add all.
185 QProcess git;
186 QStringList arguments;
187 arguments << "ls-files" << "-s";
188 GitRunner runner(git, arguments);
189 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
190 if (rc)
191 return rc;
192 char buf[1024];
193 while(true) {
194 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
195 if (lineLength == -1)
196 break;
197 File file;
198 file.setProtection(QString::fromAscii(buf, 6));
199 file.setSha1(QString::fromAscii(buf+7, 40));
200 file.setFileName(QByteArray(buf + 50, lineLength - 51));
201 d->files.append(file);
203 return AbstractCommand::Ok;
206 // TODO the below misses the usecase of vng add and then a filesystem rm. Use diff-index --cached to show those.
207 QProcess git;
208 QStringList arguments;
209 arguments << "diff-index" << "-M" << "HEAD";
210 if (! paths.isEmpty())
211 arguments << "--" << paths;
213 // for each line
214 // new file:
215 // :000000 100644 0000000000000000000000000000000000000000 8c3ae1d344f18b23c3bdde5d26658b70b03c65d9 A bar
216 // rename main.cpp => notmain.cpp
217 // :100644 100644 e58cfe72cb7a9559a0090886bea5b0ce00db6b47 477c729e5abbd701eb708df563e7e4f749b50435 R074 main.cpp notmain.cpp
218 // normal change
219 // :100644 100644 0bdd73e9ea0ba026f6796799946c4bfc9dd1b0b8 0000000000000000000000000000000000000000 M test
221 GitRunner runner(git, arguments);
222 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
223 if (rc)
224 return rc;
225 char buf[1024];
226 while(true) {
227 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
228 if (lineLength == -1)
229 break;
230 if (lineLength > 0 && buf[0] != ':') // not a diff line, ignore.
231 continue;
233 File file;
234 file.setOldProtection(QString::fromAscii(buf+1, 6));
235 file.setProtection(QString::fromAscii(buf+8, 6));
236 file.setOldSha1(QString::fromAscii(buf+15, 40));
237 file.setSha1(QString::fromAscii(buf+56, 40));
238 int offset = 98;
239 while (buf[offset] != '\t' && offset < lineLength)
240 offset++;
241 if (buf[97] == 'R') { // rename
242 int tab = offset + 1;
243 while (buf[tab] != '\t' && tab < lineLength)
244 tab++;
245 file.setOldFileName(QByteArray(buf + offset + 1, tab - offset - 1));
246 file.setFileName(QByteArray(buf + tab + 1, lineLength - tab - 2));
248 else if (buf[97] == 'C') { // Copied file
249 int tab = offset + 1;
250 while (buf[tab] != '\t' && tab < lineLength)
251 tab++;
252 QByteArray filename(buf + offset + 1, tab - offset - 1);
253 file.setOldFileName(filename);
254 file.setFileName(filename);
256 else {
257 QByteArray filename(buf + offset + 1, lineLength - offset - 2);
258 if (buf[97] != 'A') // Add
259 file.setOldFileName(filename);
260 if (buf[97] != 'D') // Delete
261 file.setFileName(filename);
263 d->files.append(file);
265 if (doGenerateHunks)
266 generateHunks();
268 return AbstractCommand::Ok;
271 void ChangeSet::generateHunks()
273 Q_ASSERT(d->hunksFetcher == 0);
274 d->hunksFetcher = new HunksFetcher(d->files, *this, d->changeSetOnIndex);
275 d->finishedOneHunk = false;
276 d->hunksFetcher->start();
279 void ChangeSet::lockFile(const File &file)
281 // qDebug() << "ChangeSet::lockFile";
282 QMutexLocker ml(&d->fileAccessLock);
283 d->lockedFile = file;
284 if (d->files.count() == 0 || d->files.at(0) != file) { // as soon as we have done a file, we can start the interaction
285 // qDebug() << " unlock cursorAccessWaiter";
286 d->cursorAccessLock.lock();
287 d->finishedOneHunk = true;
288 d->cursorAccessWaiter.wakeAll();
289 d->cursorAccessLock.unlock();
292 d->fileAccessWaiter.wakeAll();
293 // qDebug() << "~ChangeSet::lockFile";
296 void ChangeSet::removeFile(const File &file)
298 QMutexLocker ml(&d->fileAccessLock);
299 // TODO move the cursor if this file is the current file.
300 d->files.removeAll(file);
301 d->fileAccessWaiter.wakeAll();
304 void ChangeSet::allHunksFetched()
306 // qDebug() << "ChangeSet::allHunksFetched";
307 QMutexLocker ml(&d->fileAccessLock);
308 d->lockedFile = File();
309 d->fileAccessWaiter.wakeAll();
310 // qDebug() << "~ChangeSet::allHunksFetched";
313 bool ChangeSet::hasAllHunks() const
315 return d->hunksFetcher == 0 || d->hunksFetcher->isFinished();
318 // static
319 QList<File> ChangeSet::readGitDiff(QIODevice &git, File *fileToDiff)
321 QList<File> filesInDiff;
323 // parse the output and create objects for each hunk. (note how that can be made multi-threading)
324 // we have to have the filename in the hunk too to allow skipping a whole hunk
325 char buf[1024];
326 bool inPatch = false;
327 // a diff can be multiple files, or just the one fileToDiff. Lets keep one File object to point to the current file.
328 File file;
329 Hunk hunk;
330 while(true) {
331 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
332 if (lineLength == -1)
333 break;
334 QString line = QString::fromLocal8Bit(buf, lineLength);
336 const bool newfile = line.startsWith("--- /dev/null");
337 if (line.length() > 6 && (newfile || line.startsWith("--- a/"))) {
338 file.addHunk(hunk);
339 hunk = Hunk();
340 if (file.isValid())
341 filesInDiff << file;
342 if (fileToDiff)
343 file = File(*fileToDiff);
344 else {
345 file = File();
346 if (!newfile) {
347 QByteArray array(buf + 6, strlen(buf) - 7);
348 file.setOldFileName(array);
351 inPatch = false;
353 else if (fileToDiff == 0 && line.length() > 6 && line.startsWith("+++ b/")) {
354 QByteArray array(buf + 6, strlen(buf) - 7);
355 file.setFileName(array);
357 else if (line.length() > 5 && line.startsWith("@@ -")) {
358 file.addHunk(hunk);
359 hunk = Hunk();
360 inPatch = true;
362 else if (inPatch && line.startsWith("diff --git "))
363 inPatch = false;
364 else if (line.startsWith("Binary files a/") && line.indexOf(" differ") > 0) { // TODO does git do translations?
365 Q_ASSERT(fileToDiff);
366 fileToDiff->setBinary(true);
367 git.close();
368 return filesInDiff;
370 if (inPatch) {
371 QByteArray array(buf, lineLength);
372 hunk.addLine(array);
375 file.addHunk(hunk);
376 if (fileToDiff == 0 && file.isValid())
377 filesInDiff << file;
378 git.close();
379 return filesInDiff;
382 void ChangeSet::addFile(const File &file)
384 if (file.isValid())
385 d->files << file;
388 int ChangeSet::count() const
390 return d->files.count();
393 void ChangeSet::writeDiff(QIODevice &file, ChangeSet::Selection selection) const
395 waitFinishGenerateHunks();
396 file.open(QIODevice::WriteOnly | QIODevice::Truncate);
397 QDataStream diff(&file);
398 foreach(File file, d->files) {
399 bool fileHeaderWritten = false;
400 foreach(Hunk hunk, file.hunks()) {
401 if (selection == AllHunks
402 || (selection == UserSelection
403 && (hunk.acceptance() == Vng::Accepted || hunk.acceptance() == Vng::MixedAcceptance))
404 || (selection == InvertedUserSelection && hunk.acceptance() != Vng::Accepted)) {
405 if (!fileHeaderWritten) {
406 diff.writeRawData("--- a/", 6);
407 diff.writeRawData(file.oldFileName().data(), file.oldFileName().size());
408 diff.writeRawData("\n+++ b/", 7);
409 diff.writeRawData(file.fileName().data(), file.fileName().size());
410 diff.writeRawData("\n", 1);
411 fileHeaderWritten = true;
413 QByteArray acceptedPatch;
414 if (selection == InvertedUserSelection)
415 acceptedPatch =hunk.rejectedPatch();
416 else if (selection == AllHunks) {
417 acceptedPatch = hunk.header();
418 acceptedPatch.append(hunk.patch());
420 else
421 acceptedPatch = hunk.acceptedPatch();
422 diff.writeRawData(acceptedPatch.data(), acceptedPatch.size());
426 file.close();
429 bool ChangeSet::hasAcceptedChanges() const
431 waitFinishGenerateHunks();
432 foreach(File file, d->files) {
433 if (file.renameAcceptance() == Vng::Accepted && file.fileName() != file.oldFileName())
434 return true;
435 if (file.protectionAcceptance() == Vng::Accepted && file.protection() != file.oldProtection())
436 return true;
437 if (file.isBinary() && file.binaryChangeAcceptance() == Vng::Accepted)
438 return true;
439 foreach(Hunk hunk, file.hunks()) {
440 Vng::Acceptance a = hunk.acceptance();
441 if (a == Vng::Accepted || a == Vng::MixedAcceptance)
442 return true;
445 return false;
448 File ChangeSet::file(int index) const
450 // qDebug() << "ChangeSet::file" << index << d->finishedOneHunk;
451 waitForFinishFirstFile();
452 QMutexLocker ml2(&d->fileAccessLock);
453 while (d->files.count() > index && d->files[index] == d->lockedFile)
454 // { qDebug() << " waiting for file to be unlocked";
455 d->fileAccessWaiter.wait(&d->fileAccessLock);
456 // }
457 if (d->files.count() <= index)
458 return File();
460 // qDebug() << "ChangeSet::~file";
461 return d->files[index];
464 ChangeSet &ChangeSet::operator=(const ChangeSet &other)
466 #if QT_VERSION >= 0x040400
467 other.d->ref.ref();
468 if (!d->ref.deref())
469 #else
470 other.d->ref++;
471 if (--d->ref == 0)
472 #endif
473 delete d;
474 d = other.d;
475 return *this;
478 void ChangeSet::waitFinishGenerateHunks() const
480 if (d->hunksFetcher)
481 d->hunksFetcher->wait();
484 void ChangeSet::waitForFinishFirstFile() const
486 d->cursorAccessLock.lock();
487 if (! d->finishedOneHunk)
488 d->cursorAccessWaiter.wait(&d->cursorAccessLock);
489 d->cursorAccessLock.unlock();