Move the code to allow diffs to be fetched multithreaded
[vng.git] / hunks / ChangeSet.cpp
blob2e09cd54e6331d45833097035b2c1f62ac4b4cce
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>
31 class HunksFetcher : public QThread
33 public:
34 HunksFetcher(const QList<File> &files, ChangeSet &changeSet, bool changeSetOnIndex)
35 : m_files(files),
36 m_changeSet(changeSet),
37 m_changeSetOnIndex(changeSetOnIndex)
41 void run()
43 setPriority(QThread::LowPriority);
44 foreach(File file, m_files) {
45 m_changeSet.lockFile(file);
47 file.fetchHunks(m_changeSetOnIndex);
49 if (!file.isBinary() && file.count() == 0 && !file.hasChanged()) { // No change in file at all.
50 Logger::debug() << "file: `" << file.oldFileName() << "' => `" << file.fileName() << "'\n";
51 Logger::debug() << " +- Unchanged file, skipping\n";
52 //m_changeSet.removFile(file); // TODO
53 QProcess git;
54 QStringList arguments;
55 arguments << "update-index" << "-q" << "--refresh" << file.fileName();
56 GitRunner runner(git, arguments);
57 runner.start(GitRunner::WaitUntilFinished);
58 continue;
60 if (file.fileName().isEmpty() || file.oldFileName().isEmpty())
61 file.setProtectionAcceptance(Vng::Accepted);
63 int i=0;
64 if (Logger::verbosity() >= Logger::Debug) {
65 Logger::debug() << "file: `" << file.oldFileName() << "' => `" << file.fileName() << "'\n";
66 if (file.isBinary())
67 Logger::debug() << " +- is a binary file" << endl;
68 Logger::debug() << " +- " << file.oldProtection() << " => " << file.protection() << endl;
69 foreach(Hunk h, file.hunks()) {
70 Logger::debug() << " +-(" << i++ << ") @" << h.lineNumber() << "; " << h.patch().size() << " bytes\n";
71 for(int i = 0; i < h.subHunkCount(); i++) {
72 Logger::debug() << " " << i <<"/"<< h.subHunkCount() <<"; "<< h.subHunk(i).size() <<" bytes\n";
77 m_changeSet.allHunksFetched();
79 // how do we make it be deleted ?...
82 private:
83 QList<File> m_files;
84 ChangeSet &m_changeSet;
85 bool m_changeSetOnIndex;
88 class ChangeSet::Private
90 public:
91 Private() :
92 changeSetOnIndex(false),
93 hunksFetcher(0),
94 ref(1),
95 finishedOneHunk(false)
99 ~Private() {
100 delete hunksFetcher;
103 QList<File> files;
104 bool changeSetOnIndex; // the changesSet shows the changes of the working dir
105 QMutex fileAccessLock;
106 QWaitCondition fileAccessWaiter;
107 QWaitCondition cursorAccessWaiter;
108 QMutex cursorAccessLock;
109 File lockedFile;
110 QThread *hunksFetcher;
111 int ref;
112 bool finishedOneHunk;
115 ChangeSet::ChangeSet()
116 : d(new Private())
120 ChangeSet::~ChangeSet()
122 if (--d->ref == 0)
123 delete d;
126 AbstractCommand::ReturnCodes ChangeSet::fillFromDiffFile(QIODevice &file)
128 file.open(QIODevice::ReadOnly);
130 foreach (File f, readGitDiff(file))
131 addFile(f);
132 d->finishedOneHunk = true;
134 if (Logger::verbosity() >= Logger::Debug) {
135 foreach(File f, d->files) {
136 Logger::debug() << "changes in file: " << f.fileName() << endl;
137 int i=0;
138 foreach(Hunk h, f.hunks()) {
139 Logger::debug() << " +-(" << i++ << ") @" << h.lineNumber() << "; " << h.patch().size() << " bytes\n";
140 for(int i = 0; i < h.subHunkCount(); i++) {
141 Logger::debug() << " " << i <<"/"<< h.subHunkCount() <<"; "<< h.subHunk(i).size() <<" bytes\n";
145 Logger::debug().flush();
147 return AbstractCommand::Ok;
150 AbstractCommand::ReturnCodes ChangeSet::fillFromCurrentChanges(const QStringList &paths)
152 d->changeSetOnIndex = true;
153 QDir refs(".git/refs/heads");
154 const bool emptyRepo = refs.count() == 2; // only '.' and '..'
155 if (emptyRepo) { // all files added are new, just add all.
156 QProcess git;
157 QStringList arguments;
158 arguments << "ls-files" << "-s";
159 GitRunner runner(git, arguments);
160 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
161 if (rc)
162 return rc;
163 char buf[1024];
164 while(true) {
165 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
166 if (lineLength == -1)
167 break;
168 File file;
169 file.setProtection(QString::fromAscii(buf, 6));
170 file.setSha1(QString::fromAscii(buf+7, 40));
171 file.setFileName(QByteArray(buf + 50, lineLength - 51));
172 d->files.append(file);
174 return AbstractCommand::Ok;
177 QProcess git;
178 QStringList arguments;
179 arguments << "diff-index" << "-M" << "HEAD";
180 if (! paths.isEmpty())
181 arguments << "--" << paths;
183 // for each line
184 // new file:
185 // :000000 100644 0000000000000000000000000000000000000000 8c3ae1d344f18b23c3bdde5d26658b70b03c65d9 A bar
186 // rename main.cpp => notmain.cpp
187 // :100644 100644 e58cfe72cb7a9559a0090886bea5b0ce00db6b47 477c729e5abbd701eb708df563e7e4f749b50435 R074 main.cpp notmain.cpp
188 // normal change
189 // :100644 100644 0bdd73e9ea0ba026f6796799946c4bfc9dd1b0b8 0000000000000000000000000000000000000000 M test
191 GitRunner runner(git, arguments);
192 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
193 if (rc)
194 return rc;
195 char buf[1024];
196 while(true) {
197 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
198 if (lineLength == -1)
199 break;
200 if (lineLength > 0 && buf[0] != ':') // not a diff line, ignore.
201 continue;
203 File file;
204 file.setOldProtection(QString::fromAscii(buf+1, 6));
205 file.setProtection(QString::fromAscii(buf+8, 6));
206 file.setOldSha1(QString::fromAscii(buf+15, 40));
207 file.setSha1(QString::fromAscii(buf+56, 40));
208 int offset = 98;
209 while (buf[offset] != '\t' && offset < lineLength)
210 offset++;
211 if (buf[97] == 'R') { // rename
212 int tab = offset + 1;
213 while (buf[tab] != '\t' && tab < lineLength)
214 tab++;
215 file.setOldFileName(QByteArray(buf + offset + 1, tab - offset - 1));
216 file.setFileName(QByteArray(buf + tab + 1, lineLength - tab - 2));
218 else {
219 QByteArray filename(buf + offset + 1, lineLength - offset - 2);
220 if (buf[97] != 'A') // Add
221 file.setOldFileName(filename);
222 if (buf[97] != 'D') // Delete
223 file.setFileName(filename);
225 d->files.append(file);
227 generateHunks();
229 return AbstractCommand::Ok;
232 void ChangeSet::generateHunks()
234 Q_ASSERT(d->hunksFetcher == 0);
235 d->hunksFetcher = new HunksFetcher(d->files, *this, d->changeSetOnIndex);
236 d->hunksFetcher->start();
239 void ChangeSet::lockFile(const File &file)
241 QMutexLocker ml(&d->fileAccessLock);
242 d->lockedFile = file;
243 if (d->files.count() == 0 || d->files.at(1) == file) { // as soon as we have done a file, we can start the interaction
244 d->cursorAccessLock.lock();
245 d->finishedOneHunk = true;
246 d->cursorAccessWaiter.wakeAll();
247 d->cursorAccessLock.unlock();
250 d->fileAccessWaiter.wakeAll();
253 void ChangeSet::removeFile(const File &file)
255 QMutexLocker ml(&d->fileAccessLock);
256 // TODO move the cursor if this file is the current file.
257 d->files.removeAll(file);
258 d->fileAccessWaiter.wakeAll();
261 void ChangeSet::allHunksFetched()
263 QMutexLocker ml(&d->fileAccessLock);
264 d->lockedFile = File();
265 d->fileAccessWaiter.wakeAll();
266 d->hunksFetcher->deleteLater();
267 d->hunksFetcher = 0;
268 // TODO emit something to show to the cursor we have all the changes??
271 bool ChangeSet::hasAllHunks() const
273 return d->hunksFetcher == 0 && d->finishedOneHunk;
276 // static
277 QList<File> ChangeSet::readGitDiff(QIODevice &git, File *fileToDiff)
279 QList<File> filesInDiff;
281 // parse the output and create objects for each hunk. (note how that can be made multi-threading)
282 // we have to have the filename in the hunk too to allow skipping a whole hunk
283 char buf[1024];
284 bool inPatch = false;
285 // a diff can be multiple files, or just the one fileToDiff. Lets keep one File object to point to the current file.
286 File file;
287 Hunk hunk;
288 while(true) {
289 qint64 lineLength = Vng::readLine(&git, buf, sizeof(buf));
290 if (lineLength == -1)
291 break;
292 QString line = QString::fromLocal8Bit(buf, lineLength);
294 const bool newfile = line.startsWith("--- /dev/null");
295 if (line.length() > 6 && (newfile || line.startsWith("--- a/"))) {
296 file.addHunk(hunk);
297 hunk = Hunk();
298 if (file.isValid())
299 filesInDiff << file;
300 if (fileToDiff)
301 file = File(*fileToDiff);
302 else {
303 file = File();
304 if (!newfile) {
305 QByteArray array(buf + 6, strlen(buf) - 7);
306 file.setOldFileName(array);
309 inPatch = false;
311 else if (fileToDiff == 0 && line.length() > 6 && line.startsWith("+++ b/")) {
312 QByteArray array(buf + 6, strlen(buf) - 7);
313 file.setFileName(array);
315 else if (line.length() > 5 && line.startsWith("@@ -")) {
316 file.addHunk(hunk);
317 hunk = Hunk();
318 inPatch = true;
320 else if (inPatch && line.startsWith("diff --git "))
321 inPatch = false;
322 else if (line.startsWith("Binary files a/") && line.indexOf(" differ") > 0) { // TODO does git do translations?
323 Q_ASSERT(fileToDiff);
324 fileToDiff->setBinary(true);
325 git.close();
326 return filesInDiff;
328 if (inPatch) {
329 QByteArray array(buf, lineLength);
330 hunk.addLine(array);
333 file.addHunk(hunk);
334 if (fileToDiff == 0 && file.isValid())
335 filesInDiff << file;
336 git.close();
337 return filesInDiff;
340 void ChangeSet::addFile(const File &file)
342 if (file.isValid())
343 d->files << file;
346 int ChangeSet::count() const
348 return d->files.count();
351 void ChangeSet::writeDiff(QIODevice &file, ChangeSet::Selection selection) const
353 file.open(QIODevice::WriteOnly | QIODevice::Truncate);
354 QDataStream diff(&file);
355 foreach(File file, d->files) {
356 bool fileHeaderWritten = false;
357 foreach(Hunk hunk, file.hunks()) {
358 if (selection == AllHunks
359 || selection == UserSelection
360 && (hunk.acceptance() == Vng::Accepted || hunk.acceptance() == Vng::MixedAcceptance)
361 || selection == InvertedUserSelection && hunk.acceptance() != Vng::Accepted) {
362 if (!fileHeaderWritten) {
363 diff.writeRawData("--- a/", 6);
364 diff.writeRawData(file.oldFileName().data(), file.oldFileName().size());
365 diff.writeRawData("\n+++ b/", 7);
366 diff.writeRawData(file.fileName().data(), file.fileName().size());
367 diff.writeRawData("\n", 1);
368 fileHeaderWritten = true;
370 QByteArray acceptedPatch;
371 if (selection == InvertedUserSelection)
372 acceptedPatch =hunk.rejectedPatch();
373 else if (selection == AllHunks) {
374 acceptedPatch = hunk.header();
375 acceptedPatch.append(hunk.patch());
377 else
378 acceptedPatch = hunk.acceptedPatch();
379 diff.writeRawData(acceptedPatch.data(), acceptedPatch.size());
383 file.close();
386 bool ChangeSet::hasAcceptedChanges() const
388 foreach(File file, d->files) {
389 if (file.renameAcceptance() == Vng::Accepted && file.fileName() != file.oldFileName())
390 return true;
391 if (file.protectionAcceptance() == Vng::Accepted && file.protection() != file.oldProtection())
392 return true;
393 foreach(Hunk hunk, file.hunks()) {
394 Vng::Acceptance a = hunk.acceptance();
395 if (a == Vng::Accepted || a == Vng::MixedAcceptance)
396 return true;
399 return false;
402 File ChangeSet::file(int index) const
404 QMutexLocker ml(&d->cursorAccessLock);
405 if (! d->finishedOneHunk)
406 d->cursorAccessWaiter.wait(&d->cursorAccessLock);
407 QMutexLocker ml2(&d->fileAccessLock);
408 while (d->files[index] == d->lockedFile)
409 d->fileAccessWaiter.wait(&d->fileAccessLock);
411 return d->files[index];
414 ChangeSet &ChangeSet::operator=(const ChangeSet &other)
416 other.d->ref++;
417 if (--d->ref == 0)
418 delete d;
419 d = other.d;
420 return *this;