forked from qt-creator/qt-creator
604 lines
19 KiB
C++
604 lines
19 KiB
C++
/**************************************************************************
|
|
**
|
|
** This file is part of Qt Creator
|
|
**
|
|
** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
|
|
**
|
|
** Contact: Nokia Corporation (info@qt.nokia.com)
|
|
**
|
|
**
|
|
** GNU Lesser General Public License Usage
|
|
**
|
|
** This file may be used under the terms of the GNU Lesser General Public
|
|
** License version 2.1 as published by the Free Software Foundation and
|
|
** appearing in the file LICENSE.LGPL included in the packaging of this file.
|
|
** Please review the following information to ensure the GNU Lesser General
|
|
** Public License version 2.1 requirements will be met:
|
|
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
|
|
**
|
|
** In addition, as a special exception, Nokia gives you certain additional
|
|
** rights. These rights are described in the Nokia Qt LGPL Exception
|
|
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
|
|
**
|
|
** Other Usage
|
|
**
|
|
** Alternatively, this file may be used in accordance with the terms and
|
|
** conditions contained in a signed written agreement between you and Nokia.
|
|
**
|
|
** If you have questions regarding the use of this file, please contact
|
|
** Nokia at info@qt.nokia.com.
|
|
**
|
|
**************************************************************************/
|
|
|
|
#include "gitorious.h"
|
|
|
|
#include <QtCore/QDebug>
|
|
#include <QtCore/QCoreApplication>
|
|
#include <QtCore/QXmlStreamReader>
|
|
#include <QtCore/QSettings>
|
|
|
|
#include <QtNetwork/QNetworkAccessManager>
|
|
#include <QtNetwork/QNetworkReply>
|
|
|
|
enum { debug = 0 };
|
|
|
|
enum Protocol { ListCategoriesProtocol, ListProjectsProtocol };
|
|
|
|
static const char protocolPropertyC[] = "gitoriousProtocol";
|
|
static const char hostNamePropertyC[] = "gitoriousHost";
|
|
static const char pagePropertyC[] = "requestPage";
|
|
|
|
static const char settingsKeyC[] = "GitoriousHosts";
|
|
|
|
// Gitorious paginates projects as 20 per page. It starts with page 1.
|
|
enum { ProjectsPageSize = 20 };
|
|
|
|
// Format an URL for a XML request
|
|
static inline QUrl xmlRequest(const QString &host, const QString &request, int page = -1)
|
|
{
|
|
QUrl url;
|
|
url.setScheme(QLatin1String("http"));
|
|
url.setHost(host);
|
|
url.setPath(QLatin1Char('/') + request);
|
|
url.addQueryItem(QLatin1String("format"), QLatin1String("xml"));
|
|
if (page >= 0)
|
|
url.addQueryItem(QLatin1String("page"), QString::number(page));
|
|
return url;
|
|
}
|
|
|
|
namespace Gitorious {
|
|
namespace Internal {
|
|
|
|
GitoriousRepository::GitoriousRepository() :
|
|
type(BaselineRepository),
|
|
id(0)
|
|
{
|
|
}
|
|
|
|
static inline GitoriousRepository::Type repositoryType(const QString &nspace)
|
|
{
|
|
if (nspace == QLatin1String("Repository::Namespace::BASELINE"))
|
|
return GitoriousRepository::BaselineRepository;
|
|
if (nspace == QLatin1String("Repository::Namespace::SHARED"))
|
|
return GitoriousRepository::SharedRepository;
|
|
if (nspace == QLatin1String("Repository::Namespace::PERSONAL"))
|
|
return GitoriousRepository::PersonalRepository;
|
|
return GitoriousRepository::BaselineRepository;
|
|
}
|
|
|
|
GitoriousCategory::GitoriousCategory(const QString &n) :
|
|
name(n)
|
|
{
|
|
}
|
|
|
|
GitoriousHost::GitoriousHost(const QString &h, const QString &d) :
|
|
hostName(h),
|
|
description(d),
|
|
state(ProjectsQueryRunning)
|
|
{
|
|
}
|
|
|
|
int GitoriousHost::findCategory(const QString &n) const
|
|
{
|
|
const int count = categories.size();
|
|
for (int i = 0; i < count; i++)
|
|
if (categories.at(i)->name == n)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
QDebug operator<<(QDebug d, const GitoriousRepository &r)
|
|
{
|
|
QDebug nospace = d.nospace();
|
|
nospace << "name=" << r.name << '/' << r.id << '/' << r.type << r.owner
|
|
<<" push=" << r.pushUrl << " clone=" << r.cloneUrl << " descr=" << r.description;
|
|
return d;
|
|
}
|
|
|
|
QDebug operator<<(QDebug d, const GitoriousProject &p)
|
|
{
|
|
QDebug nospace = d.nospace();
|
|
nospace << " project=" << p.name << " description=" << p.description << '\n';
|
|
foreach(const GitoriousRepository &r, p.repositories)
|
|
nospace << " " << r << '\n';
|
|
return d;
|
|
}
|
|
|
|
QDebug operator<<(QDebug d, const GitoriousCategory &c)
|
|
{
|
|
d.nospace() << " category=" << c.name << '\n';
|
|
return d;
|
|
}
|
|
|
|
QDebug operator<<(QDebug d, const GitoriousHost &h)
|
|
{
|
|
QDebug nospace = d.nospace();
|
|
nospace << " Host=" << h.hostName << " description=" << h.description << '\n';
|
|
foreach(const QSharedPointer<GitoriousCategory> &c, h.categories)
|
|
nospace << *c;
|
|
foreach(const QSharedPointer<GitoriousProject> &p, h.projects)
|
|
nospace << *p;
|
|
return d;
|
|
}
|
|
|
|
/* GitoriousProjectReader: Helper class for parsing project list output
|
|
* \code
|
|
projects...>
|
|
<project>
|
|
<bugtracker-url>
|
|
<created-at>
|
|
<description>... </description>
|
|
<home-url> (rarely set)
|
|
<license>
|
|
<mailinglist-url>
|
|
<slug> (name)
|
|
<title>MuleFTW</title>
|
|
<owner>
|
|
<repositories>
|
|
<mainlines> // Optional
|
|
<repository>
|
|
<id>
|
|
<name>
|
|
<owner>
|
|
<clone_url>
|
|
</repository>
|
|
</mainlines>
|
|
<clones> // Optional
|
|
</clones>
|
|
</repositories>
|
|
</project>
|
|
|
|
* \endcode */
|
|
|
|
class GitoriousProjectReader
|
|
{
|
|
Q_DISABLE_COPY(GitoriousProjectReader)
|
|
public:
|
|
typedef GitoriousCategory::ProjectList ProjectList;
|
|
|
|
GitoriousProjectReader();
|
|
ProjectList read(const QByteArray &a, QString *errorMessage);
|
|
|
|
private:
|
|
void readProjects(QXmlStreamReader &r);
|
|
QSharedPointer<GitoriousProject> readProject(QXmlStreamReader &r);
|
|
QList<GitoriousRepository> readRepositories(QXmlStreamReader &r);
|
|
GitoriousRepository readRepository(QXmlStreamReader &r, int defaultType = -1);
|
|
void readUnknownElement(QXmlStreamReader &r);
|
|
|
|
const QString m_mainLinesElement;
|
|
const QString m_clonesElement;
|
|
ProjectList m_projects;
|
|
};
|
|
|
|
GitoriousProjectReader::GitoriousProjectReader() :
|
|
m_mainLinesElement(QLatin1String("mainlines")),
|
|
m_clonesElement(QLatin1String("clones"))
|
|
{
|
|
}
|
|
|
|
GitoriousProjectReader::ProjectList GitoriousProjectReader::read(const QByteArray &a, QString *errorMessage)
|
|
{
|
|
m_projects.clear();
|
|
QXmlStreamReader reader(a);
|
|
|
|
while (!reader.atEnd()) {
|
|
reader.readNext();
|
|
if (reader.isStartElement()) {
|
|
if (reader.name() == QLatin1String("projects")) {
|
|
readProjects(reader);
|
|
} else {
|
|
readUnknownElement(reader);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (reader.hasError()) {
|
|
*errorMessage = QString::fromLatin1("Error at %1:%2: %3").arg(reader.lineNumber()).arg(reader.columnNumber()).arg(reader.errorString());
|
|
m_projects.clear();
|
|
}
|
|
|
|
return m_projects;
|
|
}
|
|
|
|
bool gitoriousProjectLessThan(const QSharedPointer<GitoriousProject> &p1, const QSharedPointer<GitoriousProject> &p2)
|
|
{
|
|
return p1->name.compare(p2->name, Qt::CaseInsensitive) < 0;
|
|
}
|
|
|
|
void GitoriousProjectReader::readProjects(QXmlStreamReader &reader)
|
|
{
|
|
while (!reader.atEnd()) {
|
|
reader.readNext();
|
|
|
|
if (reader.isEndElement())
|
|
break;
|
|
|
|
if (reader.isStartElement()) {
|
|
if (reader.name() == "project") {
|
|
const QSharedPointer<GitoriousProject> p = readProject(reader);
|
|
if (!p->name.isEmpty())
|
|
m_projects.push_back(p);
|
|
} else {
|
|
readUnknownElement(reader);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QSharedPointer<GitoriousProject> GitoriousProjectReader::readProject(QXmlStreamReader &reader)
|
|
{
|
|
QSharedPointer<GitoriousProject> project(new GitoriousProject);
|
|
|
|
while (!reader.atEnd()) {
|
|
reader.readNext();
|
|
if (reader.isEndElement())
|
|
break;
|
|
|
|
if (reader.isStartElement()) {
|
|
const QStringRef name = reader.name();
|
|
if (name == QLatin1String("description")) {
|
|
project->description = reader.readElementText();
|
|
} else if (name == QLatin1String("title")) {
|
|
project->name = reader.readElementText();
|
|
} else if (name == QLatin1String("slug") && project->name.isEmpty()) {
|
|
project->name = reader.readElementText();
|
|
} else if (name == QLatin1String("repositories")) {
|
|
project->repositories = readRepositories(reader);
|
|
} else {
|
|
readUnknownElement(reader);
|
|
}
|
|
}
|
|
}
|
|
return project;
|
|
}
|
|
|
|
QList<GitoriousRepository> GitoriousProjectReader::readRepositories(QXmlStreamReader &reader)
|
|
{
|
|
QList<GitoriousRepository> repositories;
|
|
int defaultType = -1;
|
|
|
|
// The "mainlines"/"clones" elements are not used in the
|
|
// Nokia setup, handle them optionally.
|
|
while (!reader.atEnd()) {
|
|
reader.readNext();
|
|
|
|
if (reader.isEndElement()) {
|
|
const QStringRef name = reader.name();
|
|
if (name == m_mainLinesElement || name == m_clonesElement) {
|
|
defaultType = -1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (reader.isStartElement()) {
|
|
const QStringRef name = reader.name();
|
|
if (reader.name() == QLatin1String("repository")) {
|
|
repositories.push_back(readRepository(reader, defaultType));
|
|
} else if (name == m_mainLinesElement) {
|
|
defaultType = GitoriousRepository::MainLineRepository;
|
|
} else if (name == m_clonesElement) {
|
|
defaultType = GitoriousRepository::CloneRepository;
|
|
} else {
|
|
readUnknownElement(reader);
|
|
}
|
|
}
|
|
}
|
|
return repositories;
|
|
}
|
|
|
|
GitoriousRepository GitoriousProjectReader::readRepository(QXmlStreamReader &reader, int defaultType)
|
|
{
|
|
GitoriousRepository repository;
|
|
if (defaultType >= 0)
|
|
repository.type = static_cast<GitoriousRepository::Type>(defaultType);
|
|
|
|
while (!reader.atEnd()) {
|
|
reader.readNext();
|
|
|
|
if (reader.isEndElement())
|
|
break;
|
|
|
|
if (reader.isStartElement()) {
|
|
const QStringRef name = reader.name();
|
|
if (name == QLatin1String("name")) {
|
|
repository.name = reader.readElementText();
|
|
} else if (name == QLatin1String("owner")) {
|
|
repository.owner = reader.readElementText();
|
|
} else if (name == QLatin1String("id")) {
|
|
repository.id = reader.readElementText().toInt();
|
|
} else if (name == QLatin1String("description")) {
|
|
repository.description = reader.readElementText();
|
|
} else if (name == QLatin1String("push_url")) {
|
|
repository.pushUrl = reader.readElementText();
|
|
} else if (name == QLatin1String("clone_url")) {
|
|
repository.cloneUrl = reader.readElementText();
|
|
} else if (name == QLatin1String("namespace")) {
|
|
repository.type = repositoryType(reader.readElementText());
|
|
} else {
|
|
readUnknownElement(reader);
|
|
}
|
|
}
|
|
}
|
|
return repository;
|
|
}
|
|
|
|
void GitoriousProjectReader::readUnknownElement(QXmlStreamReader &reader)
|
|
{
|
|
Q_ASSERT(reader.isStartElement());
|
|
|
|
while (!reader.atEnd()) {
|
|
reader.readNext();
|
|
|
|
if (reader.isEndElement())
|
|
break;
|
|
|
|
if (reader.isStartElement())
|
|
readUnknownElement(reader);
|
|
}
|
|
}
|
|
|
|
// --- Gitorious
|
|
|
|
Gitorious::Gitorious() :
|
|
m_networkManager(0)
|
|
{
|
|
}
|
|
|
|
Gitorious &Gitorious::instance()
|
|
{
|
|
static Gitorious gitorious;
|
|
return gitorious;
|
|
}
|
|
|
|
void Gitorious::emitError(const QString &e)
|
|
{
|
|
qWarning("%s\n", qPrintable(e));
|
|
emit error(e);
|
|
}
|
|
|
|
void Gitorious::addHost(const QString &addr, const QString &description)
|
|
{
|
|
addHost(GitoriousHost(addr, description));
|
|
}
|
|
|
|
void Gitorious::addHost(const GitoriousHost &host)
|
|
{
|
|
if (debug)
|
|
qDebug() << host;
|
|
const int index = m_hosts.size();
|
|
m_hosts.push_back(host);
|
|
if (host.categories.empty()) {
|
|
updateCategories(index);
|
|
m_hosts.back().state = GitoriousHost::ProjectsQueryRunning;
|
|
} else {
|
|
m_hosts.back().state = GitoriousHost::ProjectsComplete;
|
|
}
|
|
if (host.projects.empty())
|
|
updateProjectList(index);
|
|
emit hostAdded(index);
|
|
}
|
|
|
|
void Gitorious::removeAt(int index)
|
|
{
|
|
m_hosts.removeAt(index);
|
|
emit hostRemoved(index);
|
|
}
|
|
|
|
int Gitorious::findByHostName(const QString &hostName) const
|
|
{
|
|
const int size = m_hosts.size();
|
|
for (int i = 0; i < size; i++)
|
|
if (m_hosts.at(i).hostName == hostName)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
void Gitorious::setHostDescription(int index, const QString &s)
|
|
{
|
|
m_hosts[index].description = s;
|
|
}
|
|
|
|
QString Gitorious::hostDescription(int index) const
|
|
{
|
|
return m_hosts.at(index).description;
|
|
}
|
|
|
|
void Gitorious::listCategoriesReply(int index, QByteArray dataB)
|
|
{
|
|
/* For now, parse the HTML of the projects site for "Popular Categories":
|
|
* \code
|
|
* <h4>Popular Categories:</h4>
|
|
* <ul class="...">
|
|
* <li class="..."><a href="..."><category></a> </li>
|
|
* \endcode */
|
|
do {
|
|
const int catIndex = dataB.indexOf("Popular Categories:");
|
|
const int endIndex = catIndex != -1 ? dataB.indexOf("</ul>", catIndex) : -1;
|
|
if (debug)
|
|
qDebug() << "listCategoriesReply cat pos=" << catIndex << endIndex;
|
|
if (endIndex == -1)
|
|
break;
|
|
dataB.truncate(endIndex);
|
|
dataB.remove(0, catIndex);
|
|
const QString data = QString::fromUtf8(dataB);
|
|
// Cut out the contents of the anchors
|
|
QRegExp pattern = QRegExp(QLatin1String("<a href=[^>]+>([^<]+)</a>"));
|
|
Q_ASSERT(pattern.isValid());
|
|
GitoriousHost::CategoryList &categories = m_hosts[index].categories;
|
|
for (int pos = pattern.indexIn(data) ; pos != -1; ) {
|
|
const QString cat = pattern.cap(1);
|
|
categories.push_back(QSharedPointer<GitoriousCategory>(new GitoriousCategory(cat)));
|
|
pos = pattern.indexIn(data, pos + pattern.matchedLength());
|
|
}
|
|
} while (false);
|
|
|
|
emit categoryListReceived(index);
|
|
}
|
|
|
|
void Gitorious::listProjectsReply(int hostIndex, int page, const QByteArray &data)
|
|
{
|
|
// Receive projects.
|
|
QString errorMessage;
|
|
GitoriousCategory::ProjectList projects = GitoriousProjectReader().read(data, &errorMessage);
|
|
|
|
if (debug) {
|
|
qDebug() << "listProjectsReply" << hostName(hostIndex)
|
|
<< "page=" << page << " got" << projects.size();
|
|
if (debug > 1)
|
|
qDebug() << '\n' <<data;
|
|
}
|
|
|
|
if (!errorMessage.isEmpty()) {
|
|
emitError(tr("Error parsing reply from '%1': %2").arg(hostName(hostIndex), errorMessage));
|
|
if (projects.empty())
|
|
m_hosts[hostIndex].state = GitoriousHost::Error;
|
|
}
|
|
|
|
// Add the projects and start next request if 20 projects received
|
|
GitoriousCategory::ProjectList &hostProjects = m_hosts[hostIndex].projects;
|
|
if (!projects.empty())
|
|
hostProjects.append(projects);
|
|
|
|
if (projects.size() == ProjectsPageSize) {
|
|
startProjectsRequest(hostIndex, page + 1);
|
|
emit projectListPageReceived(hostIndex, page);
|
|
} else {
|
|
// We are done
|
|
m_hosts[hostIndex].state = GitoriousHost::ProjectsComplete;
|
|
emit projectListReceived(hostIndex);
|
|
}
|
|
}
|
|
|
|
static inline int replyPage(const QNetworkReply *reply)
|
|
{ return reply->property(pagePropertyC).toInt(); }
|
|
|
|
void Gitorious::slotReplyFinished()
|
|
{
|
|
// Dispatch the answers via dynamic properties
|
|
if (QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender())) {
|
|
const int protocol = reply->property(protocolPropertyC).toInt();
|
|
// Locate host by name (in case one was deleted in the meantime)
|
|
const QString hostName = reply->property(hostNamePropertyC).toString();
|
|
const int hostIndex = findByHostName(hostName);
|
|
if (hostIndex == -1) // Entry deleted in-between?
|
|
return;
|
|
if (reply->error() == QNetworkReply::NoError) {
|
|
const QByteArray data = reply->readAll();
|
|
switch (protocol) {
|
|
case ListProjectsProtocol:
|
|
listProjectsReply(hostIndex, replyPage(reply), data);
|
|
break;
|
|
case ListCategoriesProtocol:
|
|
listCategoriesReply(hostIndex, data);
|
|
break;
|
|
|
|
} // switch protocol
|
|
} else {
|
|
const QString msg = tr("Request failed for '%1': %2").arg(m_hosts.at(hostIndex).hostName, reply->errorString());
|
|
emitError(msg);
|
|
}
|
|
reply->deleteLater();
|
|
}
|
|
}
|
|
|
|
// Create a network request. Set dynamic properties on it to be able to
|
|
// dispatch. Use host name in case an entry is removed in-between
|
|
QNetworkReply *Gitorious::createRequest(const QUrl &url, int protocol, int hostIndex, int page)
|
|
{
|
|
if (!m_networkManager)
|
|
m_networkManager = new QNetworkAccessManager(this);
|
|
QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
|
|
connect(reply, SIGNAL(finished()), this, SLOT(slotReplyFinished()));
|
|
reply->setProperty(protocolPropertyC, QVariant(protocol));
|
|
reply->setProperty(hostNamePropertyC, QVariant(hostName(hostIndex)));
|
|
if (page >= 0)
|
|
reply->setProperty(pagePropertyC, QVariant(page));
|
|
if (debug)
|
|
qDebug() << "createRequest" << url;
|
|
return reply;
|
|
}
|
|
|
|
void Gitorious::updateCategories(int index)
|
|
{
|
|
// For now, parse the HTML of the projects site for "Popular Categories":
|
|
QUrl url;
|
|
url.setScheme(QLatin1String("http"));
|
|
url.setHost(hostName(index));
|
|
url.setPath(QLatin1String("/projects"));
|
|
createRequest(url, ListCategoriesProtocol, index);
|
|
}
|
|
|
|
void Gitorious::updateProjectList(int hostIndex)
|
|
{
|
|
startProjectsRequest(hostIndex);
|
|
}
|
|
|
|
void Gitorious::startProjectsRequest(int hostIndex, int page)
|
|
{
|
|
const QUrl url = xmlRequest(hostName(hostIndex), QLatin1String("projects"), page);
|
|
createRequest(url, ListProjectsProtocol, hostIndex, page);
|
|
}
|
|
|
|
// Serialize hosts/descriptions as a list of "<host>|descr".
|
|
void Gitorious::saveSettings(const QString &group, QSettings *s)
|
|
{
|
|
const QChar separator = QLatin1Char('|');
|
|
QStringList hosts;
|
|
foreach(const GitoriousHost &h, m_hosts) {
|
|
QString entry = h.hostName;
|
|
if (!h.description.isEmpty()) {
|
|
entry += separator;
|
|
entry += h.description;
|
|
}
|
|
hosts.push_back(entry);
|
|
}
|
|
s->beginGroup(group);
|
|
s->setValue(QLatin1String(settingsKeyC), hosts);
|
|
s->endGroup();
|
|
}
|
|
|
|
void Gitorious::restoreSettings(const QString &group, const QSettings *s)
|
|
{
|
|
m_hosts.clear();
|
|
const QChar separator = QLatin1Char('|');
|
|
const QStringList hosts = s->value(group + QLatin1Char('/') + QLatin1String(settingsKeyC), QStringList()).toStringList();
|
|
foreach (const QString &h, hosts) {
|
|
const int sepPos = h.indexOf(separator);
|
|
if (sepPos == -1) {
|
|
addHost(GitoriousHost(h));
|
|
} else {
|
|
addHost(GitoriousHost(h.mid(0, sepPos), h.mid(sepPos + 1)));
|
|
}
|
|
}
|
|
}
|
|
|
|
GitoriousHost Gitorious::gitoriousOrg()
|
|
{
|
|
return GitoriousHost(QLatin1String("gitorious.org"), tr("Open source projects that use Git."));
|
|
}
|
|
|
|
} // namespace Internal
|
|
} // namespace Gitorious
|