diff --git a/README.md b/README.md index 1f73b3c9..f79c774b 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,9 @@ This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationD For **Windows** users: * **MSYS Git is NOT supported**. Please use official [Git for Windows](https://git-scm.com/download/win) instead. -* `sourcegit_x.y.win-x64.zip` may be reported as virus by Windows Defender. I don't know why. I have manually tested the zip to be uploaded using Windows Defender before uploading and no virus was found. If you have installed .NET 8 SDK locally, I suggest you to compile it yourself. And if you have any idea about how to fix this, please open an issue. +* You can install the latest stable by `winget install SourceGit`. + - Note: `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar. +* Portable versions can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) For **macOS** users: @@ -80,9 +82,9 @@ This app supports open repository in external tools listed in the table below. | JetBrains Fleet | YES | YES | YES | FLEET_PATH | | Sublime Text | YES | YES | YES | SUBLIME_TEXT_PATH | -> * You can set the given environment variable for special tool if it can NOT be found by this app automatically. -> * Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device. -> * On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables. +* You can set the given environment variable for special tool if it can NOT be found by this app automatically. +* Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device. +* On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables. ## Screenshots @@ -94,46 +96,9 @@ This app supports open repository in external tools listed in the table below. ![Theme Light](./screenshots/theme_light.png) -## How to Customize Theme +* Custom Themes -1. Create a new json file, and provide your favorite colors with follow keys: - -| Key | Description | -| --- | --- | -| Color.Window | Window background color | -| Color.WindowBorder | Window border color. Only used on Linux. | -| Color.TitleBar | Title bar background color | -| Color.ToolBar | Tool bar background color | -| Color.Popup | Popup panel background color | -| Color.Contents | Background color used in inputs, data grids, file content viewer, change lists, text diff viewer, etc. | -| Color.Badge | Badge background color | -| Color.BadgeFG | Badge foreground color | -| Color.Conflict | Conflict panel background color | -| Color.ConflictForeground | Conflict panel foreground color | -| Color.Border0 | Border color used in some controls, like Window, Tab, Toolbar, etc. | -| Color.Border1 | Border color used in inputs, like TextBox, ComboBox, etc. | -| Color.Border2 | Border color used in visual lines, like seperators, Rectange, etc. | -| Color.FlatButton.Background | Flat button background color, like `Cancel`, `Commit & Push` button | -| Color.FlatButton.BackgroundHovered | Flat button background color when hovered, like `Cancel` button | -| Color.FG1 | Primary foreground color for all text elements | -| Color.FG2 | Secondary foreground color for all text elements | -| Color.Diff.EmptyBG | Background color used in empty lines in diff viewer | -| Color.Diff.AddedBG | Background color used in added lines in diff viewer | -| Color.Diff.DeletedBG | Background color used in deleted lines in diff viewer | -| Color.Diff.AddedHighlight | Background color used for changed words in added lines in diff viewer | -| Color.Diff.DeletedHighlight | Background color used for changed words in deleted lines in diff viewer | - -For example: - -```json -{ - "Color.Window": "#FFFF6059" -} -``` - -2. Open `Preference` -> `Appearance`, choose the json file you just created in `Custom Color Schema`. - -> **NOTE**: The `Custom Color Schema` will override the colors with same keys in current active theme. +You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git) ## Contributing diff --git a/VERSION b/VERSION index 9e16784b..d8101a48 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.19 \ No newline at end of file +8.20 \ No newline at end of file diff --git a/build/build.linux.sh b/build/build.linux.sh index 0432dcfe..55bc2f62 100755 --- a/build/build.linux.sh +++ b/build/build.linux.sh @@ -3,17 +3,16 @@ version=`cat ../VERSION` # Cleanup -rm -rf SourceGit *.tar.gz resources/deb/opt *.deb *.rpm +rm -rf SourceGit *.tar.gz resources/deb/opt *.deb *.rpm *.AppImage -# Compile -dotnet publish ../src/SourceGit.csproj -c Release -r linux-x64 -o SourceGit -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained -mv SourceGit/SourceGit SourceGit/sourcegit -cp resources/app/App.icns SourceGit/sourcegit.icns -rm -f SourceGit/*.dbg +# Generic AppImage +cd resources/appimage +./publish-appimage -y -o sourcegit-${version}.linux.x86_64.AppImage -# General Linux archive -tar -zcvf sourcegit_${version}.linux-x64.tar.gz SourceGit -rm -f SourceGit/sourcegit.icns +# Move to build dir +mv AppImages/sourcegit-${version}.linux.x86_64.AppImage ../../ +mv AppImages/AppDir/usr/bin ../../SourceGit +cd ../../ # Debain/Ubuntu package mkdir -p resources/deb/opt/sourcegit/ diff --git a/build/resources/appimage/publish-appimage b/build/resources/appimage/publish-appimage new file mode 100755 index 00000000..b8010187 --- /dev/null +++ b/build/resources/appimage/publish-appimage @@ -0,0 +1,708 @@ +#!/bin/bash +################################################################################ +# PROJECT : Publish-AppImage for .NET +# WEBPAGE : https://github.com/kuiperzone/Publish-AppImage +# COPYRIGHT : Andy Thomas 2021-2023 +# LICENSE : MIT +################################################################################ + +############################### +# CONSTANTS +############################### + +declare -r _SCRIPT_VERSION="1.3.1" +declare -r _SCRIPT_TITLE="Publish-AppImage for .NET" +declare -r _SCRIPT_IMPL_MIN=1 +declare -r _SCRIPT_IMPL_MAX=1 +declare -r _SCRIPT_COPYRIGHT="Copyright 2023 Andy Thomas" +declare -r _SCRIPT_WEBSITE="https://github.com/kuiperzone/Publish-AppImage" + +declare -r _SCRIPT_NAME="publish-appimage" +declare -r _DEFAULT_CONF="${_SCRIPT_NAME}.conf" + +declare -r _APPIMAGE_KIND="appimage" +declare -r _ZIP_KIND="zip" +declare -r _DOTNET_NONE="null" + + +############################### +# FUNCTIONS +############################### + +function assert_result +{ + local _ret=$? + + if [ ${_ret} -ne 0 ]; then + echo + exit ${_ret} + fi +} + +function exec_or_die +{ + echo "${1}" + eval "${1}" + assert_result +} + +function ensure_directory +{ + local _path="${1}" + + if [ ! -d "${_path}" ]; then + mkdir -p "${_path}" + assert_result + fi +} + +function remove_path +{ + local _path="${1}" + + if [ -d "${_path}" ]; then + rm -rf "${_path}" + assert_result + elif [ -f "${_path}" ]; then + rm -f "${_path}" + assert_result + fi +} + +function assert_mandatory +{ + local _name="${1}" + local _value="${2}" + + if [ "${_value}" == "" ]; then + echo "${_name} undefined in: ${_conf_arg_value}" + echo + exit 1 + fi +} + +function assert_opt_file +{ + local _name="${1}" + local _value="${2}" + + if [ "${_value}" != "" ] && [ ! -f "${_value}" ]; then + echo "File not found: ${_value}" + + if [ "${_name}" != "" ]; then + echo "See ${_name} in: ${_conf_arg_value}" + fi + + echo + exit 1 + fi +} + +############################### +# HANDLE ARGUMENTS +############################### + +# Specify conf file +declare -r _CONF_ARG="f" +declare -r _CONF_ARG_NAME="conf" +_conf_arg_value="${_DEFAULT_CONF}" +_arg_syntax=":${_CONF_ARG}:" + +# Runtime ID +declare -r _RID_ARG="r" +declare -r _RID_ARG_NAME="runtime" +_rid_arg_value="linux-x64" +_arg_syntax="${_arg_syntax}${_RID_ARG}:" + +# Package kind +declare -r _KIND_ARG="k" +declare -r _KIND_ARG_NAME="kind" +declare -l _kind_arg_value="${_APPIMAGE_KIND}" +_arg_syntax="${_arg_syntax}${_KIND_ARG}:" + +# Run app +declare -r _RUNAPP_ARG="u" +declare -r _RUNAPP_ARG_NAME="run" +_runapp_arg_value=false +_arg_syntax="${_arg_syntax}${_RUNAPP_ARG}" + +# Verbose +declare -r _VERBOSE_ARG="b" +declare -r _VERBOSE_ARG_NAME="verbose" +_verbose_arg_value=false +_arg_syntax="${_arg_syntax}${_VERBOSE_ARG}" + +# Skip yes (no prompt) +declare -r _SKIPYES_ARG="y" +declare -r _SKIPYES_ARG_NAME="skip-yes" +_skipyes_arg_value=false +_arg_syntax="${_arg_syntax}${_SKIPYES_ARG}" + +# Output name +declare -r _OUTPUT_ARG="o" +declare -r _OUTPUT_ARG_NAME="output" +_output_arg_value="" +_arg_syntax="${_arg_syntax}${_OUTPUT_ARG}:" + +# Show version +declare -r _VERSION_ARG="v" +declare -r _VERSION_ARG_NAME="version" +_version_arg_value=false +_arg_syntax="${_arg_syntax}${_VERSION_ARG}" + +# Show help +declare -r _HELP_ARG="h" +declare -r _HELP_ARG_NAME="help" +_help_arg_value=false +_arg_syntax="${_arg_syntax}${_HELP_ARG}" + +_exit_help=0 + +# Transform long options to short ones +for arg in "${@}"; do + shift + case "${arg}" in + ("--${_CONF_ARG_NAME}") set -- "$@" "-${_CONF_ARG}" ;; + ("--${_RID_ARG_NAME}") set -- "$@" "-${_RID_ARG}" ;; + ("--${_KIND_ARG_NAME}") set -- "$@" "-${_KIND_ARG}" ;; + ("--${_RUNAPP_NAME}") set -- "$@" "-${_RUNAPP_ARG}" ;; + ("--${_VERBOSE_ARG_NAME}") set -- "$@" "-${_VERBOSE_ARG}" ;; + ("--${_SKIPYES_ARG_NAME}") set -- "$@" "-${_SKIPYES_ARG}" ;; + ("--${_OUTPUT_ARG_NAME}") set -- "$@" "-${_OUTPUT_ARG}" ;; + ("--${_VERSION_ARG_NAME}") set -- "$@" "-${_VERSION_ARG}" ;; + ("--${_HELP_ARG_NAME}") set -- "$@" "-${_HELP_ARG}" ;; + ("--"*) + echo "Illegal argument: ${arg}" + echo + + _exit_help=1 + break + ;; + (*) set -- "$@" "${arg}" ;; + esac +done + +if [ ${_exit_help} == 0 ]; then + # Read arguments + while getopts ${_arg_syntax} arg; do + case "${arg}" in + (${_CONF_ARG}) _conf_arg_value="${OPTARG}" ;; + (${_RID_ARG}) _rid_arg_value="${OPTARG}" ;; + (${_KIND_ARG}) _kind_arg_value="${OPTARG}" ;; + (${_RUNAPP_ARG}) _runapp_arg_value=true ;; + (${_VERBOSE_ARG}) _verbose_arg_value=true ;; + (${_SKIPYES_ARG}) _skipyes_arg_value=true ;; + (${_OUTPUT_ARG}) _output_arg_value="${OPTARG}" ;; + (${_VERSION_ARG}) _version_arg_value=true ;; + (${_HELP_ARG}) _help_arg_value=true ;; + (*) + echo "Illegal argument" + echo + + _exit_help=1 + break + ;; + esac + done +fi + +# Handle and help and version +if [ ${_help_arg_value} == true ] || [ $_exit_help != 0 ]; then + + _indent=" " + echo "Usage:" + echo "${_indent}${_SCRIPT_NAME} [-flags] [-option-n value-n]" + echo + + echo "Help Options:" + echo "${_indent}-${_HELP_ARG}, --${_HELP_ARG_NAME}" + echo "${_indent}Show help information flag." + echo + echo "${_indent}-${_VERSION_ARG}, --${_VERSION_ARG_NAME}" + echo "${_indent}Show version and about information flag." + echo + + echo "Build Options:" + echo "${_indent}-${_CONF_ARG}, --${_CONF_ARG_NAME} value" + echo "${_indent}Specifies the conf file. Defaults to ${_SCRIPT_NAME}.conf." + echo + echo "${_indent}-${_RID_ARG}, --${_RID_ARG_NAME} value" + echo "${_indent}Dotnet publish runtime identifier. Valid examples include:" + echo "${_indent}linux-x64 and linux-arm64. Default is linux-x64 if unspecified." + echo "${_indent}See also: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog" + echo + echo "${_indent}-${_KIND_ARG}, --${_KIND_ARG_NAME} value" + echo "${_indent}Package output kind. Value must be one of: ${_APPIMAGE_KIND} or ${_ZIP_KIND}." + echo "${_indent}Default is ${_APPIMAGE_KIND} if unspecified." + echo + echo "${_indent}-${_VERBOSE_ARG}, --${_VERBOSE_ARG_NAME}" + echo "${_indent}Verbose review info output flag." + echo + echo "${_indent}-${_RUNAPP_ARG}, --${_RUNAPP_ARG_NAME}" + echo "${_indent}Run the application after successful build flag." + echo + echo "${_indent}-${_SKIPYES_ARG}, --${_SKIPYES_ARG_NAME}" + echo "${_indent}Skip confirmation prompt flag (assumes yes)." + echo + echo "${_indent}-${_OUTPUT_ARG}, --${_OUTPUT_ARG_NAME}" + echo "${_indent}Explicit final output filename (excluding directory part)." + echo + + echo "Example:" + echo "${_indent}${_SCRIPT_NAME} -${_RID_ARG} linux-arm64" + echo + + exit $_exit_help +fi + +if [ ${_version_arg_value} == true ]; then + echo + echo "${_SCRIPT_TITLE}, ${_SCRIPT_VERSION}" + echo "${_SCRIPT_COPYRIGHT}" + echo "${_SCRIPT_WEBSITE}" + echo + echo "MIT License" + echo + echo "Permission is hereby granted, free of charge, to any person obtaining a copy" + echo "of this software and associated documentation files (the "Software"), to deal" + echo "in the Software without restriction, including without limitation the rights" + echo "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell" + echo "copies of the Software, and to permit persons to whom the Software is" + echo "furnished to do so, subject to the following conditions:" + echo + echo "The above copyright notice and this permission notice shall be included in all" + echo "copies or substantial portions of the Software." + echo + echo "THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR" + echo "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY," + echo "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE" + echo "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER" + echo "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM," + echo "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE" + echo "SOFTWARE." + echo + + exit 0 +fi + + +############################### +# SOURCE & WORKING +############################### + +# Export these now as may be +# useful in an advanced config file +export DOTNET_RID="${_rid_arg_value}" +export PKG_KIND="${_kind_arg_value}" +export ISO_DATE=`date +"%Y-%m-%d"` + +if [ ! -f "${_conf_arg_value}" ]; then + echo "Configuration file not found: ${_conf_arg_value}" + echo + exit 1 +fi + +# Export contents to any post publish command +set -a + +# Source local to PWD +source "${_conf_arg_value}" +set +a + +# For AppImage tool and backward compatibility +export VERSION="${APP_VERSION}" + + +# Then change PWD to conf file +cd "$(dirname "${_conf_arg_value}")" + + +############################### +# SANITY +############################### + +if (( ${CONF_IMPL_VERSION} < ${_SCRIPT_IMPL_MIN} )) || (( ${CONF_IMPL_VERSION} > ${_SCRIPT_IMPL_MAX} )); then + echo "Configuration format version ${_SCRIPT_IMPL_VERSION} not compatible" + echo "Older conf file but newer ${_SCRIPT_NAME} implementation?" + echo "Update from: ${_SCRIPT_WEBSITE}" + echo + exit 1 +fi + +assert_mandatory "APP_MAIN" "${APP_MAIN}" +assert_mandatory "APP_ID" "${APP_ID}" +assert_mandatory "APP_ICON_SRC" "${APP_ICON_SRC}" +assert_mandatory "DE_NAME" "${DE_NAME}" +assert_mandatory "DE_CATEGORIES" "${DE_CATEGORIES}" +assert_mandatory "PKG_OUTPUT_DIR" "${PKG_OUTPUT_DIR}" + +if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then + assert_mandatory "APPIMAGETOOL_COMMAND" "${APPIMAGETOOL_COMMAND}" +fi + +assert_opt_file "APP_ICON_SRC" "${APP_ICON_SRC}" +assert_opt_file "APP_XML_SRC" "${APP_XML_SRC}" + +if [ "${DE_TERMINAL_FLAG}" != "true" ] && [ "${DE_TERMINAL_FLAG}" != "false" ]; then + echo "DE_TERMINAL_FLAG invalid value: ${DE_TERMINAL_FLAG}" + echo + exit 1 +fi + +if [ "${DOTNET_PROJECT_PATH}" == "${_DOTNET_NONE}" ] && [ "${POST_PUBLISH}" == "" ]; then + echo "No publish or build operation defined (nothing will be built)" + echo "See DOTNET_PROJECT_PATH and POST_PUBLISH in: ${_conf_arg_value}" + echo + exit 1 +fi + +if [ "${DOTNET_PROJECT_PATH}" != "" ] && [ "${DOTNET_PROJECT_PATH}" != "${_DOTNET_NONE}" ] && + [ ! -f "${DOTNET_PROJECT_PATH}" ] && [ ! -d "${DOTNET_PROJECT_PATH}" ]; then + echo "DOTNET_PROJECT_PATH path not found: ${DOTNET_PROJECT_PATH}" + echo + exit 1 +fi + +if [ "${_kind_arg_value}" != "${_APPIMAGE_KIND}" ] && [ "${_kind_arg_value}" != "${_ZIP_KIND}" ]; then + echo "Invalid argument value: ${_kind_arg_value}" + echo "Use one of: ${_APPIMAGE_KIND} or ${_ZIP_KIND}" + echo + exit 1 +fi + + +# Detect if publish for windows +_exec_ext="" +declare -l _tw="${_rid_arg_value}" + +if [[ "${_tw}" == "win"* ]]; then + + # May use this in future + _exec_ext=".exe" + + if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then + echo "Invalid AppImage payload" + echo "Looks like a windows binary to be packaged as AppImage." + echo "Use --${_KIND_ARG_NAME} ${_ZIP_KIND} instead." + echo + exit 1 + fi +fi + + +############################### +# VARIABLES +############################### + +# Abbreviate RID where it maps well to arch +if [ "${_rid_arg_value}" == "linux-x64" ]; then + _file_out_arch="-x86_64" +elif [ "${_rid_arg_value}" == "linux-arm64" ]; then + _file_out_arch="-aarch64" +else + # Otherwise use RID itself + _file_out_arch="-${_rid_arg_value}" +fi + +# APPDIR LOCATIONS +export APPDIR_ROOT="${PKG_OUTPUT_DIR}/AppDir" + +if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then + # AppImage + export APPDIR_USR="${APPDIR_ROOT}/usr" + export APPDIR_BIN="${APPDIR_ROOT}/usr/bin" + export APPDIR_SHARE="${APPDIR_ROOT}/usr/share" + + _local_run="usr/bin/${APP_MAIN}${_exec_ext}" +else + # Simple zip + export APPDIR_USR="" + export APPDIR_BIN="${APPDIR_ROOT}" + export APPDIR_SHARE="${APPDIR_ROOT}" + + _local_run="${APP_MAIN}${_exec_ext}" +fi + +export APPRUN_TARGET="${APPDIR_BIN}/${APP_MAIN}${_exec_ext}" + + +# DOTNET PUBLISH +if [ "${DOTNET_PROJECT_PATH}" != "${_DOTNET_NONE}" ]; then + + _publish_cmd="dotnet publish" + + if [ "${DOTNET_PROJECT_PATH}" != "" ] && [ "${DOTNET_PROJECT_PATH}" != "." ]; then + _publish_cmd="${_publish_cmd} \"${DOTNET_PROJECT_PATH}\"" + fi + + _publish_cmd="${_publish_cmd} -r ${_rid_arg_value}" + + if [ "${APP_VERSION}" != "" ]; then + _publish_cmd="${_publish_cmd} -p:Version=${APP_VERSION}" + fi + + if [ "${DOTNET_PUBLISH_ARGS}" != "" ]; then + _publish_cmd="${_publish_cmd} ${DOTNET_PUBLISH_ARGS}" + fi + + _publish_cmd="${_publish_cmd} -o \"${APPDIR_BIN}\"" + +fi + + +# PACKAGE OUTPUT +if [ $PKG_VERSION_FLAG == true ] && [ "${APP_VERSION}" != "" ]; then + _version_out="-${APP_VERSION}" +fi + +if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then + + # AppImageTool + if [ "${_output_arg_value}" != "" ]; then + _package_out="${PKG_OUTPUT_DIR}/${_output_arg_value}" + else + _package_out="${PKG_OUTPUT_DIR}/${APP_MAIN}${_version_out}${_file_out_arch}${PKG_APPIMAGE_SUFFIX}" + fi + + _package_cmd="${APPIMAGETOOL_COMMAND}" + + if [ "${PKG_APPIMAGE_ARGS}" != "" ]; then + _package_cmd="${_package_cmd} ${PKG_APPIMAGE_ARGS}" + fi + + _package_cmd="${_package_cmd} \"${APPDIR_ROOT}\" \"${_package_out}\"" + + if [ ${_runapp_arg_value} == true ]; then + _packrun_cmd="${_package_out}" + fi + +else + + # Simple zip + if [ "${_output_arg_value}" != "" ]; then + _package_out="${PKG_OUTPUT_DIR}/${_output_arg_value}" + else + _package_out="${PKG_OUTPUT_DIR}/${APP_MAIN}${_version_out}${_file_out_arch}.zip" + fi + + _package_cmd="(cd \"${APPDIR_ROOT}\" && zip -r \"${PWD}/${_package_out}\" ./)" + + if [ ${_runapp_arg_value} == true ]; then + _packrun_cmd="${APPRUN_TARGET}" + fi + +fi + + +############################### +# DESKTOP ENTRY & APPDATA +############################### + +if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then + + _desktop="[Desktop Entry]\n" + _desktop="${_desktop}Type=Application\n" + _desktop="${_desktop}Name=${DE_NAME}\n" + _desktop="${_desktop}Exec=AppRun\n" + _desktop="${_desktop}Terminal=${DE_TERMINAL_FLAG}\n" + _desktop="${_desktop}Categories=${DE_CATEGORIES}\n" + + # Follow app-id + _desktop="${_desktop}Icon=${APP_ID}\n" + + if [ "${DE_COMMENT}" != "" ]; then + _desktop="${_desktop}Comment=${DE_COMMENT}\n" + fi + + if [ "${DE_KEYWORDS}" != "" ]; then + _desktop="${_desktop}Keywords=${DE_KEYWORDS}\n" + fi + + _desktop="${_desktop}${DE_EXTEND}\n" +fi + + +# Load appdata.xml +if [ "${APP_XML_SRC}" != "" ]; then + + if command -v envsubst &> /dev/null; then + _appxml=$(envsubst <"${APP_XML_SRC}") + else + _appxml=$(<"${APP_XML_SRC}") + echo "WARNING: Variable substitution not available for: ${APP_XML_SRC}" + echo + fi + +fi + + +############################### +# DISPLAY & CONFIRM +############################### + +echo "${_SCRIPT_TITLE}, ${_SCRIPT_VERSION}" +echo "${_SCRIPT_COPYRIGHT}" +echo + +echo "APP_MAIN: ${APP_MAIN}" +echo "APP_ID: ${APP_ID}" +echo "APP_VERSION: ${APP_VERSION}" +echo "OUTPUT: ${_package_out}" +echo + +if [ "${_desktop}" != "" ]; then + echo -e "${_desktop}" +fi + +if [ ${_verbose_arg_value} == true ] && [ "${_appxml}" != "" ]; then + echo -e "${_appxml}\n" +fi + +echo "Build Commands:" + +if [ "${_publish_cmd}" != "" ]; then + echo + echo "${_publish_cmd}" +fi + +if [ "${POST_PUBLISH}" != "" ]; then + echo + echo "${POST_PUBLISH}" +fi + +echo +echo "${_package_cmd}" +echo + +# Prompt +if [ $_skipyes_arg_value == false ]; then + + echo + read -p "Build now [N/y]? " prompt + + if [ "${prompt}" != "y" ] && [ "${prompt}" != "Y" ]; then + echo + exit 1 + fi + + # Continue + echo +fi + + +############################### +# PUBLISH & BUILD +############################### + +# Clean and ensure directoy exists +ensure_directory "${PKG_OUTPUT_DIR}" +remove_path "${APPDIR_ROOT}" +remove_path "${_package_out}" + +# Create AppDir structure +ensure_directory "${APPDIR_BIN}" + +if [ "${_kind_arg_value}" != "${_ZIP_KIND}" ]; then + # We also create usr/share/icons, as some packages require this. + # See: https://github.com/kuiperzone/Publish-AppImage/issues/7 + ensure_directory "${APPDIR_SHARE}/icons" +fi + +echo + +# Publish dotnet +if [ "${_publish_cmd}" != "" ]; then + exec_or_die "${_publish_cmd}" + echo +fi + +# Post-publish +if [ "${POST_PUBLISH}" != "" ]; then + + exec_or_die "${POST_PUBLISH}" + echo + +fi + +# Application file must exist! +if [ ! -f "${APPRUN_TARGET}" ]; then + echo "Expected application file not found: ${APPRUN_TARGET}" + echo + exit 1 +fi + +if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then + + echo + + # Create desktop + if [ "${_desktop}" != "" ]; then + _file="${APPDIR_ROOT}/${APP_ID}.desktop" + echo "Creating: ${_file}" + echo -e "${_desktop}" > "${_file}" + assert_result + fi + + if [ "${_appxml}" != "" ]; then + _dir="${APPDIR_SHARE}/metainfo" + _file="${_dir}/${APP_ID}.appdata.xml" + echo "Creating: ${_file}" + ensure_directory "${_dir}" + echo -e "${_appxml}" > "${_file}" + assert_result + + if [ "${_desktop}" != "" ]; then + # Copy of desktop under "applications" + # Needed for launchable in appinfo.xml (if used) + # See https://github.com/AppImage/AppImageKit/issues/603 + _dir="${APPDIR_SHARE}/applications" + _file="${_dir}/${APP_ID}.desktop" + echo "Creating: ${_file}" + ensure_directory "${_dir}" + echo -e "${_desktop}" > "${_file}" + assert_result + fi + fi + + # Copy icon + if [ "${APP_ICON_SRC}" != "" ]; then + + _icon_ext="${APP_ICON_SRC##*.}" + + if [ "${_icon_ext}" != "" ]; then + _icon_ext=".${_icon_ext}" + fi + + _temp="${APPDIR_ROOT}/${APP_ID}${_icon_ext}" + echo "Creating: ${_temp}" + + cp "${APP_ICON_SRC}" "${_temp}" + assert_result + fi + + # AppRun + _temp="${APPDIR_ROOT}/AppRun" + + if [ ! -f "${_temp}" ]; then + + echo "Creating: ${_temp}" + ln -s "${_local_run}" "${_temp}" + assert_result + fi +fi + +# Build package +echo +exec_or_die "${_package_cmd}" +echo + +echo "OUTPUT OK: ${_package_out}" +echo + +if [ "${_packrun_cmd}" != "" ]; then + echo "RUNNING ..." + exec_or_die "${_packrun_cmd}" + echo +fi + +exit 0 \ No newline at end of file diff --git a/build/resources/appimage/publish-appimage.conf b/build/resources/appimage/publish-appimage.conf new file mode 100644 index 00000000..6b17ddc0 --- /dev/null +++ b/build/resources/appimage/publish-appimage.conf @@ -0,0 +1,140 @@ +################################################################################ +# BASH FORMAT CONFIG: Publish-AppImage for .NET +# WEBPAGE : https://kuiper.zone/publish-appimage-dotnet/ +################################################################################ + + +######################################## +# Application +######################################## + +# Mandatory application (file) name. This must be the base name of the main +# runnable file to be created by the publish/build process. It should NOT +# include any directory part or extension, i.e. do not append ".exe" or ".dll" +# for dotnet. Example: "MyApp" +APP_MAIN="sourcegit" + +# Mandatory application ID in reverse DNS form, i.e. "tld.my-domain.MyApp". +# Exclude any ".desktop" post-fix. Note that reverse DNS form is necessary +# for compatibility with Freedesktop.org metadata. +APP_ID="com.sourcegit-scm.SourceGit" + +# Mandatory icon source file relative to this file (appimagetool seems to +# require this). Use .svg or .png only. PNG should be one of standard sizes, +# i.e, 128x128 or 256x256 pixels. Example: "Assets/app.svg" +APP_ICON_SRC="sourcegit.png" + +# Optional Freedesktop.org metadata source file relative to this file. It is not essential +# (leave empty) but will be used by appimagetool for repository information if provided. +# See for information: https://docs.appimage.org/packaging-guide/optional/appstream.html +# NB. The file may embed bash variables defined in this file and those listed below +# (these will be substituted during the build). Examples include: "${APP_ID}" +# and "". +# $ISO_DATE : date of build, i.e. "2021-10-29", +# $APP_VERSION : application version (if provided), +# Example: "Assets/appdata.xml". +APP_XML_SRC="sourcegit.appdata.xml" + + +######################################## +# Desktop Entry +######################################## + +# Mandatory friendly name of the application. +DE_NAME="SourceGit" + +# Mandatory category(ies), separated with semicolon, in which the entry should be +# shown. See https://specifications.freedesktop.org/menu-spec/latest/apa.html +# Examples: "Development", "Graphics", "Network", "Utility" etc. +DE_CATEGORIES="Utility" + +# Optional short comment text (single line). +# Example: "Perform calculations" +DE_COMMENT="Open-source GUI client for git users" + +# Optional keywords, separated with semicolon. Values are not meant for +# display and should not be redundant with the value of DE_NAME. +DE_KEYWORDS="" + +# Flag indicating whether the program runs in a terminal window. Use true or false only. +DE_TERMINAL_FLAG=false + +# Optional name-value text to be appended to the Desktop Entry file, thus providing +# additional metadata. Name-values should not be redundant with values above and +# are to be terminated with new line ("\n"). +# Example: "Comment[fr]=Effectue des calculs compliqués\nMimeType=image/x-foo" +DE_EXTEND="" + + +######################################## +# Dotnet Publish +######################################## + +# Optional path relative to this file in which to find the dotnet project (.csproj) +# or solution (.sln) file, or the directory containing it. If empty (default), a single +# project or solution file is expected under the same directory as this file. +# IMPORTANT. If set to "null", dotnet publish is disabled (it is NOT called). Instead, +# only POST_PUBLISH is called. Example: "Source/MyProject" +DOTNET_PROJECT_PATH="../../../src/SourceGit.csproj" + +# Optional arguments suppled to "dotnet publish". Do NOT include "-r" (runtime) or version here as they will +# be added (see also $APP_VERSION). Typically you want as a minimum: "-c Release --self-contained true". +# Additional useful arguments include: +# "-p:DebugType=None -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishTrimmed=true -p:TrimMode=link" +# Refer: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish +DOTNET_PUBLISH_ARGS="-c Release -p:DebugType=None -p:DebugSymbols=false -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained" + + +######################################## +# POST-PUBLISH +######################################## + +# Optional post-publish or standalone build command. The value could, for example, copy +# additional files into the "bin" directory. The working directory will be the location +# of this file. The value is mandatory if DOTNET_PROJECT_PATH equals "null". In +# addition to variables in this file, the following variables are exported prior: +# $ISO_DATE : date of build, i.e. "2021-10-29", +# $APP_VERSION : application version (if provided), +# $DOTNET_RID : dotnet runtime identifier string provided at command line (i.e. "linux-x64), +# $PKG_KIND : package kind (i.e. "appimage", "zip") provided at command line. +# $APPDIR_ROOT : AppImage build directory root (i.e. "AppImages/AppDir"). +# $APPDIR_USR : AppImage user directory under root (i.e. "AppImages/AppDir/usr"). +# $APPDIR_BIN : AppImage bin directory under root (i.e. "AppImages/AppDir/usr/bin"). +# $APPRUN_TARGET : The expected target executable file (i.e. "AppImages/AppDir/usr/bin/app-name"). +# Example: "Assets/post-publish.sh" +POST_PUBLISH="mv AppImages/AppDir/usr/bin/SourceGit AppImages/AppDir/usr/bin/sourcegit; rm -f AppImages/AppDir/usr/bin/*.dbg" + + +######################################## +# Package Output +######################################## + +# Additional arguments for use with appimagetool. See appimagetool --help. +# Default is empty. Example: "--sign" +PKG_APPIMAGE_ARGS="--runtime-file=runtime-x86_64" + +# Mandatory output directory relative to this file. It will be created if it does not +# exist. It will contain the final package file and temporary AppDir. Default: "AppImages". +PKG_OUTPUT_DIR="AppImages" + +# Boolean which sets whether to include the application version in the filename of the final +# output package (i.e. "HelloWorld-1.2.3-x86_64.AppImage"). It is ignored if $APP_VERSION is +# empty or the "output" command arg is specified. Default and recommended: false. +PKG_VERSION_FLAG=false + +# Optional AppImage output filename extension. It is ignored if generating a zip file, or if +# the "output" command arg is specified. Default and recommended: ".AppImage". +PKG_APPIMAGE_SUFFIX=".AppImage" + + +######################################## +# Advanced Other +######################################## + +# The appimagetool command. Default is "appimagetool" which is expected to be found +# in the $PATH. If the tool is not in path or has different name, a full path can be given, +# example: "/home/user/Apps/appimagetool-x86_64.AppImage" +APPIMAGETOOL_COMMAND="appimagetool" + +# Internal use only. Used for compatibility between conf and script. Do not modify. +CONF_IMPL_VERSION=1 \ No newline at end of file diff --git a/build/resources/appimage/runtime-x86_64 b/build/resources/appimage/runtime-x86_64 new file mode 100755 index 00000000..0c9535a1 Binary files /dev/null and b/build/resources/appimage/runtime-x86_64 differ diff --git a/build/resources/appimage/sourcegit.appdata.xml b/build/resources/appimage/sourcegit.appdata.xml new file mode 100644 index 00000000..ca304b4b --- /dev/null +++ b/build/resources/appimage/sourcegit.appdata.xml @@ -0,0 +1,15 @@ + + + com.sourcegit-scm.SourceGit + MIT + MIT + SourceGit + Open-source GUI client for git users + +

Open-source GUI client for git users

+
+ com.sourcegit-scm.SourceGit.desktop + + com.sourcegit-scm.SourceGit.desktop + +
\ No newline at end of file diff --git a/build/resources/appimage/sourcegit.png b/build/resources/appimage/sourcegit.png new file mode 100644 index 00000000..8cdcd3a8 Binary files /dev/null and b/build/resources/appimage/sourcegit.png differ diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 61f00074..901a9b5b 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -4,10 +4,11 @@ using System.Text.Json.Serialization; namespace SourceGit { [JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)] - [JsonSerializable(typeof(Models.Version))] - [JsonSerializable(typeof(Models.JetBrainsState))] [JsonSerializable(typeof(List))] - [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Models.JetBrainsState))] + [JsonSerializable(typeof(Models.Version))] + [JsonSerializable(typeof(Models.CustomColorSchema))] [JsonSerializable(typeof(ViewModels.Preference))] + [JsonSerializable(typeof(ViewModels.RepositorySettings))] internal partial class JsonCodeGen : JsonSerializerContext { } } diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 0b319792..f989a325 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Reflection; @@ -159,15 +160,33 @@ namespace SourceGit app._colorOverrides = null; } + Models.CommitGraph.SetDefaultPens(); + if (!string.IsNullOrEmpty(colorsFile) && File.Exists(colorsFile)) { try { var resDic = new ResourceDictionary(); - var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.DictionaryStringString); - foreach (var kv in schema) - resDic[kv.Key] = Color.Parse(kv.Value); + var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.CustomColorSchema); + foreach (var kv in schema.Basic) + { + if (kv.Key.Equals("SystemAccentColor", StringComparison.Ordinal)) + resDic["SystemAccentColor"] = Color.Parse(kv.Value); + else + resDic[$"Color.{kv.Key}"] = Color.Parse(kv.Value); + } + + + if (schema.Graph.Count > 0) + { + var penColors = new List(); + + foreach (var c in schema.Graph) + penColors.Add(Color.Parse(c)); + + Models.CommitGraph.SetPenColors(penColors); + } app.Resources.MergedDictionaries.Add(resDic); app._colorOverrides = resDic; @@ -187,6 +206,18 @@ namespace SourceGit } } + public static async Task GetClipboardTextAsync() + { + if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow.Clipboard is { } clipboard) + { + return await clipboard.GetTextAsync(); + } + } + return default; + } + public static string Text(string key, params object[] args) { var fmt = Current.FindResource($"Text.{key}") as string; @@ -259,6 +290,21 @@ namespace SourceGit }); } + public static ViewModels.Repository FindOpenedRepository(string repoPath) + { + if (Current is App app && app._launcher != null) + { + foreach (var page in app._launcher.Pages) + { + var id = page.Node.Id.Replace("\\", "/"); + if (id == repoPath && page.Data is ViewModels.Repository repo) + return repo; + } + } + + return null; + } + public static void Quit() { if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) @@ -288,9 +334,10 @@ namespace SourceGit _launcher = new ViewModels.Launcher(); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; - if (ViewModels.Preference.Instance.ShouldCheck4UpdateOnStartup) + var pref = ViewModels.Preference.Instance; + if (pref.ShouldCheck4UpdateOnStartup) { - ViewModels.Preference.Save(); + pref.Save(); Check4Update(); } } @@ -317,18 +364,6 @@ namespace SourceGit }); } - public static async Task GetClipboardTextAsync() - { - if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - if (desktop.MainWindow.Clipboard is { } clipboard) - { - return await clipboard.GetTextAsync(); - } - } - return default; - } - private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; private ResourceDictionary _colorOverrides = null; diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs index 5d047d8c..e4c7f12a 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -9,7 +9,6 @@ namespace SourceGit.Commands [GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")] private static partial Regex REG_FORMAT(); - private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); public Blame(string repo, string file, string revision) { @@ -67,7 +66,7 @@ namespace SourceGit.Commands var commit = match.Groups[1].Value; var author = match.Groups[2].Value; var timestamp = int.Parse(match.Groups[3].Value); - var when = UTC_START.AddSeconds(timestamp).ToString("yyyy/MM/dd"); + var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString("yyyy/MM/dd"); var info = new Models.BlameLineInfo() { diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs index 8ac6501f..492d00c7 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Commit : Command { - public Commit(string repo, string message, bool amend, bool allowEmpty = false) + public Commit(string repo, string message, bool autoStage, bool amend, bool allowEmpty = false) { var file = Path.GetTempFileName(); File.WriteAllText(file, message); @@ -12,6 +12,8 @@ namespace SourceGit.Commands WorkingDirectory = repo; Context = repo; Args = $"commit --file=\"{file}\""; + if (autoStage) + Args += " --all"; if (amend) Args += " --amend --no-edit"; if (allowEmpty) diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index d65fee4e..ca1d83d6 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -7,7 +7,7 @@ namespace SourceGit.Commands { public class Fetch : Command { - public Fetch(string repo, string remote, bool prune, Action outputHandler) + public Fetch(string repo, string remote, bool prune, bool noTags, Action outputHandler) { _outputHandler = outputHandler; WorkingDirectory = repo; @@ -24,9 +24,15 @@ namespace SourceGit.Commands Args = "-c credential.helper=manager "; } - Args += "fetch --force --progress --verbose "; + Args += "fetch --progress --verbose "; if (prune) Args += "--prune "; + + if (noTags) + Args += "--no-tags "; + else + Args += "--force "; + Args += remote; AutoFetch.MarkFetched(repo); @@ -132,7 +138,7 @@ namespace SourceGit.Commands { var job = new Job { - Cmd = new Fetch(repo, "--all", true, null) { RaiseError = false }, + Cmd = new Fetch(repo, "--all", true, false, null) { RaiseError = false }, NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)), }; diff --git a/src/Commands/IsLFSFiltered.cs b/src/Commands/IsLFSFiltered.cs index b29039de..2a7234bb 100644 --- a/src/Commands/IsLFSFiltered.cs +++ b/src/Commands/IsLFSFiltered.cs @@ -6,7 +6,15 @@ { WorkingDirectory = repo; Context = repo; - Args = $"check-attr -a -z \"{path}\""; + Args = $"check-attr -z filter \"{path}\""; + RaiseError = false; + } + + public IsLFSFiltered(string repo, string sha, string path) + { + WorkingDirectory = repo; + Context = repo; + Args = $"check-attr --source {sha} -z filter \"{path}\""; RaiseError = false; } diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index d4f15dda..43418825 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Pull : Command { - public Pull(string repo, string remote, string branch, bool useRebase, Action outputHandler) + public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action outputHandler) { _outputHandler = outputHandler; WorkingDirectory = repo; @@ -24,6 +24,9 @@ namespace SourceGit.Commands Args += "pull --verbose --progress --tags "; if (useRebase) Args += "--rebase "; + if (noTags) + Args += "--no-tags "; + Args += $"{remote} {branch}"; } diff --git a/src/Commands/QueryFileSize.cs b/src/Commands/QueryFileSize.cs index 5ce7641e..c36984dd 100644 --- a/src/Commands/QueryFileSize.cs +++ b/src/Commands/QueryFileSize.cs @@ -4,7 +4,6 @@ namespace SourceGit.Commands { public partial class QueryFileSize : Command { - [GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")] private static partial Regex REG_FORMAT(); @@ -25,9 +24,7 @@ namespace SourceGit.Commands { var match = REG_FORMAT().Match(rs.StdOut); if (match.Success) - { return long.Parse(match.Groups[1].Value); - } } return 0; diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index 6c200940..99e89093 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -10,7 +10,7 @@ namespace SourceGit.Commands { public static void Run(string repo, string revision, string file, string saveTo) { - var isLFSFiltered = new IsLFSFiltered(repo, file).Result(); + var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result(); if (isLFSFiltered) { var tmpFile = saveTo + ".tmp"; diff --git a/src/Converters/BranchConverters.cs b/src/Converters/BranchConverters.cs deleted file mode 100644 index d20ed89f..00000000 --- a/src/Converters/BranchConverters.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Avalonia.Data.Converters; - -namespace SourceGit.Converters -{ - public static class BranchConverters - { - public static readonly FuncValueConverter ToName = - new FuncValueConverter(v => v.IsLocal ? v.Name : $"{v.Remote}/{v.Name}"); - } -} diff --git a/src/Converters/DecoratorTypeConverters.cs b/src/Converters/DecoratorTypeConverters.cs index e19cb37c..f730b613 100644 --- a/src/Converters/DecoratorTypeConverters.cs +++ b/src/Converters/DecoratorTypeConverters.cs @@ -11,8 +11,8 @@ namespace SourceGit.Converters new FuncValueConverter(v => { if (v == Models.DecoratorType.Tag) - return Models.DecoratorResources.Backgrounds[0]; - return Models.DecoratorResources.Backgrounds[1]; + return Application.Current.FindResource("Brush.DecoratorTag") as IBrush; + return Application.Current.FindResource("Brush.DecoratorBranch") as IBrush; }); public static readonly FuncValueConverter ToIcon = diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 64f9b357..137f6c9b 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -1,4 +1,5 @@ -using Avalonia.Data.Converters; +using Avalonia; +using Avalonia.Data.Converters; namespace SourceGit.Converters { @@ -24,5 +25,8 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsSubjectLengthGood = new FuncValueConverter(v => v <= ViewModels.Preference.Instance.SubjectGuideLength); + + public static readonly FuncValueConverter ToTreeMargin = + new FuncValueConverter(v => new Thickness(v * 16, 0, 0, 0)); } } diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs index dac55076..fef0f584 100644 --- a/src/Converters/ListConverters.cs +++ b/src/Converters/ListConverters.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using Avalonia.Data.Converters; @@ -11,5 +12,11 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsNotNullOrEmpty = new FuncValueConverter(v => v != null && v.Count > 0); + + public static readonly FuncValueConverter, List> Top100Changes = + new FuncValueConverter, List>(v => (v == null || v.Count < 100) ? v : v.GetRange(0, 100)); + + public static readonly FuncValueConverter IsOnlyTop100Shows = + new FuncValueConverter(v => v != null && v.Count > 100); } } diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index a22ee553..07f5374e 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -11,5 +11,7 @@ public string UpstreamTrackStatus { get; set; } public string Remote { get; set; } public bool IsHead { get; set; } + + public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}"; } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 363b4b08..0a95eac3 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -19,14 +19,13 @@ namespace SourceGit.Models public bool IsMerged { get; set; } = false; public Thickness Margin { get; set; } = new Thickness(0); - public string AuthorTimeStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd HH:mm:ss"); - public string CommitterTimeStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd HH:mm:ss"); - public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd"); - public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd"); + + public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); + public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); + public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd"); + public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToString("yyyy/MM/dd"); public bool IsCommitterVisible => Author != Committer || AuthorTime != CommitterTime; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; - - private static readonly DateTime _utcStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); } } diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 6b26eba5..bc0ea8e1 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -8,17 +8,6 @@ namespace SourceGit.Models { public class CommitGraph { - public static readonly Pen[] Pens = [ - new Pen(Brushes.Orange, 2), - new Pen(Brushes.ForestGreen, 2), - new Pen(Brushes.Gold, 2), - new Pen(Brushes.Magenta, 2), - new Pen(Brushes.Red, 2), - new Pen(Brushes.Gray, 2), - new Pen(Brushes.Turquoise, 2), - new Pen(Brushes.Olive, 2), - ]; - public class Path { public List Points = new List(); @@ -113,7 +102,28 @@ namespace SourceGit.Models public List Links { get; set; } = new List(); public List Dots { get; set; } = new List(); - public static CommitGraph Parse(List commits, int colorCount) + public static List Pens + { + get; + private set; + } = new List(); + + public static void SetDefaultPens() + { + SetPenColors(_defaultPenColors); + } + + public static void SetPenColors(List colors) + { + Pens.Clear(); + + foreach (var c in colors) + Pens.Add(new Pen(c.ToUInt32(), 2)); + + _penCount = colors.Count; + } + + public static CommitGraph Parse(List commits) { double UNIT_WIDTH = 12; double HALF_WIDTH = 6; @@ -184,7 +194,7 @@ namespace SourceGit.Models major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY)); unsolved.Add(major); temp.Paths.Add(major.Path); - colorIdx = (colorIdx + 1) % colorCount; + colorIdx = (colorIdx + 1) % _penCount; } // Calculate link position of this commit. @@ -223,7 +233,7 @@ namespace SourceGit.Models var l = new PathHelper(commit.Parents[j], isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT)); unsolved.Add(l); temp.Paths.Add(l.Path); - colorIdx = (colorIdx + 1) % colorCount; + colorIdx = (colorIdx + 1) % _penCount; } } @@ -257,5 +267,19 @@ namespace SourceGit.Models return temp; } + + private static int _penCount = 0; + private static readonly List _defaultPenColors = [ + Colors.Orange, + Colors.ForestGreen, + Colors.Gold, + Colors.Magenta, + Colors.Red, + Colors.Gray, + Colors.Turquoise, + Colors.Olive, + Colors.Khaki, + Colors.Lime, + ]; } } diff --git a/src/Models/CustomColorSchema.cs b/src/Models/CustomColorSchema.cs new file mode 100644 index 00000000..4266b98e --- /dev/null +++ b/src/Models/CustomColorSchema.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public class CustomColorSchema + { + public Dictionary Basic { get; set; } = new Dictionary(); + public List Graph { get; set; } = new List(); + } +} diff --git a/src/Models/Decorator.cs b/src/Models/Decorator.cs index 60ffc1ee..235101cc 100644 --- a/src/Models/Decorator.cs +++ b/src/Models/Decorator.cs @@ -1,6 +1,4 @@ -using Avalonia.Media; - -namespace SourceGit.Models +namespace SourceGit.Models { public enum DecoratorType { @@ -17,12 +15,4 @@ namespace SourceGit.Models public DecoratorType Type { get; set; } = DecoratorType.None; public string Name { get; set; } = ""; } - - public static class DecoratorResources - { - public static readonly IBrush[] Backgrounds = [ - new SolidColorBrush(0xFF02C302), - new SolidColorBrush(0xFFFFB835), - ]; - } } diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs index 2fab0f2f..06da763a 100644 --- a/src/Models/Stash.cs +++ b/src/Models/Stash.cs @@ -4,13 +4,11 @@ namespace SourceGit.Models { public class Stash { - private static readonly DateTime UTC_START = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); - public string Name { get; set; } = ""; public string SHA { get; set; } = ""; public ulong Time { get; set; } = 0; public string Message { get; set; } = ""; - public string TimeStr => UTC_START.AddSeconds(Time).ToString("yyyy/MM/dd HH:mm:ss"); + public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); } } diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs index b0d619e1..abdd24df 100644 --- a/src/Models/Statistics.cs +++ b/src/Models/Statistics.cs @@ -49,7 +49,6 @@ namespace SourceGit.Models public Statistics() { - _utcStart = DateTime.UnixEpoch; _today = DateTime.Today; _thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24 - _today.Hour * 3600 - _today.Minute * 60 - _today.Second); _thisWeekEnd = _thisWeekStart.AddDays(7); @@ -115,7 +114,7 @@ namespace SourceGit.Models public void AddCommit(string committer, double timestamp) { - var time = _utcStart.AddSeconds(timestamp).ToLocalTime(); + var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); if (time.CompareTo(_thisWeekStart) >= 0 && time.CompareTo(_thisWeekEnd) < 0) { Week.AddCommit((int)time.DayOfWeek, committer); @@ -136,7 +135,6 @@ namespace SourceGit.Models Week.Complete(); } - private readonly DateTime _utcStart; private readonly DateTime _today; private readonly DateTime _thisWeekStart; private readonly DateTime _thisWeekEnd; diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 0586ee99..55b05a01 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -111,6 +111,7 @@ AUTHOR CHANGED COMMITTER + Shows only the first 100 changes. See all changes on the CHANGES tab. MESSAGE PARENTS REFS @@ -200,6 +201,7 @@ Fast-Forward (without checkout) Fetch Fetch all remotes + Fetch without tags Prune remote dead branches Remote: Fetch Remote Changes @@ -386,6 +388,7 @@ Discard Do Nothing Stash & Reapply + Fetch without tags Remote: Pull (Fetch & Merge) Use rebase instead of merge @@ -540,6 +543,8 @@ Ignore files in the same folder Ignore this file only Amend + Auto-Stage + Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected. You can stage this file now. COMMIT COMMIT & PUSH diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 3b6bfb9d..58a72d47 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -114,6 +114,7 @@ 修改者 变更列表 提交者 + 仅显示前100项变更。请前往【变更对比】页面查看全部。 提交信息 父提交 相关引用 @@ -203,6 +204,7 @@ 快进(fast-forward,无需checkout) 拉取(fetch) 拉取所有的远程仓库 + 不拉取远程标签 自动清理远程已删除分支 远程仓库 : 拉取远程仓库内容 @@ -389,6 +391,7 @@ 丢弃更改 不做处理 贮藏并自动恢复 + 不拉取远程标签 远程 : 拉回(拉取并合并) 使用变基方式合并分支 @@ -542,6 +545,8 @@ 忽略同目录下所有文件 忽略本文件 修补(--amend) + 自动暂存(--all) + 提交前自动将修改过和删除的文件加入暂存区,但新增文件需要手动添加。 现在您已可将其加入暂存区中 提交 提交并推送 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 5563a21a..73176126 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -114,6 +114,7 @@ 修改者 變更列表 提交者 + 僅顯示前100項變更。 請前往『變更對比』頁面查看全部。 提交資訊 父提交 相關引用 @@ -203,6 +204,7 @@ 快進(fast-forward,無需checkout) 拉取(fetch) 拉取所有的遠端倉庫 + 不拉取遠端標籤 自動清理遠端已刪除分支 遠端倉庫 : 拉取遠端倉庫內容 @@ -389,6 +391,7 @@ 丟棄更改 不做處理 儲藏並自動恢復 + 不拉取遠端標籤 遠端 : 拉回(拉取併合並) 使用變基方式合併分支 @@ -542,6 +545,8 @@ 忽略同路徑下所有檔案 忽略本檔案 修補(--amend) + 自動暫存(--all) + 提交前自動將修改過和刪除的檔案加入暫存區,但新增檔案需要手動添加。 現在您已可將其加入暫存區中 提交 提交併推送 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 460a6ce5..139e5bd5 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -29,7 +29,7 @@ - + Red @@ -160,6 +160,11 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs new file mode 100644 index 00000000..95e70f48 --- /dev/null +++ b/src/Views/BranchTree.axaml.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class BranchTreeNodeIcon : UserControl + { + public static readonly StyledProperty NodeProperty = + AvaloniaProperty.Register(nameof(Node)); + + public ViewModels.BranchTreeNode Node + { + get => GetValue(NodeProperty); + set => SetValue(NodeProperty, value); + } + + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register(nameof(IsExpanded)); + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + static BranchTreeNodeIcon() + { + NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + } + + private void UpdateContent() + { + var node = Node; + if (node == null) + { + Content = null; + return; + } + + if (node.Backend is Models.Remote) + { + CreateContent(12, new Thickness(0,2,0,0), "Icons.Remote"); + } + else if (node.Backend is Models.Branch branch) + { + if (branch.IsCurrent) + CreateContent(12, new Thickness(0,2,0,0), "Icons.Check"); + else + CreateContent(12, new Thickness(2,0,0,0), "Icons.Branch"); + } + else + { + if (node.IsExpanded) + CreateContent(10, new Thickness(0,2,0,0), "Icons.Folder.Open"); + else + CreateContent(10, new Thickness(0,2,0,0), "Icons.Folder.Fill"); + } + } + + private void CreateContent(double size, Thickness margin, string iconKey) + { + var geo = this.FindResource(iconKey) as StreamGeometry; + if (geo == null) + return; + + Content = new Path() + { + Width = size, + Height = size, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = margin, + Data = geo, + }; + } + } + + public partial class BranchTree : UserControl + { + public static readonly StyledProperty> NodesProperty = + AvaloniaProperty.Register>(nameof(Nodes)); + + public List Nodes + { + get => GetValue(NodesProperty); + set => SetValue(NodesProperty, value); + } + + public AvaloniaList Rows + { + get; + private set; + } = new AvaloniaList(); + + public static readonly RoutedEvent SelectionChangedEvent = + RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler SelectionChanged + { + add { AddHandler(SelectionChangedEvent, value); } + remove { RemoveHandler(SelectionChangedEvent, value); } + } + + public static readonly RoutedEvent RowsChangedEvent = + RoutedEvent.Register(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler RowsChanged + { + add { AddHandler(RowsChangedEvent, value); } + remove { RemoveHandler(RowsChangedEvent, value); } + } + + public BranchTree() + { + InitializeComponent(); + } + + public void UnselectAll() + { + BranchesPresenter.SelectedItem = null; + } + + protected override void OnSizeChanged(SizeChangedEventArgs e) + { + base.OnSizeChanged(e); + + if (Bounds.Height >= 23.0) + BranchesPresenter.Height = Bounds.Height; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == NodesProperty) + { + Rows.Clear(); + + if (Nodes is { Count: > 0 }) + { + var rows = new List(); + MakeRows(rows, Nodes, 0); + Rows.AddRange(rows); + } + + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + else if (change.Property == IsVisibleProperty) + { + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } + + private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e) + { + var repo = DataContext as ViewModels.Repository; + if (repo?.Settings == null) + return; + + foreach (var item in e.AddedItems) + { + if (item is ViewModels.BranchTreeNode node) + node.IsSelected = true; + } + + foreach (var item in e.RemovedItems) + { + if (item is ViewModels.BranchTreeNode node) + node.IsSelected = false; + } + + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + repo.NavigateToCommit(branch.Head); + + var prev = null as ViewModels.BranchTreeNode; + foreach (var row in Rows) + { + if (row.IsSelected) + { + if (prev is { IsSelected: true }) + { + var prevTop = prev.CornerRadius.TopLeft; + prev.CornerRadius = new CornerRadius(prevTop, 0); + row.CornerRadius = new CornerRadius(0, 4); + } + else + { + row.CornerRadius = new CornerRadius(4); + } + } + + prev = row; + } + + RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); + } + + private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2) + { + var repo = DataContext as ViewModels.Repository; + if (repo?.Settings == null) + return; + + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) + { + var menu = repo.CreateContextMenuForRemote(remote); + this.OpenContextMenu(menu); + return; + } + + var branches = new List(); + foreach (var item in selected) + { + if (item is ViewModels.BranchTreeNode node) + CollectBranchesInNode(branches, node); + } + + if (branches.Count == 1) + { + var branch = branches[0]; + var menu = branch.IsLocal ? + repo.CreateContextMenuForLocalBranch(branch) : + repo.CreateContextMenuForRemoteBranch(branch); + this.OpenContextMenu(menu); + } + else if (branches.Find(x => x.IsCurrent) == null) + { + var menu = new ContextMenu(); + var deleteMulti = new MenuItem(); + deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); + deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); + deleteMulti.Click += (_, ev) => + { + repo.DeleteMultipleBranches(branches, branches[0].IsLocal); + ev.Handled = true; + }; + menu.Items.Add(deleteMulti); + this.OpenContextMenu(menu); + } + } + + private void OnDoubleTappedBranchNode(object sender, TappedEventArgs _) + { + if (sender is Grid { DataContext: ViewModels.BranchTreeNode node }) + { + if (node.Backend is Models.Branch branch) + { + if (branch.IsCurrent) + return; + + if (DataContext is ViewModels.Repository { Settings: not null } repo) + repo.CheckoutBranch(branch); + } + else + { + node.IsExpanded = !node.IsExpanded; + + var rows = Rows; + var depth = node.Depth; + var idx = rows.IndexOf(node); + if (idx == -1) + return; + + if (node.IsExpanded) + { + var subtree = new List(); + MakeRows(subtree, node.Children, depth + 1); + rows.InsertRange(idx + 1, subtree); + } + else + { + var removeCount = 0; + for (int i = idx + 1; i < rows.Count; i++) + { + var row = rows[i]; + if (row.Depth <= depth) + break; + + removeCount++; + } + rows.RemoveRange(idx + 1, removeCount); + } + + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } + } + + private void OnToggleFilter(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo) + { + if (toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + repo.UpdateFilter(branch.FullName, toggle.IsChecked == true); + } + + e.Handled = true; + } + + private void MakeRows(List rows, List nodes, int depth) + { + foreach (var node in nodes) + { + node.Depth = depth; + rows.Add(node); + + if (!node.IsExpanded || node.Backend is Models.Branch) + continue; + + MakeRows(rows, node.Children, depth + 1); + } + } + + private void CollectBranchesInNode(List outs, ViewModels.BranchTreeNode node) + { + if (node.Backend is Models.Branch branch && !outs.Contains(branch)) + { + outs.Add(branch); + return; + } + + foreach (var sub in node.Children) + CollectBranchesInNode(outs, sub); + } + } +} + diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index 42fa2b48..19b75a50 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -90,11 +90,11 @@ - + - + @@ -104,9 +104,7 @@ - - - + diff --git a/src/Views/CommitDetail.axaml b/src/Views/CommitDetail.axaml index a46d5e54..8be5eeac 100644 --- a/src/Views/CommitDetail.axaml +++ b/src/Views/CommitDetail.axaml @@ -17,58 +17,55 @@ - - - + + + + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index 1209e41a..999d1c07 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -10,37 +10,23 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnChangeListDoubleTapped(object sender, TappedEventArgs e) + private void OnChangeDoubleTapped(object sender, TappedEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail) + if (DataContext is ViewModels.CommitDetail detail && sender is Grid grid && grid.DataContext is Models.Change change) { - var datagrid = sender as DataGrid; - if (datagrid.SelectedItem == null) - { - e.Handled = true; - return; - } - detail.ActivePageIndex = 1; - detail.SelectedChanges = new() { datagrid.SelectedItem as Models.Change }; + detail.SelectedChanges = new() { change }; } e.Handled = true; } - private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail) + if (DataContext is ViewModels.CommitDetail detail && sender is Grid grid && grid.DataContext is Models.Change change) { - var datagrid = sender as DataGrid; - if (datagrid.SelectedItem == null) - { - e.Handled = true; - return; - } - - var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); - datagrid.OpenContextMenu(menu); + var menu = detail.CreateChangeContextMenu(change); + grid.OpenContextMenu(menu); } e.Handled = true; diff --git a/src/Views/CommitMessageTextBox.axaml.cs b/src/Views/CommitMessageTextBox.axaml.cs index 043c05aa..1b458ede 100644 --- a/src/Views/CommitMessageTextBox.axaml.cs +++ b/src/Views/CommitMessageTextBox.axaml.cs @@ -20,6 +20,11 @@ namespace SourceGit.Views protected override Type StyleKeyOverride => typeof(TextBox); + public void Paste(string text) + { + OnTextInput(new TextInputEventArgs() { Text = text }); + } + protected override void OnKeyDown(KeyEventArgs e) { var dump = new KeyEventArgs() @@ -112,7 +117,7 @@ namespace SourceGit.Views } } - private void OnSubjectTextBoxPreviewKeyDown(object sender, KeyEventArgs e) + private async void OnSubjectTextBoxPreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter || (e.Key == Key.Right && SubjectEditor.CaretIndex == Subject.Length)) { @@ -120,6 +125,36 @@ namespace SourceGit.Views DescriptionEditor.CaretIndex = 0; e.Handled = true; } + else if (e.Key == Key.V && ((OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Meta) || (!OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Control))) + { + var text = await App.GetClipboardTextAsync(); + if (!string.IsNullOrWhiteSpace(text)) + { + text = text.Trim(); + + if (SubjectEditor.CaretIndex == Subject.Length) + { + var idx = text.IndexOf('\n'); + if (idx == -1) + { + SubjectEditor.Paste(text); + } + else + { + SubjectEditor.Paste(text.Substring(0, idx)); + DescriptionEditor.Focus(); + DescriptionEditor.CaretIndex = 0; + DescriptionEditor.Paste(text.Substring(idx + 1) + "\n"); + } + } + else + { + SubjectEditor.Paste(text.ReplaceLineEndings(" ")); + } + } + + e.Handled = true; + } } private void OnDescriptionTextBoxPreviewKeyDown(object sender, KeyEventArgs e) diff --git a/src/Views/CreateBranch.axaml b/src/Views/CreateBranch.axaml index a6565807..f69ea150 100644 --- a/src/Views/CreateBranch.axaml +++ b/src/Views/CreateBranch.axaml @@ -24,7 +24,7 @@ - + diff --git a/src/Views/CreateTag.axaml b/src/Views/CreateTag.axaml index f3de6717..a3f901a4 100644 --- a/src/Views/CreateTag.axaml +++ b/src/Views/CreateTag.axaml @@ -23,7 +23,7 @@ - + diff --git a/src/Views/DeleteBranch.axaml b/src/Views/DeleteBranch.axaml index 8245c091..b2693bf0 100644 --- a/src/Views/DeleteBranch.axaml +++ b/src/Views/DeleteBranch.axaml @@ -2,10 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" - xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.DeleteBranch" x:DataType="vm:DeleteBranch"> @@ -18,7 +15,7 @@ - + diff --git a/src/Views/DeleteMultipleBranches.axaml b/src/Views/DeleteMultipleBranches.axaml index 75f28674..9fd87b90 100644 --- a/src/Views/DeleteMultipleBranches.axaml +++ b/src/Views/DeleteMultipleBranches.axaml @@ -64,8 +64,7 @@ - + diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml index e3680140..cd100b6d 100644 --- a/src/Views/DiffView.axaml +++ b/src/Views/DiffView.axaml @@ -66,8 +66,14 @@ Background="Transparent" Padding="9,6" IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap, Mode=TwoWay}" - IsVisible="{Binding IsTextDiff}" ToolTip.Tip="{DynamicResource Text.Diff.ToggleWordWrap}"> + + + + + + + diff --git a/src/Views/FastForwardWithoutCheckout.axaml b/src/Views/FastForwardWithoutCheckout.axaml index cb33c09e..713e9c3b 100644 --- a/src/Views/FastForwardWithoutCheckout.axaml +++ b/src/Views/FastForwardWithoutCheckout.axaml @@ -16,7 +16,7 @@ - + diff --git a/src/Views/Fetch.axaml b/src/Views/Fetch.axaml index aaf9bec6..1f91b24a 100644 --- a/src/Views/Fetch.axaml +++ b/src/Views/Fetch.axaml @@ -12,7 +12,7 @@ - + + + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 565daa8e..e07b8d0b 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -46,7 +46,7 @@ - + diff --git a/src/Views/InteractiveRebase.axaml b/src/Views/InteractiveRebase.axaml index 8e966509..d4238d65 100644 --- a/src/Views/InteractiveRebase.axaml +++ b/src/Views/InteractiveRebase.axaml @@ -54,7 +54,7 @@ - + diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index d2d1bebb..0d749e60 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -13,7 +13,7 @@ - + - + @@ -88,6 +88,10 @@ + + diff --git a/src/Views/Rebase.axaml b/src/Views/Rebase.axaml index aa52a15b..f98756af 100644 --- a/src/Views/Rebase.axaml +++ b/src/Views/Rebase.axaml @@ -31,7 +31,7 @@ - + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 6e7873e9..569709a6 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -119,7 +119,7 @@ - + @@ -231,396 +231,303 @@ - - - - - + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + - + + + + + + + + + + + - - - + - - - - - + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -794,14 +701,14 @@ - + - + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 43e9a4a6..0e3d7a19 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Generic; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.VisualTree; namespace SourceGit.Views { @@ -17,6 +15,12 @@ namespace SourceGit.Views InitializeComponent(); } + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + UpdateLeftSidebarLayout(); + } + private void OpenWithExternalTools(object sender, RoutedEventArgs e) { if (sender is Button button && DataContext is ViewModels.Repository repo) @@ -49,24 +53,23 @@ namespace SourceGit.Views e.Handled = true; } - private async void OpenStatistics(object sender, RoutedEventArgs e) + private async void OpenStatistics(object _, RoutedEventArgs e) { - if (DataContext is ViewModels.Repository repo) + if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner) { var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; - await dialog.ShowDialog(TopLevel.GetTopLevel(this) as Window); + await dialog.ShowDialog(owner); e.Handled = true; } } private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { - var grid = sender as Grid; - if (e.Property == IsVisibleProperty && grid.IsVisible) + if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true}) txtSearchCommitsBox.Focus(); } - private void OnSearchKeyDown(object sender, KeyEventArgs e) + private void OnSearchKeyDown(object _, KeyEventArgs e) { if (e.Key == Key.Enter) { @@ -79,190 +82,39 @@ namespace SourceGit.Views private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) + if (sender is DataGrid { SelectedItem: Models.Commit commit } && DataContext is ViewModels.Repository repo) { - if (DataContext is ViewModels.Repository repo) - { - var commit = datagrid.SelectedItem as Models.Commit; - repo.NavigateToCommit(commit.SHA); - } + repo.NavigateToCommit(commit.SHA); } + e.Handled = true; } - private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) + private void OnBranchTreeRowsChanged(object _, RoutedEventArgs e) { - if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - remoteBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - ViewModels.BranchTreeNode prev = null; - foreach (var node in repo.LocalBranchTrees) - node.UpdateCornerRadius(ref prev); - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch) - repo.NavigateToCommit((node.Backend as Models.Branch).Head); - } - } + UpdateLeftSidebarLayout(); + e.Handled = true; } - private void OnRemoteBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - localBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - ViewModels.BranchTreeNode prev = null; - foreach (var node in repo.RemoteBranchTrees) - node.UpdateCornerRadius(ref prev); - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch) - repo.NavigateToCommit((node.Backend as Models.Branch).Head); - } - } - } - - private void OnLocalBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) + private void OnLocalBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { remoteBranchTree.UnselectAll(); tagsList.SelectedItem = null; - - var repo = DataContext as ViewModels.Repository; - var tree = sender as TreeView; - if (tree.SelectedItems.Count == 0) - { - e.Handled = true; - return; - } - - var branches = new List(); - foreach (var item in tree.SelectedItems) - CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); - - if (branches.Count == 1) - { - var item = (e.Source as Control)?.FindAncestorOfType(true); - if (item != null) - { - var menu = repo.CreateContextMenuForLocalBranch(branches[0]); - item.OpenContextMenu(menu); - } - } - else if (branches.Count > 1 && branches.Find(x => x.IsCurrent) == null) - { - var menu = new ContextMenu(); - var deleteMulti = new MenuItem(); - deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); - deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); - deleteMulti.Click += (_, ev) => - { - repo.DeleteMultipleBranches(branches, true); - ev.Handled = true; - }; - menu.Items.Add(deleteMulti); - tree.OpenContextMenu(menu); - } - - e.Handled = true; } - - private void OnRemoteBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) + + private void OnRemoteBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { localBranchTree.UnselectAll(); tagsList.SelectedItem = null; - - var repo = DataContext as ViewModels.Repository; - var tree = sender as TreeView; - if (tree.SelectedItems.Count == 0) - { - e.Handled = true; - return; - } - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node != null && node.IsRemote) - { - var item = (e.Source as Control)?.FindAncestorOfType(true); - if (item != null && item.DataContext == node) - { - var menu = repo.CreateContextMenuForRemote(node.Backend as Models.Remote); - item.OpenContextMenu(menu); - } - - e.Handled = true; - return; - } - } - - var branches = new List(); - foreach (var item in tree.SelectedItems) - CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); - - if (branches.Count == 1) - { - var item = (e.Source as Control)?.FindAncestorOfType(true); - if (item != null) - { - var menu = repo.CreateContextMenuForRemoteBranch(branches[0]); - item.OpenContextMenu(menu); - } - } - else if (branches.Count > 1) - { - var menu = new ContextMenu(); - var deleteMulti = new MenuItem(); - deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); - deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); - deleteMulti.Click += (_, ev) => - { - repo.DeleteMultipleBranches(branches, false); - ev.Handled = true; - }; - menu.Items.Add(deleteMulti); - tree.OpenContextMenu(menu); - } - - e.Handled = true; } - private void OnDoubleTappedBranchNode(object sender, TappedEventArgs e) + private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs _) { - if (!ViewModels.PopupHost.CanCreatePopup()) - return; - - if (sender is Grid grid && DataContext is ViewModels.Repository repo) - { - var node = grid.DataContext as ViewModels.BranchTreeNode; - if (node != null && node.IsBranch) - { - var branch = node.Backend as Models.Branch; - if (branch.IsCurrent) - return; - - repo.CheckoutBranch(branch); - e.Handled = true; - } - } - } - - private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) + if (sender is DataGrid { SelectedItem: Models.Tag tag }) { localBranchTree.UnselectAll(); remoteBranchTree.UnselectAll(); - var tag = datagrid.SelectedItem as Models.Tag; if (DataContext is ViewModels.Repository repo) repo.NavigateToCommit(tag.SHA); } @@ -280,37 +132,11 @@ namespace SourceGit.Views e.Handled = true; } - private void OnTagPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void OnToggleTagFilter(object sender, RoutedEventArgs e) { - if (e.Property == DataGrid.ItemsSourceProperty && DataContext is ViewModels.Repository vm) + if (sender is ToggleButton { DataContext: Models.Tag tag } toggle && DataContext is ViewModels.Repository repo) { - if (vm.VisibleTags == null) - return; - - var desiredHeight = tagsList.RowHeight * vm.VisibleTags.Count; - tagsList.Height = Math.Min(200, desiredHeight); - } - } - - private void OnToggleFilter(object sender, RoutedEventArgs e) - { - if (sender is ToggleButton toggle) - { - var filter = string.Empty; - if (toggle.DataContext is ViewModels.BranchTreeNode node) - { - if (node.IsBranch) - filter = (node.Backend as Models.Branch).FullName; - } - else if (toggle.DataContext is Models.Tag tag) - { - filter = tag.Name; - } - - if (!string.IsNullOrEmpty(filter) && DataContext is ViewModels.Repository repo) - { - repo.UpdateFilter(filter, toggle.IsChecked == true); - } + repo.UpdateFilter(tag.Name, toggle.IsChecked == true); } e.Handled = true; @@ -330,10 +156,10 @@ namespace SourceGit.Views private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e) { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) { - var submodule = datagrid.SelectedItem as string; - (DataContext as ViewModels.Repository).OpenSubmodule(submodule); + var submodule = grid.SelectedItem as string; + repo.OpenSubmodule(submodule); } e.Handled = true; @@ -341,11 +167,11 @@ namespace SourceGit.Views private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e) { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) { - var worktree = datagrid.SelectedItem as Models.Worktree; + var worktree = grid.SelectedItem as Models.Worktree; var menu = repo.CreateContextMenuForWorktree(worktree); - datagrid.OpenContextMenu(menu); + grid.OpenContextMenu(menu); } e.Handled = true; @@ -353,31 +179,143 @@ namespace SourceGit.Views private void OnDoubleTappedWorktree(object sender, TappedEventArgs e) { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) { - var worktree = datagrid.SelectedItem as Models.Worktree; - (DataContext as ViewModels.Repository).OpenWorktree(worktree); + var worktree = grid.SelectedItem as Models.Worktree; + repo.OpenWorktree(worktree); } e.Handled = true; } - private void CollectBranchesFromNode(List outs, ViewModels.BranchTreeNode node) + private void OnLeftSidebarDataGridPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e) { - if (node == null || node.IsRemote) + if (e.Property == DataGrid.ItemsSourceProperty || e.Property == DataGrid.IsVisibleProperty) + { + UpdateLeftSidebarLayout(); + } + } + + private void UpdateLeftSidebarLayout() + { + var vm = DataContext as ViewModels.Repository; + if (vm == null || vm.Settings == null) return; - if (node.IsFolder) + if (!IsLoaded) + return; + + var leftHeight = leftSidebarGroups.Bounds.Height - 28.0 * 5; + var localBranchRows = vm.IsLocalBranchGroupExpanded ? localBranchTree.Rows.Count : 0; + var remoteBranchRows = vm.IsRemoteGroupExpanded ? remoteBranchTree.Rows.Count : 0; + var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; + var desiredTag = vm.IsTagGroupExpanded ? tagsList.RowHeight * vm.VisibleTags.Count : 0; + var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? submoduleList.RowHeight * vm.Submodules.Count : 0; + var desiredWorktree = vm.IsWorktreeGroupExpanded ? worktreeList.RowHeight * vm.Worktrees.Count : 0; + var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree; + var hasOverflow = (desiredBranches + desiredOthers > leftHeight); + + if (vm.IsTagGroupExpanded) { - foreach (var child in node.Children) - CollectBranchesFromNode(outs, child); + var height = desiredTag; + if (hasOverflow) + { + var test = leftHeight - desiredBranches - desiredSubmodule - desiredWorktree; + if (test < 0) + height = Math.Min(200, height); + else + height = Math.Max(200, test); + } + + leftHeight -= height; + tagsList.Height = height; + hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight; + } + + if (vm.IsSubmoduleGroupExpanded) + { + var height = desiredSubmodule; + if (hasOverflow) + { + var test = leftHeight - desiredBranches - desiredWorktree; + if (test < 0) + height = Math.Min(200, height); + else + height = Math.Max(200, test); + } + + leftHeight -= height; + submoduleList.Height = height; + hasOverflow = (desiredBranches + desiredWorktree) > leftHeight; + } + + if (vm.IsWorktreeGroupExpanded) + { + var height = desiredWorktree; + if (hasOverflow) + { + var test = leftHeight - desiredBranches; + if (test < 0) + height = Math.Min(200, height); + else + height = Math.Max(200, test); + } + + leftHeight -= height; + worktreeList.Height = height; + } + + if (desiredBranches > leftHeight) + { + var local = localBranchRows * 24.0; + var remote = remoteBranchRows * 24.0; + var half = leftHeight / 2; + if (vm.IsLocalBranchGroupExpanded) + { + if (vm.IsRemoteGroupExpanded) + { + if (local < half) + { + localBranchTree.Height = local; + remoteBranchTree.Height = leftHeight - local; + } + else if (remote < half) + { + remoteBranchTree.Height = remote; + localBranchTree.Height = leftHeight - remote; + } + else + { + localBranchTree.Height = half; + remoteBranchTree.Height = half; + } + } + else + { + localBranchTree.Height = leftHeight; + } + } + else if (vm.IsRemoteGroupExpanded) + { + remoteBranchTree.Height = leftHeight; + } } else { - var b = node.Backend as Models.Branch; - if (b != null && !outs.Contains(b)) - outs.Add(b); + if (vm.IsLocalBranchGroupExpanded) + { + var height = localBranchRows * 24; + localBranchTree.Height = height; + } + + if (vm.IsRemoteGroupExpanded) + { + var height = remoteBranchRows * 24; + remoteBranchTree.Height = height; + } } + + leftSidebarGroups.InvalidateMeasure(); } } } diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index 4b4fc449..954f0ecc 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -9,8 +9,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Interactivity; using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Styling; using AvaloniaEdit; using AvaloniaEdit.Document; diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 22ee8123..5e925686 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -429,7 +429,18 @@ namespace SourceGit.Views { var builder = new StringBuilder(); foreach (var line in textDiff.Lines) - builder.AppendLine(line.Content); + { + if (line.Content.Length > 10000) + { + builder.Append(line.Content.Substring(0, 1000)); + builder.Append($"...({line.Content.Length - 1000} character trimmed)"); + builder.AppendLine(); + } + else + { + builder.AppendLine(line.Content); + } + } Text = builder.ToString(); } @@ -718,7 +729,18 @@ namespace SourceGit.Views var builder = new StringBuilder(); var lines = IsOld ? diff.Old : diff.New; foreach (var line in lines) - builder.AppendLine(line.Content); + { + if (line.Content.Length > 10000) + { + builder.Append(line.Content.Substring(0, 1000)); + builder.Append($"...({line.Content.Length - 1000} characters trimmed)"); + builder.AppendLine(); + } + else + { + builder.AppendLine(line.Content); + } + } Text = builder.ToString(); } diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index aa5054c4..0fb89989 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -253,11 +253,10 @@ namespace SourceGit.Views return; } - var gitDir = new Commands.QueryGitDir(root).Result(); Dispatcher.UIThread.Invoke(() => { - var repo = ViewModels.Preference.AddRepository(root, gitDir); - var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, parent, true); + var normalizedPath = root.Replace("\\", "/"); + var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(normalizedPath, parent, true); launcher.OpenRepositoryInTab(node, page); }); }); diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 49d693ae..8e77d9d8 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -174,7 +174,7 @@ - +