Fix the showing of the total hunk count to happen again at end of checking filesystem.
[vng.git] / hunks / ChangeSet.cpp
blob14c1b52157ada1bb4bf33a6c882c743a0f564f53
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"
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 QAtomicInt ref;
124 bool finishedOneHunk;
127 ChangeSet::ChangeSet()
128 : d(new Private())
132 ChangeSet::~ChangeSet()
134 if (!d->ref.deref())
135 delete d;
138 ChangeSet::ChangeSet(const ChangeSet &other)
139 : d(other.d)
141 d->ref.ref();
144 AbstractCommand::ReturnCodes ChangeSet::fillFromDiffFile(QIODevice &file)
146 file.open(QIODevice::ReadOnly);
148 foreach (File f, readGitDiff(file))
149 addFile(f);
151 if (Logger::verbosity() >= Logger::Debug) {
152 foreach(File f, d->files) {
153 Logger::debug() << "changes in file: " << f.fileName() << endl;
154 int i=0;
155 foreach(Hunk h, f.hunks()) {
156 Logger::debug() << " +-(" << i++ << ") @" << h.lineNumber() << "; " << h.patch().size() << " bytes\n";
157 for(int i = 0; i < h.subHunkCount(); i++) {
158 Logger::debug() << " " << i <<"/"<< h.subHunkCount() <<"; "<< h.subHunk(i).size() <<" bytes\n";
162 Logger::debug().flush();
164 return AbstractCommand::Ok;
167 AbstractCommand::ReturnCodes ChangeSet::fillFromCurrentChanges(const QStringList &paths)
169 d->changeSetOnIndex = true;
170 QDir refs(".git/refs/heads");
171 const bool emptyRepo = refs.count() == 2; // only '.' and '..'
172 if (emptyRepo) { // all files added are new, just add all.
173 QProcess git;
174 QStringList arguments;
175 arguments << "ls-files" << "-s";
176 GitRunner runner(git, arguments);
177 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
178 if (rc)
179 return rc;
180 char buf[1024];
181 while(true) {
182 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
183 if (lineLength == -1)
184 break;
185 File file;
186 file.setProtection(QString::fromAscii(buf, 6));
187 file.setSha1(QString::fromAscii(buf+7, 40));
188 file.setFileName(QByteArray(buf + 50, lineLength - 51));
189 d->files.append(file);
191 return AbstractCommand::Ok;
194 QProcess git;
195 QStringList arguments;
196 arguments << "diff-index" << "-M" << "HEAD";
197 if (! paths.isEmpty())
198 arguments << "--" << paths;
200 // for each line
201 // new file:
202 // :000000 100644 0000000000000000000000000000000000000000 8c3ae1d344f18b23c3bdde5d26658b70b03c65d9 A bar
203 // rename main.cpp => notmain.cpp
204 // :100644 100644 e58cfe72cb7a9559a0090886bea5b0ce00db6b47 477c729e5abbd701eb708df563e7e4f749b50435 R074 main.cpp notmain.cpp
205 // normal change
206 // :100644 100644 0bdd73e9ea0ba026f6796799946c4bfc9dd1b0b8 0000000000000000000000000000000000000000 M test
208 GitRunner runner(git, arguments);
209 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
210 if (rc)
211 return rc;
212 char buf[1024];
213 while(true) {
214 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
215 if (lineLength == -1)
216 break;
217 if (lineLength > 0 && buf[0] != ':') // not a diff line, ignore.
218 continue;
220 File file;
221 file.setOldProtection(QString::fromAscii(buf+1, 6));
222 file.setProtection(QString::fromAscii(buf+8, 6));
223 file.setOldSha1(QString::fromAscii(buf+15, 40));
224 file.setSha1(QString::fromAscii(buf+56, 40));
225 int offset = 98;
226 while (buf[offset] != '\t' && offset < lineLength)
227 offset++;
228 if (buf[97] == 'R') { // rename
229 int tab = offset + 1;
230 while (buf[tab] != '\t' && tab < lineLength)
231 tab++;
232 file.setOldFileName(QByteArray(buf + offset + 1, tab - offset - 1));
233 file.setFileName(QByteArray(buf + tab + 1, lineLength - tab - 2));
235 else {
236 QByteArray filename(buf + offset + 1, lineLength - offset - 2);
237 if (buf[97] != 'A') // Add
238 file.setOldFileName(filename);
239 if (buf[97] != 'D') // Delete
240 file.setFileName(filename);
242 d->files.append(file);
244 generateHunks();
246 return AbstractCommand::Ok;
249 void ChangeSet::generateHunks()
251 Q_ASSERT(d->hunksFetcher == 0);
252 d->hunksFetcher = new HunksFetcher(d->files, *this, d->changeSetOnIndex);
253 d->finishedOneHunk = false;
254 d->hunksFetcher->start();
257 void ChangeSet::lockFile(const File &file)
259 // qDebug() << "ChangeSet::lockFile";
260 QMutexLocker ml(&d->fileAccessLock);
261 d->lockedFile = file;
262 if (d->files.count() == 0 || d->files.at(0) != file) { // as soon as we have done a file, we can start the interaction
263 // qDebug() << " unlock cursorAccessWaiter";
264 d->cursorAccessLock.lock();
265 d->finishedOneHunk = true;
266 d->cursorAccessWaiter.wakeAll();
267 d->cursorAccessLock.unlock();
270 d->fileAccessWaiter.wakeAll();
271 // qDebug() << "~ChangeSet::lockFile";
274 void ChangeSet::removeFile(const File &file)
276 QMutexLocker ml(&d->fileAccessLock);
277 // TODO move the cursor if this file is the current file.
278 d->files.removeAll(file);
279 d->fileAccessWaiter.wakeAll();
282 void ChangeSet::allHunksFetched()
284 // qDebug() << "ChangeSet::allHunksFetched";
285 QMutexLocker ml(&d->fileAccessLock);
286 d->lockedFile = File();
287 d->fileAccessWaiter.wakeAll();
288 // qDebug() << "~ChangeSet::allHunksFetched";
291 bool ChangeSet::hasAllHunks() const
293 return d->hunksFetcher == 0 || d->hunksFetcher->isFinished();
296 // static
297 QList<File> ChangeSet::readGitDiff(QIODevice &git, File *fileToDiff)
299 QList<File> filesInDiff;
301 // parse the output and create objects for each hunk. (note how that can be made multi-threading)
302 // we have to have the filename in the hunk too to allow skipping a whole hunk
303 char buf[1024];
304 bool inPatch = false;
305 // a diff can be multiple files, or just the one fileToDiff. Lets keep one File object to point to the current file.
306 File file;
307 Hunk hunk;
308 while(true) {
309 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
310 if (lineLength == -1)
311 break;
312 QString line = QString::fromLocal8Bit(buf, lineLength);
314 const bool newfile = line.startsWith("--- /dev/null");
315 if (line.length() > 6 && (newfile || line.startsWith("--- a/"))) {
316 file.addHunk(hunk);
317 hunk = Hunk();
318 if (file.isValid())
319 filesInDiff << file;
320 if (fileToDiff)
321 file = File(*fileToDiff);
322 else {
323 file = File();
324 if (!newfile) {
325 QByteArray array(buf + 6, strlen(buf) - 7);
326 file.setOldFileName(array);
329 inPatch = false;
331 else if (fileToDiff == 0 && line.length() > 6 && line.startsWith("+++ b/")) {
332 QByteArray array(buf + 6, strlen(buf) - 7);
333 file.setFileName(array);
335 else if (line.length() > 5 && line.startsWith("@@ -")) {
336 file.addHunk(hunk);
337 hunk = Hunk();
338 inPatch = true;
340 else if (inPatch && line.startsWith("diff --git "))
341 inPatch = false;
342 else if (line.startsWith("Binary files a/") && line.indexOf(" differ") > 0) { // TODO does git do translations?
343 Q_ASSERT(fileToDiff);
344 fileToDiff->setBinary(true);
345 git.close();
346 return filesInDiff;
348 if (inPatch) {
349 QByteArray array(buf, lineLength);
350 hunk.addLine(array);
353 file.addHunk(hunk);
354 if (fileToDiff == 0 && file.isValid())
355 filesInDiff << file;
356 git.close();
357 return filesInDiff;
360 void ChangeSet::addFile(const File &file)
362 if (file.isValid())
363 d->files << file;
366 int ChangeSet::count() const
368 return d->files.count();
371 void ChangeSet::writeDiff(QIODevice &file, ChangeSet::Selection selection) const
373 waitFinishGenerateHunks();
374 file.open(QIODevice::WriteOnly | QIODevice::Truncate);
375 QDataStream diff(&file);
376 foreach(File file, d->files) {
377 bool fileHeaderWritten = false;
378 foreach(Hunk hunk, file.hunks()) {
379 if (selection == AllHunks
380 || selection == UserSelection
381 && (hunk.acceptance() == Vng::Accepted || hunk.acceptance() == Vng::MixedAcceptance)
382 || selection == InvertedUserSelection && hunk.acceptance() != Vng::Accepted) {
383 if (!fileHeaderWritten) {
384 diff.writeRawData("--- a/", 6);
385 diff.writeRawData(file.oldFileName().data(), file.oldFileName().size());
386 diff.writeRawData("\n+++ b/", 7);
387 diff.writeRawData(file.fileName().data(), file.fileName().size());
388 diff.writeRawData("\n", 1);
389 fileHeaderWritten = true;
391 QByteArray acceptedPatch;
392 if (selection == InvertedUserSelection)
393 acceptedPatch =hunk.rejectedPatch();
394 else if (selection == AllHunks) {
395 acceptedPatch = hunk.header();
396 acceptedPatch.append(hunk.patch());
398 else
399 acceptedPatch = hunk.acceptedPatch();
400 diff.writeRawData(acceptedPatch.data(), acceptedPatch.size());
404 file.close();
407 bool ChangeSet::hasAcceptedChanges() const
409 waitFinishGenerateHunks();
410 foreach(File file, d->files) {
411 if (file.renameAcceptance() == Vng::Accepted && file.fileName() != file.oldFileName())
412 return true;
413 if (file.protectionAcceptance() == Vng::Accepted && file.protection() != file.oldProtection())
414 return true;
415 foreach(Hunk hunk, file.hunks()) {
416 Vng::Acceptance a = hunk.acceptance();
417 if (a == Vng::Accepted || a == Vng::MixedAcceptance)
418 return true;
421 return false;
424 File ChangeSet::file(int index) const
426 // qDebug() << "ChangeSet::file" << index << d->finishedOneHunk;
427 waitForFinishFirstFile();
428 QMutexLocker ml2(&d->fileAccessLock);
429 while (d->files[index] == d->lockedFile)
430 // { qDebug() << " waiting for file to be unlocked";
431 d->fileAccessWaiter.wait(&d->fileAccessLock);
432 // }
433 if (d->files.count() == 0)
434 return File();
436 // qDebug() << "ChangeSet::~file";
437 return d->files[index];
440 ChangeSet &ChangeSet::operator=(const ChangeSet &other)
442 other.d->ref.ref();
443 if (!d->ref.deref())
444 delete d;
445 d = other.d;
446 return *this;
449 void ChangeSet::waitFinishGenerateHunks() const
451 if (d->hunksFetcher)
452 d->hunksFetcher->wait();
455 void ChangeSet::waitForFinishFirstFile() const
457 d->cursorAccessLock.lock();
458 if (! d->finishedOneHunk)
459 d->cursorAccessWaiter.wait(&d->cursorAccessLock);
460 d->cursorAccessLock.unlock();