diff --git a/.gitattributes b/.gitattributes
index 9f713b466..9d1ecabf4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,6 +1,7 @@
src/version.h.cmake export-subst
.gitattributes export-ignore
.gitignore export-ignore
+.github export-ignore
.travis.yml export-ignore
.tx export-ignore
snapcraft.yaml export-ignore
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 01b8d6137..67b0e1746 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,31 +1,32 @@
-# Contributing to KeePassX Reboot
+# Contributing to KeePassXC
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
-The following is a set of guidelines for contributing to KeePassX Reboot on GitHub.
+The following is a set of guidelines for contributing to KeePassXC on GitHub.
These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
-#### Table Of Contents
+#### Table of contents
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
* [Open Source Contribution Policy](#open-source-contribution-policy)
-[How Can I Contribute?](#how-can-i-contribute)
- * [Feature Requests](#feature-requests)
- * [Bug Reports](#bug-reports)
- * [Your First Code Contribution](#your-first-code-contribution)
- * [Pull Requests](#pull-requests)
+[How can I contribute?](#how-can-i-contribute)
+ * [Feature requests](#feature-requests)
+ * [Bug reports](#bug-reports)
+ * [Discuss with the team](#discuss-with-the-team)
+ * [Your first code contribution](#your-first-code-contribution)
+ * [Pull requests](#pull-requests)
* [Translations](#translations)
[Styleguides](#styleguides)
- * [Git Branch Strategy](#git_branch_strategy)
- * [Git Commit Messages](#git-commit-messages)
- * [Coding Styleguide](#coding-styleguide)
+ * [Git branch strategy](#git-branch-strategy)
+ * [Git commit messages](#git-commit-messages)
+ * [Coding styleguide](#coding-styleguide)
## What should I know before I get started?
### Open Source Contribution Policy
-[Version 0.3, 2015–11–18](https://medium.com/@jmaynard/a-contribution-policy-for-open-source-that-works-bfc4600c9d83#.i9ntbhmad)
+**Source**: [Version 0.3, 2015–11–18](https://medium.com/@jmaynard/a-contribution-policy-for-open-source-that-works-bfc4600c9d83#.i9ntbhmad)
#### Policy
@@ -49,35 +50,35 @@ If we reject your contribution, it means only that we do not consider it suitabl
* 0.3, 2011–11–19: Added “irrevocably” to “we can use” and changed “it” to “your contribution” in the “if rejected” section. Thanks to Patrick Maupin.
-## How Can I Contribute?
-### Feature Requests
+## How can I contribute?
+### Feature requests
-We're always looking for suggestions to improve our application. If you have a suggestion for improving an existing feature, or would like to suggest a completely new feature for KeePassX Reboot, please use the Issues section or our [Google Groups](https://groups.google.com/forum/#!forum/keepassx-reboot) forum.
+We're always looking for suggestions to improve our application. If you have a suggestion to improve an existing feature, or would like to suggest a completely new feature for KeePassXC, please use the [issue tracker on GitHub][issues-section]. For more general discussion, try using our [Google Groups][google-groups] forum.
-### Bug Reports
+### Bug reports
-Our software isn't always perfect, but we strive to always improve our work. You may file bug reports in the Issues section.
+Our software isn't always perfect, but we strive to always improve our work. You may file bug reports in the issue tracker.
-Before submitting a Bug Report, check if the problem has already been reported. Please refrain from opening a duplicate issue. If you want to highlight a deficiency on an existing issue, simply add a comment.
+Before submitting a bug report, check if the problem has already been reported. Please refrain from opening a duplicate issue. If you want to add further information to an existing issue, simply add a comment on that issue.
-### Discuss with the Team
+### Discuss with the team
-You can talk to the KeePassX Reboot Team about Bugs, new feature, Issue and PullRequests at our [Google Groups](https://groups.google.com/forum/#!forum/keepassx-reboot) forum
+As with feature requests, you can talk to the KeePassXC team about bugs, new features, other issues and pull requests on the dedicated issue tracker, using the [Google Groups][google-groups] forum, or in the IRC channel on Freenode (`#keepassxc-dev` on `irc.freenode.net`, or use a [webchat link](https://webchat.freenode.net/?channels=%23keepassxc-dev)).
-### Your First Code Contribution
+### Your first code contribution
-Unsure where to begin contributing to KeePassX Reboot? You can start by looking through these `beginner` and `help-wanted` issues:
+Unsure where to begin contributing to KeePassXC? You can start by looking through these `beginner` and `help-wanted` issues:
-* [Beginner issues][beginner] - issues which should only require a few lines of code, and a test or two.
-* [Help wanted issues][help-wanted] - issues which should be a bit more involved than `beginner` issues.
+* [Beginner issues][beginner] – issues which should only require a few lines of code, and a test or two.
+* ['Help wanted' issues][help-wanted] – issues which should be a bit more involved than `beginner` issues.
-Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have.
+Both issue lists are sorted by total number of comments. While not perfect, looking at the number of comments on an issue can give a general idea of how much an impact a given change will have.
-### Pull Requests
+### Pull requests
Along with our desire to hear your feedback and suggestions, we're also interested in accepting direct assistance in the form of code.
-All pull requests must comply with the above requirements and with the [Styleguides](#styleguides).
+All pull requests must comply with the above requirements and with the [styleguides](#styleguides).
### Translations
@@ -86,19 +87,20 @@ Please join an existing language team or request a new one if there is none.
## Styleguides
-### Git Branch Strategy
+### Git branch strategy
The Branch Strategy is based on [git-flow-lite](http://nvie.com/posts/a-successful-git-branching-model/).
-* **master** -> always points to the last release published
-* **develop** -> points to the next planned release, tested and reviewed code
-* **feature/**[name] -> points to brand new feature in codebase, candidate for merge into develop (subject to rebase)
+* **master** – points to the latest public release
+* **develop** – points to the development of the next release, contains tested and reviewed code
+* **feature/**[name] – points to a branch with a new feature, one which is candidate for merge into develop (subject to rebase)
+* **hotfix/**[id]-[description] – points to a branch with a fix for a particular issue ID
-### Git Commit Messages
+### Git commit messages
* Use the present tense ("Add feature" not "Added feature")
-* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
+* Use the imperative mood ("Move cursor to…" not "Moves cursor to…")
* Limit the first line to 72 characters or less
* Reference issues and pull requests liberally
* When only changing documentation, include `[ci skip]` in the commit description
@@ -114,21 +116,21 @@ The Branch Strategy is based on [git-flow-lite](http://nvie.com/posts/a-successf
* :lock: `:lock:` when dealing with security
-### Coding Styleguide
+### Coding styleguide
This project follows the [Qt Coding Style](https://wiki.qt.io/Qt_Coding_Style). All submissions are expected to follow this style.
-In particular Code must follow the following specific rules:
+In particular, code must stick to the following rules:
-#### Naming Convention
+#### Naming convention
`lowerCamelCase`
-For names made of only one word, the fist letter is lowercase.
-For names made of multiple concatenated words, the first letter is lowercase and each subsequent concatenated word is capitalized.
+For names made of only one word, the first letter should be lowercase.
+For names made of multiple concatenated words, the first letter of the whole is lowercase, and the first letter of each subsequent word is capitalized.
#### Indention
-For C++ files (.cpp .h): 4 spaces
-For Qt-UI files (.ui): 2 spaces
+For **C++ files** (*.cpp .h*): 4 spaces
+For **Qt-UI files** (*.ui*): 2 spaces
#### Pointers
```c
@@ -165,9 +167,8 @@ Use prefix: `m_*`
Example: `m_variable`
-#### GUI Widget names
-Widget names must be related to the desired program behaviour.
-Preferably end the name with the Widget Classname
+#### GUI widget names
+Widget names must be related to the desired program behavior, and preferably end with the widget's classname.
Example: ``
@@ -175,3 +176,5 @@ Example: ``
[beginner]:https://github.com/keepassxreboot/keepassx/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3A%22help+wanted%22+sort%3Acomments-desc
[help-wanted]:https://github.com/keepassxreboot/keepassx/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22+sort%3Acomments-desc
+[issues-section]:https://github.com/keepassxreboot/keepassxc/issues
+[google-groups]:https://groups.google.com/forum/#!forum/keepassx-reboot
diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh
index 9575f077b..dc30cb696 100755
--- a/AppImage-Recipe.sh
+++ b/AppImage-Recipe.sh
@@ -39,26 +39,54 @@ mkdir -p $APP.AppDir
wget -q https://github.com/probonopd/AppImages/raw/master/functions.sh -O ./functions.sh
. ./functions.sh
+LIB_DIR=./usr/lib
+if [ -d ./usr/lib/x86_64-linux-gnu ]; then
+ LIB_DIR=./usr/lib/x86_64-linux-gnu
+fi
+
cd $APP.AppDir
cp -a ../../bin-release/* .
-mv ./usr/local/* ./usr
-rmdir ./usr/local
-patch_strings_in_file /usr/local ./
+cp -a ./usr/local/* ./usr
+rm -R ./usr/local
+rmdir ./opt 2> /dev/null
+patch_strings_in_file /usr/local ././
patch_strings_in_file /usr ./
+# bundle Qt platform plugins and themes
+QXCB_PLUGIN="$(find /usr/lib -name 'libqxcb.so' 2> /dev/null)"
+if [ "$QXCB_PLUGIN" == "" ]; then
+ QXCB_PLUGIN="$(find /opt/qt*/plugins -name 'libqxcb.so' 2> /dev/null)"
+fi
+QT_PLUGIN_PATH="$(dirname $(dirname $QXCB_PLUGIN))"
+mkdir -p ".${QT_PLUGIN_PATH}/platforms"
+cp "$QXCB_PLUGIN" ".${QT_PLUGIN_PATH}/platforms/"
+
get_apprun
copy_deps
delete_blacklisted
+# remove dbus and systemd libs as they are not blacklisted
+find . -name libdbus-1.so.3 -exec rm {} \;
+find . -name libsystemd.so.0 -exec rm {} \;
+
get_desktop
get_icon
+cat << EOF > ./usr/bin/keepassxc_env
+#!/usr/bin/env bash
+#export QT_QPA_PLATFORMTHEME=gtk2
+export LD_LIBRARY_PATH="../opt/qt58/lib:\${LD_LIBRARY_PATH}"
+export QT_PLUGIN_PATH="..${QT_PLUGIN_PATH}"
+exec keepassxc "\$@"
+EOF
+chmod +x ./usr/bin/keepassxc_env
+sed -i 's/Exec=keepassxc/Exec=keepassxc_env/' keepassxc.desktop
get_desktopintegration $LOWERAPP
GLIBC_NEEDED=$(glibc_needed)
cd ..
-generate_appimage
+generate_type2_appimage
mv ../out/*.AppImage ..
rmdir ../out > /dev/null 2>&1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 051aba199..2fd890e01 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -172,6 +172,8 @@ set(CMAKE_AUTOMOC ON)
# Make sure we don't enable asserts there.
set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS_NONE QT_NO_DEBUG)
+find_package(LibGPGError REQUIRED)
+
find_package(Gcrypt 1.6.0 REQUIRED)
if (WITH_XC_HTTP)
diff --git a/Dockerfile b/Dockerfile
index 3aee19e3c..9623b60dd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,21 +14,41 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-FROM ubuntu:16.04
+FROM ubuntu:14.04
-RUN set -x && apt-get update
RUN set -x \
+ && apt-get update \
+ && apt-get install --yes software-properties-common
+
+RUN set -x \
+ && add-apt-repository --yes ppa:beineri/opt-qt58-trusty
+
+RUN set -x \
+ && apt-get update \
&& apt-get install --yes \
+ g++ \
cmake \
libgcrypt20-dev \
- qtbase5-dev \
- qttools5-dev-tools \
+ qt58base \
+ qt58tools \
+ qt58x11extras \
libmicrohttpd-dev \
- libqt5x11extras5-dev \
libxi-dev \
libxtst-dev \
- zlib1g-dev
+ zlib1g-dev \
+ wget \
+ file \
+ fuse \
+ python
+RUN set -x \
+ && apt-get install --yes mesa-common-dev
+
VOLUME /keepassxc/src
VOLUME /keepassxc/out
WORKDIR /keepassxc
+
+ENV CMAKE_PREFIX_PATH=/opt/qt58/lib/cmake
+ENV LD_LIBRARY_PATH=/opt/qt58/lib
+RUN set -x \
+ && echo /opt/qt58/lib > /etc/ld.so.conf.d/qt58.conf
diff --git a/cmake/FindLibGPGError.cmake b/cmake/FindLibGPGError.cmake
new file mode 100644
index 000000000..fe9ef9123
--- /dev/null
+++ b/cmake/FindLibGPGError.cmake
@@ -0,0 +1,9 @@
+
+find_path(GPGERROR_INCLUDE_DIR gpg-error.h)
+
+find_library(GPGERROR_LIBRARIES gpg-error)
+
+mark_as_advanced(GPGERROR_LIBRARIES GPGERROR_INCLUDE_DIR)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(LibGPGError DEFAULT_MSG GPGERROR_LIBRARIES GPGERROR_INCLUDE_DIR)
diff --git a/make_release.sh b/make_release.sh
deleted file mode 100755
index 19c661b38..000000000
--- a/make_release.sh
+++ /dev/null
@@ -1,350 +0,0 @@
-#!/usr/bin/env bash
-#
-# KeePassXC Release Preparation Helper
-# Copyright (C) 2017 KeePassXC team
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 or (at your option)
-# version 3 of the License.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-echo -e "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper"
-echo -e "Copyright (C) 2017 KeePassXC Team \n"
-
-
-# default values
-RELEASE_NAME=""
-APP_NAME="KeePassXC"
-APP_NAME_LOWER="keepassxc"
-SRC_DIR="."
-GPG_KEY="CFB4C2166397D0D2"
-GPG_GIT_KEY=""
-OUTPUT_DIR="release"
-BRANCH=""
-RELEASE_BRANCH="master"
-TAG_NAME=""
-BUILD_SOURCES=false
-DOCKER_IMAGE=""
-DOCKER_CONTAINER_NAME="${APP_NAME_LOWER}-build-container"
-CMAKE_OPTIONS=""
-COMPILER="g++"
-MAKE_OPTIONS="-j8"
-BUILD_PLUGINS="autotype"
-INSTALL_PREFIX="/usr/local"
-
-ORIG_BRANCH="$(git rev-parse --abbrev-ref HEAD 2> /dev/null)"
-ORIG_CWD="$(pwd)"
-
-
-# helper functions
-printUsage() {
- echo -e "\e[1mUsage:\e[0m $(basename $0) [options]"
- cat << EOF
-
-Options:
- -v, --version Release version number or name (required)
- -a, --app-name Application name (default: '${APP_NAME}')
- -s, --source-dir Source directory (default: '${SRC_DIR}')
- -k, --gpg-key GPG key used to sign the release tarball
- (default: '${GPG_KEY}')
- -g, --gpg-git-key GPG key used to sign the merge commit and release tag,
- leave empty to let Git choose your default key
- (default: '${GPG_GIT_KEY}')
- -o, --output-dir Output directory where to build the release
- (default: '${OUTPUT_DIR}')
- --develop-branch Development branch to merge from (default: 'release/VERSION')
- --release-branch Target release branch to merge to (default: '${RELEASE_BRANCH}')
- -t, --tag-name Override release tag name (defaults to version number)
- -b, --build Build sources after exporting release
- -d, --docker-image Use the specified Docker image to compile the application.
- The image must have all required build dependencies installed.
- This option has no effect if --build is not set.
- --container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}')
- The container must not exist already
- -c, --cmake-options Additional CMake options for compiling the sources
- --compiler Compiler to use (default: '${COMPILER}')
- -m, --make-options Make options for compiling sources (default: '${MAKE_OPTIONS}')
- -i, --install-prefix Install prefix (default: '${INSTALL_PREFIX}')
- -p, --plugins Space-separated list of plugins to build
- (default: ${BUILD_PLUGINS})
- -h, --help Show this help
-
-EOF
-}
-
-logInfo() {
- echo -e "\e[1m[ \e[34mINFO\e[39m ]\e[0m $1"
-}
-
-logError() {
- echo -e "\e[1m[ \e[31mERROR\e[39m ]\e[0m $1" >&2
-}
-
-exitError() {
- logError "$1"
- if [ "" != "$ORIG_BRANCH" ]; then
- git checkout "$ORIG_BRANCH" > /dev/null 2>&1
- fi
- cd "$ORIG_CWD"
- exit 1
-}
-
-
-# parse command line options
-while [ $# -ge 1 ]; do
- arg="$1"
-
- case "$arg" in
- -a|--app-name)
- APP_NAME="$2"
- shift ;;
-
- -s|--source-dir)
- SRC_DIR"$2"
- shift ;;
-
- -v|--version)
- RELEASE_NAME="$2"
- shift ;;
-
- -k|--gpg-key)
- GPG_KEY="$2"
- shift ;;
-
- -g|--gpg-git-key)
- GPG_GIT_KEY="$2"
- shift ;;
-
- -o|--output-dir)
- OUTPUT_DIR="$2"
- shift ;;
-
- --develop-branch)
- BRANCH="$2"
- shift ;;
-
- --release-branch)
- RELEASE_BRANCH="$2"
- shift ;;
-
- -t|--tag-name)
- TAG_NAME="$2"
- shift ;;
-
- -b|--build)
- BUILD_SOURCES=true ;;
-
- -d|--docker-image)
- DOCKER_IMAGE="$2"
- shift ;;
-
- --container-name)
- DOCKER_CONTAINER_NAME="$2"
- shift ;;
-
- -c|--cmake-options)
- CMAKE_OPTIONS="$2"
- shift ;;
-
- -m|--make-options)
- MAKE_OPTIONS="$2"
- shift ;;
-
- --compiler)
- COMPILER="$2"
- shift ;;
-
- -p|--plugins)
- BUILD_PLUGINS="$2"
- shift ;;
-
- -h|--help)
- printUsage
- exit ;;
-
- *)
- logError "Unknown option '$arg'\n"
- printUsage
- exit 1 ;;
- esac
- shift
-done
-
-
-if [ "" == "$RELEASE_NAME" ]; then
- logError "Missing arguments, --version is required!\n"
- printUsage
- exit 1
-fi
-
-if [ "" == "$TAG_NAME" ]; then
- TAG_NAME="$RELEASE_NAME"
-fi
-if [ "" == "$BRANCH" ]; then
- BRANCH="release/${RELEASE_NAME}"
-fi
-APP_NAME_LOWER="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')"
-APP_NAME_UPPER="$(echo "$APP_NAME" | tr '[:lower:]' '[:upper:]')"
-
-SRC_DIR="$(realpath "$SRC_DIR")"
-OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
-if [ ! -d "$SRC_DIR" ]; then
- exitError "Source directory '${SRC_DIR}' does not exist!"
-fi
-
-logInfo "Changing to source directory..."
-cd "${SRC_DIR}"
-
-logInfo "Performing basic checks..."
-
-if [ -e "$OUTPUT_DIR" ]; then
- exitError "Output directory '$OUTPUT_DIR' already exists. Please choose a different location!"
-fi
-
-if [ ! -d .git ] || [ ! -f CHANGELOG ]; then
- exitError "Source directory is not a valid Git repository!"
-fi
-
-git tag | grep -q "$RELEASE_NAME"
-if [ $? -eq 0 ]; then
- exitError "Release '$RELEASE_NAME' already exists!"
-fi
-
-git diff-index --quiet HEAD --
-if [ $? -ne 0 ]; then
- exitError "Current working tree is not clean! Please commit or unstage any changes."
-fi
-
-git checkout "$BRANCH" > /dev/null 2>&1
-if [ $? -ne 0 ]; then
- exitError "Source branch '$BRANCH' does not exist!"
-fi
-
-grep -q "${APP_NAME_UPPER}_VERSION \"${RELEASE_NAME}\"" CMakeLists.txt
-if [ $? -ne 0 ]; then
- exitError "${APP_NAME_UPPER}_VERSION version not updated to '${RELEASE_NAME}' in CMakeLists.txt!"
-fi
-
-grep -q "${APP_NAME_UPPER}_VERSION_NUM \"${RELEASE_NAME}\"" CMakeLists.txt
-if [ $? -ne 0 ]; then
- exitError "${APP_NAME_UPPER}_VERSION_NUM version not updated to '${RELEASE_NAME}' in CMakeLists.txt!"
-fi
-
-if [ ! -f CHANGELOG ]; then
- exitError "No CHANGELOG file found!"
-fi
-
-grep -qPzo "${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n=+\n" CHANGELOG
-if [ $? -ne 0 ]; then
- exitError "CHANGELOG does not contain any information about the '${RELEASE_NAME}' release!"
-fi
-
-git checkout "$RELEASE_BRANCH" > /dev/null 2>&1
-if [ $? -ne 0 ]; then
- exitError "Release branch '$RELEASE_BRANCH' does not exist!"
-fi
-
-logInfo "All checks pass, getting our hands dirty now!"
-
-logInfo "Merging '${BRANCH}' into '${RELEASE_BRANCH}'..."
-
-CHANGELOG=$(grep -Pzo "(?<=${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n)=+\n\n(?:.|\n)+?\n(?=\n)" \
- CHANGELOG | grep -Pzo '(?<=\n\n)(.|\n)+' | tr -d \\0)
-COMMIT_MSG="Release ${RELEASE_NAME}"
-
-git merge "$BRANCH" --no-ff -m "$COMMIT_MSG" -m "${CHANGELOG}" "$BRANCH" -S"$GPG_GIT_KEY"
-
-logInfo "Creating tag '${RELEASE_NAME}'..."
-if [ "" == "$GPG_GIT_KEY" ]; then
- git tag -a "$RELEASE_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s
-else
- git tag -a "$RELEASE_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s -u "$GPG_GIT_KEY"
-fi
-
-logInfo "Merge done, creating target directory..."
-mkdir -p "$OUTPUT_DIR"
-
-if [ $? -ne 0 ]; then
- exitError "Failed to create output directory!"
-fi
-
-logInfo "Creating source tarball..."
-TARBALL_NAME="${APP_NAME_LOWER}-${RELEASE_NAME}-src.tar.bz2"
-git archive --format=tar "$RELEASE_BRANCH" --prefix="${APP_NAME_LOWER}-${RELEASE_NAME}/" \
- | bzip2 -9 > "${OUTPUT_DIR}/${TARBALL_NAME}"
-
-
-if $BUILD_SOURCES; then
- logInfo "Creating build directory..."
- mkdir -p "${OUTPUT_DIR}/build-release"
- mkdir -p "${OUTPUT_DIR}/bin-release"
- cd "${OUTPUT_DIR}/build-release"
-
- logInfo "Configuring sources..."
- for p in $BUILD_PLUGINS; do
- CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_XC_$(echo $p | tr '[:lower:]' '[:upper:]')=On"
- done
-
- if [ "$COMPILER" == "g++" ]; then
- export CC=gcc
- elif [ "$COMPILER" == "clang++" ]; then
- export CC=clang
- fi
- export CXX="$COMPILER"
-
- if [ "" == "$DOCKER_IMAGE" ]; then
- cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \
- -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR"
-
- logInfo "Compiling sources..."
- make $MAKE_OPTIONS
-
- logInfo "Installing to bin dir..."
- make DESTDIR="${OUTPUT_DIR}/bin-release" install/strip
- else
- logInfo "Launching Docker container to compile sources..."
-
- docker run --name "$DOCKER_CONTAINER_NAME" --rm \
- -e "CC=${CC}" -e "CXX=${CXX}" \
- -v "$(realpath "$SRC_DIR"):/keepassxc/src:ro" \
- -v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \
- "$DOCKER_IMAGE" \
- bash -c "cd /keepassxc/out/build-release && \
- cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \
- -DCMAKE_INSTALL_PREFIX=\"${INSTALL_PREFIX}\" /keepassxc/src && \
- make $MAKE_OPTIONS && make DESTDIR=/keepassxc/out/bin-release install/strip"
-
- logInfo "Build finished, Docker container terminated."
- fi
-
- logInfo "Creating AppImage..."
- ${SRC_DIR}/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME"
-
- cd ..
- logInfo "Signing source tarball..."
- gpg --output "${TARBALL_NAME}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$TARBALL_NAME"
-
- logInfo "Signing AppImage..."
- APPIMAGE_NAME="${APP_NAME}-${RELEASE_NAME}-x86_64.AppImage"
- gpg --output "${APPIMAGE_NAME}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$APPIMAGE_NAME"
-
- logInfo "Creating digests..."
- sha256sum "$TARBALL_NAME" > "${TARBALL_NAME}.DIGEST"
- sha256sum "$APPIMAGE_NAME" > "${APPIMAGE_NAME}.DIGEST"
-fi
-
-logInfo "Leaving source directory..."
-cd "$ORIG_CWD"
-git checkout "$ORIG_BRANCH" > /dev/null 2>&1
-
-logInfo "All done!"
-logInfo "Please merge the release branch back into the develop branch now and then push your changes."
-logInfo "Don't forget to also push the tags using \e[1mgit push --tags\e[0m."
diff --git a/release-tool b/release-tool
new file mode 100755
index 000000000..7bc54cda0
--- /dev/null
+++ b/release-tool
@@ -0,0 +1,682 @@
+#!/usr/bin/env bash
+#
+# KeePassXC Release Preparation Helper
+# Copyright (C) 2017 KeePassXC team
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 or (at your option)
+# version 3 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+echo -e "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper"
+echo -e "Copyright (C) 2017 KeePassXC Team \n"
+
+
+# -----------------------------------------------------------------------
+# global default values
+# -----------------------------------------------------------------------
+RELEASE_NAME=""
+APP_NAME="KeePassXC"
+SRC_DIR="."
+GPG_KEY="CFB4C2166397D0D2"
+GPG_GIT_KEY=""
+OUTPUT_DIR="release"
+SOURCE_BRANCH=""
+TARGET_BRANCH="master"
+TAG_NAME=""
+DOCKER_IMAGE=""
+DOCKER_CONTAINER_NAME="keepassxc-build-container"
+CMAKE_OPTIONS=""
+COMPILER="g++"
+MAKE_OPTIONS="-j8"
+BUILD_PLUGINS="autotype"
+INSTALL_PREFIX="/usr/local"
+BUILD_SOURCE_TARBALL=true
+ORIG_BRANCH=""
+ORIG_CWD="$(pwd)"
+
+# -----------------------------------------------------------------------
+# helper functions
+# -----------------------------------------------------------------------
+printUsage() {
+ local cmd
+ if [ "" == "$1" ] || [ "help" == "$1" ]; then
+ cmd="COMMAND"
+ elif [ "merge" == "$1" ] || [ "build" == "$1" ] || [ "sign" == "$1" ]; then
+ cmd="$1"
+ else
+ logError "Unknown command: '$1'\n"
+ cmd="COMMAND"
+ fi
+
+ echo -e "\e[1mUsage:\e[0m $(basename $0) $cmd [options]"
+
+ if [ "COMMAND" == "$cmd" ]; then
+ cat << EOF
+
+Commands:
+ merge Merge release branch into main branch and create release tags
+ build Build and package binary release from sources
+ sign Sign previously compiled release packages
+ help Show help for the given command
+EOF
+ elif [ "merge" == "$cmd" ]; then
+ cat << EOF
+
+Merge release branch into main branch and create release tags
+
+Options:
+ -v, --version Release version number or name (required)
+ -a, --app-name Application name (default: '${APP_NAME}')
+ -s, --source-dir Source directory (default: '${SRC_DIR}')
+ -g, --gpg-key GPG key used to sign the merge commit and release tag,
+ leave empty to let Git choose your default key
+ (default: '${GPG_GIT_KEY}')
+ -r, --release-branch Source release branch to merge from (default: 'release/VERSION')
+ --target-branch Target branch to merge to (default: '${TARGET_BRANCH}')
+ -t, --tag-name Override release tag name (defaults to version number)
+ -h, --help Show this help
+EOF
+ elif [ "build" == "$cmd" ]; then
+ cat << EOF
+
+Build and package binary release from sources
+
+Options:
+ -v, --version Release version number or name (required)
+ -a, --app-name Application name (default: '${APP_NAME}')
+ -s, --source-dir Source directory (default: '${SRC_DIR}')
+ -o, --output-dir Output directory where to build the release
+ (default: '${OUTPUT_DIR}')
+ -t, --tag-name Release tag to check out (defaults to version number)
+ -b, --build Build sources after exporting release
+ -d, --docker-image Use the specified Docker image to compile the application.
+ The image must have all required build dependencies installed.
+ This option has no effect if --build is not set.
+ --container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}')
+ The container must not exist already
+ -c, --cmake-options Additional CMake options for compiling the sources
+ --compiler Compiler to use (default: '${COMPILER}')
+ -m, --make-options Make options for compiling sources (default: '${MAKE_OPTIONS}')
+ -i, --install-prefix Install prefix (default: '${INSTALL_PREFIX}')
+ -p, --plugins Space-separated list of plugins to build
+ (default: ${BUILD_PLUGINS})
+ -n, --no-source-tarball Don't build source tarball
+ -h, --help Show this help
+EOF
+ elif [ "sign" == "$cmd" ]; then
+ cat << EOF
+
+Sign previously compiled release packages
+
+Options:
+ -f, --files Files to sign (required)
+ -g, --gpg-key GPG key used to sign the files (default: '${GPG_KEY}')
+ -h, --help Show this help
+EOF
+ fi
+}
+
+logInfo() {
+ echo -e "\e[1m[ \e[34mINFO\e[39m ]\e[0m $1"
+}
+
+logError() {
+ echo -e "\e[1m[ \e[31mERROR\e[39m ]\e[0m $1" >&2
+}
+
+init() {
+ ORIG_CWD="$(pwd)"
+ cd "$SRC_DIR" > /dev/null 2>&1
+ ORIG_BRANCH="$(git rev-parse --abbrev-ref HEAD 2> /dev/null)"
+ cd "$ORIG_CWD"
+}
+
+cleanup() {
+ logInfo "Checking out original branch..."
+ if [ "" != "$ORIG_BRANCH" ]; then
+ git checkout "$ORIG_BRANCH" > /dev/null 2>&1
+ fi
+ logInfo "Leaving source directory..."
+ cd "$ORIG_CWD"
+}
+
+exitError() {
+ logError "$1"
+ cleanup
+ exit 1
+}
+
+exitTrap() {
+ exitError "Existing upon user request..."
+}
+
+checkSourceDirExists() {
+ if [ ! -d "$SRC_DIR" ]; then
+ exitError "Source directory '${SRC_DIR}' does not exist!"
+ fi
+}
+
+checkOutputDirDoesNotExist() {
+ if [ -e "$OUTPUT_DIR" ]; then
+ exitError "Output directory '$OUTPUT_DIR' already exists. Please choose a different location!"
+ fi
+}
+
+checkGitRepository() {
+ if [ ! -d .git ] || [ ! -f CHANGELOG ]; then
+ exitError "Source directory is not a valid Git repository!"
+ fi
+}
+
+checkTagExists() {
+ git tag | grep -q "$TAG_NAME"
+ if [ $? -ne 0 ]; then
+ exitError "Tag '${TAG_NAME}' does not exist!"
+ fi
+}
+
+checkReleaseDoesNotExist() {
+ git tag | grep -q "$TAG_NAME"
+ if [ $? -eq 0 ]; then
+ exitError "Release '$RELEASE_NAME' (tag: '$TAG_NAME') already exists!"
+ fi
+}
+
+checkWorkingTreeClean() {
+ git diff-index --quiet HEAD --
+ if [ $? -ne 0 ]; then
+ exitError "Current working tree is not clean! Please commit or unstage any changes."
+ fi
+}
+
+checkSourceBranchExists() {
+ git rev-parse "$SOURCE_BRANCH" > /dev/null 2>&1
+ if [ $? -ne 0 ]; then
+ exitError "Source branch '$SOURCE_BRANCH' does not exist!"
+ fi
+}
+
+checkTargetBranchExists() {
+ git rev-parse "$TARGET_BRANCH" > /dev/null 2>&1
+ if [ $? -ne 0 ]; then
+ exitError "Target branch '$TARGET_BRANCH' does not exist!"
+ fi
+}
+
+checkVersionInCMake() {
+ local app_name_upper="$(echo "$APP_NAME" | tr '[:lower:]' '[:upper:]')"
+
+ grep -q "${app_name_upper}_VERSION \"${RELEASE_NAME}\"" CMakeLists.txt
+ if [ $? -ne 0 ]; then
+ exitError "${app_name_upper}_VERSION version not updated to '${RELEASE_NAME}' in CMakeLists.txt!"
+ fi
+
+ grep -q "${app_name_upper}_VERSION_NUM \"${RELEASE_NAME}\"" CMakeLists.txt
+ if [ $? -ne 0 ]; then
+ exitError "${app_name_upper}_VERSION_NUM version not updated to '${RELEASE_NAME}' in CMakeLists.txt!"
+ fi
+}
+
+checkChangeLog() {
+ if [ ! -f CHANGELOG ]; then
+ exitError "No CHANGELOG file found!"
+ fi
+
+ grep -qPzo "${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n=+\n" CHANGELOG
+ if [ $? -ne 0 ]; then
+ exitError "CHANGELOG does not contain any information about the '${RELEASE_NAME}' release!"
+ fi
+}
+
+checkTransifexCommandExists() {
+ command -v tx > /dev/null
+ if [ 0 -ne $? ]; then
+ exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'"
+ fi
+}
+
+# re-implement realpath for OS X (thanks mschrag)
+# https://superuser.com/questions/205127/
+if $(command -v realpath > /dev/null); then
+ realpath() {
+ pushd . > /dev/null
+ if [ -d "$1" ]; then
+ cd "$1"
+ dirs -l +0
+ else
+ cd "$(dirname "$1")"
+ cur_dir=$(dirs -l +0)
+
+ if [ "$cur_dir" == "/" ]; then
+ echo "$cur_dir$(basename "$1")"
+ else
+ echo "$cur_dir/$(basename "$1")"
+ fi
+ fi
+ popd > /dev/null
+ }
+fi
+
+
+trap exitTrap SIGINT SIGTERM
+
+
+# -----------------------------------------------------------------------
+# merge command
+# -----------------------------------------------------------------------
+merge() {
+ while [ $# -ge 1 ]; do
+ local arg="$1"
+ case "$arg" in
+ -v|--version)
+ RELEASE_NAME="$2"
+ shift ;;
+
+ -a|--app-name)
+ APP_NAME="$2"
+ shift ;;
+
+ -s|--source-dir)
+ SRC_DIR="$2"
+ shift ;;
+
+ -g|--gpg-key)
+ GPG_GIT_KEY="$2"
+ shift ;;
+
+ -r|--release-branch)
+ SOURCE_BRANCH="$2"
+ shift ;;
+
+ --target-branch)
+ TARGET_BRANCH="$2"
+ shift ;;
+
+ -t|--tag-name)
+ TAG_NAME="$2"
+ shift ;;
+
+ -h|--help)
+ printUsage "merge"
+ exit ;;
+
+ *)
+ logError "Unknown option '$arg'\n"
+ printUsage "merge"
+ exit 1 ;;
+ esac
+ shift
+ done
+
+ if [ "" == "$RELEASE_NAME" ]; then
+ logError "Missing arguments, --version is required!\n"
+ printUsage "merge"
+ exit 1
+ fi
+
+ if [ "" == "$TAG_NAME" ]; then
+ TAG_NAME="$RELEASE_NAME"
+ fi
+
+ if [ "" == "$SOURCE_BRANCH" ]; then
+ SOURCE_BRANCH="release/${RELEASE_NAME}"
+ fi
+
+ init
+
+ SRC_DIR="$(realpath "$SRC_DIR")"
+
+ logInfo "Performing basic checks..."
+
+ checkSourceDirExists
+
+ logInfo "Changing to source directory..."
+ cd "${SRC_DIR}"
+
+ checkTransifexCommandExists
+ checkGitRepository
+ checkReleaseDoesNotExist
+ checkWorkingTreeClean
+ checkSourceBranchExists
+ checkTargetBranchExists
+ checkVersionInCMake
+ checkChangeLog
+
+ logInfo "All checks pass, getting our hands dirty now!"
+
+ logInfo "Checking out source branch..."
+ git checkout "$SOURCE_BRANCH"
+
+ logInfo "Updating language files..."
+ ./share/translations/update.sh
+ if [ 0 -ne $? ]; then
+ exitError "Updating translations failed!"
+ fi
+ git diff-index --quiet HEAD --
+ if [ $? -ne 0 ]; then
+ git add ./share/translations/*
+ logInfo "Committing changes..."
+ if [ "" == "$GPG_GIT_KEY" ]; then
+ git commit -m "Update translations"
+ else
+ git commit -m "Update translations" -S"$GPG_GIT_KEY"
+ fi
+ fi
+
+ logInfo "Checking out target branch '${TARGET_BRANCH}'..."
+ git checkout "$TARGET_BRANCH"
+
+ logInfo "Merging '${SOURCE_BRANCH}' into '${TARGET_BRANCH}'..."
+
+ CHANGELOG=$(grep -Pzo "(?<=${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n)=+\n\n?(?:.|\n)+?\n(?=\n)" \
+ CHANGELOG | grep -Pzo '(?<=\n\n)(.|\n)+' | tr -d \\0)
+ COMMIT_MSG="Release ${RELEASE_NAME}"
+
+ git merge "$SOURCE_BRANCH" --no-ff -m "$COMMIT_MSG" -m "${CHANGELOG}" "$SOURCE_BRANCH" -S"$GPG_GIT_KEY"
+
+ logInfo "Creating tag '${TAG_NAME}'..."
+ if [ "" == "$GPG_GIT_KEY" ]; then
+ git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s
+ else
+ git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s -u "$GPG_GIT_KEY"
+ fi
+
+ cleanup
+
+ logInfo "All done!"
+ logInfo "Please merge the release branch back into the develop branch now and then push your changes."
+ logInfo "Don't forget to also push the tags using \e[1mgit push --tags\e[0m."
+}
+
+# -----------------------------------------------------------------------
+# build command
+# -----------------------------------------------------------------------
+build() {
+ while [ $# -ge 1 ]; do
+ local arg="$1"
+ case "$arg" in
+ -v|--version)
+ RELEASE_NAME="$2"
+ shift ;;
+
+ -a|--app-name)
+ APP_NAME="$2"
+ shift ;;
+
+ -s|--source-dir)
+ SRC_DIR="$2"
+ shift ;;
+
+ -o|--output-dir)
+ OUTPUT_DIR="$2"
+ shift ;;
+
+ -t|--tag-name)
+ TAG_NAME="$2"
+ shift ;;
+
+ -d|--docker-image)
+ DOCKER_IMAGE="$2"
+ shift ;;
+
+ --container-name)
+ DOCKER_CONTAINER_NAME="$2"
+ shift ;;
+
+ -c|--cmake-options)
+ CMAKE_OPTIONS="$2"
+ shift ;;
+
+ --compiler)
+ COMPILER="$2"
+ shift ;;
+
+ -m|--make-options)
+ MAKE_OPTIONS="$2"
+ shift ;;
+
+ -i|--install-prefix)
+ INSTALL_PREFIX="$2"
+ shift ;;
+
+ -p|--plugins)
+ BUILD_PLUGINS="$2"
+ shift ;;
+
+ -n|--no-source-tarball)
+ BUILD_SOURCE_TARBALL=false ;;
+
+ -h|--help)
+ printUsage "build"
+ exit ;;
+
+ *)
+ logError "Unknown option '$arg'\n"
+ printUsage "build"
+ exit 1 ;;
+ esac
+ shift
+ done
+
+ if [ "" == "$RELEASE_NAME" ]; then
+ logError "Missing arguments, --version is required!\n"
+ printUsage "build"
+ exit 1
+ fi
+
+ if [ "" == "$TAG_NAME" ]; then
+ TAG_NAME="$RELEASE_NAME"
+ fi
+
+ init
+
+ SRC_DIR="$(realpath "$SRC_DIR")"
+ OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
+
+ logInfo "Performing basic checks..."
+
+ checkSourceDirExists
+
+ logInfo "Changing to source directory..."
+ cd "${SRC_DIR}"
+
+ checkTagExists
+ checkGitRepository
+ checkWorkingTreeClean
+ checkOutputDirDoesNotExist
+
+ logInfo "All checks pass, getting our hands dirty now!"
+
+ logInfo "Checking out release tag '${TAG_NAME}'..."
+ git checkout "$TAG_NAME"
+
+ logInfo "Creating output directory..."
+ mkdir -p "$OUTPUT_DIR"
+
+ if [ $? -ne 0 ]; then
+ exitError "Failed to create output directory!"
+ fi
+
+ if $BUILD_SOURCE_TARBALL; then
+ logInfo "Creating source tarball..."
+ local app_name_lower="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')"
+ TARBALL_NAME="${app_name_lower}-${RELEASE_NAME}-src.tar.xz"
+ git archive --format=tar "$TAG_NAME" --prefix="${app_name_lower}-${RELEASE_NAME}/" \
+ | xz -6 > "${OUTPUT_DIR}/${TARBALL_NAME}"
+ fi
+
+ logInfo "Creating build directory..."
+ mkdir -p "${OUTPUT_DIR}/build-release"
+ cd "${OUTPUT_DIR}/build-release"
+
+ logInfo "Configuring sources..."
+ for p in $BUILD_PLUGINS; do
+ CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_XC_$(echo $p | tr '[:lower:]' '[:upper:]')=On"
+ done
+
+ if [ "$COMPILER" == "g++" ]; then
+ export CC=gcc
+ elif [ "$COMPILER" == "clang++" ]; then
+ export CC=clang
+ fi
+ export CXX="$COMPILER"
+
+ if [ "" == "$DOCKER_IMAGE" ]; then
+ if [ "$(uname -s)" == "Darwin" ]; then
+ # Building on OS X
+ local qt_vers="$(ls /usr/local/Cellar/qt5 2> /dev/null | sort -r | head -n1)"
+ if [ "" == "$qt_vers" ]; then
+ exitError "Couldn't find Qt5! Please make sure it is available in '/usr/local/Cellar/qt5'."
+ fi
+ export MACOSX_DEPLOYMENT_TARGET=10.7
+
+ logInfo "Configuring build..."
+ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \
+ -DCMAKE_OSX_ARCHITECTURES=x86_64 -DWITH_CXX11=OFF \
+ -DCMAKE_PREFIX_PATH="/usr/local/Cellar/qt5/${qt_vers}/lib/cmake" \
+ -DQT_BINARY_DIR="/usr/local/Cellar/qt5/${qt_vers}/bin" $CMAKE_OPTIONS "$SRC_DIR"
+
+ logInfo "Compiling and packaging sources..."
+ make $MAKE_OPTIONS package
+
+ mv "./${APP_NAME}-${RELEASE_NAME}.dmg" ../
+ elif [ "$(uname -o)" == "Msys" ]; then
+ # Building on Windows with Msys
+ logInfo "Configuring build..."
+ cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off -G"MSYS Makefiles" \
+ -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" $CMAKE_OPTIONS "$SRC_DIR"
+
+ logInfo "Compiling and packaging sources..."
+ make $MAKE_OPTIONS package
+
+ mv "./${APP_NAME}-${RELEASE_NAME}-"*.{exe,zip} ../
+ else
+ mkdir -p "${OUTPUT_DIR}/bin-release"
+
+ # Building on Linux without Docker container
+ logInfo "Configuring build..."
+ cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \
+ -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR"
+
+ logInfo "Compiling sources..."
+ make $MAKE_OPTIONS
+
+ logInfo "Installing to bin dir..."
+ make DESTDIR="${OUTPUT_DIR}/bin-release" install/strip
+
+ logInfo "Creating AppImage..."
+ ${SRC_DIR}/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME"
+ fi
+ else
+ mkdir -p "${OUTPUT_DIR}/bin-release"
+
+ logInfo "Launching Docker container to compile sources..."
+
+ docker run --name "$DOCKER_CONTAINER_NAME" --rm \
+ --cap-add SYS_ADMIN --device /dev/fuse \
+ -e "CC=${CC}" -e "CXX=${CXX}" \
+ -v "$(realpath "$SRC_DIR"):/keepassxc/src:ro" \
+ -v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \
+ "$DOCKER_IMAGE" \
+ bash -c "cd /keepassxc/out/build-release && \
+ cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \
+ -DCMAKE_INSTALL_PREFIX=\"${INSTALL_PREFIX}\" /keepassxc/src && \
+ make $MAKE_OPTIONS && make DESTDIR=/keepassxc/out/bin-release install/strip && \
+ /keepassxc/src/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME""
+
+ if [ 0 -ne $? ]; then
+ exitError "Docker build failed!"
+ fi
+
+ logInfo "Build finished, Docker container terminated."
+ fi
+
+ cleanup
+
+ logInfo "All done!"
+}
+
+
+# -----------------------------------------------------------------------
+# sign command
+# -----------------------------------------------------------------------
+sign() {
+ SIGN_FILES=()
+
+ while [ $# -ge 1 ]; do
+ local arg="$1"
+ case "$arg" in
+ -f|--files)
+ while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do
+ SIGN_FILES+=("$2")
+ shift
+ done ;;
+
+ -g|--gpg-key)
+ GPG_KEY="$2"
+ shift ;;
+
+ -h|--help)
+ printUsage "sign"
+ exit ;;
+
+ *)
+ logError "Unknown option '$arg'\n"
+ printUsage "sign"
+ exit 1 ;;
+ esac
+ shift
+ done
+
+ if [ -z "$SIGN_FILES" ]; then
+ logError "Missing arguments, --files is required!\n"
+ printUsage "sign"
+ exit 1
+ fi
+
+ for f in "${SIGN_FILES[@]}"; do
+ if [ ! -f "$f" ]; then
+ exitError "File '${f}' does not exist!"
+ fi
+
+ logInfo "Signing file '${f}'..."
+ gpg --output "${f}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$f"
+
+ if [ 0 -ne $? ]; then
+ exitError "Signing failed!"
+ fi
+
+ logInfo "Creating digest for file '${f}'..."
+ sha256sum "$f" > "${f}.DIGEST"
+ done
+
+ logInfo "All done!"
+}
+
+
+# -----------------------------------------------------------------------
+# parse global command line
+# -----------------------------------------------------------------------
+MODE="$1"
+shift
+if [ "" == "$MODE" ]; then
+ logError "Missing arguments!\n"
+ printUsage
+ exit 1
+elif [ "help" == "$MODE" ]; then
+ printUsage "$1"
+ exit
+elif [ "merge" == "$MODE" ] || [ "build" == "$MODE" ] || [ "sign" == "$MODE" ]; then
+ $MODE "$@"
+else
+ printUsage "$MODE"
+fi
diff --git a/share/windows/installer-header.bmp b/share/windows/installer-header.bmp
new file mode 100644
index 000000000..f9e17cae7
Binary files /dev/null and b/share/windows/installer-header.bmp differ
diff --git a/share/windows/installer-wizard.bmp b/share/windows/installer-wizard.bmp
new file mode 100644
index 000000000..76d04518f
Binary files /dev/null and b/share/windows/installer-wizard.bmp differ
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 30332c71e..8c3948842 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -218,6 +218,7 @@ target_link_libraries(${PROGNAME}
Qt5::Widgets
Qt5::Network
${GCRYPT_LIBRARIES}
+ ${GPGERROR_LIBRARIES}
${ZLIB_LIBRARIES})
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
@@ -257,9 +258,25 @@ if(APPLE)
endif()
if(MINGW)
- set(CPACK_GENERATOR "ZIP")
+ string(REPLACE "AMD" "Win" OUTPUT_FILE_POSTFIX "${CMAKE_HOST_SYSTEM_PROCESSOR}")
+ set(CPACK_GENERATOR "ZIP;NSIS")
set(CPACK_STRIP_FILES ON)
- set(CPACK_PACKAGE_FILE_NAME "${PROGNAME}-${KEEPASSXC_VERSION_NUM}")
+ set(CPACK_PACKAGE_FILE_NAME "${PROGNAME}-${KEEPASSXC_VERSION}-${OUTPUT_FILE_POSTFIX}")
+ set(CPACK_PACKAGE_INSTALL_DIRECTORY ${PROGNAME})
+ set(CPACK_PACKAGE_VERSION ${KEEPASSXC_VERSION})
+ set(CPACK_PACKAGE_VENDOR "${PROGNAME} Team")
+ string(REGEX REPLACE "/" "\\\\\\\\" CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/share/windows/installer-header.bmp")
+ set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.GPL-2")
+ set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/share/windows/keepassxc.ico")
+ set(CPACK_NSIS_MUI_UNIICON "${CPACK_NSIS_MUI_ICON}")
+ set(CPACK_NSIS_INSTALLED_ICON_NAME "\\\\${PROGNAME}.exe")
+ string(REGEX REPLACE "/" "\\\\\\\\" CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP "${CMAKE_SOURCE_DIR}/share/windows/installer-wizard.bmp")
+ set(CPACK_NSIS_MUI_UNWELCOMEFINISHPAGE_BITMAP "${CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP}")
+ set(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${PROGNAME}.lnk' '$INSTDIR\\\\${PROGNAME}.exe'")
+ set(CPACK_NSIS_DELETE_ICONS_EXTRA "Delete '$SMPROGRAMS\\\\$START_MENU\\\\${PROGNAME}.lnk'")
+ set(CPACK_NSIS_URL_INFO_ABOUT "https://keepassxc.org")
+ set(CPACK_NSIS_PACKAGE_NAME "${PROGNAME} v${KEEPASSXC_VERSION}")
+ set(CPACK_NSIS_MUI_FINISHPAGE_RUN "../${PROGNAME}.exe")
include(CPack)
install(CODE "
@@ -267,5 +284,9 @@ if(MINGW)
" COMPONENT Runtime)
include(DeployQt4)
- install_qt4_executable(${PROGNAME}.exe "qjpeg;qgif;qico;qtaccessiblewidgets")
+ install_qt4_executable(${PROGNAME}.exe)
+ add_custom_command(TARGET ${PROGNAME} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different ${Qt5Core_DIR}/../../../share/qt5/plugins/platforms/qwindows$<$:d>.dll
+ $)
+ install(FILES $/qwindows$<$:d>.dll DESTINATION "platforms")
endif()
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index b2b06e7c8..46e2670ac 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -353,6 +353,12 @@ void Entry::setTitle(const QString& title)
void Entry::setUrl(const QString& url)
{
+ bool remove = url != m_attributes->value(EntryAttributes::URLKey) &&
+ (m_attributes->value(EntryAttributes::RememberCmdExecAttr) == "1" ||
+ m_attributes->value(EntryAttributes::RememberCmdExecAttr) == "0");
+ if (remove) {
+ m_attributes->remove(EntryAttributes::RememberCmdExecAttr);
+ }
m_attributes->set(EntryAttributes::URLKey, url, m_attributes->isProtected(EntryAttributes::URLKey));
}
@@ -508,7 +514,8 @@ Entry* Entry::clone(CloneFlags flags) const
entry->m_data.timeInfo.setLocationChanged(now);
}
-
+ if (flags & CloneRenameTitle)
+ entry->setTitle(entry->title() + tr(" - Clone"));
return entry;
}
diff --git a/src/core/Entry.h b/src/core/Entry.h
index 66b9362a6..ae60b596c 100644
--- a/src/core/Entry.h
+++ b/src/core/Entry.h
@@ -115,7 +115,8 @@ public:
CloneNoFlags = 0,
CloneNewUuid = 1, // generate a random uuid for the clone
CloneResetTimeInfo = 2, // set all TimeInfo attributes to the current time
- CloneIncludeHistory = 4 // clone the history items
+ CloneIncludeHistory = 4, // clone the history items
+ CloneRenameTitle = 8 // add "-Clone" after the original title
};
Q_DECLARE_FLAGS(CloneFlags, CloneFlag)
diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp
index 195a8f14a..b633cae32 100644
--- a/src/core/EntryAttributes.cpp
+++ b/src/core/EntryAttributes.cpp
@@ -24,6 +24,7 @@ const QString EntryAttributes::URLKey = "URL";
const QString EntryAttributes::NotesKey = "Notes";
const QStringList EntryAttributes::DefaultAttributes(QStringList() << TitleKey << UserNameKey
<< PasswordKey << URLKey << NotesKey);
+const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
EntryAttributes::EntryAttributes(QObject* parent)
: QObject(parent)
diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h
index 1c0ddaaeb..211b6d483 100644
--- a/src/core/EntryAttributes.h
+++ b/src/core/EntryAttributes.h
@@ -52,6 +52,7 @@ public:
static const QString URLKey;
static const QString NotesKey;
static const QStringList DefaultAttributes;
+ static const QString RememberCmdExecAttr;
static bool isDefaultAttribute(const QString& key);
Q_SIGNALS:
diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp
index c0360a36c..01e152e2a 100644
--- a/src/core/EntrySearcher.cpp
+++ b/src/core/EntrySearcher.cpp
@@ -42,7 +42,11 @@ QList EntrySearcher::searchEntries(const QString& searchTerm, const Grou
const QList children = group->children();
for (Group* childGroup : children) {
if (childGroup->searchingEnabled() != Group::Disable) {
- searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity));
+ if (matchGroup(searchTerm, childGroup, caseSensitivity)) {
+ searchResult.append(childGroup->entriesRecursive());
+ } else {
+ searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity));
+ }
}
}
@@ -69,3 +73,21 @@ bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensiti
entry->url().contains(word, caseSensitivity) ||
entry->notes().contains(word, caseSensitivity);
}
+
+bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
+{
+ const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
+ for (const QString& word : wordList) {
+ if (!wordMatch(word, group, caseSensitivity)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool EntrySearcher::wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity)
+{
+ return group->name().contains(word, caseSensitivity) ||
+ group->notes().contains(word, caseSensitivity);
+}
diff --git a/src/core/EntrySearcher.h b/src/core/EntrySearcher.h
index c7075dc9b..4e8d4eabe 100644
--- a/src/core/EntrySearcher.h
+++ b/src/core/EntrySearcher.h
@@ -33,6 +33,8 @@ private:
QList searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
QList matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity);
bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity);
+ bool matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
+ bool wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity);
};
#endif // KEEPASSX_ENTRYSEARCHER_H
diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp
index d982f22ca..26d9d2283 100644
--- a/src/gui/Application.cpp
+++ b/src/gui/Application.cpp
@@ -17,12 +17,20 @@
*/
#include "Application.h"
+#include "MainWindow.h"
#include
#include
+#include
#include "autotype/AutoType.h"
+#if defined(Q_OS_UNIX)
+#include
+#include
+#include
+#endif
+
#if defined(Q_OS_UNIX) && !defined(Q_OS_OSX)
class XcbEventFilter : public QAbstractNativeEventFilter
{
@@ -65,12 +73,18 @@ public:
Application::Application(int& argc, char** argv)
: QApplication(argc, argv)
, m_mainWindow(nullptr)
+#ifdef Q_OS_UNIX
+ , m_unixSignalNotifier(nullptr)
+#endif
{
#if defined(Q_OS_UNIX) && !defined(Q_OS_OSX)
installNativeEventFilter(new XcbEventFilter());
#elif defined(Q_OS_WIN)
installNativeEventFilter(new WinEventFilter());
#endif
+#if defined(Q_OS_UNIX)
+ registerUnixSignals();
+#endif
}
void Application::setMainWindow(QWidget* mainWindow)
@@ -98,3 +112,57 @@ bool Application::event(QEvent* event)
return QApplication::event(event);
}
+
+#if defined(Q_OS_UNIX)
+int Application::unixSignalSocket[2];
+
+void Application::registerUnixSignals()
+{
+ int result = ::socketpair(AF_UNIX, SOCK_STREAM, 0, unixSignalSocket);
+ Q_ASSERT(0 == result);
+ if (0 != result) {
+ // do not register handles when socket creation failed, otherwise
+ // application will be unresponsive to signals such as SIGINT or SIGTERM
+ return;
+ }
+
+ QVector const handledSignals = { SIGQUIT, SIGINT, SIGTERM, SIGHUP };
+ for (auto s: handledSignals) {
+ struct sigaction sigAction;
+
+ sigAction.sa_handler = handleUnixSignal;
+ sigemptyset(&sigAction.sa_mask);
+ sigAction.sa_flags = 0 | SA_RESTART;
+ sigaction(s, &sigAction, nullptr);
+ }
+
+ m_unixSignalNotifier = new QSocketNotifier(unixSignalSocket[1], QSocketNotifier::Read, this);
+ connect(m_unixSignalNotifier, SIGNAL(activated(int)), this, SLOT(quitBySignal()));
+}
+
+void Application::handleUnixSignal(int sig)
+{
+ switch (sig) {
+ case SIGQUIT:
+ case SIGINT:
+ case SIGTERM:
+ {
+ char buf = 0;
+ ::write(unixSignalSocket[0], &buf, sizeof(buf));
+ return;
+ }
+ case SIGHUP:
+ return;
+ }
+}
+
+void Application::quitBySignal()
+{
+ m_unixSignalNotifier->setEnabled(false);
+ char buf;
+ ::read(unixSignalSocket[1], &buf, sizeof(buf));
+
+ if (nullptr != m_mainWindow)
+ static_cast(m_mainWindow)->appExit();
+}
+#endif
diff --git a/src/gui/Application.h b/src/gui/Application.h
index 149b61ddf..9bfe4d549 100644
--- a/src/gui/Application.h
+++ b/src/gui/Application.h
@@ -21,6 +21,8 @@
#include
+class QSocketNotifier;
+
class Application : public QApplication
{
Q_OBJECT
@@ -34,8 +36,23 @@ public:
Q_SIGNALS:
void openFile(const QString& filename);
+private Q_SLOTS:
+#if defined(Q_OS_UNIX)
+ void quitBySignal();
+#endif
+
private:
QWidget* m_mainWindow;
+
+#if defined(Q_OS_UNIX)
+ /**
+ * Register Unix signals such as SIGINT and SIGTERM for clean shutdown.
+ */
+ void registerUnixSignals();
+ QSocketNotifier* m_unixSignalNotifier;
+ static void handleUnixSignal(int sig);
+ static int unixSignalSocket[2];
+#endif
};
#endif // KEEPASSX_APPLICATION_H
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index 00bf1168a..4a1298deb 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -19,6 +19,7 @@
#include
#include
+#include
#include
#include
#include
@@ -312,8 +313,10 @@ void DatabaseWidget::cloneEntry()
return;
}
- Entry* entry = currentEntry->clone(Entry::CloneNewUuid | Entry::CloneResetTimeInfo);
+ Entry* entry = currentEntry->clone(Entry::CloneNewUuid | Entry::CloneResetTimeInfo | Entry::CloneRenameTitle);
entry->setGroup(currentEntry->group());
+ if (isInSearchMode())
+ search(m_lastSearchText);
m_entryView->setFocus();
m_entryView->setCurrentEntry(entry);
}
@@ -494,8 +497,46 @@ void DatabaseWidget::openUrlForEntry(Entry* entry)
}
if (urlString.startsWith("cmd://")) {
+ // check if decision to execute command was stored
+ if (entry->attributes()->hasKey(EntryAttributes::RememberCmdExecAttr)) {
+ if (entry->attributes()->value(EntryAttributes::RememberCmdExecAttr) == "1") {
+ QProcess::startDetached(urlString.mid(6));
+ }
+ return;
+ }
+
+ // otherwise ask user
if (urlString.length() > 6) {
- QProcess::startDetached(urlString.mid(6));
+ QString cmdTruncated = urlString.mid(6);
+ if (cmdTruncated.length() > 400)
+ cmdTruncated = cmdTruncated.left(400) + " […]";
+ QMessageBox msgbox(QMessageBox::Icon::Question,
+ tr("Execute command?"),
+ tr("Do you really want to execute the following command?
%1
")
+ .arg(cmdTruncated.toHtmlEscaped()),
+ QMessageBox::Yes | QMessageBox::No,
+ this
+ );
+ msgbox.setDefaultButton(QMessageBox::No);
+
+ QCheckBox* checkbox = new QCheckBox(tr("Remember my choice"), &msgbox);
+ msgbox.setCheckBox(checkbox);
+ bool remember = false;
+ QObject::connect(checkbox, &QCheckBox::stateChanged, [&](int state) {
+ if (static_cast(state) == Qt::CheckState::Checked) {
+ remember = true;
+ }
+ });
+
+ int result = msgbox.exec();
+ if (result == QMessageBox::Yes) {
+ QProcess::startDetached(urlString.mid(6));
+ }
+
+ if (remember) {
+ entry->attributes()->set(EntryAttributes::RememberCmdExecAttr,
+ result == QMessageBox::Yes ? "1" : "0");
+ }
}
}
else {
@@ -722,15 +763,10 @@ void DatabaseWidget::unlockDatabase(bool accepted)
replaceDatabase(db);
- const QList groups = m_db->rootGroup()->groupsRecursive(true);
- for (Group* group : groups) {
- if (group->uuid() == m_groupBeforeLock) {
- m_groupView->setCurrentGroup(group);
- break;
- }
- }
-
+ restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock);
m_groupBeforeLock = Uuid();
+ m_entryBeforeLock = Uuid();
+
setCurrentWidget(m_mainWidget);
m_unlockDatabaseWidget->clearForms();
Q_EMIT unlockedDatabase();
@@ -755,7 +791,7 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
void DatabaseWidget::switchToEntryEdit()
{
Entry* entry = m_entryView->currentEntry();
- Q_ASSERT(entry);
+
if (!entry) {
return;
}
@@ -766,7 +802,7 @@ void DatabaseWidget::switchToEntryEdit()
void DatabaseWidget::switchToGroupEdit()
{
Group* group = m_groupView->currentGroup();
- Q_ASSERT(group);
+
if (!group) {
return;
}
@@ -943,6 +979,10 @@ void DatabaseWidget::lock()
m_groupBeforeLock = m_db->rootGroup()->uuid();
}
+ if (m_entryView->currentEntry()) {
+ m_entryBeforeLock = m_entryView->currentEntry()->uuid();
+ }
+
clearAllWidgets();
m_unlockDatabaseWidget->load(m_filename);
setCurrentWidget(m_unlockDatabaseWidget);
@@ -1028,7 +1068,22 @@ void DatabaseWidget::reloadDatabaseFile()
}
}
+ Uuid groupBeforeReload;
+ if (m_groupView && m_groupView->currentGroup()) {
+ groupBeforeReload = m_groupView->currentGroup()->uuid();
+ }
+ else {
+ groupBeforeReload = m_db->rootGroup()->uuid();
+ }
+
+ Uuid entryBeforeReload;
+ if (m_entryView && m_entryView->currentEntry()) {
+ entryBeforeReload = m_entryView->currentEntry()->uuid();
+ }
+
replaceDatabase(db);
+ restoreGroupEntryFocus(groupBeforeReload, entryBeforeReload);
+
}
else {
MessageBox::critical(this, tr("Autoreload Failed"),
@@ -1061,6 +1116,35 @@ QStringList DatabaseWidget::customEntryAttributes() const
return entry->attributes()->customKeys();
}
+/*
+ * Restores the focus on the group and entry that was focused
+ * before the database was locked or reloaded.
+ */
+void DatabaseWidget::restoreGroupEntryFocus(Uuid groupUuid, Uuid entryUuid)
+{
+ Group* restoredGroup = nullptr;
+ const QList groups = m_db->rootGroup()->groupsRecursive(true);
+ for (Group* group : groups) {
+ if (group->uuid() == groupUuid) {
+ restoredGroup = group;
+ break;
+ }
+ }
+
+ if (restoredGroup != nullptr) {
+ m_groupView->setCurrentGroup(restoredGroup);
+
+ const QList entries = restoredGroup->entries();
+ for (Entry* entry : entries) {
+ if (entry->uuid() == entryUuid) {
+ m_entryView->setCurrentEntry(entry);
+ break;
+ }
+ }
+ }
+
+}
+
bool DatabaseWidget::isGroupSelected() const
{
return m_groupView->currentGroup() != nullptr;
diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h
index f55fa2027..79e58cecf 100644
--- a/src/gui/DatabaseWidget.h
+++ b/src/gui/DatabaseWidget.h
@@ -163,6 +163,7 @@ private Q_SLOTS:
// Database autoreload slots
void onWatchedFileChanged();
void reloadDatabaseFile();
+ void restoreGroupEntryFocus(Uuid groupUuid, Uuid EntryUuid);
private:
void setClipboardTextAndMinimize(const QString& text);
@@ -190,6 +191,7 @@ private:
Group* m_newParent;
QString m_filename;
Uuid m_groupBeforeLock;
+ Uuid m_entryBeforeLock;
// Search state
QString m_lastSearchText;
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index cc94ca9a9..819bda5dc 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -364,7 +364,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
bool groupSelected = dbWidget->isGroupSelected();
m_ui->actionEntryNew->setEnabled(!inSearch);
- m_ui->actionEntryClone->setEnabled(singleEntrySelected && !inSearch);
+ m_ui->actionEntryClone->setEnabled(singleEntrySelected);
m_ui->actionEntryEdit->setEnabled(singleEntrySelected);
m_ui->actionEntryDelete->setEnabled(entriesSelected);
m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle());
diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h
index cf2c9cd96..ab9924a75 100644
--- a/src/gui/MainWindow.h
+++ b/src/gui/MainWindow.h
@@ -42,6 +42,7 @@ public:
public Q_SLOTS:
void openDatabase(const QString& fileName, const QString& pw = QString(),
const QString& keyFile = QString());
+ void appExit();
protected:
void closeEvent(QCloseEvent* event) override;
@@ -68,7 +69,6 @@ private Q_SLOTS:
void applySettingsChanges();
void trayIconTriggered(QSystemTrayIcon::ActivationReason reason);
void toggleWindow();
- void appExit();
void lockDatabasesAfterInactivity();
void repairDatabase();
diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui
index 05b80caa2..188ef1586 100644
--- a/src/gui/MainWindow.ui
+++ b/src/gui/MainWindow.ui
@@ -155,15 +155,15 @@
-
-
-
-
+
+
+
+