Git++ Kate plugin: initial release
Full-featured Git integration for Kate with tabbed side panel: Status tab: - Live file status tree (staged/changed/untracked) - Stage, unstage, discard with double-click support - Commit with inline message editor and amend support - Push, pull, fetch with progress spinner - Filesystem watcher for auto-refresh Branches tab: - Filterable list of local and remote branches - Checkout, create, delete, compare - Create worktree from branch - Copy branch name to clipboard Stashes tab: - List all stashes with apply, pop, drop, diff Worktrees tab: - List active worktrees - Create (with branch picker), remove, prune - Open worktree as Kate project Indicators: - Panel header with repo/branch/dirty state - Status bar widget always visible - Detached HEAD warning - Remote operation progress
This commit is contained in:
commit
ab2d531240
|
|
@ -0,0 +1 @@
|
|||
/build/
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
project(gitplusplus VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
find_package(ECM REQUIRED NO_MODULE)
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Widgets)
|
||||
find_package(KF5TextEditor REQUIRED)
|
||||
find_package(KF5I18n REQUIRED)
|
||||
find_package(KF5CoreAddons REQUIRED)
|
||||
|
||||
kcoreaddons_add_plugin(gitplusplus
|
||||
SOURCES
|
||||
gitplusplus.cpp
|
||||
gitstatusmodel.cpp
|
||||
INSTALL_NAMESPACE "ktexteditor"
|
||||
)
|
||||
|
||||
target_link_libraries(gitplusplus
|
||||
KF5::TextEditor
|
||||
KF5::I18n
|
||||
Qt5::Core
|
||||
Qt5::Widgets
|
||||
)
|
||||
|
||||
target_include_directories(gitplusplus PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
.PHONY: all clean install
|
||||
|
||||
BUILD_DIR := build
|
||||
|
||||
all: $(BUILD_DIR)/gitplusplus.so
|
||||
|
||||
$(BUILD_DIR)/gitplusplus.so: gitplusplus.cpp gitplusplus.h gitprocess.h gitstatusmodel.cpp gitstatusmodel.h metadata.json
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@cd $(BUILD_DIR) && cmake .. -DCMAKE_INSTALL_PREFIX=/usr -Wno-dev 2>/dev/null && make -j$$(nproc)
|
||||
|
||||
install: all
|
||||
cd $(BUILD_DIR) && sudo make install
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
# Git++ Kate Plugin
|
||||
|
||||
A comprehensive Git integration plugin for the Kate text editor. Provides a
|
||||
tabbed side panel with status tracking, branch management, stash operations,
|
||||
and worktree support — all integrated with Kate's project module.
|
||||
|
||||
## Features
|
||||
|
||||
### Status Tab
|
||||
|
||||
- **Live file status** — tree view showing Staged, Changed, and Untracked files
|
||||
- **Stage / Unstage** — select files and click, or double-click to toggle
|
||||
- **Discard changes** — revert selected files (with confirmation)
|
||||
- **Commit** — inline message editor with amend support
|
||||
- **Amend** — checkbox loads last commit message for editing
|
||||
- **Push / Pull / Fetch** — with real-time progress spinner and git transfer output
|
||||
- **Stash** — quick-save button in the toolbar
|
||||
- **Open commit** — view any commit's diff in the editor
|
||||
- **Auto-refresh** — watches `.git/index` and `HEAD` for external changes
|
||||
|
||||
### Branches Tab
|
||||
|
||||
- **Filterable list** of all local and remote branches
|
||||
- **Checkout** — double-click or button; remote branches auto-create local tracking branch
|
||||
- **Create branch** — dialog shows source branch name
|
||||
- **Delete branch** — with confirmation
|
||||
- **Compare** — diff between selected branch and current, opens in editor with syntax highlighting
|
||||
- **Create worktree** — from any branch (see Worktrees below)
|
||||
- **Copy branch name** — right-click to clipboard
|
||||
|
||||
### Stashes Tab
|
||||
|
||||
- **List** of all stashes with their messages
|
||||
- **Save** — with optional message
|
||||
- **Apply** — applies selected stash (keeps it)
|
||||
- **Pop** — applies and removes selected stash
|
||||
- **Drop** — removes selected stash (with confirmation)
|
||||
- **Diff** — view stash contents in the editor
|
||||
|
||||
### Worktrees Tab
|
||||
|
||||
- **List** of all active worktrees with path, commit, and branch
|
||||
- **Create** — editable combo box lets you pick an existing branch or type a new one
|
||||
- **Remove** — deletes the worktree directory (with confirmation)
|
||||
- **Prune** — clean up stale worktree references
|
||||
- **Open project** — double-click to open worktree in Kate
|
||||
- **Copy path** — right-click to clipboard
|
||||
|
||||
### Indicators
|
||||
|
||||
- **Panel header** — shows repo name, branch, dirty state with color coding
|
||||
- Green `○` when clean
|
||||
- Orange `●` with file counts when dirty
|
||||
- Red `⚠ DETACHED` with dark background when HEAD is detached
|
||||
- **Status bar** — persistent branch + dirty indicator visible even when panel is hidden
|
||||
- **Remote operations** — animated spinner with progress streaming during push/pull/fetch
|
||||
|
||||
## Worktree Workflow
|
||||
|
||||
Git worktrees let you have multiple branches checked out simultaneously in
|
||||
separate directories, without needing multiple clones. Git++ makes this
|
||||
practical for daily use.
|
||||
|
||||
### How It Works
|
||||
|
||||
When you create a worktree from a branch, Git++ creates a sibling directory
|
||||
next to your main repo using the naming convention:
|
||||
|
||||
```
|
||||
{repo-name}.{branch-name}
|
||||
```
|
||||
|
||||
For example, if your repo is at:
|
||||
```
|
||||
~/projects/myproject/
|
||||
```
|
||||
|
||||
And you create a worktree for branch `ISSUE-123_my-feature`, it creates:
|
||||
```
|
||||
~/projects/myproject.ISSUE-123_my-feature/
|
||||
```
|
||||
|
||||
This convention means:
|
||||
- Worktrees are always next to the main repo (easy to find)
|
||||
- The name tells you both the repo and the branch
|
||||
- No collisions if you have worktrees from multiple repos in the same parent directory
|
||||
|
||||
### Recommended Workflow
|
||||
|
||||
1. **Start a feature** — go to the Branches tab, select the base branch,
|
||||
right-click → "Create worktree" (or use the Worktrees tab → Create button)
|
||||
2. **Work in isolation** — double-click the worktree entry to open it as a
|
||||
Kate project. Each worktree has its own working directory, so you can
|
||||
switch between features without stashing.
|
||||
3. **Review another branch** — create a worktree for a colleague's branch
|
||||
to review their code without disrupting your current work.
|
||||
4. **Clean up** — when done with a feature branch, remove the worktree from
|
||||
the Worktrees tab. The branch remains intact for merging.
|
||||
|
||||
### Kate Project Detection
|
||||
|
||||
Git++ automatically creates a `.kateproject` file in each new worktree so
|
||||
that Kate's project plugin recognizes it immediately. The project uses git
|
||||
for file listing, so the file tree shows exactly what git tracks.
|
||||
|
||||
## Building
|
||||
|
||||
Requires KF5 (or KF6) development packages:
|
||||
|
||||
```sh
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_INSTALL_PREFIX=/usr -Wno-dev
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Or use the provided Makefile:
|
||||
|
||||
```sh
|
||||
make install
|
||||
```
|
||||
|
||||
## Installation Path
|
||||
|
||||
The plugin installs to Kate's KTextEditor plugin directory:
|
||||
```
|
||||
/usr/lib/x86_64-linux-gnu/qt5/plugins/ktexteditor/gitplusplus.so
|
||||
```
|
||||
|
||||
Enable it in Kate → Settings → Configure Kate → Plugins → "Git++".
|
||||
|
||||
## Project Integration
|
||||
|
||||
Git++ reads the project base directory from Kate's project plugin
|
||||
(`kateprojectplugin`). When you switch projects or open a new one, all tabs
|
||||
refresh automatically. If no project is open, it falls back to the directory
|
||||
of the active file.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,180 @@
|
|||
#ifndef GITPLUSPLUS_H
|
||||
#define GITPLUSPLUS_H
|
||||
|
||||
#include <KTextEditor/Plugin>
|
||||
#include <KTextEditor/MainWindow>
|
||||
#include <KTextEditor/View>
|
||||
#include <ktexteditor_version.h>
|
||||
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
#include <KPluginMetaData>
|
||||
#endif
|
||||
|
||||
#include <QPointer>
|
||||
#include <QProcess>
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
|
||||
class GitPlusPlusView;
|
||||
class GitStatusModel;
|
||||
class QTreeView;
|
||||
class QListWidget;
|
||||
class QLineEdit;
|
||||
class QTextEdit;
|
||||
class QToolButton;
|
||||
class QLabel;
|
||||
class QComboBox;
|
||||
class QCheckBox;
|
||||
class QTabWidget;
|
||||
class QSortFilterProxyModel;
|
||||
class QStringListModel;
|
||||
class QFileSystemWatcher;
|
||||
|
||||
/**
|
||||
* Git++ Plugin - standalone KTextEditor plugin providing git operations.
|
||||
*/
|
||||
class GitPlusPlusPlugin : public KTextEditor::Plugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
explicit GitPlusPlusPlugin(QObject *parent, const KPluginMetaData &data, const QVariantList &args = {});
|
||||
#else
|
||||
explicit GitPlusPlusPlugin(QObject *parent, const QVariantList &args = {});
|
||||
#endif
|
||||
~GitPlusPlusPlugin() override = default;
|
||||
|
||||
QObject *createView(KTextEditor::MainWindow *mainWindow) override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-main-window view providing the Git++ tool view.
|
||||
*/
|
||||
class GitPlusPlusView : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GitPlusPlusView(GitPlusPlusPlugin *plugin, KTextEditor::MainWindow *mainWindow);
|
||||
~GitPlusPlusView() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
// Status
|
||||
void updateStatus();
|
||||
void onStatusFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
||||
|
||||
// Stage/commit
|
||||
void stageSelected();
|
||||
void unstageSelected();
|
||||
void discardSelected();
|
||||
void commitChanges();
|
||||
|
||||
// Remote
|
||||
void push();
|
||||
void pull();
|
||||
void fetch();
|
||||
|
||||
// Branches
|
||||
void refreshBranches();
|
||||
void onBranchFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
||||
void checkoutBranch();
|
||||
void createBranch();
|
||||
void deleteBranch();
|
||||
void compareBranch();
|
||||
void createWorktree();
|
||||
|
||||
// Worktrees
|
||||
void refreshWorktrees();
|
||||
void createWorktreeFromTab();
|
||||
void removeWorktree();
|
||||
void pruneWorktrees();
|
||||
void worktreeContextMenu(const QPoint &pos);
|
||||
|
||||
// Stash
|
||||
void stashSave();
|
||||
void stashApply();
|
||||
void stashPop();
|
||||
void stashDrop();
|
||||
void stashDiff();
|
||||
void refreshStashes();
|
||||
void stashContextMenu(const QPoint &pos);
|
||||
|
||||
// Log/commit
|
||||
void openCommit();
|
||||
|
||||
// Branch context menu
|
||||
void branchContextMenu(const QPoint &pos);
|
||||
|
||||
private:
|
||||
QString projectBaseDir() const;
|
||||
QProcess *gitProcess(const QStringList &args) const;
|
||||
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 updateIndicators();
|
||||
void setRemoteBusy(bool busy, const QString &opName = {});
|
||||
void refreshAll();
|
||||
void setupFileWatcher();
|
||||
QStringList selectedFiles(bool staged) const;
|
||||
QString selectedBranch() const;
|
||||
QString selectedStash() const;
|
||||
|
||||
GitPlusPlusPlugin *m_plugin;
|
||||
KTextEditor::MainWindow *m_mainWindow;
|
||||
QWidget *m_toolView;
|
||||
|
||||
// Tab widget
|
||||
QTabWidget *m_tabWidget;
|
||||
|
||||
// --- Status tab ---
|
||||
QTreeView *m_treeView;
|
||||
GitStatusModel *m_model;
|
||||
QSortFilterProxyModel *m_proxy;
|
||||
QLineEdit *m_filterEdit;
|
||||
QTextEdit *m_commitMsgEdit;
|
||||
QCheckBox *m_amendCheck;
|
||||
QLabel *m_statusLabel;
|
||||
QToolButton *m_commitBtn;
|
||||
QToolButton *m_pushBtn;
|
||||
QToolButton *m_pullBtn;
|
||||
QToolButton *m_fetchBtn;
|
||||
QToolButton *m_stageBtn;
|
||||
QToolButton *m_unstageBtn;
|
||||
QToolButton *m_discardBtn;
|
||||
QToolButton *m_refreshBtn;
|
||||
QToolButton *m_stashBtn;
|
||||
|
||||
// --- Branches tab ---
|
||||
QLineEdit *m_branchFilterEdit;
|
||||
QListWidget *m_branchList;
|
||||
QToolButton *m_branchCheckoutBtn;
|
||||
QToolButton *m_branchCreateBtn;
|
||||
QToolButton *m_branchDeleteBtn;
|
||||
QToolButton *m_branchCompareBtn;
|
||||
|
||||
// --- Stashes tab ---
|
||||
QListWidget *m_stashList;
|
||||
QToolButton *m_stashSaveBtn;
|
||||
QToolButton *m_stashApplyBtn;
|
||||
QToolButton *m_stashPopBtn;
|
||||
QToolButton *m_stashDropBtn;
|
||||
|
||||
// --- Worktrees tab ---
|
||||
QListWidget *m_worktreeList;
|
||||
|
||||
// --- Indicators ---
|
||||
QLabel *m_repoLabel;
|
||||
QLabel *m_statusBarWidget;
|
||||
|
||||
// State
|
||||
QString m_gitDir;
|
||||
QString m_currentBranch;
|
||||
QStringList m_allBranches;
|
||||
bool m_isDirty = false;
|
||||
QPointer<QProcess> m_remoteProc;
|
||||
int m_spinnerFrame = 0;
|
||||
QTimer m_fsDebounce;
|
||||
QFileSystemWatcher *m_watcher = nullptr;
|
||||
};
|
||||
|
||||
#endif // GITPLUSPLUS_H
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
#ifndef GITPROCESS_H
|
||||
#define GITPROCESS_H
|
||||
|
||||
#include <QProcess>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QStandardPaths>
|
||||
|
||||
/**
|
||||
* Helper to create a QProcess configured for running git in a given working directory.
|
||||
* Sets up SSH_ASKPASS for password prompts (ksshaskpass if available).
|
||||
*/
|
||||
namespace GitProcess
|
||||
{
|
||||
|
||||
/**
|
||||
* Create a QProcess ready to run git with the given arguments in workingDir.
|
||||
* The caller owns the returned QProcess.
|
||||
*/
|
||||
inline QProcess *create(const QString &workingDir, const QStringList &args, QObject *parent = nullptr)
|
||||
{
|
||||
auto *proc = new QProcess(parent);
|
||||
proc->setWorkingDirectory(workingDir);
|
||||
proc->setProgram(QStringLiteral("git"));
|
||||
proc->setArguments(args);
|
||||
|
||||
// Set up environment for SSH password prompts
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
// ksshaskpass for KDE integration
|
||||
const QString ksshaskpass = QStandardPaths::findExecutable(QStringLiteral("ksshaskpass"));
|
||||
if (!ksshaskpass.isEmpty()) {
|
||||
env.insert(QStringLiteral("SSH_ASKPASS"), ksshaskpass);
|
||||
env.insert(QStringLiteral("GIT_ASKPASS"), ksshaskpass);
|
||||
}
|
||||
// Avoid git pager
|
||||
env.insert(QStringLiteral("GIT_TERMINAL_PROMPT"), QStringLiteral("0"));
|
||||
proc->setProcessEnvironment(env);
|
||||
|
||||
return proc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run git synchronously and return stdout. Returns empty QByteArray on failure.
|
||||
*/
|
||||
inline QByteArray run(const QString &workingDir, const QStringList &args)
|
||||
{
|
||||
QProcess proc;
|
||||
proc.setWorkingDirectory(workingDir);
|
||||
proc.setProgram(QStringLiteral("git"));
|
||||
proc.setArguments(args);
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("GIT_TERMINAL_PROMPT"), QStringLiteral("0"));
|
||||
proc.setProcessEnvironment(env);
|
||||
|
||||
proc.start();
|
||||
if (!proc.waitForFinished(10000)) {
|
||||
return {};
|
||||
}
|
||||
if (proc.exitCode() != 0) {
|
||||
return {};
|
||||
}
|
||||
return proc.readAllStandardOutput();
|
||||
}
|
||||
|
||||
} // namespace GitProcess
|
||||
|
||||
#endif // GITPROCESS_H
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
#include "gitstatusmodel.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QIcon>
|
||||
|
||||
GitStatusModel::GitStatusModel(QObject *parent)
|
||||
: QAbstractItemModel(parent)
|
||||
{
|
||||
m_nodeNames[NodeStaged] = QStringLiteral("Staged");
|
||||
m_nodeNames[NodeChanged] = QStringLiteral("Changed");
|
||||
m_nodeNames[NodeUntracked] = QStringLiteral("Untracked");
|
||||
|
||||
m_nodeIcons[NodeStaged] = QIcon::fromTheme(QStringLiteral("vcs-added"));
|
||||
m_nodeIcons[NodeChanged] = QIcon::fromTheme(QStringLiteral("vcs-locally-modified"));
|
||||
m_nodeIcons[NodeUntracked] = QIcon::fromTheme(QStringLiteral("vcs-added"));
|
||||
}
|
||||
|
||||
QModelIndex GitStatusModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (column != 0)
|
||||
return {};
|
||||
|
||||
if (!parent.isValid()) {
|
||||
// Top-level nodes
|
||||
if (row >= 0 && row < NodeCount)
|
||||
return createIndex(row, 0, TopLevelId);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Child items — parent's internalId is TopLevelId, parent's row gives the node type
|
||||
if (parent.internalId() == TopLevelId) {
|
||||
int nodeType = parent.row();
|
||||
if (nodeType >= 0 && nodeType < NodeCount && row >= 0 && row < m_entries[nodeType].size())
|
||||
return createIndex(row, 0, quintptr(nodeType));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QModelIndex GitStatusModel::parent(const QModelIndex &child) const
|
||||
{
|
||||
if (!child.isValid())
|
||||
return {};
|
||||
|
||||
if (child.internalId() == TopLevelId)
|
||||
return {}; // top-level has no parent
|
||||
|
||||
// internalId is the node type
|
||||
int nodeType = int(child.internalId());
|
||||
if (nodeType >= 0 && nodeType < NodeCount)
|
||||
return createIndex(nodeType, 0, TopLevelId);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int GitStatusModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (!parent.isValid())
|
||||
return NodeCount;
|
||||
|
||||
if (parent.internalId() == TopLevelId) {
|
||||
int nodeType = parent.row();
|
||||
if (nodeType >= 0 && nodeType < NodeCount)
|
||||
return m_entries[nodeType].size();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GitStatusModel::columnCount(const QModelIndex &) const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant GitStatusModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
|
||||
// Top-level nodes
|
||||
if (index.internalId() == TopLevelId) {
|
||||
int nodeType = index.row();
|
||||
if (nodeType < 0 || nodeType >= NodeCount)
|
||||
return {};
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
return QStringLiteral("%1 (%2)").arg(m_nodeNames[nodeType]).arg(m_entries[nodeType].size());
|
||||
case Qt::DecorationRole:
|
||||
return m_nodeIcons[nodeType];
|
||||
case Qt::FontRole: {
|
||||
QFont f;
|
||||
f.setBold(true);
|
||||
return f;
|
||||
}
|
||||
case NodeTypeRole:
|
||||
return nodeType;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// File entries
|
||||
int nodeType = int(index.internalId());
|
||||
if (nodeType < 0 || nodeType >= NodeCount)
|
||||
return {};
|
||||
|
||||
int row = index.row();
|
||||
if (row < 0 || row >= m_entries[nodeType].size())
|
||||
return {};
|
||||
|
||||
const auto &entry = m_entries[nodeType][row];
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
return entry.file;
|
||||
case Qt::DecorationRole: {
|
||||
// Status char icon
|
||||
QChar sc = (nodeType == NodeStaged) ? entry.indexStatus : entry.workStatus;
|
||||
if (sc == QLatin1Char('M'))
|
||||
return QIcon::fromTheme(QStringLiteral("vcs-locally-modified"));
|
||||
if (sc == QLatin1Char('A') || sc == QLatin1Char('?'))
|
||||
return QIcon::fromTheme(QStringLiteral("vcs-added"));
|
||||
if (sc == QLatin1Char('D'))
|
||||
return QIcon::fromTheme(QStringLiteral("vcs-removed"));
|
||||
if (sc == QLatin1Char('R'))
|
||||
return QIcon::fromTheme(QStringLiteral("vcs-locally-modified"));
|
||||
return QIcon::fromTheme(QStringLiteral("text-plain"));
|
||||
}
|
||||
case Qt::ToolTipRole: {
|
||||
QChar sc = (nodeType == NodeStaged) ? entry.indexStatus : entry.workStatus;
|
||||
return QStringLiteral("[%1] %2").arg(sc).arg(entry.file);
|
||||
}
|
||||
case FilePathRole:
|
||||
return entry.file;
|
||||
case NodeTypeRole:
|
||||
return nodeType;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Qt::ItemFlags GitStatusModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
|
||||
if (index.internalId() == TopLevelId)
|
||||
return Qt::ItemIsEnabled; // top-level nodes not selectable
|
||||
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
void GitStatusModel::parseStatus(const QByteArray &raw)
|
||||
{
|
||||
beginResetModel();
|
||||
|
||||
for (int i = 0; i < NodeCount; ++i)
|
||||
m_entries[i].clear();
|
||||
|
||||
const auto lines = raw.split('\n');
|
||||
for (const auto &line : lines) {
|
||||
if (line.size() < 4)
|
||||
continue;
|
||||
|
||||
// Format: XY <space> filename
|
||||
// or XY <space> old -> new (for renames)
|
||||
QChar x = QLatin1Char(line.at(0));
|
||||
QChar y = QLatin1Char(line.at(1));
|
||||
// line[2] is space
|
||||
QString file = QString::fromUtf8(line.mid(3));
|
||||
|
||||
// Handle renames: "R old -> new"
|
||||
int arrowIdx = file.indexOf(QStringLiteral(" -> "));
|
||||
if (arrowIdx >= 0)
|
||||
file = file.mid(arrowIdx + 4);
|
||||
|
||||
GitStatusEntry entry{file, x, y};
|
||||
|
||||
// Staged: X is not ' ', '?' or '!'
|
||||
if (x != QLatin1Char(' ') && x != QLatin1Char('?') && x != QLatin1Char('!')) {
|
||||
m_entries[NodeStaged].append(entry);
|
||||
}
|
||||
|
||||
// Changed (working tree): Y is not ' ', '?' or '!'
|
||||
if (y == QLatin1Char('?')) {
|
||||
m_entries[NodeUntracked].append(entry);
|
||||
} else if (y != QLatin1Char(' ') && y != QLatin1Char('!')) {
|
||||
m_entries[NodeChanged].append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
#ifndef GITSTATUSMODEL_H
|
||||
#define GITSTATUSMODEL_H
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QIcon>
|
||||
|
||||
/**
|
||||
* Represents a single file entry in git status output.
|
||||
*/
|
||||
struct GitStatusEntry {
|
||||
QString file; // relative path
|
||||
QChar indexStatus; // X column from --porcelain=v1
|
||||
QChar workStatus; // Y column from --porcelain=v1
|
||||
};
|
||||
|
||||
/**
|
||||
* Tree model for displaying git status.
|
||||
* Has three top-level nodes: Staged, Changed (unstaged), Untracked.
|
||||
* Each node contains file entries.
|
||||
*/
|
||||
class GitStatusModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum NodeType {
|
||||
NodeStaged = 0,
|
||||
NodeChanged,
|
||||
NodeUntracked,
|
||||
NodeCount
|
||||
};
|
||||
|
||||
enum Roles {
|
||||
FilePathRole = Qt::UserRole + 1,
|
||||
NodeTypeRole,
|
||||
};
|
||||
|
||||
explicit GitStatusModel(QObject *parent = nullptr);
|
||||
~GitStatusModel() override = default;
|
||||
|
||||
// --- QAbstractItemModel interface ---
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override;
|
||||
QModelIndex parent(const QModelIndex &child) const override;
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
int columnCount(const QModelIndex &parent = {}) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
|
||||
/**
|
||||
* Parse raw output from `git status --porcelain=v1` and populate the model.
|
||||
*/
|
||||
void parseStatus(const QByteArray &raw);
|
||||
|
||||
/**
|
||||
* Get files for a given node type.
|
||||
*/
|
||||
const QList<GitStatusEntry> &entries(NodeType type) const { return m_entries[type]; }
|
||||
|
||||
private:
|
||||
static constexpr quintptr TopLevelId = 0xFFFFFFFF;
|
||||
|
||||
QList<GitStatusEntry> m_entries[NodeCount];
|
||||
QString m_nodeNames[NodeCount];
|
||||
QIcon m_nodeIcons[NodeCount];
|
||||
};
|
||||
|
||||
#endif // GITSTATUSMODEL_H
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"KPlugin": {
|
||||
"Id": "gitplusplus",
|
||||
"Name": "Git++",
|
||||
"Description": "Git integration with status, commit, push, pull, fetch, stage, unstage, and branch operations",
|
||||
"Icon": "vcs-commit",
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "lkn"
|
||||
}
|
||||
]
|
||||
},
|
||||
"X-KDE-PluginInfo-EnabledByDefault": true
|
||||
}
|
||||
Loading…
Reference in New Issue