File indexing completed on 2024-05-12 05:44:26
0001 /*************************************************************************** 0002 * Copyright (C) 2005-2009 by Rajko Albrecht * 0003 * ral@alwins-world.de * 0004 * * 0005 * This program is free software; you can redistribute it and/or modify * 0006 * it under the terms of the GNU General Public License as published by * 0007 * the Free Software Foundation; either version 2 of the License, or * 0008 * (at your option) any later version. * 0009 * * 0010 * This program is distributed in the hope that it will be useful, * 0011 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0013 * GNU General Public License for more details. * 0014 * * 0015 * You should have received a copy of the GNU General Public License * 0016 * along with this program; if not, write to the * 0017 * Free Software Foundation, Inc., * 0018 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 0019 ***************************************************************************/ 0020 #include "revisiontree.h" 0021 #include "../ccontextlistener.h" 0022 #include "../stopdlg.h" 0023 #include "elogentry.h" 0024 #include "revgraphview.h" 0025 #include "revtreewidget.h" 0026 #include "settings/kdesvnsettings.h" 0027 #include "svnfrontend/fronthelpers/cursorstack.h" 0028 #include "svnqt/cache/LogCache.h" 0029 #include "svnqt/cache/ReposConfig.h" 0030 #include "svnqt/cache/ReposLog.h" 0031 #include "svnqt/client_parameter.h" 0032 #include "svnqt/log_entry.h" 0033 #include "svnqt/url.h" 0034 0035 #include <KLocalizedString> 0036 #include <KMessageBox> 0037 0038 #include <QProgressDialog> 0039 #include <QWidget> 0040 0041 #define INTERNALCOPY 1 0042 #define INTERNALRENAME 2 0043 0044 class CContextListener; 0045 class RtreeData 0046 { 0047 public: 0048 RtreeData(); 0049 virtual ~RtreeData(); 0050 0051 QMap<long, eLog_Entry> m_History; 0052 0053 svn::LogEntriesMap m_OldHistory; 0054 0055 long max_rev, min_rev; 0056 QProgressDialog *progress; 0057 QTime m_stopTick; 0058 0059 QWidget *dlgParent; 0060 RevTreeWidget *m_TreeDisplay; 0061 0062 svn::ClientP m_Client; 0063 CContextListener *m_Listener; 0064 0065 bool getLogs(const QString &, const svn::Revision &startr, const svn::Revision &endr, const QString &origin); 0066 }; 0067 0068 RtreeData::RtreeData() 0069 : max_rev(-1) 0070 , min_rev(-1) 0071 { 0072 progress = nullptr; 0073 m_TreeDisplay = nullptr; 0074 dlgParent = nullptr; 0075 m_Listener = nullptr; 0076 } 0077 0078 RtreeData::~RtreeData() 0079 { 0080 delete progress; 0081 } 0082 0083 bool RtreeData::getLogs(const QString &reposRoot, const svn::Revision &startr, const svn::Revision &endr, const QString &origin) 0084 { 0085 Q_UNUSED(origin); 0086 if (!m_Listener || !m_Client) { 0087 return false; 0088 } 0089 svn::LogParameter params; 0090 params.targets(reposRoot).revisionRange(endr, startr).peg(startr).limit(0).discoverChangedPathes(true).strictNodeHistory(false); 0091 const svn::StringArray ex(svn::cache::ReposConfig::self()->readEntry(reposRoot, "tree_exclude_list", QStringList())); 0092 try { 0093 CursorStack a(Qt::BusyCursor); 0094 StopDlg sdlg(m_Listener, dlgParent, i18nc("@title:window", "Logs"), i18n("Getting logs - hit Cancel for abort")); 0095 if (svn::Url::isLocal(reposRoot)) { 0096 m_Client->log(params.excludeList(ex), m_OldHistory); 0097 } else { 0098 svn::cache::ReposLog rl(m_Client, reposRoot); 0099 if (rl.isValid()) { 0100 rl.simpleLog(m_OldHistory, startr, endr, (!Kdesvnsettings::network_on() || !Kdesvnsettings::fill_cache_on_tree()), ex); 0101 } else if (Kdesvnsettings::network_on()) { 0102 m_Client->log(params.excludeList(ex), m_OldHistory); 0103 } else { 0104 KMessageBox::error(nullptr, 0105 i18n("Could not retrieve logs, reason:\n%1", i18n("No log cache possible due broken database and networking not allowed."))); 0106 return false; 0107 } 0108 } 0109 } catch (const svn::Exception &ce) { 0110 KMessageBox::error(nullptr, i18n("Could not retrieve logs, reason:\n%1", ce.msg())); 0111 return false; 0112 } 0113 return true; 0114 } 0115 0116 RevisionTree::RevisionTree(const svn::ClientP &aClient, 0117 CContextListener *aListener, 0118 const QString &reposRoot, 0119 const svn::Revision &startr, 0120 const svn::Revision &endr, 0121 const QString &origin, 0122 const svn::Revision &baserevision, 0123 QWidget *parent) 0124 : m_InitialRevsion(0) 0125 , m_Path(origin) 0126 , m_Valid(false) 0127 { 0128 m_Data = new RtreeData; 0129 m_Data->m_Client = aClient; 0130 m_Data->m_Listener = aListener; 0131 m_Data->dlgParent = parent; 0132 0133 if (!m_Data->getLogs(reposRoot, startr, endr, origin)) { 0134 return; 0135 } 0136 0137 long possible_rev = -1; 0138 0139 m_Data->progress = new QProgressDialog(i18n("Scanning the logs for %1", origin), i18n("Cancel"), 0, m_Data->m_OldHistory.size(), parent); 0140 m_Data->progress->setWindowTitle(i18nc("@title:window", "Scanning logs")); 0141 m_Data->progress->setMinimumDuration(100); 0142 m_Data->progress->setAutoClose(false); 0143 m_Data->progress->setWindowModality(Qt::WindowModal); 0144 bool cancel = false; 0145 int count = 0; 0146 for (auto it = m_Data->m_OldHistory.begin(); it != m_Data->m_OldHistory.end(); ++it) { 0147 m_Data->progress->setValue(count); 0148 QCoreApplication::processEvents(); 0149 if (m_Data->progress->wasCanceled()) { 0150 cancel = true; 0151 break; 0152 } 0153 if (it.key() > m_Data->max_rev) { 0154 m_Data->max_rev = it.key(); 0155 } 0156 if (it.key() < m_Data->min_rev || m_Data->min_rev == -1) { 0157 m_Data->min_rev = it.key(); 0158 } 0159 if (baserevision.kind() == svn_opt_revision_date) { 0160 if ((baserevision.date() <= it.value().date && possible_rev == -1) || possible_rev > it.key()) { 0161 possible_rev = it.key(); 0162 } 0163 } 0164 ++count; 0165 } 0166 if (baserevision.kind() == svn_opt_revision_head || baserevision.kind() == svn_opt_revision_working) { 0167 m_Baserevision = m_Data->max_rev; 0168 } else if (baserevision.kind() == svn_opt_revision_number) { 0169 m_Baserevision = baserevision.revnum(); 0170 } else if (baserevision.kind() == svn_opt_revision_date) { 0171 m_Baserevision = possible_rev; 0172 } else { 0173 m_Baserevision = m_Data->min_rev; 0174 } 0175 if (!cancel) { 0176 if (topDownScan()) { 0177 m_Data->progress->setAutoReset(true); 0178 m_Data->progress->setRange(0, 100); 0179 m_Data->m_stopTick.restart(); 0180 m_Data->m_TreeDisplay = new RevTreeWidget(m_Data->m_Client); 0181 if (bottomUpScan(m_InitialRevsion, 0, m_Path, 0)) { 0182 m_Valid = true; 0183 m_Data->m_TreeDisplay->setBasePath(reposRoot); 0184 m_Data->m_TreeDisplay->dumpRevtree(); 0185 } else { 0186 delete m_Data->m_TreeDisplay; 0187 m_Data->m_TreeDisplay = nullptr; 0188 } 0189 } 0190 } 0191 m_Data->progress->hide(); 0192 } 0193 0194 RevisionTree::~RevisionTree() 0195 { 0196 delete m_Data; 0197 } 0198 0199 bool RevisionTree::isDeleted(long revision, const QString &path) 0200 { 0201 for (long i = 0; i < m_Data->m_History[revision].changedPaths.count(); ++i) { 0202 if (isParent(m_Data->m_History[revision].changedPaths[i].path, path) && m_Data->m_History[revision].changedPaths[i].action == 'D') { 0203 return true; 0204 } 0205 } 0206 return false; 0207 } 0208 0209 bool RevisionTree::topDownScan() 0210 { 0211 m_Data->progress->setRange(0, m_Data->max_rev - m_Data->min_rev); 0212 bool cancel = false; 0213 QString label; 0214 QString olabel = m_Data->progress->labelText(); 0215 for (long j = m_Data->max_rev; j >= m_Data->min_rev; --j) { 0216 m_Data->progress->setValue(m_Data->max_rev - j); 0217 QCoreApplication::processEvents(); 0218 if (m_Data->progress->wasCanceled()) { 0219 cancel = true; 0220 break; 0221 } 0222 for (long i = 0; i < m_Data->m_OldHistory[j].changedPaths.count(); ++i) { 0223 if (i > 0 && i % 100 == 0) { 0224 if (m_Data->progress->wasCanceled()) { 0225 cancel = true; 0226 break; 0227 } 0228 label = i18n("%1<br>Check change entry %2 of %3", olabel, i, m_Data->m_OldHistory[j].changedPaths.count()); 0229 m_Data->progress->setLabelText(label); 0230 QCoreApplication::processEvents(); 0231 } 0232 /* find min revision of item */ 0233 if (m_Data->m_OldHistory[j].changedPaths[i].action == 'A' && isParent(m_Data->m_OldHistory[j].changedPaths[i].path, m_Path)) { 0234 if (!m_Data->m_OldHistory[j].changedPaths[i].copyFromPath.isEmpty()) { 0235 if (m_InitialRevsion < m_Data->m_OldHistory[j].revision) { 0236 QString r = m_Path.mid(m_Data->m_OldHistory[j].changedPaths[i].path.length()); 0237 m_Path = m_Data->m_OldHistory[j].changedPaths[i].copyFromPath; 0238 m_Path += r; 0239 } 0240 } else if (m_Data->m_OldHistory[j].changedPaths[i].path == m_Path && m_Data->m_OldHistory[j].changedPaths[i].copyToPath.isEmpty()) { 0241 // here it is added 0242 m_InitialRevsion = m_Data->m_OldHistory[j].revision; 0243 } 0244 } 0245 } 0246 } 0247 if (cancel == true) { 0248 return false; 0249 } 0250 m_Data->progress->setLabelText(olabel); 0251 /* find forward references and filter them out */ 0252 for (long j = m_Data->max_rev; j >= m_Data->min_rev; --j) { 0253 m_Data->progress->setValue(m_Data->max_rev - j); 0254 QCoreApplication::processEvents(); 0255 if (m_Data->progress->wasCanceled()) { 0256 cancel = true; 0257 break; 0258 } 0259 for (long i = 0; i < m_Data->m_OldHistory[j].changedPaths.count(); ++i) { 0260 if (i > 0 && i % 100 == 0) { 0261 if (m_Data->progress->wasCanceled()) { 0262 cancel = true; 0263 break; 0264 } 0265 label = i18n("%1<br>Check change entry %2 of %3", olabel, i, m_Data->m_OldHistory[j].changedPaths.count()); 0266 m_Data->progress->setLabelText(label); 0267 QCoreApplication::processEvents(); 0268 } 0269 if (!m_Data->m_OldHistory[j].changedPaths[i].copyFromPath.isEmpty()) { 0270 long r = m_Data->m_OldHistory[j].changedPaths[i].copyFromRevision; 0271 QString sourcepath = m_Data->m_OldHistory[j].changedPaths[i].copyFromPath; 0272 char a = m_Data->m_OldHistory[j].changedPaths[i].action; 0273 if (m_Data->m_OldHistory[j].changedPaths[i].path.isEmpty()) { 0274 continue; 0275 } 0276 if (a == 'R') { 0277 m_Data->m_OldHistory[j].changedPaths[i].action = 0; 0278 } else if (a == 'A') { 0279 a = INTERNALCOPY; 0280 for (long z = 0; z < m_Data->m_OldHistory[j].changedPaths.count(); ++z) { 0281 if (m_Data->m_OldHistory[j].changedPaths[z].action == 'D' && isParent(m_Data->m_OldHistory[j].changedPaths[z].path, sourcepath)) { 0282 a = INTERNALRENAME; 0283 m_Data->m_OldHistory[j].changedPaths[z].action = 0; 0284 break; 0285 } 0286 } 0287 m_Data->m_History[r].addCopyTo(sourcepath, m_Data->m_OldHistory[j].changedPaths[i].path, j, a, r); 0288 m_Data->m_OldHistory[j].changedPaths[i].action = 0; 0289 } 0290 } 0291 } 0292 } 0293 if (cancel == true) { 0294 return false; 0295 } 0296 m_Data->progress->setLabelText(olabel); 0297 for (long j = m_Data->max_rev; j >= m_Data->min_rev; --j) { 0298 m_Data->progress->setValue(m_Data->max_rev - j); 0299 QCoreApplication::processEvents(); 0300 if (m_Data->progress->wasCanceled()) { 0301 cancel = true; 0302 break; 0303 } 0304 for (long i = 0; i < m_Data->m_OldHistory[j].changedPaths.count(); ++i) { 0305 if (m_Data->m_OldHistory[j].changedPaths[i].action == 0) { 0306 continue; 0307 } 0308 if (i > 0 && i % 100 == 0) { 0309 if (m_Data->progress->wasCanceled()) { 0310 cancel = true; 0311 break; 0312 } 0313 label = i18n("%1<br>Check change entry %2 of %3", olabel, i, m_Data->m_OldHistory[j].changedPaths.count()); 0314 m_Data->progress->setLabelText(label); 0315 QCoreApplication::processEvents(); 0316 } 0317 m_Data->m_History[j].addCopyTo(m_Data->m_OldHistory[j].changedPaths[i].path, QString(), -1, m_Data->m_OldHistory[j].changedPaths[i].action); 0318 } 0319 m_Data->m_History[j].author = m_Data->m_OldHistory[j].author; 0320 m_Data->m_History[j].date = m_Data->m_OldHistory[j].date; 0321 m_Data->m_History[j].revision = m_Data->m_OldHistory[j].revision; 0322 m_Data->m_History[j].message = m_Data->m_OldHistory[j].message; 0323 } 0324 return !cancel; 0325 } 0326 0327 bool RevisionTree::isParent(const QString &_par, const QString &tar) 0328 { 0329 if (_par == tar) { 0330 return true; 0331 } 0332 QString par = _par.endsWith(QLatin1Char('/')) ? _par : _par + QLatin1Char('/'); 0333 return tar.startsWith(par); 0334 } 0335 0336 bool RevisionTree::isValid() const 0337 { 0338 return m_Valid; 0339 } 0340 0341 static QString uniqueNodeName(long rev, const QString &path) 0342 { 0343 QString res = QString::fromUtf8(path.toLocal8Bit().toBase64()); 0344 res.replace(QLatin1Char('\"'), QLatin1String("_quot_")); 0345 res.replace(QLatin1Char(' '), QLatin1String("_space_")); 0346 QString n = QString::asprintf("%05ld", rev); 0347 return QLatin1Char('\"') + n + QLatin1Char('_') + res + QLatin1Char('\"'); 0348 } 0349 0350 bool RevisionTree::bottomUpScan(long startrev, unsigned recurse, const QString &_path, long _last) 0351 { 0352 #define REVENTRY m_Data->m_History[j] 0353 #define FORWARDENTRY m_Data->m_History[j].changedPaths[i] 0354 0355 QString path = _path; 0356 long lastrev = _last; 0357 #ifdef DEBUG_PARSE 0358 qCDebug(KDESVN_LOG) << "Searching for " << path << " at revision " << startrev << " recursion " << recurse << Qt::endl; 0359 #endif 0360 bool cancel = false; 0361 for (long j = startrev; j <= m_Data->max_rev; ++j) { 0362 if (m_Data->m_stopTick.elapsed() > 500) { 0363 m_Data->progress->setValue(m_Data->progress->value() + 1); 0364 QCoreApplication::processEvents(); 0365 m_Data->m_stopTick.restart(); 0366 } 0367 if (m_Data->progress->wasCanceled()) { 0368 cancel = true; 0369 break; 0370 } 0371 for (long i = 0; i < REVENTRY.changedPaths.count(); ++i) { 0372 if (!isParent(FORWARDENTRY.path, path)) { 0373 continue; 0374 } 0375 QString n1, n2; 0376 if (isParent(FORWARDENTRY.path, path)) { 0377 bool get_out = false; 0378 if (FORWARDENTRY.path != path) { 0379 #ifdef DEBUG_PARSE 0380 qCDebug(KDESVN_LOG) << "Parent rename? " << FORWARDENTRY.path << " -> " << FORWARDENTRY.copyToPath << " -> " << FORWARDENTRY.copyFromPath 0381 << Qt::endl; 0382 #endif 0383 } 0384 if (FORWARDENTRY.action == INTERNALCOPY || FORWARDENTRY.action == INTERNALRENAME) { 0385 bool ren = FORWARDENTRY.action == INTERNALRENAME; 0386 QString tmpPath = path; 0387 QString recPath; 0388 if (FORWARDENTRY.copyToPath.length() == 0) { 0389 continue; 0390 } 0391 QString r = path.mid(FORWARDENTRY.path.length()); 0392 recPath = FORWARDENTRY.copyToPath; 0393 recPath += r; 0394 n1 = uniqueNodeName(lastrev, tmpPath); 0395 n2 = uniqueNodeName(FORWARDENTRY.copyToRevision, recPath); 0396 if (lastrev > 0) { 0397 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n1].targets.append(RevGraphView::targetData(n2, FORWARDENTRY.action)); 0398 } 0399 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].name = recPath; 0400 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].rev = FORWARDENTRY.copyToRevision; 0401 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].Action = FORWARDENTRY.action; 0402 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].Author = m_Data->m_History[FORWARDENTRY.copyToRevision].author; 0403 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].Message = m_Data->m_History[FORWARDENTRY.copyToRevision].message; 0404 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].Date = svn::DateTime(m_Data->m_History[FORWARDENTRY.copyToRevision].date).toString(); 0405 if (ren) { 0406 lastrev = FORWARDENTRY.copyToRevision; 0407 /* skip items between */ 0408 #ifdef DEBUG_PARSE 0409 qCDebug(KDESVN_LOG) << "Renamed to " << recPath << " at revision " << FORWARDENTRY.copyToRevision << Qt::endl; 0410 #endif 0411 j = lastrev; 0412 path = recPath; 0413 } else { 0414 #ifdef DEBUG_PARSE 0415 qCDebug(KDESVN_LOG) << "Copy to " << recPath << Qt::endl; 0416 #endif 0417 if (!bottomUpScan(FORWARDENTRY.copyToRevision, recurse + 1, recPath, FORWARDENTRY.copyToRevision)) { 0418 return false; 0419 } 0420 } 0421 } else if (FORWARDENTRY.path == path) { 0422 switch (FORWARDENTRY.action) { 0423 case 'A': 0424 #ifdef DEBUG_PARSE 0425 qCDebug(KDESVN_LOG) << "Inserting adding base item" << Qt::endl; 0426 #endif 0427 n1 = uniqueNodeName(j, FORWARDENTRY.path); 0428 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n1].Action = FORWARDENTRY.action; 0429 fillItem(j, i, n1, path); 0430 lastrev = j; 0431 break; 0432 case 'M': 0433 case 'R': 0434 #ifdef DEBUG_PARSE 0435 qCDebug(KDESVN_LOG) << "Item modified at revision " << j << " recurse " << recurse << Qt::endl; 0436 #endif 0437 n1 = uniqueNodeName(j, FORWARDENTRY.path); 0438 n2 = uniqueNodeName(lastrev, FORWARDENTRY.path); 0439 if (lastrev > 0) { 0440 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].targets.append(RevGraphView::targetData(n1, FORWARDENTRY.action)); 0441 } 0442 fillItem(j, i, n1, path); 0443 /* modify of same item (in same recurse) should be only once at a revision 0444 * so check if lastrev==j must not be done but will cost cpu ticks so I always 0445 * set trev and lastrev. 0446 */ 0447 lastrev = j; 0448 break; 0449 case 'D': 0450 #ifdef DEBUG_PARSE 0451 qCDebug(KDESVN_LOG) << "(Sloppy match) Item deleted at revision " << j << " recurse " << recurse << Qt::endl; 0452 #endif 0453 n1 = uniqueNodeName(j, path); 0454 n2 = uniqueNodeName(lastrev, path); 0455 if (n1 == n2) { 0456 /* cvs import - copy and deletion at same revision. 0457 * CVS sucks. 0458 */ 0459 n1 = uniqueNodeName(j, "D_" + path); 0460 } 0461 if (lastrev > 0) { 0462 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].targets.append(RevGraphView::targetData(n1, FORWARDENTRY.action)); 0463 } 0464 fillItem(j, i, n1, path); 0465 lastrev = j; 0466 get_out = true; 0467 break; 0468 default: 0469 break; 0470 } 0471 } else { 0472 switch (FORWARDENTRY.action) { 0473 case 'D': 0474 #ifdef DEBUG_PARSE 0475 qCDebug(KDESVN_LOG) << "(Exact match) Item deleted at revision " << j << " recurse " << recurse << Qt::endl; 0476 #endif 0477 n1 = uniqueNodeName(j, path); 0478 n2 = uniqueNodeName(lastrev, path); 0479 if (n1 == n2) { 0480 /* cvs import - copy and deletion at same revision. 0481 * CVS sucks. 0482 */ 0483 n1 = uniqueNodeName(j, "D_" + path); 0484 } 0485 if (lastrev > 0) { 0486 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[n2].targets.append(RevGraphView::targetData(n1, FORWARDENTRY.action)); 0487 } 0488 fillItem(j, i, n1, path); 0489 lastrev = j; 0490 get_out = true; 0491 break; 0492 default: 0493 break; 0494 } 0495 } 0496 if (get_out) { 0497 return true; 0498 } 0499 } 0500 } 0501 } 0502 return !cancel; 0503 } 0504 0505 RevTreeWidget *RevisionTree::getView() 0506 { 0507 return m_Data->m_TreeDisplay; 0508 } 0509 0510 void RevisionTree::fillItem(long rev, int pathIndex, const QString &nodeName, const QString &path) 0511 { 0512 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].name = path; 0513 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].rev = rev; 0514 if (pathIndex >= 0) { 0515 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Action = m_Data->m_History[rev].changedPaths[pathIndex].action; 0516 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Author = m_Data->m_History[rev].author; 0517 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Message = m_Data->m_History[rev].message; 0518 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Date = svn::DateTime(m_Data->m_History[rev].date).toString(); 0519 } else { 0520 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Action = 0; 0521 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Author.clear(); 0522 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Message.clear(); 0523 m_Data->m_TreeDisplay->m_RevGraphView->m_Tree[nodeName].Date = svn::DateTime(0).toString(); 0524 } 0525 }