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"
27 #include <QMutexLocker>
29 #include <QWaitCondition>
32 class HunksFetcher
: public QThread
35 HunksFetcher(const QList
<File
> &files
, ChangeSet
&changeSet
, bool changeSetOnIndex
)
37 m_changeSet(changeSet
),
38 m_changeSetOnIndex(changeSetOnIndex
),
45 setPriority(QThread::LowPriority
);
46 foreach(File file
, m_files
) {
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
);
58 QStringList arguments
;
59 arguments
<< "update-index" << "-q" << "--refresh" << file
.fileName();
60 GitRunner
runner(git
, arguments
);
61 runner
.start(GitRunner::WaitUntilFinished
);
64 if (file
.fileName().isEmpty() || file
.oldFileName().isEmpty())
65 file
.setProtectionAcceptance(Vng::Accepted
);
68 if (Logger::verbosity() >= Logger::Debug
) {
69 Logger::debug() << "file: `" << file
.oldFileName() << "' => `" << file
.fileName() << "'\n";
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();
92 ChangeSet
&m_changeSet
;
93 bool m_changeSetOnIndex
, m_interrupted
;
96 class ChangeSet::Private
100 changeSetOnIndex(false),
103 finishedOneHunk(true)
109 hunksFetcher
->interrupt();
110 hunksFetcher
->wait();
116 bool changeSetOnIndex
; // the changesSet shows the changes of the working dir
117 QMutex fileAccessLock
;
118 QWaitCondition fileAccessWaiter
;
119 QWaitCondition cursorAccessWaiter
;
120 QMutex cursorAccessLock
;
122 HunksFetcher
*hunksFetcher
;
123 #if QT_VERSION >= 0x040400
128 bool finishedOneHunk
;
131 ChangeSet::ChangeSet()
136 ChangeSet::~ChangeSet()
138 #if QT_VERSION >= 0x040400
146 ChangeSet::ChangeSet(const ChangeSet
&other
)
149 #if QT_VERSION >= 0x040400
156 AbstractCommand::ReturnCodes
ChangeSet::fillFromDiffFile(QIODevice
&file
)
158 file
.open(QIODevice::ReadOnly
);
160 foreach (File f
, readGitDiff(file
))
163 if (Logger::verbosity() >= Logger::Debug
) {
164 foreach(File f
, d
->files
) {
165 Logger::debug() << "changes in file: " << f
.fileName() << endl
;
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.
186 QStringList arguments
;
187 arguments
<< "ls-files" << "-s";
188 GitRunner
runner(git
, arguments
);
189 AbstractCommand::ReturnCodes rc
= runner
.start(GitRunner::WaitForStandardOutput
);
194 qint64 lineLength
= Vng::readLine(&git
, buf
, sizeof(buf
));
195 if (lineLength
== -1)
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.
208 QStringList arguments
;
209 arguments
<< "diff-index" << "-M" << "HEAD";
210 if (! paths
.isEmpty())
211 arguments
<< "--" << paths
;
215 // :000000 100644 0000000000000000000000000000000000000000 8c3ae1d344f18b23c3bdde5d26658b70b03c65d9 A bar
216 // rename main.cpp => notmain.cpp
217 // :100644 100644 e58cfe72cb7a9559a0090886bea5b0ce00db6b47 477c729e5abbd701eb708df563e7e4f749b50435 R074 main.cpp notmain.cpp
219 // :100644 100644 0bdd73e9ea0ba026f6796799946c4bfc9dd1b0b8 0000000000000000000000000000000000000000 M test
221 GitRunner
runner(git
, arguments
);
222 AbstractCommand::ReturnCodes rc
= runner
.start(GitRunner::WaitForStandardOutput
);
227 qint64 lineLength
= Vng::readLine(&git
, buf
, sizeof(buf
));
228 if (lineLength
== -1)
230 if (lineLength
> 0 && buf
[0] != ':') // not a diff line, ignore.
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));
239 while (buf
[offset
] != '\t' && offset
< lineLength
)
241 if (buf
[97] == 'R') { // rename
242 int tab
= offset
+ 1;
243 while (buf
[tab
] != '\t' && tab
< lineLength
)
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
)
252 QByteArray
filename(buf
+ offset
+ 1, tab
- offset
- 1);
253 file
.setOldFileName(filename
);
254 file
.setFileName(filename
);
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
);
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();
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
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.
331 qint64 lineLength
= Vng::readLine(&git
, buf
, sizeof(buf
));
332 if (lineLength
== -1)
334 QString line
= QString::fromLocal8Bit(buf
, lineLength
);
336 const bool newfile
= line
.startsWith("--- /dev/null");
337 if (line
.length() > 6 && (newfile
|| line
.startsWith("--- a/"))) {
343 file
= File(*fileToDiff
);
347 QByteArray
array(buf
+ 6, strlen(buf
) - 7);
348 file
.setOldFileName(array
);
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("@@ -")) {
362 else if (inPatch
&& line
.startsWith("diff --git "))
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);
371 QByteArray
array(buf
, lineLength
);
376 if (fileToDiff
== 0 && file
.isValid())
382 void ChangeSet::addFile(const File
&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());
421 acceptedPatch
= hunk
.acceptedPatch();
422 diff
.writeRawData(acceptedPatch
.data(), acceptedPatch
.size());
429 bool ChangeSet::hasAcceptedChanges() const
431 waitFinishGenerateHunks();
432 foreach(File file
, d
->files
) {
433 if (file
.renameAcceptance() == Vng::Accepted
&& file
.fileName() != file
.oldFileName())
435 if (file
.protectionAcceptance() == Vng::Accepted
&& file
.protection() != file
.oldProtection())
437 if (file
.isBinary() && file
.binaryChangeAcceptance() == Vng::Accepted
)
439 foreach(Hunk hunk
, file
.hunks()) {
440 Vng::Acceptance a
= hunk
.acceptance();
441 if (a
== Vng::Accepted
|| a
== Vng::MixedAcceptance
)
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
);
457 if (d
->files
.count() <= index
)
460 // qDebug() << "ChangeSet::~file";
461 return d
->files
[index
];
464 ChangeSet
&ChangeSet::operator=(const ChangeSet
&other
)
466 #if QT_VERSION >= 0x040400
478 void ChangeSet::waitFinishGenerateHunks() const
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();