diff --git a/src/plugins/coreplugin/iversioncontrol.h b/src/plugins/coreplugin/iversioncontrol.h index 90831b2323f4ec6255e2180b57100dcefd685a8a..ad7b8175f0e9f16a69184918f8a006bfe0da11fb 100644 --- a/src/plugins/coreplugin/iversioncontrol.h +++ b/src/plugins/coreplugin/iversioncontrol.h @@ -41,8 +41,11 @@ class CORE_EXPORT IVersionControl : public QObject { Q_OBJECT public: - enum Operation { AddOperation, DeleteOperation, OpenOperation, - CreateRepositoryOperation }; + enum Operation { + AddOperation, DeleteOperation, OpenOperation, + CreateRepositoryOperation, + SnapshotOperations + }; explicit IVersionControl(QObject *parent = 0) : QObject(parent) {} virtual ~IVersionControl() {} @@ -98,10 +101,31 @@ public: virtual bool vcsDelete(const QString &filename) = 0; /*! - * Called to initialize the version control systemin a directory. + * Called to initialize the version control system in a directory. */ virtual bool vcsCreateRepository(const QString &directory) = 0; + /*! + * Create a snapshot of the current state and return an identifier or + * an empty string in case of failure. + */ + virtual QString vcsCreateSnapshot(const QString &topLevel) = 0; + + /*! + * List snapshots. + */ + virtual QStringList vcsSnapshots(const QString &topLevel) = 0; + + /*! + * Restore a snapshot. + */ + virtual bool vcsRestoreSnapshot(const QString &topLevel, const QString &name) = 0; + + /*! + * Remove a snapshot. + */ + virtual bool vcsRemoveSnapshot(const QString &topLevel, const QString &name) = 0; + signals: void repositoryChanged(const QString &repository); void filesChanged(const QStringList &files); diff --git a/src/plugins/cvs/cvscontrol.cpp b/src/plugins/cvs/cvscontrol.cpp index da57f43d5a96c17e269139935c857d3e6d4751e9..343834cc66da9e95299b37209f210d593baa13a8 100644 --- a/src/plugins/cvs/cvscontrol.cpp +++ b/src/plugins/cvs/cvscontrol.cpp @@ -55,6 +55,7 @@ bool CVSControl::supportsOperation(Operation operation) const break; case OpenOperation: case CreateRepositoryOperation: + case SnapshotOperations: rc = false; break; } @@ -84,6 +85,26 @@ bool CVSControl::vcsCreateRepository(const QString &) return false; } +QString CVSControl::vcsCreateSnapshot(const QString &) +{ + return QString(); +} + +QStringList CVSControl::vcsSnapshots(const QString &) +{ + return QStringList(); +} + +bool CVSControl::vcsRestoreSnapshot(const QString &, const QString &) +{ + return false; +} + +bool CVSControl::vcsRemoveSnapshot(const QString &, const QString &) +{ + return false; +} + bool CVSControl::managesDirectory(const QString &directory) const { return m_plugin->managesDirectory(directory); diff --git a/src/plugins/cvs/cvscontrol.h b/src/plugins/cvs/cvscontrol.h index f53c38b92574179797a966d7fba6759a84551a96..2d8a1cc99c7b606def2f28de5b0dca9e524e6a66 100644 --- a/src/plugins/cvs/cvscontrol.h +++ b/src/plugins/cvs/cvscontrol.h @@ -53,6 +53,10 @@ public: virtual bool vcsAdd(const QString &fileName); virtual bool vcsDelete(const QString &filename); virtual bool vcsCreateRepository(const QString &directory); + virtual QString vcsCreateSnapshot(const QString &topLevel); + virtual QStringList vcsSnapshots(const QString &topLevel); + virtual bool vcsRestoreSnapshot(const QString &topLevel, const QString &name); + virtual bool vcsRemoveSnapshot(const QString &topLevel, const QString &name); void emitRepositoryChanged(const QString &s); void emitFilesChanged(const QStringList &l); diff --git a/src/plugins/git/git.pro b/src/plugins/git/git.pro index fa87f66d3d80c5003fd6f7cb808aeebb4acccaea..0ef64c74bb39ce2c71dffaa7910f007212c92afa 100644 --- a/src/plugins/git/git.pro +++ b/src/plugins/git/git.pro @@ -22,7 +22,9 @@ HEADERS += gitplugin.h \ branchmodel.h \ gitcommand.h \ clonewizard.h \ - clonewizardpage.h + clonewizardpage.h \ + stashdialog.h \ + gitutils.h SOURCES += gitplugin.cpp \ gitclient.cpp \ changeselectiondialog.cpp \ @@ -38,11 +40,13 @@ SOURCES += gitplugin.cpp \ branchmodel.cpp \ gitcommand.cpp \ clonewizard.cpp \ - clonewizardpage.cpp + clonewizardpage.cpp \ + stashdialog.cpp \ + gitutils.cpp FORMS += changeselectiondialog.ui \ settingspage.ui \ gitsubmitpanel.ui \ - branchdialog.ui - + branchdialog.ui \ + stashdialog.ui OTHER_FILES += ScmGit.pluginspec include(gitorious/gitorious.pri) diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp index 6e45bc7818c13e42fab66c6bb24716a7b2538878..88b88b9c633891fba265d9b45cd42a58c9fd4c26 100644 --- a/src/plugins/git/gitclient.cpp +++ b/src/plugins/git/gitclient.cpp @@ -29,6 +29,7 @@ #include "gitclient.h" #include "gitcommand.h" +#include "gitutils.h" #include "commitdata.h" #include "gitconstants.h" @@ -64,15 +65,12 @@ #include <QtGui/QMessageBox> #include <QtGui/QPushButton> -using namespace Git; -using namespace Git::Internal; - static const char *const kGitDirectoryC = ".git"; static const char *const kBranchIndicatorC = "# On branch"; static inline QString msgServerFailure() { - return GitClient::tr( + return Git::Internal::GitClient::tr( "Note that the git plugin for QtCreator is not able to interact with the server " "so far. Thus, manual ssh-identification etc. will not work."); } @@ -85,6 +83,29 @@ inline Core::IEditor* locateEditor(const Core::ICore *core, const char *property return 0; } +// Return converted command output, remove '\r' read on Windows +static inline QString commandOutputFromLocal8Bit(const QByteArray &a) +{ + QString output = QString::fromLocal8Bit(a); + output.remove(QLatin1Char('\r')); + return output; +} + +// Return converted command output split into lines +static inline QStringList commandOutputLinesFromLocal8Bit(const QByteArray &a) +{ + QString output = commandOutputFromLocal8Bit(a); + const QChar newLine = QLatin1Char('\n'); + if (output.endsWith(newLine)) + output.truncate(output.size() - 1); + if (output.isEmpty()) + return QStringList(); + return output.split(newLine); +} + +namespace Git { +namespace Internal { + static inline QString msgRepositoryNotFound(const QString &dir) { return GitClient::tr("Unable to determine the repository for %1.").arg(dir); @@ -103,6 +124,9 @@ static QString formatCommand(const QString &binary, const QStringList &args) } // ---------------- GitClient + +const char *GitClient::stashNamePrefix = "stash@{"; + GitClient::GitClient(GitPlugin* plugin) : m_msgWait(tr("Waiting for data...")), m_plugin(plugin), @@ -364,6 +388,30 @@ void GitClient::checkoutBranch(const QString &workingDirectory, const QString &b connectRepositoryChanged(workingDirectory, cmd); } +bool GitClient::synchronousCheckoutBranch(const QString &workingDirectory, + const QString &branch, + QString *errorMessage /* = 0 */) +{ + QByteArray outputText; + QByteArray errorText; + QStringList arguments; + arguments << QLatin1String("checkout") << branch; + const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); + const QString output = commandOutputFromLocal8Bit(outputText); + VCSBase::VCSBaseOutputWindow::instance()->append(output); + if (!rc) { + const QString stdErr = commandOutputFromLocal8Bit(errorText); + const QString msg = tr("Unable to checkout %1 of %2: %3").arg(branch, workingDirectory, stdErr); + if (errorMessage) { + *errorMessage = msg; + } else { + VCSBase::VCSBaseOutputWindow::instance()->appendError(msg); + } + return false; + } + return true; +} + void GitClient::checkout(const QString &workingDirectory, const QString &fileName) { // Passing an empty argument as the file name is very dangereous, since this makes @@ -408,22 +456,12 @@ bool GitClient::synchronousAdd(const QString &workingDirectory, const QStringLis const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); if (!rc) { const QString errorMessage = tr("Unable to add %n file(s) to %1: %2", 0, files.size()). - arg(workingDirectory, QString::fromLocal8Bit(errorText)); + arg(workingDirectory, commandOutputFromLocal8Bit(errorText)); VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage); } return rc; } -bool GitClient::synchronousReset(const QString &workingDirectory, - const QStringList &files) -{ - QString errorMessage; - const bool rc = synchronousReset(workingDirectory, files, &errorMessage); - if (!rc) - VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage); - return rc; -} - bool GitClient::synchronousReset(const QString &workingDirectory, const QStringList &files, QString *errorMessage) @@ -433,14 +471,27 @@ bool GitClient::synchronousReset(const QString &workingDirectory, QByteArray outputText; QByteArray errorText; QStringList arguments; - arguments << QLatin1String("reset") << QLatin1String("HEAD") << QLatin1String("--") << files; + arguments << QLatin1String("reset"); + if (files.isEmpty()) { + arguments << QLatin1String("--hard"); + } else { + arguments << QLatin1String("HEAD") << QLatin1String("--") << files; + } const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); - const QString output = QString::fromLocal8Bit(outputText); + const QString output = commandOutputFromLocal8Bit(outputText); VCSBase::VCSBaseOutputWindow::instance()->append(output); // Note that git exits with 1 even if the operation is successful // Assume real failure if the output does not contain "foo.cpp modified" if (!rc && !output.contains(QLatin1String("modified"))) { - *errorMessage = tr("Unable to reset %n file(s) in %1: %2", 0, files.size()).arg(workingDirectory, QString::fromLocal8Bit(errorText)); + const QString stdErr = commandOutputFromLocal8Bit(errorText); + const QString msg = files.isEmpty() ? + tr("Unable to reset %1: %2").arg(workingDirectory, stdErr) : + tr("Unable to reset %n file(s) in %1: %2", 0, files.size()).arg(workingDirectory, stdErr); + if (errorMessage) { + *errorMessage = msg; + } else { + VCSBase::VCSBaseOutputWindow::instance()->appendError(msg); + } return false; } return true; @@ -456,25 +507,41 @@ bool GitClient::synchronousInit(const QString &workingDirectory) const QStringList arguments(QLatin1String("init")); const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); // '[Re]Initialized...' - VCSBase::VCSBaseOutputWindow::instance()->append(QString::fromLocal8Bit(outputText)); + VCSBase::VCSBaseOutputWindow::instance()->append(commandOutputFromLocal8Bit(outputText)); if (!rc) - VCSBase::VCSBaseOutputWindow::instance()->append(QString::fromLocal8Bit(errorText)); + VCSBase::VCSBaseOutputWindow::instance()->appendError(commandOutputFromLocal8Bit(errorText)); return rc; } -bool GitClient::synchronousCheckout(const QString &workingDirectory, - const QStringList &files, - QString *errorMessage) +/* Checkout, supports: + * git checkout -- <files> + * git checkout revision -- <files> + * git checkout revision -- . */ +bool GitClient::synchronousCheckoutFiles(const QString &workingDirectory, + QStringList files /* = QStringList() */, + QString revision /* = QString() */, + QString *errorMessage /* = 0 */) { if (Git::Constants::debug) qDebug() << Q_FUNC_INFO << workingDirectory << files; + if (revision.isEmpty()) + revision = QLatin1String("HEAD"); + if (files.isEmpty()) + files = QStringList(QString(QLatin1Char('.'))); QByteArray outputText; QByteArray errorText; QStringList arguments; - arguments << QLatin1String("checkout") << QLatin1String("--") << files; + arguments << QLatin1String("checkout") << revision << QLatin1String("--") << files; const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); if (!rc) { - *errorMessage = tr("Unable to checkout %n file(s) in %1: %2", 0, files.size()).arg(workingDirectory, QString::fromLocal8Bit(errorText)); + const QString fileArg = files.join(QLatin1String(", ")); + const QString msg = tr("Unable to checkout %1 of %2 in %3: %4"). + arg(revision, fileArg, workingDirectory, commandOutputFromLocal8Bit(errorText)); + if (errorMessage) { + *errorMessage = msg; + } else { + VCSBase::VCSBaseOutputWindow::instance()->appendError(msg); + } return false; } return true; @@ -533,13 +600,12 @@ bool GitClient::synchronousParentRevisions(const QString &workingDirectory, } const bool rc = synchronousGit(workingDirectory, arguments, &outputTextData, &errorText); if (!rc) { - *errorMessage = msgParentRevisionFailed(workingDirectory, revision, QString::fromLocal8Bit(errorText)); + *errorMessage = msgParentRevisionFailed(workingDirectory, revision, commandOutputFromLocal8Bit(errorText)); return false; } // Should result in one line of blank-delimited revisions, specifying current first // unless it is top. - QString outputText = QString::fromLocal8Bit(outputTextData); - outputText.remove(QLatin1Char('\r')); + QString outputText = commandOutputFromLocal8Bit(outputTextData); outputText.remove(QLatin1Char('\n')); if (!splitCommitParents(outputText, 0, parents)) { *errorMessage = msgParentRevisionFailed(workingDirectory, revision, msgInvalidRevision()); @@ -578,6 +644,75 @@ bool GitClient::synchronousShortDescriptions(const QString &workingDirectory, co return true; } +static inline QString msgCannotDetermineBranch(const QString &workingDirectory, const QString &why) +{ + return GitClient::tr("Unable to retrieve branch of %1: %2").arg(workingDirectory, why); +} + +// Retrieve head revision/branch +bool GitClient::synchronousTopRevision(const QString &workingDirectory, + QString *revision /* = 0 */, + QString *branch /* = 0 */, + QString *errorMessageIn /* = 0 */) +{ + if (Git::Constants::debug) + qDebug() << Q_FUNC_INFO << workingDirectory; + QByteArray outputTextData; + QByteArray errorText; + QStringList arguments; + QString errorMessage; + do { + // get revision + if (revision) { + revision->clear(); + arguments << QLatin1String("log") << QLatin1String(noColorOption) + << QLatin1String("--max-count=1") << QLatin1String("--pretty=format:%H"); + if (!synchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) { + errorMessage = tr("Unable to retrieve top revision of %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText)); + break; + } + *revision = commandOutputFromLocal8Bit(outputTextData); + revision->remove(QLatin1Char('\n')); + } // revision desired + // get branch + if (branch) { + branch->clear(); + arguments.clear(); + arguments << QLatin1String("branch") << QLatin1String(noColorOption); + if (!synchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) { + errorMessage = msgCannotDetermineBranch(workingDirectory, commandOutputFromLocal8Bit(errorText)); + break; + } + /* parse output for current branch: \code +* master + branch2 +\endcode */ + const QString branchPrefix = QLatin1String("* "); + foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputTextData)) { + if (line.startsWith(branchPrefix)) { + *branch = line; + branch->remove(0, branchPrefix.size()); + break; + } + } + if (branch->isEmpty()) { + errorMessage = msgCannotDetermineBranch(workingDirectory, + QString::fromLatin1("Internal error: Failed to parse output: %1").arg(commandOutputFromLocal8Bit(outputTextData))); + break; + } + } // branch + } while (false); + const bool failed = (revision && revision->isEmpty()) || (branch && branch->isEmpty()); + if (failed && !errorMessage.isEmpty()) { + if (errorMessageIn) { + *errorMessageIn = errorMessage; + } else { + VCSBase::VCSBaseOutputWindow::instance()->appendError(errorMessage); + } + } + return !failed; +} + // Format an entry in a one-liner for selection list using git log. bool GitClient::synchronousShortDescription(const QString &workingDirectory, const QString &revision, @@ -595,17 +730,83 @@ bool GitClient::synchronousShortDescription(const QString &workingDirectory, << QLatin1String("--max-count=1") << revision; const bool rc = synchronousGit(workingDirectory, arguments, &outputTextData, &errorText); if (!rc) { - *errorMessage = tr("Unable to describe revision %1 in %2: %3").arg(revision, workingDirectory, QString::fromLocal8Bit(errorText)); + *errorMessage = tr("Unable to describe revision %1 in %2: %3").arg(revision, workingDirectory, commandOutputFromLocal8Bit(errorText)); return false; } - *description = QString::fromLocal8Bit(outputTextData); - description->remove(QLatin1Char('\r')); + *description = commandOutputFromLocal8Bit(outputTextData); if (description->endsWith(QLatin1Char('\n'))) description->truncate(description->size() - 1); return true; } -bool GitClient::synchronousStash(const QString &workingDirectory, QString *errorMessage) +// Create a default message to be used for describing stashes +static inline QString creatorStashMessage(const QString &keyword = QString()) +{ + QString rc = QCoreApplication::applicationName(); + rc += QLatin1Char(' '); + if (!keyword.isEmpty()) { + rc += keyword; + rc += QLatin1Char(' '); + } + rc += QDateTime::currentDateTime().toString(Qt::ISODate); + return rc; +} + +/* Do a stash and return the message as identifier. Note that stash names (stash{n}) + * shift as they are pushed, so, enforce the use of messages to identify them. Flags: + * StashPromptDescription: Prompt the user for a description message. + * StashImmediateRestore: Immediately re-apply this stash (used for snapshots), user keeps on working + * StashIgnoreUnchanged: Be quiet about unchanged repositories (used for IVersionControl's snapshots). */ + +QString GitClient::synchronousStash(const QString &workingDirectory, + const QString &messageKeyword /* = QString() */, + unsigned flags, + bool *unchanged /* =0 */) +{ + if (unchanged) + *unchanged = false; + QString message; + bool success = false; + // Check for changes and stash + QString errorMessage; + switch (gitStatus(workingDirectory, false, 0, &errorMessage)) { + case StatusChanged: { + message = creatorStashMessage(messageKeyword); + do { + if ((flags & StashPromptDescription)) { + if (!inputText(Core::ICore::instance()->mainWindow(), + tr("Stash description"), tr("Description:"), &message)) + break; + } + if (!executeSynchronousStash(workingDirectory, message)) + break; + if ((flags & StashImmediateRestore) + && !synchronousStashRestore(workingDirectory, QLatin1String("stash@{0}"))) + break; + success = true; + } while (false); + } + break; + case StatusUnchanged: + if (unchanged) + *unchanged = true; + if (!(flags & StashIgnoreUnchanged)) + VCSBase::VCSBaseOutputWindow::instance()->append(msgNoChangedFiles()); + break; + case StatusFailed: + VCSBase::VCSBaseOutputWindow::instance()->append(errorMessage); + break; + } + if (!success) + message.clear(); + if (Git::Constants::debug) + qDebug() << Q_FUNC_INFO << '\n' << workingDirectory << messageKeyword << "returns" << message; + return message; +} + +bool GitClient::executeSynchronousStash(const QString &workingDirectory, + const QString &message, + QString *errorMessage /* = 0*/) { if (Git::Constants::debug) qDebug() << Q_FUNC_INFO << workingDirectory; @@ -613,14 +814,50 @@ bool GitClient::synchronousStash(const QString &workingDirectory, QString *error QByteArray errorText; QStringList arguments; arguments << QLatin1String("stash"); + if (!message.isEmpty()) + arguments << QLatin1String("save") << message; const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); if (!rc) { - *errorMessage = tr("Unable stash in %1: %2").arg(workingDirectory, QString::fromLocal8Bit(errorText)); + const QString msg = tr("Unable stash in %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText)); + if (errorMessage) { + *errorMessage = msg; + } else { + VCSBase::VCSBaseOutputWindow::instance()->append(msg); + } return false; } return true; } +// Resolve a stash name from message +bool GitClient::stashNameFromMessage(const QString &workingDirectory, + const QString &message, QString *name, + QString *errorMessage /* = 0 */) +{ + // All happy + if (message.startsWith(QLatin1String(stashNamePrefix))) { + *name = message; + return true; + } + // Retrieve list and find via message + QList<Stash> stashes; + if (!synchronousStashList(workingDirectory, &stashes, errorMessage)) + return false; + foreach (const Stash &s, stashes) { + if (s.message == message) { + *name = s.name; + return true; + } + } + const QString msg = tr("Unable to resolve stash message '%1' in %2").arg(message, workingDirectory); + if (errorMessage) { + *errorMessage = msg; + } else { + VCSBase::VCSBaseOutputWindow::instance()->append(msg); + } + return false; +} + bool GitClient::synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs, QString *output, QString *errorMessage) { @@ -631,10 +868,10 @@ bool GitClient::synchronousBranchCmd(const QString &workingDirectory, QStringLis QByteArray errorText; const bool rc = synchronousGit(workingDirectory, branchArgs, &outputText, &errorText); if (!rc) { - *errorMessage = tr("Unable to run branch command: %1: %2").arg(workingDirectory, QString::fromLocal8Bit(errorText)); + *errorMessage = tr("Unable to run branch command: %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText)); return false; } - *output = QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r')); + *output = commandOutputFromLocal8Bit(outputText); return true; } @@ -649,10 +886,10 @@ bool GitClient::synchronousShow(const QString &workingDirectory, const QString & QByteArray errorText; const bool rc = synchronousGit(workingDirectory, args, &outputText, &errorText); if (!rc) { - *errorMessage = tr("Unable to run show: %1: %2").arg(workingDirectory, QString::fromLocal8Bit(errorText)); + *errorMessage = tr("Unable to run show: %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText)); return false; } - *output = QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r')); + *output = commandOutputFromLocal8Bit(outputText); return true; } @@ -805,7 +1042,7 @@ GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory, Q case QMessageBox::Cancel: return StashCanceled; case QMessageBox::Yes: - if (!synchronousStash(workingDirectory, errorMessage)) + if (!executeSynchronousStash(workingDirectory, creatorStashMessage(QLatin1String("push")), errorMessage)) return StashFailed; break; case QMessageBox::No: // At your own risk, so. @@ -845,11 +1082,11 @@ GitClient::StatusResult GitClient::gitStatus(const QString &workingDirectory, const bool statusRc = synchronousGit(workingDirectory, statusArgs, &outputText, &errorText); GitCommand::removeColorCodes(&outputText); if (output) - *output = QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r')); + *output = commandOutputFromLocal8Bit(outputText); // Is it something really fatal? if (!statusRc && !outputText.contains(kBranchIndicatorC)) { if (errorMessage) { - const QString error = QString::fromLocal8Bit(errorText).remove(QLatin1Char('\r')); + const QString error = commandOutputFromLocal8Bit(errorText); *errorMessage = tr("Unable to obtain the status: %1").arg(error); } return StatusFailed; @@ -890,7 +1127,7 @@ bool GitClient::getCommitData(const QString &workingDirectory, if (QFileInfo(descriptionFile).isFile()) { QFile file(descriptionFile); if (file.open(QIODevice::ReadOnly|QIODevice::Text)) - d->panelInfo.description = QString::fromLocal8Bit(file.readAll()).trimmed(); + d->panelInfo.description = commandOutputFromLocal8Bit(file.readAll()).trimmed(); } // Run status. Note that it has exitcode 1 if there are no added files. @@ -996,7 +1233,7 @@ bool GitClient::addAndCommit(const QString &repositoryDirectory, if (rc) { VCSBase::VCSBaseOutputWindow::instance()->append(tr("Committed %n file(s).\n", 0, checkedFiles.size())); } else { - VCSBase::VCSBaseOutputWindow::instance()->appendError(tr("Unable to commit %n file(s): %1\n", 0, checkedFiles.size()).arg(QString::fromLocal8Bit(errorText))); + VCSBase::VCSBaseOutputWindow::instance()->appendError(tr("Unable to commit %n file(s): %1\n", 0, checkedFiles.size()).arg(commandOutputFromLocal8Bit(errorText))); } return rc; } @@ -1086,7 +1323,7 @@ GitClient::RevertResult GitClient::revertI(QStringList files, bool *ptrToIsDirec if (!stagedFiles.empty() && !synchronousReset(repoDirectory, stagedFiles, errorMessage)) return RevertFailed; // Finally revert! - if (!synchronousCheckout(repoDirectory, stagedFiles + unstagedFiles, errorMessage)) + if (!synchronousCheckoutFiles(repoDirectory, stagedFiles + unstagedFiles, QString(), errorMessage)) return RevertFailed; return RevertOk; } @@ -1128,23 +1365,6 @@ QString GitClient::msgNoChangedFiles() return tr("There are no modified files."); } -void GitClient::stash(const QString &workingDirectory) -{ - // Check for changes and stash - QString errorMessage; - switch (gitStatus(workingDirectory, false, 0, &errorMessage)) { - case StatusChanged: - executeGit(workingDirectory, QStringList(QLatin1String("stash")), 0, true); - break; - case StatusUnchanged: - VCSBase::VCSBaseOutputWindow::instance()->append(msgNoChangedFiles()); - break; - case StatusFailed: - VCSBase::VCSBaseOutputWindow::instance()->append(errorMessage); - break; - } -} - void GitClient::stashPop(const QString &workingDirectory) { QStringList arguments(QLatin1String("stash")); @@ -1153,6 +1373,70 @@ void GitClient::stashPop(const QString &workingDirectory) connectRepositoryChanged(workingDirectory, cmd); } +bool GitClient::synchronousStashRestore(const QString &workingDirectory, + const QString &stash, + const QString &branch /* = QString()*/, + QString *errorMessage) +{ + QStringList arguments(QLatin1String("stash")); + if (branch.isEmpty()) { + arguments << QLatin1String("apply") << stash; + } else { + arguments << QLatin1String("branch") << branch << stash; + } + QByteArray outputText; + QByteArray errorText; + const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); + if (!rc) { + const QString stdErr = commandOutputFromLocal8Bit(errorText); + const QString msg = branch.isEmpty() ? + tr("Unable to restore stash %1: %2").arg(workingDirectory, stdErr) : + tr("Unable to restore stash %1 to branch %2: %3").arg(workingDirectory, branch, stdErr); + if (errorMessage) { + *errorMessage = msg; + } else { + VCSBase::VCSBaseOutputWindow::instance()->append(msg); + } + return false; + } + QString output = commandOutputFromLocal8Bit(outputText); + if (!output.isEmpty()) + VCSBase::VCSBaseOutputWindow::instance()->append(output); + GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory); + return true; +} + +bool GitClient::synchronousStashRemove(const QString &workingDirectory, + const QString &stash /* = QString() */, + QString *errorMessage /* = 0 */) +{ + QStringList arguments(QLatin1String("stash")); + if (stash.isEmpty()) { + arguments << QLatin1String("clear"); + } else { + arguments << QLatin1String("drop") << stash; + } + QByteArray outputText; + QByteArray errorText; + const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); + if (!rc) { + const QString stdErr = commandOutputFromLocal8Bit(errorText); + const QString msg = stash.isEmpty() ? + tr("Unable to remove stashes of %1: %2").arg(workingDirectory, stdErr) : + tr("Unable to remove stash %1 of %2: %3").arg(stash, workingDirectory, stdErr); + if (errorMessage) { + *errorMessage = msg; + } else { + VCSBase::VCSBaseOutputWindow::instance()->append(msg); + } + return false; + } + QString output = commandOutputFromLocal8Bit(outputText); + if (!output.isEmpty()) + VCSBase::VCSBaseOutputWindow::instance()->append(output); + return true; +} + void GitClient::branchList(const QString &workingDirectory) { QStringList arguments(QLatin1String("branch")); @@ -1167,6 +1451,34 @@ void GitClient::stashList(const QString &workingDirectory) executeGit(workingDirectory, arguments, 0, true); } +bool GitClient::synchronousStashList(const QString &workingDirectory, + QList<Stash> *stashes, + QString *errorMessage /* = 0 */) +{ + stashes->clear(); + QStringList arguments(QLatin1String("stash")); + arguments << QLatin1String("list") << QLatin1String(noColorOption); + QByteArray outputText; + QByteArray errorText; + const bool rc = synchronousGit(workingDirectory, arguments, &outputText, &errorText); + if (!rc) { + const QString msg = tr("Unable retrieve stash list of %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText)); + if (errorMessage) { + *errorMessage = msg; + } else { + VCSBase::VCSBaseOutputWindow::instance()->append(msg); + } + return false; + } + Stash stash; + foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputText)) + if (stash.parseStashLine(line)) + stashes->push_back(stash); + if (Git::Constants::debug) + qDebug() << Q_FUNC_INFO << *stashes; + return true; +} + QString GitClient::readConfig(const QString &workingDirectory, const QStringList &configVar) { QStringList arguments; @@ -1174,7 +1486,7 @@ QString GitClient::readConfig(const QString &workingDirectory, const QStringList QByteArray outputText; if (synchronousGit(workingDirectory, arguments, &outputText, 0, false)) - return QString::fromLocal8Bit(outputText).remove(QLatin1Char('\r')); + return commandOutputFromLocal8Bit(outputText); return QString(); } @@ -1211,3 +1523,6 @@ void GitClient::connectRepositoryChanged(const QString & repository, GitCommand connect(cmd, SIGNAL(success()), m_repositoryChangedSignalMapper, SLOT(map()), Qt::QueuedConnection); } + +} +} diff --git a/src/plugins/git/gitclient.h b/src/plugins/git/gitclient.h index 6d6e0c253736e38ac57d95a066ec90f65bcc39df..790273aa06a5c4bfc05330e39787a1af758f58d4 100644 --- a/src/plugins/git/gitclient.h +++ b/src/plugins/git/gitclient.h @@ -41,6 +41,7 @@ QT_BEGIN_NAMESPACE class QErrorMessage; class QSignalMapper; +class QDebug; QT_END_NAMESPACE namespace Core { @@ -59,12 +60,15 @@ class GitOutputWindow; class GitCommand; struct CommitData; struct GitSubmitEditorPanelData; +struct Stash; class GitClient : public QObject { Q_OBJECT public: + static const char *stashNamePrefix; + explicit GitClient(GitPlugin *plugin); ~GitClient(); @@ -84,17 +88,37 @@ public: bool enableAnnotationContextMenu = false); void blame(const QString &workingDirectory, const QString &fileName, const QString &revision = QString(), int lineNumber = -1); - void showCommit(const QString &workingDirectory, const QString &commit); void checkout(const QString &workingDirectory, const QString &file); void checkoutBranch(const QString &workingDirectory, const QString &branch); - void hardReset(const QString &workingDirectory, const QString &commit); + void hardReset(const QString &workingDirectory, const QString &commit = QString()); void addFile(const QString &workingDirectory, const QString &fileName); bool synchronousAdd(const QString &workingDirectory, const QStringList &files); - bool synchronousReset(const QString &workingDirectory, const QStringList &files); - bool synchronousReset(const QString &workingDirectory, const QStringList &files, QString *errorMessage); + bool synchronousReset(const QString &workingDirectory, + const QStringList &files = QStringList(), + QString *errorMessage = 0); bool synchronousInit(const QString &workingDirectory); - bool synchronousCheckout(const QString &workingDirectory, const QStringList &files, QString *errorMessage); - bool synchronousStash(const QString &workingDirectory, QString *errorMessage); + bool synchronousCheckoutFiles(const QString &workingDirectory, + QStringList files = QStringList(), + QString revision = QString(), QString *errorMessage = 0); + // Checkout branch + bool synchronousCheckoutBranch(const QString &workingDirectory, const QString &branch, QString *errorMessage = 0); + + // Do a stash and return identier. + enum { StashPromptDescription = 0x1, StashImmediateRestore = 0x2, StashIgnoreUnchanged = 0x4 }; + QString synchronousStash(const QString &workingDirectory, + const QString &messageKeyword = QString(), + unsigned flags = 0, bool *unchanged = 0); + + bool executeSynchronousStash(const QString &workingDirectory, + const QString &message = QString(), + QString *errorMessage = 0); + bool synchronousStashRestore(const QString &workingDirectory, + const QString &stash, + const QString &branch = QString(), + QString *errorMessage = 0); + bool synchronousStashRemove(const QString &workingDirectory, + const QString &stash = QString(), + QString *errorMessage = 0); bool synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs, QString *output, QString *errorMessage); bool synchronousShow(const QString &workingDirectory, const QString &id, @@ -110,15 +134,23 @@ public: const QString &format, QString *description, QString *errorMessage); bool synchronousShortDescriptions(const QString &workingDirectory, const QStringList &revisions, QStringList *descriptions, QString *errorMessage); + bool synchronousTopRevision(const QString &workingDirectory, QString *revision = 0, + QString *branch = 0, QString *errorMessage = 0); void pull(const QString &workingDirectory); void push(const QString &workingDirectory); - void stash(const QString &workingDirectory); void stashPop(const QString &workingDirectory); void revert(const QStringList &files); void branchList(const QString &workingDirectory); void stashList(const QString &workingDirectory); + bool synchronousStashList(const QString &workingDirectory, + QList<Stash> *stashes, + QString *errorMessage = 0); + // Resolve a stash name from message (for IVersionControl's names). + bool stashNameFromMessage(const QString &workingDirectory, + const QString &messge, QString *name, + QString *errorMessage = 0); QString readConfig(const QString &workingDirectory, const QStringList &configVar); diff --git a/src/plugins/git/gitplugin.cpp b/src/plugins/git/gitplugin.cpp index a25b5f93c1ea2dc7d412f5c57c71e50145e715cf..cb034ff2a53c227f1748b7206dc89128991f9fd1 100644 --- a/src/plugins/git/gitplugin.cpp +++ b/src/plugins/git/gitplugin.cpp @@ -39,6 +39,7 @@ #include "branchdialog.h" #include "clonewizard.h" #include "gitoriousclonewizard.h" +#include "stashdialog.h" #include <coreplugin/icore.h> #include <coreplugin/coreconstants.h> @@ -134,6 +135,7 @@ GitPlugin::GitPlugin() : m_undoAction(0), m_redoAction(0), m_stashAction(0), + m_stashSnapshotAction(0), m_stashPopAction(0), m_stashListAction(0), m_branchListAction(0), @@ -314,36 +316,37 @@ bool GitPlugin::initialize(const QStringList &arguments, QString *errorMessage) gitContainer->addAction(createSeparator(actionManager, globalcontext, QLatin1String("Git.Sep.Global"), this)); + m_stashSnapshotAction = new QAction(tr("Stash snapshot..."), this); + m_stashSnapshotAction->setToolTip(tr("Saves the current state of your work.")); + command = actionManager->registerAction(m_stashSnapshotAction, "Git.StashSnapshot", globalcontext); + connect(m_stashSnapshotAction, SIGNAL(triggered()), this, SLOT(stashSnapshot())); + gitContainer->addAction(command); + m_stashAction = new QAction(tr("Stash"), this); - m_stashAction->setToolTip(tr("Saves the current state of your work.")); + m_stashAction->setToolTip(tr("Saves the current state of your work and resets the repository.")); command = actionManager->registerAction(m_stashAction, "Git.Stash", globalcontext); - command->setAttribute(Core::Command::CA_UpdateText); connect(m_stashAction, SIGNAL(triggered()), this, SLOT(stash())); gitContainer->addAction(command); m_pullAction = new QAction(tr("Pull"), this); command = actionManager->registerAction(m_pullAction, "Git.Pull", globalcontext); - command->setAttribute(Core::Command::CA_UpdateText); connect(m_pullAction, SIGNAL(triggered()), this, SLOT(pull())); gitContainer->addAction(command); m_stashPopAction = new QAction(tr("Stash Pop"), this); m_stashAction->setToolTip(tr("Restores changes saved to the stash list using \"Stash\".")); command = actionManager->registerAction(m_stashPopAction, "Git.StashPop", globalcontext); - command->setAttribute(Core::Command::CA_UpdateText); connect(m_stashPopAction, SIGNAL(triggered()), this, SLOT(stashPop())); gitContainer->addAction(command); m_commitAction = new QAction(tr("Commit..."), this); command = actionManager->registerAction(m_commitAction, "Git.Commit", globalcontext); command->setDefaultKeySequence(QKeySequence(tr("Alt+G,Alt+C"))); - command->setAttribute(Core::Command::CA_UpdateText); connect(m_commitAction, SIGNAL(triggered()), this, SLOT(startCommit())); gitContainer->addAction(command); m_pushAction = new QAction(tr("Push"), this); command = actionManager->registerAction(m_pushAction, "Git.Push", globalcontext); - command->setAttribute(Core::Command::CA_UpdateText); connect(m_pushAction, SIGNAL(triggered()), this, SLOT(push())); gitContainer->addAction(command); @@ -351,22 +354,30 @@ bool GitPlugin::initialize(const QStringList &arguments, QString *errorMessage) m_branchListAction = new QAction(tr("Branches..."), this); command = actionManager->registerAction(m_branchListAction, "Git.BranchList", globalcontext); - command->setAttribute(Core::Command::CA_UpdateText); connect(m_branchListAction, SIGNAL(triggered()), this, SLOT(branchList())); gitContainer->addAction(command); - m_stashListAction = new QAction(tr("List Stashes"), this); + m_stashListAction = new QAction(tr("Stashes..."), this); command = actionManager->registerAction(m_stashListAction, "Git.StashList", globalcontext); - command->setAttribute(Core::Command::CA_UpdateText); connect(m_stashListAction, SIGNAL(triggered()), this, SLOT(stashList())); gitContainer->addAction(command); m_showAction = new QAction(tr("Show Commit..."), this); command = actionManager->registerAction(m_showAction, "Git.ShowCommit", globalcontext); - command->setAttribute(Core::Command::CA_UpdateText); connect(m_showAction, SIGNAL(triggered()), this, SLOT(showCommit())); gitContainer->addAction(command); + if (0) { + const QList<QAction*> snapShotActions = createSnapShotTestActions(); + const int count = snapShotActions.size(); + for (int i = 0; i < count; i++) { + command = actionManager->registerAction(snapShotActions.at(i), + QLatin1String("Git.Snapshot.") + QString::number(i), + globalcontext); + gitContainer->addAction(command); + } + } + // Submit editor QList<int> submitContext; submitContext.push_back(m_core->uniqueIDManager()->uniqueIdentifier(QLatin1String(Constants::C_GITSUBMITEDITOR))); @@ -648,9 +659,22 @@ void GitPlugin::push() void GitPlugin::stash() { + // Simple stash without prompt, reset repo. + const VCSBase::VCSBasePluginState state = currentState(); + QTC_ASSERT(state.hasTopLevel(), return) + const QString id = m_gitClient->synchronousStash(state.topLevel(), QString(), 0); + if (!id.isEmpty() && m_stashDialog) + m_stashDialog->refresh(state.topLevel(), true); +} + +void GitPlugin::stashSnapshot() +{ + // Prompt for description, restore immediately and keep on working. const VCSBase::VCSBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return) - m_gitClient->stash(state.topLevel()); + const QString id = m_gitClient->synchronousStash(state.topLevel(), QString(), GitClient::StashImmediateRestore|GitClient::StashPromptDescription); + if (!id.isEmpty() && m_stashDialog) + m_stashDialog->refresh(state.topLevel(), true); } void GitPlugin::stashPop() @@ -676,9 +700,15 @@ void GitPlugin::branchList() void GitPlugin::stashList() { - const VCSBase::VCSBasePluginState state = currentState(); - QTC_ASSERT(state.hasTopLevel(), return) - m_gitClient->stashList(state.topLevel()); + // Raise non-modal stash dialog. + if (m_stashDialog) { + m_stashDialog->show(); + m_stashDialog->raise(); + } else { + m_stashDialog = new StashDialog(Core::ICore::instance()->mainWindow()); + m_stashDialog->refresh(currentState().topLevel(), true); + m_stashDialog->show(); + } } void GitPlugin::updateActions(VCSBase::VCSBasePlugin::ActionState as) @@ -716,6 +746,7 @@ void GitPlugin::updateActions(VCSBase::VCSBasePlugin::ActionState as) m_branchListAction->setEnabled(repositoryEnabled); m_stashListAction->setEnabled(repositoryEnabled); m_stashAction->setEnabled(repositoryEnabled); + m_stashSnapshotAction->setEnabled(repositoryEnabled); m_pullAction->setEnabled(repositoryEnabled); m_commitAction->setEnabled(repositoryEnabled); m_stashPopAction->setEnabled(repositoryEnabled); @@ -723,6 +754,9 @@ void GitPlugin::updateActions(VCSBase::VCSBasePlugin::ActionState as) m_undoRepositoryAction->setEnabled(repositoryEnabled); m_pushAction->setEnabled(repositoryEnabled); + if (m_stashDialog) + m_stashDialog->refresh(currentState().topLevel(), false); + // Prompts for repo. m_showAction->setEnabled(true); } diff --git a/src/plugins/git/gitplugin.h b/src/plugins/git/gitplugin.h index 415efef5c3d28aaacef6f0593588c67d49810bc8..30992c32d2a891e8650e68c88b154320f8929a01 100644 --- a/src/plugins/git/gitplugin.h +++ b/src/plugins/git/gitplugin.h @@ -39,6 +39,7 @@ #include <QtCore/QObject> #include <QtCore/QProcess> #include <QtCore/QStringList> +#include <QtCore/QPointer> QT_BEGIN_NAMESPACE class QFile; @@ -64,6 +65,7 @@ class ChangeSelectionDialog; class GitSubmitEditor; struct CommitData; struct GitSettings; +class StashDialog; class GitPlugin : public VCSBase::VCSBasePlugin { @@ -104,6 +106,7 @@ private slots: void showCommit(); void startCommit(); void stash(); + void stashSnapshot(); void stashPop(); void branchList(); void stashList(); @@ -145,6 +148,7 @@ private: QAction *m_undoAction; QAction *m_redoAction; QAction *m_stashAction; + QAction *m_stashSnapshotAction; QAction *m_stashPopAction; QAction *m_stashListAction; QAction *m_branchListAction; @@ -152,11 +156,13 @@ private: GitClient *m_gitClient; ChangeSelectionDialog *m_changeSelectionDialog; + QPointer<StashDialog> m_stashDialog; QString m_submitRepository; QStringList m_submitOrigCommitFiles; QStringList m_submitOrigDeleteFiles; QString m_commitMessageFileName; bool m_submitActionTriggered; + }; } // namespace Git diff --git a/src/plugins/git/gitutils.cpp b/src/plugins/git/gitutils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5c27a64f5b0279977b1cf08e8f9722ca564e9644 --- /dev/null +++ b/src/plugins/git/gitutils.cpp @@ -0,0 +1,104 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#include "gitutils.h" + +#include <QtCore/QDebug> +#include <QtCore/QStringList> +#include <QtGui/QInputDialog> +#include <QtGui/QLineEdit> + +namespace Git { +namespace Internal { + +QDebug operator<<(QDebug d, const Stash &s) +{ + QDebug nospace = d.nospace(); + nospace << "name=" << s.name << " branch=" << s.branch << " message=" << s.message; + return d; +} + +void Stash::clear() +{ + name.clear(); + branch.clear(); + message.clear(); +} + +/* Parse a stash line in its 2 manifestations (with message/without message containing + * <base_sha1>+subject): +\code +stash@{1}: WIP on <branch>: <base_sha1> <subject_base_sha1> +stash@{2}: On <branch>: <message> +\endcode */ + +bool Stash::parseStashLine(const QString &l) +{ + const QChar colon = QLatin1Char(':'); + const int branchPos = l.indexOf(colon); + if (branchPos < 0) + return false; + const int messagePos = l.indexOf(colon, branchPos + 1); + if (messagePos < 0) + return false; + // Name + const QString newName = l.left(branchPos); + // Branch spec + const QString branchSpec = l.mid(branchPos + 1, messagePos - branchPos - 1); + const bool emptyMessage = branchSpec.contains(QLatin1String("WIP")); // "Work in Progress or sth" + const int onIndex = branchSpec.indexOf(QLatin1String("on "), 0, Qt::CaseInsensitive); + if (onIndex == -1) + return false; + const QString newBranch = branchSpec.mid(onIndex + 3); + // Happy! + name = newName; + branch = newBranch; + if (!emptyMessage) + message = l.mid(messagePos + 2); // skip blank + return true; +} + +// Make QInputDialog play nicely, widen it a bit. +bool inputText(QWidget *parent, const QString &title, const QString &prompt, QString *s) +{ + QInputDialog dialog(parent); + dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); + dialog.setWindowTitle(title); + dialog.setLabelText(prompt); + dialog.setTextValue(*s); + // Nasty hack: + if (QLineEdit *le = qFindChild<QLineEdit*>(&dialog)) + le->setMinimumWidth(500); + if (dialog.exec() != QDialog::Accepted) + return false; + *s = dialog.textValue(); + return true; +} +} // namespace Internal +} // namespace Git diff --git a/src/plugins/git/gitutils.h b/src/plugins/git/gitutils.h new file mode 100644 index 0000000000000000000000000000000000000000..16d80ca656c28061e7f6fa2598f1382adca26bc4 --- /dev/null +++ b/src/plugins/git/gitutils.h @@ -0,0 +1,60 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#ifndef GITUTILS_H +#define GITUTILS_H + +#include <QtCore/QString> + +QT_BEGIN_NAMESPACE +class QDebug; +class QWidget; +QT_END_NAMESPACE + +namespace Git { +namespace Internal { + +struct Stash { + void clear(); + bool parseStashLine(const QString &l); + + QString name; + QString branch; + QString message; +}; + +QDebug operator<<(QDebug d, const Stash &); + +// Make QInputDialog play nicely +bool inputText(QWidget *parent, const QString &title, const QString &prompt, QString *s); + +} // namespace Internal +} // namespace Git + +#endif // GITUTILS_H diff --git a/src/plugins/git/gitversioncontrol.cpp b/src/plugins/git/gitversioncontrol.cpp index 76541548ccbbf0949be4f41813265fbb37df51ca..c98f06889217ab8269bc458da274a8d9f9fceb9f 100644 --- a/src/plugins/git/gitversioncontrol.cpp +++ b/src/plugins/git/gitversioncontrol.cpp @@ -30,10 +30,19 @@ #include "gitversioncontrol.h" #include "gitclient.h" #include "gitplugin.h" +#include "gitutils.h" + +static const char stashMessageKeywordC[] = "IVersionControl@"; +static const char stashRevisionIdC[] = "revision"; namespace Git { namespace Internal { +static inline GitClient *gitClient() +{ + return GitPlugin::instance()->gitClient(); +} + GitVersionControl::GitVersionControl(GitClient *client) : m_enabled(true), m_client(client) @@ -54,6 +63,7 @@ bool GitVersionControl::supportsOperation(Operation operation) const case OpenOperation: break; case CreateRepositoryOperation: + case SnapshotOperations: rc = true; break; } @@ -78,7 +88,91 @@ bool GitVersionControl::vcsDelete(const QString & /*fileName*/) bool GitVersionControl::vcsCreateRepository(const QString &directory) { - return GitPlugin::instance()->gitClient()->synchronousInit(directory); + return gitClient()->synchronousInit(directory); +} +/* Snapshots are implement using stashes, relying on stash messages for + * naming as the actual stash names (stash{n}) are rotated as one adds stashes. + * Note that the snapshot interface does not care whether we have an unmodified + * repository state, in which case git refuses to stash. + * In that case, return a special identifier as "specialprefix:<branch>:<head revision>", + * which will trigger a checkout in restore(). */ + +QString GitVersionControl::vcsCreateSnapshot(const QString &topLevel) +{ + bool repositoryUnchanged; + // Create unique keyword + static int n = 1; + QString keyword = QLatin1String(stashMessageKeywordC) + QString::number(n++); + const QString stashMessage = + gitClient()->synchronousStash(topLevel, keyword, + GitClient::StashImmediateRestore|GitClient::StashIgnoreUnchanged, + &repositoryUnchanged); + if (!stashMessage.isEmpty()) + return stashMessage; + if (repositoryUnchanged) { + // For unchanged repository state: return identifier + top revision + QString topRevision; + QString branch; + if (!gitClient()->synchronousTopRevision(topLevel, &topRevision, &branch)) + return QString(); + const QChar colon = QLatin1Char(':'); + QString id = QLatin1String(stashRevisionIdC); + id += colon; + id += branch; + id += colon; + id += topRevision; + return id; + } + return QString(); // Failure +} + +QStringList GitVersionControl::vcsSnapshots(const QString &topLevel) +{ + QList<Stash> stashes; + if (!gitClient()->synchronousStashList(topLevel, &stashes)) + return QStringList(); + // Return the git stash 'message' as identifier, ignoring empty ones + QStringList rc; + foreach(const Stash &s, stashes) + if (!s.message.isEmpty()) + rc.push_back(s.message); + return rc; +} + +bool GitVersionControl::vcsRestoreSnapshot(const QString &topLevel, const QString &name) +{ + bool success = false; + do { + // Is this a revision or a stash + if (name.startsWith(QLatin1String(stashRevisionIdC))) { + // Restore "id:branch:revision" + const QStringList tokens = name.split(QLatin1Char(':')); + if (tokens.size() != 3) + break; + const QString branch = tokens.at(1); + const QString revision = tokens.at(2); + success = gitClient()->synchronousReset(topLevel) + && gitClient()->synchronousCheckoutBranch(topLevel, branch) + && gitClient()->synchronousCheckoutFiles(topLevel, QStringList(), revision); + } else { + // Restore stash if it can be resolved. + QString stashName; + success = gitClient()->stashNameFromMessage(topLevel, name, &stashName) + && gitClient()->synchronousReset(topLevel) + && gitClient()->synchronousStashRestore(topLevel, stashName); + } + } while (false); + return success; +} + +bool GitVersionControl::vcsRemoveSnapshot(const QString &topLevel, const QString &name) +{ + // Is this a revision -> happy + if (name.startsWith(QLatin1String(stashRevisionIdC))) + return true; + QString stashName; + return gitClient()->stashNameFromMessage(topLevel, name, &stashName) + && gitClient()->synchronousStashRemove(topLevel, stashName); } bool GitVersionControl::managesDirectory(const QString &directory) const @@ -96,5 +190,10 @@ void GitVersionControl::emitFilesChanged(const QStringList &l) emit filesChanged(l); } +void GitVersionControl::emitRepositoryChanged(const QString &r) +{ + emit repositoryChanged(r); +} + } // Internal } // Git diff --git a/src/plugins/git/gitversioncontrol.h b/src/plugins/git/gitversioncontrol.h index e5013cce6e09a2116ff4c2245567babae6511e16..88927fdf14f312f290c2b2a17e40c02f19110b5f 100644 --- a/src/plugins/git/gitversioncontrol.h +++ b/src/plugins/git/gitversioncontrol.h @@ -54,11 +54,13 @@ public: virtual bool vcsAdd(const QString &fileName); virtual bool vcsDelete(const QString &filename); virtual bool vcsCreateRepository(const QString &directory); + virtual QString vcsCreateSnapshot(const QString &topLevel); + virtual QStringList vcsSnapshots(const QString &topLevel); + virtual bool vcsRestoreSnapshot(const QString &topLevel, const QString &name); + virtual bool vcsRemoveSnapshot(const QString &topLevel, const QString &name); void emitFilesChanged(const QStringList &); - -signals: - void enabledChanged(bool); + void emitRepositoryChanged(const QString &); private: bool m_enabled; diff --git a/src/plugins/git/stashdialog.cpp b/src/plugins/git/stashdialog.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a39559ad597b964fbf7882a1e5ad289492b986b2 --- /dev/null +++ b/src/plugins/git/stashdialog.cpp @@ -0,0 +1,411 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#include "stashdialog.h" +#include "gitclient.h" +#include "gitplugin.h" +#include "gitutils.h" +#include "ui_stashdialog.h" + +#include <utils/qtcassert.h> +#include <vcsbase/vcsbaseoutputwindow.h> + +#include <QtCore/QDebug> +#include <QtCore/QModelIndex> +#include <QtCore/QDateTime> +#include <QtGui/QStandardItemModel> +#include <QtGui/QSortFilterProxyModel> +#include <QtGui/QItemSelectionModel> +#include <QtGui/QMessageBox> +#include <QtGui/QPushButton> + +enum { NameColumn, BranchColumn, MessageColumn, ColumnCount }; + +namespace Git { +namespace Internal { + +static inline GitClient *gitClient() +{ + return GitPlugin::instance()->gitClient(); +} + +static inline QList<QStandardItem*> stashModelRowItems(const Stash &s) +{ + Qt::ItemFlags itemFlags = Qt::ItemIsSelectable|Qt::ItemIsEnabled; + QStandardItem *nameItem = new QStandardItem(s.name); + nameItem->setFlags(itemFlags); + QStandardItem *branchItem = new QStandardItem(s.branch); + branchItem->setFlags(itemFlags); + QStandardItem *messageItem = new QStandardItem(s.message); + messageItem->setFlags(itemFlags); + QList<QStandardItem*> rc; + rc << nameItem << branchItem << messageItem; + return rc; +} + +// ----------- StashModel +class StashModel : public QStandardItemModel { +public: + explicit StashModel(QObject *parent = 0); + + void setStashes(const QList<Stash> &stashes); + const Stash &at(int i) { return m_stashes.at(i); } + +private: + QList<Stash> m_stashes; +}; + +StashModel::StashModel(QObject *parent) : + QStandardItemModel(0, ColumnCount, parent) +{ + QStringList headers; + headers << StashDialog::tr("Name") << StashDialog::tr("Branch") << StashDialog::tr("Message"); + setHorizontalHeaderLabels(headers); +} + +void StashModel::setStashes(const QList<Stash> &stashes) +{ + m_stashes = stashes; + if (const int rows = rowCount()) + removeRows(0, rows); + foreach(const Stash &s, stashes) + appendRow(stashModelRowItems(s)); +} + +// ---------- StashDialog +StashDialog::StashDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::StashDialog), + m_model(new StashModel), + m_proxyModel(new QSortFilterProxyModel), + m_deleteAllButton(new QPushButton(tr("Delete all..."))), + m_deleteSelectionButton(new QPushButton(tr("Delete..."))), + m_showCurrentButton(new QPushButton(tr("Show"))), + m_restoreCurrentButton(new QPushButton(tr("Restore..."))), + m_restoreCurrentInBranchButton(new QPushButton(tr("Restore to branch..."))), + m_refreshButton(new QPushButton(tr("Refresh"))) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + ui->setupUi(this); + // Buttons + ui->buttonBox->addButton(m_showCurrentButton, QDialogButtonBox::ActionRole); + connect(m_showCurrentButton, SIGNAL(clicked()), this, SLOT(showCurrent())); + ui->buttonBox->addButton(m_refreshButton, QDialogButtonBox::ActionRole); + connect(m_refreshButton, SIGNAL(clicked()), this, SLOT(forceRefresh())); + ui->buttonBox->addButton(m_restoreCurrentButton, QDialogButtonBox::ActionRole); + connect(m_restoreCurrentButton, SIGNAL(clicked()), this, SLOT(restoreCurrent())); + ui->buttonBox->addButton(m_restoreCurrentInBranchButton, QDialogButtonBox::ActionRole); + connect(m_restoreCurrentInBranchButton, SIGNAL(clicked()), this, SLOT(restoreCurrentInBranch())); + ui->buttonBox->addButton(m_deleteSelectionButton, QDialogButtonBox::ActionRole); + connect(m_deleteSelectionButton, SIGNAL(clicked()), this, SLOT(deleteSelection())); + ui->buttonBox->addButton(m_deleteAllButton, QDialogButtonBox::ActionRole); + connect(m_deleteAllButton, SIGNAL(clicked()), this, SLOT(deleteAll())); + // Models + m_proxyModel->setSourceModel(m_model); + m_proxyModel->setFilterKeyColumn(-1); + m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + ui->stashView->setModel(m_proxyModel); + ui->stashView->setSelectionMode(QAbstractItemView::MultiSelection); + connect(ui->filterLineEdit, SIGNAL(filterChanged(QString)), m_proxyModel, SLOT(setFilterFixedString(QString))); + connect(ui->stashView->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), + this, SLOT(enableButtons())); + connect(ui->stashView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + this, SLOT(enableButtons())); + connect(ui->stashView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(showCurrent())); + ui->stashView->setFocus(); +} + +StashDialog::~StashDialog() +{ + delete ui; +} + +void StashDialog::changeEvent(QEvent *e) +{ + QDialog::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +void StashDialog::refresh(const QString &repository, bool force) +{ + if (m_repository == repository && !force) + return; + // Refresh + m_repository = repository; + if (m_repository.isEmpty()) { + ui->repositoryLabel->setText(tr("<No repository>")); + m_model->setStashes(QList<Stash>()); + } else { + ui->repositoryLabel->setText(tr("Repository: %1").arg(repository)); + QList<Stash> stashes; + gitClient()->synchronousStashList(m_repository, &stashes); + m_model->setStashes(stashes); + if (!stashes.isEmpty()) { + for(int c = 0; c < ColumnCount; c++) + ui->stashView->resizeColumnToContents(c); + } + } + enableButtons(); +} + +void StashDialog::deleteAll() +{ + const QString title = tr("Delete stashes"); + if (!ask(title, tr("Do you want to delete all stashes?"))) + return; + QString errorMessage; + if (gitClient()->synchronousStashRemove(m_repository, QString(), &errorMessage)) { + refresh(m_repository, true); + } else { + warning(title, errorMessage); + } +} + +void StashDialog::deleteSelection() +{ + const QList<int> rows = selectedRows(); + QTC_ASSERT(!rows.isEmpty(), return) + const QString title = tr("Delete stashes"); + if (!ask(title, tr("Do you want to delete %n stash(es)?", 0, rows.size()))) + return; + QString errorMessage; + QStringList errors; + // Delete in reverse order as stashes rotate + for (int r = rows.size() - 1; r >= 0; r--) + if (!gitClient()->synchronousStashRemove(m_repository, m_model->at(r).name, &errorMessage)) + errors.push_back(errorMessage); + refresh(m_repository, true); + if (!errors.isEmpty()) + warning(title, errors.join(QString(QLatin1Char('\n')))); +} + +void StashDialog::showCurrent() +{ + const int index = currentRow(); + QTC_ASSERT(index >= 0, return) + gitClient()->show(m_repository, m_model->at(index).name); +} + +// Suggest Branch name to restore 'stash@{0}' -> 'stash0-date' +static inline QString stashRestoreDefaultBranch(QString stash) +{ + stash.remove(QLatin1Char('{')); + stash.remove(QLatin1Char('}')); + stash.remove(QLatin1Char('@')); + stash += QLatin1Char('-'); + stash += QDateTime::currentDateTime().toString(QLatin1String("yyMMddhhmmss")); + return stash; +} + +// Return next stash id 'stash@{0}' -> 'stash@{1}' +static inline QString nextStash(const QString &stash) +{ + const int openingBracePos = stash.indexOf(QLatin1Char('{')); + if (openingBracePos == -1) + return QString(); + const int closingBracePos = stash.indexOf(QLatin1Char('}'), openingBracePos + 2); + if (closingBracePos == -1) + return QString(); + bool ok; + const int n = stash.mid(openingBracePos + 1, closingBracePos - openingBracePos - 1).toInt(&ok); + if (!ok) + return QString(); + QString rc = stash.left(openingBracePos + 1); + rc += QString::number(n + 1); + rc += QLatin1Char('}'); + return rc; +} + +StashDialog::ModifiedRepositoryAction StashDialog::promptModifiedRepository(const QString &stash) +{ + QMessageBox box(QMessageBox::Question, + tr("Repository modified"), + tr("%1 cannot be restored since the repository is modified.\n" + "You can choose between stashing the changes or discarding them.").arg(stash), + QMessageBox::Cancel, this); + QPushButton *stashButton = box.addButton(tr("Stash"), QMessageBox::AcceptRole); + QPushButton *discardButton = box.addButton(tr("Discard"), QMessageBox::AcceptRole); + box.exec(); + const QAbstractButton *clickedButton = box.clickedButton(); + if (clickedButton == stashButton) + return ModifiedRepositoryStash; + if (clickedButton == discardButton) + return ModifiedRepositoryDiscard; + return ModifiedRepositoryCancel; +} + +// Prompt for restore: Make sure repository is unmodified, +// prompt for a branch if desired or just ask to restore. +// Note that the stash to be restored changes if the user +// chooses to stash away modified repository. +bool StashDialog::promptForRestore(QString *stash, + QString *branch /* = 0*/, + QString *errorMessage) +{ + const QString stashIn = *stash; + bool modifiedPromptShown = false; + switch (gitClient()->gitStatus(m_repository, false, 0, errorMessage)) { + case GitClient::StatusFailed: + return false; + case GitClient::StatusChanged: { + switch (promptModifiedRepository(*stash)) { + case ModifiedRepositoryCancel: + return false; + case ModifiedRepositoryStash: + if (gitClient()->synchronousStash(m_repository, QString(), GitClient::StashPromptDescription).isEmpty()) + return false; + *stash = nextStash(*stash); // Our stash id to be restored changed + QTC_ASSERT(!stash->isEmpty(), return false) + break; + case ModifiedRepositoryDiscard: + if (!gitClient()->synchronousReset(m_repository)) + return false; + break; + } + modifiedPromptShown = true; + } + break; + case GitClient::StatusUnchanged: + break; + } + // Prompt for branch or just ask. + if (branch) { + *branch = stashRestoreDefaultBranch(*stash); + if (!inputText(this, tr("Restore Stash to Branch"), tr("Branch:"), branch) + || branch->isEmpty()) + return false; + } else { + if (!modifiedPromptShown && !ask(tr("Stash Restore"), tr("Would you like to restore %1?").arg(stashIn))) + return false; + } + return true; +} + +static inline QString msgRestoreFailedTitle(const QString &stash) +{ + return StashDialog::tr("Error restoring %1").arg(stash); +} + +void StashDialog::restoreCurrent() +{ + const int index = currentRow(); + QTC_ASSERT(index >= 0, return) + QString errorMessage; + QString name = m_model->at(index).name; + // Make sure repository is not modified, restore. The command will + // output to window on success. + const bool success = promptForRestore(&name, 0, &errorMessage) + && gitClient()->synchronousStashRestore(m_repository, name, QString(), &errorMessage); + if (success) { + refresh(m_repository, true); // Might have stashed away local changes. + } else { + if (!errorMessage.isEmpty()) + warning(msgRestoreFailedTitle(name), errorMessage); + } +} + +void StashDialog::restoreCurrentInBranch() +{ + const int index = currentRow(); + QTC_ASSERT(index >= 0, return) + QString errorMessage; + QString branch; + QString name = m_model->at(index).name; + const bool success = promptForRestore(&name, &branch, &errorMessage) + && gitClient()->synchronousStashRestore(m_repository, name, branch, &errorMessage); + if (success) { + refresh(m_repository, true); // git deletes the stash, unfortunately. + } else { + if (!errorMessage.isEmpty()) + warning(msgRestoreFailedTitle(name), errorMessage); + } +} + +int StashDialog::currentRow() const +{ + const QModelIndex proxyIndex = ui->stashView->currentIndex(); + if (proxyIndex.isValid()) { + const QModelIndex index = m_proxyModel->mapToSource(proxyIndex); + if (index.isValid()) + return index.row(); + } + return -1; +} + +QList<int> StashDialog::selectedRows() const +{ + QList<int> rc; + foreach(const QModelIndex &proxyIndex, ui->stashView->selectionModel()->selectedRows()) { + const QModelIndex index = m_proxyModel->mapToSource(proxyIndex); + if (index.isValid()) + rc.push_back(index.row()); + } + qSort(rc); + return rc; +} + +void StashDialog::forceRefresh() +{ + refresh(m_repository, true); +} + +void StashDialog::enableButtons() +{ + const bool hasStashes = m_model->rowCount(); + const bool hasCurrentRow = hasStashes && currentRow() >= 0; + m_deleteAllButton->setEnabled(hasStashes); + m_showCurrentButton->setEnabled(hasCurrentRow); + m_restoreCurrentButton->setEnabled(hasCurrentRow); + m_restoreCurrentInBranchButton->setEnabled(hasCurrentRow); + const bool hasSelection = !ui->stashView->selectionModel()->selectedRows().isEmpty(); + m_deleteSelectionButton->setEnabled(hasSelection); +} + +void StashDialog::warning(const QString &title, const QString &what, const QString &details) +{ + QMessageBox msgBox(QMessageBox::Warning, title, what, QMessageBox::Ok, this); + if (!details.isEmpty()) + msgBox.setDetailedText(details); + msgBox.exec(); +} + +bool StashDialog::ask(const QString &title, const QString &what, bool defaultButton) +{ + return QMessageBox::question(this, title, what, QMessageBox::Yes|QMessageBox::No, + defaultButton ? QMessageBox::Yes : QMessageBox::No) == QMessageBox::Yes; +} + +} // namespace Internal +} // namespace Git diff --git a/src/plugins/git/stashdialog.h b/src/plugins/git/stashdialog.h new file mode 100644 index 0000000000000000000000000000000000000000..377fef41e88be469d59d43412359ae01fae1386d --- /dev/null +++ b/src/plugins/git/stashdialog.h @@ -0,0 +1,105 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** GNU Lesser General Public License Usage +** +** Alternatively, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#ifndef STASHDIALOG_H +#define STASHDIALOG_H + +#include <QtGui/QDialog> + +QT_BEGIN_NAMESPACE +class QSortFilterProxyModel; +class QPushButton; +class QModelIndex; +QT_END_NAMESPACE + +namespace Git { +namespace Internal { + +namespace Ui { + class StashDialog; +} +class StashModel; + +/* StashDialog: Non-modal dialog that manages the list of stashes + * of the repository. Offers to show, restore, restore to branch + * (in case restore fails due to conflicts) on current and + * delete on selection/all. */ + +class StashDialog : public QDialog { + Q_OBJECT +public: + explicit StashDialog(QWidget *parent = 0); + ~StashDialog(); + +public slots: + void refresh(const QString &repository, bool force); + +protected: + void changeEvent(QEvent *e); + +private slots: + void deleteAll(); + void deleteSelection(); + void showCurrent(); + void restoreCurrent(); + void restoreCurrentInBranch(); + void enableButtons(); + void forceRefresh(); + +private: + // Prompt dialog for modified repositories. Ask to undo or stash away. + enum ModifiedRepositoryAction { + ModifiedRepositoryCancel, + ModifiedRepositoryStash, + ModifiedRepositoryDiscard + }; + + ModifiedRepositoryAction promptModifiedRepository(const QString &stash); + bool promptForRestore(QString *stash, QString *branch /* = 0 */, QString *errorMessage); + bool ask(const QString &title, const QString &what, bool defaultButton = true); + void warning(const QString &title, const QString &what, const QString &details = QString()); + int currentRow() const; + QList<int> selectedRows() const; \ + + Ui::StashDialog *ui; + StashModel *m_model; + QSortFilterProxyModel *m_proxyModel; + QPushButton *m_deleteAllButton; + QPushButton *m_deleteSelectionButton; + QPushButton *m_showCurrentButton; + QPushButton *m_restoreCurrentButton; + QPushButton *m_restoreCurrentInBranchButton; + QPushButton *m_refreshButton; + QString m_repository; +}; + +} // namespace Internal +} // namespace Git + +#endif // STASHDIALOG_H diff --git a/src/plugins/git/stashdialog.ui b/src/plugins/git/stashdialog.ui new file mode 100644 index 0000000000000000000000000000000000000000..323175570108e9b474697b6a20847c1b70a2a402 --- /dev/null +++ b/src/plugins/git/stashdialog.ui @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Git::Internal::StashDialog</class> + <widget class="QDialog" name="Git::Internal::StashDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>599</width> + <height>485</height> + </rect> + </property> + <property name="windowTitle"> + <string>Stashes</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="repositoryLabel"> + <property name="text"> + <string notr="true">Repository: Dummy</string> + </property> + </widget> + </item> + <item> + <widget class="Utils::FilterLineEdit" name="filterLineEdit"/> + </item> + <item> + <widget class="QTreeView" name="stashView"/> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>Utils::FancyLineEdit</class> + <extends>QLineEdit</extends> + <header location="global">utils/fancylineedit.h</header> + </customwidget> + <customwidget> + <class>Utils::FilterLineEdit</class> + <extends>Utils::FancyLineEdit</extends> + <header location="global">utils/filterlineedit.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>Git::Internal::StashDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>Git::Internal::StashDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/plugins/mercurial/mercurialcontrol.cpp b/src/plugins/mercurial/mercurialcontrol.cpp index 3ca94573be47988b2b4a63f014bc0ec5680d5530..a0af2093104df5a5e7bb0c2cdcc6ba9ae0cabc19 100644 --- a/src/plugins/mercurial/mercurialcontrol.cpp +++ b/src/plugins/mercurial/mercurialcontrol.cpp @@ -68,6 +68,7 @@ bool MercurialControl::supportsOperation(Operation operation) const case Core::IVersionControl::CreateRepositoryOperation: break; case Core::IVersionControl::OpenOperation: + case Core::IVersionControl::SnapshotOperations: supported = false; break; } @@ -97,6 +98,26 @@ bool MercurialControl::vcsCreateRepository(const QString &directory) return mercurialClient->createRepositorySync(directory); } +QString MercurialControl::vcsCreateSnapshot(const QString &) +{ + return QString(); +} + +QStringList MercurialControl::vcsSnapshots(const QString &) +{ + return QStringList(); +} + +bool MercurialControl::vcsRestoreSnapshot(const QString &, const QString &) +{ + return false; +} + +bool MercurialControl::vcsRemoveSnapshot(const QString &, const QString &) +{ + return false; +} + bool MercurialControl::sccManaged(const QString &filename) { const QFileInfo fi(filename); diff --git a/src/plugins/mercurial/mercurialcontrol.h b/src/plugins/mercurial/mercurialcontrol.h index d447a5bc1643c0651be07f23b4c1f4828ec4d296..c82401bad89804cc4baa28b6f5851d3e1da83b16 100644 --- a/src/plugins/mercurial/mercurialcontrol.h +++ b/src/plugins/mercurial/mercurialcontrol.h @@ -57,6 +57,10 @@ public: bool vcsAdd(const QString &filename); bool vcsDelete(const QString &filename); bool vcsCreateRepository(const QString &directory); + QString vcsCreateSnapshot(const QString &topLevel); + QStringList vcsSnapshots(const QString &topLevel); + bool vcsRestoreSnapshot(const QString &topLevel, const QString &name); + bool vcsRemoveSnapshot(const QString &topLevel, const QString &name); bool sccManaged(const QString &filename); public slots: diff --git a/src/plugins/perforce/perforceversioncontrol.cpp b/src/plugins/perforce/perforceversioncontrol.cpp index 0be2ef59019e472ef078c7b5f151e1ff37f7a16a..7afd5815fea5479e4660be11f46700645159bcea 100644 --- a/src/plugins/perforce/perforceversioncontrol.cpp +++ b/src/plugins/perforce/perforceversioncontrol.cpp @@ -56,6 +56,7 @@ bool PerforceVersionControl::supportsOperation(Operation operation) const case OpenOperation: return true; case CreateRepositoryOperation: + case SnapshotOperations: break; } return false; @@ -84,6 +85,26 @@ bool PerforceVersionControl::vcsCreateRepository(const QString &) return false; } +QString PerforceVersionControl::vcsCreateSnapshot(const QString &) +{ + return QString(); +} + +QStringList PerforceVersionControl::vcsSnapshots(const QString &) +{ + return QStringList(); +} + +bool PerforceVersionControl::vcsRestoreSnapshot(const QString &, const QString &) +{ + return false; +} + +bool PerforceVersionControl::vcsRemoveSnapshot(const QString &, const QString &) +{ + return false; +} + bool PerforceVersionControl::managesDirectory(const QString &directory) const { const bool rc = m_plugin->managesDirectory(directory); diff --git a/src/plugins/perforce/perforceversioncontrol.h b/src/plugins/perforce/perforceversioncontrol.h index 815b743053976815c1d50c64032aa983b196e0a2..ee31794c7c5dfa190e864f4db62f313c616ce034 100644 --- a/src/plugins/perforce/perforceversioncontrol.h +++ b/src/plugins/perforce/perforceversioncontrol.h @@ -53,6 +53,10 @@ public: virtual bool vcsAdd(const QString &fileName); virtual bool vcsDelete(const QString &filename); virtual bool vcsCreateRepository(const QString &directory); + virtual QString vcsCreateSnapshot(const QString &topLevel); + virtual QStringList vcsSnapshots(const QString &topLevel); + virtual bool vcsRestoreSnapshot(const QString &topLevel, const QString &name); + virtual bool vcsRemoveSnapshot(const QString &topLevel, const QString &name); void emitRepositoryChanged(const QString &s); void emitFilesChanged(const QStringList &l); diff --git a/src/plugins/subversion/subversioncontrol.cpp b/src/plugins/subversion/subversioncontrol.cpp index f8b795e5f48a508b34e42b7bc5dfb23a62bd85a8..99cbdc6883e41e8e21d948001d3a79938616a383 100644 --- a/src/plugins/subversion/subversioncontrol.cpp +++ b/src/plugins/subversion/subversioncontrol.cpp @@ -55,6 +55,7 @@ bool SubversionControl::supportsOperation(Operation operation) const break; case OpenOperation: case CreateRepositoryOperation: + case SnapshotOperations: rc = false; break; } @@ -84,6 +85,26 @@ bool SubversionControl::vcsCreateRepository(const QString &) return false; } +QString SubversionControl::vcsCreateSnapshot(const QString &) +{ + return QString(); +} + +QStringList SubversionControl::vcsSnapshots(const QString &) +{ + return QStringList(); +} + +bool SubversionControl::vcsRestoreSnapshot(const QString &, const QString &) +{ + return false; +} + +bool SubversionControl::vcsRemoveSnapshot(const QString &, const QString &) +{ + return false; +} + bool SubversionControl::managesDirectory(const QString &directory) const { return m_plugin->managesDirectory(directory); diff --git a/src/plugins/subversion/subversioncontrol.h b/src/plugins/subversion/subversioncontrol.h index 5aa60373acee78d72f5997768cef6553ea0aa2ed..7a3aec8c83f489dfa42bf5b4dce9f494e471671c 100644 --- a/src/plugins/subversion/subversioncontrol.h +++ b/src/plugins/subversion/subversioncontrol.h @@ -54,6 +54,11 @@ public: virtual bool vcsDelete(const QString &filename); virtual bool vcsCreateRepository(const QString &directory); + virtual QString vcsCreateSnapshot(const QString &topLevel); + virtual QStringList vcsSnapshots(const QString &topLevel); + virtual bool vcsRestoreSnapshot(const QString &topLevel, const QString &name); + virtual bool vcsRemoveSnapshot(const QString &topLevel, const QString &name); + void emitRepositoryChanged(const QString &); void emitFilesChanged(const QStringList &); diff --git a/src/plugins/vcsbase/vcsbaseplugin.cpp b/src/plugins/vcsbase/vcsbaseplugin.cpp index 0cb54a70087d6cc09871ae4e066661c650774ede..ea701e6a22062ae01e3c5b9c1ae0c7d5b8690578 100644 --- a/src/plugins/vcsbase/vcsbaseplugin.cpp +++ b/src/plugins/vcsbase/vcsbaseplugin.cpp @@ -30,6 +30,7 @@ #include "vcsbaseplugin.h" #include "vcsbasesubmiteditor.h" #include "vcsplugin.h" +#include "vcsbaseoutputwindow.h" #include "corelistener.h" #include <coreplugin/icore.h> @@ -353,6 +354,11 @@ struct VCSBasePluginPrivate { Core::IVersionControl *m_versionControl; VCSBasePluginState m_state; int m_actionState; + QAction *m_testSnapshotAction; + QAction *m_testListSnapshotsAction; + QAction *m_testRestoreSnapshotAction; + QAction *m_testRemoveSnapshotAction; + QString m_testLastSnapshot; static Internal::StateListener *m_listener; }; @@ -360,7 +366,11 @@ struct VCSBasePluginPrivate { VCSBasePluginPrivate::VCSBasePluginPrivate(const QString &submitEditorId) : m_submitEditorId(submitEditorId), m_versionControl(0), - m_actionState(-1) + m_actionState(-1), + m_testSnapshotAction(0), + m_testListSnapshotsAction(0), + m_testRestoreSnapshotAction(0), + m_testRemoveSnapshotAction(0) { } @@ -507,6 +517,62 @@ void VCSBasePlugin::createRepository() } } +// For internal tests: Create actions driving IVersionControl's snapshot interface. +QList<QAction*> VCSBasePlugin::createSnapShotTestActions() +{ + if (!d->m_testSnapshotAction) { + d->m_testSnapshotAction = new QAction(QLatin1String("Take snapshot"), this); + connect(d->m_testSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestSnapshot())); + d->m_testListSnapshotsAction = new QAction(QLatin1String("List snapshots"), this); + connect(d->m_testListSnapshotsAction, SIGNAL(triggered()), this, SLOT(slotTestListSnapshots())); + d->m_testRestoreSnapshotAction = new QAction(QLatin1String("Restore snapshot"), this); + connect(d->m_testRestoreSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestRestoreSnapshot())); + d->m_testRemoveSnapshotAction = new QAction(QLatin1String("Remove snapshot"), this); + connect(d->m_testRemoveSnapshotAction, SIGNAL(triggered()), this, SLOT(slotTestRemoveSnapshot())); + } + QList<QAction*> rc; + rc << d->m_testSnapshotAction << d->m_testListSnapshotsAction << d->m_testRestoreSnapshotAction + << d->m_testRemoveSnapshotAction; + return rc; +} + +void VCSBasePlugin::slotTestSnapshot() +{ + QTC_ASSERT(currentState().hasTopLevel(), return) + d->m_testLastSnapshot = versionControl()->vcsCreateSnapshot(currentState().topLevel()); + qDebug() << "Snapshot " << d->m_testLastSnapshot; + VCSBaseOutputWindow::instance()->append(QLatin1String("Snapshot: ") + d->m_testLastSnapshot); + if (!d->m_testLastSnapshot.isEmpty()) + d->m_testRestoreSnapshotAction->setText(QLatin1String("Restore snapshot ") + d->m_testLastSnapshot); +} + +void VCSBasePlugin::slotTestListSnapshots() +{ + QTC_ASSERT(currentState().hasTopLevel(), return) + const QStringList snapshots = versionControl()->vcsSnapshots(currentState().topLevel()); + qDebug() << "Snapshots " << snapshots; + VCSBaseOutputWindow::instance()->append(QLatin1String("Snapshots: ") + snapshots.join(QLatin1String(", "))); +} + +void VCSBasePlugin::slotTestRestoreSnapshot() +{ + QTC_ASSERT(currentState().hasTopLevel() && !d->m_testLastSnapshot.isEmpty(), return) + const bool ok = versionControl()->vcsRestoreSnapshot(currentState().topLevel(), d->m_testLastSnapshot); + const QString msg = d->m_testLastSnapshot+ (ok ? QLatin1String(" restored") : QLatin1String(" failed")); + qDebug() << msg; + VCSBaseOutputWindow::instance()->append(msg); +} + +void VCSBasePlugin::slotTestRemoveSnapshot() +{ + QTC_ASSERT(currentState().hasTopLevel() && !d->m_testLastSnapshot.isEmpty(), return) + const bool ok = versionControl()->vcsRemoveSnapshot(currentState().topLevel(), d->m_testLastSnapshot); + const QString msg = d->m_testLastSnapshot+ (ok ? QLatin1String(" removed") : QLatin1String(" failed")); + qDebug() << msg; + VCSBaseOutputWindow::instance()->append(msg); + d->m_testLastSnapshot.clear(); +} + } // namespace VCSBase #include "vcsbaseplugin.moc" diff --git a/src/plugins/vcsbase/vcsbaseplugin.h b/src/plugins/vcsbase/vcsbaseplugin.h index a9351ed47039409cdfa888519b7d0dd4217b900e..4f4a6393dff87e668f1d6c85f436c2bab2c30634 100644 --- a/src/plugins/vcsbase/vcsbaseplugin.h +++ b/src/plugins/vcsbase/vcsbaseplugin.h @@ -34,7 +34,8 @@ #include <extensionsystem/iplugin.h> -#include <QSharedDataPointer> +#include <QtCore/QSharedDataPointer> +#include <QtCore/QList> QT_BEGIN_NAMESPACE class QAction; @@ -149,6 +150,9 @@ public: const VCSBasePluginState ¤tState() const; Core::IVersionControl *versionControl() const; + // For internal tests: Create actions driving IVersionControl's snapshot interface. + QList<QAction*> createSnapShotTestActions(); + public slots: // Convenience slot for "Delete current file" action. Prompts to // delete the file via VCSManager. @@ -175,6 +179,10 @@ protected: private slots: void slotSubmitEditorAboutToClose(VCSBaseSubmitEditor *submitEditor, bool *result); void slotStateChanged(const VCSBase::Internal::State &s, Core::IVersionControl *vc); + void slotTestSnapshot(); + void slotTestListSnapshots(); + void slotTestRestoreSnapshot(); + void slotTestRemoveSnapshot(); private: VCSBasePluginPrivate *d; diff --git a/src/shared/qscripthighlighter/qscriptindenter.cpp b/src/shared/qscripthighlighter/qscriptindenter.cpp index c697e16c69b07524c49313f6fb44a56fb6ae04d3..0abaf574dc92e0c9cd0cd61f0174db29d40f7d73 100644 --- a/src/shared/qscripthighlighter/qscriptindenter.cpp +++ b/src/shared/qscripthighlighter/qscriptindenter.cpp @@ -222,26 +222,9 @@ QString QScriptIndenter::trimmedCodeLine(const QString &t) trimmed.append(QLatin1Char('X')); } else if (token.is(QScriptIncrementalScanner::Token::Comment)) { - int i = 0; - int e = token.length; - - if (token.offset > 0 || startState == 0) { - trimmed.append(QLatin1String("/*")); - i += 2; - } - - bool needEndOfComment = false; - if (e > 2 && token.end() == t.length() && scanner.endState() != 0) { - needEndOfComment = true; - e -= 2; - } - - for (; i < e; ++i) + for (int i = 0; i < token.length; ++i) trimmed.append(QLatin1Char(' ')); - if (needEndOfComment) - trimmed.append(QLatin1String("*/")); - } else { trimmed.append(t.midRef(token.offset, token.length)); } @@ -355,39 +338,6 @@ bool QScriptIndenter::readLine() yyLinizerState.line = trimmedCodeLine(yyLinizerState.line); - /* - Remove C-style comments that span multiple lines. If the - bottom line starts in a C-style comment, we are not aware - of that and eventually yyLine will contain a slash-aster. - - Notice that both if's can be executed, since - yyLinizerState.inCComment is potentially set to false in - the first if. The order of the if's is also important. - */ - - if (yyLinizerState.inComment) { - const QLatin1String slashAster("/*"); - - k = yyLinizerState.line.indexOf(slashAster); - if (k == -1) { - yyLinizerState.line.clear(); - } else { - yyLinizerState.line.truncate(k); - yyLinizerState.inComment = false; - } - } - - if (!yyLinizerState.inComment) { - const QLatin1String asterSlash("*/"); - - k = yyLinizerState.line.indexOf(asterSlash); - if (k != -1) { - for (int i = 0; i < k + 2; i++) - eraseChar(yyLinizerState.line, i, QLatin1Char(' ')); - yyLinizerState.inComment = true; - } - } - /* Remove preprocessor directives. */ @@ -447,7 +397,6 @@ bool QScriptIndenter::readLine() void QScriptIndenter::startLinizer() { yyLinizerState.braceDepth = 0; - yyLinizerState.inComment = false; yyLinizerState.pendingRightBrace = false; yyLine = &yyLinizerState.line; @@ -497,30 +446,7 @@ int QScriptIndenter::indentWhenBottomLineStartsInMultiLineComment() break; } - const QString codeLine = trimmedCodeLine(blockText); - - int k = codeLine.lastIndexOf(QLatin1String("/*")); - if (k == -1) { - /* - We found a normal text line in a comment. Align the - bottom line with the text on this line. - */ - return indentOfLine(codeLine); - } else { - /* - The C-style comment starts on this line. If there is - text on the same line, align with it. Otherwise, align - with the slash-aster plus a given offset. - */ - int indent = columnForIndex(codeLine, k); - k += 2; - while (k < yyLine->length()) { - if (!codeLine.at(k).isSpace()) - return columnForIndex(codeLine, k); - k++; - } - return indent + ppCommentOffset; - } + return indentOfLine(blockText); } /* diff --git a/src/shared/qscripthighlighter/qscriptindenter.h b/src/shared/qscripthighlighter/qscriptindenter.h index 114e5c23f89934a1d32b8b14a2b6a3c4074c12ad..6bb0296c5a4f7b6d3a86d36d7319607157735cdc 100644 --- a/src/shared/qscripthighlighter/qscriptindenter.h +++ b/src/shared/qscripthighlighter/qscriptindenter.h @@ -94,13 +94,11 @@ private: LinizerState() : braceDepth(0), leftBraceFollows(false), - inComment(false), pendingRightBrace(false) { } int braceDepth; bool leftBraceFollows; - bool inComment; bool pendingRightBrace; QString line; QList<QScriptIncrementalScanner::Token> tokens;