Merge branch 'release/v8.20'

This commit is contained in:
leo 2024-07-08 10:23:50 +08:00
commit 7d3f228e0c
No known key found for this signature in database
85 changed files with 2523 additions and 1233 deletions

View file

@ -52,7 +52,9 @@ This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationD
For **Windows** users: For **Windows** users:
* **MSYS Git is NOT supported**. Please use official [Git for Windows](https://git-scm.com/download/win) instead. * **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: 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 | | JetBrains Fleet | YES | YES | YES | FLEET_PATH |
| Sublime Text | YES | YES | YES | SUBLIME_TEXT_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. * 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. * 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. * On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables.
## Screenshots ## Screenshots
@ -94,46 +96,9 @@ This app supports open repository in external tools listed in the table below.
![Theme Light](./screenshots/theme_light.png) ![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: You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git)
| 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.
## Contributing ## Contributing

View file

@ -1 +1 @@
8.19 8.20

View file

@ -3,17 +3,16 @@
version=`cat ../VERSION` version=`cat ../VERSION`
# Cleanup # Cleanup
rm -rf SourceGit *.tar.gz resources/deb/opt *.deb *.rpm rm -rf SourceGit *.tar.gz resources/deb/opt *.deb *.rpm *.AppImage
# Compile # Generic AppImage
dotnet publish ../src/SourceGit.csproj -c Release -r linux-x64 -o SourceGit -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained cd resources/appimage
mv SourceGit/SourceGit SourceGit/sourcegit ./publish-appimage -y -o sourcegit-${version}.linux.x86_64.AppImage
cp resources/app/App.icns SourceGit/sourcegit.icns
rm -f SourceGit/*.dbg
# General Linux archive # Move to build dir
tar -zcvf sourcegit_${version}.linux-x64.tar.gz SourceGit mv AppImages/sourcegit-${version}.linux.x86_64.AppImage ../../
rm -f SourceGit/sourcegit.icns mv AppImages/AppDir/usr/bin ../../SourceGit
cd ../../
# Debain/Ubuntu package # Debain/Ubuntu package
mkdir -p resources/deb/opt/sourcegit/ mkdir -p resources/deb/opt/sourcegit/

View file

@ -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

View file

@ -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: "<id>${APP_ID}</id>"
# and "<release version="${APP_VERSION}" date="${ISO_DATE}">".
# $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

Binary file not shown.

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.sourcegit-scm.SourceGit</id>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<name>SourceGit</name>
<summary>Open-source GUI client for git users</summary>
<description>
<p>Open-source GUI client for git users</p>
</description>
<launchable type="desktop-id">com.sourcegit-scm.SourceGit.desktop</launchable>
<provides>
<id>com.sourcegit-scm.SourceGit.desktop</id>
</provides>
</component>

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -4,10 +4,11 @@ using System.Text.Json.Serialization;
namespace SourceGit namespace SourceGit
{ {
[JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)] [JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.JetBrainsState))]
[JsonSerializable(typeof(List<Models.InteractiveRebaseJob>))] [JsonSerializable(typeof(List<Models.InteractiveRebaseJob>))]
[JsonSerializable(typeof(Dictionary<string, string>))] [JsonSerializable(typeof(Models.JetBrainsState))]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.CustomColorSchema))]
[JsonSerializable(typeof(ViewModels.Preference))] [JsonSerializable(typeof(ViewModels.Preference))]
[JsonSerializable(typeof(ViewModels.RepositorySettings))]
internal partial class JsonCodeGen : JsonSerializerContext { } internal partial class JsonCodeGen : JsonSerializerContext { }
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
@ -159,15 +160,33 @@ namespace SourceGit
app._colorOverrides = null; app._colorOverrides = null;
} }
Models.CommitGraph.SetDefaultPens();
if (!string.IsNullOrEmpty(colorsFile) && File.Exists(colorsFile)) if (!string.IsNullOrEmpty(colorsFile) && File.Exists(colorsFile))
{ {
try try
{ {
var resDic = new ResourceDictionary(); var resDic = new ResourceDictionary();
var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.DictionaryStringString); var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.CustomColorSchema);
foreach (var kv in schema) foreach (var kv in schema.Basic)
resDic[kv.Key] = Color.Parse(kv.Value); {
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<Color>();
foreach (var c in schema.Graph)
penColors.Add(Color.Parse(c));
Models.CommitGraph.SetPenColors(penColors);
}
app.Resources.MergedDictionaries.Add(resDic); app.Resources.MergedDictionaries.Add(resDic);
app._colorOverrides = resDic; app._colorOverrides = resDic;
@ -187,6 +206,18 @@ namespace SourceGit
} }
} }
public static async Task<string> 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) public static string Text(string key, params object[] args)
{ {
var fmt = Current.FindResource($"Text.{key}") as string; 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() public static void Quit()
{ {
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
@ -288,9 +334,10 @@ namespace SourceGit
_launcher = new ViewModels.Launcher(); _launcher = new ViewModels.Launcher();
desktop.MainWindow = new Views.Launcher() { DataContext = _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(); Check4Update();
} }
} }
@ -317,18 +364,6 @@ namespace SourceGit
}); });
} }
public static async Task<string> 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 ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null; private ResourceDictionary _activeLocale = null;
private ResourceDictionary _colorOverrides = null; private ResourceDictionary _colorOverrides = null;

View file

@ -9,7 +9,6 @@ namespace SourceGit.Commands
[GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")] [GeneratedRegex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)")]
private static partial Regex REG_FORMAT(); 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) public Blame(string repo, string file, string revision)
{ {
@ -67,7 +66,7 @@ namespace SourceGit.Commands
var commit = match.Groups[1].Value; var commit = match.Groups[1].Value;
var author = match.Groups[2].Value; var author = match.Groups[2].Value;
var timestamp = int.Parse(match.Groups[3].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() var info = new Models.BlameLineInfo()
{ {

View file

@ -4,7 +4,7 @@ namespace SourceGit.Commands
{ {
public class Commit : Command 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(); var file = Path.GetTempFileName();
File.WriteAllText(file, message); File.WriteAllText(file, message);
@ -12,6 +12,8 @@ namespace SourceGit.Commands
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"commit --file=\"{file}\""; Args = $"commit --file=\"{file}\"";
if (autoStage)
Args += " --all";
if (amend) if (amend)
Args += " --amend --no-edit"; Args += " --amend --no-edit";
if (allowEmpty) if (allowEmpty)

View file

@ -7,7 +7,7 @@ namespace SourceGit.Commands
{ {
public class Fetch : Command public class Fetch : Command
{ {
public Fetch(string repo, string remote, bool prune, Action<string> outputHandler) public Fetch(string repo, string remote, bool prune, bool noTags, Action<string> outputHandler)
{ {
_outputHandler = outputHandler; _outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
@ -24,9 +24,15 @@ namespace SourceGit.Commands
Args = "-c credential.helper=manager "; Args = "-c credential.helper=manager ";
} }
Args += "fetch --force --progress --verbose "; Args += "fetch --progress --verbose ";
if (prune) if (prune)
Args += "--prune "; Args += "--prune ";
if (noTags)
Args += "--no-tags ";
else
Args += "--force ";
Args += remote; Args += remote;
AutoFetch.MarkFetched(repo); AutoFetch.MarkFetched(repo);
@ -132,7 +138,7 @@ namespace SourceGit.Commands
{ {
var job = new Job 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)), NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)),
}; };

View file

@ -6,7 +6,15 @@
{ {
WorkingDirectory = repo; WorkingDirectory = repo;
Context = 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; RaiseError = false;
} }

View file

@ -4,7 +4,7 @@ namespace SourceGit.Commands
{ {
public class Pull : Command public class Pull : Command
{ {
public Pull(string repo, string remote, string branch, bool useRebase, Action<string> outputHandler) public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action<string> outputHandler)
{ {
_outputHandler = outputHandler; _outputHandler = outputHandler;
WorkingDirectory = repo; WorkingDirectory = repo;
@ -24,6 +24,9 @@ namespace SourceGit.Commands
Args += "pull --verbose --progress --tags "; Args += "pull --verbose --progress --tags ";
if (useRebase) if (useRebase)
Args += "--rebase "; Args += "--rebase ";
if (noTags)
Args += "--no-tags ";
Args += $"{remote} {branch}"; Args += $"{remote} {branch}";
} }

View file

@ -4,7 +4,6 @@ namespace SourceGit.Commands
{ {
public partial class QueryFileSize : Command public partial class QueryFileSize : Command
{ {
[GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")] [GeneratedRegex(@"^\d+\s+\w+\s+[0-9a-f]+\s+(\d+)\s+.*$")]
private static partial Regex REG_FORMAT(); private static partial Regex REG_FORMAT();
@ -25,10 +24,8 @@ namespace SourceGit.Commands
{ {
var match = REG_FORMAT().Match(rs.StdOut); var match = REG_FORMAT().Match(rs.StdOut);
if (match.Success) if (match.Success)
{
return long.Parse(match.Groups[1].Value); return long.Parse(match.Groups[1].Value);
} }
}
return 0; return 0;
} }

View file

@ -10,7 +10,7 @@ namespace SourceGit.Commands
{ {
public static void Run(string repo, string revision, string file, string saveTo) 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) if (isLFSFiltered)
{ {
var tmpFile = saveTo + ".tmp"; var tmpFile = saveTo + ".tmp";

View file

@ -1,10 +0,0 @@
using Avalonia.Data.Converters;
namespace SourceGit.Converters
{
public static class BranchConverters
{
public static readonly FuncValueConverter<Models.Branch, string> ToName =
new FuncValueConverter<Models.Branch, string>(v => v.IsLocal ? v.Name : $"{v.Remote}/{v.Name}");
}
}

View file

@ -11,8 +11,8 @@ namespace SourceGit.Converters
new FuncValueConverter<Models.DecoratorType, IBrush>(v => new FuncValueConverter<Models.DecoratorType, IBrush>(v =>
{ {
if (v == Models.DecoratorType.Tag) if (v == Models.DecoratorType.Tag)
return Models.DecoratorResources.Backgrounds[0]; return Application.Current.FindResource("Brush.DecoratorTag") as IBrush;
return Models.DecoratorResources.Backgrounds[1]; return Application.Current.FindResource("Brush.DecoratorBranch") as IBrush;
}); });
public static readonly FuncValueConverter<Models.DecoratorType, StreamGeometry> ToIcon = public static readonly FuncValueConverter<Models.DecoratorType, StreamGeometry> ToIcon =

View file

@ -1,4 +1,5 @@
using Avalonia.Data.Converters; using Avalonia;
using Avalonia.Data.Converters;
namespace SourceGit.Converters namespace SourceGit.Converters
{ {
@ -24,5 +25,8 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter<int, bool> IsSubjectLengthGood = public static readonly FuncValueConverter<int, bool> IsSubjectLengthGood =
new FuncValueConverter<int, bool>(v => v <= ViewModels.Preference.Instance.SubjectGuideLength); new FuncValueConverter<int, bool>(v => v <= ViewModels.Preference.Instance.SubjectGuideLength);
public static readonly FuncValueConverter<int, Thickness> ToTreeMargin =
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));
} }
} }

View file

@ -1,4 +1,5 @@
using System.Collections; using System.Collections;
using System.Collections.Generic;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
@ -11,5 +12,11 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter<IList, bool> IsNotNullOrEmpty = public static readonly FuncValueConverter<IList, bool> IsNotNullOrEmpty =
new FuncValueConverter<IList, bool>(v => v != null && v.Count > 0); new FuncValueConverter<IList, bool>(v => v != null && v.Count > 0);
public static readonly FuncValueConverter<List<Models.Change>, List<Models.Change>> Top100Changes =
new FuncValueConverter<List<Models.Change>, List<Models.Change>>(v => (v == null || v.Count < 100) ? v : v.GetRange(0, 100));
public static readonly FuncValueConverter<IList, bool> IsOnlyTop100Shows =
new FuncValueConverter<IList, bool>(v => v != null && v.Count > 100);
} }
} }

View file

@ -11,5 +11,7 @@
public string UpstreamTrackStatus { get; set; } public string UpstreamTrackStatus { get; set; }
public string Remote { get; set; } public string Remote { get; set; }
public bool IsHead { get; set; } public bool IsHead { get; set; }
public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}";
} }
} }

View file

@ -19,14 +19,13 @@ namespace SourceGit.Models
public bool IsMerged { get; set; } = false; public bool IsMerged { get; set; } = false;
public Thickness Margin { get; set; } = new Thickness(0); 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 AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
public string AuthorTimeShortStr => _utcStart.AddSeconds(AuthorTime).ToString("yyyy/MM/dd"); public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
public string CommitterTimeShortStr => _utcStart.AddSeconds(CommitterTime).ToString("yyyy/MM/dd"); 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 IsCommitterVisible => Author != Committer || AuthorTime != CommitterTime;
public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; 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();
} }
} }

View file

@ -8,17 +8,6 @@ namespace SourceGit.Models
{ {
public class CommitGraph 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 class Path
{ {
public List<Point> Points = new List<Point>(); public List<Point> Points = new List<Point>();
@ -113,7 +102,28 @@ namespace SourceGit.Models
public List<Link> Links { get; set; } = new List<Link>(); public List<Link> Links { get; set; } = new List<Link>();
public List<Dot> Dots { get; set; } = new List<Dot>(); public List<Dot> Dots { get; set; } = new List<Dot>();
public static CommitGraph Parse(List<Commit> commits, int colorCount) public static List<Pen> Pens
{
get;
private set;
} = new List<Pen>();
public static void SetDefaultPens()
{
SetPenColors(_defaultPenColors);
}
public static void SetPenColors(List<Color> colors)
{
Pens.Clear();
foreach (var c in colors)
Pens.Add(new Pen(c.ToUInt32(), 2));
_penCount = colors.Count;
}
public static CommitGraph Parse(List<Commit> commits)
{ {
double UNIT_WIDTH = 12; double UNIT_WIDTH = 12;
double HALF_WIDTH = 6; double HALF_WIDTH = 6;
@ -184,7 +194,7 @@ namespace SourceGit.Models
major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY)); major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
unsolved.Add(major); unsolved.Add(major);
temp.Paths.Add(major.Path); temp.Paths.Add(major.Path);
colorIdx = (colorIdx + 1) % colorCount; colorIdx = (colorIdx + 1) % _penCount;
} }
// Calculate link position of this commit. // 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)); var l = new PathHelper(commit.Parents[j], isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT));
unsolved.Add(l); unsolved.Add(l);
temp.Paths.Add(l.Path); temp.Paths.Add(l.Path);
colorIdx = (colorIdx + 1) % colorCount; colorIdx = (colorIdx + 1) % _penCount;
} }
} }
@ -257,5 +267,19 @@ namespace SourceGit.Models
return temp; return temp;
} }
private static int _penCount = 0;
private static readonly List<Color> _defaultPenColors = [
Colors.Orange,
Colors.ForestGreen,
Colors.Gold,
Colors.Magenta,
Colors.Red,
Colors.Gray,
Colors.Turquoise,
Colors.Olive,
Colors.Khaki,
Colors.Lime,
];
} }
} }

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace SourceGit.Models
{
public class CustomColorSchema
{
public Dictionary<string, string> Basic { get; set; } = new Dictionary<string, string>();
public List<string> Graph { get; set; } = new List<string>();
}
}

View file

@ -1,6 +1,4 @@
using Avalonia.Media; namespace SourceGit.Models
namespace SourceGit.Models
{ {
public enum DecoratorType public enum DecoratorType
{ {
@ -17,12 +15,4 @@ namespace SourceGit.Models
public DecoratorType Type { get; set; } = DecoratorType.None; public DecoratorType Type { get; set; } = DecoratorType.None;
public string Name { get; set; } = ""; public string Name { get; set; } = "";
} }
public static class DecoratorResources
{
public static readonly IBrush[] Backgrounds = [
new SolidColorBrush(0xFF02C302),
new SolidColorBrush(0xFFFFB835),
];
}
} }

View file

@ -4,13 +4,11 @@ namespace SourceGit.Models
{ {
public class Stash 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 Name { get; set; } = "";
public string SHA { get; set; } = ""; public string SHA { get; set; } = "";
public ulong Time { get; set; } = 0; public ulong Time { get; set; } = 0;
public string Message { get; set; } = ""; 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");
} }
} }

View file

@ -49,7 +49,6 @@ namespace SourceGit.Models
public Statistics() public Statistics()
{ {
_utcStart = DateTime.UnixEpoch;
_today = DateTime.Today; _today = DateTime.Today;
_thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24 - _today.Hour * 3600 - _today.Minute * 60 - _today.Second); _thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24 - _today.Hour * 3600 - _today.Minute * 60 - _today.Second);
_thisWeekEnd = _thisWeekStart.AddDays(7); _thisWeekEnd = _thisWeekStart.AddDays(7);
@ -115,7 +114,7 @@ namespace SourceGit.Models
public void AddCommit(string committer, double timestamp) 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) if (time.CompareTo(_thisWeekStart) >= 0 && time.CompareTo(_thisWeekEnd) < 0)
{ {
Week.AddCommit((int)time.DayOfWeek, committer); Week.AddCommit((int)time.DayOfWeek, committer);
@ -136,7 +135,6 @@ namespace SourceGit.Models
Week.Complete(); Week.Complete();
} }
private readonly DateTime _utcStart;
private readonly DateTime _today; private readonly DateTime _today;
private readonly DateTime _thisWeekStart; private readonly DateTime _thisWeekStart;
private readonly DateTime _thisWeekEnd; private readonly DateTime _thisWeekEnd;

View file

@ -111,6 +111,7 @@
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">AUTHOR</x:String> <x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">AUTHOR</x:String>
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">CHANGED</x:String> <x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">CHANGED</x:String>
<x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">COMMITTER</x:String> <x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">COMMITTER</x:String>
<x:String x:Key="Text.CommitDetail.Info.GotoChangesPage" xml:space="preserve">Shows only the first 100 changes. See all changes on the CHANGES tab.</x:String>
<x:String x:Key="Text.CommitDetail.Info.Message" xml:space="preserve">MESSAGE</x:String> <x:String x:Key="Text.CommitDetail.Info.Message" xml:space="preserve">MESSAGE</x:String>
<x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">PARENTS</x:String> <x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">PARENTS</x:String>
<x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">REFS</x:String> <x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">REFS</x:String>
@ -200,6 +201,7 @@
<x:String x:Key="Text.FastForwardWithoutCheck" xml:space="preserve">Fast-Forward (without checkout)</x:String> <x:String x:Key="Text.FastForwardWithoutCheck" xml:space="preserve">Fast-Forward (without checkout)</x:String>
<x:String x:Key="Text.Fetch" xml:space="preserve">Fetch</x:String> <x:String x:Key="Text.Fetch" xml:space="preserve">Fetch</x:String>
<x:String x:Key="Text.Fetch.AllRemotes" xml:space="preserve">Fetch all remotes</x:String> <x:String x:Key="Text.Fetch.AllRemotes" xml:space="preserve">Fetch all remotes</x:String>
<x:String x:Key="Text.Fetch.NoTags" xml:space="preserve">Fetch without tags</x:String>
<x:String x:Key="Text.Fetch.Prune" xml:space="preserve">Prune remote dead branches</x:String> <x:String x:Key="Text.Fetch.Prune" xml:space="preserve">Prune remote dead branches</x:String>
<x:String x:Key="Text.Fetch.Remote" xml:space="preserve">Remote:</x:String> <x:String x:Key="Text.Fetch.Remote" xml:space="preserve">Remote:</x:String>
<x:String x:Key="Text.Fetch.Title" xml:space="preserve">Fetch Remote Changes</x:String> <x:String x:Key="Text.Fetch.Title" xml:space="preserve">Fetch Remote Changes</x:String>
@ -386,6 +388,7 @@
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Discard</x:String> <x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">Discard</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">Do Nothing</x:String> <x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">Do Nothing</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reapply</x:String> <x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reapply</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">Fetch without tags</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">Remote:</x:String> <x:String x:Key="Text.Pull.Remote" xml:space="preserve">Remote:</x:String>
<x:String x:Key="Text.Pull.Title" xml:space="preserve">Pull (Fetch &amp; Merge)</x:String> <x:String x:Key="Text.Pull.Title" xml:space="preserve">Pull (Fetch &amp; Merge)</x:String>
<x:String x:Key="Text.Pull.UseRebase" xml:space="preserve">Use rebase instead of merge</x:String> <x:String x:Key="Text.Pull.UseRebase" xml:space="preserve">Use rebase instead of merge</x:String>
@ -540,6 +543,8 @@
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">Ignore files in the same folder</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">Ignore files in the same folder</x:String>
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">Ignore this file only</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">Ignore this file only</x:String>
<x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">Amend</x:String> <x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">Amend</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage" xml:space="preserve">Auto-Stage</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage.Tip" xml:space="preserve">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.</x:String>
<x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">You can stage this file now.</x:String> <x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">You can stage this file now.</x:String>
<x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">COMMIT</x:String> <x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">COMMIT</x:String>
<x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">COMMIT &amp; PUSH</x:String> <x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">COMMIT &amp; PUSH</x:String>

View file

@ -114,6 +114,7 @@
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">修改者</x:String> <x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">修改者</x:String>
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">变更列表</x:String> <x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">变更列表</x:String>
<x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">提交者</x:String> <x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">提交者</x:String>
<x:String x:Key="Text.CommitDetail.Info.GotoChangesPage" xml:space="preserve">仅显示前100项变更。请前往【变更对比】页面查看全部。</x:String>
<x:String x:Key="Text.CommitDetail.Info.Message" xml:space="preserve">提交信息</x:String> <x:String x:Key="Text.CommitDetail.Info.Message" xml:space="preserve">提交信息</x:String>
<x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">父提交</x:String> <x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">父提交</x:String>
<x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">相关引用</x:String> <x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">相关引用</x:String>
@ -203,6 +204,7 @@
<x:String x:Key="Text.FastForwardWithoutCheck" xml:space="preserve">快进(fast-forward无需checkout)</x:String> <x:String x:Key="Text.FastForwardWithoutCheck" xml:space="preserve">快进(fast-forward无需checkout)</x:String>
<x:String x:Key="Text.Fetch" xml:space="preserve">拉取(fetch)</x:String> <x:String x:Key="Text.Fetch" xml:space="preserve">拉取(fetch)</x:String>
<x:String x:Key="Text.Fetch.AllRemotes" xml:space="preserve">拉取所有的远程仓库</x:String> <x:String x:Key="Text.Fetch.AllRemotes" xml:space="preserve">拉取所有的远程仓库</x:String>
<x:String x:Key="Text.Fetch.NoTags" xml:space="preserve">不拉取远程标签</x:String>
<x:String x:Key="Text.Fetch.Prune" xml:space="preserve">自动清理远程已删除分支</x:String> <x:String x:Key="Text.Fetch.Prune" xml:space="preserve">自动清理远程已删除分支</x:String>
<x:String x:Key="Text.Fetch.Remote" xml:space="preserve">远程仓库 </x:String> <x:String x:Key="Text.Fetch.Remote" xml:space="preserve">远程仓库 </x:String>
<x:String x:Key="Text.Fetch.Title" xml:space="preserve">拉取远程仓库内容</x:String> <x:String x:Key="Text.Fetch.Title" xml:space="preserve">拉取远程仓库内容</x:String>
@ -389,6 +391,7 @@
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">丢弃更改</x:String> <x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">丢弃更改</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">不做处理</x:String> <x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">不做处理</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">贮藏并自动恢复</x:String> <x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">贮藏并自动恢复</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">不拉取远程标签</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">远程 </x:String> <x:String x:Key="Text.Pull.Remote" xml:space="preserve">远程 </x:String>
<x:String x:Key="Text.Pull.Title" xml:space="preserve">拉回(拉取并合并)</x:String> <x:String x:Key="Text.Pull.Title" xml:space="preserve">拉回(拉取并合并)</x:String>
<x:String x:Key="Text.Pull.UseRebase" xml:space="preserve">使用变基方式合并分支</x:String> <x:String x:Key="Text.Pull.UseRebase" xml:space="preserve">使用变基方式合并分支</x:String>
@ -542,6 +545,8 @@
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">忽略同目录下所有文件</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">忽略同目录下所有文件</x:String>
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">忽略本文件</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">忽略本文件</x:String>
<x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修补(--amend)</x:String> <x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修补(--amend)</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage" xml:space="preserve">自动暂存(--all)</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage.Tip" xml:space="preserve">提交前自动将修改过和删除的文件加入暂存区,但新增文件需要手动添加。</x:String>
<x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">现在您已可将其加入暂存区中</x:String> <x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">现在您已可将其加入暂存区中</x:String>
<x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String> <x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String>
<x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交并推送</x:String> <x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交并推送</x:String>

View file

@ -114,6 +114,7 @@
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">修改者</x:String> <x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">修改者</x:String>
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">變更列表</x:String> <x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">變更列表</x:String>
<x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">提交者</x:String> <x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">提交者</x:String>
<x:String x:Key="Text.CommitDetail.Info.GotoChangesPage" xml:space="preserve">僅顯示前100項變更。 請前往『變更對比』頁面查看全部。</x:String>
<x:String x:Key="Text.CommitDetail.Info.Message" xml:space="preserve">提交資訊</x:String> <x:String x:Key="Text.CommitDetail.Info.Message" xml:space="preserve">提交資訊</x:String>
<x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">父提交</x:String> <x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">父提交</x:String>
<x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">相關引用</x:String> <x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">相關引用</x:String>
@ -203,6 +204,7 @@
<x:String x:Key="Text.FastForwardWithoutCheck" xml:space="preserve">快進(fast-forward無需checkout)</x:String> <x:String x:Key="Text.FastForwardWithoutCheck" xml:space="preserve">快進(fast-forward無需checkout)</x:String>
<x:String x:Key="Text.Fetch" xml:space="preserve">拉取(fetch)</x:String> <x:String x:Key="Text.Fetch" xml:space="preserve">拉取(fetch)</x:String>
<x:String x:Key="Text.Fetch.AllRemotes" xml:space="preserve">拉取所有的遠端倉庫</x:String> <x:String x:Key="Text.Fetch.AllRemotes" xml:space="preserve">拉取所有的遠端倉庫</x:String>
<x:String x:Key="Text.Fetch.NoTags" xml:space="preserve">不拉取遠端標籤</x:String>
<x:String x:Key="Text.Fetch.Prune" xml:space="preserve">自動清理遠端已刪除分支</x:String> <x:String x:Key="Text.Fetch.Prune" xml:space="preserve">自動清理遠端已刪除分支</x:String>
<x:String x:Key="Text.Fetch.Remote" xml:space="preserve">遠端倉庫 </x:String> <x:String x:Key="Text.Fetch.Remote" xml:space="preserve">遠端倉庫 </x:String>
<x:String x:Key="Text.Fetch.Title" xml:space="preserve">拉取遠端倉庫內容</x:String> <x:String x:Key="Text.Fetch.Title" xml:space="preserve">拉取遠端倉庫內容</x:String>
@ -389,6 +391,7 @@
<x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">丟棄更改</x:String> <x:String x:Key="Text.Pull.LocalChanges.Discard" xml:space="preserve">丟棄更改</x:String>
<x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">不做處理</x:String> <x:String x:Key="Text.Pull.LocalChanges.DoNothing" xml:space="preserve">不做處理</x:String>
<x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">儲藏並自動恢復</x:String> <x:String x:Key="Text.Pull.LocalChanges.StashAndReply" xml:space="preserve">儲藏並自動恢復</x:String>
<x:String x:Key="Text.Pull.NoTags" xml:space="preserve">不拉取遠端標籤</x:String>
<x:String x:Key="Text.Pull.Remote" xml:space="preserve">遠端 </x:String> <x:String x:Key="Text.Pull.Remote" xml:space="preserve">遠端 </x:String>
<x:String x:Key="Text.Pull.Title" xml:space="preserve">拉回(拉取併合並)</x:String> <x:String x:Key="Text.Pull.Title" xml:space="preserve">拉回(拉取併合並)</x:String>
<x:String x:Key="Text.Pull.UseRebase" xml:space="preserve">使用變基方式合併分支</x:String> <x:String x:Key="Text.Pull.UseRebase" xml:space="preserve">使用變基方式合併分支</x:String>
@ -542,6 +545,8 @@
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">忽略同路徑下所有檔案</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">忽略同路徑下所有檔案</x:String>
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">忽略本檔案</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">忽略本檔案</x:String>
<x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修補(--amend)</x:String> <x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修補(--amend)</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage" xml:space="preserve">自動暫存(--all)</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage.Tip" xml:space="preserve">提交前自動將修改過和刪除的檔案加入暫存區,但新增檔案需要手動添加。</x:String>
<x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">現在您已可將其加入暫存區中</x:String> <x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">現在您已可將其加入暫存區中</x:String>
<x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String> <x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String>
<x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交併推送</x:String> <x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交併推送</x:String>

View file

@ -29,7 +29,7 @@
<Style.Resources> <Style.Resources>
<SolidColorBrush x:Key="SystemControlErrorTextForegroundBrush" Color="Red"/> <SolidColorBrush x:Key="SystemControlErrorTextForegroundBrush" Color="Red"/>
<SolidColorBrush x:Key="SystemErrorTextColor" Color="Red"/> <Color x:Key="SystemErrorTextColor">Red</Color>
</Style.Resources> </Style.Resources>
</Style> </Style>
@ -160,6 +160,11 @@
<Setter Property="FontSize" Value="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize}"/> <Setter Property="FontSize" Value="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize}"/>
</Style> </Style>
<Style Selector="ToolTip">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG1}"/>
<Setter Property="Background" Value="{DynamicResource Brush.Popup}"/>
</Style>
<Style Selector="FlyoutPresenter"> <Style Selector="FlyoutPresenter">
<Setter Property="MaxWidth" Value="1024"/> <Setter Property="MaxWidth" Value="1024"/>
<Setter Property="MaxHeight" Value="768"/> <Setter Property="MaxHeight" Value="768"/>
@ -767,6 +772,7 @@
<Popup Name="PART_Popup" <Popup Name="PART_Popup"
WindowManagerAddShadowHint="False" WindowManagerAddShadowHint="False"
Placement="RightEdgeAlignedTop" Placement="RightEdgeAlignedTop"
MaxHeight="400"
IsLightDismissEnabled="False" IsLightDismissEnabled="False"
HorizontalOffset="-4" HorizontalOffset="-4"
VerticalOffset="-4" VerticalOffset="-4"
@ -788,7 +794,13 @@
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}" Margin="{DynamicResource MenuFlyoutScrollerMargin}"
Grid.IsSharedSizeScope="True" /> Grid.IsSharedSizeScope="True">
<ItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsPresenter.ItemsPanel>
</ItemsPresenter>
</ScrollViewer> </ScrollViewer>
</Border> </Border>
</Grid> </Grid>
@ -1211,7 +1223,8 @@
Classes="tree_expander" Classes="tree_expander"
Focusable="False" Focusable="False"
HorizontalAlignment="Center" HorizontalAlignment="Center"
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" /> IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}"
IsHitTestVisible="False" />
</Panel> </Panel>
<ContentPresenter Name="PART_HeaderPresenter" <ContentPresenter Name="PART_HeaderPresenter"
Grid.Column="1" Grid.Column="1"

View file

@ -13,8 +13,11 @@
<Color x:Key="Color.Contents">#FFFAFAFA</Color> <Color x:Key="Color.Contents">#FFFAFAFA</Color>
<Color x:Key="Color.Badge">#FFB0CEE8</Color> <Color x:Key="Color.Badge">#FFB0CEE8</Color>
<Color x:Key="Color.BadgeFG">#FF1F1F1F</Color> <Color x:Key="Color.BadgeFG">#FF1F1F1F</Color>
<Color x:Key="Color.Decorator">#FF6F6F6F</Color> <Color x:Key="Color.DecoratorIconBG">#FF6F6F6F</Color>
<Color x:Key="Color.DecoratorIcon">#FFF8F8F8</Color> <Color x:Key="Color.DecoratorIcon">#FFF8F8F8</Color>
<Color x:Key="Color.DecoratorBranch">#FFFFB835</Color>
<Color x:Key="Color.DecoratorTag">#FF02C302</Color>
<Color x:Key="Color.DecoratorFG">Black</Color>
<Color x:Key="Color.Conflict">#FF836C2E</Color> <Color x:Key="Color.Conflict">#FF836C2E</Color>
<Color x:Key="Color.ConflictForeground">#FFFFFFFF</Color> <Color x:Key="Color.ConflictForeground">#FFFFFFFF</Color>
<Color x:Key="Color.Border0">#FFCFCFCF</Color> <Color x:Key="Color.Border0">#FFCFCFCF</Color>
@ -43,8 +46,11 @@
<Color x:Key="Color.Contents">#FF1B1B1B</Color> <Color x:Key="Color.Contents">#FF1B1B1B</Color>
<Color x:Key="Color.Badge">#FF8F8F8F</Color> <Color x:Key="Color.Badge">#FF8F8F8F</Color>
<Color x:Key="Color.BadgeFG">#FFDDDDDD</Color> <Color x:Key="Color.BadgeFG">#FFDDDDDD</Color>
<Color x:Key="Color.Decorator">#FF505050</Color> <Color x:Key="Color.DecoratorIconBG">#FF505050</Color>
<Color x:Key="Color.DecoratorIcon">#FFF8F8F8</Color> <Color x:Key="Color.DecoratorIcon">#FFF8F8F8</Color>
<Color x:Key="Color.DecoratorBranch">#FFFFB835</Color>
<Color x:Key="Color.DecoratorTag">#FF02C302</Color>
<Color x:Key="Color.DecoratorFG">Black</Color>
<Color x:Key="Color.Conflict">#FFFAFAD2</Color> <Color x:Key="Color.Conflict">#FFFAFAD2</Color>
<Color x:Key="Color.ConflictForeground">#FF252525</Color> <Color x:Key="Color.ConflictForeground">#FF252525</Color>
<Color x:Key="Color.Border0">#FF181818</Color> <Color x:Key="Color.Border0">#FF181818</Color>
@ -73,8 +79,11 @@
<SolidColorBrush x:Key="Brush.Contents" Color="{DynamicResource Color.Contents}"/> <SolidColorBrush x:Key="Brush.Contents" Color="{DynamicResource Color.Contents}"/>
<SolidColorBrush x:Key="Brush.Badge" Color="{DynamicResource Color.Badge}"/> <SolidColorBrush x:Key="Brush.Badge" Color="{DynamicResource Color.Badge}"/>
<SolidColorBrush x:Key="Brush.BadgeFG" Color="{DynamicResource Color.BadgeFG}"/> <SolidColorBrush x:Key="Brush.BadgeFG" Color="{DynamicResource Color.BadgeFG}"/>
<SolidColorBrush x:Key="Brush.Decorator" Color="{DynamicResource Color.Decorator}"/> <SolidColorBrush x:Key="Brush.DecoratorIconBG" Color="{DynamicResource Color.DecoratorIconBG}"/>
<SolidColorBrush x:Key="Brush.DecoratorIcon" Color="{DynamicResource Color.DecoratorIcon}"/> <SolidColorBrush x:Key="Brush.DecoratorIcon" Color="{DynamicResource Color.DecoratorIcon}"/>
<SolidColorBrush x:Key="Brush.DecoratorBranch" Color="{DynamicResource Color.DecoratorBranch}"/>
<SolidColorBrush x:Key="Brush.DecoratorTag" Color="{DynamicResource Color.DecoratorTag}"/>
<SolidColorBrush x:Key="Brush.DecoratorFG" Color="{DynamicResource Color.DecoratorFG}"/>
<SolidColorBrush x:Key="Brush.Conflict" Color="{DynamicResource Color.Conflict}"/> <SolidColorBrush x:Key="Brush.Conflict" Color="{DynamicResource Color.Conflict}"/>
<SolidColorBrush x:Key="Brush.ConflictForeground" Color="{DynamicResource Color.ConflictForeground}"/> <SolidColorBrush x:Key="Brush.ConflictForeground" Color="{DynamicResource Color.ConflictForeground}"/>
<SolidColorBrush x:Key="Brush.Border0" Color="{DynamicResource Color.Border0}"/> <SolidColorBrush x:Key="Brush.Border0" Color="{DynamicResource Color.Border0}"/>

View file

@ -100,7 +100,7 @@ namespace SourceGit.ViewModels
{ {
SetProgressDescription("Fetching from added remote ..."); SetProgressDescription("Fetching from added remote ...");
new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", _useSSH ? SSHKey : null);
new Commands.Fetch(_repo.FullPath, _name, true, SetProgressDescription).Exec(); new Commands.Fetch(_repo.FullPath, _name, true, false, SetProgressDescription).Exec();
} }
CallUIThread(() => CallUIThread(() =>
{ {

View file

@ -71,7 +71,7 @@ namespace SourceGit.ViewModels
if (branch.IsLocal) if (branch.IsLocal)
LocalBranches.Add(branch.Name); LocalBranches.Add(branch.Name);
else else
RemoteBranches.Add($"{branch.Remote}/{branch.Name}"); RemoteBranches.Add(branch.FriendlyName);
} }
if (RemoteBranches.Count > 0) if (RemoteBranches.Count > 0)

View file

@ -49,9 +49,8 @@ namespace SourceGit.ViewModels
public void NavigateToCommit(string commitSHA) public void NavigateToCommit(string commitSHA)
{ {
var repo = Preference.FindRepository(_repo); var repo = App.FindOpenedRepository(_repo);
if (repo != null) repo?.NavigateToCommit(commitSHA);
repo.NavigateToCommit(commitSHA);
} }
private readonly string _repo = string.Empty; private readonly string _repo = string.Empty;

View file

@ -110,9 +110,8 @@ namespace SourceGit.ViewModels
public void NavigateTo(string commitSHA) public void NavigateTo(string commitSHA)
{ {
var repo = Preference.FindRepository(_repo); var repo = App.FindOpenedRepository(_repo);
if (repo != null) repo?.NavigateToCommit(commitSHA);
repo.NavigateToCommit(commitSHA);
} }
public void ClearSearchFilter() public void ClearSearchFilter()

View file

@ -1,72 +1,28 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Avalonia; using Avalonia;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels namespace SourceGit.ViewModels
{ {
public enum BranchTreeNodeType
{
DetachedHead,
Remote,
Folder,
Branch,
}
public class BranchTreeNode : ObservableObject public class BranchTreeNode : ObservableObject
{ {
public const double DEFAULT_CORNER = 4.0; public string Name { get; private set; } = string.Empty;
public object Backend { get; private set; } = null;
public int Depth { get; set; } = 0;
public bool IsFiltered { get; set; } = false;
public bool IsSelected { get; set; } = false;
public List<BranchTreeNode> Children { get; private set; } = new List<BranchTreeNode>();
public string Name { get; set; } public bool IsExpanded
public BranchTreeNodeType Type { get; set; }
public object Backend { get; set; }
public bool IsExpanded { get; set; }
public bool IsFiltered { get; set; }
public List<BranchTreeNode> Children { get; set; } = new List<BranchTreeNode>();
public bool IsUpstreamTrackStatusVisible
{ {
get => IsBranch && !string.IsNullOrEmpty((Backend as Models.Branch).UpstreamTrackStatus); get => _isExpanded;
} set => SetProperty(ref _isExpanded, value);
public string UpstreamTrackStatus
{
get => Type == BranchTreeNodeType.Branch ? (Backend as Models.Branch).UpstreamTrackStatus : "";
}
public bool IsRemote
{
get => Type == BranchTreeNodeType.Remote;
}
public bool IsFolder
{
get => Type == BranchTreeNodeType.Folder;
}
public bool IsBranch
{
get => Type == BranchTreeNodeType.Branch;
}
public bool IsDetachedHead
{
get => Type == BranchTreeNodeType.DetachedHead;
}
public bool IsCurrent
{
get => IsBranch && (Backend as Models.Branch).IsCurrent;
}
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
} }
public CornerRadius CornerRadius public CornerRadius CornerRadius
@ -75,30 +31,33 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _cornerRadius, value); set => SetProperty(ref _cornerRadius, value);
} }
public void UpdateCornerRadius(ref BranchTreeNode prev) public bool IsBranch
{ {
if (_isSelected && prev != null && prev.IsSelected) get => Backend is Models.Branch;
{
var prevTop = prev.CornerRadius.TopLeft;
prev.CornerRadius = new CornerRadius(prevTop, 0);
CornerRadius = new CornerRadius(0, DEFAULT_CORNER);
}
else if (CornerRadius.TopLeft != DEFAULT_CORNER ||
CornerRadius.BottomLeft != DEFAULT_CORNER)
{
CornerRadius = new CornerRadius(DEFAULT_CORNER);
} }
prev = this; public bool IsUpstreamTrackStatusVisible
if (!IsBranch && IsExpanded)
{ {
foreach (var child in Children) get => Backend is Models.Branch { IsLocal: true } branch && !string.IsNullOrEmpty(branch.UpstreamTrackStatus);
child.UpdateCornerRadius(ref prev);
} }
public string UpstreamTrackStatus
{
get => Backend is Models.Branch branch ? branch.UpstreamTrackStatus : "";
} }
private bool _isSelected = false;
private CornerRadius _cornerRadius = new CornerRadius(DEFAULT_CORNER); public FontWeight NameFontWeight
{
get => Backend is Models.Branch { IsCurrent: true } ? FontWeight.Bold : FontWeight.Regular;
}
public string Tooltip
{
get => Backend is Models.Branch b ? b.FriendlyName : null;
}
private bool _isExpanded = false;
private CornerRadius _cornerRadius = new CornerRadius(4);
public class Builder public class Builder
{ {
@ -115,7 +74,6 @@ namespace SourceGit.ViewModels
var node = new BranchTreeNode() var node = new BranchTreeNode()
{ {
Name = remote.Name, Name = remote.Name,
Type = BranchTreeNodeType.Remote,
Backend = remote, Backend = remote,
IsExpanded = bForceExpanded || _expanded.Contains(path), IsExpanded = bForceExpanded || _expanded.Contains(path),
}; };
@ -158,9 +116,13 @@ namespace SourceGit.ViewModels
{ {
foreach (var node in nodes) foreach (var node in nodes)
{ {
if (node.Backend is Models.Branch)
continue;
var path = prefix + "/" + node.Name; var path = prefix + "/" + node.Name;
if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded) if (node.IsExpanded)
_expanded.Add(path); _expanded.Add(path);
CollectExpandedNodes(node.Children, path); CollectExpandedNodes(node.Children, path);
} }
} }
@ -173,7 +135,6 @@ namespace SourceGit.ViewModels
roots.Add(new BranchTreeNode() roots.Add(new BranchTreeNode()
{ {
Name = branch.Name, Name = branch.Name,
Type = BranchTreeNodeType.Branch,
Backend = branch, Backend = branch,
IsExpanded = false, IsExpanded = false,
IsFiltered = isFiltered, IsFiltered = isFiltered,
@ -197,7 +158,6 @@ namespace SourceGit.ViewModels
lastFolder = new BranchTreeNode() lastFolder = new BranchTreeNode()
{ {
Name = name, Name = name,
Type = BranchTreeNodeType.Folder,
IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder), IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder),
}; };
roots.Add(lastFolder); roots.Add(lastFolder);
@ -208,7 +168,6 @@ namespace SourceGit.ViewModels
var cur = new BranchTreeNode() var cur = new BranchTreeNode()
{ {
Name = name, Name = name,
Type = BranchTreeNodeType.Folder,
IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder), IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder),
}; };
lastFolder.Children.Add(cur); lastFolder.Children.Add(cur);
@ -220,10 +179,9 @@ namespace SourceGit.ViewModels
sepIdx = branch.Name.IndexOf('/', start); sepIdx = branch.Name.IndexOf('/', start);
} }
lastFolder.Children.Add(new BranchTreeNode() lastFolder?.Children.Add(new BranchTreeNode()
{ {
Name = Path.GetFileName(branch.Name), Name = Path.GetFileName(branch.Name),
Type = branch.IsHead ? BranchTreeNodeType.DetachedHead : BranchTreeNodeType.Branch,
Backend = branch, Backend = branch,
IsExpanded = false, IsExpanded = false,
IsFiltered = isFiltered, IsFiltered = isFiltered,
@ -234,16 +192,13 @@ namespace SourceGit.ViewModels
{ {
nodes.Sort((l, r) => nodes.Sort((l, r) =>
{ {
if (l.Type == BranchTreeNodeType.DetachedHead) if (l.Backend is Models.Branch { IsHead: true })
{
return -1; return -1;
}
if (l.Type == r.Type)
{
return l.Name.CompareTo(r.Name);
}
return (int)l.Type - (int)r.Type; if (l.Backend is Models.Branch)
return r.Backend is Models.Branch ? string.Compare(l.Name, r.Name, StringComparison.Ordinal) : 1;
return r.Backend is Models.Branch ? -1 : string.Compare(l.Name, r.Name, StringComparison.Ordinal);
}); });
foreach (var node in nodes) foreach (var node in nodes)

View file

@ -12,8 +12,8 @@ namespace SourceGit.ViewModels
public Models.DealWithLocalChanges PreAction public Models.DealWithLocalChanges PreAction
{ {
get => _preAction; get => _repo.Settings.DealWithLocalChangesOnCheckoutBranch;
set => SetProperty(ref _preAction, value); set => _repo.Settings.DealWithLocalChangesOnCheckoutBranch = value;
} }
public Checkout(Repository repo, string branch) public Checkout(Repository repo, string branch)
@ -34,7 +34,7 @@ namespace SourceGit.ViewModels
var needPopStash = false; var needPopStash = false;
if (hasLocalChanges) if (hasLocalChanges)
{ {
if (_preAction == Models.DealWithLocalChanges.StashAndReaply) if (PreAction == Models.DealWithLocalChanges.StashAndReaply)
{ {
SetProgressDescription("Adding untracked changes ..."); SetProgressDescription("Adding untracked changes ...");
var succ = new Commands.Add(_repo.FullPath).Exec(); var succ = new Commands.Add(_repo.FullPath).Exec();
@ -52,7 +52,7 @@ namespace SourceGit.ViewModels
needPopStash = true; needPopStash = true;
} }
else if (_preAction == Models.DealWithLocalChanges.Discard) else if (PreAction == Models.DealWithLocalChanges.Discard)
{ {
SetProgressDescription("Discard local changes ..."); SetProgressDescription("Discard local changes ...");
Commands.Discard.All(_repo.FullPath); Commands.Discard.All(_repo.FullPath);
@ -78,6 +78,5 @@ namespace SourceGit.ViewModels
} }
private readonly Repository _repo = null; private readonly Repository _repo = null;
private Models.DealWithLocalChanges _preAction = Models.DealWithLocalChanges.DoNothing;
} }
} }

View file

@ -128,8 +128,8 @@ namespace SourceGit.ViewModels
CallUIThread(() => CallUIThread(() =>
{ {
var repo = Preference.AddRepository(path, Path.Combine(path, ".git")); var normalizedPath = path.Replace("\\", "/");
var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null, true); var node = Preference.FindOrAddNodeByRepositoryPath(normalizedPath, null, true);
_launcher.OpenRepositoryInTab(node, _page); _launcher.OpenRepositoryInTab(node, _page);
}); });

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
@ -12,7 +13,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels namespace SourceGit.ViewModels
{ {
public class CommitDetail : ObservableObject public partial class CommitDetail : ObservableObject
{ {
public DiffContext DiffContext public DiffContext DiffContext
{ {
@ -110,9 +111,8 @@ namespace SourceGit.ViewModels
public void NavigateTo(string commitSHA) public void NavigateTo(string commitSHA)
{ {
var repo = Preference.FindRepository(_repo); var repo = App.FindOpenedRepository(_repo);
if (repo != null) repo?.NavigateToCommit(commitSHA);
repo.NavigateToCommit(commitSHA);
} }
public void ClearSearchChangeFilter() public void ClearSearchChangeFilter()
@ -165,31 +165,17 @@ namespace SourceGit.ViewModels
var contentStream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path); var contentStream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path);
var content = new StreamReader(contentStream).ReadToEnd(); var content = new StreamReader(contentStream).ReadToEnd();
if (content.StartsWith("version https://git-lfs.github.com/spec/", StringComparison.Ordinal)) var matchLFS = REG_LFS_FORMAT().Match(content);
if (matchLFS.Success)
{ {
var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() }; var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries); obj.Object.Oid = matchLFS.Groups[1].Value;
if (lines.Length == 3) obj.Object.Size = long.Parse(matchLFS.Groups[2].Value);
{
foreach (var line in lines)
{
if (line.StartsWith("oid sha256:", StringComparison.Ordinal))
{
obj.Object.Oid = line.Substring(11);
}
else if (line.StartsWith("size ", StringComparison.Ordinal))
{
obj.Object.Size = long.Parse(line.Substring(5));
}
}
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = obj;
});
return;
}
}
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = obj);
}
else
{
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
ViewRevisionFileContent = new Models.RevisionTextFile() ViewRevisionFileContent = new Models.RevisionTextFile()
@ -198,6 +184,7 @@ namespace SourceGit.ViewModels
Content = content Content = content
}; };
}); });
}
}); });
break; break;
case Models.ObjectType.Commit: case Models.ObjectType.Commit:
@ -332,6 +319,7 @@ namespace SourceGit.ViewModels
var blame = new MenuItem(); var blame = new MenuItem();
blame.Header = App.Text("Blame"); blame.Header = App.Text("Blame");
blame.Icon = App.CreateMenuIcon("Icons.Blame"); blame.Icon = App.CreateMenuIcon("Icons.Blame");
blame.IsEnabled = file.Type == Models.ObjectType.Blob;
blame.Click += (o, ev) => blame.Click += (o, ev) =>
{ {
var window = new Views.Blame() { DataContext = new Blame(_repo, file.Path, _commit.SHA) }; var window = new Views.Blame() { DataContext = new Blame(_repo, file.Path, _commit.SHA) };
@ -465,6 +453,9 @@ namespace SourceGit.ViewModels
} }
} }
[GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")]
private static partial Regex REG_LFS_FORMAT();
private static readonly HashSet<string> IMG_EXTS = new HashSet<string>() private static readonly HashSet<string> IMG_EXTS = new HashSet<string>()
{ {
".ico", ".bmp", ".jpg", ".png", ".jpeg" ".ico", ".bmp", ".jpg", ".png", ".jpeg"

View file

@ -22,15 +22,15 @@ namespace SourceGit.ViewModels
public Models.DealWithLocalChanges PreAction public Models.DealWithLocalChanges PreAction
{ {
get => _preAction; get => _repo.Settings.DealWithLocalChangesOnCreateBranch;
set => SetProperty(ref _preAction, value); set => _repo.Settings.DealWithLocalChangesOnCreateBranch = value;
} }
public bool CheckoutAfterCreated public bool CheckoutAfterCreated
{ {
get; get => _repo.Settings.CheckoutBranchOnCreateBranch;
set; set => _repo.Settings.CheckoutBranchOnCreateBranch = value;
} = true; }
public CreateBranch(Repository repo, Models.Branch branch) public CreateBranch(Repository repo, Models.Branch branch)
{ {
@ -72,8 +72,7 @@ namespace SourceGit.ViewModels
foreach (var b in creator._repo.Branches) foreach (var b in creator._repo.Branches)
{ {
var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}"; if (b.FriendlyName == name)
if (test == name)
return new ValidationResult("A branch with same name already exists!"); return new ValidationResult("A branch with same name already exists!");
} }
@ -90,7 +89,7 @@ namespace SourceGit.ViewModels
bool needPopStash = false; bool needPopStash = false;
if (_repo.WorkingCopyChangesCount > 0) if (_repo.WorkingCopyChangesCount > 0)
{ {
if (_preAction == Models.DealWithLocalChanges.StashAndReaply) if (PreAction == Models.DealWithLocalChanges.StashAndReaply)
{ {
SetProgressDescription("Adding untracked changes..."); SetProgressDescription("Adding untracked changes...");
var succ = new Commands.Add(_repo.FullPath).Exec(); var succ = new Commands.Add(_repo.FullPath).Exec();
@ -108,7 +107,7 @@ namespace SourceGit.ViewModels
needPopStash = true; needPopStash = true;
} }
else if (_preAction == Models.DealWithLocalChanges.Discard) else if (PreAction == Models.DealWithLocalChanges.Discard)
{ {
SetProgressDescription("Discard local changes..."); SetProgressDescription("Discard local changes...");
Commands.Discard.All(_repo.FullPath); Commands.Discard.All(_repo.FullPath);
@ -138,6 +137,5 @@ namespace SourceGit.ViewModels
private readonly Repository _repo = null; private readonly Repository _repo = null;
private string _name = null; private string _name = null;
private readonly string _baseOnRevision = null; private readonly string _baseOnRevision = null;
private Models.DealWithLocalChanges _preAction = Models.DealWithLocalChanges.DoNothing;
} }
} }

View file

@ -35,9 +35,9 @@ namespace SourceGit.ViewModels
if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream)) if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream))
{ {
var upstream = branch.Upstream.Substring(13); TrackingRemoteBranch = repo.Branches.Find(x => x.FullName == branch.Upstream);
TrackingRemoteBranch = repo.Branches.Find(x => !x.IsLocal && $"{x.Remote}/{x.Name}" == upstream); if (TrackingRemoteBranch != null)
DeleteTrackingRemoteTip = new Views.NameHighlightedTextBlock("DeleteBranch.WithTrackingRemote", upstream); DeleteTrackingRemoteTip = new Views.NameHighlightedTextBlock("DeleteBranch.WithTrackingRemote", TrackingRemoteBranch.FriendlyName);
} }
View = new Views.DeleteBranch() { DataContext = this }; View = new Views.DeleteBranch() { DataContext = this };

View file

@ -37,7 +37,7 @@ namespace SourceGit.ViewModels
{ {
foreach (var target in Targets) foreach (var target in Targets)
{ {
SetProgressDescription($"Deleting remote branch : {target.Remote}/{target.Name}"); SetProgressDescription($"Deleting remote branch : {target.FriendlyName}");
Commands.Branch.DeleteRemote(_repo.FullPath, target.Remote, target.Name); Commands.Branch.DeleteRemote(_repo.FullPath, target.Remote, target.Name);
} }
} }

View file

@ -111,27 +111,34 @@ namespace SourceGit.ViewModels
if (latest.TextDiff != null) if (latest.TextDiff != null)
{ {
var repo = Preference.FindRepository(_repo); var count = latest.TextDiff.Lines.Count;
if (repo != null && repo.Submodules.Contains(_option.Path)) var isSubmodule = false;
if (count <= 3)
{ {
var submoduleDiff = new Models.SubmoduleDiff(); var submoduleDiff = new Models.SubmoduleDiff();
var submoduleRoot = $"{_repo}/{_option.Path}".Replace("\\", "/"); var submoduleRoot = $"{_repo}/{_option.Path}".Replace("\\", "/");
foreach (var line in latest.TextDiff.Lines) isSubmodule = true;
for (int i = 1; i < count; i++)
{ {
if (line.Type == Models.TextDiffLineType.Added) var line = latest.TextDiff.Lines[i];
if (!line.Content.StartsWith("Subproject commit ", StringComparison.Ordinal))
{ {
var sha = line.Content.Substring("Subproject commit ".Length); isSubmodule = false;
submoduleDiff.New = QuerySubmoduleRevision(submoduleRoot, sha); break;
} }
var sha = line.Content.Substring(18);
if (line.Type == Models.TextDiffLineType.Added)
submoduleDiff.New = QuerySubmoduleRevision(submoduleRoot, sha);
else if (line.Type == Models.TextDiffLineType.Deleted) else if (line.Type == Models.TextDiffLineType.Deleted)
{
var sha = line.Content.Substring("Subproject commit ".Length);
submoduleDiff.Old = QuerySubmoduleRevision(submoduleRoot, sha); submoduleDiff.Old = QuerySubmoduleRevision(submoduleRoot, sha);
} }
}
if (isSubmodule)
rs = submoduleDiff; rs = submoduleDiff;
} }
else
if (!isSubmodule)
{ {
latest.TextDiff.File = _option.Path; latest.TextDiff.File = _option.Path;
rs = latest.TextDiff; rs = latest.TextDiff;

View file

@ -26,6 +26,12 @@ namespace SourceGit.ViewModels
{ {
get; get;
set; set;
} = true;
public bool NoTags
{
get => _repo.Settings.FetchWithoutTags;
set => _repo.Settings.FetchWithoutTags = value;
} }
public Fetch(Repository repo, Models.Remote preferedRemote = null) public Fetch(Repository repo, Models.Remote preferedRemote = null)
@ -33,13 +39,13 @@ namespace SourceGit.ViewModels
_repo = repo; _repo = repo;
_fetchAllRemotes = preferedRemote == null; _fetchAllRemotes = preferedRemote == null;
SelectedRemote = preferedRemote != null ? preferedRemote : _repo.Remotes[0]; SelectedRemote = preferedRemote != null ? preferedRemote : _repo.Remotes[0];
Prune = true;
View = new Views.Fetch() { DataContext = this }; View = new Views.Fetch() { DataContext = this };
} }
public override Task<bool> Sure() public override Task<bool> Sure()
{ {
_repo.SetWatcherEnabled(false); _repo.SetWatcherEnabled(false);
return Task.Run(() => return Task.Run(() =>
{ {
if (FetchAllRemotes) if (FetchAllRemotes)
@ -47,13 +53,13 @@ namespace SourceGit.ViewModels
foreach (var remote in _repo.Remotes) foreach (var remote in _repo.Remotes)
{ {
SetProgressDescription($"Fetching remote: {remote.Name}"); SetProgressDescription($"Fetching remote: {remote.Name}");
new Commands.Fetch(_repo.FullPath, remote.Name, Prune, SetProgressDescription).Exec(); new Commands.Fetch(_repo.FullPath, remote.Name, Prune, NoTags, SetProgressDescription).Exec();
} }
} }
else else
{ {
SetProgressDescription($"Fetching remote: {SelectedRemote.Name}"); SetProgressDescription($"Fetching remote: {SelectedRemote.Name}");
new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, Prune, SetProgressDescription).Exec(); new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, Prune, NoTags, SetProgressDescription).Exec();
} }
CallUIThread(() => _repo.SetWatcherEnabled(true)); CallUIThread(() => _repo.SetWatcherEnabled(true));

View file

@ -39,8 +39,7 @@ namespace SourceGit.ViewModels
var check = $"{starter._prefix}{name}"; var check = $"{starter._prefix}{name}";
foreach (var b in starter._repo.Branches) foreach (var b in starter._repo.Branches)
{ {
var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}"; if (b.FriendlyName == check)
if (test == check)
return new ValidationResult("A branch with same name already exists!"); return new ValidationResult("A branch with same name already exists!");
} }
} }

View file

@ -175,7 +175,7 @@ namespace SourceGit.ViewModels
} }
else if (d.Type == Models.DecoratorType.RemoteBranchHead) else if (d.Type == Models.DecoratorType.RemoteBranchHead)
{ {
var b = _repo.Branches.Find(x => !x.IsLocal && d.Name == $"{x.Remote}/{x.Name}"); var b = _repo.Branches.Find(x => !x.IsLocal && d.Name == x.FriendlyName);
FillRemoteBranchMenu(menu, b, current, commit.IsMerged); FillRemoteBranchMenu(menu, b, current, commit.IsMerged);
} }
else if (d.Type == Models.DecoratorType.Tag) else if (d.Type == Models.DecoratorType.Tag)
@ -583,7 +583,7 @@ namespace SourceGit.ViewModels
private void FillRemoteBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) private void FillRemoteBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged)
{ {
var name = $"{branch.Remote}/{branch.Name}"; var name = branch.FriendlyName;
var submenu = new MenuItem(); var submenu = new MenuItem();
submenu.Icon = App.CreateMenuIcon("Icons.Branch"); submenu.Icon = App.CreateMenuIcon("Icons.Branch");

View file

@ -1,5 +1,4 @@
using System.IO; using System.Threading.Tasks;
using System.Threading.Tasks;
namespace SourceGit.ViewModels namespace SourceGit.ViewModels
{ {
@ -29,11 +28,10 @@ namespace SourceGit.ViewModels
if (!succ) if (!succ)
return false; return false;
var gitDir = Path.GetFullPath(Path.Combine(_targetPath, ".git"));
CallUIThread(() => CallUIThread(() =>
{ {
var repo = Preference.AddRepository(_targetPath, gitDir); var normalizedPath = _targetPath.Replace("\\", "/");
Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, _parentNode, true); Preference.FindOrAddNodeByRepositoryPath(normalizedPath, _parentNode, true);
}); });
return true; return true;

View file

@ -48,9 +48,8 @@ namespace SourceGit.ViewModels
return; return;
} }
var gitDir = new Commands.QueryGitDir(root).Result(); var normalized = root.Replace("\\", "/");
var repo = Preference.AddRepository(root, gitDir); var node = Preference.FindOrAddNodeByRepositoryPath(normalized, null, false);
var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null, false);
OpenRepositoryInTab(node, null); OpenRepositoryInTab(node, null);
} }
else if (Preference.Instance.RestoreTabs) else if (Preference.Instance.RestoreTabs)
@ -59,7 +58,15 @@ namespace SourceGit.ViewModels
{ {
var node = Preference.FindNode(id); var node = Preference.FindNode(id);
if (node == null) if (node == null)
continue; {
node = new RepositoryNode()
{
Id = id,
Name = Path.GetFileName(id),
Bookmark = 0,
IsRepository = true,
};
}
OpenRepositoryInTab(node, null); OpenRepositoryInTab(node, null);
} }
@ -72,19 +79,26 @@ namespace SourceGit.ViewModels
public void Quit() public void Quit()
{ {
Preference.Instance.OpenedTabs.Clear(); var pref = Preference.Instance;
pref.OpenedTabs.Clear();
if (Preference.Instance.RestoreTabs) if (pref.RestoreTabs)
{ {
foreach (var page in Pages) foreach (var page in Pages)
{ {
if (page.Node.IsRepository) if (page.Node.IsRepository)
Preference.Instance.OpenedTabs.Add(page.Node.Id); pref.OpenedTabs.Add(page.Node.Id);
} }
} }
Preference.Instance.LastActiveTabIdx = Pages.IndexOf(ActivePage); pref.LastActiveTabIdx = Pages.IndexOf(ActivePage);
Preference.Save(); pref.Save();
foreach (var page in Pages)
{
if (page.Data is Repository repo)
repo.Close();
}
} }
public void AddNewTab() public void AddNewTab()
@ -211,14 +225,27 @@ namespace SourceGit.ViewModels
} }
} }
var repo = Preference.FindRepository(node.Id); if (!Path.Exists(node.Id))
if (repo == null || !Path.Exists(repo.FullPath))
{ {
var ctx = page == null ? ActivePage.Node.Id : page.Node.Id; var ctx = page == null ? ActivePage.Node.Id : page.Node.Id;
App.RaiseException(ctx, "Repository does NOT exists any more. Please remove it."); App.RaiseException(ctx, "Repository does NOT exists any more. Please remove it.");
return; return;
} }
var gitDir = new Commands.QueryGitDir(node.Id).Result();
if (string.IsNullOrEmpty(gitDir))
{
var ctx = page == null ? ActivePage.Node.Id : page.Node.Id;
App.RaiseException(ctx, "Given path is not a valid git repository!");
return;
}
var repo = new Repository()
{
FullPath = node.Id,
GitDir = gitDir,
};
repo.Open(); repo.Open();
Commands.AutoFetch.AddRepository(repo.FullPath); Commands.AutoFetch.AddRepository(repo.FullPath);

View file

@ -37,23 +37,14 @@ namespace SourceGit.ViewModels
} }
} }
// It will cause some issue on Linux. See https://github.com/sourcegit-scm/sourcegit/issues/99
// _instance.Repositories.RemoveAll(x => !Directory.Exists(x.FullPath));
if (_instance.DefaultFont == null) if (_instance.DefaultFont == null)
{
_instance.DefaultFont = FontManager.Current.DefaultFontFamily; _instance.DefaultFont = FontManager.Current.DefaultFontFamily;
}
if (_instance.MonospaceFont == null) if (_instance.MonospaceFont == null)
{
_instance.MonospaceFont = new FontFamily("fonts:SourceGit#JetBrains Mono"); _instance.MonospaceFont = new FontFamily("fonts:SourceGit#JetBrains Mono");
}
if (!_instance.IsGitConfigured) if (!_instance.IsGitConfigured)
{
_instance.GitInstallPath = Native.OS.FindGitExecutable(); _instance.GitInstallPath = Native.OS.FindGitExecutable();
}
return _instance; return _instance;
} }
@ -274,9 +265,8 @@ namespace SourceGit.ViewModels
set set
{ {
if (value is null or < 1) if (value is null or < 1)
{
return; return;
}
if (Commands.AutoFetch.Interval != value) if (Commands.AutoFetch.Interval != value)
{ {
Commands.AutoFetch.Interval = (int)value; Commands.AutoFetch.Interval = (int)value;
@ -308,12 +298,6 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _externalMergeToolPath, value); set => SetProperty(ref _externalMergeToolPath, value);
} }
public List<Repository> Repositories
{
get;
set;
} = new List<Repository>();
public AvaloniaList<RepositoryNode> RepositoryNodes public AvaloniaList<RepositoryNode> RepositoryNodes
{ {
get => _repositoryNodes; get => _repositoryNodes;
@ -366,21 +350,15 @@ namespace SourceGit.ViewModels
list.Sort((l, r) => list.Sort((l, r) =>
{ {
if (l.IsRepository != r.IsRepository) if (l.IsRepository != r.IsRepository)
{
return l.IsRepository ? 1 : -1; return l.IsRepository ? 1 : -1;
}
else else
{
return l.Name.CompareTo(r.Name); return l.Name.CompareTo(r.Name);
}
}); });
collection.Clear(); collection.Clear();
foreach (var one in list) foreach (var one in list)
{
collection.Add(one); collection.Add(one);
} }
}
public static RepositoryNode FindNode(string id) public static RepositoryNode FindNode(string id)
{ {
@ -451,39 +429,9 @@ namespace SourceGit.ViewModels
container.Add(one); container.Add(one);
} }
public static Repository FindRepository(string path) public void Save()
{ {
foreach (var repo in _instance.Repositories) var data = JsonSerializer.Serialize(this, JsonCodeGen.Default.Preference);
{
if (repo.FullPath == path)
return repo;
}
return null;
}
public static Repository AddRepository(string rootDir, string gitDir)
{
var normalized = rootDir.Replace('\\', '/');
var repo = FindRepository(normalized);
if (repo != null)
{
repo.GitDir = gitDir;
return repo;
}
repo = new Repository()
{
FullPath = normalized,
GitDir = gitDir
};
_instance.Repositories.Add(repo);
return repo;
}
public static void Save()
{
var data = JsonSerializer.Serialize(_instance, JsonCodeGen.Default.Preference);
File.WriteAllText(_savePath, data); File.WriteAllText(_savePath, data);
} }

View file

@ -49,14 +49,20 @@ namespace SourceGit.ViewModels
public Models.DealWithLocalChanges PreAction public Models.DealWithLocalChanges PreAction
{ {
get => _preAction; get => _repo.Settings.DealWithLocalChangesOnPull;
set => SetProperty(ref _preAction, value); set => _repo.Settings.DealWithLocalChangesOnPull = value;
} }
public bool UseRebase public bool UseRebase
{ {
get => _repo.PreferRebaseInsteadOfMerge; get => _repo.Settings.PreferRebaseInsteadOfMerge;
set => _repo.PreferRebaseInsteadOfMerge = value; set => _repo.Settings.PreferRebaseInsteadOfMerge = value;
}
public bool NoTags
{
get => _repo.Settings.FetchWithoutTagsOnPull;
set => _repo.Settings.FetchWithoutTagsOnPull = value;
} }
public Pull(Repository repo, Models.Branch specifiedRemoteBranch) public Pull(Repository repo, Models.Branch specifiedRemoteBranch)
@ -114,12 +120,13 @@ namespace SourceGit.ViewModels
public override Task<bool> Sure() public override Task<bool> Sure()
{ {
_repo.SetWatcherEnabled(false); _repo.SetWatcherEnabled(false);
return Task.Run(() => return Task.Run(() =>
{ {
var needPopStash = false; var needPopStash = false;
if (_repo.WorkingCopyChangesCount > 0) if (_repo.WorkingCopyChangesCount > 0)
{ {
if (_preAction == Models.DealWithLocalChanges.StashAndReaply) if (PreAction == Models.DealWithLocalChanges.StashAndReaply)
{ {
SetProgressDescription("Adding untracked changes..."); SetProgressDescription("Adding untracked changes...");
var succ = new Commands.Add(_repo.FullPath).Exec(); var succ = new Commands.Add(_repo.FullPath).Exec();
@ -137,7 +144,7 @@ namespace SourceGit.ViewModels
needPopStash = true; needPopStash = true;
} }
else if (_preAction == Models.DealWithLocalChanges.Discard) else if (PreAction == Models.DealWithLocalChanges.Discard)
{ {
SetProgressDescription("Discard local changes ..."); SetProgressDescription("Discard local changes ...");
Commands.Discard.All(_repo.FullPath); Commands.Discard.All(_repo.FullPath);
@ -145,7 +152,7 @@ namespace SourceGit.ViewModels
} }
SetProgressDescription($"Pull {_selectedRemote.Name}/{_selectedBranch.Name}..."); SetProgressDescription($"Pull {_selectedRemote.Name}/{_selectedBranch.Name}...");
var rs = new Commands.Pull(_repo.FullPath, _selectedRemote.Name, _selectedBranch.Name, UseRebase, SetProgressDescription).Exec(); var rs = new Commands.Pull(_repo.FullPath, _selectedRemote.Name, _selectedBranch.Name, UseRebase, NoTags, SetProgressDescription).Exec();
if (rs && needPopStash) if (rs && needPopStash)
{ {
SetProgressDescription("Re-apply local changes..."); SetProgressDescription("Re-apply local changes...");
@ -162,6 +169,5 @@ namespace SourceGit.ViewModels
private Models.Remote _selectedRemote = null; private Models.Remote _selectedRemote = null;
private List<Models.Branch> _remoteBranches = null; private List<Models.Branch> _remoteBranches = null;
private Models.Branch _selectedBranch = null; private Models.Branch _selectedBranch = null;
private Models.DealWithLocalChanges _preAction = Models.DealWithLocalChanges.DoNothing;
} }
} }

View file

@ -64,8 +64,8 @@ namespace SourceGit.ViewModels
public bool PushAllTags public bool PushAllTags
{ {
get; get => _repo.Settings.PushAllTags;
set; set => _repo.Settings.PushAllTags = value;
} }
public bool IsSetTrackOptionVisible public bool IsSetTrackOptionVisible

View file

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json.Serialization; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Collections; using Avalonia.Collections;
@ -14,6 +14,93 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels namespace SourceGit.ViewModels
{ {
public class RepositorySettings
{
public Models.DealWithLocalChanges DealWithLocalChangesOnCheckoutBranch
{
get;
set;
} = Models.DealWithLocalChanges.DoNothing;
public bool FetchWithoutTags
{
get;
set;
} = false;
public Models.DealWithLocalChanges DealWithLocalChangesOnPull
{
get;
set;
} = Models.DealWithLocalChanges.DoNothing;
public bool PreferRebaseInsteadOfMerge
{
get;
set;
} = true;
public bool FetchWithoutTagsOnPull
{
get;
set;
} = false;
public bool PushAllTags
{
get;
set;
} = false;
public Models.DealWithLocalChanges DealWithLocalChangesOnCreateBranch
{
get;
set;
} = Models.DealWithLocalChanges.DoNothing;
public bool CheckoutBranchOnCreateBranch
{
get;
set;
} = true;
public bool AutoStageBeforeCommit
{
get;
set;
} = false;
public AvaloniaList<string> Filters
{
get;
set;
} = new AvaloniaList<string>();
public AvaloniaList<string> CommitMessages
{
get;
set;
} = new AvaloniaList<string>();
public void PushCommitMessage(string message)
{
var existIdx = CommitMessages.IndexOf(message);
if (existIdx == 0)
return;
if (existIdx > 0)
{
CommitMessages.Move(existIdx, 0);
return;
}
if (CommitMessages.Count > 9)
CommitMessages.RemoveRange(9, CommitMessages.Count - 9);
CommitMessages.Insert(0, message);
}
}
public class Repository : ObservableObject, Models.IRepository public class Repository : ObservableObject, Models.IRepository
{ {
public string FullPath public string FullPath
@ -39,25 +126,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _gitDir, value); set => SetProperty(ref _gitDir, value);
} }
public bool PreferRebaseInsteadOfMerge public RepositorySettings Settings
{ {
get; get => _settings;
set; private set => SetProperty(ref _settings, value);
} = true; }
public AvaloniaList<string> Filters
{
get;
set;
} = new AvaloniaList<string>();
public AvaloniaList<string> CommitMessages
{
get;
set;
} = new AvaloniaList<string>();
[JsonIgnore]
public int SelectedViewIndex public int SelectedViewIndex
{ {
get => _selectedViewIndex; get => _selectedViewIndex;
@ -81,14 +155,12 @@ namespace SourceGit.ViewModels
} }
} }
[JsonIgnore]
public object SelectedView public object SelectedView
{ {
get => _selectedView; get => _selectedView;
set => SetProperty(ref _selectedView, value); set => SetProperty(ref _selectedView, value);
} }
[JsonIgnore]
public string SearchBranchFilter public string SearchBranchFilter
{ {
get => _searchBranchFilter; get => _searchBranchFilter;
@ -104,75 +176,64 @@ namespace SourceGit.ViewModels
} }
} }
[JsonIgnore]
public List<Models.Remote> Remotes public List<Models.Remote> Remotes
{ {
get => _remotes; get => _remotes;
private set => SetProperty(ref _remotes, value); private set => SetProperty(ref _remotes, value);
} }
[JsonIgnore]
public List<Models.Branch> Branches public List<Models.Branch> Branches
{ {
get => _branches; get => _branches;
private set => SetProperty(ref _branches, value); private set => SetProperty(ref _branches, value);
} }
[JsonIgnore]
public List<BranchTreeNode> LocalBranchTrees public List<BranchTreeNode> LocalBranchTrees
{ {
get => _localBranchTrees; get => _localBranchTrees;
private set => SetProperty(ref _localBranchTrees, value); private set => SetProperty(ref _localBranchTrees, value);
} }
[JsonIgnore]
public List<BranchTreeNode> RemoteBranchTrees public List<BranchTreeNode> RemoteBranchTrees
{ {
get => _remoteBranchTrees; get => _remoteBranchTrees;
private set => SetProperty(ref _remoteBranchTrees, value); private set => SetProperty(ref _remoteBranchTrees, value);
} }
[JsonIgnore]
public List<Models.Worktree> Worktrees public List<Models.Worktree> Worktrees
{ {
get => _worktrees; get => _worktrees;
private set => SetProperty(ref _worktrees, value); private set => SetProperty(ref _worktrees, value);
} }
[JsonIgnore]
public List<Models.Tag> Tags public List<Models.Tag> Tags
{ {
get => _tags; get => _tags;
private set => SetProperty(ref _tags, value); private set => SetProperty(ref _tags, value);
} }
[JsonIgnore]
public List<Models.Tag> VisibleTags public List<Models.Tag> VisibleTags
{ {
get => _visibleTags; get => _visibleTags;
private set => SetProperty(ref _visibleTags, value); private set => SetProperty(ref _visibleTags, value);
} }
[JsonIgnore]
public List<string> Submodules public List<string> Submodules
{ {
get => _submodules; get => _submodules;
private set => SetProperty(ref _submodules, value); private set => SetProperty(ref _submodules, value);
} }
[JsonIgnore]
public int WorkingCopyChangesCount public int WorkingCopyChangesCount
{ {
get => _workingCopy == null ? 0 : _workingCopy.Count; get => _workingCopy == null ? 0 : _workingCopy.Count;
} }
[JsonIgnore]
public int StashesCount public int StashesCount
{ {
get => _stashesPage == null ? 0 : _stashesPage.Stashes.Count; get => _stashesPage == null ? 0 : _stashesPage.Stashes.Count;
} }
[JsonIgnore]
public bool IncludeUntracked public bool IncludeUntracked
{ {
get => _includeUntracked; get => _includeUntracked;
@ -183,7 +244,6 @@ namespace SourceGit.ViewModels
} }
} }
[JsonIgnore]
public bool IsSearching public bool IsSearching
{ {
get => _isSearching; get => _isSearching;
@ -199,63 +259,66 @@ namespace SourceGit.ViewModels
} }
} }
[JsonIgnore]
public int SearchCommitFilterType public int SearchCommitFilterType
{ {
get => _searchCommitFilterType; get => _searchCommitFilterType;
set => SetProperty(ref _searchCommitFilterType, value); set => SetProperty(ref _searchCommitFilterType, value);
} }
[JsonIgnore]
public string SearchCommitFilter public string SearchCommitFilter
{ {
get => _searchCommitFilter; get => _searchCommitFilter;
set => SetProperty(ref _searchCommitFilter, value); set => SetProperty(ref _searchCommitFilter, value);
} }
[JsonIgnore]
public List<Models.Commit> SearchedCommits public List<Models.Commit> SearchedCommits
{ {
get => _searchedCommits; get => _searchedCommits;
set => SetProperty(ref _searchedCommits, value); set => SetProperty(ref _searchedCommits, value);
} }
[JsonIgnore] public bool IsLocalBranchGroupExpanded
{
get => _isLocalBranchGroupExpanded;
set => SetProperty(ref _isLocalBranchGroupExpanded, value);
}
public bool IsRemoteGroupExpanded
{
get => _isRemoteGroupExpanded;
set => SetProperty(ref _isRemoteGroupExpanded, value);
}
public bool IsTagGroupExpanded public bool IsTagGroupExpanded
{ {
get => _isTagGroupExpanded; get => _isTagGroupExpanded;
set => SetProperty(ref _isTagGroupExpanded, value); set => SetProperty(ref _isTagGroupExpanded, value);
} }
[JsonIgnore]
public bool IsSubmoduleGroupExpanded public bool IsSubmoduleGroupExpanded
{ {
get => _isSubmoduleGroupExpanded; get => _isSubmoduleGroupExpanded;
set => SetProperty(ref _isSubmoduleGroupExpanded, value); set => SetProperty(ref _isSubmoduleGroupExpanded, value);
} }
[JsonIgnore]
public bool IsWorktreeGroupExpanded public bool IsWorktreeGroupExpanded
{ {
get => _isWorktreeGroupExpanded; get => _isWorktreeGroupExpanded;
set => SetProperty(ref _isWorktreeGroupExpanded, value); set => SetProperty(ref _isWorktreeGroupExpanded, value);
} }
[JsonIgnore]
public InProgressContext InProgressContext public InProgressContext InProgressContext
{ {
get => _inProgressContext; get => _inProgressContext;
private set => SetProperty(ref _inProgressContext, value); private set => SetProperty(ref _inProgressContext, value);
} }
[JsonIgnore]
public bool HasUnsolvedConflicts public bool HasUnsolvedConflicts
{ {
get => _hasUnsolvedConflicts; get => _hasUnsolvedConflicts;
private set => SetProperty(ref _hasUnsolvedConflicts, value); private set => SetProperty(ref _hasUnsolvedConflicts, value);
} }
[JsonIgnore]
public Models.Commit SearchResultSelectedCommit public Models.Commit SearchResultSelectedCommit
{ {
get => _searchResultSelectedCommit; get => _searchResultSelectedCommit;
@ -264,6 +327,23 @@ namespace SourceGit.ViewModels
public void Open() public void Open()
{ {
var settingsFile = Path.Combine(_gitDir, "sourcegit.settings");
if (File.Exists(settingsFile))
{
try
{
_settings = JsonSerializer.Deserialize(File.ReadAllText(settingsFile), JsonCodeGen.Default.RepositorySettings);
}
catch
{
_settings = new RepositorySettings();
}
}
else
{
_settings = new RepositorySettings();
}
_watcher = new Models.Watcher(this); _watcher = new Models.Watcher(this);
_histories = new Histories(this); _histories = new Histories(this);
_workingCopy = new WorkingCopy(this); _workingCopy = new WorkingCopy(this);
@ -280,6 +360,10 @@ namespace SourceGit.ViewModels
{ {
SelectedView = 0.0; // Do NOT modify. Used to remove exists widgets for GC.Collect SelectedView = 0.0; // Do NOT modify. Used to remove exists widgets for GC.Collect
var settingsSerialized = JsonSerializer.Serialize(_settings, JsonCodeGen.Default.RepositorySettings);
File.WriteAllText(Path.Combine(_gitDir, "sourcegit.settings"), settingsSerialized);
_settings = null;
_watcher.Dispose(); _watcher.Dispose();
_histories.Cleanup(); _histories.Cleanup();
_workingCopy.Cleanup(); _workingCopy.Cleanup();
@ -289,14 +373,7 @@ namespace SourceGit.ViewModels
_histories = null; _histories = null;
_workingCopy = null; _workingCopy = null;
_stashesPage = null; _stashesPage = null;
_isSearching = false;
_searchCommitFilter = string.Empty;
_isTagGroupExpanded = false;
_isSubmoduleGroupExpanded = false;
_inProgressContext = null; _inProgressContext = null;
_hasUnsolvedConflicts = false;
_remotes.Clear(); _remotes.Clear();
_branches.Clear(); _branches.Clear();
@ -436,7 +513,7 @@ namespace SourceGit.ViewModels
public void ClearHistoriesFilter() public void ClearHistoriesFilter()
{ {
Filters.Clear(); _settings.Filters.Clear();
Task.Run(() => Task.Run(() =>
{ {
@ -525,15 +602,15 @@ namespace SourceGit.ViewModels
var changed = false; var changed = false;
if (toggle) if (toggle)
{ {
if (!Filters.Contains(filter)) if (!_settings.Filters.Contains(filter))
{ {
Filters.Add(filter); _settings.Filters.Add(filter);
changed = true; changed = true;
} }
} }
else else
{ {
changed = Filters.Remove(filter); changed = _settings.Filters.Remove(filter);
} }
if (changed) if (changed)
@ -637,7 +714,7 @@ namespace SourceGit.ViewModels
{ {
var tags = new Commands.QueryTags(FullPath).Result(); var tags = new Commands.QueryTags(FullPath).Result();
foreach (var tag in tags) foreach (var tag in tags)
tag.IsFiltered = Filters.Contains(tag.Name); tag.IsFiltered = _settings.Filters.Contains(tag.Name);
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
@ -652,7 +729,7 @@ namespace SourceGit.ViewModels
var limits = $"-{Preference.Instance.MaxHistoryCommits} "; var limits = $"-{Preference.Instance.MaxHistoryCommits} ";
var validFilters = new List<string>(); var validFilters = new List<string>();
foreach (var filter in Filters) foreach (var filter in _settings.Filters)
{ {
if (filter.StartsWith("refs/", StringComparison.Ordinal)) if (filter.StartsWith("refs/", StringComparison.Ordinal))
{ {
@ -670,12 +747,12 @@ namespace SourceGit.ViewModels
{ {
limits += string.Join(" ", validFilters); limits += string.Join(" ", validFilters);
if (Filters.Count != validFilters.Count) if (_settings.Filters.Count != validFilters.Count)
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
Filters.Clear(); _settings.Filters.Clear();
Filters.AddRange(validFilters); _settings.Filters.AddRange(validFilters);
}); });
} }
} }
@ -685,7 +762,7 @@ namespace SourceGit.ViewModels
} }
var commits = new Commands.QueryCommits(FullPath, limits).Result(); var commits = new Commands.QueryCommits(FullPath, limits).Result();
var graph = Models.CommitGraph.Parse(commits, 8); var graph = Models.CommitGraph.Parse(commits);
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
@ -851,23 +928,24 @@ namespace SourceGit.ViewModels
public void OpenSubmodule(string submodule) public void OpenSubmodule(string submodule)
{ {
var root = Path.GetFullPath(Path.Combine(_fullpath, submodule)); var root = Path.GetFullPath(Path.Combine(_fullpath, submodule));
var gitDir = new Commands.QueryGitDir(root).Result(); var normalizedPath = root.Replace("\\", "/");
var repo = Preference.AddRepository(root, gitDir);
var node = new RepositoryNode() var node = Preference.FindNode(normalizedPath);
if (node == null)
{ {
Id = repo.FullPath, node = new RepositoryNode()
Name = Path.GetFileName(repo.FullPath), {
Id = normalizedPath,
Name = Path.GetFileName(normalizedPath),
Bookmark = 0, Bookmark = 0,
IsRepository = true, IsRepository = true,
}; };
}
var launcher = App.GetTopLevel().DataContext as Launcher; var launcher = App.GetTopLevel().DataContext as Launcher;
if (launcher != null) if (launcher != null)
{
launcher.OpenRepositoryInTab(node, null); launcher.OpenRepositoryInTab(node, null);
} }
}
public void AddWorktree() public void AddWorktree()
{ {
@ -883,16 +961,17 @@ namespace SourceGit.ViewModels
public void OpenWorktree(Models.Worktree worktree) public void OpenWorktree(Models.Worktree worktree)
{ {
var gitDir = new Commands.QueryGitDir(worktree.FullPath).Result(); var node = Preference.FindNode(worktree.FullPath);
var repo = Preference.AddRepository(worktree.FullPath, gitDir); if (node == null)
var node = new RepositoryNode()
{ {
Id = repo.FullPath, node = new RepositoryNode()
Name = Path.GetFileName(repo.FullPath), {
Id = worktree.FullPath,
Name = Path.GetFileName(worktree.FullPath),
Bookmark = 0, Bookmark = 0,
IsRepository = true, IsRepository = true,
}; };
}
var launcher = App.GetTopLevel().DataContext as Launcher; var launcher = App.GetTopLevel().DataContext as Launcher;
if (launcher != null) if (launcher != null)
@ -1183,7 +1262,7 @@ namespace SourceGit.ViewModels
if (upstream != null) if (upstream != null)
{ {
var fastForward = new MenuItem(); var fastForward = new MenuItem();
fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", $"{upstream.Remote}/{upstream.Name}"); fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", upstream.FriendlyName);
fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); fastForward.Icon = App.CreateMenuIcon("Icons.FastForward");
fastForward.IsEnabled = !string.IsNullOrEmpty(branch.UpstreamTrackStatus) && branch.UpstreamTrackStatus.IndexOf('↑') < 0; fastForward.IsEnabled = !string.IsNullOrEmpty(branch.UpstreamTrackStatus) && branch.UpstreamTrackStatus.IndexOf('↑') < 0;
fastForward.Click += (o, e) => fastForward.Click += (o, e) =>
@ -1472,9 +1551,10 @@ namespace SourceGit.ViewModels
{ {
var menu = new ContextMenu(); var menu = new ContextMenu();
var current = Branches.Find(x => x.IsCurrent); var current = Branches.Find(x => x.IsCurrent);
var name = branch.FriendlyName;
var checkout = new MenuItem(); var checkout = new MenuItem();
checkout.Header = new Views.NameHighlightedTextBlock("BranchCM.Checkout", $"{branch.Remote}/{branch.Name}"); checkout.Header = new Views.NameHighlightedTextBlock("BranchCM.Checkout", name);
checkout.Icon = App.CreateMenuIcon("Icons.Check"); checkout.Icon = App.CreateMenuIcon("Icons.Check");
checkout.Click += (o, e) => checkout.Click += (o, e) =>
{ {
@ -1487,7 +1567,7 @@ namespace SourceGit.ViewModels
if (current != null) if (current != null)
{ {
var pull = new MenuItem(); var pull = new MenuItem();
pull.Header = new Views.NameHighlightedTextBlock("BranchCM.PullInto", $"{branch.Remote}/{branch.Name}", current.Name); pull.Header = new Views.NameHighlightedTextBlock("BranchCM.PullInto", name, current.Name);
pull.Icon = App.CreateMenuIcon("Icons.Pull"); pull.Icon = App.CreateMenuIcon("Icons.Pull");
pull.Click += (o, e) => pull.Click += (o, e) =>
{ {
@ -1497,17 +1577,17 @@ namespace SourceGit.ViewModels
}; };
var merge = new MenuItem(); var merge = new MenuItem();
merge.Header = new Views.NameHighlightedTextBlock("BranchCM.Merge", $"{branch.Remote}/{branch.Name}", current.Name); merge.Header = new Views.NameHighlightedTextBlock("BranchCM.Merge", name, current.Name);
merge.Icon = App.CreateMenuIcon("Icons.Merge"); merge.Icon = App.CreateMenuIcon("Icons.Merge");
merge.Click += (o, e) => merge.Click += (o, e) =>
{ {
if (PopupHost.CanCreatePopup()) if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new Merge(this, $"{branch.Remote}/{branch.Name}", current.Name)); PopupHost.ShowPopup(new Merge(this, name, current.Name));
e.Handled = true; e.Handled = true;
}; };
var rebase = new MenuItem(); var rebase = new MenuItem();
rebase.Header = new Views.NameHighlightedTextBlock("BranchCM.Rebase", current.Name, $"{branch.Remote}/{branch.Name}"); rebase.Header = new Views.NameHighlightedTextBlock("BranchCM.Rebase", current.Name, name);
rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); rebase.Icon = App.CreateMenuIcon("Icons.Rebase");
rebase.Click += (o, e) => rebase.Click += (o, e) =>
{ {
@ -1554,7 +1634,7 @@ namespace SourceGit.ViewModels
menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(new MenuItem() { Header = "-" });
var delete = new MenuItem(); var delete = new MenuItem();
delete.Header = new Views.NameHighlightedTextBlock("BranchCM.Delete", $"{branch.Remote}/{branch.Name}"); delete.Header = new Views.NameHighlightedTextBlock("BranchCM.Delete", name);
delete.Icon = App.CreateMenuIcon("Icons.Clear"); delete.Icon = App.CreateMenuIcon("Icons.Clear");
delete.Click += (o, e) => delete.Click += (o, e) =>
{ {
@ -1598,7 +1678,7 @@ namespace SourceGit.ViewModels
copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Icon = App.CreateMenuIcon("Icons.Copy");
copy.Click += (o, e) => copy.Click += (o, e) =>
{ {
App.CopyText(branch.Remote + "/" + branch.Name); App.CopyText(name);
e.Handled = true; e.Handled = true;
}; };
@ -1791,7 +1871,7 @@ namespace SourceGit.ViewModels
{ {
var dup = b; var dup = b;
var target = new MenuItem(); var target = new MenuItem();
target.Header = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}"; target.Header = b.FriendlyName;
target.Icon = App.CreateMenuIcon(b.IsCurrent ? "Icons.Check" : "Icons.Branch"); target.Icon = App.CreateMenuIcon(b.IsCurrent ? "Icons.Check" : "Icons.Branch");
target.Click += (_, e) => target.Click += (_, e) =>
{ {
@ -1814,7 +1894,7 @@ namespace SourceGit.ViewModels
private BranchTreeNode.Builder BuildBranchTree(List<Models.Branch> branches, List<Models.Remote> remotes) private BranchTreeNode.Builder BuildBranchTree(List<Models.Branch> branches, List<Models.Remote> remotes)
{ {
var builder = new BranchTreeNode.Builder(); var builder = new BranchTreeNode.Builder();
builder.SetFilters(Filters); builder.SetFilters(_settings.Filters);
if (string.IsNullOrEmpty(_searchBranchFilter)) if (string.IsNullOrEmpty(_searchBranchFilter))
{ {
@ -1831,7 +1911,7 @@ namespace SourceGit.ViewModels
visibles.Add(b); visibles.Add(b);
} }
builder.Run(visibles, remotes, visibles.Count <= 20); builder.Run(visibles, remotes, true);
} }
return builder; return builder;
@ -1858,6 +1938,7 @@ namespace SourceGit.ViewModels
private string _fullpath = string.Empty; private string _fullpath = string.Empty;
private string _gitDir = string.Empty; private string _gitDir = string.Empty;
private RepositorySettings _settings = null;
private Models.Watcher _watcher = null; private Models.Watcher _watcher = null;
private Histories _histories = null; private Histories _histories = null;
@ -1871,6 +1952,8 @@ namespace SourceGit.ViewModels
private string _searchCommitFilter = string.Empty; private string _searchCommitFilter = string.Empty;
private List<Models.Commit> _searchedCommits = new List<Models.Commit>(); private List<Models.Commit> _searchedCommits = new List<Models.Commit>();
private bool _isLocalBranchGroupExpanded = true;
private bool _isRemoteGroupExpanded = false;
private bool _isTagGroupExpanded = false; private bool _isTagGroupExpanded = false;
private bool _isSubmoduleGroupExpanded = false; private bool _isSubmoduleGroupExpanded = false;
private bool _isWorktreeGroupExpanded = false; private bool _isWorktreeGroupExpanded = false;

View file

@ -118,9 +118,8 @@ namespace SourceGit.ViewModels
public void NavigateTo(string commitSHA) public void NavigateTo(string commitSHA)
{ {
var repo = Preference.FindRepository(_repo); var repo = App.FindOpenedRepository(_repo);
if (repo != null) repo?.NavigateToCommit(commitSHA);
repo.NavigateToCommit(commitSHA);
} }
public void ClearSearchFilter() public void ClearSearchFilter()

View file

@ -39,7 +39,7 @@ namespace SourceGit.ViewModels
return Task.Run(() => return Task.Run(() =>
{ {
var succ = new Commands.Commit(_repo.FullPath, _message, true, true).Exec(); var succ = new Commands.Commit(_repo.FullPath, _message, false, true, true).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true)); CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ; return succ;
}); });

View file

@ -43,7 +43,7 @@ namespace SourceGit.ViewModels
{ {
var succ = new Commands.Reset(_repo.FullPath, Parent.SHA, "--soft").Exec(); var succ = new Commands.Reset(_repo.FullPath, Parent.SHA, "--soft").Exec();
if (succ) if (succ)
succ = new Commands.Commit(_repo.FullPath, _message, true).Exec(); succ = new Commands.Commit(_repo.FullPath, _message, false, true).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true)); CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ; return succ;
}); });

View file

@ -90,7 +90,6 @@ namespace SourceGit.ViewModels
public ContextMenu CreateContextMenu(RepositoryNode node) public ContextMenu CreateContextMenu(RepositoryNode node)
{ {
var menu = new ContextMenu(); var menu = new ContextMenu();
var hasRepo = Preference.FindRepository(node.Id) != null;
if (!node.IsRepository && node.SubNodes.Count > 0) if (!node.IsRepository && node.SubNodes.Count > 0)
{ {
@ -115,7 +114,6 @@ namespace SourceGit.ViewModels
var edit = new MenuItem(); var edit = new MenuItem();
edit.Header = App.Text("Welcome.Edit"); edit.Header = App.Text("Welcome.Edit");
edit.Icon = App.CreateMenuIcon("Icons.Edit"); edit.Icon = App.CreateMenuIcon("Icons.Edit");
edit.IsEnabled = !node.IsRepository || hasRepo;
edit.Click += (_, e) => edit.Click += (_, e) =>
{ {
node.Edit(); node.Edit();
@ -128,7 +126,6 @@ namespace SourceGit.ViewModels
var explore = new MenuItem(); var explore = new MenuItem();
explore.Header = App.Text("Repository.Explore"); explore.Header = App.Text("Repository.Explore");
explore.Icon = App.CreateMenuIcon("Icons.Folder.Open"); explore.Icon = App.CreateMenuIcon("Icons.Folder.Open");
explore.IsEnabled = hasRepo;
explore.Click += (_, e) => explore.Click += (_, e) =>
{ {
node.OpenInFileManager(); node.OpenInFileManager();
@ -139,7 +136,6 @@ namespace SourceGit.ViewModels
var terminal = new MenuItem(); var terminal = new MenuItem();
terminal.Header = App.Text("Repository.Terminal"); terminal.Header = App.Text("Repository.Terminal");
terminal.Icon = App.CreateMenuIcon("Icons.Terminal"); terminal.Icon = App.CreateMenuIcon("Icons.Terminal");
terminal.IsEnabled = hasRepo;
terminal.Click += (_, e) => terminal.Click += (_, e) =>
{ {
node.OpenTerminal(); node.OpenTerminal();

View file

@ -77,6 +77,12 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _isCommitting, value); private set => SetProperty(ref _isCommitting, value);
} }
public bool AutoStageBeforeCommit
{
get => _repo.Settings.AutoStageBeforeCommit;
set => _repo.Settings.AutoStageBeforeCommit = value;
}
public bool UseAmend public bool UseAmend
{ {
get => _useAmend; get => _useAmend;
@ -1113,7 +1119,7 @@ namespace SourceGit.ViewModels
public ContextMenu CreateContextMenuForCommitMessages() public ContextMenu CreateContextMenuForCommitMessages()
{ {
var menu = new ContextMenu(); var menu = new ContextMenu();
if (_repo.CommitMessages.Count == 0) if (_repo.Settings.CommitMessages.Count == 0)
{ {
var empty = new MenuItem(); var empty = new MenuItem();
empty.Header = App.Text("WorkingCopy.NoCommitHistories"); empty.Header = App.Text("WorkingCopy.NoCommitHistories");
@ -1128,7 +1134,7 @@ namespace SourceGit.ViewModels
menu.Items.Add(tip); menu.Items.Add(tip);
menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(new MenuItem() { Header = "-" });
foreach (var message in _repo.CommitMessages) foreach (var message in _repo.Settings.CommitMessages)
{ {
var dump = message; var dump = message;
@ -1216,7 +1222,13 @@ namespace SourceGit.ViewModels
return; return;
} }
if (_staged.Count == 0) if (_count == 0)
{
App.RaiseException(_repo.FullPath, "No files added to commit!");
return;
}
if (!AutoStageBeforeCommit && _staged.Count == 0)
{ {
App.RaiseException(_repo.FullPath, "No files added to commit!"); App.RaiseException(_repo.FullPath, "No files added to commit!");
return; return;
@ -1228,15 +1240,16 @@ namespace SourceGit.ViewModels
return; return;
} }
PushCommitMessage(); _repo.Settings.PushCommitMessage(_commitMessage);
SetDetail(null); SetDetail(null);
IsCommitting = true; IsCommitting = true;
_repo.SetWatcherEnabled(false); _repo.SetWatcherEnabled(false);
var autoStage = AutoStageBeforeCommit;
Task.Run(() => Task.Run(() =>
{ {
var succ = new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec(); var succ = new Commands.Commit(_repo.FullPath, _commitMessage, autoStage, _useAmend).Exec();
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
if (succ) if (succ)
@ -1257,27 +1270,6 @@ namespace SourceGit.ViewModels
}); });
} }
private void PushCommitMessage()
{
var existIdx = _repo.CommitMessages.IndexOf(CommitMessage);
if (existIdx == 0)
{
return;
}
else if (existIdx > 0)
{
_repo.CommitMessages.Move(existIdx, 0);
return;
}
if (_repo.CommitMessages.Count > 9)
{
_repo.CommitMessages.RemoveRange(9, _repo.CommitMessages.Count - 9);
}
_repo.CommitMessages.Insert(0, CommitMessage);
}
private Repository _repo = null; private Repository _repo = null;
private bool _isLoadingData = false; private bool _isLoadingData = false;
private bool _isStaging = false; private bool _isStaging = false;

View file

@ -23,7 +23,7 @@
<DataTemplate DataType="m:Branch"> <DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/> <Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/> <TextBlock VerticalAlignment="Center" Text="{Binding FriendlyName}" Margin="8,0,0,0"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>

View file

@ -378,11 +378,9 @@ namespace SourceGit.Views
private void OnCommitSHAPointerPressed(object sender, PointerPressedEventArgs e) private void OnCommitSHAPointerPressed(object sender, PointerPressedEventArgs e)
{ {
if (DataContext is ViewModels.Blame blame) if (sender is TextBlock txt && DataContext is ViewModels.Blame blame)
{
var txt = sender as TextBlock;
blame.NavigateToCommit(txt.Text); blame.NavigateToCommit(txt.Text);
}
e.Handled = true; e.Handled = true;
} }

View file

@ -61,7 +61,7 @@
User="{Binding BaseHead.Author}"/> User="{Binding BaseHead.Author}"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding BaseHead.Author.Name}" Margin="8,0,0,0"/> <TextBlock Grid.Column="1" Classes="monospace" Text="{Binding BaseHead.Author.Name}" Margin="8,0,0,0"/>
<Border Grid.Column="2" Background="{DynamicResource Brush.Accent}" CornerRadius="4"> <Border Grid.Column="2" Background="{DynamicResource Brush.Accent}" CornerRadius="4">
<TextBlock Text="{Binding Base, Converter={x:Static c:BranchConverters.ToName}}" Classes="monospace" Margin="4,0" Foreground="#FFDDDDDD"/> <TextBlock Text="{Binding Base.FriendlyName}" Classes="monospace" Margin="4,0" Foreground="#FFDDDDDD"/>
</Border> </Border>
<TextBlock Grid.Column="3" Classes="monospace" Text="{Binding BaseHead.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0" TextDecorations="Underline" PointerPressed="OnPressedSHA"/> <TextBlock Grid.Column="3" Classes="monospace" Text="{Binding BaseHead.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0" TextDecorations="Underline" PointerPressed="OnPressedSHA"/>
<TextBlock Grid.Column="4" Classes="monospace" Text="{Binding BaseHead.CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/> <TextBlock Grid.Column="4" Classes="monospace" Text="{Binding BaseHead.CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
@ -82,7 +82,7 @@
User="{Binding ToHead.Author}"/> User="{Binding ToHead.Author}"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding ToHead.Author.Name}" Margin="8,0,0,0"/> <TextBlock Grid.Column="1" Classes="monospace" Text="{Binding ToHead.Author.Name}" Margin="8,0,0,0"/>
<Border Grid.Column="2" Background="{DynamicResource Brush.Accent}" CornerRadius="4"> <Border Grid.Column="2" Background="{DynamicResource Brush.Accent}" CornerRadius="4">
<TextBlock Text="{Binding To, Converter={x:Static c:BranchConverters.ToName}}" Classes="monospace" Margin="4,0" Foreground="#FFDDDDDD"/> <TextBlock Text="{Binding To.FriendlyName}" Classes="monospace" Margin="4,0" Foreground="#FFDDDDDD"/>
</Border> </Border>
<TextBlock Grid.Column="3" Classes="monospace" Text="{Binding ToHead.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0" TextDecorations="Underline" PointerPressed="OnPressedSHA"/> <TextBlock Grid.Column="3" Classes="monospace" Text="{Binding ToHead.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" Margin="8,0,0,0" TextDecorations="Underline" PointerPressed="OnPressedSHA"/>
<TextBlock Grid.Column="4" Classes="monospace" Text="{Binding ToHead.CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/> <TextBlock Grid.Column="4" Classes="monospace" Text="{Binding ToHead.CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>

108
src/Views/BranchTree.axaml Normal file
View file

@ -0,0 +1,108 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:c="using:SourceGit.Converters"
xmlns:v="using:SourceGit.Views"
xmlns:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.BranchTree"
x:Name="ThisControl">
<DataGrid x:Name="BranchesPresenter"
ItemsSource="{Binding #ThisControl.Rows}"
Background="Transparent"
RowHeight="24"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
HeadersVisibility="None"
SelectionChanged="OnNodesSelectionChanged"
ContextRequested="OnTreeContextRequested">
<DataGrid.Styles>
<Style Selector="DataGridRow" x:DataType="vm:BranchTreeNode">
<Setter Property="CornerRadius" Value="{Binding CornerRadius}" />
</Style>
<Style Selector="DataGridRow /template/ Border#RowBorder">
<Setter Property="ClipToBounds" Value="True" />
</Style>
<Style Selector="Grid.repository_leftpanel DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".5"/>
</Style>
<Style Selector="Grid.repository_leftpanel DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:BranchTreeNode">
<Grid Height="24"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
ColumnDefinitions="16,20,*,Auto,Auto"
Background="Transparent"
DoubleTapped="OnDoubleTappedBranchNode"
ToolTip.Tip="{Binding Tooltip}">
<!-- Tree Expander -->
<ToggleButton Grid.Column="0"
Classes="tree_expander"
Focusable="False"
HorizontalAlignment="Center"
IsChecked="{Binding IsExpanded}"
IsHitTestVisible="False"
IsVisible="{Binding !IsBranch}"/>
<!-- Icon -->
<v:BranchTreeNodeIcon Grid.Column="1"
Node="{Binding}"
IsExpanded="{Binding IsExpanded}"/>
<!-- Name -->
<TextBlock Grid.Column="2"
Text="{Binding Name}"
Classes="monospace"
FontWeight="{Binding NameFontWeight}"/>
<!-- Tracking status -->
<Border Grid.Column="3"
Margin="8,0"
Height="18"
CornerRadius="9"
VerticalAlignment="Center"
Background="{DynamicResource Brush.Badge}"
IsVisible="{Binding IsUpstreamTrackStatusVisible}">
<TextBlock Classes="monospace" FontSize="10" HorizontalAlignment="Center" Margin="9,0" Text="{Binding UpstreamTrackStatus}" Foreground="{DynamicResource Brush.BadgeFG}"/>
</Border>
<!-- Filter Toggle Button -->
<ToggleButton Grid.Column="4"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
IsCheckedChanged="OnToggleFilter"
IsVisible="{Binding IsBranch}"
IsChecked="{Binding IsFiltered}"
ToolTip.Tip="{DynamicResource Text.Filter}"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</UserControl>

View file

@ -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<ViewModels.BranchTreeNode> NodeProperty =
AvaloniaProperty.Register<BranchTreeNodeIcon, ViewModels.BranchTreeNode>(nameof(Node));
public ViewModels.BranchTreeNode Node
{
get => GetValue(NodeProperty);
set => SetValue(NodeProperty, value);
}
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<BranchTreeNodeIcon, bool>(nameof(IsExpanded));
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
static BranchTreeNodeIcon()
{
NodeProperty.Changed.AddClassHandler<BranchTreeNodeIcon>((icon, _) => icon.UpdateContent());
IsExpandedProperty.Changed.AddClassHandler<BranchTreeNodeIcon>((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<List<ViewModels.BranchTreeNode>> NodesProperty =
AvaloniaProperty.Register<BranchTree, List<ViewModels.BranchTreeNode>>(nameof(Nodes));
public List<ViewModels.BranchTreeNode> Nodes
{
get => GetValue(NodesProperty);
set => SetValue(NodesProperty, value);
}
public AvaloniaList<ViewModels.BranchTreeNode> Rows
{
get;
private set;
} = new AvaloniaList<ViewModels.BranchTreeNode>();
public static readonly RoutedEvent<RoutedEventArgs> SelectionChangedEvent =
RoutedEvent.Register<BranchTree, RoutedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> SelectionChanged
{
add { AddHandler(SelectionChangedEvent, value); }
remove { RemoveHandler(SelectionChangedEvent, value); }
}
public static readonly RoutedEvent<RoutedEventArgs> RowsChangedEvent =
RoutedEvent.Register<BranchTree, RoutedEventArgs>(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> 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<ViewModels.BranchTreeNode>();
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<Models.Branch>();
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<ViewModels.BranchTreeNode>();
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<ViewModels.BranchTreeNode> rows, List<ViewModels.BranchTreeNode> 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<Models.Branch> 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);
}
}
}

View file

@ -90,11 +90,11 @@
<DataTemplate DataType="{x:Type m:Decorator}"> <DataTemplate DataType="{x:Type m:Decorator}">
<Border Height="16" Margin="0,0,6,0" CornerRadius="2" ClipToBounds="True"> <Border Height="16" Margin="0,0,6,0" CornerRadius="2" ClipToBounds="True">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Border Background="{DynamicResource Brush.Decorator}" Width="16"> <Border Background="{DynamicResource Brush.DecoratorIconBG}" Width="16">
<Path Width="8" Height="8" Data="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToIcon}}" Fill="{DynamicResource Brush.DecoratorIcon}"/> <Path Width="8" Height="8" Data="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToIcon}}" Fill="{DynamicResource Brush.DecoratorIcon}"/>
</Border> </Border>
<Border Background="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToBackground}}"> <Border Background="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToBackground}}">
<TextBlock Classes="monospace" Text="{Binding Name}" FontSize="10" Margin="4,0" Foreground="Black"/> <TextBlock Classes="monospace" Text="{Binding Name}" FontSize="10" Margin="4,0" Foreground="{DynamicResource Brush.DecoratorFG}"/>
</Border> </Border>
</StackPanel> </StackPanel>
</Border> </Border>
@ -104,9 +104,7 @@
<!-- Messages --> <!-- Messages -->
<TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Message}" VerticalAlignment="Top" Margin="0,4,0,0" /> <TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Message}" VerticalAlignment="Top" Margin="0,4,0,0" />
<ScrollViewer Grid.Row="3" Grid.Column="1" Margin="12,5,8,0" MaxHeight="64" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"> <SelectableTextBlock Grid.Row="3" Grid.Column="1" Margin="12,5,8,0" Text="{Binding #ThisControl.Message}" FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}" TextWrapping="Wrap"/>
<SelectableTextBlock Text="{Binding #ThisControl.Message}" FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}" TextWrapping="Wrap"/>
</ScrollViewer>
</Grid> </Grid>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>

View file

@ -17,58 +17,55 @@
<TextBlock Classes="tab_header" Text="{DynamicResource Text.CommitDetail.Info}"/> <TextBlock Classes="tab_header" Text="{DynamicResource Text.CommitDetail.Info}"/>
</TabItem.Header> </TabItem.Header>
<Grid RowDefinitions="Auto,1,*"> <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical">
<!-- Base Information --> <!-- Base Information -->
<v:CommitBaseInfo Grid.Row="0" Content="{Binding Commit}" Message="{Binding FullMessage}"/> <v:CommitBaseInfo Content="{Binding Commit}" Message="{Binding FullMessage}"/>
<!-- Line --> <!-- Line -->
<Rectangle Grid.Row="1" Height=".65" Margin="8" Fill="{DynamicResource Brush.Border2}" VerticalAlignment="Center"/> <Rectangle Height=".65" Margin="8" Fill="{DynamicResource Brush.Border2}"/>
<!-- Change List --> <!-- Change List -->
<DataGrid Grid.Row="2" <ListBox Background="Transparent"
Background="Transparent" Margin="64,0,8,4"
ItemsSource="{Binding Changes}"
SelectionMode="Single" SelectionMode="Single"
CanUserReorderColumns="False" ItemsSource="{Binding Changes, Converter={x:Static c:ListConverters.Top100Changes}}">
CanUserResizeColumns="False" <ListBox.Styles>
CanUserSortColumns="False" <Style Selector="ListBoxItem">
IsReadOnly="True" <Setter Property="Padding" Value="0"/>
HeadersVisibility="None" <Setter Property="MinHeight" Value="26"/>
Focusable="False"
RowHeight="26"
Margin="64,0,8,16"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
ContextRequested="OnChangeListContextRequested"
DoubleTapped="OnChangeListDoubleTapped">
<DataGrid.Styles>
<Style Selector="DataGridRow">
<Setter Property="CornerRadius" Value="4"/> <Setter Property="CornerRadius" Value="4"/>
</Style> </Style>
<Style Selector="DataGridRow /template/ Border#RowBorder"> </ListBox.Styles>
<Setter Property="ClipToBounds" Value="True"/>
</Style>
</DataGrid.Styles>
<DataGrid.Columns> <ListBox.ItemsPanel>
<DataGridTemplateColumn Width="36" Header="ICON"> <ItemsPanelTemplate>
<DataGridTemplateColumn.CellTemplate> <StackPanel Orientation="Vertical"/>
<DataTemplate> </ItemsPanelTemplate>
<v:ChangeStatusIcon Width="14" Height="14" HorizontalAlignment="Left" Margin="16,0,0,0" IsWorkingCopyChange="False" Change="{Binding}"/> </ListBox.ItemsPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="PATH"> <ListBox.ItemTemplate>
<DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="m:Change">
<DataTemplate> <Grid Background="Transparent" Height="26" ColumnDefinitions="36,*" ContextRequested="OnChangeContextRequested" DoubleTapped="OnChangeDoubleTapped">
<TextBlock Classes="monospace" Text="{Binding Path}" Margin="8,0,0,0"/> <v:ChangeStatusIcon Grid.Column="0"
</DataTemplate> Width="14" Height="14"
</DataGridTemplateColumn.CellTemplate> HorizontalAlignment="Left"
</DataGridTemplateColumn> Margin="16,0,0,0"
</DataGrid.Columns> IsWorkingCopyChange="False"
</DataGrid> Change="{Binding}"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding Path}" Margin="8,0" TextTrimming="CharacterEllipsis"/>
</Grid> </Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Only Top100 Tip -->
<TextBlock Margin="108,0,0,16"
Text="{DynamicResource Text.CommitDetail.Info.GotoChangesPage}"
Foreground="{DynamicResource Brush.FG2}"
IsVisible="{Binding Changes, Converter={x:Static c:ListConverters.IsOnlyTop100Shows}}"/>
</StackPanel>
</ScrollViewer>
</TabItem> </TabItem>
<TabItem> <TabItem>

View file

@ -10,37 +10,23 @@ namespace SourceGit.Views
InitializeComponent(); 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.ActivePageIndex = 1;
detail.SelectedChanges = new() { datagrid.SelectedItem as Models.Change }; detail.SelectedChanges = new() { change };
} }
e.Handled = true; 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; var menu = detail.CreateChangeContextMenu(change);
if (datagrid.SelectedItem == null) grid.OpenContextMenu(menu);
{
e.Handled = true;
return;
}
var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change);
datagrid.OpenContextMenu(menu);
} }
e.Handled = true; e.Handled = true;

View file

@ -20,6 +20,11 @@ namespace SourceGit.Views
protected override Type StyleKeyOverride => typeof(TextBox); protected override Type StyleKeyOverride => typeof(TextBox);
public void Paste(string text)
{
OnTextInput(new TextInputEventArgs() { Text = text });
}
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
{ {
var dump = new KeyEventArgs() 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)) if (e.Key == Key.Enter || (e.Key == Key.Right && SubjectEditor.CaretIndex == Subject.Length))
{ {
@ -120,6 +125,36 @@ namespace SourceGit.Views
DescriptionEditor.CaretIndex = 0; DescriptionEditor.CaretIndex = 0;
e.Handled = true; 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) private void OnDescriptionTextBoxPreviewKeyDown(object sender, KeyEventArgs e)

View file

@ -24,7 +24,7 @@
<DataTemplate DataType="m:Branch"> <DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/> <Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/> <TextBlock VerticalAlignment="Center" Text="{Binding FriendlyName}" Margin="8,0,0,0"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>

View file

@ -23,7 +23,7 @@
<DataTemplate DataType="m:Branch"> <DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/> <Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/> <TextBlock VerticalAlignment="Center" Text="{Binding FriendlyName}" Margin="8,0,0,0"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>

View file

@ -2,10 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels" xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.DeleteBranch" x:Class="SourceGit.Views.DeleteBranch"
x:DataType="vm:DeleteBranch"> x:DataType="vm:DeleteBranch">
@ -18,7 +15,7 @@
<TextBlock Grid.Row="0" Grid.Column="0" HorizontalAlignment="Right" Text="{DynamicResource Text.DeleteBranch.Branch}"/> <TextBlock Grid.Row="0" Grid.Column="0" HorizontalAlignment="Right" Text="{DynamicResource Text.DeleteBranch.Branch}"/>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal"> <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal">
<Path Width="14" Height="14" Margin="8,0" Data="{StaticResource Icons.Branch}"/> <Path Width="14" Height="14" Margin="8,0" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding Target, Converter={x:Static c:BranchConverters.ToName}}"/> <TextBlock Text="{Binding Target.FriendlyName}"/>
</StackPanel> </StackPanel>
<Border Grid.Row="1" Grid.Column="1" Height="32" IsVisible="{Binding !Target.IsLocal}"> <Border Grid.Row="1" Grid.Column="1" Height="32" IsVisible="{Binding !Target.IsLocal}">

View file

@ -64,8 +64,7 @@
<DataGridTemplateColumn Width="*" Header="NAME"> <DataGridTemplateColumn Width="*" Header="NAME">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding Converter={x:Static c:BranchConverters.ToName}}" ClipToBounds="True" <TextBlock Text="{Binding FriendlyName}" ClipToBounds="True" Classes="monospace" />
Classes="monospace" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>

View file

@ -66,8 +66,14 @@
Background="Transparent" Background="Transparent"
Padding="9,6" Padding="9,6"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap, Mode=TwoWay}" IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap, Mode=TwoWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.ToggleWordWrap}"> ToolTip.Tip="{DynamicResource Text.Diff.ToggleWordWrap}">
<ToggleButton.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="IsTextDiff"/>
<Binding Source="{x:Static vm:Preference.Instance}" Path="UseSideBySideDiff" Mode="OneWay" Converter="{x:Static BoolConverters.Not}"/>
</MultiBinding>
</ToggleButton.IsVisible>
<Path Width="12" Height="12" Data="{StaticResource Icons.WordWrap}" Margin="0,2,0,0"/> <Path Width="12" Height="12" Data="{StaticResource Icons.WordWrap}" Margin="0,2,0,0"/>
</ToggleButton> </ToggleButton>

View file

@ -16,7 +16,7 @@
<TextBlock Text="{Binding Local.Name}" Margin="8,0,0,0"/> <TextBlock Text="{Binding Local.Name}" Margin="8,0,0,0"/>
<TextBlock Text="→" Margin="8,0"/> <TextBlock Text="→" Margin="8,0"/>
<Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/> <Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding To, Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/> <TextBlock Text="{Binding To.FriendlyName}" Margin="8,0,0,0"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View file

@ -12,7 +12,7 @@
<TextBlock FontSize="18" <TextBlock FontSize="18"
Classes="bold" Classes="bold"
Text="{DynamicResource Text.Fetch.Title}"/> Text="{DynamicResource Text.Fetch.Title}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,32" ColumnDefinitions="120,*"> <Grid Margin="0,16,0,0" RowDefinitions="32,32,32,32" ColumnDefinitions="120,*">
<TextBlock Grid.Column="0" <TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center" HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0" Margin="0,0,8,0"
@ -40,6 +40,10 @@
<CheckBox Grid.Row="2" Grid.Column="1" <CheckBox Grid.Row="2" Grid.Column="1"
Content="{DynamicResource Text.Fetch.Prune}" Content="{DynamicResource Text.Fetch.Prune}"
IsChecked="{Binding Prune, Mode=TwoWay}"/> IsChecked="{Binding Prune, Mode=TwoWay}"/>
<CheckBox Grid.Row="3" Grid.Column="1"
Content="{DynamicResource Text.Fetch.NoTags}"
IsChecked="{Binding NoTags, Mode=TwoWay}"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View file

@ -46,7 +46,7 @@
<DataTemplate DataType="{x:Type m:Decorator}"> <DataTemplate DataType="{x:Type m:Decorator}">
<Border Height="16" Margin="0,0,4,0" CornerRadius="2" ClipToBounds="True"> <Border Height="16" Margin="0,0,4,0" CornerRadius="2" ClipToBounds="True">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Border Background="{DynamicResource Brush.Decorator}" Width="16"> <Border Background="{DynamicResource Brush.DecoratorIconBG}" Width="16">
<Path Width="8" Height="8" <Path Width="8" Height="8"
Stretch="Uniform" Stretch="Uniform"
Data="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToIcon}}" Data="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToIcon}}"
@ -58,7 +58,7 @@
Text="{Binding Name}" Text="{Binding Name}"
FontSize="10" FontSize="10"
Margin="4,0" Margin="4,0"
Foreground="Black" Foreground="{DynamicResource Brush.DecoratorFG}"
FontWeight="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToFontWeight}}"/> FontWeight="{Binding Type, Converter={x:Static c:DecoratorTypeConverters.ToFontWeight}}"/>
</Border> </Border>
</StackPanel> </StackPanel>

View file

@ -54,7 +54,7 @@
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*" Margin="8"> <Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*" Margin="8">
<TextBlock Grid.Column="0" Text="{DynamicResource Text.InteractiveRebase.Target}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/> <TextBlock Grid.Column="0" Text="{DynamicResource Text.InteractiveRebase.Target}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
<Path Grid.Column="1" Width="14" Height="14" Margin="8,0,0,0" Data="{StaticResource Icons.Branch}"/> <Path Grid.Column="1" Width="14" Height="14" Margin="8,0,0,0" Data="{StaticResource Icons.Branch}"/>
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding Current, Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/> <TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding Current.FriendlyName}" Margin="8,0,0,0"/>
<TextBlock Grid.Column="3" Margin="48,0,0,0" Text="{DynamicResource Text.InteractiveRebase.On}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/> <TextBlock Grid.Column="3" Margin="48,0,0,0" Text="{DynamicResource Text.InteractiveRebase.On}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
<Path Grid.Column="4" Width="14" Height="14" Margin="8,8,0,0" Data="{StaticResource Icons.Commit}"/> <Path Grid.Column="4" Width="14" Height="14" Margin="8,8,0,0" Data="{StaticResource Icons.Commit}"/>

View file

@ -13,7 +13,7 @@
<TextBlock FontSize="18" <TextBlock FontSize="18"
Classes="bold" Classes="bold"
Text="{DynamicResource Text.Pull.Title}"/> Text="{DynamicResource Text.Pull.Title}"/>
<Grid Margin="0,16,0,0" RowDefinitions="32,32,32,32,32" ColumnDefinitions="140,*"> <Grid Margin="0,16,0,0" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="140,*">
<TextBlock Grid.Column="0" <TextBlock Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center" HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0" Margin="0,0,8,0"
@ -48,7 +48,7 @@
<DataTemplate x:DataType="{x:Type m:Branch}"> <DataTemplate x:DataType="{x:Type m:Branch}">
<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center">
<Path Margin="0,0,8,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG1}" Data="{StaticResource Icons.Branch}"/> <Path Margin="0,0,8,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG1}" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding Converter={x:Static c:BranchConverters.ToName}}"/> <TextBlock Text="{Binding FriendlyName}"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
@ -88,6 +88,10 @@
<CheckBox Grid.Row="4" Grid.Column="1" <CheckBox Grid.Row="4" Grid.Column="1"
Content="{DynamicResource Text.Pull.UseRebase}" Content="{DynamicResource Text.Pull.UseRebase}"
IsChecked="{Binding UseRebase, Mode=TwoWay}"/> IsChecked="{Binding UseRebase, Mode=TwoWay}"/>
<CheckBox Grid.Row="5" Grid.Column="1"
Content="{DynamicResource Text.Pull.NoTags}"
IsChecked="{Binding NoTags, Mode=TwoWay}"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View file

@ -31,7 +31,7 @@
<DataTemplate DataType="m:Branch"> <DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/> <Path Width="14" Height="14" Data="{StaticResource Icons.Branch}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Converter={x:Static c:BranchConverters.ToName}}" Margin="8,0,0,0"/> <TextBlock VerticalAlignment="Center" Text="{Binding FriendlyName}" Margin="8,0,0,0"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>

View file

@ -119,7 +119,7 @@
</Grid> </Grid>
<!-- Dashboard --> <!-- Dashboard -->
<Grid Grid.Row="1" Margin="0,0,0,8" RowDefinitions="Auto,Auto,28,Auto,28,*,28,Auto,28,Auto,28,Auto" IsVisible="{Binding !IsSearching}"> <Grid Grid.Row="1" Margin="0,0,0,8" RowDefinitions="Auto,Auto,*" IsVisible="{Binding !IsSearching}">
<!-- Page Switcher for Right Panel --> <!-- Page Switcher for Right Panel -->
<Border Grid.Row="0" Margin="8,0,4,0" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" CornerRadius="6"> <Border Grid.Row="0" Margin="8,0,4,0" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" CornerRadius="6">
<Border CornerRadius="6" ClipToBounds="True"> <Border CornerRadius="6" ClipToBounds="True">
@ -231,140 +231,40 @@
</TextBox.InnerRightContent> </TextBox.InnerRightContent>
</TextBox> </TextBox>
<Grid Grid.Row="2" x:Name="leftSidebarGroups" Margin="0,4,0,0" RowDefinitions="28,Auto,28,Auto,28,Auto,28,Auto,28,Auto">
<!-- Local Branches --> <!-- Local Branches -->
<TextBlock Grid.Row="2" Classes="group_header_label" Text="{DynamicResource Text.Repository.LocalBranches}"/> <ToggleButton Grid.Row="0" Classes="group_expander" IsChecked="{Binding IsLocalBranchGroupExpanded, Mode=TwoWay}">
<TreeView Grid.Row="3" <TextBlock Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.LocalBranches}"/>
</ToggleButton>
<v:BranchTree Grid.Row="1"
x:Name="localBranchTree" x:Name="localBranchTree"
MaxHeight="400" Height="0"
Margin="8,0,4,0" Margin="8,0,4,0"
SelectionMode="Multiple" Nodes="{Binding LocalBranchTrees}"
ItemsSource="{Binding LocalBranchTrees}" IsVisible="{Binding IsLocalBranchGroupExpanded}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionChanged="OnLocalBranchTreeSelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Auto" RowsChanged="OnBranchTreeRowsChanged"/>
ContextRequested="OnLocalBranchContextMenuRequested"
SelectionChanged="OnLocalBranchTreeSelectionChanged">
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:BranchTreeNode">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}" x:DataType="{x:Type vm:BranchTreeNode}">
<Grid Height="24" ColumnDefinitions="20,*,Auto,Auto" Background="Transparent" DoubleTapped="OnDoubleTappedBranchNode">
<Path Grid.Column="0" Classes="folder_icon" Width="12" Height="12" HorizontalAlignment="Left" Margin="0,1,0,0" IsVisible="{Binding IsFolder}"/>
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Margin="0,2,0,0" Data="{StaticResource Icons.Check}" IsVisible="{Binding IsCurrent}" VerticalAlignment="Center"/>
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Margin="2,0,0,0" Data="{StaticResource Icons.Branch}" VerticalAlignment="Center">
<Path.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="!IsFolder"/>
<Binding Path="!IsCurrent"/>
</MultiBinding>
</Path.IsVisible>
</Path>
<TextBlock Grid.Column="1" Text="{Binding Name}" Classes="monospace" FontWeight="{Binding IsCurrent, Converter={x:Static c:BoolConverters.BoldIfTrue}}"/>
<Border Grid.Column="2" Margin="8,0" Height="18" CornerRadius="9" VerticalAlignment="Center" Background="{DynamicResource Brush.Badge}" IsVisible="{Binding IsUpstreamTrackStatusVisible}">
<TextBlock Classes="monospace" FontSize="10" HorizontalAlignment="Center" Margin="9,0" Text="{Binding UpstreamTrackStatus}" Foreground="{DynamicResource Brush.BadgeFG}"/>
</Border>
<ToggleButton Grid.Column="3"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
IsVisible="{Binding IsBranch}"
Checked="OnToggleFilter"
Unchecked="OnToggleFilter"
IsChecked="{Binding IsFiltered}"/>
</Grid>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Remotes --> <!-- Remotes -->
<Grid Grid.Row="4" ColumnDefinitions="*,Auto"> <ToggleButton Grid.Row="2" Classes="group_expander" IsChecked="{Binding IsRemoteGroupExpanded, Mode=TwoWay}">
<TextBlock Grid.Column="0" Classes="group_header_label" Text="{DynamicResource Text.Repository.Remotes}"/> <Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Remotes}"/>
<Button Grid.Column="1" Classes="icon_button" Width="14" Margin="8,0" Command="{Binding AddRemote}" ToolTip.Tip="{DynamicResource Text.Repository.Remotes.Add}"> <Button Grid.Column="1" Classes="icon_button" Width="14" Margin="8,0" Command="{Binding AddRemote}" ToolTip.Tip="{DynamicResource Text.Repository.Remotes.Add}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Remote.Add}"/> <Path Width="12" Height="12" Data="{StaticResource Icons.Remote.Add}"/>
</Button> </Button>
</Grid> </Grid>
<TreeView Grid.Row="5" </ToggleButton>
<v:BranchTree Grid.Row="3"
x:Name="remoteBranchTree" x:Name="remoteBranchTree"
Height="0"
Margin="8,0,4,0" Margin="8,0,4,0"
SelectionMode="Multiple" Nodes="{Binding RemoteBranchTrees}"
ItemsSource="{Binding RemoteBranchTrees}" IsVisible="{Binding IsRemoteGroupExpanded}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionChanged="OnRemoteBranchTreeSelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Auto" RowsChanged="OnBranchTreeRowsChanged"/>
ContextRequested="OnRemoteBranchContextMenuRequested"
SelectionChanged="OnRemoteBranchTreeSelectionChanged">
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:BranchTreeNode">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}" x:DataType="{x:Type vm:BranchTreeNode}">
<Grid Height="24" ColumnDefinitions="20,*,Auto" Background="Transparent" DoubleTapped="OnDoubleTappedBranchNode">
<Path Grid.Column="0" Classes="folder_icon" Width="10" Height="10" HorizontalAlignment="Left" Margin="0,2,0,0" IsVisible="{Binding IsFolder}" VerticalAlignment="Center"/>
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Margin="0,2,0,0" Data="{StaticResource Icons.Remote}" IsVisible="{Binding IsRemote}" VerticalAlignment="Center"/>
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Margin="2,0,0,0" Data="{StaticResource Icons.Branch}" IsVisible="{Binding IsBranch}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Classes="monospace"/>
<ToggleButton Grid.Column="2"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
Checked="OnToggleFilter"
Unchecked="OnToggleFilter"
IsVisible="{Binding IsBranch}"
IsChecked="{Binding IsFiltered}"/>
</Grid>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Tags --> <!-- Tags -->
<ToggleButton Grid.Row="6" Classes="group_expander" IsChecked="{Binding IsTagGroupExpanded, Mode=TwoWay}"> <ToggleButton Grid.Row="4" Classes="group_expander" IsChecked="{Binding IsTagGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Tags}"/> <TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Tags}"/>
<TextBlock Grid.Column="1" Text="{Binding Tags, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/> <TextBlock Grid.Column="1" Text="{Binding Tags, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
@ -373,8 +273,9 @@
</Button> </Button>
</Grid> </Grid>
</ToggleButton> </ToggleButton>
<DataGrid Grid.Row="7" <DataGrid Grid.Row="5"
x:Name="tagsList" x:Name="tagsList"
Height="0"
Margin="8,0,4,0" Margin="8,0,4,0"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding VisibleTags}" ItemsSource="{Binding VisibleTags}"
@ -391,7 +292,7 @@
IsVisible="{Binding IsTagGroupExpanded, Mode=OneWay}" IsVisible="{Binding IsTagGroupExpanded, Mode=OneWay}"
SelectionChanged="OnTagDataGridSelectionChanged" SelectionChanged="OnTagDataGridSelectionChanged"
ContextRequested="OnTagContextRequested" ContextRequested="OnTagContextRequested"
PropertyChanged="OnTagPropertyChanged"> PropertyChanged="OnLeftSidebarDataGridPropertyChanged">
<DataGrid.Styles> <DataGrid.Styles>
<Style Selector="DataGridRow"> <Style Selector="DataGridRow">
<Setter Property="CornerRadius" Value="4" /> <Setter Property="CornerRadius" Value="4" />
@ -442,9 +343,10 @@
<ToggleButton Classes="filter" <ToggleButton Classes="filter"
Margin="0,0,8,0" Margin="0,0,8,0"
Background="Transparent" Background="Transparent"
Checked="OnToggleFilter" Checked="OnToggleTagFilter"
Unchecked="OnToggleFilter" Unchecked="OnToggleTagFilter"
IsChecked="{Binding IsFiltered}"/> IsChecked="{Binding IsFiltered}"
ToolTip.Tip="{DynamicResource Text.Filter}"/>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -452,7 +354,7 @@
</DataGrid> </DataGrid>
<!-- Submodules --> <!-- Submodules -->
<ToggleButton Grid.Row="8" Classes="group_expander" IsChecked="{Binding IsSubmoduleGroupExpanded, Mode=TwoWay}"> <ToggleButton Grid.Row="6" Classes="group_expander" IsChecked="{Binding IsSubmoduleGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="Auto,*,Auto,Auto"> <Grid ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Submodules}"/> <TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Submodules}"/>
<TextBlock Grid.Column="1" Text="{Binding Submodules, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/> <TextBlock Grid.Column="1" Text="{Binding Submodules, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
@ -475,8 +377,9 @@
</Button> </Button>
</Grid> </Grid>
</ToggleButton> </ToggleButton>
<DataGrid Grid.Row="9" <DataGrid Grid.Row="7"
MaxHeight="200" x:Name="submoduleList"
Height="0"
Margin="8,0,4,0" Margin="8,0,4,0"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding Submodules}" ItemsSource="{Binding Submodules}"
@ -492,6 +395,7 @@
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
ContextRequested="OnSubmoduleContextRequested" ContextRequested="OnSubmoduleContextRequested"
DoubleTapped="OnDoubleTappedSubmodule" DoubleTapped="OnDoubleTappedSubmodule"
PropertyChanged="OnLeftSidebarDataGridPropertyChanged"
IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}"> IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}">
<DataGrid.Styles> <DataGrid.Styles>
<Style Selector="DataGridRow"> <Style Selector="DataGridRow">
@ -533,7 +437,7 @@
</DataGrid> </DataGrid>
<!-- Worktrees --> <!-- Worktrees -->
<ToggleButton Grid.Row="10" Classes="group_expander" IsChecked="{Binding IsWorktreeGroupExpanded, Mode=TwoWay}"> <ToggleButton Grid.Row="8" Classes="group_expander" IsChecked="{Binding IsWorktreeGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="Auto,*,Auto,Auto"> <Grid ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Worktrees}"/> <TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Worktrees}"/>
<TextBlock Grid.Column="1" Text="{Binding Worktrees, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/> <TextBlock Grid.Column="1" Text="{Binding Worktrees, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
@ -556,8 +460,9 @@
</Button> </Button>
</Grid> </Grid>
</ToggleButton> </ToggleButton>
<DataGrid Grid.Row="11" <DataGrid Grid.Row="9"
MaxHeight="200" x:Name="worktreeList"
Height="0"
Margin="8,0,4,0" Margin="8,0,4,0"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding Worktrees}" ItemsSource="{Binding Worktrees}"
@ -573,6 +478,7 @@
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
ContextRequested="OnWorktreeContextRequested" ContextRequested="OnWorktreeContextRequested"
DoubleTapped="OnDoubleTappedWorktree" DoubleTapped="OnDoubleTappedWorktree"
PropertyChanged="OnLeftSidebarDataGridPropertyChanged"
IsVisible="{Binding IsWorktreeGroupExpanded, Mode=OneWay}"> IsVisible="{Binding IsWorktreeGroupExpanded, Mode=OneWay}">
<DataGrid.Styles> <DataGrid.Styles>
<Style Selector="DataGridRow"> <Style Selector="DataGridRow">
@ -622,6 +528,7 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</Grid> </Grid>
</Grid>
<!-- Commit Search Panel --> <!-- Commit Search Panel -->
<Grid Grid.Row="1" RowDefinitions="Auto,32,*" Margin="8,0,4,8" IsVisible="{Binding IsSearching}" PropertyChanged="OnSearchCommitPanelPropertyChanged"> <Grid Grid.Row="1" RowDefinitions="Auto,32,*" Margin="8,0,4,8" IsVisible="{Binding IsSearching}" PropertyChanged="OnSearchCommitPanelPropertyChanged">
@ -794,14 +701,14 @@
<Border.IsVisible> <Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectedViewIndex" Converter="{x:Static c:IntConverters.IsZero}"/> <Binding Path="SelectedViewIndex" Converter="{x:Static c:IntConverters.IsZero}"/>
<Binding Path="Filters.Count" Converter="{x:Static c:IntConverters.IsGreaterThanZero}"/> <Binding Path="Settings.Filters.Count" Converter="{x:Static c:IntConverters.IsGreaterThanZero}"/>
</MultiBinding> </MultiBinding>
</Border.IsVisible> </Border.IsVisible>
<Grid Height="28" ColumnDefinitions="Auto,*,Auto"> <Grid Height="28" ColumnDefinitions="Auto,*,Auto">
<TextBlock Grid.Column="0" Margin="8,0,0,0" Classes="info_label" Text="{DynamicResource Text.Repository.FilterCommitPrefix}"/> <TextBlock Grid.Column="0" Margin="8,0,0,0" Classes="info_label" Text="{DynamicResource Text.Repository.FilterCommitPrefix}"/>
<ItemsControl Grid.Column="1" Margin="8,0,0,0" ItemsSource="{Binding Filters}"> <ItemsControl Grid.Column="1" Margin="8,0,0,0" ItemsSource="{Binding Settings.Filters}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/> <VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>

View file

@ -1,12 +1,10 @@
using System; using System;
using System.Collections.Generic;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace SourceGit.Views namespace SourceGit.Views
{ {
@ -17,6 +15,12 @@ namespace SourceGit.Views
InitializeComponent(); InitializeComponent();
} }
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
UpdateLeftSidebarLayout();
}
private void OpenWithExternalTools(object sender, RoutedEventArgs e) private void OpenWithExternalTools(object sender, RoutedEventArgs e)
{ {
if (sender is Button button && DataContext is ViewModels.Repository repo) if (sender is Button button && DataContext is ViewModels.Repository repo)
@ -49,24 +53,23 @@ namespace SourceGit.Views
e.Handled = true; 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) }; 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; e.Handled = true;
} }
} }
private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{ {
var grid = sender as Grid; if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true})
if (e.Property == IsVisibleProperty && grid.IsVisible)
txtSearchCommitsBox.Focus(); txtSearchCommitsBox.Focus();
} }
private void OnSearchKeyDown(object sender, KeyEventArgs e) private void OnSearchKeyDown(object _, KeyEventArgs e)
{ {
if (e.Key == Key.Enter) if (e.Key == Key.Enter)
{ {
@ -79,190 +82,39 @@ namespace SourceGit.Views
private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) 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; 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) UpdateLeftSidebarLayout();
e.Handled = true;
}
private void OnLocalBranchTreeSelectionChanged(object _1, RoutedEventArgs _2)
{ {
remoteBranchTree.UnselectAll(); remoteBranchTree.UnselectAll();
tagsList.SelectedItem = null; 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);
}
}
} }
private void OnRemoteBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) private void OnRemoteBranchTreeSelectionChanged(object _1, RoutedEventArgs _2)
{
if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo)
{ {
localBranchTree.UnselectAll(); localBranchTree.UnselectAll();
tagsList.SelectedItem = null; 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 OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs _)
{ {
remoteBranchTree.UnselectAll(); if (sender is DataGrid { SelectedItem: Models.Tag tag })
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<Models.Branch>();
foreach (var item in tree.SelectedItems)
CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode);
if (branches.Count == 1)
{
var item = (e.Source as Control)?.FindAncestorOfType<TreeViewItem>(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)
{
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<TreeViewItem>(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<Models.Branch>();
foreach (var item in tree.SelectedItems)
CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode);
if (branches.Count == 1)
{
var item = (e.Source as Control)?.FindAncestorOfType<TreeViewItem>(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)
{
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)
{ {
localBranchTree.UnselectAll(); localBranchTree.UnselectAll();
remoteBranchTree.UnselectAll(); remoteBranchTree.UnselectAll();
var tag = datagrid.SelectedItem as Models.Tag;
if (DataContext is ViewModels.Repository repo) if (DataContext is ViewModels.Repository repo)
repo.NavigateToCommit(tag.SHA); repo.NavigateToCommit(tag.SHA);
} }
@ -280,37 +132,11 @@ namespace SourceGit.Views
e.Handled = true; 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) repo.UpdateFilter(tag.Name, toggle.IsChecked == true);
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);
}
} }
e.Handled = true; e.Handled = true;
@ -330,10 +156,10 @@ namespace SourceGit.Views
private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e) 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; var submodule = grid.SelectedItem as string;
(DataContext as ViewModels.Repository).OpenSubmodule(submodule); repo.OpenSubmodule(submodule);
} }
e.Handled = true; e.Handled = true;
@ -341,11 +167,11 @@ namespace SourceGit.Views
private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e) 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); var menu = repo.CreateContextMenuForWorktree(worktree);
datagrid.OpenContextMenu(menu); grid.OpenContextMenu(menu);
} }
e.Handled = true; e.Handled = true;
@ -353,31 +179,143 @@ namespace SourceGit.Views
private void OnDoubleTappedWorktree(object sender, TappedEventArgs e) 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; var worktree = grid.SelectedItem as Models.Worktree;
(DataContext as ViewModels.Repository).OpenWorktree(worktree); repo.OpenWorktree(worktree);
} }
e.Handled = true; e.Handled = true;
} }
private void CollectBranchesFromNode(List<Models.Branch> 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; 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) var height = desiredTag;
CollectBranchesFromNode(outs, child); 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 else
{ {
var b = node.Backend as Models.Branch; localBranchTree.Height = half;
if (b != null && !outs.Contains(b)) remoteBranchTree.Height = half;
outs.Add(b); }
} }
else
{
localBranchTree.Height = leftHeight;
}
}
else if (vm.IsRemoteGroupExpanded)
{
remoteBranchTree.Height = leftHeight;
}
}
else
{
if (vm.IsLocalBranchGroupExpanded)
{
var height = localBranchRows * 24;
localBranchTree.Height = height;
}
if (vm.IsRemoteGroupExpanded)
{
var height = remoteBranchRows * 24;
remoteBranchTree.Height = height;
}
}
leftSidebarGroups.InvalidateMeasure();
} }
} }
} }

View file

@ -9,8 +9,6 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Styling;
using AvaloniaEdit; using AvaloniaEdit;
using AvaloniaEdit.Document; using AvaloniaEdit.Document;

View file

@ -429,7 +429,18 @@ namespace SourceGit.Views
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
foreach (var line in textDiff.Lines) foreach (var line in textDiff.Lines)
{
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); builder.AppendLine(line.Content);
}
}
Text = builder.ToString(); Text = builder.ToString();
} }
@ -718,7 +729,18 @@ namespace SourceGit.Views
var builder = new StringBuilder(); var builder = new StringBuilder();
var lines = IsOld ? diff.Old : diff.New; var lines = IsOld ? diff.Old : diff.New;
foreach (var line in lines) foreach (var line in lines)
{
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); builder.AppendLine(line.Content);
}
}
Text = builder.ToString(); Text = builder.ToString();
} }

View file

@ -253,11 +253,10 @@ namespace SourceGit.Views
return; return;
} }
var gitDir = new Commands.QueryGitDir(root).Result();
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
var repo = ViewModels.Preference.AddRepository(root, gitDir); var normalizedPath = root.Replace("\\", "/");
var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, parent, true); var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(normalizedPath, parent, true);
launcher.OpenRepositoryInTab(node, page); launcher.OpenRepositoryInTab(node, page);
}); });
}); });

View file

@ -174,7 +174,7 @@
<v:CommitMessageTextBox Grid.Row="2" Text="{Binding CommitMessage, Mode=TwoWay}"/> <v:CommitMessageTextBox Grid.Row="2" Text="{Binding CommitMessage, Mode=TwoWay}"/>
<!-- Commit Options --> <!-- Commit Options -->
<Grid Grid.Row="3" Margin="0,6,0,0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto"> <Grid Grid.Row="3" Margin="0,6,0,0" ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto,Auto">
<Button Grid.Column="0" <Button Grid.Column="0"
Classes="icon_button" Classes="icon_button"
Width="14" Height="14" Width="14" Height="14"
@ -184,15 +184,23 @@
</Button> </Button>
<CheckBox Grid.Column="1" <CheckBox Grid.Column="1"
Height="24"
Margin="12,0,0,0"
HorizontalAlignment="Left"
IsChecked="{Binding AutoStageBeforeCommit, Mode=TwoWay}"
Content="{DynamicResource Text.WorkingCopy.AutoStage}"
ToolTip.Tip="{DynamicResource Text.WorkingCopy.AutoStage.Tip}"/>
<CheckBox Grid.Column="2"
Height="24" Height="24"
Margin="12,0,0,0" Margin="12,0,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
IsChecked="{Binding UseAmend, Mode=TwoWay}" IsChecked="{Binding UseAmend, Mode=TwoWay}"
Content="{DynamicResource Text.WorkingCopy.Amend}"/> Content="{DynamicResource Text.WorkingCopy.Amend}"/>
<v:LoadingIcon Grid.Column="3" Width="18" Height="18" IsVisible="{Binding IsCommitting}"/> <v:LoadingIcon Grid.Column="4" Width="18" Height="18" IsVisible="{Binding IsCommitting}"/>
<Button Grid.Column="4" <Button Grid.Column="5"
Classes="flat primary" Classes="flat primary"
Content="{DynamicResource Text.WorkingCopy.Commit}" Content="{DynamicResource Text.WorkingCopy.Commit}"
Height="28" Height="28"
@ -202,7 +210,7 @@
HotKey="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}" HotKey="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}"
ToolTip.Tip="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}"/> ToolTip.Tip="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}"/>
<Button Grid.Column="5" <Button Grid.Column="6"
Classes="flat" Classes="flat"
Content="{DynamicResource Text.WorkingCopy.CommitAndPush}" Content="{DynamicResource Text.WorkingCopy.CommitAndPush}"
Height="28" Height="28"