Use Kate's native diff viewer, add per-file and full changeset diff
- Integrate with libkateprivate's Utils::showDiff() to use Kate's built-in diff widget (respects user's Unified/SideBySide/Raw style preference) - Add bundled kate-compat headers for DiffParams and Utils::showDiff decl - CMakeLists.txt auto-detects libkateprivate, falls back to plain text diff - Add 'Diff selected' button: shows diff for selected files with stage/unstage/discard actions available in the diff viewer - Add 'Diff All' button: shows git diff HEAD for full changeset review - Staged diffs show unstage action, unstaged diffs show stage+discard - ReloadOnShow flag keeps diff views fresh after interactive operations
This commit is contained in:
parent
163c3d18a9
commit
ac8980a203
|
|
@ -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()
|
||||
|
|
|
|||
205
gitplusplus.cpp
205
gitplusplus.cpp
|
|
@ -2,6 +2,10 @@
|
|||
#include "gitprocess.h"
|
||||
#include "gitstatusmodel.h"
|
||||
|
||||
#ifdef HAVE_KATE_DIFF
|
||||
#include "ktexteditor_utils.h"
|
||||
#endif
|
||||
|
||||
#include <KPluginFactory>
|
||||
#include <KLocalizedString>
|
||||
#include <ktexteditor_version.h>
|
||||
|
|
@ -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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
Minimal copy of Kate's DiffParams for external plugin use.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <functional>
|
||||
|
||||
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<void()> updateStatusCallback;
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
Minimal declaration of Utils::showDiff for external plugin use.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "diffparams.h"
|
||||
#include <QByteArray>
|
||||
|
||||
namespace KTextEditor { class MainWindow; }
|
||||
|
||||
namespace Utils {
|
||||
void showDiff(const QByteArray &diff, const DiffParams ¶ms, KTextEditor::MainWindow *mainWindow);
|
||||
}
|
||||
Loading…
Reference in New Issue