1#include "FileBlameWidget.h"
2
3#include <ButtonLink.hpp>
4#include <CommitInfo.h>
5#include <GitCache.h>
6#include <GitHistory.h>
7
8#include <QGridLayout>
9#include <QLabel>
10#include <QMessageBox>
11#include <QScrollArea>
12#include <QtMath>
13
14#include <array>
15
16namespace
17{
18static const int kTotalColors = 8;
19static const std::array<const char *, kTotalColors> kBorderColors { { "25, 65, 99", "36, 95, 146", "44, 116, 177",
20 "56, 136, 205", "87, 155, 213", "118, 174, 221",
21 "150, 192, 221", "197, 220, 240" } };
22qint64 kSecondsNewest = 0;
23qint64 kSecondsOldest = QDateTime::currentDateTime().toSecsSinceEpoch();
24qint64 kIncrementSecs = 0;
25}
26
27FileBlameWidget::FileBlameWidget(const QSharedPointer<GitCache> &cache, const QSharedPointer<GitBase> &git,
28 QWidget *parent)
29 : QFrame(parent)
30 , mCache(cache)
31 , mGit(git)
32 , mAnotation(new QFrame())
33 , mCurrentSha(new QLabel())
34 , mPreviousSha(new QLabel())
35{
36 setAttribute(Qt::WA_DeleteOnClose);
37
38 mAnotation->setObjectName("AnnotationFrame");
39
40 auto initialLayout = new QGridLayout(mAnotation);
41 initialLayout->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Expanding), 0, 0);
42 initialLayout->addWidget(new QLabel(tr("Select a file to blame")), 1, 1);
43 initialLayout->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Expanding), 2, 2);
44
45 mInfoFont.setPointSize(9);
46
47 mCodeFont = QFont(mInfoFont);
48 mCodeFont.setFamily("DejaVu Sans Mono");
49 mCodeFont.setPointSize(8);
50
51 mScrollArea = new QScrollArea();
52 mScrollArea->setWidget(mAnotation);
53 mScrollArea->setWidgetResizable(true);
54
55 const auto lSha = new QLabel(tr("Current SHA:"));
56 const auto lSha2 = new QLabel(tr("Previous SHA:"));
57
58 const auto separator = new QFrame();
59 separator->setObjectName("separator");
60
61 const auto shasLayout = new QGridLayout();
62 shasLayout->setSpacing(10);
63 shasLayout->setContentsMargins(QMargins());
64 shasLayout->addWidget(lSha, 0, 0);
65 shasLayout->addWidget(mCurrentSha, 0, 1);
66 shasLayout->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Fixed), 0, 2);
67 shasLayout->addWidget(lSha2, 1, 0);
68 shasLayout->addWidget(mPreviousSha, 1, 1);
69 shasLayout->addWidget(separator, 2, 0, 1, 3);
70
71 const auto layout = new QVBoxLayout(this);
72 layout->setContentsMargins(10, 10, 10, 0);
73 layout->setSpacing(0);
74 layout->addLayout(shasLayout);
75 layout->addWidget(mScrollArea);
76}
77
78void FileBlameWidget::setup(const QString &fileName, const QString &currentSha, const QString &previousSha)
79{
80 mCurrentFile = fileName;
81 QScopedPointer<GitHistory> git(new GitHistory(mGit));
82 const auto ret = git->blame(mCurrentFile, currentSha);
83
84 if (ret.success && !ret.output.startsWith("fatal:"))
85 {
86 delete mAnotation;
87 mAnotation = nullptr;
88
89 mCurrentSha->setText(currentSha);
90 mPreviousSha->setText(previousSha);
91
92 const auto annotations = processBlame(ret.output);
93 formatAnnotatedFile(annotations);
94 }
95 else
96 QMessageBox::warning(
97 this, tr("File not in Git"),
98 tr("The file {%1} is not under Git control version. You cannot blame it.").arg(mCurrentFile));
99}
100
101void FileBlameWidget::reload(const QString &currentSha, const QString &previousSha)
102{
103 setup(mCurrentFile, currentSha, previousSha);
104}
105
106QString FileBlameWidget::getCurrentSha() const
107{
108 return mCurrentSha->text();
109}
110
111QVector<FileBlameWidget::Annotation> FileBlameWidget::processBlame(const QString &blame)
112{
113#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
114 const auto lines = blame.split("\n", Qt::SkipEmptyParts);
115#else
116 const auto lines = blame.split("\n", QString::SkipEmptyParts);
117#endif
118 QVector<Annotation> annotations;
119
120 for (const auto &line : lines)
121 {
122 auto start = 0;
123 auto indexOfTab = line.indexOf('\t');
124 const auto shortSha = line.mid(start, indexOfTab);
125 const auto revision = mCache->commitInfo(shortSha);
126
127 start = indexOfTab + 1;
128 indexOfTab = line.indexOf('\t', start);
129 const auto name = line.mid(start, indexOfTab - start).remove("(");
130
131 start = indexOfTab + 1;
132 indexOfTab = line.indexOf('\t', start);
133 const auto dtValue = line.mid(start, indexOfTab - start);
134 const auto dt = QDateTime::fromString(dtValue, Qt::ISODate);
135
136 start = indexOfTab + 1;
137
138 const auto lineNumAndContent = line.mid(start);
139 const auto divisorChar = lineNumAndContent.indexOf(")");
140 const auto lineText = lineNumAndContent.mid(0, divisorChar);
141 const auto content = lineNumAndContent.mid(divisorChar + 1, lineNumAndContent.count() - lineText.count() - 1);
142
143 annotations.append({ revision.sha, name, dt, lineText.toInt(), content });
144
145 if (revision.sha != CommitInfo::ZERO_SHA)
146 {
147 const auto dtSinceEpoch = dt.toSecsSinceEpoch();
148
149 if (kSecondsNewest < dtSinceEpoch)
150 kSecondsNewest = dtSinceEpoch;
151
152 if (kSecondsOldest > dtSinceEpoch)
153 kSecondsOldest = dtSinceEpoch;
154 }
155 }
156
157 kIncrementSecs = kSecondsNewest != kSecondsOldest ? (kSecondsNewest - kSecondsOldest) / (kTotalColors - 1) : 1;
158
159 return annotations;
160}
161
162void FileBlameWidget::formatAnnotatedFile(const QVector<Annotation> &annotations)
163{
164 auto labelRow = 0;
165 auto labelRowSpan = 1;
166 QLabel *dateLabel = nullptr;
167 QLabel *authorLabel = nullptr;
168 ButtonLink *messageLabel = nullptr;
169
170 const auto annotationLayout = new QGridLayout();
171 annotationLayout->setContentsMargins(QMargins());
172 annotationLayout->setSpacing(0);
173
174 const auto totalAnnot = annotations.count();
175 for (auto row = 0; row < totalAnnot; ++row)
176 {
177 const auto &lastAnnotation = row == 0 ? Annotation() : annotations.at(row - 1);
178
179 if (lastAnnotation.sha != annotations.at(row).sha)
180 {
181 if (dateLabel)
182 annotationLayout->addWidget(dateLabel, labelRow, 0);
183
184 if (authorLabel)
185 annotationLayout->addWidget(authorLabel, labelRow, 1);
186
187 if (messageLabel)
188 annotationLayout->addWidget(messageLabel, labelRow, 2);
189
190 dateLabel = createDateLabel(annotations.at(row), row == 0);
191 authorLabel = createAuthorLabel(annotations.at(row).author, row == 0);
192 messageLabel = createMessageLabel(annotations.at(row).sha, row == 0);
193
194 labelRow = row;
195 labelRowSpan = 1;
196 }
197 else
198 ++labelRowSpan;
199
200 annotationLayout->addWidget(createNumLabel(annotations.at(row), row), row, 3);
201 annotationLayout->addWidget(createCodeLabel(annotations.at(row).content), row, 4);
202 }
203
204 // Adding the last row
205 if (dateLabel)
206 annotationLayout->addWidget(dateLabel, labelRow, 0);
207
208 if (authorLabel)
209 annotationLayout->addWidget(authorLabel, labelRow, 1);
210
211 if (messageLabel)
212 annotationLayout->addWidget(messageLabel, labelRow, 2);
213
214 annotationLayout->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Expanding), totalAnnot, 4);
215
216 mAnotation = new QFrame();
217 mAnotation->setObjectName("AnnotationFrame");
218 mAnotation->setLayout(annotationLayout);
219
220 mScrollArea->setWidget(mAnotation);
221 mScrollArea->setWidgetResizable(true);
222}
223
224QLabel *FileBlameWidget::createDateLabel(const Annotation &annotation, bool isFirst)
225{
226 auto isWip = annotation.sha == CommitInfo::ZERO_SHA;
227 QString when;
228
229 if (!isWip)
230 {
231 const auto days = annotation.dateTime.daysTo(QDateTime::currentDateTime());
232 const auto secs = annotation.dateTime.secsTo(QDateTime::currentDateTime());
233 if (days > 365)
234 when.append(tr("more than 1 year ago"));
235 else if (days > 1)
236 when.append(QString::number(days)).append(tr(" days ago"));
237 else if (days == 1)
238 when.append(tr("yesterday"));
239 else if (secs > 3600)
240 when.append(QString::number(secs / 3600)).append(tr(" hours ago"));
241 else if (secs == 3600)
242 when.append(tr("1 hour ago"));
243 else if (secs > 60)
244 when.append(QString::number(secs / 60)).append(tr(" minutes ago"));
245 else if (secs == 60)
246 when.append(tr("1 minute ago"));
247 else
248 when.append(QString::number(secs)).append(tr(" secs ago"));
249 }
250
251 const auto dateLabel = new QLabel(when);
252 dateLabel->setObjectName(isFirst ? QString("authorPrimusInterPares") : QString("authorFirstOfItsName"));
253 dateLabel->setToolTip(annotation.dateTime.toString("dd/MM/yyyy hh:mm"));
254 dateLabel->setFont(mInfoFont);
255 dateLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
256
257 return dateLabel;
258}
259
260QLabel *FileBlameWidget::createAuthorLabel(const QString &author, bool isFirst)
261{
262 const auto authorLabel = new QLabel(author);
263 authorLabel->setObjectName(isFirst ? QString("authorPrimusInterPares") : QString("authorFirstOfItsName"));
264 authorLabel->setFont(mInfoFont);
265 authorLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
266
267 return authorLabel;
268}
269
270ButtonLink *FileBlameWidget::createMessageLabel(const QString &sha, bool isFirst)
271{
272 const auto revision = mCache->commitInfo(sha);
273 auto commitMsg = tr("Local changes");
274
275 if (!revision.sha.isEmpty())
276 {
277 auto log = revision.shortLog;
278
279 if (log.count() > 47)
280 log = log.left(47) + QString("...");
281
282 commitMsg = log;
283 }
284
285 const auto messageLabel = new ButtonLink(commitMsg);
286 messageLabel->setObjectName(isFirst ? QString("primusInterPares") : QString("firstOfItsName"));
287 messageLabel->setToolTip(QString("<p>%1</p><p>%2</p>").arg(sha, commitMsg));
288 messageLabel->setFont(mInfoFont);
289
290 connect(messageLabel, &ButtonLink::clicked, this, [this, sha]() { emit signalCommitSelected(sha); });
291
292 return messageLabel;
293}
294
295QLabel *FileBlameWidget::createNumLabel(const Annotation &annotation, int row)
296{
297 const auto numberLabel = new QLabel(QString::number(row + 1));
298 numberLabel->setFont(mCodeFont);
299 numberLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
300 numberLabel->setObjectName("numberLabel");
301 numberLabel->setAlignment(Qt::AlignVCenter | Qt::AlignRight);
302
303 if (annotation.sha != CommitInfo::ZERO_SHA)
304 {
305 const auto dtSinceEpoch = annotation.dateTime.toSecsSinceEpoch();
306 const auto colorIndex = qCeil((kSecondsNewest - dtSinceEpoch) / kIncrementSecs);
307 numberLabel->setStyleSheet(
308 QString("QLabel { border-left: 5px solid rgb(%1) }").arg(QString::fromUtf8(kBorderColors.at(colorIndex))));
309 }
310 else
311 numberLabel->setStyleSheet("QLabel { border-left: 5px solid #D89000 }");
312
313 return numberLabel;
314}
315
316QLabel *FileBlameWidget::createCodeLabel(const QString &content)
317{
318 const auto contentLabel = new QLabel(content);
319 contentLabel->setFont(mCodeFont);
320 contentLabel->setObjectName("normalLabel");
321
322 return contentLabel;
323}
324