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 | |
27 | FileDiffWidget::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 | |
176 | void FileDiffWidget::clear() |
177 | { |
178 | mNewFile->clear(); |
179 | } |
180 | |
181 | bool FileDiffWidget::reload() |
182 | { |
183 | if (mCurrentSha == CommitInfo::ZERO_SHA) |
184 | return configure(mCurrentSha, mPreviousSha, mCurrentFile, mIsCached, mEdition->isChecked()); |
185 | |
186 | return false; |
187 | } |
188 | |
189 | bool FileDiffWidget::configure(const QString ¤tSha, 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 | |
280 | void 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 | |
309 | void 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 | |
338 | void FileDiffWidget::hideBackButton() const |
339 | { |
340 | mBack->setVisible(true); |
341 | } |
342 | |
343 | void 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 | |
365 | void 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 | |
391 | void 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 | |
413 | void FileDiffWidget::endEditFile() |
414 | { |
415 | mViewStackedWidget->setCurrentIndex(0); |
416 | } |
417 | |
418 | void 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 | |
430 | void 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 | |
450 | void 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 | |