Skip to content
Snippets Groups Projects
gitplugin.cpp 60 KiB
Newer Older
                     tr("Do you want to commit the change?"),
                     tr("Git will not accept this commit. Do you want to continue to edit it?"),
                     promptData, !m_submitActionTriggered, false);
    }
con's avatar
con committed
    switch (answer) {
hjk's avatar
hjk committed
    case VcsBase::VcsBaseSubmitEditor::SubmitCanceled:
con's avatar
con committed
        return false; // Keep editing and change file
hjk's avatar
hjk committed
    case VcsBase::VcsBaseSubmitEditor::SubmitDiscarded:
con's avatar
con committed
        return true; // Cancel all
    default:
        break;
    }
con's avatar
con committed
    // Go ahead!
hjk's avatar
hjk committed
    VcsBase::SubmitFileModel *model = qobject_cast<VcsBase::SubmitFileModel *>(editor->fileModel());
    CommitType commitType = editor->commitType();
    QString amendSHA1 = editor->amendSHA1();
    if (model->hasCheckedFiles() || !amendSHA1.isEmpty()) {
con's avatar
con committed
        // get message & commit
        if (!Core::DocumentManager::saveDocument(editorDocument))
con's avatar
con committed

        closeEditor = m_gitClient->addAndCommit(m_submitRepository, editor->panelData(),
                                                commitType, amendSHA1,
                                                m_commitMessageFileName, model);
con's avatar
con committed
    }
        if (commitType == FixupCommit) {
            QScopedPointer<GitClient::StashGuard> stashGuard(
                        new GitClient::StashGuard(m_submitRepository, QLatin1String("Rebase-fixup"),
                                                  NoPrompt));
            if (stashGuard->stashingFailed())
                return false;
            m_gitClient->interactiveRebase(m_submitRepository, amendSHA1, *stashGuard.take(), true);
        } else {
            m_gitClient->continueCommandIfNeeded(m_submitRepository);
        }
con's avatar
con committed
}

void GitPlugin::fetch()
{
Tobias Hunger's avatar
Tobias Hunger committed
    m_gitClient->synchronousFetch(currentState().topLevel(), QString());
con's avatar
con committed
void GitPlugin::pull()
{
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
Tobias Hunger's avatar
Tobias Hunger committed
    QTC_ASSERT(state.hasTopLevel(), return);
    QString topLevel = state.topLevel();
    bool rebase = m_gitClient->settings()->boolValue(GitSettings::pullRebaseKey);

    if (!rebase) {
        QString currentBranch = m_gitClient->synchronousCurrentLocalBranch(topLevel);
        if (!currentBranch.isEmpty()) {
            currentBranch.prepend(QLatin1String("branch."));
            currentBranch.append(QLatin1String(".rebase"));
            rebase = (m_gitClient->readConfigValue(topLevel, currentBranch) == QLatin1String("true"));
    GitClient::StashGuard stashGuard(topLevel, QLatin1String("Pull"),
                                     rebase ? Default : AllowUnstashed);
    if (stashGuard.stashingFailed())
Orgad Shaneh's avatar
Orgad Shaneh committed
        return;
    if (!m_gitClient->synchronousPull(topLevel, rebase))
Orgad Shaneh's avatar
Orgad Shaneh committed
        stashGuard.preventPop();
con's avatar
con committed
}

void GitPlugin::push()
{
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    m_gitClient->synchronousPush(state.topLevel());
void GitPlugin::startMergeTool()
{
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    m_gitClient->merge(state.topLevel());
}

void GitPlugin::continueOrAbortCommand()
{
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    QObject *action = QObject::sender();

    if (action == m_abortMergeAction)
        m_gitClient->synchronousMerge(state.topLevel(), QLatin1String("--abort"));
    else if (action == m_abortRebaseAction)
        m_gitClient->synchronousRebase(state.topLevel(), QLatin1String("--abort"));
    else if (action == m_abortCherryPickAction)
        m_gitClient->synchronousCherryPick(state.topLevel(), QLatin1String("--abort"));
    else if (action == m_abortRevertAction)
        m_gitClient->synchronousRevert(state.topLevel(), QLatin1String("--abort"));
    else if (action == m_continueRebaseAction)
        m_gitClient->synchronousRebase(state.topLevel(), QLatin1String("--continue"));
    else if (action == m_continueCherryPickAction)
        m_gitClient->synchronousCherryPick(state.topLevel(), QLatin1String("--continue"));
    else if (action == m_continueRevertAction)
        m_gitClient->synchronousRevert(state.topLevel(), QLatin1String("--continue"));

    updateContinueAndAbortCommands();
}

// Retrieve member function of git client stored as user data of action
static inline GitClientMemberFunc memberFunctionFromAction(const QObject *o)
{
    if (o) {
        if (const QAction *action = qobject_cast<const QAction *>(o)) {
            const QVariant v = action->data();
            if (v.canConvert<GitClientMemberFunc>())
                return qvariant_cast<GitClientMemberFunc>(v);
        }
    }
    return 0;
}

void GitPlugin::gitClientMemberFuncRepositoryAction()
{
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    // Retrieve member function and invoke on repository
    GitClientMemberFunc func = memberFunctionFromAction(sender());
    QTC_ASSERT(func, return);
    (m_gitClient->*func)(state.topLevel());
}

void GitPlugin::cleanProject()
{
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasProject(), return);
    cleanRepository(state.currentProjectPath());
}

void GitPlugin::cleanRepository()
{
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    cleanRepository(state.topLevel());
}
void GitPlugin::cleanRepository(const QString &directory)
{
    // Find files to be deleted
    QString errorMessage;
    QStringList files;
Orgad Shaneh's avatar
Orgad Shaneh committed
    QStringList ignoredFiles;
    QApplication::setOverrideCursor(Qt::WaitCursor);
Orgad Shaneh's avatar
Orgad Shaneh committed
    const bool gotFiles = m_gitClient->synchronousCleanList(directory, &files, &ignoredFiles, &errorMessage);
    QApplication::restoreOverrideCursor();

hjk's avatar
hjk committed
    QWidget *parent = Core::ICore::mainWindow();
Orgad Shaneh's avatar
Orgad Shaneh committed
        QMessageBox::warning(parent, tr("Unable to retrieve file list"), errorMessage);
Orgad Shaneh's avatar
Orgad Shaneh committed
    if (files.isEmpty() && ignoredFiles.isEmpty()) {
        QMessageBox::information(parent, tr("Repository Clean"),
                                 tr("The repository is clean."));
        return;
    }

    // Show in dialog
hjk's avatar
hjk committed
    VcsBase::CleanDialog dialog(parent);
Orgad Shaneh's avatar
Orgad Shaneh committed
    dialog.setFileList(directory, files, ignoredFiles);
void GitPlugin::updateSubmodules()
{
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    m_gitClient->submoduleUpdate(state.topLevel());
}

// If the file is modified in an editor, make sure it is saved.
static bool ensureFileSaved(const QString &fileName)
{
    const QList<Core::IEditor*> editors = Core::EditorManager::instance()->editorsForFileName(fileName);
    if (editors.isEmpty())
        return true;
    Core::IDocument *document = editors.front()->document();
    if (!document || !document->isModified())
    QList<Core::IDocument *> documents;
    documents << document;
    Core::DocumentManager::saveModifiedDocuments(documents, &canceled);
    return !canceled;
}

void GitPlugin::applyCurrentFilePatch()
{
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasPatchFile() && state.hasTopLevel(), return);
    const QString patchFile = state.currentPatchFile();
    if (!ensureFileSaved(patchFile))
        return;
    applyPatch(state.topLevel(), patchFile);
}

void GitPlugin::promptApplyPatch()
{
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    applyPatch(state.topLevel(), QString());
}

void GitPlugin::applyPatch(const QString &workingDirectory, QString file)
{
    // Ensure user has been notified about pending changes
    GitClient::StashGuard stashGuard(workingDirectory, QLatin1String("Apply-Patch"), AllowUnstashed);
    if (stashGuard.stashingFailed())
        return;
    // Prompt for file
    if (file.isEmpty()) {
        const QString filter = tr("Patches (*.patch *.diff)");
hjk's avatar
hjk committed
        file =  QFileDialog::getOpenFileName(Core::ICore::mainWindow(),
                                             tr("Choose Patch"),
                                             QString(), filter);
        if (file.isEmpty())
            return;
    }
    // Run!
hjk's avatar
hjk committed
    VcsBase::VcsBaseOutputWindow *outwin = VcsBase::VcsBaseOutputWindow::instance();
    QString errorMessage;
    if (m_gitClient->synchronousApplyPatch(workingDirectory, file, &errorMessage)) {
        if (errorMessage.isEmpty())
            outwin->append(tr("Patch %1 successfully applied to %2").arg(file, workingDirectory));
            outwin->append(errorMessage);
    } else {
        outwin->appendError(errorMessage);
    }
}

    // Simple stash without prompt, reset repo.
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);

    GitClient::StashGuard stashGuard(state.topLevel(), QString(), NoPrompt);
    if (stashGuard.stashingFailed())
        return;
    stashGuard.preventPop();
    if (stashGuard.result() == GitClient::StashGuard::Stashed && m_stashDialog)
        m_stashDialog->refresh(state.topLevel(), true);
}

void GitPlugin::stashSnapshot()
{
    // Prompt for description, restore immediately and keep on working.
hjk's avatar
hjk committed
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    const QString id = m_gitClient->synchronousStash(state.topLevel(), QString(),
                GitClient::StashImmediateRestore|GitClient::StashPromptDescription);
    if (!id.isEmpty() && m_stashDialog)
        m_stashDialog->refresh(state.topLevel(), true);
// Create a non-modal dialog with refresh method or raise if it exists
template <class NonModalDialog>
    inline void showNonModalDialog(const QString &topLevel,
                                   QPointer<NonModalDialog> &dialog)
{
    if (dialog) {
        dialog->show();
        dialog->raise();
    } else {
hjk's avatar
hjk committed
        dialog = new NonModalDialog(Core::ICore::mainWindow());
        dialog->refresh(topLevel, true);
        dialog->show();
    }
}

   showNonModalDialog(currentState().topLevel(), m_branchDialog);
void GitPlugin::remoteList()
{
    showNonModalDialog(currentState().topLevel(), m_remoteDialog);
}

    showNonModalDialog(currentState().topLevel(), m_stashDialog);
con's avatar
con committed
}

hjk's avatar
hjk committed
void GitPlugin::updateActions(VcsBase::VcsBasePlugin::ActionState as)
con's avatar
con committed
{
Friedemann Kleint's avatar
Friedemann Kleint committed
    const bool repositoryEnabled = currentState().hasTopLevel();
    if (m_stashDialog)
        m_stashDialog->refresh(currentState().topLevel(), false);
    if (m_branchDialog)
        m_branchDialog->refresh(currentState().topLevel(), false);
    if (m_remoteDialog)
        m_remoteDialog->refresh(currentState().topLevel(), false);
Friedemann Kleint's avatar
Friedemann Kleint committed
    m_commandLocator->setEnabled(repositoryEnabled);
    if (!enableMenuAction(as, m_menuAction))
Orgad Shaneh's avatar
Orgad Shaneh committed
    if (repositoryEnabled)
        updateVersionWarning();
    // Note: This menu is visible if there is no repository. Only
    // 'Create Repository'/'Show' actions should be available.
    const QString fileName = currentState().currentFileName();
    foreach (Utils::ParameterAction *fileAction, m_fileActions)
        fileAction->setParameter(fileName);
    // If the current file looks like a patch, offer to apply
    m_applyCurrentFilePatchAction->setParameter(currentState().currentPatchFileDisplayName());
    const QString projectName = currentState().currentProjectName();
    foreach (Utils::ParameterAction *projectAction, m_projectActions)
        projectAction->setParameter(projectName);

    foreach (QAction *repositoryAction, m_repositoryActions)
        repositoryAction->setEnabled(repositoryEnabled);
    m_submoduleUpdateAction->setVisible(repositoryEnabled
            && QFile::exists(currentState().topLevel() + QLatin1String("/.gitmodules")));

    updateContinueAndAbortCommands();
    updateRepositoryBrowserAction();

    m_gerritPlugin->updateActions(repositoryEnabled);
con's avatar
con committed
}

void GitPlugin::updateContinueAndAbortCommands()
{
    if (currentState().hasTopLevel()) {
        GitClient::CommandInProgress gitCommandInProgress =
                m_gitClient->checkCommandInProgress(currentState().topLevel());

        m_mergeToolAction->setVisible(gitCommandInProgress != GitClient::NoCommand);
        m_abortMergeAction->setVisible(gitCommandInProgress == GitClient::Merge);
        m_abortCherryPickAction->setVisible(gitCommandInProgress == GitClient::CherryPick);
        m_abortRevertAction->setVisible(gitCommandInProgress == GitClient::Revert);
        m_abortRebaseAction->setVisible(gitCommandInProgress == GitClient::Rebase
                                        || gitCommandInProgress == GitClient::RebaseMerge);
        m_continueCherryPickAction->setVisible(gitCommandInProgress == GitClient::CherryPick);
        m_continueRevertAction->setVisible(gitCommandInProgress == GitClient::Revert);
        m_continueRebaseAction->setVisible(gitCommandInProgress == GitClient::Rebase
                                           || gitCommandInProgress == GitClient::RebaseMerge);
    } else {
        m_mergeToolAction->setVisible(false);
        m_abortMergeAction->setVisible(false);
        m_abortCherryPickAction->setVisible(false);
        m_abortRevertAction->setVisible(false);
        m_abortRebaseAction->setVisible(false);
        m_continueCherryPickAction->setVisible(false);
        m_continueRevertAction->setVisible(false);
        m_continueRebaseAction->setVisible(false);
    }
}

void GitPlugin::updateRepositoryBrowserAction()
{
    const bool repositoryEnabled = currentState().hasTopLevel();
    const bool hasRepositoryBrowserCmd = !settings().stringValue(GitSettings::repositoryBrowserCmd).isEmpty();
    m_repositoryBrowserAction->setEnabled(repositoryEnabled && hasRepositoryBrowserCmd);
}

const GitSettings &GitPlugin::settings() const
    return m_settings;
}

void GitPlugin::setSettings(const GitSettings &s)
{
    if (s == m_settings)
        return;

    m_settings = s;
Tobias Hunger's avatar
Tobias Hunger committed
    m_gitClient->saveSettings();
    static_cast<GitVersionControl *>(versionControl())->emitConfigurationChanged();
    updateRepositoryBrowserAction();
GitClient *GitPlugin::gitClient() const
{
    return m_gitClient;
}

#ifdef WITH_TESTS
#include <QTest>
Q_DECLARE_METATYPE(FileStates)
void GitPlugin::testStatusParsing_data()
{
    QTest::addColumn<FileStates>("first");
    QTest::addColumn<FileStates>("second");

    QTest::newRow(" M") << FileStates(ModifiedFile) << FileStates(UnknownFileState);
    QTest::newRow(" D") << FileStates(DeletedFile) << FileStates(UnknownFileState);
    QTest::newRow("M ") << (ModifiedFile | StagedFile) << FileStates(UnknownFileState);
    QTest::newRow("MM") << (ModifiedFile | StagedFile) << FileStates(ModifiedFile);
    QTest::newRow("MD") << (ModifiedFile | StagedFile) << FileStates(DeletedFile);
    QTest::newRow("A ") << (AddedFile | StagedFile) << FileStates(UnknownFileState);
    QTest::newRow("AM") << (AddedFile | StagedFile) << FileStates(ModifiedFile);
    QTest::newRow("AD") << (AddedFile | StagedFile) << FileStates(DeletedFile);
    QTest::newRow("D ") << (DeletedFile | StagedFile) << FileStates(UnknownFileState);
    QTest::newRow("DM") << (DeletedFile | StagedFile) << FileStates(ModifiedFile);
    QTest::newRow("R ") << (RenamedFile | StagedFile) << FileStates(UnknownFileState);
    QTest::newRow("RM") << (RenamedFile | StagedFile) << FileStates(ModifiedFile);
    QTest::newRow("RD") << (RenamedFile | StagedFile) << FileStates(DeletedFile);
    QTest::newRow("C ") << (CopiedFile | StagedFile) << FileStates(UnknownFileState);
    QTest::newRow("CM") << (CopiedFile | StagedFile) << FileStates(ModifiedFile);
    QTest::newRow("CD") << (CopiedFile | StagedFile) << FileStates(DeletedFile);
    QTest::newRow("??") << FileStates(UntrackedFile) << FileStates(UnknownFileState);
Orgad Shaneh's avatar
Orgad Shaneh committed

    // Merges
    QTest::newRow("DD") << (DeletedFile | UnmergedFile | UnmergedUs | UnmergedThem) << FileStates(UnknownFileState);
    QTest::newRow("AA") << (AddedFile | UnmergedFile | UnmergedUs | UnmergedThem) << FileStates(UnknownFileState);
    QTest::newRow("UU") << (ModifiedFile | UnmergedFile | UnmergedUs | UnmergedThem) << FileStates(UnknownFileState);
    QTest::newRow("AU") << (AddedFile | UnmergedFile | UnmergedUs) << FileStates(UnknownFileState);
    QTest::newRow("UD") << (DeletedFile | UnmergedFile | UnmergedThem) << FileStates(UnknownFileState);
    QTest::newRow("UA") << (AddedFile | UnmergedFile | UnmergedThem) << FileStates(UnknownFileState);
    QTest::newRow("DU") << (DeletedFile | UnmergedFile | UnmergedUs) << FileStates(UnknownFileState);
}

void GitPlugin::testStatusParsing()
{
    CommitData data;
    QFETCH(FileStates, first);
    QFETCH(FileStates, second);
    QString output = QLatin1String("## master...origin/master [ahead 1]\n");
    output += QString::fromLatin1(QTest::currentDataTag()) + QLatin1String(" main.cpp\n");
    data.parseFilesFromStatus(output);
    QCOMPARE(data.files.at(0).first, first);
    if (second == UnknownFileState)
        QCOMPARE(data.files.size(), 1);
    else
        QCOMPARE(data.files.at(1).first, second);
}

void GitPlugin::testDiffFileResolving_data()
{
    QTest::addColumn<QByteArray>("header");
    QTest::addColumn<QByteArray>("fileName");

    QTest::newRow("New") << QByteArray(
            "diff --git a/src/plugins/git/giteditor.cpp b/src/plugins/git/giteditor.cpp\n"
            "new file mode 100644\n"
            "index 0000000..40997ff\n"
            "--- /dev/null\n"
            "+++ b/src/plugins/git/giteditor.cpp\n"
            "@@ -0,0 +1,281 @@\n\n")
        << QByteArray("src/plugins/git/giteditor.cpp");
    QTest::newRow("Deleted") << QByteArray(
            "diff --git a/src/plugins/git/giteditor.cpp b/src/plugins/git/giteditor.cpp\n"
            "deleted file mode 100644\n"
            "index 40997ff..0000000\n"
            "--- a/src/plugins/git/giteditor.cpp\n"
            "+++ /dev/null\n"
            "@@ -1,281 +0,0 @@\n\n")
        << QByteArray("src/plugins/git/giteditor.cpp");
    QTest::newRow("Normal") << QByteArray(
            "diff --git a/src/plugins/git/giteditor.cpp b/src/plugins/git/giteditor.cpp\n"
            "index 69e0b52..8fc974d 100644\n"
            "--- a/src/plugins/git/giteditor.cpp\n"
            "+++ b/src/plugins/git/giteditor.cpp\n"
            "@@ -49,6 +49,8 @@\n\n")
        << QByteArray("src/plugins/git/giteditor.cpp");
}

void GitPlugin::testDiffFileResolving()
{
    GitEditor editor(editorParameters + 3, 0);
    editor.testDiffFileResolving();

void GitPlugin::testLogResolving()
{
    QByteArray data(
                "commit 50a6b54c03219ad74b9f3f839e0321be18daeaf6 (HEAD, origin/master)\n"
                "Merge: 3587b51 bc93ceb\n"
                "Author: Junio C Hamano <gitster@pobox.com>\n"
                "Date:   Fri Jan 25 12:53:31 2013 -0800\n"
                "\n"
                "   Merge branch 'for-junio' of git://bogomips.org/git-svn\n"
                "    \n"
                "    * 'for-junio' of git://bogomips.org/git-svn:\n"
                "      git-svn: Simplify calculation of GIT_DIR\n"
                "      git-svn: cleanup sprintf usage for uppercasing hex\n"
                "\n"
                "commit 3587b513bafd7a83d8c816ac1deed72b5e3a27e9\n"
                "Author: Junio C Hamano <gitster@pobox.com>\n"
                "Date:   Fri Jan 25 12:52:55 2013 -0800\n"
                "\n"
                "    Update draft release notes to 1.8.2\n"
                "    \n"
                "    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n"
                );
    GitEditor editor(editorParameters + 1, 0);
    editor.testLogResolving(data,
                            "50a6b54c - Merge branch 'for-junio' of git://bogomips.org/git-svn",
                            "3587b513 - Update draft release notes to 1.8.2");
con's avatar
con committed
Q_EXPORT_PLUGIN(GitPlugin)