filehistorywidget.cpp 8.14 KB
Newer Older
Waqar Ahmed's avatar
Waqar Ahmed committed
1
2
3
4
5
/*
    SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/
6
7
8
9
10
#include "filehistorywidget.h"

#include <QDate>
#include <QDebug>
#include <QFileInfo>
Waqar Ahmed's avatar
Waqar Ahmed committed
11
#include <QPainter>
12
#include <QProcess>
Waqar Ahmed's avatar
Waqar Ahmed committed
13
#include <QStyledItemDelegate>
14
15
16
17
18
#include <QVBoxLayout>

#include <KLocalizedString>

// git log --format=%H%n%aN%n%aE%n%at%n%ct%n%P%n%B --author-date-order
19
QList<QByteArray> FileHistoryWidget::getFileHistory(const QString &file)
20
21
22
23
24
25
26
27
28
29
{
    QProcess git;
    git.setWorkingDirectory(QFileInfo(file).absolutePath());
    QStringList args{QStringLiteral("log"),
                     QStringLiteral("--format=%H%n%aN%n%aE%n%at%n%ct%n%P%n%B"),
                     QStringLiteral("-z"),
                     QStringLiteral("--author-date-order"),
                     file};
    git.start(QStringLiteral("git"), args, QProcess::ReadOnly);
    if (git.waitForStarted() && git.waitForFinished(-1)) {
30
31
32
33
34
        if (git.exitStatus() == QProcess::NormalExit && git.exitCode() == 0) {
            return git.readAll().split(0x00);
        } else {
            Q_EMIT errorMessage(i18n("Failed to get file history: %1", QString::fromUtf8(git.readAllStandardError())), true);
        }
35
36
37
38
39
40
41
42
43
44
45
46
47
    }
    return {};
}

struct Commit {
    QByteArray hash;
    QString authorName;
    QString email;
    qint64 authorDate;
    qint64 commitDate;
    QByteArray parentHash;
    QString msg;
};
Waqar Ahmed's avatar
Waqar Ahmed committed
48
Q_DECLARE_METATYPE(Commit)
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

static QVector<Commit> parseCommits(const QList<QByteArray> &raw)
{
    QVector<Commit> commits;
    commits.reserve(raw.size());
    std::transform(raw.cbegin(), raw.cend(), std::back_inserter(commits), [](const QByteArray &r) {
        const auto lines = r.split('\n');
        if (lines.length() < 7) {
            return Commit{};
        }
        auto hash = lines.at(0);
        //        qWarning() << hash;
        auto author = QString::fromUtf8(lines.at(1));
        //        qWarning() << author;
        auto email = QString::fromUtf8(lines.at(2));
        //        qWarning() << email;
        qint64 authorDate = lines.at(3).toLong();
        //        qWarning() << authorDate;
        qint64 commitDate = lines.at(4).toLong();
        //        qWarning() << commitDate;
        auto parent = lines.at(5);
        //        qWarning() << parent;
        auto msg = QString::fromUtf8(lines.at(6));
        //        qWarning() << msg;
        return Commit{hash, author, email, authorDate, commitDate, parent, msg};
    });

    return commits;
}

class CommitListModel : public QAbstractListModel
{
public:
    CommitListModel(QObject *parent = nullptr)
        : QAbstractListModel(parent)
    {
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
87
    enum Role { CommitRole = Qt::UserRole + 1, CommitHash };
88
89
90
91
92
93
94
95
96
97
98
99

    int rowCount(const QModelIndex &) const override
    {
        return m_rows.count();
    }
    QVariant data(const QModelIndex &index, int role) const override
    {
        if (!index.isValid()) {
            return {};
        }
        auto row = index.row();
        switch (role) {
Waqar Ahmed's avatar
Waqar Ahmed committed
100
101
102
103
104
105
        case Role::CommitRole: {
            QVariant v;
            v.setValue(m_rows[row]);
            return v;
        }
        case Role::CommitHash:
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
            return m_rows[row].hash;
        }

        return {};
    }

    void refresh(const QVector<Commit> &cmts)
    {
        beginResetModel();
        m_rows = cmts;
        endResetModel();
    }

private:
    QVector<Commit> m_rows;
};

Waqar Ahmed's avatar
Waqar Ahmed committed
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
class CommitDelegate : public QStyledItemDelegate
{
public:
    CommitDelegate(QObject *parent)
        : QStyledItemDelegate(parent)
    {
    }

    void paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const override
    {
        auto commit = index.data(CommitListModel::CommitRole).value<Commit>();
        if (commit.hash.isEmpty()) {
            return;
        }

        QStyleOptionViewItem options = opt;
        initStyleOption(&options, index);

        options.text = QString();
        QStyledItemDelegate::paint(painter, options, index);

Waqar Ahmed's avatar
Waqar Ahmed committed
144
        constexpr int lineHeight = 2;
Waqar Ahmed's avatar
Waqar Ahmed committed
145
146
147
        QFontMetrics fm = opt.fontMetrics;

        QRect prect = opt.rect;
Waqar Ahmed's avatar
Waqar Ahmed committed
148

149
        //        const int ascent = (opt.fontMetrics.ascent() / 2);
Waqar Ahmed's avatar
Waqar Ahmed committed
150
151

        // draw line
152
153
154
155
156
        //        prect.setX(prect.x() + ascent + 2);
        //        auto sp = painter->pen();
        //        auto p = painter->pen();
        //        p.setWidth(2);
        //        painter->setPen(p);
Waqar Ahmed's avatar
Waqar Ahmed committed
157

158
159
160
161
162
        //        auto p1 = prect.bottomLeft();
        //        int w = opt.fontMetrics.ascent();
        //        int h = opt.rect.height();
        //        int r = w / 3;
        //        p1.ry() -= (h / 2) - r;
Waqar Ahmed's avatar
Waqar Ahmed committed
163

164
        //        painter->setRenderHint(QPainter::Antialiasing, true);
Waqar Ahmed's avatar
Waqar Ahmed committed
165

166
167
168
169
170
171
172
        //        QPoint pp = p1;
        //        pp.ry() -= 3 + 1;
        //        painter->drawLine(prect.topLeft(), pp);
        //        painter->drawEllipse(p1, r, r);
        //        auto p2 = p1;
        //        p2.ry() += r + 1;
        //        painter->drawLine(p2, prect.bottomLeft());
Waqar Ahmed's avatar
Waqar Ahmed committed
173

174
        //        painter->setRenderHint(QPainter::Antialiasing, false);
Waqar Ahmed's avatar
Waqar Ahmed committed
175

176
        //        painter->setPen(sp);
Waqar Ahmed's avatar
Waqar Ahmed committed
177

Waqar Ahmed's avatar
Waqar Ahmed committed
178
        // padding
179
        prect.setX(prect.x() + 5);
Waqar Ahmed's avatar
Waqar Ahmed committed
180
181
182
        prect.setY(prect.y() + lineHeight);

        // draw author on left
Waqar Ahmed's avatar
Waqar Ahmed committed
183
184
185
        QFont f = opt.font;
        f.setBold(true);
        painter->setFont(f);
Waqar Ahmed's avatar
Waqar Ahmed committed
186
        painter->drawText(prect, Qt::AlignLeft, commit.authorName);
Waqar Ahmed's avatar
Waqar Ahmed committed
187
        painter->setFont(opt.font);
Waqar Ahmed's avatar
Waqar Ahmed committed
188
189
190

        // draw author on right
        auto dt = QDateTime::fromSecsSinceEpoch(commit.authorDate);
Waqar Ahmed's avatar
Waqar Ahmed committed
191
192
193
        QString timestamp =
            (dt.date() == QDate::currentDate()) ? dt.time().toString(Qt::DefaultLocaleShortDate) : dt.date().toString(Qt::DefaultLocaleShortDate);
        painter->drawText(prect, Qt::AlignRight, timestamp);
Waqar Ahmed's avatar
Waqar Ahmed committed
194
195

        // draw commit hash
Waqar Ahmed's avatar
Waqar Ahmed committed
196
197
        auto fg = painter->pen();
        painter->setPen(Qt::gray);
Waqar Ahmed's avatar
Waqar Ahmed committed
198
199
        prect.setY(prect.y() + fm.height() + lineHeight);
        painter->drawText(prect, Qt::AlignLeft, QString::fromUtf8(commit.hash.left(7)));
Waqar Ahmed's avatar
Waqar Ahmed committed
200
        painter->setPen(fg);
Waqar Ahmed's avatar
Waqar Ahmed committed
201
202
203

        // draw msg
        prect.setY(prect.y() + fm.height() + lineHeight);
Waqar Ahmed's avatar
Waqar Ahmed committed
204
205
        auto elidedMsg = opt.fontMetrics.elidedText(commit.msg, Qt::ElideRight, prect.width());
        painter->drawText(prect, Qt::AlignLeft, elidedMsg);
Waqar Ahmed's avatar
Waqar Ahmed committed
206
207

        // draw separator
Waqar Ahmed's avatar
Waqar Ahmed committed
208
        painter->setPen(opt.palette.button().color());
Waqar Ahmed's avatar
Waqar Ahmed committed
209
        painter->drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight());
Waqar Ahmed's avatar
Waqar Ahmed committed
210
        painter->setPen(fg);
Waqar Ahmed's avatar
Waqar Ahmed committed
211
212
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
213
    QSize sizeHint(const QStyleOptionViewItem &opt, const QModelIndex &) const override
Waqar Ahmed's avatar
Waqar Ahmed committed
214
    {
Waqar Ahmed's avatar
Waqar Ahmed committed
215
216
        auto height = opt.fontMetrics.height();
        return QSize(0, height * 3 + (3 * 2));
Waqar Ahmed's avatar
Waqar Ahmed committed
217
218
219
    }
};

220
221
222
223
224
225
226
FileHistoryWidget::FileHistoryWidget(const QString &file, QWidget *parent)
    : QWidget(parent)
    , m_file(file)
{
    setLayout(new QVBoxLayout);

    m_backBtn.setText(i18n("Back"));
Waqar Ahmed's avatar
Waqar Ahmed committed
227
    m_backBtn.setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
228
229
230
231
232
233
234
235
236
237
    connect(&m_backBtn, &QPushButton::clicked, this, &FileHistoryWidget::backClicked);
    layout()->addWidget(&m_backBtn);

    m_listView = new QListView;
    layout()->addWidget(m_listView);

    auto model = new CommitListModel(this);
    model->refresh(parseCommits(getFileHistory(file)));

    m_listView->setModel(model);
238
    connect(m_listView, &QListView::clicked, this, &FileHistoryWidget::itemClicked);
Waqar Ahmed's avatar
Waqar Ahmed committed
239
240

    m_listView->setItemDelegate(new CommitDelegate(this));
241
242
243
244
245
246
247
}

void FileHistoryWidget::itemClicked(const QModelIndex &idx)
{
    QProcess git;
    QFileInfo fi(m_file);
    git.setWorkingDirectory(fi.absolutePath());
248
249
250
251

    const auto commit = idx.data(CommitListModel::CommitRole).value<Commit>();

    QStringList args{QStringLiteral("diff"), QString::fromUtf8(commit.hash), QStringLiteral("--"), m_file};
252
253
254
255
256
    git.start(QStringLiteral("git"), args, QProcess::ReadOnly);
    if (git.waitForStarted() && git.waitForFinished(-1)) {
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
            return;
        }
257
258
259
        QByteArray contents = commit.msg.toUtf8();
        contents.append("\n\n");
        contents.append(git.readAllStandardOutput());
260
261
        // we send this signal to the parent, which will pass it on to
        // the GitWidget from where a temporary file is opened
262
        Q_EMIT commitClicked(contents);
263
    }
264
}