diff --git a/.gitignore b/.gitignore index edbbfdfca..6786e6c10 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ build CMakeLists.txt.user cmake-build-debug/ cmake-build-release/ +build .idea/ +.venv diff --git a/cl-fmt.sh b/cl-fmt.sh index 05922b135..2243194eb 100755 --- a/cl-fmt.sh +++ b/cl-fmt.sh @@ -1,5 +1,6 @@ #!/bin/bash +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd "`dirname "$0"`" # Variable that will hold the name of the clang-format command @@ -7,12 +8,11 @@ FMT="" FOLDERS=("./src" "./test") -# Some distros just call it clang-format. Others (e.g. Ubuntu) are insistent -# that the version number be part of the command. We prefer clang-format if -# that's present, otherwise we work backwards from highest version to lowest -# version but at least 13. -for clangfmt in clang-format{,-{1,2,3}{9,8,7,6,5,4,3}}; do - if which "$clangfmt" &>/dev/null; then +# We specifically require clang-format v13. Some distros include the version +# number in the name, others don't. Prefer the specifically-named version. +for clangfmt in clang-format-13 clang-format +do + if command -v "$clangfmt" &>/dev/null; then FMT="$clangfmt" break fi @@ -24,6 +24,14 @@ if [ -z "$FMT" ]; then exit 1 fi +# Check we have v13 of clang-format +VERSION=`$FMT --version | grep -Po 'version\s\K(\d+)'` +if [ "$VERSION" != "13" ]; then + echo "Found clang-format v$VERSION, but v13 is required. Please install v13 of clang-format and try again." + echo "On Debian-derived distributions, this can be done via: apt install clang-format-13" + exit 1 +fi + function format() { for f in $(find $@ \( -type d -path './test/dep/*' -prune \) -o \( -name '*.h' -or -name '*.m' -or -name '*.mm' -or -name '*.c' -or -name '*.cpp' \)); do echo "format ${f}"; @@ -41,9 +49,18 @@ for dir in ${FOLDERS[@]}; do fi done +# Format cmake files +# NOTE: requires support for python venv; on Debian-like distros, this can be +# installed using apt install python3-venv echo "Start formatting cmake files" -pip install cmake-format==0.6.13 +CMAKE_FORMAT=${SCRIPT_DIR}/.venv/bin/cmake-format +if [ ! -f "$CMAKE_FORMAT" ]; then + pushd ${SCRIPT_DIR} + python3 -m venv .venv + .venv/bin/pip install cmake-format==0.6.13 + popd +fi find . \ \( -type d -path './test/dep/*' -prune \) \ -o \( -type d -path './dep/*/*' -prune \) \ - -o \( -name CMakeLists.txt -exec cmake-format --in-place {} + \) + -o \( -name CMakeLists.txt -exec "$CMAKE_FORMAT" --in-place {} + \) diff --git a/docs/changelog.md b/docs/changelog.md index bd984193b..567609a77 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,8 +4,15 @@ Description #### Added +* UI(Commit List): Added a right-click menu entry to rename branches. +* UI(Main Menu): Added a menu-entry to rename the current branch. + #### Changed +* UI(Commit List): Collapse multiple branch and tag right-click menu entries + into submenus. This affects the checkout and delete operations. +* Fix(Build System): Force usage of clang-format v13 to ensure consistent formatting. + ---- ### v1.3.0 - 2023-04-20 diff --git a/src/dialogs/CMakeLists.txt b/src/dialogs/CMakeLists.txt index bcb82b873..0295257e2 100644 --- a/src/dialogs/CMakeLists.txt +++ b/src/dialogs/CMakeLists.txt @@ -24,6 +24,7 @@ add_library( RebaseConflictDialog.cpp RemoteDialog.cpp RemoteTableModel.cpp + RenameBranchDialog.cpp SettingsDialog.cpp StartDialog.cpp SubmoduleDelegate.cpp diff --git a/src/dialogs/RenameBranchDialog.cpp b/src/dialogs/RenameBranchDialog.cpp new file mode 100644 index 000000000..b7580ed4f --- /dev/null +++ b/src/dialogs/RenameBranchDialog.cpp @@ -0,0 +1,58 @@ +// This software is licensed under the MIT License. The LICENSE.md file +// describes the conditions under which this software may be distributed. +// +// Author: Michael WERLE +// + +#include "RenameBranchDialog.h" +#include "git/Branch.h" +#include "ui/ExpandButton.h" +#include "ui/ReferenceList.h" +#include "ui/RepoView.h" +#include +#include +#include +#include +#include +#include +#include + +RenameBranchDialog::RenameBranchDialog(const git::Repository &repo, + const git::Branch &branch, + QWidget *parent) + : QDialog(parent) { + Q_ASSERT(branch.isValid() && branch.isLocalBranch()); + setAttribute(Qt::WA_DeleteOnClose); + + mName = new QLineEdit(branch.name(), this); + mName->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); + mName->setMinimumWidth(QFontMetrics(mName->font()).averageCharWidth() * 40); + + QFormLayout *form = new QFormLayout; + form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + form->addRow(tr("Name:"), mName); + + QDialogButtonBox *buttons = new QDialogButtonBox(this); + buttons->addButton(QDialogButtonBox::Cancel); + QPushButton *rename = + buttons->addButton(tr("Rename Branch"), QDialogButtonBox::AcceptRole); + rename->setEnabled(false); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->addLayout(form); + layout->addWidget(buttons); + + // Update button when name text changes. + connect(mName, &QLineEdit::textChanged, [repo, rename](const QString &text) { + rename->setEnabled(git::Branch::isNameValid(text) && + !repo.lookupBranch(text, GIT_BRANCH_LOCAL).isValid()); + }); + + // Perform the rename when the button is clicked + connect(rename, &QPushButton::clicked, + [this, branch] { git::Branch(branch).rename(mName->text()); }); +} + +QString RenameBranchDialog::name() const { return mName->text(); } diff --git a/src/dialogs/RenameBranchDialog.h b/src/dialogs/RenameBranchDialog.h new file mode 100644 index 000000000..5aa4538c8 --- /dev/null +++ b/src/dialogs/RenameBranchDialog.h @@ -0,0 +1,33 @@ +// This software is licensed under the MIT License. The LICENSE.md file +// describes the conditions under which this software may be distributed. +// +// Author: Michael WERLE +// + +#ifndef RENAMEBRANCHDIALOG_H +#define RENAMEBRANCHDIALOG_H + +#include "git/Branch.h" +#include + +class QLineEdit; + +namespace git { +class Reference; +class Repository; +} // namespace git + +class RenameBranchDialog : public QDialog { + Q_OBJECT + +public: + RenameBranchDialog(const git::Repository &repo, const git::Branch &branch, + QWidget *parent = nullptr); + + QString name() const; + +private: + QLineEdit *mName; +}; + +#endif diff --git a/src/ui/CommitList.cpp b/src/ui/CommitList.cpp index 23dc1067d..c3cb132b7 100644 --- a/src/ui/CommitList.cpp +++ b/src/ui/CommitList.cpp @@ -1426,6 +1426,24 @@ void CommitList::setModel(QAbstractItemModel *model) { restoreSelection(); } +/// @brief Helper function to add a list of items to a menu. +/// A single item is added directly to the menu, whereas multiple items will +/// be added to a sub-menu. +static void addMenuEntries(QMenu &menu, const QString &operation, + const QList &items, + std::function action) { + QMenu *submenu = &menu; + QString entryName(operation + " %1"); + if (items.count() > 1) { + submenu = menu.addMenu(operation); + entryName = QString("%1"); + } + for (const git::Reference &ref : items) { + submenu->addAction(entryName.arg(ref.name()), + [action, ref] { action(ref); }); + } +} + void CommitList::contextMenuEvent(QContextMenuEvent *event) { QModelIndex index = indexAt(event->pos()); if (!index.isValid()) @@ -1496,26 +1514,41 @@ void CommitList::contextMenuEvent(QContextMenuEvent *event) { menu.addAction(tr("New Branch..."), [view, commit] { view->promptToCreateBranch(commit); }); - bool separator = true; - foreach (const git::Reference &ref, commit.refs()) { + // Add operations on existing references; there may be 0, 1, or multiple + // of each type of reference on a commit. + QList rename_branches; + QList tags; + QList delete_branches; + QList all_branches; // used later + for (const git::Reference &ref : commit.refs()) { if (ref.isTag()) { - if (separator) { - menu.addSeparator(); - separator = false; - } - menu.addAction(tr("Delete Tag %1").arg(ref.name()), - [view, ref] { view->promptToDeleteTag(ref); }); - } - if (ref.isLocalBranch() && (view->repo().head().name() != ref.name())) { - if (separator) { - menu.addSeparator(); - separator = false; + tags.append(ref); + } else if (ref.isBranch()) { + all_branches.append(ref); + if (ref.isLocalBranch()) { + rename_branches.append(ref); + if (view->repo().head().name() != ref.name()) { + delete_branches.append(ref); + } } - menu.addAction(tr("Delete Branch %1").arg(ref.name()), - [view, ref] { view->promptToDeleteBranch(ref); }); } } + if (rename_branches.count() > 0 || delete_branches.count() > 0 || + tags.count() > 0) { + menu.addSeparator(); + } + addMenuEntries(menu, tr("Rename Branch"), rename_branches, + std::bind(&RepoView::promptToRenameBranch, view, + std::placeholders::_1)); + + addMenuEntries(menu, tr("Delete Branch"), delete_branches, + std::bind(&RepoView::promptToDeleteBranch, view, + std::placeholders::_1)); + + addMenuEntries( + menu, tr("Delete Tag"), tags, + std::bind(&RepoView::promptToDeleteTag, view, std::placeholders::_1)); menu.addSeparator(); menu.addAction(tr("Merge..."), [view, commit] { @@ -1573,19 +1606,23 @@ void CommitList::contextMenuEvent(QContextMenuEvent *event) { menu.addSeparator(); git::Reference head = view->repo().head(); - foreach (const git::Reference &ref, commit.refs()) { + auto submenu = &menu; + auto entryName = tr("Checkout %1"); + if (all_branches.count() > 1) { + submenu = menu.addMenu(tr("Checkout")); + entryName = QString("%1"); + } + for (const git::Reference &ref : all_branches) { if (ref.isLocalBranch()) { - QAction *checkout = - menu.addAction(tr("Checkout %1").arg(ref.name()), - [view, ref] { view->checkout(ref); }); + QAction *checkout = submenu->addAction( + entryName.arg(ref.name()), [view, ref] { view->checkout(ref); }); checkout->setEnabled(head.isValid() && head.qualifiedName() != ref.qualifiedName() && !view->repo().isBare()); } else if (ref.isRemoteBranch()) { - QAction *checkout = - menu.addAction(tr("Checkout %1").arg(ref.name()), - [view, ref] { view->checkout(ref); }); + QAction *checkout = submenu->addAction( + entryName.arg(ref.name()), [view, ref] { view->checkout(ref); }); // Calculate local branch name in the same way as checkout() does QString local = ref.name().section('/', 1); diff --git a/src/ui/MenuBar.cpp b/src/ui/MenuBar.cpp index 4f14cec16..9c7549006 100644 --- a/src/ui/MenuBar.cpp +++ b/src/ui/MenuBar.cpp @@ -192,6 +192,9 @@ static Hotkey configureBranchesHotkey = HotkeyManager::registerHotkey( static Hotkey newBranchHotkey = HotkeyManager::registerHotkey(nullptr, "branch/new", "Branch/New"); +static Hotkey renameBranchHotkey = + HotkeyManager::registerHotkey(nullptr, "branch/rename", "Branch/Rename"); + static Hotkey checkoutCurrentHotkey = HotkeyManager::registerHotkey( "Ctrl+Shift+Alt+H", "branch/checkoutCurrent", "Branch/Checkout Current"); @@ -640,6 +643,12 @@ MenuBar::MenuBar(QWidget *parent) : QMenuBar(parent) { connect(mNewBranch, &QAction::triggered, [this] { view()->promptToCreateBranch(); }); + mRenameBranch = branch->addAction(tr("Rename Branch")); + renameBranchHotkey.use(mRenameBranch); + connect(mRenameBranch, &QAction::triggered, [this] { + this->view()->promptToRenameBranch(this->view()->reference()); + }); + branch->addSeparator(); mCheckoutCurrent = branch->addAction(tr("Checkout Current")); @@ -1050,6 +1059,7 @@ void MenuBar::updateBranch() { mCheckoutCurrent->setEnabled(ref.isValid() && head.isValid() && ref.qualifiedName() != head.qualifiedName()); mCheckout->setEnabled(head.isValid() && !view->repo().isBare()); + mRenameBranch->setEnabled(ref.isLocalBranch()); mNewBranch->setEnabled(head.isValid()); mMerge->setEnabled(head.isValid()); diff --git a/src/ui/MenuBar.h b/src/ui/MenuBar.h index 516eac261..7e04d898c 100644 --- a/src/ui/MenuBar.h +++ b/src/ui/MenuBar.h @@ -116,6 +116,7 @@ class MenuBar : public QMenuBar { QAction *mNewBranch; QAction *mCheckoutCurrent; QAction *mCheckout; + QAction *mRenameBranch; QAction *mMerge; QAction *mRebase; QAction *mSquash; diff --git a/src/ui/RepoView.cpp b/src/ui/RepoView.cpp index d7f9223ae..1c850d015 100644 --- a/src/ui/RepoView.cpp +++ b/src/ui/RepoView.cpp @@ -33,6 +33,7 @@ #include "dialogs/NewBranchDialog.h" #include "dialogs/RebaseConflictDialog.h" #include "dialogs/RemoteDialog.h" +#include "dialogs/RenameBranchDialog.h" #include "dialogs/SettingsDialog.h" #include "dialogs/TagDialog.h" #include "editor/TextEditor.h" @@ -2051,6 +2052,13 @@ void RepoView::promptToDeleteBranch(const git::Reference &ref) { dialog->open(); } +void RepoView::promptToRenameBranch(const git::Branch &branch) { + Q_ASSERT(branch.isValid() && branch.isLocalBranch()); + RenameBranchDialog *dialog = new RenameBranchDialog(mRepo, branch, this); + // The dialog contains the code which performs the rename + dialog->open(); +} + void RepoView::promptToStash() { // Prompt to edit stash commit message. if (!Settings::instance()->prompt(Prompt::Kind::Stash)) { diff --git a/src/ui/RepoView.h b/src/ui/RepoView.h index d045d85d4..fd9cbf2c9 100644 --- a/src/ui/RepoView.h +++ b/src/ui/RepoView.h @@ -251,6 +251,7 @@ class RepoView : public QSplitter { const git::Branch &upstream = git::Branch(), bool checkout = false, bool force = false); void promptToDeleteBranch(const git::Reference &ref); + void promptToRenameBranch(const git::Branch &branch); // stash void promptToStash();