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 "revisiontree.h"
21 #include "../stopdlg.h"
22 #include "../ccontextlistener.h"
23 #include "svnqt/log_entry.h"
24 #include "svnqt/cache/LogCache.h"
25 #include "svnqt/cache/ReposLog.h"
26 #include "svnqt/cache/ReposConfig.h"
27 #include "svnqt/url.h"
28 #include "svnqt/client_parameter.h"
29 #include "revtreewidget.h"
30 #include "revgraphview.h"
31 #include "elogentry.h"
32 #include "svnfrontend/fronthelpers/cursorstack.h"
33 #include "settings/kdesvnsettings.h"
34 
35 #include <KLocalizedString>
36 #include <KMessageBox>
37 
38 #include <QWidget>
39 #include <QProgressDialog>
40 
41 #define INTERNALCOPY 1
42 #define INTERNALRENAME 2
43 
44 class CContextListener;
45 class RtreeData
46 {
47 public:
48     RtreeData();
49     virtual ~RtreeData();
50 
51     QMap<long, eLog_Entry> m_History;
52 
53     svn::LogEntriesMap m_OldHistory;
54 
55     long max_rev, min_rev;
56     QProgressDialog *progress;
57     QTime m_stopTick;
58 
59     QWidget *dlgParent;
60     RevTreeWidget *m_TreeDisplay;
61 
62     svn::ClientP m_Client;
63     CContextListener *m_Listener;
64 
65     bool getLogs(const QString &, const svn::Revision &startr, const svn::Revision &endr, const QString &origin);
66 };
67 
RtreeData()68 RtreeData::RtreeData()
69     : max_rev(-1), min_rev(-1)
70 {
71     progress = nullptr;
72     m_TreeDisplay = nullptr;
73     dlgParent = nullptr;
74     m_Listener = nullptr;
75 }
76 
~RtreeData()77 RtreeData::~RtreeData()
78 {
79     delete progress;
80 }
81 
getLogs(const QString & reposRoot,const svn::Revision & startr,const svn::Revision & endr,const QString & origin)82 bool RtreeData::getLogs(const QString &reposRoot, const svn::Revision &startr, const svn::Revision &endr, const QString &origin)
83 {
84     Q_UNUSED(origin);
85     if (!m_Listener || !m_Client) {
86         return false;
87     }
88     svn::LogParameter params;
89     params.targets(reposRoot).revisionRange(endr, startr).peg(startr).limit(0).discoverChangedPathes(true).strictNodeHistory(false);
90     const svn::StringArray ex(svn::cache::ReposConfig::self()->readEntry(reposRoot, "tree_exclude_list", QStringList()));
91     try {
92         CursorStack a(Qt::BusyCursor);
93         StopDlg sdlg(m_Listener, dlgParent,
94                      i18nc("@title:window", "Logs"), i18n("Getting logs - hit Cancel for abort"));
95         if (svn::Url::isLocal(reposRoot)) {
96             m_Client->log(params.excludeList(ex), m_OldHistory);
97         } else {
98             svn::cache::ReposLog rl(m_Client, reposRoot);
99             if (rl.isValid()) {
100                 rl.simpleLog(m_OldHistory, startr, endr, (!Kdesvnsettings::network_on() || !Kdesvnsettings::fill_cache_on_tree()), ex);
101             } else if (Kdesvnsettings::network_on()) {
102                 m_Client->log(params.excludeList(ex), m_OldHistory);
103             } else {
104                 KMessageBox::error(nullptr, i18n("Could not retrieve logs, reason:\n%1", i18n("No log cache possible due broken database and networking not allowed.")));
105                 return false;
106             }
107         }
108     } catch (const svn::Exception &ce) {
109         KMessageBox::error(nullptr, i18n("Could not retrieve logs, reason:\n%1", ce.msg()));
110         return false;
111     }
112     return true;
113 }
114 
RevisionTree(const svn::ClientP & aClient,CContextListener * aListener,const QString & reposRoot,const svn::Revision & startr,const svn::Revision & endr,const QString & origin,const svn::Revision & baserevision,QWidget * parent)115 RevisionTree::RevisionTree(const svn::ClientP &aClient,
116                            CContextListener *aListener,
117                            const QString &reposRoot,
118                            const svn::Revision &startr, const svn::Revision &endr,
119                            const QString &origin,
120                            const svn::Revision &baserevision, QWidget *parent)
121     : m_InitialRevsion(0), m_Path(origin), m_Valid(false)
122 {
123     m_Data = new RtreeData;
124     m_Data->m_Client = aClient;
125     m_Data->m_Listener = aListener;
126     m_Data->dlgParent = parent;
127 
128     if (!m_Data->getLogs(reposRoot, startr, endr, origin)) {
129         return;
130     }
131 
132     long possible_rev = -1;
133 
134     m_Data->progress = new QProgressDialog(i18n("Scanning the logs for %1", origin), i18n("Cancel"), 0, m_Data->m_OldHistory.size(), parent);
135     m_Data->progress->setWindowTitle(i18nc("@title:window", "Scanning logs"));
136     m_Data->progress->setMinimumDuration(100);
137     m_Data->progress->setAutoClose(false);
138     m_Data->progress->setWindowModality(Qt::WindowModal);
139     bool cancel = false;
140     svn::LogEntriesMap::Iterator it;
141     unsigned count = 0;
142     for (it = m_Data->m_OldHistory.begin(); it != m_Data->m_OldHistory.end(); ++it) {
143         m_Data->progress->setValue(count);
144         QCoreApplication::processEvents();
145         if (m_Data->progress->wasCanceled()) {
146             cancel = true;
147             break;
148         }
149         if (it.key() > m_Data->max_rev) {
150             m_Data->max_rev = it.key();
151         }
152         if (it.key() < m_Data->min_rev || m_Data->min_rev == -1) {
153             m_Data->min_rev = it.key();
154         }
155         if (baserevision.kind() == svn_opt_revision_date) {
156             if ((baserevision.date() <= it.value().date && possible_rev == -1) || possible_rev > it.key()) {
157                 possible_rev = it.key();
158             }
159         }
160         ++count;
161     }
162     if (baserevision.kind() == svn_opt_revision_head || baserevision.kind() == svn_opt_revision_working) {
163         m_Baserevision = m_Data->max_rev;
164     } else if (baserevision.kind() == svn_opt_revision_number) {
165         m_Baserevision = baserevision.revnum();
166     } else if (baserevision.kind() == svn_opt_revision_date) {
167         m_Baserevision = possible_rev;
168     } else {
169         m_Baserevision = m_Data->min_rev;
170     }
171     if (!cancel) {
172         if (topDownScan()) {
173             m_Data->progress->setAutoReset(true);
174             m_Data->progress->setRange(0, 100);
175             m_Data->m_stopTick.restart();
176             m_Data->m_TreeDisplay = new RevTreeWidget(m_Data->m_Client);
177             if (bottomUpScan(m_InitialRevsion, 0, m_Path, 0)) {
178                 m_Valid = true;
179                 m_Data->m_TreeDisplay->setBasePath(reposRoot);
180                 m_Data->m_TreeDisplay->dumpRevtree();
181             } else {
182                 delete m_Data->m_TreeDisplay;
183                 m_Data->m_TreeDisplay = nullptr;
184             }
185         }
186     }
187     m_Data->progress->hide();
188 }
189 
~RevisionTree()190 RevisionTree::~RevisionTree()
191 {
192     delete m_Data;
193 }
194 
isDeleted(long revision,const QString & path)195 bool RevisionTree::isDeleted(long revision, const QString &path)
196 {
197     for (long i = 0; i < m_Data->m_History[revision].changedPaths.count(); ++i) {
198         if (isParent(m_Data->m_History[revision].changedPaths[i].path, path) &&
199                 m_Data->m_History[revision].changedPaths[i].action == 'D') {
200             return true;
201         }
202     }
203     return false;
204 }
205 
topDownScan()206 bool RevisionTree::topDownScan()
207 {
208     m_Data->progress->setRange(0, m_Data->max_rev - m_Data->min_rev);
209     bool cancel = false;
210     QString label;
211     QString olabel = m_Data->progress->labelText();
212     for (long j = m_Data->max_rev; j >= m_Data->min_rev; --j) {
213         m_Data->progress->setValue(m_Data->max_rev - j);
214         QCoreApplication::processEvents();
215         if (m_Data->progress->wasCanceled()) {
216             cancel = true;
217             break;
218         }
219         for (long i = 0; i < m_Data->m_OldHistory[j].changedPaths.count(); ++i) {
220             if (i > 0 && i % 100 == 0) {
221                 if (m_Data->progress->wasCanceled()) {
222                     cancel = true;
223                     break;
224                 }
225                 label = i18n("%1<br>Check change entry %2 of %3", olabel, i, m_Data->m_OldHistory[j].changedPaths.count());
226                 m_Data->progress->setLabelText(label);
227                 QCoreApplication::processEvents();
228             }
229             /* find min revision of item */
230             if (m_Data->m_OldHistory[j].changedPaths[i].action == 'A' &&
231                     isParent(m_Data->m_OldHistory[j].changedPaths[i].path, m_Path)) {
232                 if (!m_Data->m_OldHistory[j].changedPaths[i].copyFromPath.isEmpty()) {
233                     if (m_InitialRevsion < m_Data->m_OldHistory[j].revision) {
234                         QString r = m_Path.mid(m_Data->m_OldHistory[j].changedPaths[i].path.length());
235                         m_Path = m_Data->m_OldHistory[j].changedPaths[i].copyFromPath;
236                         m_Path += r;
237                     }
238                 } else if (m_Data->m_OldHistory[j].changedPaths[i].path == m_Path && m_Data->m_OldHistory[j].changedPaths[i].copyToPath.isEmpty()) {
239                     // here it is added
240                     m_InitialRevsion = m_Data->m_OldHistory[j].revision;
241                 }
242             }
243         }
244     }
245     if (cancel == true) {
246         return false;
247     }
248     m_Data->progress->setLabelText(olabel);
249     /* find forward references and filter them out */
250     for (long j = m_Data->max_rev; j >= m_Data->min_rev; --j) {
251         m_Data->progress->setValue(m_Data->max_rev - j);
252         QCoreApplication::processEvents();
253         if (m_Data->progress->wasCanceled()) {
254             cancel = true;
255             break;
256         }
257         for (long i = 0; i < m_Data->m_OldHistory[j].changedPaths.count(); ++i) {
258             if (i > 0 && i % 100 == 0) {
259                 if (m_Data->progress->wasCanceled()) {
260                     cancel = true;
261                     break;
262                 }
263                 label = i18n("%1<br>Check change entry %2 of %3", olabel, i, m_Data->m_OldHistory[j].changedPaths.count());
264                 m_Data->progress->setLabelText(label);
265                 QCoreApplication::processEvents();
266             }
267             if (!m_Data->m_OldHistory[j].changedPaths[i].copyFromPath.isEmpty()) {
268                 long r = m_Data->m_OldHistory[j].changedPaths[i].copyFromRevision;
269                 QString sourcepath = m_Data->m_OldHistory[j].changedPaths[i].copyFromPath;
270                 char a = m_Data->m_OldHistory[j].changedPaths[i].action;
271                 if (m_Data->m_OldHistory[j].changedPaths[i].path.isEmpty()) {
272                     continue;
273                 }
274                 if (a == 'R') {
275                     m_Data->m_OldHistory[j].changedPaths[i].action = 0;
276                 } else if (a == 'A') {
277                     a = INTERNALCOPY;
278                     for (long z = 0; z < m_Data->m_OldHistory[j].changedPaths.count(); ++z) {
279                         if (m_Data->m_OldHistory[j].changedPaths[z].action == 'D'
280                                 && isParent(m_Data->m_OldHistory[j].changedPaths[z].path, sourcepath)) {
281                             a = INTERNALRENAME;
282                             m_Data->m_OldHistory[j].changedPaths[z].action = 0;
283                             break;
284                         }
285                     }
286                     m_Data->m_History[r].addCopyTo(sourcepath, m_Data->m_OldHistory[j].changedPaths[i].path, j, a, r);
287                     m_Data->m_OldHistory[j].changedPaths[i].action = 0;
288                 }
289             }
290         }
291     }
292     if (cancel == true) {
293         return false;
294     }
295     m_Data->progress->setLabelText(olabel);
296     for (long j = m_Data->max_rev; j >= m_Data->min_rev; --j) {
297         m_Data->progress->setValue(m_Data->max_rev - j);
298         QCoreApplication::processEvents();
299         if (m_Data->progress->wasCanceled()) {
300             cancel = true;
301             break;
302         }
303         for (long i = 0; i < m_Data->m_OldHistory[j].changedPaths.count(); ++i) {
304             if (m_Data->m_OldHistory[j].changedPaths[i].action == 0) {
305                 continue;
306             }
307             if (i > 0 && i % 100 == 0) {
308                 if (m_Data->progress->wasCanceled()) {
309                     cancel = true;
310                     break;
311                 }
312                 label = i18n("%1<br>Check change entry %2 of %3", olabel, i, m_Data->m_OldHistory[j].changedPaths.count());
313                 m_Data->progress->setLabelText(label);
314                 QCoreApplication::processEvents();
315             }
316             m_Data->m_History[j].addCopyTo(m_Data->m_OldHistory[j].changedPaths[i].path, QString(), -1, m_Data->m_OldHistory[j].changedPaths[i].action);
317         }
318         m_Data->m_History[j].author = m_Data->m_OldHistory[j].author;
319         m_Data->m_History[j].date = m_Data->m_OldHistory[j].date;
320         m_Data->m_History[j].revision = m_Data->m_OldHistory[j].revision;
321         m_Data->m_History[j].message = m_Data->m_OldHistory[j].message;
322     }
323     return !cancel;
324 }
325 
isParent(const QString & _par,const QString & tar)326 bool RevisionTree::isParent(const QString &_par, const QString &tar)
327 {
328     if (_par == tar) {
329         return true;
330     }
331     QString par = _par.endsWith(QLatin1Char('/')) ? _par : _par + QLatin1Char('/');
332     return tar.startsWith(par);
333 }
334 
isValid() const335 bool RevisionTree::isValid()const
336 {
337     return m_Valid;
338 }
339 
uniqueNodeName(long rev,const QString & path)340 static QString uniqueNodeName(long rev, const QString &path)
341 {
342     QString res = QString::fromUtf8(path.toLocal8Bit().toBase64());
343     res.replace(QLatin1Char('\"'), QLatin1String("_quot_"));
344     res.replace(QLatin1Char(' '), QLatin1String("_space_"));
345     QString n; n.sprintf("%05ld", rev);
346     return QLatin1Char('\"') + n + QLatin1Char('_') + res + QLatin1Char('\"');
347 }
348 
bottomUpScan(long startrev,unsigned recurse,const QString & _path,long _last)349 bool RevisionTree::bottomUpScan(long startrev, unsigned recurse, const QString &_path, long _last)
350 {
351 #define REVENTRY m_Data->m_History[j]
352 #define FORWARDENTRY m_Data->m_History[j].changedPaths[i]
353 
354     QString path = _path;
355     long lastrev = _last;
356 #ifdef DEBUG_PARSE
357     qCDebug(KDESVN_LOG) << "Searching for " << path << " at revision " << startrev
358                  << " recursion " << recurse << endl;
359 #endif
360     bool cancel = false;
361     for (long j = startrev; j <= m_Data->max_rev; ++j) {
362         if (m_Data->m_stopTick.elapsed() > 500) {
363             m_Data->progress->setValue(m_Data->progress->value() + 1);
364             QCoreApplication::processEvents();
365             m_Data->m_stopTick.restart();
366         }
367         if (m_Data->progress->wasCanceled()) {
368             cancel = true;
369             break;
370         }
371         for (long i = 0; i < REVENTRY.changedPaths.count(); ++i) {
372             if (!isParent(FORWARDENTRY.path, path)) {
373                 continue;
374             }
375             QString n1, n2;
376             if (isParent(FORWARDENTRY.path, path)) {
377                 bool get_out = false;
378                 if (FORWARDENTRY.path != path) {
379 #ifdef DEBUG_PARSE
380                     qCDebug(KDESVN_LOG) << "Parent rename? " << FORWARDENTRY.path << " -> " << FORWARDENTRY.copyToPath << " -> " << FORWARDENTRY.copyFromPath << endl;
381 #endif
382                 }
383                 if (FORWARDENTRY.action == INTERNALCOPY ||
384                         FORWARDENTRY.action == INTERNALRENAME) {
385                     bool ren = FORWARDENTRY.action == INTERNALRENAME;
386                     QString tmpPath = path;
387                     QString recPath;
388                     if (FORWARDENTRY.copyToPath.length() == 0) {
389                         continue;
390                     }
391                     QString r = path.mid(FORWARDENTRY.path.length());
392                     recPath = FORWARDENTRY.copyToPath;
393                     recPath += r;
394                     n1 = uniqueNodeName(lastrev, tmpPath);
395                     n2 = uniqueNodeName(FORWARDENTRY.copyToRevision, recPath);
396                     if (lastrev > 0) {
397                         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n1].targets.append(RevGraphView::targetData(n2, FORWARDENTRY.action));
398                     }
399                     m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].name = recPath;
400                     m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].rev = FORWARDENTRY.copyToRevision;
401                     m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].Action = FORWARDENTRY.action;
402                     m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].Author = m_Data->m_History[FORWARDENTRY.copyToRevision].author;
403                     m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].Message = m_Data->m_History[FORWARDENTRY.copyToRevision].message;
404                     m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].Date = svn::DateTime(m_Data->m_History[FORWARDENTRY.copyToRevision].date).toString();
405                     if (ren) {
406                         lastrev = FORWARDENTRY.copyToRevision;
407                         /* skip items between */
408 #ifdef DEBUG_PARSE
409                         qCDebug(KDESVN_LOG) << "Renamed to " << recPath << " at revision " << FORWARDENTRY.copyToRevision << endl;
410 #endif
411                         j = lastrev;
412                         path = recPath;
413                     } else {
414 #ifdef DEBUG_PARSE
415                         qCDebug(KDESVN_LOG) << "Copy to " << recPath << endl;
416 #endif
417                         if (!bottomUpScan(FORWARDENTRY.copyToRevision, recurse + 1, recPath, FORWARDENTRY.copyToRevision)) {
418                             return false;
419                         }
420                     }
421                 } else if (FORWARDENTRY.path == path) {
422                     switch (FORWARDENTRY.action) {
423                     case 'A':
424 #ifdef DEBUG_PARSE
425                         qCDebug(KDESVN_LOG) << "Inserting adding base item" << endl;
426 #endif
427                         n1 = uniqueNodeName(j, FORWARDENTRY.path);
428                         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n1].Action = FORWARDENTRY.action;
429                         fillItem(j, i, n1, path);
430                         lastrev = j;
431                         break;
432                     case 'M':
433                     case 'R':
434 #ifdef DEBUG_PARSE
435                         qCDebug(KDESVN_LOG) << "Item modified at revision " << j << " recurse " << recurse << endl;
436 #endif
437                         n1 = uniqueNodeName(j, FORWARDENTRY.path);
438                         n2 = uniqueNodeName(lastrev, FORWARDENTRY.path);
439                         if (lastrev > 0) {
440                             m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].targets.append(RevGraphView::targetData(n1, FORWARDENTRY.action));
441                         }
442                         fillItem(j, i, n1, path);
443                         /* modify of same item (in same recurse) should be only once at a revision
444                          * so check if lastrev==j must not be done but will cost cpu ticks so I always
445                          * set trev and lastrev.
446                          */
447                         lastrev = j;
448                         break;
449                     case 'D':
450 #ifdef DEBUG_PARSE
451                         qCDebug(KDESVN_LOG) << "(Sloppy match) Item deleted at revision " << j << " recurse " << recurse << endl;
452 #endif
453                         n1 = uniqueNodeName(j, path);
454                         n2 = uniqueNodeName(lastrev, path);
455                         if (n1 == n2) {
456                             /* cvs import - copy and deletion at same revision.
457                              * CVS sucks.
458                              */
459                             n1 = uniqueNodeName(j, "D_" + path);
460                         }
461                         if (lastrev > 0) {
462                             m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].targets.append(RevGraphView::targetData(n1, FORWARDENTRY.action));
463                         }
464                         fillItem(j, i, n1, path);
465                         lastrev = j;
466                         get_out = true;
467                         break;
468                     default:
469                         break;
470                     }
471                 } else {
472                     switch (FORWARDENTRY.action) {
473                     case 'D':
474 #ifdef DEBUG_PARSE
475                         qCDebug(KDESVN_LOG) << "(Exact match) Item deleted at revision " << j << " recurse " << recurse << endl;
476 #endif
477                         n1 = uniqueNodeName(j, path);
478                         n2 = uniqueNodeName(lastrev, path);
479                         if (n1 == n2) {
480                             /* cvs import - copy and deletion at same revision.
481                              * CVS sucks.
482                              */
483                             n1 = uniqueNodeName(j, "D_" + path);
484                         }
485                         if (lastrev > 0) {
486                             m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].targets.append(RevGraphView::targetData(n1, FORWARDENTRY.action));
487                         }
488                         fillItem(j, i, n1, path);
489                         lastrev = j;
490                         get_out = true;
491                         break;
492                     default:
493                         break;
494                     }
495                 }
496                 if (get_out) {
497                     return true;
498                 }
499             }
500         }
501     }
502     return !cancel;
503 }
504 
getView()505 RevTreeWidget *RevisionTree::getView()
506 {
507     return m_Data->m_TreeDisplay;
508 }
509 
fillItem(long rev,int pathIndex,const QString & nodeName,const QString & path)510 void RevisionTree::fillItem(long rev, int pathIndex, const QString &nodeName, const QString &path)
511 {
512     m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].name = path;
513     m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].rev = rev;
514     if (pathIndex >= 0) {
515         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Action = m_Data->m_History[rev].changedPaths[pathIndex].action;
516         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Author = m_Data->m_History[rev].author;
517         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Message = m_Data->m_History[rev].message;
518         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Date = svn::DateTime(m_Data->m_History[rev].date).toString();
519     } else {
520         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Action = 0;
521         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Author.clear();
522         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Message.clear();
523         m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Date = svn::DateTime(0).toString();
524     }
525 }
526