1#include "BlameWidget.h"
2
3#include <BranchesViewDelegate.h>
4#include <CommitHistoryColumns.h>
5#include <CommitHistoryModel.h>
6#include <CommitHistoryView.h>
7#include <CommitInfo.h>
8#include <FileBlameWidget.h>
9#include <GitHistory.h>
10#include <RepositoryViewDelegate.h>
11
12#include <QApplication>
13#include <QClipboard>
14#include <QFileSystemModel>
15#include <QGridLayout>
16#include <QHeaderView>
17#include <QMenu>
18#include <QTabWidget>
19#include <QTreeView>
20
21BlameWidget::BlameWidget(const QSharedPointer<GitCache> &cache, const QSharedPointer<GitBase> &git,
22 const QSharedPointer<GitQlientSettings> &settings, QWidget *parent)
23 : QFrame(parent)
24 , mCache(cache)
25 , mGit(git)
26 , mSettings(settings)
27 , fileSystemModel(new QFileSystemModel())
28 , mRepoModel(new CommitHistoryModel(mCache, mGit, nullptr))
29 , mRepoView(new CommitHistoryView(mCache, mGit, mSettings, nullptr))
30 , fileSystemView(new QTreeView())
31 , mTabWidget(new QTabWidget())
32{
33 mTabWidget->setObjectName("HistoryTab");
34 mRepoView->setObjectName("blameGraphView");
35 mRepoView->setModel(mRepoModel);
36 mRepoView->header()->setSectionHidden(static_cast<int>(CommitHistoryColumns::Graph), true);
37 mRepoView->header()->setSectionHidden(static_cast<int>(CommitHistoryColumns::Date), true);
38 mRepoView->header()->setSectionHidden(static_cast<int>(CommitHistoryColumns::Author), true);
39 mRepoView->setItemDelegate(mItemDelegate = new RepositoryViewDelegate(cache, mGit, nullptr, mRepoView));
40 mRepoView->setEnabled(true);
41 mRepoView->setMaximumWidth(450);
42 mRepoView->setSelectionBehavior(QAbstractItemView::SelectRows);
43 mRepoView->setSelectionMode(QAbstractItemView::SingleSelection);
44 mRepoView->setContextMenuPolicy(Qt::CustomContextMenu);
45 mRepoView->header()->setContextMenuPolicy(Qt::NoContextMenu);
46 mRepoView->activateFilter(true);
47 mRepoView->filterBySha({});
48 connect(mRepoView, &CommitHistoryView::customContextMenuRequested, this, &BlameWidget::showRepoViewMenu);
49 connect(mRepoView, &CommitHistoryView::clicked, this, &BlameWidget::reloadBlame);
50 connect(mRepoView, &CommitHistoryView::doubleClicked, this, &BlameWidget::openDiff);
51
52 fileSystemModel->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
53
54 fileSystemView->setModel(fileSystemModel);
55 fileSystemView->setMaximumWidth(450);
56 fileSystemView->header()->setSectionHidden(1, true);
57 fileSystemView->header()->setSectionHidden(2, true);
58 fileSystemView->header()->setSectionHidden(3, true);
59 fileSystemView->setContextMenuPolicy(Qt::CustomContextMenu);
60 connect(fileSystemView, &QTreeView::clicked, this, &BlameWidget::showFileHistoryByIndex);
61
62 const auto historyBlameLayout = new QGridLayout(this);
63 historyBlameLayout->setContentsMargins(QMargins());
64 historyBlameLayout->addWidget(mRepoView, 0, 0);
65 historyBlameLayout->addWidget(fileSystemView, 1, 0);
66 historyBlameLayout->addWidget(mTabWidget, 0, 1, 2, 1);
67
68 mTabWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
69
70 connect(mTabWidget, &QTabWidget::tabCloseRequested, mTabWidget, [this](int index) {
71 if (index == mLastTabIndex)
72 {
73 fileSystemView->clearSelection();
74 mRepoView->blockSignals(true);
75 mRepoView->filterBySha({});
76 mRepoView->blockSignals(false);
77 }
78
79 auto widget = qobject_cast<FileBlameWidget *>(mTabWidget->widget(index));
80 mTabWidget->removeTab(index);
81 const auto key = mTabsMap.key(widget);
82 mTabsMap.remove(key);
83
84 delete widget;
85 });
86 connect(mTabWidget, &QTabWidget::currentChanged, this, &BlameWidget::reloadHistory);
87
88 setAttribute(Qt::WA_DeleteOnClose);
89}
90
91BlameWidget::~BlameWidget()
92{
93 delete mRepoModel;
94 delete mItemDelegate;
95 delete fileSystemModel;
96}
97
98void BlameWidget::init(const QString &workingDirectory)
99{
100 mWorkingDirectory = workingDirectory;
101 fileSystemModel->setRootPath(workingDirectory);
102 fileSystemView->setRootIndex(fileSystemModel->index(workingDirectory));
103}
104
105void BlameWidget::showFileHistory(const QString &filePath)
106{
107 if (!mTabsMap.contains(filePath))
108 {
109 QScopedPointer<GitHistory> git(new GitHistory(mGit));
110 auto ret = git->history(filePath);
111
112 if (ret.success)
113 {
114#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
115 auto shaHistory = ret.output.split("\n", Qt::SkipEmptyParts);
116#else
117 auto shaHistory = ret.output.split("\n", QString::SkipEmptyParts);
118#endif
119 for (auto i = 0; i < shaHistory.size();)
120 {
121 if (shaHistory.at(i).startsWith("gpg:"))
122 {
123 shaHistory.takeAt(i);
124
125 if (shaHistory.size() <= i)
126 break;
127 }
128 else
129 ++i;
130 }
131
132 mRepoView->blockSignals(true);
133 mRepoView->filterBySha(shaHistory);
134 mRepoView->blockSignals(false);
135
136 const auto previousSha = shaHistory.count() > 1 ? shaHistory.at(1) : QString(tr("No info"));
137 const auto fileBlameWidget = new FileBlameWidget(mCache, mGit);
138
139 fileBlameWidget->setup(filePath, shaHistory.constFirst(), previousSha);
140 connect(fileBlameWidget, &FileBlameWidget::signalCommitSelected, mRepoView, &CommitHistoryView::focusOnCommit);
141
142 const auto index = mTabWidget->addTab(fileBlameWidget, filePath.split("/").last());
143 mTabWidget->setTabsClosable(true);
144 mTabWidget->blockSignals(true);
145 mTabWidget->setCurrentIndex(index);
146 mTabWidget->blockSignals(false);
147
148 mLastTabIndex = index;
149 mTabsMap.insert(filePath, fileBlameWidget);
150 }
151 }
152 else
153 mTabWidget->setCurrentWidget(mTabsMap.value(filePath));
154}
155
156void BlameWidget::onNewRevisions(int totalCommits)
157{
158 mRepoModel->onNewRevisions(totalCommits);
159}
160
161void BlameWidget::reloadBlame(const QModelIndex &index)
162{
163 mSelectedRow = index.row();
164 const auto blameWidget = qobject_cast<FileBlameWidget *>(mTabWidget->currentWidget());
165
166 if (blameWidget)
167 {
168 const auto sha
169 = mRepoView->model()->index(index.row(), static_cast<int>(CommitHistoryColumns::Sha)).data().toString();
170 const auto previousSha
171 = mRepoView->model()->index(index.row() + 1, static_cast<int>(CommitHistoryColumns::Sha)).data().toString();
172 blameWidget->reload(sha, previousSha);
173 }
174}
175
176void BlameWidget::reloadHistory(int tabIndex)
177{
178 if (tabIndex >= 0)
179 {
180 mLastTabIndex = tabIndex;
181
182 const auto blameWidget = qobject_cast<FileBlameWidget *>(mTabWidget->widget(tabIndex));
183 const auto sha = blameWidget->getCurrentSha();
184 const auto file = blameWidget->getCurrentFile();
185
186 QScopedPointer<GitHistory> git(new GitHistory(mGit));
187 const auto ret = git->history(file);
188
189 if (ret.success)
190 {
191#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
192 auto shaHistory = ret.output.split("\n", Qt::SkipEmptyParts);
193#else
194 auto shaHistory = ret.output.split("\n", QString::SkipEmptyParts);
195#endif
196 for (auto i = 0; i < shaHistory.size();)
197 {
198 if (shaHistory.at(i).startsWith("gpg:"))
199 {
200 shaHistory.takeAt(i);
201
202 if (shaHistory.size() <= i)
203 break;
204 }
205 else
206 ++i;
207 }
208
209 mRepoView->blockSignals(true);
210 mRepoView->filterBySha(shaHistory);
211
212 const auto repoModel = mRepoView->model();
213 const auto totalRows = repoModel->rowCount();
214 for (auto i = 0; i < totalRows; ++i)
215 {
216 const auto index = mRepoView->model()->index(i, static_cast<int>(CommitHistoryColumns::Sha));
217
218 if (index.data().toString().startsWith(sha))
219 {
220 mRepoView->setCurrentIndex(index);
221 mRepoView->selectionModel()->select(index,
222 QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
223 }
224 }
225
226 mRepoView->blockSignals(false);
227 }
228 }
229}
230
231void BlameWidget::showFileHistoryByIndex(const QModelIndex &index)
232{
233 auto item = fileSystemModel->fileInfo(index);
234
235 if (item.isFile())
236 showFileHistory(item.filePath());
237}
238
239void BlameWidget::showRepoViewMenu(const QPoint &pos)
240{
241 const auto shaColumnIndex = static_cast<int>(CommitHistoryColumns::Sha);
242 const auto modelIndex = mRepoView->model()->index(mSelectedRow, shaColumnIndex);
243
244 reloadBlame(modelIndex);
245
246 const auto sha = modelIndex.data().toString();
247 const auto previousSha = mRepoView->model()->index(mSelectedRow + 1, shaColumnIndex).data().toString();
248 const auto menu = new QMenu(this);
249 const auto copyShaAction = menu->addAction(tr("Copy SHA"));
250 connect(copyShaAction, &QAction::triggered, this, [sha]() { QApplication::clipboard()->setText(sha); });
251
252 const auto fileDiff = menu->addAction(tr("Show file diff"));
253 connect(fileDiff, &QAction::triggered, this, [this, sha, previousSha]() {
254 const auto currentFile = qobject_cast<FileBlameWidget *>(mTabWidget->currentWidget())->getCurrentFile();
255 emit showFileDiff(sha, previousSha, currentFile, false);
256 });
257
258 const auto commitDiff = menu->addAction(tr("Show commit diff"));
259 connect(commitDiff, &QAction::triggered, this, [this, sha, previousSha]() {
260 emit signalOpenDiff({ previousSha, sha });
261 });
262
263 menu->exec(mRepoView->viewport()->mapToGlobal(pos));
264}
265
266void BlameWidget::openDiff(const QModelIndex &index)
267{
268 const auto sha
269 = mRepoView->model()->index(index.row(), static_cast<int>(CommitHistoryColumns::Sha)).data().toString();
270 const auto previousSha
271 = mRepoView->model()->index(index.row() + 1, static_cast<int>(CommitHistoryColumns::Sha)).data().toString();
272
273 emit signalOpenDiff({ previousSha, sha });
274}
275