From 72d50cc680578d77525c33440d6ebd3060310e38 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Fri, 13 Mar 2026 17:56:03 +0100 Subject: [PATCH 1/3] feat(commit-editor): add AI-powered commit message generation Add AI commit message generation feature with configurable settings for service URL, API key, model, temperature, system message, and prompt message. The feature includes: - New settings for enabling/disabling AI commit messages - Secure credential storage for API keys - AI request preparation with detailed diff information - Integration with CommitEditor UI - Error handling and user feedback - Support for both system and user messages in AI requests The implementation follows conventional commits format and provides a complete solution for generating professional git commit messages using AI. --- src/conf/Setting.cpp | 6 + src/conf/Setting.h | 47 +++++ src/dialogs/SettingsDialog.cpp | 132 ++++++++++++ src/ui/CommitEditor.cpp | 373 ++++++++++++++++++++++++++++++++- src/ui/CommitEditor.h | 6 + 5 files changed, 562 insertions(+), 2 deletions(-) diff --git a/src/conf/Setting.cpp b/src/conf/Setting.cpp index c7e2bea63..b3ec6382e 100644 --- a/src/conf/Setting.cpp +++ b/src/conf/Setting.cpp @@ -44,6 +44,12 @@ void Setting::initialize(QMap &keys) { keys[Id::ShowChangedFilesInSingleView] = "doubletreeview/single"; keys[Id::ShowChangedFilesMultiColumn] = "doubletreeview/listviewmulticolumn"; keys[Id::HideUntracked] = "untracked.hide"; + keys[Id::AiServiceUrl] = "ai/service/url"; + keys[Id::EnableAiCommitMessages] = "ai/commit/enable"; + keys[Id::AiCommitModel] = "ai/commit/model"; + keys[Id::AiCommitTemperature] = "ai/commit/temperature"; + keys[Id::AiCommitSystemMessage] = "ai/commit/system_message"; + keys[Id::AiCommitPromptMessage] = "ai/commit/prompt_message"; } void Prompt::initialize(QMap &keys) { diff --git a/src/conf/Setting.h b/src/conf/Setting.h index 24fe12b36..e2f052de5 100644 --- a/src/conf/Setting.h +++ b/src/conf/Setting.h @@ -4,6 +4,47 @@ #include #include +// AI Commit Message Generation Constants +namespace AiCommitConstants { + +inline constexpr bool enabled = true; +inline constexpr const char *DefaultServiceUrl = "https://api.mistral.ai/v1"; +inline constexpr const char *DefaultModel = "mistral-tiny"; +inline constexpr double DefaultTemperature = 0.2; + +inline constexpr const char *DefaultSystemMessage = + "You are a helpful assistant that generates concise git commit messages " + "following conventional commits format. " + "Start directly with the commit message without any prefixes like 'Commit " + "Message:', 'Git', or markdown code blocks. " + "Follow the format: type(scope): subject\n\nbody"; + +inline constexpr const char *DefaultPromptMessage = + "Generate a concise, professional git commit message based on the " + "following changes:\n\n" + "Changed files:\n" + "{{CHANGED_FILES}}\n" + "Additions:\n" + "{{ADDITIONS}}\n" + "Deletions:\n" + "{{DELETIONS}}\n" + "File changes:\n" + "{{FILE_CHANGES}}\n" + "Total changes: +{{TOTAL_ADDITIONS}} lines, -{{TOTAL_DELETIONS}} " + "lines\n\n" + "Follow git commit message conventions:\n" + "- First line: short summary (50-72 chars max)\n" + "- Body: detailed explanation (72 chars per line max)\n" + "- Use imperative mood (e.g., 'Fix bug' not 'Fixed bug')\n" + "- Be concise but descriptive\n" + "- Start directly with the commit message (no prefixes like 'Commit Message:' nor markdown in the title)\n" + "- Follow format: type(scope): subject\\n\\nbody\n" + "- Consider the commit message idea if available\n" + "\n" + "Generate the commit message:"; + +} // namespace AiCommitConstants + template class SettingsTempl { public: template static QString key(const TId id) { @@ -68,6 +109,12 @@ class Setting : public SettingsTempl { ShowChangedFilesInSingleView, HideUntracked, Language, + AiServiceUrl, + EnableAiCommitMessages, + AiCommitModel, + AiCommitTemperature, + AiCommitSystemMessage, + AiCommitPromptMessage, }; Q_ENUM(Id) diff --git a/src/dialogs/SettingsDialog.cpp b/src/dialogs/SettingsDialog.cpp index 786f7a058..d62ee420c 100644 --- a/src/dialogs/SettingsDialog.cpp +++ b/src/dialogs/SettingsDialog.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include @@ -792,10 +793,141 @@ class MiscPanel : public QWidget { Settings::instance()->setValue(Setting::Id::SshKeyFilePath, text); }); + // AI Commit Message settings + QCheckBox *enableAiCommitMessages = + new QCheckBox(tr("Enable AI commit messages"), this); + enableAiCommitMessages->setChecked( + settings->value(Setting::Id::EnableAiCommitMessages, AiCommitConstants::enabled).toBool()); + connect(enableAiCommitMessages, &QCheckBox::toggled, [](bool checked) { + Settings::instance()->setValue(Setting::Id::EnableAiCommitMessages, + checked); + }); + + // Initialize default AI service URL if not set + QString aiServiceUrl = + settings->value(Setting::Id::AiServiceUrl).toString(); + if (aiServiceUrl.isEmpty()) { + aiServiceUrl = AiCommitConstants::DefaultServiceUrl; + settings->setValue(Setting::Id::AiServiceUrl, aiServiceUrl); + } + + QLineEdit *aiServiceUrlBox = new QLineEdit(aiServiceUrl, this); + connect(aiServiceUrlBox, &QLineEdit::textChanged, [](const QString &text) { + Settings::instance()->setValue(Setting::Id::AiServiceUrl, text); + }); + + // AI Service API Key - stored securely using credential helper + QLineEdit *aiApiKeyBox = new QLineEdit(this); + + // Load API key from credential helper using the API URL as key + QString aiApiKey; + CredentialHelper *credHelper = CredentialHelper::instance(); + if (credHelper && !aiServiceUrl.isEmpty()) { + credHelper->get(aiServiceUrl, aiApiKey, aiApiKey); + if (!aiApiKey.isEmpty()) { + aiApiKeyBox->setText(tr("Api key found for specified api url")); + } + } + + connect( + aiApiKeyBox, &QLineEdit::textChanged, + [aiServiceUrlBox, aiApiKeyBox](const QString &apiKey) { + QString aiServiceUrl = aiServiceUrlBox->text(); + CredentialHelper *credHelper = CredentialHelper::instance(); + if (credHelper && !apiKey.isEmpty() && !aiServiceUrl.isEmpty()) { + bool success = + credHelper->store(aiServiceUrl, "ai-commit-key", apiKey); + if (!success) { + QMessageBox::warning( + aiApiKeyBox, QObject::tr("Credential Storage Failed"), + QObject::tr("Failed to store API key in secure storage. The " + "key will not be saved.")); + } + } else if (credHelper && apiKey.isEmpty() && + !aiServiceUrl.isEmpty()) { + // If key is empty, remove it from storage + bool success = credHelper->store(aiServiceUrl, "ai-commit-key", ""); + if (!success) { + QMessageBox::warning( + aiApiKeyBox, QObject::tr("Credential Removal Failed"), + QObject::tr("Failed to remove API key from secure storage.")); + } + } + }); + + // Configurable AI settings + QLineEdit *aiCommitModelBox = new QLineEdit( + settings + ->value(Setting::Id::AiCommitModel, AiCommitConstants::DefaultModel) + .toString(), + this); + connect(aiCommitModelBox, &QLineEdit::textChanged, [](const QString &text) { + Settings::instance()->setValue(Setting::Id::AiCommitModel, text); + }); + + // Hide temperature and system message for now + QDoubleSpinBox *aiCommitTemperatureBox = new QDoubleSpinBox(this); + aiCommitTemperatureBox->setRange(0.0, 2.0); + aiCommitTemperatureBox->setSingleStep(0.1); + aiCommitTemperatureBox->setValue( + settings + ->value(Setting::Id::AiCommitTemperature, + AiCommitConstants::DefaultTemperature) + .toDouble()); + aiCommitTemperatureBox->setVisible(false); + connect(aiCommitTemperatureBox, + QOverload::of(&QDoubleSpinBox::valueChanged), + [](double value) { + Settings::instance()->setValue(Setting::Id::AiCommitTemperature, + value); + }); + + QTextEdit *aiCommitSystemMessageBox = new QTextEdit(this); + aiCommitSystemMessageBox->setPlainText( + settings + ->value( + Setting::Id::AiCommitSystemMessage, + QObject::tr( + "You are a helpful assistant that generates concise git " + "commit messages following conventional commits format. " + "Start directly with the commit message without any " + "prefixes like 'Commit Message:', 'Git', or markdown code " + "blocks. " + "Follow the format: type(scope): subject\n\nbody")) + .toString()); + aiCommitSystemMessageBox->setVisible(false); + connect(aiCommitSystemMessageBox, &QTextEdit::textChanged, + [aiCommitSystemMessageBox]() { + Settings::instance()->setValue( + Setting::Id::AiCommitSystemMessage, + aiCommitSystemMessageBox->toPlainText()); + }); + + // Add prompt message configuration + QTextEdit *aiCommitPromptMessageBox = new QTextEdit(this); + aiCommitPromptMessageBox->setPlainText( + settings + ->value(Setting::Id::AiCommitPromptMessage, + AiCommitConstants::DefaultPromptMessage) + .toString()); + connect(aiCommitPromptMessageBox, &QTextEdit::textChanged, + [aiCommitPromptMessageBox]() { + Settings::instance()->setValue( + Setting::Id::AiCommitPromptMessage, + aiCommitPromptMessageBox->toPlainText()); + }); + QFormLayout *layout = new QFormLayout(this); layout->addRow(tr("Path to SSH config file:"), sshConfigPathBox); layout->addRow(tr("Path to default / fallback SSH key file:"), sshKeyPathBox); + + layout->addRow(new QLabel(tr("AI Commit Message Generation"))); + layout->addRow(tr("Enable AI commit messages:"), enableAiCommitMessages); + layout->addRow(tr("AI Service URL:"), aiServiceUrlBox); + layout->addRow(tr("AI Service API Key:"), aiApiKeyBox); + layout->addRow(tr("AI Commit Model:"), aiCommitModelBox); + layout->addRow(tr("AI Prompt Message:"), aiCommitPromptMessageBox); } }; diff --git a/src/ui/CommitEditor.cpp b/src/ui/CommitEditor.cpp index ebde62175..6a676194a 100644 --- a/src/ui/CommitEditor.cpp +++ b/src/ui/CommitEditor.cpp @@ -6,6 +6,8 @@ #include "ContextMenuButton.h" #include "MenuBar.h" #include "RepoView.h" +#include "git/Patch.h" +#include "cred/CredentialHelper.h" #include #include @@ -21,6 +23,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include namespace { const QString kDictKey = "commit.spellcheck.dict"; @@ -29,6 +39,184 @@ const QString kAltFmt = "%2"; QString brightText(const QString &text) { return kAltFmt.arg(QPalette().color(QPalette::BrightText).name(), text); } + +void debugWriteJsonObject(const QJsonObject &obj, const QString &filename) { + QFile debugFile(filename); + if (debugFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream stream(&debugFile); + QJsonDocument doc(obj); + stream << doc.toJson(QJsonDocument::Indented); + debugFile.close(); + } +} + +void prepareAiCommitRequest(const git::Diff &diff, Settings *settings, + QJsonObject &requestBody, const QString ¤tMessage = QString()) { + // Use configurable prompt message from settings + QString promptMessage = settings + ->value(Setting::Id::AiCommitPromptMessage, + AiCommitConstants::DefaultPromptMessage) + .toString(); + + // Create prompt variable for building the message + QString prompt = promptMessage; + + // If there's a current message, add it to the prompt as additional context + if (!currentMessage.isEmpty()) { + prompt += "\n\nCurrent commit message idea:\n" + currentMessage; + } + + // Collect diff information + QStringList changedFilesList; + QStringList additionsList; + QStringList deletionsList; + QStringList fileChangesList; + int totalAdditions = 0; + int totalDeletions = 0; + + int count = diff.count(); + git::Index index = diff.index(); + + for (int i = 0; i < count; ++i) { + QString name = diff.name(i); + + // Process both fully staged and partially staged files + // For partially staged files, only the staged portions will land in the + // commit + git::Index::StagedState status = index.isStaged(name); + if (status != git::Index::Staged && status != git::Index::PartiallyStaged) { + continue; + } + + // Get diff stats for this file + git::Patch patch = diff.patch(i); + if (patch.isValid()) { + int add = patch.lineStats().additions; + int del = patch.lineStats().deletions; + totalAdditions += add; + totalDeletions += del; + + if (add > 0) + additionsList.append(QString("%1: +%2").arg(name).arg(add)); + if (del > 0) + deletionsList.append(QString("%1: -%2").arg(name).arg(del)); + + // Get actual file content changes with context - these represent what + // will actually land in the commit + QString fileChanges; + int hunkCount = patch.count(); + for (int h = 0; h < hunkCount; ++h) { + int lineCount = patch.lineCount(h); + + // Add hunk header for reference + QByteArray header = patch.header(h); + if (!header.isEmpty()) { + fileChanges += "@@ " + QString::fromUtf8(header) + " @@\n"; + } + + for (int j = 0; j < lineCount; ++j) { + char origin = patch.lineOrigin(h, j); + QByteArray content = patch.lineContent(h, j); + QString lineText = QString::fromUtf8(content); + + switch (origin) { + case '+': // ADDITION + fileChanges += "+" + lineText + "\n"; + break; + case '-': // DELETION + fileChanges += "-" + lineText + "\n"; + break; + case ' ': // CONTEXT + // Include context lines to provide better understanding + fileChanges += " " + lineText + "\n"; + break; + default: + // Skip headers and other special lines + break; + } + } + fileChanges += "\n"; // Separate hunks with blank line + } + + if (!fileChanges.isEmpty()) { + fileChangesList.append( + QString("File: %1\n%2").arg(name).arg(fileChanges)); + } + } + + changedFilesList.append(name); + } + + // Build changed files section + QString changedFilesText; + for (const QString &file : changedFilesList) { + changedFilesText += "- " + file + "\n"; + } + prompt.replace("{{CHANGED_FILES}}", changedFilesText); + + // Build additions section + QString additionsText; + for (const QString &add : additionsList) { + additionsText += "+ " + add + "\n"; + } + prompt.replace("{{ADDITIONS}}", additionsText); + + // Build deletions section + QString deletionsText; + for (const QString &del : deletionsList) { + deletionsText += "- " + del + "\n"; + } + prompt.replace("{{DELETIONS}}", deletionsText); + + // Build file changes section + QString fileChangesText; + for (const QString &change : fileChangesList) { + fileChangesText += change + "\n"; + } + prompt.replace("{{FILE_CHANGES}}", fileChangesText); + + // Replace total stats + prompt.replace("{{TOTAL_ADDITIONS}}", QString::number(totalAdditions)); + prompt.replace("{{TOTAL_DELETIONS}}", QString::number(totalDeletions)); + + // Create the request body + // Use configurable settings + QString model = + settings + ->value(Setting::Id::AiCommitModel, AiCommitConstants::DefaultModel) + .toString(); + double temperature = settings + ->value(Setting::Id::AiCommitTemperature, + AiCommitConstants::DefaultTemperature) + .toDouble(); + QString systemMessageText = + settings + ->value(Setting::Id::AiCommitSystemMessage, + AiCommitConstants::DefaultSystemMessage) + .toString(); + + requestBody["model"] = model; + requestBody["temperature"] = temperature; + + QJsonArray messages; + QJsonObject systemMessage; + systemMessage["role"] = "system"; + systemMessage["content"] = systemMessageText; + + QJsonObject userMessage; + userMessage["role"] = "user"; + userMessage["content"] = prompt; + + messages.append(systemMessage); + messages.append(userMessage); + requestBody["messages"] = messages; + + // Debug: Write JSON to file for inspection +debugWriteJsonObject(requestBody, + QStandardPaths::writableLocation(QStandardPaths::TempLocation) + +"/gittyup_ai_commit_request.json"); +} + } // namespace class TextEdit : public QTextEdit { @@ -297,8 +485,9 @@ CommitEditor::CommitEditor(const git::Repository &repo, QWidget *parent) mUserDict = Settings::userDir().path() + "/user.dic"; QFile userDict(mUserDict); if (!userDict.exists()) { - userDict.open(QIODevice::WriteOnly); - userDict.close(); + if (userDict.open(QIODevice::WriteOnly)) { + userDict.close(); + } } // Find installed Dictionaries. @@ -437,6 +626,7 @@ CommitEditor::CommitEditor(const git::Repository &repo, QWidget *parent) labelLayout->addWidget(button); mMessage = new TextEdit(this); + mMessage->setUndoRedoEnabled(true); mMessage->setAcceptRichText(false); mMessage->setObjectName("MessageEditor"); mMessage->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); @@ -508,6 +698,14 @@ CommitEditor::CommitEditor(const git::Repository &repo, QWidget *parent) view->mergeAbort(); }); + mGenerateCommitMessage = new QPushButton(tr("Generate Commit Message"), this); + mGenerateCommitMessage->setObjectName("GenerateCommitMessage"); + connect(mGenerateCommitMessage, &QPushButton::clicked, this, + &CommitEditor::generateCommitMessage); + mGenerateCommitMessage->setVisible(Settings::instance()->value(Setting::Id::EnableAiCommitMessages, AiCommitConstants::enabled).toBool()); + + mNetworkManager = new QNetworkAccessManager(this); + // Update buttons on index change. connect(repo.notifier(), &git::RepositoryNotifier::indexChanged, [this](const QStringList &paths, bool yieldFocus) { @@ -520,6 +718,7 @@ CommitEditor::CommitEditor(const git::Repository &repo, QWidget *parent) buttonLayout->addWidget(mStage); buttonLayout->addWidget(mUnstage); buttonLayout->addWidget(mCommit); + buttonLayout->addWidget(mGenerateCommitMessage); buttonLayout->addWidget(mRebaseContinue); buttonLayout->addWidget(mRebaseAbort); buttonLayout->addWidget(mMergeAbort); @@ -824,6 +1023,176 @@ void CommitEditor::updateButtons(bool yieldFocus) { MenuBar::instance(this)->updateRepository(); } +void CommitEditor::generateCommitMessage() { + // Check if AI commit messages are enabled + Settings *settings = Settings::instance(); + bool aiEnabled = + settings->value(Setting::Id::EnableAiCommitMessages, AiCommitConstants::enabled).toBool(); + + if (!aiEnabled) { + QString errorMsg = tr("AI commit message generation is disabled. Please " + "enable it in settings."); + mGenerateCommitMessage->setToolTip(errorMsg); + mGenerateCommitMessage->setStyleSheet("QPushButton { color: red; }"); + return; + } + + // Check if we have a valid diff + if (!mDiff.isValid()) { + QString errorMsg = tr("No changes detected to generate a commit message."); + mGenerateCommitMessage->setToolTip(errorMsg); + mGenerateCommitMessage->setStyleSheet("QPushButton { color: red; }"); + return; + } + + // Prepare the AI request + QJsonObject requestBody; + prepareAiCommitRequest(mDiff, settings, requestBody, mMessage->toPlainText()); + QJsonDocument doc(requestBody); + + // Get API settings + QString apiUrl = settings->value(Setting::Id::AiServiceUrl).toString(); + + // Check if API URL is configured + if (apiUrl.isEmpty()) { + QString errorMsg = + tr("AI service URL is not configured. Please set it in settings."); + mGenerateCommitMessage->setToolTip(errorMsg); + mGenerateCommitMessage->setStyleSheet("QPushButton { color: red; }"); + return; + } + + // Retrieve API key securely from credential helper using the API URL as key + QString apiKey; + CredentialHelper *credHelper = CredentialHelper::instance(); + if (credHelper) { + credHelper->get(apiUrl, apiKey, apiKey); + } + + if (apiKey.isEmpty()) { + QString errorMsg = + tr("AI service API key is not configured. Please set it in settings."); + mGenerateCommitMessage->setToolTip(errorMsg); + mGenerateCommitMessage->setStyleSheet("QPushButton { color: red; }"); + return; + } + + // Show progress + mGenerateCommitMessage->setEnabled(false); + mGenerateCommitMessage->setText(tr("Generating...")); + QApplication::setOverrideCursor(Qt::WaitCursor); + + // Prepare the API request + QNetworkRequest request(QUrl(apiUrl + "/chat/completions")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer " + apiKey.toUtf8()); + + // Send the request + QNetworkReply *reply = mNetworkManager->post(request, doc.toJson()); + + // Handle the response + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + QApplication::restoreOverrideCursor(); + mGenerateCommitMessage->setEnabled(true); + mGenerateCommitMessage->setText(tr("Generate Commit Message")); + + // Clear any previous error styling + mGenerateCommitMessage->setStyleSheet(""); + mGenerateCommitMessage->setToolTip(""); + + if (reply->error() != QNetworkReply::NoError) { + QString errorMsg = + tr("Failed to generate commit message: %1").arg(reply->errorString()); + + // Set error tooltip and style + mGenerateCommitMessage->setToolTip(errorMsg); + mGenerateCommitMessage->setStyleSheet("QPushButton { color: red; }"); + + reply->deleteLater(); + return; + } + + // Parse the response + QByteArray responseData = reply->readAll(); + QJsonDocument responseDoc = QJsonDocument::fromJson(responseData); + + if (responseDoc.isNull() || !responseDoc.isObject()) { + QString errorMsg = tr("Invalid response from API"); + + // Set error tooltip and style + mGenerateCommitMessage->setToolTip(errorMsg); + mGenerateCommitMessage->setStyleSheet("QPushButton { color: red; }"); + + reply->deleteLater(); + return; + } + + QJsonObject responseObj = responseDoc.object(); + + if (responseObj.contains("error")) { + QJsonObject errorObj = responseObj["error"].toObject(); + QString errorMsg = errorObj["message"].toString(); + QString fullErrorMsg = tr("API Error: %1").arg(errorMsg); + + // Set error tooltip and style + mGenerateCommitMessage->setToolTip(fullErrorMsg); + mGenerateCommitMessage->setStyleSheet("QPushButton { color: red; }"); + + reply->deleteLater(); + return; + } + + // Extract the commit message + if (responseObj.contains("choices") && responseObj["choices"].isArray()) { + QJsonArray choices = responseObj["choices"].toArray(); + if (!choices.isEmpty() && choices[0].isObject()) { + QJsonObject firstChoice = choices[0].toObject(); + if (firstChoice.contains("message") && + firstChoice["message"].isObject()) { + QJsonObject messageObj = firstChoice["message"].toObject(); + if (messageObj.contains("content")) { + QString commitMessage = messageObj["content"].toString(); + + // Clean up the message and set it in the editor + commitMessage = commitMessage.trimmed(); + + // Remove any markdown formatting if present + commitMessage.remove(QRegularExpression("^```[\\w]*\n")); + commitMessage.remove(QRegularExpression("\n```$")); + commitMessage.remove(QRegularExpression("^\\*\\*\\*\n")); + commitMessage.remove(QRegularExpression("\n\\*\\*\\*$")); + + // Set the message in the editor without clearing undo stack + QTextCursor cursor = mMessage->textCursor(); + cursor.beginEditBlock(); + cursor.select(QTextCursor::Document); + cursor.removeSelectedText(); + cursor.insertText(commitMessage); + cursor.endEditBlock(); + mMessage->setTextCursor(cursor); + + // Set success tooltip + mGenerateCommitMessage->setToolTip( + tr("Commit message generated successfully")); + mGenerateCommitMessage->setStyleSheet( + "QPushButton { color: green; }"); + + return; + } + } + } + } + + QString errorMsg = tr("Could not extract commit message from API response"); + + // Set error tooltip and style + mGenerateCommitMessage->setToolTip(errorMsg); + mGenerateCommitMessage->setStyleSheet("QPushButton { color: red; }"); + + reply->deleteLater(); + }); +} + QTextEdit *CommitEditor::textEdit() const { return mMessage; } #include "CommitEditor.moc" diff --git a/src/ui/CommitEditor.h b/src/ui/CommitEditor.h index 3b452f4c8..564cd200b 100644 --- a/src/ui/CommitEditor.h +++ b/src/ui/CommitEditor.h @@ -6,12 +6,15 @@ #include #include +#include +#include class QPushButton; class QLabel; class TemplateButton; class TextEdit; class QTextEdit; +class QNetworkAccessManager; /*! * \brief The CommitEditor class @@ -39,6 +42,7 @@ class CommitEditor : public QFrame { void setMessage(const QString &message); QString message() const; void setDiff(const git::Diff &diff); + void generateCommitMessage(); public slots: void applyTemplate(const QString &t, const QStringList &files); @@ -60,6 +64,8 @@ public slots: QPushButton *mRebaseContinue; QPushButton *mMergeAbort; TemplateButton *mTemplate; + QPushButton *mGenerateCommitMessage; + QNetworkAccessManager *mNetworkManager; bool mEditorEmpty = true; bool mPopulate = true; From 4aa0bd14a1440aa35f7a802d3f90c46efa3091ea Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Sat, 14 Mar 2026 08:24:46 +0100 Subject: [PATCH 2/3] Fix minor issues --- src/conf/Setting.h | 3 ++- src/dialogs/SettingsDialog.cpp | 18 +++++++----------- src/ui/CommitEditor.cpp | 23 ++++++++++++++++------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/conf/Setting.h b/src/conf/Setting.h index e2f052de5..38788013a 100644 --- a/src/conf/Setting.h +++ b/src/conf/Setting.h @@ -37,7 +37,8 @@ inline constexpr const char *DefaultPromptMessage = "- Body: detailed explanation (72 chars per line max)\n" "- Use imperative mood (e.g., 'Fix bug' not 'Fixed bug')\n" "- Be concise but descriptive\n" - "- Start directly with the commit message (no prefixes like 'Commit Message:' nor markdown in the title)\n" + "- Start directly with the commit message (no prefixes like 'Commit " + "Message:' nor markdown in the title)\n" "- Follow format: type(scope): subject\\n\\nbody\n" "- Consider the commit message idea if available\n" "\n" diff --git a/src/dialogs/SettingsDialog.cpp b/src/dialogs/SettingsDialog.cpp index d62ee420c..6bd1d4b1f 100644 --- a/src/dialogs/SettingsDialog.cpp +++ b/src/dialogs/SettingsDialog.cpp @@ -797,7 +797,10 @@ class MiscPanel : public QWidget { QCheckBox *enableAiCommitMessages = new QCheckBox(tr("Enable AI commit messages"), this); enableAiCommitMessages->setChecked( - settings->value(Setting::Id::EnableAiCommitMessages, AiCommitConstants::enabled).toBool()); + settings + ->value(Setting::Id::EnableAiCommitMessages, + AiCommitConstants::enabled) + .toBool()); connect(enableAiCommitMessages, &QCheckBox::toggled, [](bool checked) { Settings::instance()->setValue(Setting::Id::EnableAiCommitMessages, checked); @@ -820,9 +823,9 @@ class MiscPanel : public QWidget { QLineEdit *aiApiKeyBox = new QLineEdit(this); // Load API key from credential helper using the API URL as key - QString aiApiKey; CredentialHelper *credHelper = CredentialHelper::instance(); if (credHelper && !aiServiceUrl.isEmpty()) { + QString aiApiKey; credHelper->get(aiServiceUrl, aiApiKey, aiApiKey); if (!aiApiKey.isEmpty()) { aiApiKeyBox->setText(tr("Api key found for specified api url")); @@ -885,15 +888,8 @@ class MiscPanel : public QWidget { QTextEdit *aiCommitSystemMessageBox = new QTextEdit(this); aiCommitSystemMessageBox->setPlainText( settings - ->value( - Setting::Id::AiCommitSystemMessage, - QObject::tr( - "You are a helpful assistant that generates concise git " - "commit messages following conventional commits format. " - "Start directly with the commit message without any " - "prefixes like 'Commit Message:', 'Git', or markdown code " - "blocks. " - "Follow the format: type(scope): subject\n\nbody")) + ->value(Setting::Id::AiCommitSystemMessage, + QObject::tr(AiCommitConstants::DefaultSystemMessage)) .toString()); aiCommitSystemMessageBox->setVisible(false); connect(aiCommitSystemMessageBox, &QTextEdit::textChanged, diff --git a/src/ui/CommitEditor.cpp b/src/ui/CommitEditor.cpp index 6a676194a..fefab6c70 100644 --- a/src/ui/CommitEditor.cpp +++ b/src/ui/CommitEditor.cpp @@ -51,7 +51,8 @@ void debugWriteJsonObject(const QJsonObject &obj, const QString &filename) { } void prepareAiCommitRequest(const git::Diff &diff, Settings *settings, - QJsonObject &requestBody, const QString ¤tMessage = QString()) { + QJsonObject &requestBody, + const QString ¤tMessage = QString()) { // Use configurable prompt message from settings QString promptMessage = settings ->value(Setting::Id::AiCommitPromptMessage, @@ -212,9 +213,11 @@ void prepareAiCommitRequest(const git::Diff &diff, Settings *settings, requestBody["messages"] = messages; // Debug: Write JSON to file for inspection -debugWriteJsonObject(requestBody, - QStandardPaths::writableLocation(QStandardPaths::TempLocation) + -"/gittyup_ai_commit_request.json"); + if (false) { + debugWriteJsonObject(requestBody, QStandardPaths::writableLocation( + QStandardPaths::TempLocation) + + "/gittyup_ai_commit_request.json"); + } } } // namespace @@ -702,7 +705,11 @@ CommitEditor::CommitEditor(const git::Repository &repo, QWidget *parent) mGenerateCommitMessage->setObjectName("GenerateCommitMessage"); connect(mGenerateCommitMessage, &QPushButton::clicked, this, &CommitEditor::generateCommitMessage); - mGenerateCommitMessage->setVisible(Settings::instance()->value(Setting::Id::EnableAiCommitMessages, AiCommitConstants::enabled).toBool()); + mGenerateCommitMessage->setVisible( + Settings::instance() + ->value(Setting::Id::EnableAiCommitMessages, + AiCommitConstants::enabled) + .toBool()); mNetworkManager = new QNetworkAccessManager(this); @@ -1026,8 +1033,10 @@ void CommitEditor::updateButtons(bool yieldFocus) { void CommitEditor::generateCommitMessage() { // Check if AI commit messages are enabled Settings *settings = Settings::instance(); - bool aiEnabled = - settings->value(Setting::Id::EnableAiCommitMessages, AiCommitConstants::enabled).toBool(); + bool aiEnabled = settings + ->value(Setting::Id::EnableAiCommitMessages, + AiCommitConstants::enabled) + .toBool(); if (!aiEnabled) { QString errorMsg = tr("AI commit message generation is disabled. Please " From 2a9c35ea94b1151d25b6da8cf8751e314eacc591 Mon Sep 17 00:00:00 2001 From: Martin Marmsoler Date: Wed, 29 Apr 2026 14:32:46 +0200 Subject: [PATCH 3/3] use Result to show error message --- src/cred/Cache.cpp | 18 +- src/cred/Cache.h | 7 +- src/cred/CredentialHelper.h | 27 ++- src/cred/GitCredential.cpp | 294 +++++++++++++++++---------------- src/cred/GitCredential.h | 62 +++---- src/cred/Store.cpp | 30 ++-- src/cred/Store.h | 12 +- src/dialogs/SettingsDialog.cpp | 17 +- src/host/Account.cpp | 6 +- src/ui/CommitEditor.cpp | 5 +- src/ui/RemoteCallbacks.cpp | 3 +- test/store.cpp | 14 +- 12 files changed, 279 insertions(+), 216 deletions(-) diff --git a/src/cred/Cache.cpp b/src/cred/Cache.cpp index bf1c08539..8d113c4c5 100644 --- a/src/cred/Cache.cpp +++ b/src/cred/Cache.cpp @@ -11,26 +11,28 @@ Cache::Cache() {} -bool Cache::get(const QString &url, QString &username, QString &password) { +CredentialHelper::Result Cache::get(const QString &url, QString &username, + QString &password) { if (!mCache.contains(url)) - return false; + return CredentialHelper::Result::ERROR(QStringLiteral("")); const QMap &map = mCache[url]; if (map.isEmpty()) - return false; + return CredentialHelper::Result::ERROR(QStringLiteral("")); if (username.isEmpty()) username = map.keys().first(); if (!map.contains(username)) - return false; + return CredentialHelper::Result::ERROR(QStringLiteral("")); password = map.value(username); - return true; + return CredentialHelper::Result::OK(); } -bool Cache::store(const QString &url, const QString &username, - const QString &password) { +CredentialHelper::Result Cache::store(const QString &url, + const QString &username, + const QString &password) { mCache[url][username] = password; - return true; + return CredentialHelper::Result::OK(); } diff --git a/src/cred/Cache.h b/src/cred/Cache.h index 6ab9d39d2..f53c8453e 100644 --- a/src/cred/Cache.h +++ b/src/cred/Cache.h @@ -17,10 +17,11 @@ class Cache : public CredentialHelper { public: Cache(); - bool get(const QString &url, QString &username, QString &password) override; + CredentialHelper::Result get(const QString &url, QString &username, + QString &password) override; - bool store(const QString &url, const QString &username, - const QString &password) override; + CredentialHelper::Result store(const QString &url, const QString &username, + const QString &password) override; private: QMap> mCache; diff --git a/src/cred/CredentialHelper.h b/src/cred/CredentialHelper.h index 4096f1eb2..0faabb720 100644 --- a/src/cred/CredentialHelper.h +++ b/src/cred/CredentialHelper.h @@ -18,14 +18,33 @@ // securely storing passwords associated with host accounts. class CredentialHelper : public QObject { public: + struct Result { + bool success; + QString error; + + static Result OK() { + return Result{ + .success = true, + .error = QString(), + }; + } + + static Result ERROR(const QString &error) { + return Result{ + .success = false, + .error = error, + }; + } + }; + // Username can be supplied by the caller to lookup a specific // account or filled in by the helper to get the first account // for the given host. Password will be filled in on success. - virtual bool get(const QString &url, QString &username, - QString &password) = 0; + virtual Result get(const QString &url, QString &username, + QString &password) = 0; - virtual bool store(const QString &url, const QString &username, - const QString &password) = 0; + virtual Result store(const QString &url, const QString &username, + const QString &password) = 0; // Get the correct helper for the current platform. static CredentialHelper *instance(); diff --git a/src/cred/GitCredential.cpp b/src/cred/GitCredential.cpp index 43db5c700..b26230d89 100644 --- a/src/cred/GitCredential.cpp +++ b/src/cred/GitCredential.cpp @@ -1,139 +1,155 @@ -// -// Copyright (c) 2018, Scientific Toolworks, Inc. -// -// This software is licensed under the MIT License. The LICENSE.md file -// describes the conditions under which this software may be distributed. -// -// Author: Jason Haslam -// - -#include "GitCredential.h" -#include "qtsupport.h" -#include -#include -#include -#include -#include -#include - -namespace { - -QString host(const QString &url) { - QString host = QUrl(url).host(); - if (!host.isEmpty()) - return host; - - // Extract hostname from SSH URL. - int end = url.indexOf(':'); - int begin = url.indexOf('@') + 1; - return url.mid(begin, end - begin); -} - -QString protocol(const QString &url) { - QString scheme = QUrl(url).scheme(); - return !scheme.isEmpty() ? scheme : "ssh"; -} - -} // namespace - -GitCredential::GitCredential(const QString &name) : mName(name) {} - -bool GitCredential::get(const QString &url, QString &username, - QString &password) { - QProcess process; - process.start(command(), {"get"}); - if (!process.waitForStarted()) - return false; - - QTextStream out(&process); - out << "protocol=" << protocol(url) << Qt::endl; - out << "host=" << host(url) << Qt::endl; - if (!username.isEmpty()) - out << "username=" << username << Qt::endl; - out << Qt::endl; - - process.closeWriteChannel(); - process.waitForFinished(); - - QString output = process.readAllStandardOutput(); - foreach (const QString &line, output.split('\n')) { - int pos = line.indexOf('='); - if (pos < 0) - continue; - - QString key = line.left(pos); - QString value = line.mid(pos + 1); - if (key == "username") { - username = value; - } else if (key == "password") { - password = value; - } - } - - return !username.isEmpty() && !password.isEmpty(); -} - -bool GitCredential::store(const QString &url, const QString &username, - const QString &password) { - QProcess process; - process.start(command(), {"store"}); - if (!process.waitForStarted()) - return false; - - QTextStream out(&process); - out << "protocol=" << protocol(url) << Qt::endl; - out << "host=" << host(url) << Qt::endl; - out << "username=" << username << Qt::endl; - out << "password=" << password << Qt::endl; - out << Qt::endl; - - process.closeWriteChannel(); - process.waitForFinished(); - - return true; -} - -QString GitCredential::command() const { - QString name = QString("git-credential-%1").arg(mName); - QDir appDir = QCoreApplication::applicationDirPath(); - appDir.cd("credential-helpers"); - - // Prefer credential helpers directly installed into Gittyup's app dir - QString candidate = - QStandardPaths::findExecutable(name, QStringList(appDir.path())); - if (!candidate.isEmpty()) { - return candidate; - } - - candidate = QStandardPaths::findExecutable(name); - if (!candidate.isEmpty()) { - return candidate; - } - -#ifdef Q_OS_WIN - // Look for GIT CLI installation path - QString gitPath = QStandardPaths::findExecutable("git"); - if (!gitPath.isEmpty()) { - QDir gitDir = QFileInfo(gitPath).dir(); - if (gitDir.dirName() == "cmd" || gitDir.dirName() == "bin") { - gitDir.cdUp(); - -#ifdef Q_OS_WIN64 - gitDir.cd("mingw64"); -#else - gitDir.cd("mingw32"); -#endif - - gitDir.cd("bin"); - - candidate = - QStandardPaths::findExecutable(name, QStringList(gitDir.path())); - if (!candidate.isEmpty()) { - return candidate; - } - } - } -#endif - - return name; -} +// +// Copyright (c) 2018, Scientific Toolworks, Inc. +// +// This software is licensed under the MIT License. The LICENSE.md file +// describes the conditions under which this software may be distributed. +// +// Author: Jason Haslam +// + +#include "GitCredential.h" +#include "qtsupport.h" +#include +#include +#include +#include +#include +#include + +namespace { + +QString host(const QString &url) { + QString host = QUrl(url).host(); + if (!host.isEmpty()) + return host; + + // Extract hostname from SSH URL. + int end = url.indexOf(':'); + int begin = url.indexOf('@') + 1; + return url.mid(begin, end - begin); +} + +QString protocol(const QString &url) { + QString scheme = QUrl(url).scheme(); + return !scheme.isEmpty() ? scheme : "ssh"; +} + +} // namespace + +GitCredential::GitCredential(const QString &name) : mName(name) {} + +CredentialHelper::Result +GitCredential::get(const QString &url, QString &username, QString &password) { + QProcess process; + process.start(command(), {"get"}); + if (!process.waitForStarted()) + return CredentialHelper::Result::ERROR( + QStringLiteral("Failed to start credential process")); + + QTextStream out(&process); + out << "protocol=" << protocol(url) << Qt::endl; + out << "host=" << host(url) << Qt::endl; + if (!username.isEmpty()) + out << "username=" << username << Qt::endl; + out << Qt::endl; + + process.closeWriteChannel(); + process.waitForFinished(); + + QString output = process.readAllStandardOutput(); + foreach (const QString &line, output.split('\n')) { + int pos = line.indexOf('='); + if (pos < 0) + continue; + + QString key = line.left(pos); + QString value = line.mid(pos + 1); + if (key == "username") { + username = value; + } else if (key == "password") { + password = value; + } + } + + if (username.isEmpty() || password.isEmpty()) + return CredentialHelper::Result::ERROR( + QStringLiteral("Missing username or password in response")); + + return CredentialHelper::Result::OK(); +} + +CredentialHelper::Result GitCredential::store(const QString &url, + const QString &username, + const QString &password) { + QProcess process; + process.start(command(), {"store"}); + if (!process.waitForStarted()) + return CredentialHelper::Result::ERROR( + QStringLiteral("Failed to start credential process")); + + QTextStream out(&process); + out << "protocol=" << protocol(url) << Qt::endl; + out << "host=" << host(url) << Qt::endl; + out << "username=" << username << Qt::endl; + out << "password=" << password << Qt::endl; + out << Qt::endl; + + process.closeWriteChannel(); + process.waitForFinished(); + + if (process.exitStatus() != QProcess::ExitStatus::NormalExit) { + return CredentialHelper::Result::ERROR(process.readAllStandardError()); + } + + return CredentialHelper::Result::OK(); +} + +QString GitCredential::command() const { + QString name = QString("git-credential-%1").arg(mName); + QDir appDir = QCoreApplication::applicationDirPath(); + appDir.cd("credential-helpers"); + + // Prefer credential helpers directly installed into Gittyup's app dir + QString candidate = + QStandardPaths::findExecutable(name, QStringList(appDir.path())); + if (!candidate.isEmpty()) { + return candidate; + } + + candidate = QStandardPaths::findExecutable(name); + if (!candidate.isEmpty()) { + return candidate; + } + +#ifdef Q_OS_WIN + // Look for GIT CLI installation path + QString gitPath = QStandardPaths::findExecutable("git"); + if (!gitPath.isEmpty()) { + QDir gitDir = QFileInfo(gitPath).dir(); + if (gitDir.dirName() == "cmd" || gitDir.dirName() == "bin") { + gitDir.cdUp(); + +#ifdef Q_OS_WIN64 + gitDir.cd("mingw64"); +#else + gitDir.cd("mingw32"); +#endif + + gitDir.cd("bin"); + + candidate = + QStandardPaths::findExecutable(name, QStringList(gitDir.path())); + if (!candidate.isEmpty()) { + return candidate; + } + } + } +#endif + + return name; +} + +QString GitCredential::lastError() const { + // TODO: implement + return QString(); +} diff --git a/src/cred/GitCredential.h b/src/cred/GitCredential.h index 721ca35e6..da3c79ada 100644 --- a/src/cred/GitCredential.h +++ b/src/cred/GitCredential.h @@ -1,30 +1,32 @@ -// -// Copyright (c) 2018, Scientific Toolworks, Inc. -// -// This software is licensed under the MIT License. The LICENSE.md file -// describes the conditions under which this software may be distributed. -// -// Author: Jason Haslam -// - -#ifndef GITCREDENTIAL_H -#define GITCREDENTIAL_H - -#include "CredentialHelper.h" - -class GitCredential : public CredentialHelper { -public: - GitCredential(const QString &name); - - bool get(const QString &url, QString &username, QString &password) override; - - bool store(const QString &url, const QString &username, - const QString &password) override; - -private: - QString command() const; - - QString mName; -}; - -#endif +// +// Copyright (c) 2018, Scientific Toolworks, Inc. +// +// This software is licensed under the MIT License. The LICENSE.md file +// describes the conditions under which this software may be distributed. +// +// Author: Jason Haslam +// + +#ifndef GITCREDENTIAL_H +#define GITCREDENTIAL_H + +#include "CredentialHelper.h" + +class GitCredential : public CredentialHelper { +public: + GitCredential(const QString &name); + + CredentialHelper::Result get(const QString &url, QString &username, + QString &password) override; + + CredentialHelper::Result store(const QString &url, const QString &username, + const QString &password) override; + QString lastError() const; + +private: + QString command() const; + + QString mName; +}; + +#endif diff --git a/src/cred/Store.cpp b/src/cred/Store.cpp index 65f1f8496..afb651276 100644 --- a/src/cred/Store.cpp +++ b/src/cred/Store.cpp @@ -54,37 +54,47 @@ QMap>> Store::readCredFile() { return store; } -bool Store::extractUserPass(const QMap &map, - QString &username, QString &password) { +CredentialHelper::Result +Store::extractUserPass(const QMap &map, QString &username, + QString &password) { if (map.isEmpty()) - return false; + return CredentialHelper::Result::ERROR( + QStringLiteral("Empty credential map")); if (username.isEmpty()) username = map.keys().first(); if (!map.contains(username)) - return false; + return CredentialHelper::Result::ERROR( + QStringLiteral("Username not found")); password = map.value(username); - return !username.isEmpty() && !password.isEmpty(); + if (username.isEmpty() || password.isEmpty()) + return CredentialHelper::Result::ERROR( + QStringLiteral("Missing username or password")); + + return CredentialHelper::Result::OK(); } -bool Store::get(const QString &url, QString &username, QString &password) { +CredentialHelper::Result Store::get(const QString &url, QString &username, + QString &password) { auto store = readCredFile(); const QMap &map = store[protocol(url)][host(url)]; return extractUserPass(map, username, password); } -bool Store::store(const QString &url, const QString &username, - const QString &password) { +CredentialHelper::Result Store::store(const QString &url, + const QString &username, + const QString &password) { auto store = readCredFile(); store[protocol(url)][host(url)][username] = password; QFile file(mPath); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) - return false; + return CredentialHelper::Result::ERROR( + QStringLiteral("Failed to open credential file for writing")); foreach (const auto &protocolKey, store.keys()) { auto protocol = store[protocolKey]; @@ -105,7 +115,7 @@ bool Store::store(const QString &url, const QString &username, file.close(); - return true; + return CredentialHelper::Result::OK(); } QString Store::command() const { return ""; } diff --git a/src/cred/Store.h b/src/cred/Store.h index 3f4e759e1..cbb37f1f5 100644 --- a/src/cred/Store.h +++ b/src/cred/Store.h @@ -23,16 +23,18 @@ class Store : public CredentialHelper { public: Store(const QString &path); - bool get(const QString &url, QString &username, QString &password) override; + CredentialHelper::Result get(const QString &url, QString &username, + QString &password) override; - bool store(const QString &url, const QString &username, - const QString &password) override; + CredentialHelper::Result store(const QString &url, const QString &username, + const QString &password) override; private: QString command() const; QMap>> readCredFile(); - bool extractUserPass(const QMap &map, QString &username, - QString &password); + CredentialHelper::Result extractUserPass(const QMap &map, + QString &username, + QString &password); QString mPath; }; diff --git a/src/dialogs/SettingsDialog.cpp b/src/dialogs/SettingsDialog.cpp index 6bd1d4b1f..78456e16c 100644 --- a/src/dialogs/SettingsDialog.cpp +++ b/src/dialogs/SettingsDialog.cpp @@ -9,6 +9,7 @@ #include "SettingsDialog.h" #include "AboutDialog.h" +#include "Debug.h" #include "DiffPanel.h" #include "ExternalToolsDialog.h" #include "HotkeysPanel.h" @@ -826,8 +827,8 @@ class MiscPanel : public QWidget { CredentialHelper *credHelper = CredentialHelper::instance(); if (credHelper && !aiServiceUrl.isEmpty()) { QString aiApiKey; - credHelper->get(aiServiceUrl, aiApiKey, aiApiKey); - if (!aiApiKey.isEmpty()) { + auto result = credHelper->get(aiServiceUrl, aiApiKey, aiApiKey); + if (result.success && !aiApiKey.isEmpty()) { aiApiKeyBox->setText(tr("Api key found for specified api url")); } } @@ -838,19 +839,21 @@ class MiscPanel : public QWidget { QString aiServiceUrl = aiServiceUrlBox->text(); CredentialHelper *credHelper = CredentialHelper::instance(); if (credHelper && !apiKey.isEmpty() && !aiServiceUrl.isEmpty()) { - bool success = + auto result = credHelper->store(aiServiceUrl, "ai-commit-key", apiKey); - if (!success) { + if (!result.success) { QMessageBox::warning( aiApiKeyBox, QObject::tr("Credential Storage Failed"), QObject::tr("Failed to store API key in secure storage. The " - "key will not be saved.")); + "key will not be saved: ") + + result.error); } } else if (credHelper && apiKey.isEmpty() && !aiServiceUrl.isEmpty()) { // If key is empty, remove it from storage - bool success = credHelper->store(aiServiceUrl, "ai-commit-key", ""); - if (!success) { + Debug("Remove key from storage"); + auto result = credHelper->store(aiServiceUrl, "ai-commit-key", ""); + if (!result.success) { QMessageBox::warning( aiApiKeyBox, QObject::tr("Credential Removal Failed"), QObject::tr("Failed to remove API key from secure storage.")); diff --git a/src/host/Account.cpp b/src/host/Account.cpp index 1abbac572..da26ccfe4 100644 --- a/src/host/Account.cpp +++ b/src/host/Account.cpp @@ -49,7 +49,11 @@ QString Account::password() const { QString password; QString name = username(); - CredentialHelper::instance()->get(url.toString(), name, password); + auto result = + CredentialHelper::instance()->get(url.toString(), name, password); + if (!result.success) { + return QString(); + } return password; } diff --git a/src/ui/CommitEditor.cpp b/src/ui/CommitEditor.cpp index fefab6c70..b5ad6782d 100644 --- a/src/ui/CommitEditor.cpp +++ b/src/ui/CommitEditor.cpp @@ -1075,7 +1075,10 @@ void CommitEditor::generateCommitMessage() { QString apiKey; CredentialHelper *credHelper = CredentialHelper::instance(); if (credHelper) { - credHelper->get(apiUrl, apiKey, apiKey); + auto result = credHelper->get(apiUrl, apiKey, apiKey); + if (!result.success) { + apiKey.clear(); + } } if (apiKey.isEmpty()) { diff --git a/src/ui/RemoteCallbacks.cpp b/src/ui/RemoteCallbacks.cpp index d8e7d6fa1..4fa74652a 100644 --- a/src/ui/RemoteCallbacks.cpp +++ b/src/ui/RemoteCallbacks.cpp @@ -281,7 +281,8 @@ bool RemoteCallbacks::connectToAgent() const { void RemoteCallbacks::credentialsImpl(const QString &url, QString &username, QString &password, QString &error) { CredentialHelper *helper = CredentialHelper::instance(); - if (helper->get(url, username, password)) { + auto result = helper->get(url, username, password); + if (result.success) { QStringList key({url, username, password}); if (!mQueriedCredentials.contains(key)) { mQueriedCredentials.insert(key); diff --git a/test/store.cpp b/test/store.cpp index 069ff0da0..65bbb2cb7 100644 --- a/test/store.cpp +++ b/test/store.cpp @@ -43,7 +43,7 @@ void TestStore::readUserPassTestCase() { QString password; auto result = store.get(url, username, password); - QVERIFY(result); + QVERIFY(result.success); QCOMPARE(username, "janeDoe"); QCOMPARE(password, "securePassword"); } @@ -56,7 +56,7 @@ void TestStore::wrongProtocolTestCase() { QString password; auto result = store.get(url, username, password); - QVERIFY(!result); + QVERIFY(!result.success); } void TestStore::wrongUrlTestCase() { @@ -67,7 +67,7 @@ void TestStore::wrongUrlTestCase() { QString password; auto result = store.get(url, username, password); - QVERIFY(!result); + QVERIFY(!result.success); } void TestStore::wrongUsernameTestCase() { @@ -78,7 +78,7 @@ void TestStore::wrongUsernameTestCase() { QString password; auto result = store.get(url, username, password); - QVERIFY(!result); + QVERIFY(!result.success); } void TestStore::saveUserPassTestCase() { @@ -89,13 +89,13 @@ void TestStore::saveUserPassTestCase() { QString password = "NewPassword"; auto result = store.store(url, username, password); - QVERIFY(result); + QVERIFY(result.success); QString readBackUsername; QString readBackpassword; auto result2 = store.get(url, readBackUsername, readBackpassword); - QVERIFY(result2); + QVERIFY(result2.success); QCOMPARE(readBackUsername, "NewUser"); QCOMPARE(readBackpassword, "NewPassword"); } @@ -108,7 +108,7 @@ void TestStore::wrongFilePathTestCase() { QString password = ""; auto result = store.get(url, username, password); - QVERIFY(!result); + QVERIFY(!result.success); } void TestStore::cleanupTestCase() {