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