diff --git a/CMakeLists.txt b/CMakeLists.txt index aba88dd..66ebb9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -174,6 +174,9 @@ set(codepointer_sources src/plugins/git/GitCommit.ui src/plugins/git/GitPlugin.cpp src/plugins/git/GitPlugin.hpp + src/plugins/git/CommitForm.cpp + src/plugins/git/CommitForm.hpp + src/plugins/git/CommitForm.ui src/plugins/Terminal/TerminalPlugin.cpp src/plugins/Terminal/TerminalPlugin.hpp src/AnsiToHTML.cpp @@ -187,12 +190,7 @@ file(COPY "${CMAKE_SOURCE_DIR}/${ICON_NAME}.ico" DESTINATION "${CMAKE_BINARY_DIR configure_file(codepointer.qrc.in ${CMAKE_BINARY_DIR}/codepointer.qrc) configure_file(codepointer.desktop.in ${CMAKE_BINARY_DIR}/${CODEPOINTER_APP_NAME}.desktop) - -if (WIN32) - add_executable(codepointer WIN32 ${codepointer_sources} codepointer.rc) -else() - add_executable(codepointer ${codepointer_sources}) -endif() +add_executable(codepointer WIN32 ${codepointer_sources} codepointer.rc) if (!MINGW) set_property(TARGET codepointerPROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) diff --git a/build-appimage.sh b/build-appimage.sh index 020d9d5..cf9e20e 100755 --- a/build-appimage.sh +++ b/build-appimage.sh @@ -16,7 +16,7 @@ APP_VERSION="0.1.1" QT_VERSION="6.10.1" NAME="${APP_NAME}-v${APP_VERSION}${NAME_SUFFIX}-x86_64" -QTDIR="${HOME}/qt/${QT_VERSION}/gcc_64" +QTDIR="/usr/lib/qt6" export matrix_config_build_dir=ubuntu-gcc export PATH=$QTDIR/bin:$PATH export LD_LIBRARY_PATH=$QTDIR/lib:$LD_LIBRARY_PATH diff --git a/src/plugins/git/CommitForm.cpp b/src/plugins/git/CommitForm.cpp new file mode 100644 index 0000000..5b5d2ff --- /dev/null +++ b/src/plugins/git/CommitForm.cpp @@ -0,0 +1,518 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "CommitForm.hpp" +#include "GitPlugin.hpp" +#include "plugins/texteditor/texteditor_plg.h" +#include "ui_CommitForm.h" +#include "widgets/qmdieditor.h" + +struct GitStatusEntry { + QString filename; + GitFileStatus status; + bool checked = false; +}; + +static auto parseGitStatus(QStringView statusOutput) -> QList { + auto out = QList(); + for (auto line : statusOutput.split('\n', Qt::SkipEmptyParts)) { + if (line.size() < 3) { + continue; + } + auto x = line[0]; + auto y = line[1]; + auto status = (x == '?' && y == '?') ? GitFileStatus::Untracked + : (x == 'A' || y == 'A') ? GitFileStatus::Added + : (x == 'M' || y == 'M') ? GitFileStatus::Modified + : (x == 'D' || y == 'D') ? GitFileStatus::Deleted + : (x == 'R' || y == 'R') ? GitFileStatus::Renamed + : (x == 'C' || y == 'C') ? GitFileStatus::Copied + : GitFileStatus::Unknown; + out.append({line.mid(3).trimmed().toString(), status}); + } + return out; +} + +auto createTempFileWithContent(const QString &content) -> QString { + auto file = QTemporaryFile(); + // keep file after destruction + file.setAutoRemove(false); + if (!file.open()) { + return {}; + } + auto out = QTextStream(&file); + out << content; + file.close(); + return file.fileName(); +} + +class GitStatusTableModel final : public QAbstractTableModel { + // Q_OBJECT + + public: + enum Roles { StatusRole = Qt::UserRole + 1 }; + explicit GitStatusTableModel(QObject *parent = nullptr); + + // Re-implementation rom QAbstractTableModel + auto rowCount(const QModelIndex &parent = {}) const -> int override; + auto columnCount(const QModelIndex &parent = {}) const -> int override; + auto data(const QModelIndex &index, int role) const -> QVariant override; + auto setData(const QModelIndex &index, const QVariant &value, int role) -> bool override; + auto flags(const QModelIndex &index) const -> Qt::ItemFlags override; + auto headerData(int section, Qt::Orientation orientation, int role) const -> QVariant override; + + // Public API + auto setEntries(QList entries) -> void; + auto checkedEntries() const -> QList; + auto setAllChecked(bool checked) -> void; + auto hasAnyChecked() const -> bool; + + private: + QList m_entries; + + static auto statusToText(GitFileStatus status) -> QString; +}; + +GitStatusTableModel::GitStatusTableModel(QObject *parent) : QAbstractTableModel(parent) {} + +auto GitStatusTableModel::rowCount(const QModelIndex &parent) const -> int { + return parent.isValid() ? 0 : m_entries.size(); +} + +auto GitStatusTableModel::columnCount(const QModelIndex &) const -> int { + // checkbox | filename | status + return 3; +} + +auto GitStatusTableModel::data(const QModelIndex &index, int role) const -> QVariant { + if (!index.isValid()) { + return {}; + } + const auto &e = m_entries.at(index.row()); + if (role == StatusRole) { + return static_cast(e.status); + } + if (index.column() == 0 && role == Qt::CheckStateRole) { + return e.checked ? Qt::Checked : Qt::Unchecked; + } + if (role != Qt::DisplayRole) { + return {}; + } + switch (index.column()) { + case 1: + return statusToText(e.status); + case 2: + return e.filename; + default: + return {}; + } +} + +auto GitStatusTableModel::setData(const QModelIndex &index, const QVariant &value, int role) + -> bool { + if (!index.isValid()) { + return false; + } + + if (index.column() == 0 && role == Qt::CheckStateRole) { + auto &e = m_entries[index.row()]; + e.checked = (value.toInt() == Qt::Checked); + emit dataChanged(index, index, {Qt::CheckStateRole}); + return true; + } + + return false; +} + +auto GitStatusTableModel::flags(const QModelIndex &index) const -> Qt::ItemFlags { + if (!index.isValid()) { + return Qt::NoItemFlags; + } + + auto f = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + if (index.column() == 0) { + f |= Qt::ItemIsUserCheckable; + } + return f; +} + +auto GitStatusTableModel::headerData(int section, Qt::Orientation orientation, int role) const + -> QVariant { + if (orientation != Qt::Horizontal || role != Qt::DisplayRole) { + return {}; + } + + switch (section) { + case 0: + return tr("Commit"); + case 1: + return tr("Status"); + case 2: + return tr("File"); + default: + return {}; + } +} + +auto GitStatusTableModel::setEntries(QList entries) -> void { + beginResetModel(); + m_entries = std::move(entries); + endResetModel(); +} + +auto GitStatusTableModel::checkedEntries() const -> QList { + QList out; + for (const auto &e : m_entries) { + if (e.checked) { + out.append(e); + } + } + return out; +} + +auto GitStatusTableModel::statusToText(GitFileStatus status) -> QString { + switch (status) { + case GitFileStatus::Modified: + return QStringLiteral("Modified"); + case GitFileStatus::Added: + return QStringLiteral("Added"); + case GitFileStatus::Deleted: + return QStringLiteral("Deleted"); + case GitFileStatus::Renamed: + return QStringLiteral("Renamed"); + case GitFileStatus::Copied: + return QStringLiteral("Copied"); + case GitFileStatus::Untracked: + return QStringLiteral("Untracked"); + default: + return QStringLiteral("Unknown"); + } +} + +void GitStatusTableModel::setAllChecked(bool checked) { + if (m_entries.isEmpty()) { + return; + } + for (auto &e : m_entries) { + e.checked = checked; + } + auto topLeft = index(0, 0); + auto bottomRight = index(rowCount() - 1, 0); + emit dataChanged(topLeft, bottomRight, {Qt::CheckStateRole}); +} + +bool GitStatusTableModel::hasAnyChecked() const { + for (const auto &e : m_entries) { + if (e.checked) { + return true; + } + } + return false; +} + +///////// +CommitForm::CommitForm(const QString &dir, GitPlugin *plugin, QWidget *parent) + : QWidget(parent), ui(new Ui::CommitForm) { + ui->setupUi(this); + mdiClientName = tr("Commit"); + repoRoot = dir; + git = plugin; + + model = new GitStatusTableModel(ui->tableView); + // We will make it simpler for now, no inline editing. + // I hope in the future to add a way to edit the file itself here. + // What prevetns: + // 1. We don't have a notion of shared document. We cannot open the + // same "content" in different tabs. + // 2. When double clicking a line in a diff, the code directly opens the + // modified file. Instead we will need to modify the code, and somehow + // catch this event in this class, and navigate to the file. + // 3. 3 Color layuout would be stretch on small screens. I would like that + // on smaller "displays" the editor would be below the diff view, and + // on larger screen on the side. Qt provides no such layout. + // Solution to this might be having 2 editors with shared document, and + // on resize hide/show the revevant one. Other alternative - move it + // between layouts. + { + ui->modifiedFileNameLabel->hide(); + ui->modifiedFileContents->hide(); + } + + // This code is a back hack, I use instead of changing the UI file to use + // a qmdiEditor. I am unsure how can I see QtDesigner to allocate the widget + // in a non-standard way. Note how I request the editor plugin for a widget + // instead of creating one manually here. + { + auto layout = ui->diffPreview->parentWidget()->layout(); + auto manager = git->getManager(); + auto plugin = manager->findPlugin("TextEditorPlugin"); + if (auto p = dynamic_cast(plugin)) { + auto client = p->fileNewEditor(); + client->mdiServer = git->mdiServer; + if (auto e = dynamic_cast(client)) { + e->setLineNumbersVisible(false); + e->setReadOnly(true); + e->setMinimapVisible(false); + e->setHighlighter("diff.xml"); + e->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + layout->replaceWidget(ui->diffPreview, e); + ui->diffPreview->deleteLater(); + ui->diffPreview = e->getEditor(); + + // FIXME: The diff-preview widget has the correct font + // configuration if we would create the widget from the + // plugin - instead we need to do this very ugly hack. + ui->commitMessage->setFont(e->getEditor()->font()); + } + } + } + + ui->tableView->setModel(model); + ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows); + ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->revertSelectedButton->setEnabled(false); + ui->commitButton->setEnabled(false); + ui->commitMessage->setFocusPolicy(Qt::StrongFocus); + + auto *header = ui->tableView->horizontalHeader(); + header->setSectionResizeMode(0, QHeaderView::ResizeToContents); + header->setSectionResizeMode(1, QHeaderView::ResizeToContents); + header->setSectionResizeMode(2, QHeaderView::Stretch); + + connect(ui->revertCurrentButton, &QAbstractButton::clicked, this, + &CommitForm::revertCurrentImpl); + connect(ui->revertSelectedButton, &QAbstractButton::clicked, this, + &CommitForm::revertSelectionImpl); + connect(ui->tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this](const QItemSelection &selected, const QItemSelection &) { + if (selected.indexes().size() == 0) { + newFileSelected({}, GitFileStatus::Unknown); + return; + } + auto firstIndex = selected.indexes().first(); + auto idx = model->index(firstIndex.row(), 2); + auto fileName = model->data(idx, Qt::DisplayRole).toString(); + auto status = static_cast( + model->data(idx, GitStatusTableModel::StatusRole).toInt()); + newFileSelected(fileName, status); + }); + connect(model, &QAbstractItemModel::dataChanged, this, + [this](const QModelIndex &, const QModelIndex &, const QList &roles) { + if (!roles.isEmpty() && !roles.contains(Qt::CheckStateRole)) { + return; + } + + auto hasSelection = !model->checkedEntries().isEmpty(); + ui->revertSelectedButton->setEnabled(hasSelection); + ui->commitButton->setEnabled(hasSelection); + }); + connect(model, &QAbstractItemModel::modelReset, this, + [this]() { ui->revertSelectedButton->setEnabled(false); }); + connect(ui->selectAllButton, &QAbstractButton::clicked, this, + [this]() { model->setAllChecked(true); }); + connect(ui->selectNoneButton, &QAbstractButton::clicked, this, [this]() { + model->setAllChecked(false); + ui->revertSelectedButton->setEnabled(false); + }); + connect(ui->commitButton, &QAbstractButton::clicked, this, &CommitForm::commitImpl); + updateGitStatus(); +} + +CommitForm::~CommitForm() { delete ui; } + +QString CommitForm::mdiClientFileName() { return QString("git:%1").arg(repoRoot); } + +void CommitForm::keyPressEvent(QKeyEvent *event) { + if (event->key() == Qt::Key_Escape) { + if (ui->diffPreview) { + ui->diffPreview->setFocus(Qt::ShortcutFocusReason); + event->accept(); + return; + } + } + + QWidget::keyPressEvent(event); +} + +void CommitForm::updateGitStatus() { + auto [gitOutput, exitCode] = git->runGit({"-C", repoRoot, "status", "--porcelain"}); + auto status = parseGitStatus(gitOutput); + model->setEntries(status); + if (model->rowCount() > 0) { + ui->tableView->selectRow(0); + } +} + +void CommitForm::newFileSelected(const QString &filename, GitFileStatus status) { + if (filename.isEmpty()) { + ui->diffPreview->setPlainText(""); + return; + } + + auto output = QString(); + auto highlighter = QString{"diff.xml"}; + auto fullFilePath = repoRoot + "/" + filename; + switch (status) { + case GitFileStatus::Modified: { + auto [output2, exitCode] = git->runGit({"-C", repoRoot, "diff", filename}); + if (exitCode != 0) { + qDebug() << QString("git - code=%1, output=[%2]").arg(exitCode).arg(output2); + ui->commitLogLabel->setText(""); + ui->diffPreview->setPlainText(output); + return; + } + output = output2; + } + case GitFileStatus::Deleted: + break; + case GitFileStatus::Added: + case GitFileStatus::Renamed: + case GitFileStatus::Copied: + case GitFileStatus::Untracked: { + auto manager = git->getManager(); + auto plugin = manager->findPlugin("TextEditorPlugin"); + + auto p = dynamic_cast(plugin); + if (!p) { + qDebug() << "Cannot find the text editor plugin"; + break; + } + auto score = p->canOpenFile(fullFilePath); + if (score == 1) { + qDebug() << "Text editor plugin says this is not a text file" << score << fullFilePath; + break; + } + + auto file = QFile(fullFilePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug() << "Could not open file for reading" << fullFilePath; + break; + } + auto in = QTextStream(&file); + auto lineCount = 5000; + while (!in.atEnd() && lineCount >= 0) { + output += in.readLine(); + output += "\n"; + } + file.close(); + + auto langInfo = ::Qutepart::chooseLanguage({}, {}, filename); + if (langInfo.isValid()) { + highlighter = langInfo.id; + } + break; + } + default: + break; + } + + // FIXME: Note how we need to hijack the low level APIs of Qutepart + // to set the syntax highlighter. + // We need better abstractions, some IEditor, which can be an + // interface with has implementation as Qutepart of QSCintilla or + // something different completely. + ui->diffPreview->setPlainText(output); + + // FIXME: this looks way too ugly, + // Problem - the "editor" is not the correct widge + // The UI expects a QPlainTextEdit, and we have Widget that includes a + // QPlainTextEdit. + if (auto editor = dynamic_cast(ui->diffPreview->parent()->parent())) { + editor->updateInternalMappings(repoRoot); + editor->setHighlighter(highlighter); + } else { + qDebug() << "Double click on diff will not work"; + } +} + +void CommitForm::revertCurrentImpl() { + auto selected = ui->tableView->currentIndex(); + auto idx = model->index(selected.row(), 2); + auto fileName = model->data(idx, Qt::DisplayRole).toString(); + + auto msgBox = QMessageBox(); + msgBox.setWindowTitle("Revert file"); + msgBox.setText(tr("Are you sure you want to revert this file?

%1").arg(fileName)); + msgBox.setTextFormat(Qt::RichText); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + msgBox.setIcon(QMessageBox::Icon::Question); + auto reply = msgBox.exec(); + if (reply != QMessageBox::Yes) { + return; + } + + auto args = QStringList{"-C", repoRoot, "checkout", fileName}; + auto [output, exitCode] = git->runGit(args); + ui->gitOutput->setText(output); + if (exitCode == 0) { + updateGitStatus(); + } +} + +void CommitForm::revertSelectionImpl() { + auto checked = model->checkedEntries(); + if (checked.isEmpty()) { + return; + } + + auto msgBox = QMessageBox(); + msgBox.setWindowTitle("Revert multiple files"); + msgBox.setText(tr("Are you sure you want to revert %1 files?").arg(checked.count())); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + msgBox.setIcon(QMessageBox::Icon::Question); + auto reply = msgBox.exec(); + if (reply != QMessageBox::Yes) { + return; + } + + auto args = QStringList{"-C", repoRoot, "checkout"}; + for (auto &c : std::as_const(checked)) { + args.push_back(c.filename); + } + + auto [output, exitCode] = git->runGit(args); + ui->gitOutput->setText(output); + if (exitCode == 0) { + updateGitStatus(); + } +} + +void CommitForm::commitImpl() { + auto const &checked = model->checkedEntries(); + if (checked.isEmpty()) { + return; + } + + auto args = QStringList{"-C", repoRoot, "add"}; + for (auto const &c : checked) { + args.push_back(c.filename); + } + auto [output, exitCode] = git->runGit(args); + if (exitCode != 0) { + qDebug() << QString("ExitCode=%1, output=%2\ncommand=%3") + .arg(exitCode) + .arg(output) + .arg(QString("git ") + args.join(' ')); + return; + } + + auto commitLogFileName = createTempFileWithContent(ui->commitMessage->toPlainText()); + auto cleanup = qScopeGuard([&] { QFile::remove(commitLogFileName); }); + args = QStringList{"-C", repoRoot, "commit", "-F", commitLogFileName}; + std::tie(output, exitCode) = git->runGit(args); + if (exitCode != 0) { + qDebug() << QString("ExitCode=%1, output=%2\ncommand=%3") + .arg(exitCode) + .arg(output) + .arg(QString("git ") + args.join(' ')); + return; + } + this->deleteLater(); +} diff --git a/src/plugins/git/CommitForm.hpp b/src/plugins/git/CommitForm.hpp new file mode 100644 index 0000000..c8b5efc --- /dev/null +++ b/src/plugins/git/CommitForm.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +namespace Ui { +class CommitForm; +} + +class GitPlugin; +class GitStatusTableModel; + +enum class GitFileStatus { Modified, Added, Deleted, Renamed, Copied, Untracked, Unknown }; + +class CommitForm : public QWidget, public qmdiClient { + Q_OBJECT + + public: + explicit CommitForm(const QString &dir, GitPlugin *plugin, QWidget *parent); + ~CommitForm(); + + virtual QString mdiClientFileName() override; + + protected: + void keyPressEvent(QKeyEvent *event) override; + + public slots: + void updateGitStatus(); + void newFileSelected(const QString &filename, GitFileStatus status); + void revertCurrentImpl(); + void revertSelectionImpl(); + void commitImpl(); + + private: + Ui::CommitForm *ui; + GitStatusTableModel *model; + GitPlugin *git; + QString repoRoot; +}; diff --git a/src/plugins/git/CommitForm.ui b/src/plugins/git/CommitForm.ui new file mode 100644 index 0000000..dde3971 --- /dev/null +++ b/src/plugins/git/CommitForm.ui @@ -0,0 +1,173 @@ + + + CommitForm + + + + 0 + 0 + 623 + 469 + + + + Form + + + + + + Qt::Orientation::Horizontal + + + + + + + Label + + + + + + + true + + + commit message + + + + + + + + + &Commit + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Files + + + + + + + + + + Revert current + + + + + + + Revert selected + + + + + + + Select &all + + + + + + + Select &none + + + + + + + + + + + Diff + + + + + + + + + + + + + + Content + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + diffPreview + modificationChanged(bool) + commitButton + setEnabled(bool) + + + 80 + 127 + + + 66 + 436 + + + + + diff --git a/src/plugins/git/CreateGitBranch.cpp b/src/plugins/git/CreateGitBranch.cpp index 8ca84c3..d050ab6 100644 --- a/src/plugins/git/CreateGitBranch.cpp +++ b/src/plugins/git/CreateGitBranch.cpp @@ -60,12 +60,12 @@ void CreateGitBranch::verifyBranchName(const QString &newText) { } void CreateGitBranch::findLocalBranches() { - auto res = plugin->runGit({"branch"}); - if (res.isEmpty()) { + auto [output, exitCode] = plugin->runGit({"branch"}); + if (output.isEmpty()) { return; } this->availableBranches.clear(); - for (auto &line : res.split('\n', Qt::SkipEmptyParts)) { + for (auto &line : output.split('\n', Qt::SkipEmptyParts)) { auto branchName = line.trimmed(); if (branchName.startsWith("* ")) { branchName.remove(0, 2); @@ -100,5 +100,6 @@ QString CreateGitBranch::createBranchImplementation(const QString &branchName, b } else { args = {"branch", branchName}; } - return plugin->runGit(args); + auto [gitOutput, exitCode] = plugin->runGit(args); + return gitOutput; } diff --git a/src/plugins/git/GitPlugin.cpp b/src/plugins/git/GitPlugin.cpp index eb9cc24..51db26f 100644 --- a/src/plugins/git/GitPlugin.cpp +++ b/src/plugins/git/GitPlugin.cpp @@ -14,18 +14,21 @@ #include #include -#include "CommitDelegate.hpp" -#include "CommitModel.hpp" -#include "GitPlugin.hpp" +#include + #include "GlobalCommands.hpp" -#include "iplugin.h" -#include "plugins/git/CreateGitBranch.hpp" #include "ui_GitCommands.h" #include "ui_GitCommit.h" #include "widgets/AutoShrinkLabel.hpp" #include "widgets/BoldItemDelegate.hpp" #include "widgets/qmdieditor.h" +#include "plugins/git/CommitDelegate.hpp" +#include "plugins/git/CommitForm.hpp" +#include "plugins/git/CommitModel.hpp" +#include "plugins/git/CreateGitBranch.hpp" +#include "plugins/git/GitPlugin.hpp" + QString shortGitSha1(const QString &fullSha1, int length = 7) { if (length <= 0) { return QString(); @@ -121,6 +124,7 @@ void GitPlugin::on_client_merged(qmdiHost *host) { revert->setToolTip(tr("Revert existing commits")); revert->setShortcut(QKeySequence("Ctrl+G, U")); commit->setToolTip(tr("Record changes to the repository")); + commit->setShortcut(QKeySequence("Ctrl+G, C")); stash->setToolTip(tr("tash away changes to dirty working directory")); branches->setToolTip(tr("List, create, or delete branches")); @@ -128,6 +132,7 @@ void GitPlugin::on_client_merged(qmdiHost *host) { connect(logProject, &QAction::triggered, this, &GitPlugin::logProjectHandler); connect(diffFile, &QAction::triggered, this, &GitPlugin::diffFileHandler); connect(revert, &QAction::triggered, this, &GitPlugin::revertFileHandler); + connect(commit, &QAction::triggered, this, &GitPlugin::commitHandler); auto menuName = "&Git"; host->menus.addActionGroup(menuName, "&Project"); @@ -175,6 +180,23 @@ void GitPlugin::loadConfig(QSettings &settings) { restoreGitLog(); } +int GitPlugin::canOpenFile(const QString &fileName) { + auto url = QUrl(fileName); + if (url.scheme().isEmpty()) { + return 0; + } + return url.scheme() == "git" ? 5 : 0; +} + +qmdiClient *GitPlugin::openFile(const QString &fileName, int, int, int) { + auto url = QUrl(fileName); + auto repoDir = url.path(); + auto manager = getManager(); + auto commitForm = new CommitForm(repoDir, this, manager); + mdiServer->addClient(commitForm); + return nullptr; +} + void GitPlugin::logFileHandler() { auto manager = getManager(); auto client = manager->getMdiServer()->getCurrentClient(); @@ -230,7 +252,7 @@ void GitPlugin::revertFileHandler() { return; } auto args = QStringList{"restore", client->mdiClientFileName()}; - auto output = runGit(args); + auto [output, exitCode] = runGit(args); if (auto editor = dynamic_cast(client)) { editor->loadFile(filename); editor->loadContent(false); @@ -242,12 +264,12 @@ void GitPlugin::refreshBranchesHandler() { auto client = manager->getMdiServer()->getCurrentClient(); auto filename = client->mdiClientFileName(); auto repoRoot = getConfig().getGitLastDir(); - auto output = runGit({"-C", repoRoot, "branch", "-a"}); + auto [output, exitCode] = runGit({"-C", repoRoot, "branch", "-a"}); auto branches = output.split('\n', Qt::SkipEmptyParts); form->branchListCombo->clear(); auto activeIndex = -1; auto delegate = static_cast(form->branchListCombo->itemDelegate()); - for (auto const &line : branches) { + for (auto const &line : std::as_const(branches)) { auto isActive = line.startsWith('*'); auto branchName = line.mid(2).trimmed(); if (branchName.isEmpty()) { @@ -276,7 +298,7 @@ void GitPlugin::diffBranchHandler() { auto filename = client->mdiClientFileName(); auto repoRoot = QFileInfo(filename).absolutePath(); auto branch = form->branchListCombo->currentText(); - auto diff = runGit({"diff", branch}); + auto [diff, exitCode] = runGit({"diff", branch}); if (diff.isEmpty()) { return; } @@ -315,16 +337,60 @@ void GitPlugin::deleteBranchHandler() { if (reply == QMessageBox::Yes) { auto deleteBranchArg = cb->isChecked() ? "-D" : "-d"; auto args = QStringList{"branch", deleteBranchArg, branch}; - auto res = runGit(args); - form->gitOutput->setText(res); - form->gitOutput->setToolTip(res); + auto [output, exitCode] = runGit(args); + if (exitCode != 0) { + // TODO - display this error + qDebug() << "Command failed. Error" << exitCode << output; + return; + } + form->gitOutput->setText(output); + form->gitOutput->setToolTip(output); refreshBranchesHandler(); } } +void GitPlugin::commitHandler() { + auto manager = getManager(); + auto client = manager->getMdiServer()->getCurrentClient(); + auto filename = client->mdiClientFileName(); + if (filename.isEmpty()) { + // TODO - query the current project and use it for commits + qDebug() << "Cannot commit on an empty file" << filename; + return; + } + + auto repoRoot = detectRepoRoot(filename); + if (repoRoot.isEmpty()) { + qDebug() << "Filename is not in any git repo" << filename; + return; + } + auto commitForm = new CommitForm(repoRoot, this, manager); + mdiServer->addClient(commitForm); +} + +void GitPlugin::commitDisplayHandler(const QModelIndex &mi) { + auto widget = static_cast(form->container->widget(0)); + auto manager = getManager(); + auto filename = mi.data().toString(); + auto [diff, exitCode] = + runGit({"-C", getConfig().getGitLastDir(), "show", widget->currentSha1, "--", filename}); + if (exitCode != 0) { + // TODO display this error + return; + } + auto shortSha1 = shortGitSha1(widget->currentSha1); + auto displayName = QString("%1-%2.diff").arg(shortSha1, filename); + CommandArgs args = { + {GlobalArguments::FileName, displayName}, + {GlobalArguments::Content, diff}, + {GlobalArguments::ReadOnly, true}, + {GlobalArguments::SourceDirectory, getConfig().getGitLastDir()}, + }; + manager->handleCommandAsync(GlobalCommands::DisplayText, args); +} + void GitPlugin::logHandler(GitLog log, const QString &filename) { - auto repoRoot = QFileInfo(filename).absolutePath(); - repoRoot = detectRepoRoot(repoRoot); + auto repoRoot = detectRepoRoot(filename); if (repoRoot.isEmpty()) { form->label->setText(tr("No commits or not a git repo")); form->diffBranchButton->setEnabled(true); @@ -355,7 +421,12 @@ void GitPlugin::logHandler(GitLog log, const QString &filename) { getConfig().setGitLastDir(repoRoot); getConfig().setGitLastCommand(args.join(" ")); - auto output = runGit(args); + auto [output, exitCode] = runGit(args); + if (exitCode != 0) { + // ui->commitLogLabel->setText(output); + return; + } + model->setContent(output); form->listView->setModel(model); gitDock->raise(); @@ -372,26 +443,11 @@ void GitPlugin::on_gitCommitClicked(const QModelIndex &mi) { auto rawCommit = getRawCommit(sha1); auto const fullCommit = FullCommitInfo::parse(rawCommit); auto widget = static_cast(form->container->widget(0)); - if (!widget) { widget = new GitCommitDisplay(form->container); form->container->addWidget(widget); connect(widget->ui.commits, &QAbstractItemView::doubleClicked, this, - [this, widget](const QModelIndex &i) { - auto manager = getManager(); - auto filename = i.data().toString(); - auto diff = runGit({"-C", getConfig().getGitLastDir(), "show", - widget->currentSha1, "--", filename}); - auto shortSha1 = shortGitSha1(widget->currentSha1); - auto displayName = QString("%1-%2.diff").arg(shortSha1).arg(filename); - CommandArgs args = { - {GlobalArguments::FileName, displayName}, - {GlobalArguments::Content, diff}, - {GlobalArguments::ReadOnly, true}, - {GlobalArguments::SourceDirectory, getConfig().getGitLastDir()}, - }; - manager->handleCommandAsync(GlobalCommands::DisplayText, args); - }); + &GitPlugin::commitDisplayHandler); } widget->currentSha1 = sha1; @@ -427,30 +483,36 @@ void GitPlugin::on_gitCommitDoubleClicked(const QModelIndex &mi) { manager->handleCommandAsync(GlobalCommands::DisplayText, args); } -QString GitPlugin::runGit(const QStringList &args) { +std::tuple GitPlugin::runGit(const QStringList &args) { // qDebug() << "git " << args.join(" "); QProcess p; p.setProcessChannelMode(QProcess::ProcessChannelMode::MergedChannels); p.start(gitBinary, args); p.waitForFinished(); - return QString::fromUtf8(p.readAllStandardOutput()); + return {QString::fromUtf8(p.readAllStandardOutput()), p.exitCode()}; } QString GitPlugin::detectRepoRoot(const QString &filePath) { - QProcess p; - p.setWorkingDirectory(QFileInfo(filePath).absolutePath()); - p.start(gitBinary, {"rev-parse", "--show-toplevel"}); - p.waitForFinished(); - return QString::fromUtf8(p.readAllStandardOutput()).trimmed(); + auto dir = QFileInfo(filePath).absolutePath(); + auto args = QStringList{"-C", dir, "rev-parse", "--show-toplevel"}; + auto [output, exitCode] = runGit(args); + if (exitCode != 0) { + qDebug() << "detectRepoRoot failed for file" << filePath << ", with error" << exitCode + << "output" << output << "args:" << args; + return {}; + } + return output.trimmed(); } QString GitPlugin::getDiff(const QString &path) { auto fi = QFileInfo(path); - return runGit({"-C", fi.absolutePath(), "diff"}); + auto [output, exitCode] = runGit({"-C", fi.absolutePath(), "diff"}); + return output; } QString GitPlugin::getRawCommit(const QString &sha1) { - return runGit({"-C", getConfig().getGitLastDir(), "show", sha1}); + auto [output, exitCode] = runGit({"-C", getConfig().getGitLastDir(), "show", sha1}); + return output; } void GitPlugin::restoreGitLog() { @@ -466,7 +528,7 @@ void GitPlugin::restoreGitLog() { auto args = cmd.split(" "); auto model = new CommitModel(this); form->label->setText(cmd); - auto output = runGit(args); + auto [output, exitCode] = runGit(args); model->setContent(output); form->listView->setModel(model); diff --git a/src/plugins/git/GitPlugin.hpp b/src/plugins/git/GitPlugin.hpp index 9b70e65..a613569 100644 --- a/src/plugins/git/GitPlugin.hpp +++ b/src/plugins/git/GitPlugin.hpp @@ -1,6 +1,7 @@ #pragma once #include "iplugin.h" +#include namespace Ui { class GitCommandsForm; @@ -31,6 +32,9 @@ class GitPlugin : public IPlugin { virtual void on_client_merged(qmdiHost *host) override; virtual void on_client_unmerged(qmdiHost *host) override; virtual void loadConfig(QSettings &settings) override; + virtual int canOpenFile(const QString &fileName) override; + virtual qmdiClient *openFile(const QString &fileName, int x = -1, int y = -1, + int z = -1) override; public slots: void logFileHandler(); @@ -41,12 +45,14 @@ class GitPlugin : public IPlugin { void diffBranchHandler(); void newBranchHandler(); void deleteBranchHandler(); + void commitDisplayHandler(const QModelIndex &mi); + void commitHandler(); void logHandler(GitPlugin::GitLog log, const QString &filename); void on_gitCommitClicked(const QModelIndex &mi); void on_gitCommitDoubleClicked(const QModelIndex &mi); public slots: - QString runGit(const QStringList &args); + std::tuple runGit(const QStringList &args); QString detectRepoRoot(const QString &path); QString getDiff(const QString &path); QString getRawCommit(const QString &sha1); diff --git a/src/plugins/texteditor/texteditor_plg.cpp b/src/plugins/texteditor/texteditor_plg.cpp index af87dbe..d24e3eb 100644 --- a/src/plugins/texteditor/texteditor_plg.cpp +++ b/src/plugins/texteditor/texteditor_plg.cpp @@ -25,7 +25,7 @@ #include "widgets/qmdieditor.h" TextEditorPlugin::TextEditorPlugin() { - name = tr("Text editor plugin - based on QutePart"); + name = "TextEditorPlugin"; author = tr("Diego Iastrubni "); iVersion = 0; sVersion = "0.0.1"; diff --git a/src/widgets/qmdieditor.cpp b/src/widgets/qmdieditor.cpp index a72978f..b8d782f 100644 --- a/src/widgets/qmdieditor.cpp +++ b/src/widgets/qmdieditor.cpp @@ -49,8 +49,8 @@ #include "GlobalCommands.hpp" #include "plugins/texteditor/thememanager.h" -#include "qmdieditor.h" #include "widgets/BoldItemDelegate.hpp" +#include "widgets/qmdieditor.h" #include "widgets/textoperationswidget.h" #include "widgets/textpreview.h" #include "widgets/ui_bannermessage.h" @@ -955,7 +955,8 @@ bool qmdiEditor::eventFilter(QObject *watched, QEvent *event) { auto text = block.text(); if (diffMetadata.mappings.contains(blockNumber)) { auto l = diffMetadata.mappings[blockNumber]; - auto pluginManager = dynamic_cast(mdiServer->mdiHost); + auto pluginManager = mdiServer ? + dynamic_cast(mdiServer->mdiHost) : nullptr; if (pluginManager) { // Lines start on the editor from 0 if (l.newLine >= 0) { diff --git a/src/widgets/qmdieditor.h b/src/widgets/qmdieditor.h index 12bd195..67c1f46 100644 --- a/src/widgets/qmdieditor.h +++ b/src/widgets/qmdieditor.h @@ -179,8 +179,13 @@ class qmdiEditor : public QWidget, public qmdiClient { } return textEditor->document()->isEmpty(); } - inline void foldTopLevel() const { textEditor->foldTopLevelBlocks(); }; + inline void foldTopLevel() const { textEditor->foldTopLevelBlocks(); } + inline void setMinimapVisible(bool value) const { textEditor->setMinimapVisible(value); } void setReadOnly(bool b); + inline QPlainTextEdit *getEditor() const { return textEditor; } + inline void setHighlighter(const QString &languageId) { + textEditor->setHighlighter(languageId); + } protected: virtual void focusInEvent(QFocusEvent *event) override;