filehistorywidget.cpp 7.1 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

Waqar Ahmed's avatar
Waqar Ahmed committed
149
        // padding
150
        prect.setX(prect.x() + 5);
Waqar Ahmed's avatar
Waqar Ahmed committed
151
152
153
        prect.setY(prect.y() + lineHeight);

        // draw author on left
Waqar Ahmed's avatar
Waqar Ahmed committed
154
155
156
        QFont f = opt.font;
        f.setBold(true);
        painter->setFont(f);
Waqar Ahmed's avatar
Waqar Ahmed committed
157
        painter->drawText(prect, Qt::AlignLeft, commit.authorName);
Waqar Ahmed's avatar
Waqar Ahmed committed
158
        painter->setFont(opt.font);
Waqar Ahmed's avatar
Waqar Ahmed committed
159
160
161

        // draw author on right
        auto dt = QDateTime::fromSecsSinceEpoch(commit.authorDate);
Waqar Ahmed's avatar
Waqar Ahmed committed
162
163
164
        QLocale l;
        const bool isToday = dt.date() == QDate::currentDate();
        QString timestamp = isToday ? l.toString(dt.time(), QLocale::ShortFormat) : l.toString(dt.date(), QLocale::ShortFormat);
Waqar Ahmed's avatar
Waqar Ahmed committed
165
        painter->drawText(prect, Qt::AlignRight, timestamp);
Waqar Ahmed's avatar
Waqar Ahmed committed
166
167

        // draw commit hash
Waqar Ahmed's avatar
Waqar Ahmed committed
168
169
        auto fg = painter->pen();
        painter->setPen(Qt::gray);
Waqar Ahmed's avatar
Waqar Ahmed committed
170
171
        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
172
        painter->setPen(fg);
Waqar Ahmed's avatar
Waqar Ahmed committed
173
174
175

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

        // draw separator
Waqar Ahmed's avatar
Waqar Ahmed committed
180
        painter->setPen(opt.palette.button().color());
Waqar Ahmed's avatar
Waqar Ahmed committed
181
        painter->drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight());
Waqar Ahmed's avatar
Waqar Ahmed committed
182
        painter->setPen(fg);
Waqar Ahmed's avatar
Waqar Ahmed committed
183
184
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
185
    QSize sizeHint(const QStyleOptionViewItem &opt, const QModelIndex &) const override
Waqar Ahmed's avatar
Waqar Ahmed committed
186
    {
Waqar Ahmed's avatar
Waqar Ahmed committed
187
188
        auto height = opt.fontMetrics.height();
        return QSize(0, height * 3 + (3 * 2));
Waqar Ahmed's avatar
Waqar Ahmed committed
189
190
191
    }
};

192
193
194
195
196
197
198
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
199
    m_backBtn.setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
200
201
202
203
204
205
206
207
208
209
    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);
210
    connect(m_listView, &QListView::clicked, this, &FileHistoryWidget::itemClicked);
Waqar Ahmed's avatar
Waqar Ahmed committed
211
212

    m_listView->setItemDelegate(new CommitDelegate(this));
213
214
215
216
217
218
219
}

void FileHistoryWidget::itemClicked(const QModelIndex &idx)
{
    QProcess git;
    QFileInfo fi(m_file);
    git.setWorkingDirectory(fi.absolutePath());
220
221
222

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

223
    QStringList args{QStringLiteral("show"), QString::fromUtf8(commit.hash), QStringLiteral("--"), m_file};
224
225
226
227
228
    git.start(QStringLiteral("git"), args, QProcess::ReadOnly);
    if (git.waitForStarted() && git.waitForFinished(-1)) {
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
            return;
        }
229
        QByteArray contents(git.readAllStandardOutput());
230
231
        // we send this signal to the parent, which will pass it on to
        // the GitWidget from where a temporary file is opened
232
        Q_EMIT commitClicked(contents);
233
    }
234
}