1 /***************************************************************************
2  *   Copyright (C) 2005-2009 by Rajko Albrecht                             *
3  *   ral@alwins-world.de                                                   *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20 #include "svnlogdlgimp.h"
21 #include "settings/kdesvnsettings.h"
22 #include "svnactions.h"
23 #include "svnfrontend/fronthelpers/revisionbuttonimpl.h"
24 #include "svnfrontend/models/logitemmodel.h"
25 #include "svnfrontend/models/logmodelhelper.h"
26 #include "helpers/windowgeometryhelper.h"
27 
28 #include <kconfig.h>
29 
30 #include <KHelpClient>
31 #include <KStandardGuiItem>
32 #include <QDialogButtonBox>
33 #include <QKeyEvent>
34 #include <QMenu>
35 #include <QSortFilterProxyModel>
36 #include <QTextDocumentFragment>
37 
38 const QLatin1String groupName("log_dialog_size");
39 
SvnLogDlgImp(SvnActions * ac,bool modal,QWidget * parent)40 SvnLogDlgImp::SvnLogDlgImp(SvnActions *ac, bool modal, QWidget *parent)
41     : QDialog(parent)
42 {
43     setupUi(this);
44     setModal(modal);
45     m_pbClose->setDefault(true);
46     m_pbClose->setShortcut(Qt::CTRL | Qt::Key_Return);
47     KStandardGuiItem::assign(m_pbClose, KStandardGuiItem::Close);
48     KStandardGuiItem::assign(m_pbHelp, KStandardGuiItem::Help);
49     m_DispPrevButton->setIcon(QIcon::fromTheme(QStringLiteral("kdesvndiff")));
50     m_DispSpecDiff->setIcon(QIcon::fromTheme(QStringLiteral("kdesvndiff")));
51     buttonBlame->setIcon(QIcon::fromTheme(QStringLiteral("kdesvnblame")));
52     m_SortModel = nullptr;
53     m_CurrentModel = nullptr;
54     m_ControlKeyDown = false;
55 
56     if (Kdesvnsettings::self()->log_always_list_changed_files()) {
57         buttonListFiles->hide();
58     } else {
59         m_ChangedList->hide();
60     }
61     m_Actions = ac;
62     KConfigGroup cs(Kdesvnsettings::self()->config(), groupName);
63     QByteArray t1 = cs.readEntry("logsplitter", QByteArray());
64     if (!t1.isEmpty()) {
65         m_centralSplitter->restoreState(t1);
66     }
67     t1 = cs.readEntry("right_logsplitter", QByteArray());
68     if (!t1.isEmpty()) {
69         if (cs.readEntry("laststate", false) == m_ChangedList->isHidden()) {
70             m_rightSplitter->restoreState(t1);
71         }
72     }
73 }
74 
~SvnLogDlgImp()75 SvnLogDlgImp::~SvnLogDlgImp()
76 {
77     KConfigGroup cs(Kdesvnsettings::self()->config(), groupName);
78     cs.writeEntry("right_logsplitter", m_rightSplitter->saveState());
79     cs.writeEntry("logsplitter", m_centralSplitter->saveState());
80     cs.writeEntry("laststate", m_ChangedList->isHidden());
81     delete m_SortModel;
82 }
83 
dispLog(const svn::LogEntriesMapPtr & log,const QString & what,const QString & root,const svn::Revision & peg,const QString & pegUrl)84 void SvnLogDlgImp::dispLog(const svn::LogEntriesMapPtr &log, const QString &what, const QString &root, const svn::Revision &peg, const QString &pegUrl)
85 {
86     m_peg = peg;
87     m_PegUrl = pegUrl;
88     m_startRevButton->setNoWorking(m_PegUrl.isUrl());
89     m_endRevButton->setNoWorking(m_PegUrl.isUrl());
90     if (!m_PegUrl.isUrl() || Kdesvnsettings::remote_special_properties()) {
91         QString s = m_Actions->searchProperty(_bugurl, QStringLiteral("bugtraq:url"), pegUrl, peg, true);
92         if (!s.isEmpty()) {
93             QString reg;
94             s = m_Actions->searchProperty(reg, QStringLiteral("bugtraq:logregex"), pegUrl, peg, true);
95             if (!s.isNull() && !reg.isEmpty()) {
96                 const QVector<QStringRef> s1 = reg.splitRef(QLatin1Char('\n'));
97                 if (!s1.isEmpty()) {
98                     _r1.setPattern(s1.at(0).toString());
99                     if (s1.size() > 1) {
100                         _r2.setPattern(s1.at(1).toString());
101                     }
102                 }
103             }
104         }
105     }
106     _base = root;
107     m_Entries = log;
108     if (!what.isEmpty()) {
109         setWindowTitle(i18nc("@title:window", "SVN Log of %1", what));
110     } else {
111         setWindowTitle(i18nc("@title:window", "SVN Log"));
112     }
113     _name = what;
114     if (!_name.startsWith(QLatin1Char('/'))) {
115         _name = QLatin1Char('/') + _name;
116     }
117     dispLog(log);
118 }
119 
dispLog(const svn::LogEntriesMapPtr & _log)120 void SvnLogDlgImp::dispLog(const svn::LogEntriesMapPtr &_log)
121 {
122     if (!_log) {
123         return;
124     }
125     bool must_init = false;
126     if (!m_SortModel) {
127         m_SortModel = new SvnLogSortModel(m_LogTreeView);
128         m_CurrentModel = new SvnLogModel(_log, _name, m_SortModel);
129         m_SortModel->setSourceModel(m_CurrentModel);
130         must_init = true;
131     } else {
132         m_CurrentModel->setLogData(_log, _name);
133     }
134 
135     if (must_init) {
136         m_LogTreeView->setModel(m_SortModel);
137         m_LogTreeView->sortByColumn(SvnLogModel::Revision, Qt::DescendingOrder);
138         connect(m_LogTreeView->selectionModel(), &QItemSelectionModel::selectionChanged,
139                 this, &SvnLogDlgImp::slotSelectionChanged);
140         m_LogTreeView->resizeColumnToContents(SvnLogModel::Revision);
141         m_LogTreeView->resizeColumnToContents(SvnLogModel::Author);
142         m_LogTreeView->resizeColumnToContents(SvnLogModel::Date);
143     }
144     m_startRevButton->setRevision(m_CurrentModel->max());
145     m_endRevButton->setRevision(m_CurrentModel->min());
146     QModelIndex ind = m_CurrentModel->index(m_CurrentModel->rowCount(QModelIndex()) - 1);
147     if (ind.isValid()) {
148         m_LogTreeView->selectionModel()->select(m_SortModel->mapFromSource(ind),
149                                                 QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
150     }
151     m_LogTreeView->setFocus();
152 }
153 
genReplace(const QString & r1match)154 QString SvnLogDlgImp::genReplace(const QString &r1match)
155 {
156     static QString anf(QStringLiteral("<a href=\""));
157     static QString mid(QStringLiteral("\">"));
158     static QString end(QStringLiteral("</a>"));
159     QString res;
160     if (_r2.pattern().length() < 1) {
161         res = _bugurl;
162         res.replace(QStringLiteral("%BUGID%"), _r1.cap(1));
163         res = anf + res + mid + r1match + end;
164         return res;
165     }
166     int pos = 0;
167     int count = 0;
168     int oldpos;
169 
170     while (pos > -1) {
171         oldpos = pos + count;
172         pos = r1match.indexOf(_r2, pos + count);
173         if (pos == -1) {
174             break;
175         }
176         count = _r2.matchedLength();
177         res += r1match.midRef(oldpos, pos - oldpos);
178         QString sub = r1match.mid(pos, count);
179         QString _url = _bugurl;
180         _url.replace(QStringLiteral("%BUGID%"), sub);
181         res += anf + _url + mid + sub + end;
182     }
183     res += r1match.midRef(oldpos);
184     return res;
185 }
186 
replaceBugids(QString & msg)187 void SvnLogDlgImp::replaceBugids(QString &msg)
188 {
189     if (!_r1.isValid() || _r1.pattern().length() < 1 || _bugurl.isEmpty()) {
190         return;
191     }
192     int pos = 0;
193     int count = 0;
194 
195     pos = _r1.indexIn(msg, pos + count);
196     count = _r1.matchedLength();
197 
198     while (pos > -1) {
199         QString s1 = msg.mid(pos, count);
200         QString rep = genReplace(s1);
201         msg = msg.replace(pos, count, rep);
202         pos = _r1.indexIn(msg, pos + rep.length());
203         count = _r1.matchedLength();
204     }
205 }
206 
slotSelectionChanged(const QItemSelection & current,const QItemSelection & previous)207 void SvnLogDlgImp::slotSelectionChanged(const QItemSelection &current, const QItemSelection &previous)
208 {
209     Q_UNUSED(previous);
210     m_ChangedList->clear();
211     QModelIndexList _l = current.indexes();
212     if (_l.count() < 1) {
213         m_DispPrevButton->setEnabled(false);
214         buttonListFiles->setEnabled(false);
215         buttonBlame->setEnabled(false);
216         m_ChangedList->clear();
217         return;
218     }
219     QModelIndex _index = m_SortModel->mapToSource(_l[0]);
220     m_CurrentModel->fillChangedPaths(_index, m_ChangedList);
221     QTextDocumentFragment _m = QTextDocumentFragment::fromPlainText(m_CurrentModel->fullMessage(_index));
222     QString msg = _m.toHtml();
223     replaceBugids(msg);
224     m_LogDisplay->setHtml(msg);
225     m_DispPrevButton->setEnabled(_index.row() > 0);
226     buttonBlame->setEnabled(true);
227 }
228 
229 
230 /*!
231     \fn SvnLogDlgImp::slotDispPrevious()
232  */
slotDispPrevious()233 void SvnLogDlgImp::slotDispPrevious()
234 {
235     QModelIndex _index = selectedRow();
236     if (!_index.isValid() || _index.row() == 0) {
237         m_DispPrevButton->setEnabled(false);
238         return;
239     }
240     QModelIndex _it = m_CurrentModel->index(_index.row() - 1);
241     if (!_it.isValid()) {
242         m_DispPrevButton->setEnabled(false);
243         return;
244     }
245     const SvnLogModelNodePtr k = m_CurrentModel->indexNode(_index);
246     const SvnLogModelNodePtr p = m_CurrentModel->indexNode(_it);
247     if (!k || !p) {
248         m_DispPrevButton->setEnabled(false);
249         return;
250     }
251 
252     const QString s(_base + k->realName());
253     const QString e(_base + p->realName());
254     emit makeDiff(e, p->revision(), s, k->revision(), this);
255 }
256 
257 
258 /*!
259     \fn SvnLogDlgImp::saveSize()
260  */
saveSize()261 void SvnLogDlgImp::saveSize()
262 {
263     WindowGeometryHelper::save(this, groupName);
264 }
265 
slotRevisionSelected()266 void SvnLogDlgImp::slotRevisionSelected()
267 {
268     m_goButton->setFocus();
269     //m_DispSpecDiff->setEnabled( m_first && m_second && m_first != m_second);
270 }
271 
slotDispSelected()272 void SvnLogDlgImp::slotDispSelected()
273 {
274     SvnLogModelNodePtr m_first = m_CurrentModel->indexNode(m_CurrentModel->index(m_CurrentModel->leftRow()));
275     SvnLogModelNodePtr m_second = m_CurrentModel->indexNode(m_CurrentModel->index(m_CurrentModel->rightRow()));
276     if (m_first && m_second) {
277         emit makeDiff(_base + m_first->realName(), m_first->revision(), _base + m_second->realName(), m_second->revision(), this);
278     }
279 }
280 
getSingleLog(svn::LogEntry & t,const svn::Revision & r,const QString & what,const svn::Revision & peg,QString & root)281 bool SvnLogDlgImp::getSingleLog(svn::LogEntry &t, const svn::Revision &r, const QString &what, const svn::Revision &peg, QString &root)
282 {
283     root = _base;
284     const svn::LogEntriesMap::const_iterator it = m_Entries->constFind(r.revnum());
285     if (it == m_Entries->constEnd()) {
286         return m_Actions->getSingleLog(t, r, what, peg, root);
287     }
288     t = it.value();
289     return true;
290 }
291 
slotGetLogs()292 void SvnLogDlgImp::slotGetLogs()
293 {
294     svn::LogEntriesMapPtr lm = m_Actions->getLog(m_startRevButton->revision(),
295                                                  m_endRevButton->revision(), m_peg,
296                                                  _base + _name, Kdesvnsettings::self()->log_always_list_changed_files(), 0, Kdesvnsettings::last_node_follow(), this);
297     if (lm) {
298         dispLog(lm);
299     }
300 }
301 
slotPrevFifty()302 void SvnLogDlgImp::slotPrevFifty()
303 {
304     svn::Revision now = m_CurrentModel->min();
305     if (now == 1) {
306         return;
307     }
308     svn::Revision begin = now.revnum() - 1;
309     if (begin.revnum() < 1) {
310         begin = 1;
311     }
312     svn::LogEntriesMapPtr lm = m_Actions->getLog(begin,
313                                                  (begin.revnum() > 50 ? svn::Revision::START : svn::Revision::HEAD), m_peg,
314                                                  _base + _name, Kdesvnsettings::self()->log_always_list_changed_files(), 50, Kdesvnsettings::last_node_follow(), this);
315     if (lm) {
316         dispLog(lm);
317     }
318 }
319 
slotBeginHead()320 void SvnLogDlgImp::slotBeginHead()
321 {
322     svn::LogEntriesMapPtr lm = m_Actions->getLog(svn::Revision::HEAD,
323                                                  1, m_peg,
324                                                  _base + _name, Kdesvnsettings::self()->log_always_list_changed_files(), 50, Kdesvnsettings::last_node_follow(), this);
325     if (lm) {
326         dispLog(lm);
327     }
328 }
329 
slotHelpRequested()330 void SvnLogDlgImp::slotHelpRequested()
331 {
332     KHelpClient::invokeHelp(QLatin1String("logdisplay-dlg"), QLatin1String("kdesvn"));
333 }
334 
335 
slotListEntries()336 void SvnLogDlgImp::slotListEntries()
337 {
338     QModelIndex _index = selectedRow();
339     SvnLogModelNodePtr ptr = m_CurrentModel->indexNode(_index);
340     if (!ptr) {
341         buttonListFiles->setEnabled(false);
342         return;
343     }
344     if (ptr->changedPaths().isEmpty()) {
345         svn::LogEntriesMapPtr _log = m_Actions->getLog(ptr->revision(), ptr->revision(), ptr->revision(),
346                                                        _name, true, 0, Kdesvnsettings::last_node_follow());
347         if (!_log) {
348             return;
349         }
350         if (!_log->isEmpty()) {
351             ptr->setChangedPaths(_log->value(ptr->revision()));
352         }
353     }
354     if (ptr->changedPaths().isEmpty()) {
355         m_CurrentModel->fillChangedPaths(_index, m_ChangedList);
356     }
357     buttonListFiles->setEnabled(false);
358 }
359 
keyPressEvent(QKeyEvent * e)360 void SvnLogDlgImp::keyPressEvent(QKeyEvent *e)
361 {
362     if (!e) {
363         return;
364     }
365     if (e->text().isEmpty() && e->key() == Qt::Key_Control) {
366         m_ControlKeyDown = true;
367     }
368     QDialog::keyPressEvent(e);
369 }
370 
keyReleaseEvent(QKeyEvent * e)371 void SvnLogDlgImp::keyReleaseEvent(QKeyEvent *e)
372 {
373     if (!e) {
374         return;
375     }
376     if (e->text().isEmpty() && e->key() == Qt::Key_Control) {
377         m_ControlKeyDown = false;
378     }
379     QDialog::keyReleaseEvent(e);
380 }
381 
showEvent(QShowEvent * e)382 void SvnLogDlgImp::showEvent(QShowEvent *e)
383 {
384     QDialog::showEvent(e);
385     WindowGeometryHelper::restore(this, groupName);
386 }
387 
388 
slotBlameItem()389 void SvnLogDlgImp::slotBlameItem()
390 {
391     QModelIndex ind = selectedRow();
392     if (!ind.isValid()) {
393         buttonBlame->setEnabled(false);
394         return;
395     }
396     qlonglong rev = m_CurrentModel->toRevision(ind);
397     svn::Revision start(svn::Revision::START);
398     m_Actions->makeBlame(start, rev, _base + m_CurrentModel->realName(ind), QApplication::activeModalWidget(), rev, this);
399 }
400 
401 /* it works 'cause we use single selection only */
selectedRow(int column)402 QModelIndex SvnLogDlgImp::selectedRow(int column)
403 {
404     QModelIndexList _mi = m_LogTreeView->selectionModel()->selectedRows(column);
405     if (_mi.count() < 1) {
406         return QModelIndex();
407     }
408     return m_SortModel->mapToSource(_mi[0]);
409 }
410 
slotCustomContextMenu(const QPoint & e)411 void SvnLogDlgImp::slotCustomContextMenu(const QPoint &e)
412 {
413     QModelIndex ind = m_LogTreeView->indexAt(e);
414     QModelIndex bel;
415     if (ind.isValid()) {
416         bel = m_LogTreeView->indexBelow(ind);
417         ind = m_SortModel->mapToSource(ind);
418     }
419     int row = -1;
420     if (ind.isValid()) {
421         row = ind.row();
422     } else {
423         return;
424     }
425 
426     qlonglong rev = -1;
427     if (bel.isValid()) {
428         bel = m_SortModel->mapToSource(bel);
429         rev = m_CurrentModel->toRevision(bel);
430     }
431     QMenu popup;
432     QAction *ac;
433     bool unset = false;
434     if (row != m_CurrentModel->rightRow()) {
435         ac = popup.addAction(QIcon::fromTheme(QStringLiteral("kdesvnright")), i18n("Set version as right side of diff"));
436         ac->setData(101);
437     } else {
438         unset = true;
439     }
440     if (row != m_CurrentModel->leftRow()) {
441         ac = popup.addAction(QIcon::fromTheme(QStringLiteral("kdesvnleft")), i18n("Set version as left side of diff"));
442         ac->setData(102);
443     } else {
444         unset = true;
445     }
446     if (unset) {
447         ac = popup.addAction(i18n("Unset version for diff"));
448         ac->setData(103);
449     }
450     if (rev > -1 && !m_PegUrl.isUrl()) {
451         ac = popup.addAction(i18n("Revert this commit"));
452         ac->setData(104);
453     }
454     ac = popup.exec(m_LogTreeView->viewport()->mapToGlobal(e));
455     if (!ac) {
456         return;
457     }
458     int r = ac->data().toInt();
459     switch (r) {
460     case 101:
461         m_CurrentModel->setRightRow(row);
462         break;
463     case 102:
464         m_CurrentModel->setLeftRow(row);
465         break;
466     case 103:
467         if (row != m_CurrentModel->leftRow()) {
468             m_CurrentModel->setLeftRow(-1);
469         }
470         if (row != m_CurrentModel->rightRow()) {
471             m_CurrentModel->setRightRow(-1);
472         }
473         break;
474     case 104: {
475         svn::Revision previous(rev);
476         svn::Revision current(m_CurrentModel->toRevision(ind));
477         QString _path = m_PegUrl.path();
478         m_Actions->slotMergeWcRevisions(_path, current, previous, true, true, false, false, false);
479     }
480     break;
481     }
482     m_DispSpecDiff->setEnabled(m_CurrentModel->leftRow() != -1 && m_CurrentModel->rightRow() != -1 && m_CurrentModel->leftRow() != m_CurrentModel->rightRow());
483 }
484 
slotChangedPathContextMenu(const QPoint & e)485 void SvnLogDlgImp::slotChangedPathContextMenu(const QPoint &e)
486 {
487     QTreeWidgetItem *_item = m_ChangedList->currentItem();
488     if (!_item) {
489         return;
490     }
491 
492     LogChangePathItem *item = static_cast<LogChangePathItem *>(_item);
493     if (item->action() == 'D') {
494         return;
495     }
496     QModelIndex ind = selectedRow();
497     if (!ind.isValid()) {
498         return;
499     }
500     const qlonglong rev = m_CurrentModel->toRevision(ind);
501     QMenu popup;
502     const QString name = item->path();
503     const QString source = item->revision() > -1 ? item->source() : item->path();
504     QAction *ac;
505     ac = popup.addAction(i18n("Annotate"));
506     if (ac) {
507         ac->setData(101);
508     }
509     if (item->action() != 'A' || item->revision() > -1) {
510         ac = popup.addAction(i18n("Diff previous"));
511         if (ac) {
512             ac->setData(102);
513         }
514     }
515     ac = popup.addAction(i18n("Cat this version"));
516     if (ac) {
517         ac->setData(103);
518     }
519     ac = popup.exec(m_ChangedList->viewport()->mapToGlobal(e));
520     if (!ac) {
521         return;
522     }
523     int r = ac->data().toInt();
524     svn::Revision start(svn::Revision::START);
525     switch (r) {
526     case 101: {
527         m_Actions->makeBlame(start, rev, _base + name, QApplication::activeModalWidget(), rev, this);
528         break;
529     }
530     case 102: {
531         const svn_revnum_t prev = item->revision() > 0 ? item->revision() : rev - 1;
532         emit makeDiff(_base + source, prev, _base + name, rev, this);
533         break;
534     }
535     case 103: {
536         emit makeCat(rev, _base + source, source, rev, QApplication::activeModalWidget());
537     }
538     default:
539         break;
540     }
541 }
542 
slotSingleDoubleClicked(QTreeWidgetItem * _item,int)543 void SvnLogDlgImp::slotSingleDoubleClicked(QTreeWidgetItem *_item, int)
544 {
545     if (!_item) {
546         return;
547     }
548 
549     const LogChangePathItem *item = static_cast<LogChangePathItem *>(_item);
550     const QModelIndex ind = selectedRow();
551     if (!ind.isValid()) {
552         return;
553     }
554     svn::Revision start(svn::Revision::START);
555     if (item->action() != 'D') {
556         const QString name = item->path();
557         const qlonglong rev = m_CurrentModel->toRevision(ind);
558         m_Actions->makeBlame(start, rev, _base + name, QApplication::activeModalWidget(), rev, this);
559     }
560 }
561