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