diff --git a/CMakeLists.txt b/CMakeLists.txt index f7ee3f2..150db9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_AUTOMOC ON) find_package(ECM 5.80 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) -# Probe for KF6 before including KDE macros (which need QT_MAJOR_VERSION) +# --- Detect Qt6/KF6 or fall back to Qt5/KF5 --- find_package(KF6TextEditor QUIET CONFIG) if(KF6TextEditor_FOUND) set(QT_MAJOR_VERSION 6) @@ -47,6 +47,18 @@ else() message(STATUS "Building Git++ with KF5/Qt5") endif() +# --- Find libkateprivate for diff widget support --- +# The library may only exist as a versioned .so (no dev symlink) +file(GLOB _KATEPRIVATE_CANDIDATES "/usr/lib64/libkateprivate.so*" "/usr/lib/libkateprivate.so*") +if(_KATEPRIVATE_CANDIDATES) + list(GET _KATEPRIVATE_CANDIDATES 0 KATEPRIVATE_LIB) + message(STATUS "Found libkateprivate: ${KATEPRIVATE_LIB}") + add_definitions(-DHAVE_KATE_DIFF) +else() + set(KATEPRIVATE_LIB "") + message(STATUS "libkateprivate not found — diff will open as plain text") +endif() + kcoreaddons_add_plugin(gitplusplus SOURCES gitplusplus.cpp @@ -55,5 +67,9 @@ kcoreaddons_add_plugin(gitplusplus ) target_link_libraries(gitplusplus ${KF_LINK_LIBS}) - target_include_directories(gitplusplus PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +if(KATEPRIVATE_LIB) + target_include_directories(gitplusplus PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/kate-compat) + target_link_libraries(gitplusplus ${KATEPRIVATE_LIB}) +endif() diff --git a/gitplusplus.cpp b/gitplusplus.cpp index 3958f13..cb3843a 100644 --- a/gitplusplus.cpp +++ b/gitplusplus.cpp @@ -2,6 +2,10 @@ #include "gitprocess.h" #include "gitstatusmodel.h" +#ifdef HAVE_KATE_DIFF +#include "ktexteditor_utils.h" +#endif + #include #include #include @@ -139,6 +143,8 @@ GitPlusPlusView::GitPlusPlusView(GitPlusPlusPlugin *plugin, KTextEditor::MainWin m_stageBtn = makeToolButton(QStringLiteral("list-add"), QStringLiteral("Stage")); m_unstageBtn = makeToolButton(QStringLiteral("list-remove"), QStringLiteral("Unstage")); m_discardBtn = makeToolButton(QStringLiteral("edit-delete"), QStringLiteral("Discard")); + auto *diffBtn = makeToolButton(QStringLiteral("vcs-diff"), QStringLiteral("Diff selected (or all)")); + auto *diffAllBtn = makeToolButton(QStringLiteral("view-split-left-right"), QStringLiteral("Diff entire changeset")); m_commitBtn = makeToolButton(QStringLiteral("vcs-commit"), QStringLiteral("Commit")); m_pushBtn = makeToolButton(QStringLiteral("vcs-push"), QStringLiteral("Push")); m_pullBtn = makeToolButton(QStringLiteral("vcs-pull"), QStringLiteral("Pull")); @@ -150,6 +156,8 @@ GitPlusPlusView::GitPlusPlusView(GitPlusPlusPlugin *plugin, KTextEditor::MainWin toolbar->addWidget(m_stageBtn); toolbar->addWidget(m_unstageBtn); toolbar->addWidget(m_discardBtn); + toolbar->addWidget(diffBtn); + toolbar->addWidget(diffAllBtn); toolbar->addWidget(m_commitBtn); toolbar->addWidget(m_pushBtn); toolbar->addWidget(m_pullBtn); @@ -317,6 +325,8 @@ GitPlusPlusView::GitPlusPlusView(GitPlusPlusPlugin *plugin, KTextEditor::MainWin connect(m_stageBtn, &QToolButton::clicked, this, &GitPlusPlusView::stageSelected); connect(m_unstageBtn, &QToolButton::clicked, this, &GitPlusPlusView::unstageSelected); connect(m_discardBtn, &QToolButton::clicked, this, &GitPlusPlusView::discardSelected); + connect(diffBtn, &QToolButton::clicked, this, &GitPlusPlusView::diffSelected); + connect(diffAllBtn, &QToolButton::clicked, this, &GitPlusPlusView::diffAll); connect(m_commitBtn, &QToolButton::clicked, this, &GitPlusPlusView::commitChanges); connect(m_pushBtn, &QToolButton::clicked, this, &GitPlusPlusView::push); connect(m_pullBtn, &QToolButton::clicked, this, &GitPlusPlusView::pull); @@ -467,6 +477,28 @@ void GitPlusPlusView::setMessage(const QString &text, bool isError) m_statusLabel->setStyleSheet(isError ? QStringLiteral("color: red;") : QStringLiteral("color: green;")); } +void GitPlusPlusView::showDiffOutput(const QByteArray &diff, const QString &title, const QStringList &gitArgs, int extraFlags) +{ +#ifdef HAVE_KATE_DIFF + DiffParams params; + params.tabTitle = title; + params.workingDir = m_gitDir; + params.arguments = gitArgs; + params.flags = DiffParams::Flag(DiffParams::ShowFileName | extraFlags); + params.updateStatusCallback = [this]() { updateStatus(); }; + Utils::showDiff(diff, params, m_mainWindow); +#else + Q_UNUSED(title); + Q_UNUSED(gitArgs); + Q_UNUSED(extraFlags); + auto *view = m_mainWindow->openUrl(QUrl()); + if (view) { + view->document()->setText(QString::fromUtf8(diff)); + view->document()->setHighlightingMode(QStringLiteral("Diff")); + } +#endif +} + void GitPlusPlusView::refreshAll() { updateStatus(); @@ -872,6 +904,147 @@ void GitPlusPlusView::discardSelected() QStringLiteral("Failed to discard")); } +void GitPlusPlusView::diffSelected() +{ + // Get all selected file entries (from any section) + QStringList stagedFiles; + QStringList unstagedFiles; + const auto indexes = m_treeView->selectionModel()->selectedIndexes(); + for (const auto &proxyIdx : indexes) { + QModelIndex idx = m_proxy->mapToSource(proxyIdx); + if (idx.internalId() == 0xFFFFFFFF) + continue; // skip top-level category nodes + int nodeType = int(idx.internalId()); + QString file = idx.data(GitStatusModel::FilePathRole).toString(); + if (file.isEmpty()) + continue; + if (nodeType == GitStatusModel::NodeStaged) + stagedFiles.append(file); + else if (nodeType == GitStatusModel::NodeChanged) + unstagedFiles.append(file); + // Untracked files: show as new file diff + else if (nodeType == GitStatusModel::NodeUntracked) + unstagedFiles.append(file); + } + + // If nothing selected, fall through to diffAll + if (stagedFiles.isEmpty() && unstagedFiles.isEmpty()) { + diffAll(); + return; + } + + // Show staged diff for selected staged files + if (!stagedFiles.isEmpty()) { + QStringList args = {QStringLiteral("diff"), QStringLiteral("--cached"), QStringLiteral("--")}; + args.append(stagedFiles); + auto *proc = gitProcess(args); + if (!proc) return; + connect(proc, QOverload::of(&QProcess::finished), + this, [this, proc, stagedFiles, args](int exitCode, QProcess::ExitStatus) { + if (exitCode == 0) { + QByteArray output = proc->readAllStandardOutput(); + if (!output.trimmed().isEmpty()) { + QString title = stagedFiles.size() == 1 + ? QStringLiteral("Staged: %1").arg(stagedFiles.first()) + : QStringLiteral("Staged changes (%1 files)").arg(stagedFiles.size()); +#ifdef HAVE_KATE_DIFF + showDiffOutput(output, title, args, + DiffParams::ShowUnstage | DiffParams::ReloadOnShow); +#else + showDiffOutput(output, title, args); +#endif + } else { + setMessage(QStringLiteral("No staged diff for selection")); + } + } + proc->deleteLater(); + }); + proc->start(); + } + + // Show unstaged diff for selected changed/untracked files + if (!unstagedFiles.isEmpty()) { + QStringList args = {QStringLiteral("diff"), QStringLiteral("--")}; + args.append(unstagedFiles); + auto *proc = gitProcess(args); + if (!proc) return; + connect(proc, QOverload::of(&QProcess::finished), + this, [this, proc, unstagedFiles, args](int exitCode, QProcess::ExitStatus) { + if (exitCode == 0) { + QByteArray output = proc->readAllStandardOutput(); + if (!output.trimmed().isEmpty()) { + QString title = unstagedFiles.size() == 1 + ? QStringLiteral("Unstaged: %1").arg(unstagedFiles.first()) + : QStringLiteral("Unstaged changes (%1 files)").arg(unstagedFiles.size()); +#ifdef HAVE_KATE_DIFF + showDiffOutput(output, title, args, + DiffParams::ShowStage | DiffParams::ShowDiscard | DiffParams::ReloadOnShow); +#else + showDiffOutput(output, title, args); +#endif + } else { + setMessage(QStringLiteral("No unstaged diff for selection")); + } + } + proc->deleteLater(); + }); + proc->start(); + } +} + +void GitPlusPlusView::diffAll() +{ + if (m_gitDir.isEmpty()) { + setMessage(QStringLiteral("No git directory found"), true); + return; + } + + // Show combined diff: staged + unstaged in one view + // Use "git diff HEAD" which shows all changes (staged and unstaged) relative to HEAD + QStringList args = {QStringLiteral("diff"), QStringLiteral("HEAD")}; + auto *proc = gitProcess(args); + if (!proc) return; + connect(proc, QOverload::of(&QProcess::finished), + this, [this, proc, args](int exitCode, QProcess::ExitStatus) { + if (exitCode == 0) { + QByteArray output = proc->readAllStandardOutput(); + if (output.trimmed().isEmpty()) { + setMessage(QStringLiteral("No changes to diff")); + } else { + showDiffOutput(output, QStringLiteral("All changes"), args); + setMessage(QStringLiteral("Showing all changes")); + } + } else { + // HEAD might not exist (initial commit) — fall back to staged + unstaged separately + QString err = QString::fromUtf8(proc->readAllStandardError()).trimmed(); + if (err.contains(QStringLiteral("unknown revision"))) { + // No commits yet — show diff of staged (index vs empty) + auto *proc2 = gitProcess({QStringLiteral("diff"), QStringLiteral("--cached")}); + if (proc2) { + connect(proc2, QOverload::of(&QProcess::finished), + this, [this, proc2](int exitCode2, QProcess::ExitStatus) { + if (exitCode2 == 0) { + QByteArray out2 = proc2->readAllStandardOutput(); + if (!out2.trimmed().isEmpty()) { + showDiffOutput(out2, QStringLiteral("All changes (initial commit)"), + {QStringLiteral("diff"), QStringLiteral("--cached")}); + } else { + setMessage(QStringLiteral("No changes to diff")); + } + } + proc2->deleteLater(); + }); + proc2->start(); + } + } else { + setMessage(QStringLiteral("Diff failed: ") + err, true); + } + } + proc->deleteLater(); + }); + proc->start(); +} + void GitPlusPlusView::commitChanges() { QString msg = m_commitMsgEdit->toPlainText().trimmed(); @@ -1046,16 +1219,12 @@ void GitPlusPlusView::compareBranch() connect(proc, QOverload::of(&QProcess::finished), this, [this, proc, branch](int exitCode, QProcess::ExitStatus) { if (exitCode == 0) { - QString diff = QString::fromUtf8(proc->readAllStandardOutput()); - if (diff.isEmpty()) { + QByteArray diff = proc->readAllStandardOutput(); + if (diff.trimmed().isEmpty()) { setMessage(QStringLiteral("No differences between %1 and %2").arg(m_currentBranch, branch)); } else { - // Open diff in a new untitled document - auto *view = m_mainWindow->openUrl(QUrl()); - if (view) { - view->document()->setText(diff); - view->document()->setHighlightingMode(QStringLiteral("Diff")); - } + showDiffOutput(diff, QStringLiteral("Diff: %1...%2").arg(m_currentBranch, branch), + {QStringLiteral("diff"), m_currentBranch + QStringLiteral("...") + branch}); setMessage(QStringLiteral("Comparing %1...%2").arg(m_currentBranch, branch)); } } else { @@ -1292,15 +1461,12 @@ void GitPlusPlusView::stashDiff() connect(proc, QOverload::of(&QProcess::finished), this, [this, proc, stash](int exitCode, QProcess::ExitStatus) { if (exitCode == 0) { - QString output = QString::fromUtf8(proc->readAllStandardOutput()); - if (output.isEmpty()) { + QByteArray output = proc->readAllStandardOutput(); + if (output.trimmed().isEmpty()) { setMessage(QStringLiteral("Empty stash diff")); } else { - auto *view = m_mainWindow->openUrl(QUrl()); - if (view) { - view->document()->setText(output); - view->document()->setHighlightingMode(QStringLiteral("Diff")); - } + showDiffOutput(output, QStringLiteral("Diff - %1").arg(stash), + {QStringLiteral("stash"), QStringLiteral("show"), QStringLiteral("-p"), stash}); setMessage(QStringLiteral("Diff: ") + stash); } } else { @@ -1344,12 +1510,9 @@ void GitPlusPlusView::openCommit() connect(proc, QOverload::of(&QProcess::finished), this, [this, proc, ref](int exitCode, QProcess::ExitStatus) { if (exitCode == 0) { - QString output = QString::fromUtf8(proc->readAllStandardOutput()); - auto *view = m_mainWindow->openUrl(QUrl()); - if (view) { - view->document()->setText(output); - view->document()->setHighlightingMode(QStringLiteral("Diff")); - } + QByteArray output = proc->readAllStandardOutput(); + showDiffOutput(output, QStringLiteral("Commit: %1").arg(ref), + {QStringLiteral("show"), QStringLiteral("--stat"), QStringLiteral("--patch"), ref}); setMessage(QStringLiteral("Showing commit: ") + ref); } else { QString err = QString::fromUtf8(proc->readAllStandardError()).trimmed(); diff --git a/gitplusplus.h b/gitplusplus.h index 102b929..5406a50 100644 --- a/gitplusplus.h +++ b/gitplusplus.h @@ -68,6 +68,8 @@ private Q_SLOTS: void unstageSelected(); void discardSelected(); void commitChanges(); + void diffSelected(); + void diffAll(); // Remote void push(); @@ -111,6 +113,7 @@ private: void runSimpleGit(const QStringList &args, const QString &successMsg, const QString &errorMsg); void runRemoteGit(const QStringList &args, const QString &opName); void setMessage(const QString &text, bool isError = false); + void showDiffOutput(const QByteArray &diff, const QString &title, const QStringList &gitArgs = {}, int extraFlags = 0); void updateIndicators(); void setRemoteBusy(bool busy, const QString &opName = {}); void refreshAll(); diff --git a/kate-compat/diffparams.h b/kate-compat/diffparams.h new file mode 100644 index 0000000..f7d618f --- /dev/null +++ b/kate-compat/diffparams.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2022 Waqar Ahmed + SPDX-License-Identifier: LGPL-2.0-or-later + + Minimal copy of Kate's DiffParams for external plugin use. +*/ +#pragma once + +#include +#include +#include +#include + +struct DiffParams { + enum Flag { + ShowStage = 1, + ShowUnstage = 2, + ShowDiscard = 4, + ShowFileName = 8, + ShowCommitInfo = 16, + ReloadOnShow = 32, + ShowFullContext = 64, + ShowEditLeftSide = 128, + ShowEditRightSide = 256 + }; + Q_DECLARE_FLAGS(Flags, Flag) + + QString tabTitle; + QString srcFile; + QString destFile; + QString workingDir; + QStringList arguments; + Flags flags; + + void clear() + { + tabTitle = srcFile = destFile = workingDir = QString(); + arguments.clear(); + flags = {}; + updateStatusCallback = nullptr; + } + + std::function updateStatusCallback; +}; diff --git a/kate-compat/ktexteditor_utils.h b/kate-compat/ktexteditor_utils.h new file mode 100644 index 0000000..a7b22f1 --- /dev/null +++ b/kate-compat/ktexteditor_utils.h @@ -0,0 +1,16 @@ +/* + SPDX-FileCopyrightText: 2022 Waqar Ahmed + SPDX-License-Identifier: LGPL-2.0-or-later + + Minimal declaration of Utils::showDiff for external plugin use. +*/ +#pragma once + +#include "diffparams.h" +#include + +namespace KTextEditor { class MainWindow; } + +namespace Utils { +void showDiff(const QByteArray &diff, const DiffParams ¶ms, KTextEditor::MainWindow *mainWindow); +}