1#include "FileDiffWidget.h"
2
3#include <CheckBox.h>
4#include <CommitInfo.h>
5#include <DiffHelper.h>
6#include <FileDiffView.h>
7#include <FileEditor.h>
8#include <GitBase.h>
9#include <GitCache.h>
10#include <GitHistory.h>
11#include <GitLocal.h>
12#include <GitPatches.h>
13#include <GitQlientSettings.h>
14#include <LineNumberArea.h>
15
16#include <QDateTime>
17#include <QDir>
18#include <QHBoxLayout>
19#include <QLabel>
20#include <QLineEdit>
21#include <QMessageBox>
22#include <QPushButton>
23#include <QScrollBar>
24#include <QStackedWidget>
25#include <QTemporaryFile>
26
27FileDiffWidget::FileDiffWidget(const QSharedPointer<GitBase> &git, QSharedPointer<GitCache> cache, QWidget *parent)
28 : IDiffWidget(git, cache, parent)
29 , mBack(new QPushButton())
30 , mGoPrevious(new QPushButton())
31 , mGoNext(new QPushButton())
32 , mEdition(new QPushButton())
33 , mFullView(new QPushButton())
34 , mSplitView(new QPushButton())
35 , mSave(new QPushButton())
36 , mStage(new QPushButton())
37 , mRevert(new QPushButton())
38 , mFileNameLabel(new QLabel())
39 , mTitleFrame(new QFrame())
40 , mNewFile(new FileDiffView())
41 , mSearchOld(new QLineEdit())
42 , mOldFile(new FileDiffView())
43 , mFileEditor(new FileEditor())
44 , mViewStackedWidget(new QStackedWidget())
45{
46 mNewFile->addNumberArea(new LineNumberArea(mNewFile));
47 mOldFile->addNumberArea(new LineNumberArea(mOldFile));
48
49 mNewFile->setObjectName("newFile");
50 mOldFile->setObjectName("oldFile");
51
52 const auto optionsLayout = new QHBoxLayout();
53 optionsLayout->setContentsMargins(5, 5, 0, 0);
54 optionsLayout->setSpacing(5);
55 optionsLayout->addWidget(mBack);
56 optionsLayout->addWidget(mGoPrevious);
57 optionsLayout->addWidget(mGoNext);
58 optionsLayout->addWidget(mFullView);
59 optionsLayout->addWidget(mSplitView);
60 optionsLayout->addWidget(mEdition);
61 optionsLayout->addWidget(mSave);
62 optionsLayout->addWidget(mStage);
63 optionsLayout->addWidget(mRevert);
64 optionsLayout->addStretch();
65
66 const auto searchNew = new QLineEdit();
67 searchNew->setObjectName("SearchInput");
68 searchNew->setPlaceholderText(tr("Press Enter to search a text... "));
69 connect(searchNew, &QLineEdit::editingFinished, this,
70 [this, searchNew]() { DiffHelper::findString(searchNew->text(), mNewFile, this); });
71
72 const auto newFileLayout = new QVBoxLayout();
73 newFileLayout->setContentsMargins(QMargins());
74 newFileLayout->setSpacing(5);
75 newFileLayout->addWidget(searchNew);
76 newFileLayout->addWidget(mNewFile);
77
78 mSearchOld->setPlaceholderText(tr("Press Enter to search a text... "));
79 mSearchOld->setObjectName("SearchInput");
80 connect(mSearchOld, &QLineEdit::editingFinished, this,
81 [this]() { DiffHelper::findString(mSearchOld->text(), mNewFile, this); });
82
83 const auto oldFileLayout = new QVBoxLayout();
84 oldFileLayout->setContentsMargins(QMargins());
85 oldFileLayout->setSpacing(5);
86 oldFileLayout->addWidget(mSearchOld);
87 oldFileLayout->addWidget(mOldFile);
88
89 const auto diffLayout = new QHBoxLayout();
90 diffLayout->setContentsMargins(10, 0, 10, 0);
91 diffLayout->addLayout(newFileLayout);
92 diffLayout->addLayout(oldFileLayout);
93
94 const auto diffFrame = new QFrame();
95 diffFrame->setLayout(diffLayout);
96
97 mViewStackedWidget->addWidget(diffFrame);
98 mViewStackedWidget->addWidget(mFileEditor);
99
100 mTitleFrame->setVisible(false);
101
102 const auto titleLayout = new QHBoxLayout(mTitleFrame);
103 titleLayout->setContentsMargins(0, 10, 0, 10);
104 titleLayout->setSpacing(0);
105 titleLayout->addStretch();
106 titleLayout->addWidget(mFileNameLabel);
107 titleLayout->addStretch();
108
109 const auto vLayout = new QVBoxLayout(this);
110 vLayout->setContentsMargins(QMargins());
111 vLayout->setSpacing(5);
112 vLayout->addWidget(mTitleFrame);
113 vLayout->addLayout(optionsLayout);
114 vLayout->addWidget(mViewStackedWidget);
115
116 GitQlientSettings settings(mGit->getGitDir());
117 mFileVsFile = settings.localValue(GitQlientSettings::SplitFileDiffView, false).toBool();
118
119 mBack->setIcon(QIcon(":/icons/back"));
120 mBack->setToolTip(tr("Return to the view"));
121 connect(mBack, &QPushButton::clicked, this, &FileDiffWidget::exitRequested);
122
123 mGoPrevious->setIcon(QIcon(":/icons/arrow_up"));
124 mGoPrevious->setToolTip(tr("Previous change"));
125 connect(mGoPrevious, &QPushButton::clicked, this, &FileDiffWidget::moveChunkUp);
126
127 mGoNext->setToolTip(tr("Next change"));
128 mGoNext->setIcon(QIcon(":/icons/arrow_down"));
129 connect(mGoNext, &QPushButton::clicked, this, &FileDiffWidget::moveChunkDown);
130
131 mEdition->setIcon(QIcon(":/icons/edit"));
132 mEdition->setCheckable(true);
133 mEdition->setToolTip(tr("Edit file"));
134 connect(mEdition, &QPushButton::toggled, this, &FileDiffWidget::enterEditionMode);
135
136 mFullView->setIcon(QIcon(":/icons/text-file"));
137 mFullView->setCheckable(true);
138 mFullView->setToolTip(tr("Full file view"));
139 connect(mFullView, &QPushButton::toggled, this, &FileDiffWidget::setFullViewEnabled);
140
141 mSplitView->setIcon(QIcon(":/icons/split_view"));
142 mSplitView->setCheckable(true);
143 mSplitView->setToolTip(tr("Split file view"));
144 connect(mSplitView, &QPushButton::toggled, this, &FileDiffWidget::setSplitViewEnabled);
145
146 mSave->setIcon(QIcon(":/icons/save"));
147 mSave->setDisabled(true);
148 mSave->setToolTip(tr("Save"));
149 connect(mSave, &QPushButton::clicked, mFileEditor, &FileEditor::saveFile);
150 connect(mSave, &QPushButton::clicked, mEdition, &QPushButton::toggle);
151
152 mStage->setIcon(QIcon(":/icons/staged"));
153 mStage->setToolTip(tr("Stage file"));
154 connect(mStage, &QPushButton::clicked, this, &FileDiffWidget::stageFile);
155
156 mRevert->setIcon(QIcon(":/icons/close"));
157 mRevert->setToolTip(tr("Revert changes"));
158 connect(mRevert, &QPushButton::clicked, this, &FileDiffWidget::revertFile);
159
160 mViewStackedWidget->setCurrentIndex(0);
161
162 if (!mFileVsFile)
163 {
164 mOldFile->setHidden(true);
165 mSearchOld->setHidden(true);
166 }
167
168 connect(mNewFile, &FileDiffView::signalScrollChanged, mOldFile, &FileDiffView::moveScrollBarToPos);
169 connect(mNewFile, &FileDiffView::signalStageChunk, this, &FileDiffWidget::stageChunk);
170 connect(mOldFile, &FileDiffView::signalScrollChanged, mNewFile, &FileDiffView::moveScrollBarToPos);
171 connect(mOldFile, &FileDiffView::signalStageChunk, this, &FileDiffWidget::stageChunk);
172
173 setAttribute(Qt::WA_DeleteOnClose);
174}
175
176void FileDiffWidget::clear()
177{
178 mNewFile->clear();
179}
180
181bool FileDiffWidget::reload()
182{
183 if (mCurrentSha == CommitInfo::ZERO_SHA)
184 return configure(mCurrentSha, mPreviousSha, mCurrentFile, mIsCached, mEdition->isChecked());
185
186 return false;
187}
188
189bool FileDiffWidget::configure(const QString &currentSha, const QString &previousSha, const QString &file,
190 bool isCached, bool editMode)
191{
192 auto destFile = file;
193
194 if (destFile.contains("-->"))
195 destFile = destFile.split("--> ").last().split("(").first().trimmed();
196
197 QString text;
198 QScopedPointer<GitHistory> git(new GitHistory(mGit));
199
200 if (const auto ret
201 = git->getFileDiff(currentSha == CommitInfo::ZERO_SHA ? QString() : currentSha, previousSha, destFile, isCached);
202 ret.success)
203 {
204 text = ret.output;
205
206 if (text.isEmpty())
207 {
208 if (const auto ret = git->getUntrackedFileDiff(destFile); ret.success)
209 text = ret.output;
210 }
211
212 if (text.startsWith("* "))
213 return false;
214 }
215
216 mFileNameLabel->setText(file);
217
218 const auto isWip = currentSha == CommitInfo::ZERO_SHA;
219 mBack->setVisible(isWip);
220 mEdition->setVisible(isWip);
221 mSave->setVisible(isWip);
222 mStage->setVisible(isWip);
223 mRevert->setVisible(isWip);
224 mTitleFrame->setVisible(isWip);
225
226 mIsCached = isCached;
227 mCurrentFile = file;
228 mCurrentSha = currentSha;
229 mPreviousSha = previousSha;
230
231 auto pos = 0;
232 for (auto i = 0; i < 5; ++i)
233 pos = text.indexOf("\n", pos + 1);
234
235 text = text.mid(pos + 1);
236
237 if (!text.isEmpty())
238 {
239 if (mFileVsFile)
240 {
241 QPair<QStringList, QVector<ChunkDiffInfo::ChunkInfo>> oldData;
242 QPair<QStringList, QVector<ChunkDiffInfo::ChunkInfo>> newData;
243
244 mChunks = DiffHelper::processDiff(text, newData, oldData);
245
246 mOldFile->blockSignals(true);
247 mOldFile->loadDiff(oldData.first.join('\n'), oldData.second);
248 mOldFile->blockSignals(false);
249
250 mNewFile->blockSignals(true);
251 mNewFile->loadDiff(newData.first.join('\n'), newData.second);
252 mNewFile->blockSignals(false);
253 }
254 else
255 {
256 mNewFile->blockSignals(true);
257 mNewFile->loadDiff(text, {});
258 mNewFile->blockSignals(false);
259 }
260
261 if (editMode)
262 {
263 mEdition->setChecked(true);
264 mSave->setEnabled(true);
265 }
266 else
267 {
268 mEdition->setChecked(false);
269 mSave->setDisabled(true);
270 mFullView->setChecked(!mFileVsFile);
271 mSplitView->setChecked(mFileVsFile);
272 }
273
274 return true;
275 }
276
277 return false;
278}
279
280void FileDiffWidget::setSplitViewEnabled(bool enable)
281{
282 mFileVsFile = enable;
283
284 mOldFile->setVisible(mFileVsFile);
285 mSearchOld->setVisible(mFileVsFile);
286
287 GitQlientSettings settings(mGit->getGitDir());
288 settings.setLocalValue(GitQlientSettings::SplitFileDiffView, mFileVsFile);
289
290 configure(mCurrentSha, mPreviousSha, mCurrentFile, mIsCached);
291
292 mFullView->blockSignals(true);
293 mFullView->setChecked(!mFileVsFile);
294 mFullView->blockSignals(false);
295
296 mGoNext->setEnabled(true);
297 mGoPrevious->setEnabled(true);
298
299 if (enable)
300 {
301 mSave->setDisabled(true);
302 mEdition->blockSignals(true);
303 mEdition->setChecked(false);
304 mEdition->blockSignals(false);
305 endEditFile();
306 }
307}
308
309void FileDiffWidget::setFullViewEnabled(bool enable)
310{
311 mFileVsFile = !enable;
312
313 mOldFile->setVisible(mFileVsFile);
314 mSearchOld->setVisible(mFileVsFile);
315
316 GitQlientSettings settings(mGit->getGitDir());
317 settings.setLocalValue(GitQlientSettings::SplitFileDiffView, mFileVsFile);
318
319 configure(mCurrentSha, mPreviousSha, mCurrentFile, mIsCached);
320
321 mSplitView->blockSignals(true);
322 mSplitView->setChecked(mFileVsFile);
323 mSplitView->blockSignals(false);
324
325 mGoNext->setDisabled(true);
326 mGoPrevious->setDisabled(true);
327
328 if (enable)
329 {
330 mSave->setDisabled(true);
331 mEdition->blockSignals(true);
332 mEdition->setChecked(false);
333 mEdition->blockSignals(false);
334 endEditFile();
335 }
336}
337
338void FileDiffWidget::hideBackButton() const
339{
340 mBack->setVisible(true);
341}
342
343void FileDiffWidget::moveChunkUp()
344{
345 for (auto i = mChunks.chunks.count() - 1; i >= 0; --i)
346 {
347 auto &chunk = mChunks.chunks.at(i);
348
349 if (auto [chunkNewStart, chunkOldStart] = std::make_tuple(chunk.newFile.startLine, chunk.oldFile.startLine);
350 chunkNewStart < mCurrentChunkLine || chunkOldStart < mCurrentChunkLine)
351 {
352 if (chunkNewStart < mCurrentChunkLine)
353 mCurrentChunkLine = chunkNewStart;
354 else if (chunkOldStart < mCurrentChunkLine)
355 mCurrentChunkLine = chunkOldStart;
356
357 mNewFile->moveScrollBarToPos(mCurrentChunkLine - 1);
358 mOldFile->moveScrollBarToPos(mCurrentChunkLine - 1);
359
360 break;
361 }
362 }
363}
364
365void FileDiffWidget::moveChunkDown()
366{
367 const auto endIter = mChunks.chunks.constEnd();
368 auto iter = mChunks.chunks.constBegin();
369
370 for (; iter != endIter; ++iter)
371 {
372 if (iter->newFile.startLine > mCurrentChunkLine)
373 {
374 mCurrentChunkLine = iter->newFile.startLine;
375 break;
376 }
377 else if (iter->oldFile.startLine > mCurrentChunkLine)
378 {
379 mCurrentChunkLine = iter->oldFile.startLine;
380 break;
381 }
382 }
383
384 if (iter != endIter)
385 {
386 mNewFile->moveScrollBarToPos(mCurrentChunkLine - 1);
387 mOldFile->moveScrollBarToPos(mCurrentChunkLine - 1);
388 }
389}
390
391void FileDiffWidget::enterEditionMode(bool enter)
392{
393 if (enter)
394 {
395 mSave->setEnabled(true);
396 mSplitView->blockSignals(true);
397 mSplitView->setChecked(!enter);
398 mSplitView->blockSignals(false);
399
400 mFullView->blockSignals(true);
401 mFullView->setChecked(!enter);
402 mFullView->blockSignals(false);
403
404 mFileEditor->editFile(mCurrentFile);
405 mViewStackedWidget->setCurrentIndex(1);
406 }
407 else if (mFileVsFile)
408 setSplitViewEnabled(true);
409 else
410 setFullViewEnabled(true);
411}
412
413void FileDiffWidget::endEditFile()
414{
415 mViewStackedWidget->setCurrentIndex(0);
416}
417
418void FileDiffWidget::stageFile()
419{
420 QScopedPointer<GitLocal> git(new GitLocal(mGit));
421 const auto ret = git->stageFile(mCurrentFile);
422
423 if (ret.success)
424 {
425 emit fileStaged(mCurrentFile);
426 emit exitRequested();
427 }
428}
429
430void FileDiffWidget::revertFile()
431{
432 const auto ret = QMessageBox::warning(
433 this, tr("Revert all changes"),
434 tr("Please, take into account that this will revert all the changes you have performed so far."),
435 QMessageBox::Ok, QMessageBox::Cancel);
436
437 if (ret == QMessageBox::Ok)
438 {
439 QScopedPointer<GitLocal> git(new GitLocal(mGit));
440 const auto ret = git->checkoutFile(mCurrentFile);
441
442 if (ret)
443 {
444 emit fileReverted(mCurrentFile);
445 emit exitRequested();
446 }
447 }
448}
449
450void FileDiffWidget::stageChunk(const QString &id)
451{
452 const auto iter = std::find_if(mChunks.chunks.cbegin(), mChunks.chunks.cend(),
453 [id](const ChunkDiffInfo &info) { return id == info.id; });
454
455 if (iter != mChunks.chunks.cend())
456 {
457 auto startingLine = 0;
458
459 if (iter->newFile.startLine != -1
460 && (iter->oldFile.startLine == -1 || iter->newFile.startLine < iter->oldFile.startLine))
461 startingLine = iter->newFile.startLine;
462 else
463 startingLine = iter->oldFile.startLine;
464
465 const auto buffer = 3;
466 QString text;
467 QString postLine = " \n";
468 auto fileCount = startingLine - 1 - buffer;
469
470 if (fileCount < 0)
471 fileCount = 0;
472
473 for (; fileCount < startingLine - 1 && fileCount < mChunks.oldFileDiff.count(); ++fileCount)
474 text.append(QString(" %1\n").arg(mChunks.oldFileDiff.at(fileCount)));
475
476 if (fileCount < mChunks.oldFileDiff.count())
477 postLine = QString(" %1\n").arg(mChunks.oldFileDiff.at(fileCount));
478
479 auto totalLinesOldFile = 0;
480
481 if (iter->oldFile.startLine != -1)
482 {
483 const auto realStart = iter->oldFile.startLine - 1;
484 totalLinesOldFile = iter->oldFile.startLine == iter->oldFile.endLine ? 1 : iter->oldFile.endLine - realStart;
485
486 auto i = realStart;
487 for (; i < iter->oldFile.endLine && i < mChunks.oldFileDiff.count(); ++i)
488 text.append(QString("-%1\n").arg(mChunks.oldFileDiff.at(i)));
489
490 postLine = QString(" %1\n").arg(mChunks.oldFileDiff.at(i));
491 }
492
493 auto totalLinesNewFile = 0;
494
495 if (iter->newFile.startLine != -1)
496 {
497 const auto realStart = iter->newFile.startLine - 1;
498 totalLinesNewFile = iter->newFile.startLine == iter->newFile.endLine ? 1 : iter->newFile.endLine - realStart;
499
500 for (auto i = realStart; i < iter->newFile.endLine && i < mChunks.newFileDiff.count(); ++i)
501 text.append(QString("+%1\n").arg(mChunks.newFileDiff.at(i)));
502 }
503
504 text.append(postLine);
505
506 const auto filePath = QString(mCurrentFile).remove(mGit->getWorkingDir());
507 const auto patch
508 = QString("--- a%1\n"
509 "+++ b%1\n"
510 "@@ -%2,%3 +%2,%4 @@\n"
511 "%5")
512 .arg(filePath, QString::number(startingLine - buffer), QString::number(buffer + totalLinesOldFile + 1),
513 QString::number(buffer + totalLinesNewFile + 1), text);
514
515 QTemporaryFile f;
516
517 if (f.open())
518 {
519 f.write(patch.toUtf8());
520 f.close();
521
522 QScopedPointer<GitPatches> git(new GitPatches(mGit));
523
524 if (const auto ret = git->stagePatch(f.fileName()); ret.success)
525 QMessageBox::information(this, tr("Changes staged!"), tr("The chunk has been successfully staged."));
526 else
527 {
528#ifdef DEBUG
529 QFile patch("aux.patch");
530
531 if (patch.open(QIODevice::WriteOnly) && f.open())
532 {
533 patch.write(f.readAll());
534 patch.close();
535 f.close();
536 }
537#endif
538 QMessageBox::information(this, tr("Stage failed"),
539 tr("The chunk couldn't be applied:\n%1").arg(ret.output));
540 }
541 }
542 }
543}
544