diff --git a/src/plugins/coreplugin/iversioncontrol.cpp b/src/plugins/coreplugin/iversioncontrol.cpp
index a798a782912367770f41e6836e9a740946151a6c..5839c1375d6b399fd9882d67bcad070b6470dd0c 100644
--- a/src/plugins/coreplugin/iversioncontrol.cpp
+++ b/src/plugins/coreplugin/iversioncontrol.cpp
@@ -28,7 +28,29 @@
 ****************************************************************************/
 
 #include "iversioncontrol.h"
+#include "vcsmanager.h"
+
+#include <utils/qtcassert.h>
+
+#include <QFileInfo>
 
+/*!
+ *  \class Core::IVersionControl::TopicCache
+ *  \brief The TopicCache class stores a {directory -> topic} cache
+ *
+ *  In order to support topic, an IVersionControl subclass needs to create
+ *  an instance of TopicCache subclass with appropriate overrides for its
+ *  pure virtual functions, and pass this instance to IVersionControl's constructor.
+ */
+
+/*!
+ *  \fn Core::IVersionControl::TopicCache::trackFile(const QString &repository)
+ *  Returns path to file that invalidates the cache when modified, for \a repository.
+ *  e.g. for git this file is .git/HEAD
+ *
+ *  \fn Core::IVersionControl::TopicCache::refreshTopic(const QString &repository)
+ *  Returns current topic for \a repository.
+ */
 namespace Core {
 
 QString IVersionControl::vcsOpenText() const
@@ -41,9 +63,14 @@ QString IVersionControl::vcsMakeWritableText() const
     return QString();
 }
 
-QString IVersionControl::vcsTopic(const QString &)
+QString IVersionControl::vcsTopic(const QString &topLevel)
 {
-    return QString();
+    return m_topicCache ? m_topicCache->topic(topLevel) : QString();
+}
+
+IVersionControl::~IVersionControl()
+{
+    delete m_topicCache;
 }
 
 IVersionControl::OpenSupportMode IVersionControl::openSupportMode(const QString &fileName) const
@@ -52,6 +79,30 @@ IVersionControl::OpenSupportMode IVersionControl::openSupportMode(const QString
     return NoOpen;
 }
 
+IVersionControl::TopicCache::~TopicCache()
+{
+}
+
+/*!
+ * Returns topic for repository under \a topLevel.
+ *
+ * If the cache for \a topLevel is valid, it will be used. Otherwise it will be refreshed.
+ */
+QString IVersionControl::TopicCache::topic(const QString &topLevel)
+{
+    QTC_ASSERT(!topLevel.isEmpty(), return QString());
+    TopicData &data = m_cache[topLevel];
+    QString file = trackFile(topLevel);
+
+    if (file.isEmpty())
+        return QString();
+    const QDateTime lastModified = QFileInfo(file).lastModified();
+    if (lastModified == data.timeStamp)
+        return data.topic;
+    data.timeStamp = lastModified;
+    return data.topic = refreshTopic(topLevel);
+}
+
 } // namespace Core
 
 #if defined(WITH_TESTS)
diff --git a/src/plugins/coreplugin/iversioncontrol.h b/src/plugins/coreplugin/iversioncontrol.h
index 57a93a74fc0be1e75e58668856c26465d73f6c51..cbde6138b2afbbf6581a9f532cae6f75aa435934 100644
--- a/src/plugins/coreplugin/iversioncontrol.h
+++ b/src/plugins/coreplugin/iversioncontrol.h
@@ -33,9 +33,10 @@
 #include "core_global.h"
 #include "id.h"
 
+#include <QDateTime>
+#include <QFlags>
 #include <QObject>
 #include <QString>
-#include <QFlags>
 
 namespace Core {
 
@@ -64,8 +65,29 @@ public:
         OpenMandatory  /*!< Files must always be opened by the VCS */
     };
 
-    IVersionControl() {}
-    virtual ~IVersionControl() {}
+    class TopicCache
+    {
+    public:
+        virtual ~TopicCache();
+        QString topic(const QString &topLevel);
+
+    protected:
+        virtual QString trackFile(const QString &repository) = 0;
+        virtual QString refreshTopic(const QString &repository) = 0;
+
+    private:
+        struct TopicData
+        {
+            QDateTime timeStamp;
+            QString topic;
+        };
+
+        QHash<QString, TopicData> m_cache;
+
+    };
+
+    explicit IVersionControl(TopicCache *topicCache = 0) : m_topicCache(topicCache) {}
+    virtual ~IVersionControl();
 
     virtual QString displayName() const = 0;
     virtual Id id() const = 0;
@@ -158,7 +180,7 @@ public:
     /*!
      * Topic (e.g. name of the current branch)
      */
-    virtual QString vcsTopic(const QString &directory);
+    virtual QString vcsTopic(const QString &topLevel);
 
     /*!
      * Display annotation for a file and scroll to line
@@ -179,6 +201,9 @@ signals:
     void repositoryChanged(const QString &repository);
     void filesChanged(const QStringList &files);
     void configurationChanged();
+
+private:
+    TopicCache *m_topicCache;
 };
 
 Q_DECLARE_OPERATORS_FOR_FLAGS(Core::IVersionControl::SettingsFlags)
diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp
index fbe39458f6b18b4074c8c5ca928d5163475cc819..22c094c1711138861f36ba55bfa1b7872a2d551a 100644
--- a/src/plugins/git/gitclient.cpp
+++ b/src/plugins/git/gitclient.cpp
@@ -2024,28 +2024,13 @@ bool GitClient::synchronousHeadRefs(const QString &workingDirectory, QStringList
     return true;
 }
 
-struct TopicData
-{
-    QDateTime timeStamp;
-    QString topic;
-};
-
 // Retrieve topic (branch, tag or HEAD hash)
 QString GitClient::synchronousTopic(const QString &workingDirectory)
 {
-    static QHash<QString, TopicData> topicCache;
-    QString gitDir = findGitDirForRepository(workingDirectory);
-    if (gitDir.isEmpty())
-        return QString();
-    TopicData &data = topicCache[gitDir];
-    QDateTime lastModified = QFileInfo(gitDir + QLatin1String("/HEAD")).lastModified();
-    if (lastModified == data.timeStamp)
-        return data.topic;
-    data.timeStamp = lastModified;
     // First try to find branch
     QString branch = synchronousCurrentLocalBranch(workingDirectory);
     if (!branch.isEmpty())
-        return data.topic = branch;
+        return branch;
 
     // Detached HEAD, try a tag or remote branch
     QStringList references;
@@ -2059,10 +2044,8 @@ QString GitClient::synchronousTopic(const QString &workingDirectory)
 
     foreach (const QString &ref, references) {
         int derefInd = ref.indexOf(dereference);
-        if (ref.startsWith(tagStart)) {
-            return data.topic = ref.mid(tagStart.size(),
-                                        (derefInd == -1) ? -1 : derefInd - tagStart.size());
-        }
+        if (ref.startsWith(tagStart))
+            return ref.mid(tagStart.size(), (derefInd == -1) ? -1 : derefInd - tagStart.size());
         if (ref.startsWith(remoteStart)) {
             remoteBranch = ref.mid(remoteStart.size(),
                                    (derefInd == -1) ? -1 : derefInd - remoteStart.size());
@@ -2070,7 +2053,7 @@ QString GitClient::synchronousTopic(const QString &workingDirectory)
     }
 
     // No tag
-    return data.topic = remoteBranch.isEmpty() ? tr("Detached HEAD") : remoteBranch;
+    return remoteBranch.isEmpty() ? tr("Detached HEAD") : remoteBranch;
 }
 
 bool GitClient::synchronousRevParseCmd(const QString &workingDirectory, const QString &ref,
diff --git a/src/plugins/git/gitversioncontrol.cpp b/src/plugins/git/gitversioncontrol.cpp
index dbb302d3d8bf3c3e4c7457e8409ec4ca9947adf3..c45e1dada28ea30211a7b98e17ac9a61d999861d 100644
--- a/src/plugins/git/gitversioncontrol.cpp
+++ b/src/plugins/git/gitversioncontrol.cpp
@@ -38,7 +38,32 @@
 namespace Git {
 namespace Internal {
 
+class GitTopicCache : public Core::IVersionControl::TopicCache
+{
+public:
+    GitTopicCache(GitClient *client) :
+        m_client(client)
+    {
+    }
+
+protected:
+    QString trackFile(const QString &repository)
+    {
+        const QString gitDir = m_client->findGitDirForRepository(repository);
+        return gitDir.isEmpty() ? QString() : (gitDir + QLatin1String("/HEAD"));
+    }
+
+    QString refreshTopic(const QString &repository)
+    {
+        return m_client->synchronousTopic(repository);
+    }
+
+private:
+    GitClient *m_client;
+};
+
 GitVersionControl::GitVersionControl(GitClient *client) :
+    Core::IVersionControl(new GitTopicCache(client)),
     m_client(client)
 {
 }
@@ -120,7 +145,7 @@ QString GitVersionControl::vcsGetRepositoryURL(const QString &directory)
 
 QString GitVersionControl::vcsTopic(const QString &directory)
 {
-    QString topic = m_client->synchronousTopic(directory);
+    QString topic = Core::IVersionControl::vcsTopic(directory);
     const QString commandInProgress = m_client->commandInProgressDescription(directory);
     if (!commandInProgress.isEmpty())
         topic += QLatin1String(" (") + commandInProgress + QLatin1Char(')');