1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "vcsmanager.h"
27 #include "iversioncontrol.h"
28 #include "icore.h"
29 #include "documentmanager.h"
30 #include "idocument.h"
31 
32 #include <coreplugin/dialogs/addtovcsdialog.h>
33 #include <coreplugin/editormanager/editormanager.h>
34 #include <coreplugin/editormanager/ieditor.h>
35 
36 #include <extensionsystem/pluginmanager.h>
37 #include <utils/algorithm.h>
38 #include <utils/infobar.h>
39 #include <utils/optional.h>
40 #include <utils/qtcassert.h>
41 #include <vcsbase/vcsbaseconstants.h>
42 
43 #include <QDir>
44 #include <QString>
45 #include <QList>
46 #include <QMap>
47 
48 #include <QFileInfo>
49 #include <QMessageBox>
50 
51 using namespace Utils;
52 
53 namespace Core {
54 
55 #if defined(WITH_TESTS)
56 const char TEST_PREFIX[] = "/8E3A9BA0-0B97-40DF-AEC1-2BDF9FC9EDBE/";
57 #endif
58 
59 // ---- VCSManagerPrivate:
60 // Maintains a cache of top-level directory->version control.
61 
62 class VcsManagerPrivate
63 {
64 public:
65     class VcsInfo {
66     public:
67         VcsInfo() = default;
VcsInfo(IVersionControl * vc,const QString & tl)68         VcsInfo(IVersionControl *vc, const QString &tl) :
69             versionControl(vc), topLevel(tl)
70         { }
71         VcsInfo(const VcsInfo &other) = default;
72 
operator ==(const VcsInfo & other) const73         bool operator == (const VcsInfo &other) const
74         {
75             return versionControl == other.versionControl && topLevel == other.topLevel;
76         }
77 
78         IVersionControl *versionControl = nullptr;
79         QString topLevel;
80     };
81 
findInCache(const QString & dir) const82     Utils::optional<VcsInfo> findInCache(const QString &dir) const
83     {
84         QTC_ASSERT(QDir(dir).isAbsolute(), return Utils::nullopt);
85         QTC_ASSERT(!dir.endsWith(QLatin1Char('/')), return Utils::nullopt);
86         QTC_ASSERT(QDir::fromNativeSeparators(dir) == dir, return Utils::nullopt);
87 
88         const auto it = m_cachedMatches.constFind(dir);
89         return it == m_cachedMatches.constEnd() ? Utils::nullopt : Utils::make_optional(it.value());
90     }
91 
clearCache()92     void clearCache()
93     {
94         m_cachedMatches.clear();
95     }
96 
resetCache(const QString & dir)97     void resetCache(const QString &dir)
98     {
99         QTC_ASSERT(QDir(dir).isAbsolute(), return);
100         QTC_ASSERT(!dir.endsWith(QLatin1Char('/')), return);
101         QTC_ASSERT(QDir::fromNativeSeparators(dir) == dir, return);
102 
103         const QString dirSlash = dir + QLatin1Char('/');
104         foreach (const QString &key, m_cachedMatches.keys()) {
105             if (key == dir || key.startsWith(dirSlash))
106                 m_cachedMatches.remove(key);
107         }
108     }
109 
cache(IVersionControl * vc,const QString & topLevel,const QString & dir)110     void cache(IVersionControl *vc, const QString &topLevel, const QString &dir)
111     {
112         QTC_ASSERT(QDir(dir).isAbsolute(), return);
113         QTC_ASSERT(!dir.endsWith(QLatin1Char('/')), return);
114         QTC_ASSERT(QDir::fromNativeSeparators(dir) == dir, return);
115         QTC_ASSERT(dir.startsWith(topLevel + QLatin1Char('/'))
116                    || topLevel == dir || topLevel.isEmpty(), return);
117         QTC_ASSERT((topLevel.isEmpty() && !vc) || (!topLevel.isEmpty() && vc), return);
118 
119         QString tmpDir = dir;
120         const QChar slash = QLatin1Char('/');
121         while (tmpDir.count() >= topLevel.count() && !tmpDir.isEmpty()) {
122             m_cachedMatches.insert(tmpDir, VcsInfo(vc, topLevel));
123             // if no vc was found, this might mean we're inside a repo internal directory (.git)
124             // Cache only input directory, not parents
125             if (!vc)
126                 break;
127             const int slashPos = tmpDir.lastIndexOf(slash);
128             if (slashPos >= 0)
129                 tmpDir.truncate(slashPos);
130             else
131                 tmpDir.clear();
132         }
133     }
134 
135     QList<IVersionControl *> m_versionControlList;
136     QMap<QString, VcsInfo> m_cachedMatches;
137     IVersionControl *m_unconfiguredVcs = nullptr;
138 
139     QStringList m_cachedAdditionalToolsPaths;
140     bool m_cachedAdditionalToolsPathsDirty = true;
141 };
142 
143 static VcsManagerPrivate *d = nullptr;
144 static VcsManager *m_instance = nullptr;
145 
VcsManager(QObject * parent)146 VcsManager::VcsManager(QObject *parent) :
147    QObject(parent)
148 {
149     m_instance = this;
150     d = new VcsManagerPrivate;
151 }
152 
153 // ---- VCSManager:
154 
~VcsManager()155 VcsManager::~VcsManager()
156 {
157     m_instance = nullptr;
158     delete d;
159 }
160 
addVersionControl(IVersionControl * vc)161 void VcsManager::addVersionControl(IVersionControl *vc)
162 {
163     QTC_ASSERT(!d->m_versionControlList.contains(vc), return);
164     d->m_versionControlList.append(vc);
165 }
166 
instance()167 VcsManager *VcsManager::instance()
168 {
169     return m_instance;
170 }
171 
extensionsInitialized()172 void VcsManager::extensionsInitialized()
173 {
174     // Change signal connections
175     foreach (IVersionControl *versionControl, versionControls()) {
176         connect(versionControl, &IVersionControl::filesChanged, DocumentManager::instance(),
177                 [](const QStringList fileNames) {
178                     DocumentManager::notifyFilesChangedInternally(
179                         Utils::transform(fileNames, &Utils::FilePath::fromString));
180                 });
181         connect(versionControl, &IVersionControl::repositoryChanged,
182                 m_instance, &VcsManager::repositoryChanged);
183         connect(versionControl, &IVersionControl::configurationChanged,
184                 m_instance, &VcsManager::handleConfigurationChanges);
185     }
186 }
187 
versionControls()188 const QList<IVersionControl *> VcsManager::versionControls()
189 {
190     return d->m_versionControlList;
191 }
192 
versionControl(Id id)193 IVersionControl *VcsManager::versionControl(Id id)
194 {
195     return Utils::findOrDefault(versionControls(), Utils::equal(&Core::IVersionControl::id, id));
196 }
197 
absoluteWithNoTrailingSlash(const QString & directory)198 static QString absoluteWithNoTrailingSlash(const QString &directory)
199 {
200     QString res = QDir(directory).absolutePath();
201     if (res.endsWith(QLatin1Char('/')))
202         res.chop(1);
203     return res;
204 }
205 
resetVersionControlForDirectory(const QString & inputDirectory)206 void VcsManager::resetVersionControlForDirectory(const QString &inputDirectory)
207 {
208     if (inputDirectory.isEmpty())
209         return;
210 
211     const QString directory = absoluteWithNoTrailingSlash(inputDirectory);
212     d->resetCache(directory);
213     emit m_instance->repositoryChanged(directory);
214 }
215 
findVersionControlForDirectory(const QString & inputDirectory,QString * topLevelDirectory)216 IVersionControl* VcsManager::findVersionControlForDirectory(const QString &inputDirectory,
217                                                             QString *topLevelDirectory)
218 {
219     using StringVersionControlPair = QPair<QString, IVersionControl *>;
220     using StringVersionControlPairs = QList<StringVersionControlPair>;
221     if (inputDirectory.isEmpty()) {
222         if (topLevelDirectory)
223             topLevelDirectory->clear();
224         return nullptr;
225     }
226 
227     // Make sure we an absolute path:
228     QString directory = absoluteWithNoTrailingSlash(inputDirectory);
229 #ifdef WITH_TESTS
230     if (directory[0].isLetter() && directory.indexOf(QLatin1Char(':') + QLatin1String(TEST_PREFIX)) == 1)
231         directory = directory.mid(2);
232 #endif
233     auto cachedData = d->findInCache(directory);
234     if (cachedData) {
235         if (topLevelDirectory)
236             *topLevelDirectory = cachedData->topLevel;
237         return cachedData->versionControl;
238     }
239 
240     // Nothing: ask the IVersionControls directly.
241     StringVersionControlPairs allThatCanManage;
242 
243     foreach (IVersionControl * versionControl, versionControls()) {
244         QString topLevel;
245         if (versionControl->managesDirectory(directory, &topLevel))
246             allThatCanManage.push_back(StringVersionControlPair(topLevel, versionControl));
247     }
248 
249     // To properly find a nested repository (say, git checkout inside SVN),
250     // we need to select the version control with the longest toplevel pathname.
251     Utils::sort(allThatCanManage, [](const StringVersionControlPair &l,
252                                      const StringVersionControlPair &r) {
253         return l.first.size() > r.first.size();
254     });
255 
256     if (allThatCanManage.isEmpty()) {
257         d->cache(nullptr, QString(), directory); // register that nothing was found!
258 
259         // report result;
260         if (topLevelDirectory)
261             topLevelDirectory->clear();
262         return nullptr;
263     }
264 
265     // Register Vcs(s) with the cache
266     QString tmpDir = absoluteWithNoTrailingSlash(directory);
267 #if defined WITH_TESTS
268     // Force caching of test directories (even though they do not exist):
269     if (directory.startsWith(QLatin1String(TEST_PREFIX)))
270         tmpDir = directory;
271 #endif
272     // directory might refer to a historical directory which doesn't exist.
273     // In this case, don't cache it.
274     if (!tmpDir.isEmpty()) {
275         const QChar slash = QLatin1Char('/');
276         const StringVersionControlPairs::const_iterator cend = allThatCanManage.constEnd();
277         for (StringVersionControlPairs::const_iterator i = allThatCanManage.constBegin(); i != cend; ++i) {
278             // If topLevel was already cached for another VC, skip this one
279             if (tmpDir.count() < i->first.count())
280                 continue;
281             d->cache(i->second, i->first, tmpDir);
282             tmpDir = i->first;
283             const int slashPos = tmpDir.lastIndexOf(slash);
284             if (slashPos >= 0)
285                 tmpDir.truncate(slashPos);
286         }
287     }
288 
289     // return result
290     if (topLevelDirectory)
291         *topLevelDirectory = allThatCanManage.first().first;
292     IVersionControl *versionControl = allThatCanManage.first().second;
293     const bool isVcsConfigured = versionControl->isConfigured();
294     if (!isVcsConfigured || d->m_unconfiguredVcs) {
295         Id vcsWarning("VcsNotConfiguredWarning");
296         IDocument *curDocument = EditorManager::currentDocument();
297         if (isVcsConfigured) {
298             if (curDocument && d->m_unconfiguredVcs == versionControl) {
299                 curDocument->infoBar()->removeInfo(vcsWarning);
300                 d->m_unconfiguredVcs = nullptr;
301             }
302             return versionControl;
303         } else {
304             Utils::InfoBar *infoBar = curDocument ? curDocument->infoBar() : nullptr;
305             if (infoBar && infoBar->canInfoBeAdded(vcsWarning)) {
306                 Utils::InfoBarEntry info(vcsWarning,
307                                          tr("%1 repository was detected but %1 is not configured.")
308                                              .arg(versionControl->displayName()),
309                                          Utils::InfoBarEntry::GlobalSuppression::Enabled);
310                 d->m_unconfiguredVcs = versionControl;
311                 info.setCustomButtonInfo(ICore::msgShowOptionsDialog(), []() {
312                     QTC_ASSERT(d->m_unconfiguredVcs, return);
313                     ICore::showOptionsDialog(d->m_unconfiguredVcs->id());
314                  });
315 
316                 infoBar->addInfo(info);
317             }
318             return nullptr;
319         }
320     }
321     return versionControl;
322 }
323 
findTopLevelForDirectory(const QString & directory)324 QString VcsManager::findTopLevelForDirectory(const QString &directory)
325 {
326     QString result;
327     findVersionControlForDirectory(directory, &result);
328     return result;
329 }
330 
repositories(const IVersionControl * vc)331 QStringList VcsManager::repositories(const IVersionControl *vc)
332 {
333     QStringList result;
334     for (auto it = d->m_cachedMatches.constBegin(); it != d->m_cachedMatches.constEnd(); ++it) {
335         if (it.value().versionControl == vc)
336             result.append(it.value().topLevel);
337     }
338     return result;
339 }
340 
promptToDelete(IVersionControl * versionControl,const QString & fileName)341 bool VcsManager::promptToDelete(IVersionControl *versionControl, const QString &fileName)
342 {
343     return promptToDelete(versionControl, {Utils::FilePath::fromString(fileName)}).isEmpty();
344 }
345 
promptToDelete(const FilePaths & filePaths)346 FilePaths VcsManager::promptToDelete(const FilePaths &filePaths)
347 {
348     // Categorize files by their parent directory, so we won't call
349     // findVersionControlForDirectory() more often than necessary.
350     QMap<FilePath, FilePaths> filesByParentDir;
351     for (const FilePath &fp : filePaths)
352         filesByParentDir[fp.absolutePath()].append(fp);
353 
354     // Categorize by version control system.
355     QHash<IVersionControl *, FilePaths> filesByVersionControl;
356     for (auto it = filesByParentDir.cbegin(); it != filesByParentDir.cend(); ++it) {
357         IVersionControl * const vc = findVersionControlForDirectory(it.key().toString());
358         if (vc)
359             filesByVersionControl[vc] << it.value();
360     }
361 
362     // Remove the files.
363     FilePaths failedFiles;
364     for (auto it = filesByVersionControl.cbegin(); it != filesByVersionControl.cend(); ++it)
365         failedFiles << promptToDelete(it.key(), it.value());
366 
367     return failedFiles;
368 }
369 
promptToDelete(IVersionControl * vc,const FilePaths & filePaths)370 FilePaths VcsManager::promptToDelete(IVersionControl *vc, const FilePaths &filePaths)
371 {
372     QTC_ASSERT(vc, return {});
373     if (!vc->supportsOperation(IVersionControl::DeleteOperation))
374         return {};
375 
376     const QString fileListForUi = "<ul><li>" + transform(filePaths, [](const FilePath &fp) {
377         return fp.toUserOutput();
378     }).join("</li><li>") + "</li></ul>";
379     const QString title = tr("Version Control");
380     const QString msg = tr("Remove the following files from the version control system (%2)?"
381                            "%1Note: This might remove the local file.").arg(fileListForUi, vc->displayName());
382     const QMessageBox::StandardButton button =
383         QMessageBox::question(ICore::dialogParent(), title, msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
384     if (button != QMessageBox::Yes)
385         return {};
386 
387     FilePaths failedFiles;
388     for (const FilePath &fp : filePaths) {
389         if (!vc->vcsDelete(fp.toString()))
390             failedFiles << fp;
391     }
392     return failedFiles;
393 }
394 
msgAddToVcsTitle()395 QString VcsManager::msgAddToVcsTitle()
396 {
397     return tr("Add to Version Control");
398 }
399 
msgPromptToAddToVcs(const QStringList & files,const IVersionControl * vc)400 QString VcsManager::msgPromptToAddToVcs(const QStringList &files, const IVersionControl *vc)
401 {
402     return files.size() == 1
403         ? tr("Add the file\n%1\nto version control (%2)?")
404               .arg(files.front(), vc->displayName())
405         : tr("Add the files\n%1\nto version control (%2)?")
406               .arg(files.join(QString(QLatin1Char('\n'))), vc->displayName());
407 }
408 
msgAddToVcsFailedTitle()409 QString VcsManager::msgAddToVcsFailedTitle()
410 {
411     return tr("Adding to Version Control Failed");
412 }
413 
msgToAddToVcsFailed(const QStringList & files,const IVersionControl * vc)414 QString VcsManager::msgToAddToVcsFailed(const QStringList &files, const IVersionControl *vc)
415 {
416     return files.size() == 1
417         ? tr("Could not add the file\n%1\nto version control (%2)\n")
418               .arg(files.front(), vc->displayName())
419         : tr("Could not add the following files to version control (%1)\n%2")
420               .arg(vc->displayName(), files.join(QString(QLatin1Char('\n'))));
421 }
422 
additionalToolsPath()423 QStringList VcsManager::additionalToolsPath()
424 {
425     if (d->m_cachedAdditionalToolsPathsDirty) {
426         d->m_cachedAdditionalToolsPaths.clear();
427         foreach (IVersionControl *vc, versionControls())
428             d->m_cachedAdditionalToolsPaths.append(vc->additionalToolsPath());
429         d->m_cachedAdditionalToolsPathsDirty = false;
430     }
431     return d->m_cachedAdditionalToolsPaths;
432 }
433 
promptToAdd(const QString & directory,const QStringList & fileNames)434 void VcsManager::promptToAdd(const QString &directory, const QStringList &fileNames)
435 {
436     IVersionControl *vc = findVersionControlForDirectory(directory);
437     if (!vc || !vc->supportsOperation(IVersionControl::AddOperation))
438         return;
439 
440     const QStringList unmanagedFiles = vc->unmanagedFiles(fileNames);
441     if (unmanagedFiles.isEmpty())
442         return;
443 
444     Internal::AddToVcsDialog dlg(ICore::dialogParent(), VcsManager::msgAddToVcsTitle(),
445                                  unmanagedFiles, vc->displayName());
446     if (dlg.exec() == QDialog::Accepted) {
447         QStringList notAddedToVc;
448         foreach (const QString &file, unmanagedFiles) {
449             if (!vc->vcsAdd(QDir(directory).filePath(file)))
450                 notAddedToVc << file;
451         }
452 
453         if (!notAddedToVc.isEmpty()) {
454             QMessageBox::warning(ICore::dialogParent(),
455                                  VcsManager::msgAddToVcsFailedTitle(),
456                                  VcsManager::msgToAddToVcsFailed(notAddedToVc, vc));
457         }
458     }
459 }
460 
emitRepositoryChanged(const QString & repository)461 void VcsManager::emitRepositoryChanged(const QString &repository)
462 {
463     emit m_instance->repositoryChanged(repository);
464 }
465 
clearVersionControlCache()466 void VcsManager::clearVersionControlCache()
467 {
468     QStringList repoList = d->m_cachedMatches.keys();
469     d->clearCache();
470     foreach (const QString &repo, repoList)
471         emit m_instance->repositoryChanged(repo);
472 }
473 
handleConfigurationChanges()474 void VcsManager::handleConfigurationChanges()
475 {
476     d->m_cachedAdditionalToolsPathsDirty = true;
477     auto vcs = qobject_cast<IVersionControl *>(sender());
478     if (vcs)
479         emit configurationChanged(vcs);
480 }
481 
482 } // namespace Core
483 
484 #if defined(WITH_TESTS)
485 
486 #include <QtTest>
487 
488 #include "coreplugin.h"
489 
490 #include <extensionsystem/pluginmanager.h>
491 
492 namespace Core {
493 namespace Internal {
494 
495 const char ID_VCS_A[] = "A";
496 const char ID_VCS_B[] = "B";
497 
498 using FileHash = QHash<QString, QString>;
499 
makeHash(const QStringList & list)500 static FileHash makeHash(const QStringList &list)
501 {
502     FileHash result;
503     foreach (const QString &i, list) {
504         QStringList parts = i.split(QLatin1Char(':'));
505         QTC_ASSERT(parts.count() == 2, continue);
506         result.insert(QString::fromLatin1(TEST_PREFIX) + parts.at(0),
507                       QString::fromLatin1(TEST_PREFIX) + parts.at(1));
508     }
509     return result;
510 }
511 
makeString(const QString & s)512 static QString makeString(const QString &s)
513 {
514     if (s.isEmpty())
515         return QString();
516     return QString::fromLatin1(TEST_PREFIX) + s;
517 }
518 
testVcsManager_data()519 void CorePlugin::testVcsManager_data()
520 {
521     // avoid conflicts with real files and directories:
522 
523     QTest::addColumn<QStringList>("dirsVcsA"); // <directory>:<toplevel>
524     QTest::addColumn<QStringList>("dirsVcsB"); // <directory>:<toplevel>
525     // <directory>:<toplevel>:<vcsid>:<- from cache, * from VCS>
526     QTest::addColumn<QStringList>("results");
527 
528     QTest::newRow("A and B next to each other")
529             << QStringList({"a:a", "a/1:a", "a/2:a", "a/2/5:a", "a/2/5/6:a"})
530             << QStringList({"b:b", "b/3:b", "b/4:b"})
531             << QStringList({":::-",          // empty directory to look up
532                             "c:::*",         // Neither in A nor B
533                             "a:a:A:*",       // in A
534                             "b:b:B:*",       // in B
535                             "b/3:b:B:*",     // in B
536                             "b/4:b:B:*",     // in B
537                             "a/1:a:A:*",     // in A
538                             "a/2:a:A:*",     // in A
539                             ":::-",          // empty directory to look up
540                             "a/2/5/6:a:A:*", // in A
541                             "a/2/5:a:A:-",   // in A (cached from before!)
542                             // repeat: These need to come from the cache now:
543                             "c:::-",         // Neither in A nor B
544                             "a:a:A:-",       // in A
545                             "b:b:B:-",       // in B
546                             "b/3:b:B:-",     // in B
547                             "b/4:b:B:-",     // in B
548                             "a/1:a:A:-",     // in A
549                             "a/2:a:A:-",     // in A
550                             "a/2/5/6:a:A:-", // in A
551                             "a/2/5:a:A:-"    // in A
552                 });
553     QTest::newRow("B in A")
554             << QStringList({"a:a", "a/1:a", "a/2:a", "a/2/5:a", "a/2/5/6:a"})
555             << QStringList({"a/1/b:a/1/b", "a/1/b/3:a/1/b", "a/1/b/4:a/1/b", "a/1/b/3/5:a/1/b",
556                             "a/1/b/3/5/6:a/1/b"})
557             << QStringList({"a:a:A:*",            // in A
558                             "c:::*",              // Neither in A nor B
559                             "a/3:::*",            // Neither in A nor B
560                             "a/1/b/x:::*",        // Neither in A nor B
561                             "a/1/b:a/1/b:B:*",    // in B
562                             "a/1:a:A:*",          // in A
563                             "a/1/b/../../2:a:A:*" // in A
564                 });
565     QTest::newRow("A and B") // first one wins...
566             << QStringList({"a:a", "a/1:a", "a/2:a"})
567             << QStringList({"a:a", "a/1:a", "a/2:a"})
568             << QStringList({"a/2:a:A:*"});
569 }
570 
testVcsManager()571 void CorePlugin::testVcsManager()
572 {
573     // setup:
574     QList<IVersionControl *> orig = Core::d->m_versionControlList;
575     TestVersionControl *vcsA(new TestVersionControl(ID_VCS_A, QLatin1String("A")));
576     TestVersionControl *vcsB(new TestVersionControl(ID_VCS_B, QLatin1String("B")));
577 
578     Core::d->m_versionControlList = {vcsA, vcsB};
579 
580     // test:
581     QFETCH(QStringList, dirsVcsA);
582     QFETCH(QStringList, dirsVcsB);
583     QFETCH(QStringList, results);
584 
585     vcsA->setManagedDirectories(makeHash(dirsVcsA));
586     vcsB->setManagedDirectories(makeHash(dirsVcsB));
587 
588     QString realTopLevel = QLatin1String("ABC"); // Make sure this gets cleared if needed.
589 
590     // From VCSes:
591     int expectedCount = 0;
592     foreach (const QString &result, results) {
593         // qDebug() << "Expecting:" << result;
594 
595         QStringList split = result.split(QLatin1Char(':'));
596         QCOMPARE(split.count(), 4);
597         QVERIFY(split.at(3) == QLatin1String("*") || split.at(3) == QLatin1String("-"));
598 
599 
600         const QString directory = split.at(0);
601         const QString topLevel = split.at(1);
602         const QString vcsId = split.at(2);
603         bool fromCache = split.at(3) == QLatin1String("-");
604 
605         if (!fromCache && !directory.isEmpty())
606             ++expectedCount;
607 
608         IVersionControl *vcs;
609         vcs = VcsManager::findVersionControlForDirectory(makeString(directory), &realTopLevel);
610         QCOMPARE(realTopLevel, makeString(topLevel));
611         if (vcs)
612             QCOMPARE(vcs->id().toString(), vcsId);
613         else
614             QCOMPARE(QString(), vcsId);
615         QCOMPARE(vcsA->dirCount(), expectedCount);
616         QCOMPARE(vcsA->fileCount(), 0);
617         QCOMPARE(vcsB->dirCount(), expectedCount);
618         QCOMPARE(vcsB->fileCount(), 0);
619     }
620 
621     // teardown:
622     qDeleteAll(Core::d->m_versionControlList);
623     Core::d->m_versionControlList = orig;
624 }
625 
626 } // namespace Internal
627 } // namespace Core
628 
629 #endif
630