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 @@
AUTHORCHANGEDCOMMITTER
+ Shows only the first 100 changes. See all changes on the CHANGES tab.MESSAGEPARENTSREFS
@@ -200,6 +201,7 @@
Fast-Forward (without checkout)FetchFetch all remotes
+ Fetch without tagsPrune remote dead branchesRemote:Fetch Remote Changes
@@ -386,6 +388,7 @@
DiscardDo NothingStash & Reapply
+ Fetch without tagsRemote:Pull (Fetch & Merge)Use rebase instead of merge
@@ -540,6 +543,8 @@
Ignore files in the same folderIgnore this file onlyAmend
+ 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.COMMITCOMMIT & 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 @@
-
+