diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e462a55..87d460c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,117 +1,45 @@ name: Continuous Integration on: push: - branches: - - develop + branches: [develop] pull_request: branches: [develop] workflow_dispatch: + workflow_call: jobs: - build-windows: - name: Build Windows x64 - runs-on: windows-2019 + build: + strategy: + matrix: + include: + - name : Windows x64 + os: windows-2019 + runtime: win-x64 + - name : Windows ARM64 + os: windows-2019 + runtime: win-arm64 + - name : macOS (Intel) + os: macos-13 + runtime: osx-x64 + - name : macOS (Apple Silicon) + os: macos-latest + runtime: osx-arm64 + - name : Linux + os: ubuntu-20.04 + runtime: linux-x64 + - name : Linux (arm64) + os: ubuntu-20.04 + runtime: linux-arm64 + name: Build ${{ matrix.name }} + runs-on: ${{ matrix.os }} steps: - name: Checkout sources uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Build - run: dotnet build -c Release - - name: Publish - run: dotnet publish src/SourceGit.csproj -c Release -o publish -r win-x64 - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: sourcegit.win-x64 - path: publish - build-macos-intel: - name: Build macOS (Intel) - runs-on: macos-13 - steps: - - name: Checkout sources - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Build - run: dotnet build -c Release - - name: Publish - run: dotnet publish src/SourceGit.csproj -c Release -o publish -r osx-x64 - - name: Packing Program - run: tar -cvf sourcegit.osx-x64.tar -C publish/ . - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: sourcegit.osx-x64 - path: sourcegit.osx-x64.tar - build-macos-arm64: - name: Build macOS (Apple Silicon) - runs-on: macos-latest - steps: - - name: Checkout sources - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Build - run: dotnet build -c Release - - name: Publish - run: dotnet publish src/SourceGit.csproj -c Release -o publish -r osx-arm64 - - name: Packing Program - run: tar -cvf sourcegit.osx-arm64.tar -C publish/ . - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: sourcegit.osx-arm64 - path: sourcegit.osx-arm64.tar - build-linux: - name: Build Linux - runs-on: ubuntu-20.04 - steps: - - name: Checkout sources - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Build - run: dotnet build -c Release - - name: Publish - run: dotnet publish src/SourceGit.csproj -c Release -o publish -r linux-x64 - - name: Rename Executable File - run: mv publish/SourceGit publish/sourcegit - - name: Packing Program - run: tar -cvf sourcegit.linux-x64.tar -C publish/ . - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: sourcegit.linux-x64 - path: sourcegit.linux-x64.tar - build-linux-arm64: - name: Build Linux (arm64) - runs-on: ubuntu-20.04 - steps: - - name: Checkout sources - uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Configure arm64 packages + if: ${{ matrix.runtime == 'linux-arm64' }} run: | sudo dpkg --add-architecture arm64 echo 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted @@ -121,19 +49,25 @@ jobs: sudo sed -i -e 's/^deb http/deb [arch=amd64] http/g' /etc/apt/sources.list sudo sed -i -e 's/^deb mirror/deb [arch=amd64] mirror/g' /etc/apt/sources.list - name: Install cross-compiling dependencies + if: ${{ matrix.runtime == 'linux-arm64' }} run: | sudo apt-get update sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64 - name: Build run: dotnet build -c Release - name: Publish - run: dotnet publish src/SourceGit.csproj -c Release -o publish -r linux-arm64 - - name: Rename Executable File + run: dotnet publish src/SourceGit.csproj -c Release -o publish -r ${{ matrix.runtime }} + - name: Rename executable file + if: ${{ startsWith(matrix.runtime, 'linux-') }} run: mv publish/SourceGit publish/sourcegit - - name: Packing Program - run: tar -cvf sourcegit.linux-arm64.tar -C publish/ . - - name: Upload Artifact + - name: Tar artifact + if: ${{ startsWith(matrix.runtime, 'linux-') }} + run: | + tar -cvf "sourcegit.${{ matrix.runtime }}.tar" -C publish . + rm -r publish/* + mv "sourcegit.${{ matrix.runtime }}.tar" publish + - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: sourcegit.linux-arm64 - path: sourcegit.linux-arm64.tar + name: sourcegit.${{ matrix.runtime }} + path: publish diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 00000000..8c0c192d --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,98 @@ +name: Package +on: + workflow_call: + inputs: + version: + description: Source Git package version + required: true + type: string +jobs: + build: + name: Build + uses: ./.github/workflows/ci.yml + windows-portable: + name: Package portable Windows app + needs: build + runs-on: ubuntu-latest + strategy: + matrix: + runtime: [win-x64, win-arm64] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Download build + uses: actions/download-artifact@v4 + with: + name: sourcegit.${{ matrix.runtime }} + path: build/SourceGit + - name: Package + env: + VERSION: ${{ inputs.version }} + RUNTIME: ${{ matrix.runtime }} + run: ./build/scripts/package.windows-portable.sh + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: package.${{ matrix.runtime }} + path: build/sourcegit_*.zip + osx-app: + name: Package OSX app + needs: build + runs-on: ubuntu-latest + strategy: + matrix: + runtime: [osx-x64, osx-arm64] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Download build + uses: actions/download-artifact@v4 + with: + name: sourcegit.${{ matrix.runtime }} + path: build/SourceGit + - name: Package + env: + VERSION: ${{ inputs.version }} + RUNTIME: ${{ matrix.runtime }} + run: ./build/scripts/package.osx-app.sh + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: package.${{ matrix.runtime }} + path: build/sourcegit_*.zip + linux: + name: Package Linux + needs: build + runs-on: ubuntu-latest + strategy: + matrix: + runtime: [linux-x64, linux-arm64] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Download package dependencies + run: | + sudo add-apt-repository universe + sudo apt-get update + sudo apt-get install desktop-file-utils rpm libfuse2 + - name: Download build + uses: actions/download-artifact@v4 + with: + name: sourcegit.${{ matrix.runtime }} + path: build + - name: Package + env: + VERSION: ${{ inputs.version }} + RUNTIME: ${{ matrix.runtime }} + run: | + mkdir build/SourceGit + tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit + ./build/scripts/package.linux.sh + - name: Upload package artifacts + uses: actions/upload-artifact@v4 + with: + name: package.${{ matrix.runtime }} + path: | + build/sourcegit-*.AppImage + build/sourcegit_*.deb + build/sourcegit-*.rpm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..51e247d2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +name: Release +on: + push: + tags: + - v* +jobs: + version: + name: Prepare version string + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Output version string + id: version + env: + TAG: ${{ github.ref_name }} + run: echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + package: + needs: version + name: Package + uses: ./.github/workflows/package.yml + with: + version: ${{ needs.version.outputs.version }} + release: + needs: [version, package] + name: Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Create release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: gh release create "$TAG" -t "Release ${TAG#v}" --notes-from-tag + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: package.* + path: packages + merge-multiple: true + - name: Upload assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + VERSION: ${{ needs.version.outputs.version }} + run: gh release upload "$TAG" packages/* diff --git a/README.md b/README.md index 0af00613..50d75259 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Opensource Git GUI client. * Git LFS * Issue Link +> [!WARNING] > **Linux** only tested on **Debian 12** on both **X11** & **Wayland**. ## How to Use @@ -45,11 +46,14 @@ You can download the latest stable from [Releases](https://github.com/sourcegit- This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs. -| OS | PATH | -|---------|-------------------------------------------------| -| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` | -| Linux | `${HOME}/.config/SourceGit` | -| macOS | `${HOME}/Library/Application Support/SourceGit` | +| OS | PATH | +|---------|-----------------------------------------------------| +| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` | +| Linux | `${HOME}/.config/SourceGit` or `${HOME}/.sourcegit` | +| macOS | `${HOME}/Library/Application Support/SourceGit` | + +> [!TIP] +> You can open the app data dir from the main menu. For **Windows** users: @@ -58,7 +62,8 @@ For **Windows** users: ```shell winget install SourceGit ``` - > `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. +> [!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. * You can install the latest stable by `scoope` with follow commands: ```shell scoop bucket add extras @@ -84,17 +89,27 @@ For **Linux** users: This app supports open repository in external tools listed in the table below. -| Tool | Windows | macOS | Linux | Environment Variable | -|-------------------------------|---------|-------|-------|----------------------| -| Visual Studio Code | YES | YES | YES | VSCODE_PATH | -| Visual Studio Code - Insiders | YES | YES | YES | VSCODE_INSIDERS_PATH | -| VSCodium | YES | YES | YES | VSCODIUM_PATH | -| JetBrains Fleet | YES | YES | YES | FLEET_PATH | -| Sublime Text | YES | YES | YES | SUBLIME_TEXT_PATH | +| Tool | Windows | macOS | Linux | KEY IN `external_editors.json` | +|-------------------------------|---------|-------|-------|--------------------------------| +| Visual Studio Code | YES | YES | YES | VSCODE | +| Visual Studio Code - Insiders | YES | YES | YES | VSCODE_INSIDERS | +| VSCodium | YES | YES | YES | VSCODIUM | +| JetBrains Fleet | YES | YES | YES | FLEET | +| Sublime Text | YES | YES | YES | SUBLIME_TEXT | -* You can set the given environment variable for special tool if it can NOT be found by this app automatically. -* Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device. -* On macOS, you may need to use `launchctl setenv` to make sure the app can read these environment variables. +> [!NOTE] +> This app will try to find those tools based on some pre-defined or expected locations automatically. If you are using one portable version of these tools, it will not be detected by this app. +> To solve this problem you can add a file named `external_editors.json` in app data dir and provide the path directly. For example: +```json +{ + "tools": { + "VSCODE": "D:\\VSCode\\Code.exe" + } +} +``` + +> [!NOTE] +> This app also supports a lot of `JetBrains` IDEs, installing `JetBrains Toolbox` will help this app to find them. ## Screenshots diff --git a/SourceGit.sln b/SourceGit.sln index ee35d8f6..06bd75b3 100644 --- a/SourceGit.sln +++ b/SourceGit.sln @@ -6,11 +6,6 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGit", "src\SourceGit.csproj", "{2091C34D-4A17-4375-BEF3-4D60BE8113E4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{773082AC-D9C8-4186-8521-4B6A7BEE6158}" - ProjectSection(SolutionItems) = preProject - build\build.linux.sh = build\build.linux.sh - build\build.osx.command = build\build.osx.command - build\build.windows.ps1 = build\build.windows.ps1 - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{FD384607-ED99-47B7-AF31-FB245841BC92}" EndProject @@ -19,6 +14,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{67B6D05F-A000-40BA-ADB4-C9065F880D7B}" ProjectSection(SolutionItems) = preProject .github\workflows\ci.yml = .github\workflows\ci.yml + .github\workflows\package.yml = .github\workflows\package.yml + .github\workflows\release.yml = .github\workflows\release.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}" @@ -77,13 +74,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SPECS", "SPECS", "{7802CD7A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "appimage", "appimage", "{5D125DD9-B48A-491F-B2FB-D7830D74C4DC}" ProjectSection(SolutionItems) = preProject - build\resources\appimage\publish-appimage = build\resources\appimage\publish-appimage - build\resources\appimage\publish-appimage.conf = build\resources\appimage\publish-appimage.conf - build\resources\appimage\runtime-x86_64 = build\resources\appimage\runtime-x86_64 build\resources\appimage\sourcegit.appdata.xml = build\resources\appimage\sourcegit.appdata.xml build\resources\appimage\sourcegit.png = build\resources\appimage\sourcegit.png EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C54D4001-9940-477C-A0B6-E795ED0A3209}" + ProjectSection(SolutionItems) = preProject + build\scripts\package.linux.sh = build\scripts\package.linux.sh + build\scripts\package.osx-app.sh = build\scripts\package.osx-app.sh + build\scripts\package.windows-portable.sh = build\scripts\package.windows-portable.sh + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,6 +115,7 @@ Global {9BA0B044-0CC9-46F8-B551-204F149BF45D} = {FD384607-ED99-47B7-AF31-FB245841BC92} {7802CD7A-591B-4EDD-96F8-9BF3F61692E4} = {9BA0B044-0CC9-46F8-B551-204F149BF45D} {5D125DD9-B48A-491F-B2FB-D7830D74C4DC} = {FD384607-ED99-47B7-AF31-FB245841BC92} + {C54D4001-9940-477C-A0B6-E795ED0A3209} = {773082AC-D9C8-4186-8521-4B6A7BEE6158} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7FF1B9C6-B5BF-4A50-949F-4B407A0E31C9} diff --git a/VERSION b/VERSION index 68697155..21783978 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.25 \ No newline at end of file +8.26 \ No newline at end of file diff --git a/build/build.linux.sh b/build/build.linux.sh deleted file mode 100755 index 55bc2f62..00000000 --- a/build/build.linux.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh - -version=`cat ../VERSION` - -# Cleanup -rm -rf SourceGit *.tar.gz resources/deb/opt *.deb *.rpm *.AppImage - -# Generic AppImage -cd resources/appimage -./publish-appimage -y -o sourcegit-${version}.linux.x86_64.AppImage - -# Move to build dir -mv AppImages/sourcegit-${version}.linux.x86_64.AppImage ../../ -mv AppImages/AppDir/usr/bin ../../SourceGit -cd ../../ - -# Debain/Ubuntu package -mkdir -p resources/deb/opt/sourcegit/ -mkdir -p resources/deb/usr/share/applications -mkdir -p resources/deb/usr/share/icons -cp -f SourceGit/* resources/deb/opt/sourcegit/ -cp -r resources/_common/applications resources/deb/usr/share/ -cp -r resources/_common/icons resources/deb/usr/share/ -chmod +x -R resources/deb/opt/sourcegit -sed -i "2s/.*/Version: ${version}/g" resources/deb/DEBIAN/control -dpkg-deb --build resources/deb ./sourcegit_${version}-1_amd64.deb - -# Redhat/CentOS/Fedora package -rpmbuild -bb --target=x86_64 resources/rpm/SPECS/build.spec --define "_topdir `pwd`/resources/rpm" --define "_version ${version}" -mv resources/rpm/RPMS/x86_64/sourcegit-${version}-1.x86_64.rpm . - -rm -rf SourceGit diff --git a/build/build.osx.command b/build/build.osx.command deleted file mode 100755 index f60c87b7..00000000 --- a/build/build.osx.command +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -version=`cat ../VERSION` - -rm -rf SourceGit.app *.zip - -mkdir -p SourceGit.app/Contents/Resources -cp resources/app/App.icns SourceGit.app/Contents/Resources/App.icns -sed "s/SOURCE_GIT_VERSION/${version}/g" resources/app/App.plist > SourceGit.app/Contents/Info.plist - -mkdir -p SourceGit.app/Contents/MacOS -dotnet publish ../src/SourceGit.csproj -c Release -r osx-arm64 -o SourceGit.app/Contents/MacOS -zip sourcegit_${version}.osx-arm64.zip -r SourceGit.app -x "*/*\.dsym/*" - -rm -rf SourceGit.app/Contents/MacOS - -mkdir -p SourceGit.app/Contents/MacOS -dotnet publish ../src/SourceGit.csproj -c Release -r osx-x64 -o SourceGit.app/Contents/MacOS -zip sourcegit_${version}.osx-x64.zip -r SourceGit.app -x "*/*\.dsym/*" - -rm -rf SourceGit.app diff --git a/build/build.windows.ps1 b/build/build.windows.ps1 deleted file mode 100644 index 23735e4c..00000000 --- a/build/build.windows.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -$version = Get-Content ..\VERSION - -if (Test-Path SourceGit) { - Remove-Item SourceGit -Recurse -Force -} - -Remove-Item *.zip -Force - -dotnet publish ..\src\SourceGit.csproj -c Release -r win-arm64 -o SourceGit - -Remove-Item SourceGit\*.pdb -Force - -Compress-Archive -Path SourceGit -DestinationPath "sourcegit_$version.win-arm64.zip" - -if (Test-Path SourceGit) { - Remove-Item SourceGit -Recurse -Force -} - -dotnet publish ..\src\SourceGit.csproj -c Release -r win-x64 -o SourceGit - -Remove-Item SourceGit\*.pdb -Force - -Compress-Archive -Path SourceGit -DestinationPath "sourcegit_$version.win-x64.zip" diff --git a/build/resources/appimage/publish-appimage b/build/resources/appimage/publish-appimage deleted file mode 100755 index b8010187..00000000 --- a/build/resources/appimage/publish-appimage +++ /dev/null @@ -1,708 +0,0 @@ -#!/bin/bash -################################################################################ -# PROJECT : Publish-AppImage for .NET -# WEBPAGE : https://github.com/kuiperzone/Publish-AppImage -# COPYRIGHT : Andy Thomas 2021-2023 -# LICENSE : MIT -################################################################################ - -############################### -# CONSTANTS -############################### - -declare -r _SCRIPT_VERSION="1.3.1" -declare -r _SCRIPT_TITLE="Publish-AppImage for .NET" -declare -r _SCRIPT_IMPL_MIN=1 -declare -r _SCRIPT_IMPL_MAX=1 -declare -r _SCRIPT_COPYRIGHT="Copyright 2023 Andy Thomas" -declare -r _SCRIPT_WEBSITE="https://github.com/kuiperzone/Publish-AppImage" - -declare -r _SCRIPT_NAME="publish-appimage" -declare -r _DEFAULT_CONF="${_SCRIPT_NAME}.conf" - -declare -r _APPIMAGE_KIND="appimage" -declare -r _ZIP_KIND="zip" -declare -r _DOTNET_NONE="null" - - -############################### -# FUNCTIONS -############################### - -function assert_result -{ - local _ret=$? - - if [ ${_ret} -ne 0 ]; then - echo - exit ${_ret} - fi -} - -function exec_or_die -{ - echo "${1}" - eval "${1}" - assert_result -} - -function ensure_directory -{ - local _path="${1}" - - if [ ! -d "${_path}" ]; then - mkdir -p "${_path}" - assert_result - fi -} - -function remove_path -{ - local _path="${1}" - - if [ -d "${_path}" ]; then - rm -rf "${_path}" - assert_result - elif [ -f "${_path}" ]; then - rm -f "${_path}" - assert_result - fi -} - -function assert_mandatory -{ - local _name="${1}" - local _value="${2}" - - if [ "${_value}" == "" ]; then - echo "${_name} undefined in: ${_conf_arg_value}" - echo - exit 1 - fi -} - -function assert_opt_file -{ - local _name="${1}" - local _value="${2}" - - if [ "${_value}" != "" ] && [ ! -f "${_value}" ]; then - echo "File not found: ${_value}" - - if [ "${_name}" != "" ]; then - echo "See ${_name} in: ${_conf_arg_value}" - fi - - echo - exit 1 - fi -} - -############################### -# HANDLE ARGUMENTS -############################### - -# Specify conf file -declare -r _CONF_ARG="f" -declare -r _CONF_ARG_NAME="conf" -_conf_arg_value="${_DEFAULT_CONF}" -_arg_syntax=":${_CONF_ARG}:" - -# Runtime ID -declare -r _RID_ARG="r" -declare -r _RID_ARG_NAME="runtime" -_rid_arg_value="linux-x64" -_arg_syntax="${_arg_syntax}${_RID_ARG}:" - -# Package kind -declare -r _KIND_ARG="k" -declare -r _KIND_ARG_NAME="kind" -declare -l _kind_arg_value="${_APPIMAGE_KIND}" -_arg_syntax="${_arg_syntax}${_KIND_ARG}:" - -# Run app -declare -r _RUNAPP_ARG="u" -declare -r _RUNAPP_ARG_NAME="run" -_runapp_arg_value=false -_arg_syntax="${_arg_syntax}${_RUNAPP_ARG}" - -# Verbose -declare -r _VERBOSE_ARG="b" -declare -r _VERBOSE_ARG_NAME="verbose" -_verbose_arg_value=false -_arg_syntax="${_arg_syntax}${_VERBOSE_ARG}" - -# Skip yes (no prompt) -declare -r _SKIPYES_ARG="y" -declare -r _SKIPYES_ARG_NAME="skip-yes" -_skipyes_arg_value=false -_arg_syntax="${_arg_syntax}${_SKIPYES_ARG}" - -# Output name -declare -r _OUTPUT_ARG="o" -declare -r _OUTPUT_ARG_NAME="output" -_output_arg_value="" -_arg_syntax="${_arg_syntax}${_OUTPUT_ARG}:" - -# Show version -declare -r _VERSION_ARG="v" -declare -r _VERSION_ARG_NAME="version" -_version_arg_value=false -_arg_syntax="${_arg_syntax}${_VERSION_ARG}" - -# Show help -declare -r _HELP_ARG="h" -declare -r _HELP_ARG_NAME="help" -_help_arg_value=false -_arg_syntax="${_arg_syntax}${_HELP_ARG}" - -_exit_help=0 - -# Transform long options to short ones -for arg in "${@}"; do - shift - case "${arg}" in - ("--${_CONF_ARG_NAME}") set -- "$@" "-${_CONF_ARG}" ;; - ("--${_RID_ARG_NAME}") set -- "$@" "-${_RID_ARG}" ;; - ("--${_KIND_ARG_NAME}") set -- "$@" "-${_KIND_ARG}" ;; - ("--${_RUNAPP_NAME}") set -- "$@" "-${_RUNAPP_ARG}" ;; - ("--${_VERBOSE_ARG_NAME}") set -- "$@" "-${_VERBOSE_ARG}" ;; - ("--${_SKIPYES_ARG_NAME}") set -- "$@" "-${_SKIPYES_ARG}" ;; - ("--${_OUTPUT_ARG_NAME}") set -- "$@" "-${_OUTPUT_ARG}" ;; - ("--${_VERSION_ARG_NAME}") set -- "$@" "-${_VERSION_ARG}" ;; - ("--${_HELP_ARG_NAME}") set -- "$@" "-${_HELP_ARG}" ;; - ("--"*) - echo "Illegal argument: ${arg}" - echo - - _exit_help=1 - break - ;; - (*) set -- "$@" "${arg}" ;; - esac -done - -if [ ${_exit_help} == 0 ]; then - # Read arguments - while getopts ${_arg_syntax} arg; do - case "${arg}" in - (${_CONF_ARG}) _conf_arg_value="${OPTARG}" ;; - (${_RID_ARG}) _rid_arg_value="${OPTARG}" ;; - (${_KIND_ARG}) _kind_arg_value="${OPTARG}" ;; - (${_RUNAPP_ARG}) _runapp_arg_value=true ;; - (${_VERBOSE_ARG}) _verbose_arg_value=true ;; - (${_SKIPYES_ARG}) _skipyes_arg_value=true ;; - (${_OUTPUT_ARG}) _output_arg_value="${OPTARG}" ;; - (${_VERSION_ARG}) _version_arg_value=true ;; - (${_HELP_ARG}) _help_arg_value=true ;; - (*) - echo "Illegal argument" - echo - - _exit_help=1 - break - ;; - esac - done -fi - -# Handle and help and version -if [ ${_help_arg_value} == true ] || [ $_exit_help != 0 ]; then - - _indent=" " - echo "Usage:" - echo "${_indent}${_SCRIPT_NAME} [-flags] [-option-n value-n]" - echo - - echo "Help Options:" - echo "${_indent}-${_HELP_ARG}, --${_HELP_ARG_NAME}" - echo "${_indent}Show help information flag." - echo - echo "${_indent}-${_VERSION_ARG}, --${_VERSION_ARG_NAME}" - echo "${_indent}Show version and about information flag." - echo - - echo "Build Options:" - echo "${_indent}-${_CONF_ARG}, --${_CONF_ARG_NAME} value" - echo "${_indent}Specifies the conf file. Defaults to ${_SCRIPT_NAME}.conf." - echo - echo "${_indent}-${_RID_ARG}, --${_RID_ARG_NAME} value" - echo "${_indent}Dotnet publish runtime identifier. Valid examples include:" - echo "${_indent}linux-x64 and linux-arm64. Default is linux-x64 if unspecified." - echo "${_indent}See also: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog" - echo - echo "${_indent}-${_KIND_ARG}, --${_KIND_ARG_NAME} value" - echo "${_indent}Package output kind. Value must be one of: ${_APPIMAGE_KIND} or ${_ZIP_KIND}." - echo "${_indent}Default is ${_APPIMAGE_KIND} if unspecified." - echo - echo "${_indent}-${_VERBOSE_ARG}, --${_VERBOSE_ARG_NAME}" - echo "${_indent}Verbose review info output flag." - echo - echo "${_indent}-${_RUNAPP_ARG}, --${_RUNAPP_ARG_NAME}" - echo "${_indent}Run the application after successful build flag." - echo - echo "${_indent}-${_SKIPYES_ARG}, --${_SKIPYES_ARG_NAME}" - echo "${_indent}Skip confirmation prompt flag (assumes yes)." - echo - echo "${_indent}-${_OUTPUT_ARG}, --${_OUTPUT_ARG_NAME}" - echo "${_indent}Explicit final output filename (excluding directory part)." - echo - - echo "Example:" - echo "${_indent}${_SCRIPT_NAME} -${_RID_ARG} linux-arm64" - echo - - exit $_exit_help -fi - -if [ ${_version_arg_value} == true ]; then - echo - echo "${_SCRIPT_TITLE}, ${_SCRIPT_VERSION}" - echo "${_SCRIPT_COPYRIGHT}" - echo "${_SCRIPT_WEBSITE}" - echo - echo "MIT License" - echo - echo "Permission is hereby granted, free of charge, to any person obtaining a copy" - echo "of this software and associated documentation files (the "Software"), to deal" - echo "in the Software without restriction, including without limitation the rights" - echo "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell" - echo "copies of the Software, and to permit persons to whom the Software is" - echo "furnished to do so, subject to the following conditions:" - echo - echo "The above copyright notice and this permission notice shall be included in all" - echo "copies or substantial portions of the Software." - echo - echo "THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR" - echo "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY," - echo "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE" - echo "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER" - echo "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM," - echo "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE" - echo "SOFTWARE." - echo - - exit 0 -fi - - -############################### -# SOURCE & WORKING -############################### - -# Export these now as may be -# useful in an advanced config file -export DOTNET_RID="${_rid_arg_value}" -export PKG_KIND="${_kind_arg_value}" -export ISO_DATE=`date +"%Y-%m-%d"` - -if [ ! -f "${_conf_arg_value}" ]; then - echo "Configuration file not found: ${_conf_arg_value}" - echo - exit 1 -fi - -# Export contents to any post publish command -set -a - -# Source local to PWD -source "${_conf_arg_value}" -set +a - -# For AppImage tool and backward compatibility -export VERSION="${APP_VERSION}" - - -# Then change PWD to conf file -cd "$(dirname "${_conf_arg_value}")" - - -############################### -# SANITY -############################### - -if (( ${CONF_IMPL_VERSION} < ${_SCRIPT_IMPL_MIN} )) || (( ${CONF_IMPL_VERSION} > ${_SCRIPT_IMPL_MAX} )); then - echo "Configuration format version ${_SCRIPT_IMPL_VERSION} not compatible" - echo "Older conf file but newer ${_SCRIPT_NAME} implementation?" - echo "Update from: ${_SCRIPT_WEBSITE}" - echo - exit 1 -fi - -assert_mandatory "APP_MAIN" "${APP_MAIN}" -assert_mandatory "APP_ID" "${APP_ID}" -assert_mandatory "APP_ICON_SRC" "${APP_ICON_SRC}" -assert_mandatory "DE_NAME" "${DE_NAME}" -assert_mandatory "DE_CATEGORIES" "${DE_CATEGORIES}" -assert_mandatory "PKG_OUTPUT_DIR" "${PKG_OUTPUT_DIR}" - -if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then - assert_mandatory "APPIMAGETOOL_COMMAND" "${APPIMAGETOOL_COMMAND}" -fi - -assert_opt_file "APP_ICON_SRC" "${APP_ICON_SRC}" -assert_opt_file "APP_XML_SRC" "${APP_XML_SRC}" - -if [ "${DE_TERMINAL_FLAG}" != "true" ] && [ "${DE_TERMINAL_FLAG}" != "false" ]; then - echo "DE_TERMINAL_FLAG invalid value: ${DE_TERMINAL_FLAG}" - echo - exit 1 -fi - -if [ "${DOTNET_PROJECT_PATH}" == "${_DOTNET_NONE}" ] && [ "${POST_PUBLISH}" == "" ]; then - echo "No publish or build operation defined (nothing will be built)" - echo "See DOTNET_PROJECT_PATH and POST_PUBLISH in: ${_conf_arg_value}" - echo - exit 1 -fi - -if [ "${DOTNET_PROJECT_PATH}" != "" ] && [ "${DOTNET_PROJECT_PATH}" != "${_DOTNET_NONE}" ] && - [ ! -f "${DOTNET_PROJECT_PATH}" ] && [ ! -d "${DOTNET_PROJECT_PATH}" ]; then - echo "DOTNET_PROJECT_PATH path not found: ${DOTNET_PROJECT_PATH}" - echo - exit 1 -fi - -if [ "${_kind_arg_value}" != "${_APPIMAGE_KIND}" ] && [ "${_kind_arg_value}" != "${_ZIP_KIND}" ]; then - echo "Invalid argument value: ${_kind_arg_value}" - echo "Use one of: ${_APPIMAGE_KIND} or ${_ZIP_KIND}" - echo - exit 1 -fi - - -# Detect if publish for windows -_exec_ext="" -declare -l _tw="${_rid_arg_value}" - -if [[ "${_tw}" == "win"* ]]; then - - # May use this in future - _exec_ext=".exe" - - if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then - echo "Invalid AppImage payload" - echo "Looks like a windows binary to be packaged as AppImage." - echo "Use --${_KIND_ARG_NAME} ${_ZIP_KIND} instead." - echo - exit 1 - fi -fi - - -############################### -# VARIABLES -############################### - -# Abbreviate RID where it maps well to arch -if [ "${_rid_arg_value}" == "linux-x64" ]; then - _file_out_arch="-x86_64" -elif [ "${_rid_arg_value}" == "linux-arm64" ]; then - _file_out_arch="-aarch64" -else - # Otherwise use RID itself - _file_out_arch="-${_rid_arg_value}" -fi - -# APPDIR LOCATIONS -export APPDIR_ROOT="${PKG_OUTPUT_DIR}/AppDir" - -if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then - # AppImage - export APPDIR_USR="${APPDIR_ROOT}/usr" - export APPDIR_BIN="${APPDIR_ROOT}/usr/bin" - export APPDIR_SHARE="${APPDIR_ROOT}/usr/share" - - _local_run="usr/bin/${APP_MAIN}${_exec_ext}" -else - # Simple zip - export APPDIR_USR="" - export APPDIR_BIN="${APPDIR_ROOT}" - export APPDIR_SHARE="${APPDIR_ROOT}" - - _local_run="${APP_MAIN}${_exec_ext}" -fi - -export APPRUN_TARGET="${APPDIR_BIN}/${APP_MAIN}${_exec_ext}" - - -# DOTNET PUBLISH -if [ "${DOTNET_PROJECT_PATH}" != "${_DOTNET_NONE}" ]; then - - _publish_cmd="dotnet publish" - - if [ "${DOTNET_PROJECT_PATH}" != "" ] && [ "${DOTNET_PROJECT_PATH}" != "." ]; then - _publish_cmd="${_publish_cmd} \"${DOTNET_PROJECT_PATH}\"" - fi - - _publish_cmd="${_publish_cmd} -r ${_rid_arg_value}" - - if [ "${APP_VERSION}" != "" ]; then - _publish_cmd="${_publish_cmd} -p:Version=${APP_VERSION}" - fi - - if [ "${DOTNET_PUBLISH_ARGS}" != "" ]; then - _publish_cmd="${_publish_cmd} ${DOTNET_PUBLISH_ARGS}" - fi - - _publish_cmd="${_publish_cmd} -o \"${APPDIR_BIN}\"" - -fi - - -# PACKAGE OUTPUT -if [ $PKG_VERSION_FLAG == true ] && [ "${APP_VERSION}" != "" ]; then - _version_out="-${APP_VERSION}" -fi - -if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then - - # AppImageTool - if [ "${_output_arg_value}" != "" ]; then - _package_out="${PKG_OUTPUT_DIR}/${_output_arg_value}" - else - _package_out="${PKG_OUTPUT_DIR}/${APP_MAIN}${_version_out}${_file_out_arch}${PKG_APPIMAGE_SUFFIX}" - fi - - _package_cmd="${APPIMAGETOOL_COMMAND}" - - if [ "${PKG_APPIMAGE_ARGS}" != "" ]; then - _package_cmd="${_package_cmd} ${PKG_APPIMAGE_ARGS}" - fi - - _package_cmd="${_package_cmd} \"${APPDIR_ROOT}\" \"${_package_out}\"" - - if [ ${_runapp_arg_value} == true ]; then - _packrun_cmd="${_package_out}" - fi - -else - - # Simple zip - if [ "${_output_arg_value}" != "" ]; then - _package_out="${PKG_OUTPUT_DIR}/${_output_arg_value}" - else - _package_out="${PKG_OUTPUT_DIR}/${APP_MAIN}${_version_out}${_file_out_arch}.zip" - fi - - _package_cmd="(cd \"${APPDIR_ROOT}\" && zip -r \"${PWD}/${_package_out}\" ./)" - - if [ ${_runapp_arg_value} == true ]; then - _packrun_cmd="${APPRUN_TARGET}" - fi - -fi - - -############################### -# DESKTOP ENTRY & APPDATA -############################### - -if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then - - _desktop="[Desktop Entry]\n" - _desktop="${_desktop}Type=Application\n" - _desktop="${_desktop}Name=${DE_NAME}\n" - _desktop="${_desktop}Exec=AppRun\n" - _desktop="${_desktop}Terminal=${DE_TERMINAL_FLAG}\n" - _desktop="${_desktop}Categories=${DE_CATEGORIES}\n" - - # Follow app-id - _desktop="${_desktop}Icon=${APP_ID}\n" - - if [ "${DE_COMMENT}" != "" ]; then - _desktop="${_desktop}Comment=${DE_COMMENT}\n" - fi - - if [ "${DE_KEYWORDS}" != "" ]; then - _desktop="${_desktop}Keywords=${DE_KEYWORDS}\n" - fi - - _desktop="${_desktop}${DE_EXTEND}\n" -fi - - -# Load appdata.xml -if [ "${APP_XML_SRC}" != "" ]; then - - if command -v envsubst &> /dev/null; then - _appxml=$(envsubst <"${APP_XML_SRC}") - else - _appxml=$(<"${APP_XML_SRC}") - echo "WARNING: Variable substitution not available for: ${APP_XML_SRC}" - echo - fi - -fi - - -############################### -# DISPLAY & CONFIRM -############################### - -echo "${_SCRIPT_TITLE}, ${_SCRIPT_VERSION}" -echo "${_SCRIPT_COPYRIGHT}" -echo - -echo "APP_MAIN: ${APP_MAIN}" -echo "APP_ID: ${APP_ID}" -echo "APP_VERSION: ${APP_VERSION}" -echo "OUTPUT: ${_package_out}" -echo - -if [ "${_desktop}" != "" ]; then - echo -e "${_desktop}" -fi - -if [ ${_verbose_arg_value} == true ] && [ "${_appxml}" != "" ]; then - echo -e "${_appxml}\n" -fi - -echo "Build Commands:" - -if [ "${_publish_cmd}" != "" ]; then - echo - echo "${_publish_cmd}" -fi - -if [ "${POST_PUBLISH}" != "" ]; then - echo - echo "${POST_PUBLISH}" -fi - -echo -echo "${_package_cmd}" -echo - -# Prompt -if [ $_skipyes_arg_value == false ]; then - - echo - read -p "Build now [N/y]? " prompt - - if [ "${prompt}" != "y" ] && [ "${prompt}" != "Y" ]; then - echo - exit 1 - fi - - # Continue - echo -fi - - -############################### -# PUBLISH & BUILD -############################### - -# Clean and ensure directoy exists -ensure_directory "${PKG_OUTPUT_DIR}" -remove_path "${APPDIR_ROOT}" -remove_path "${_package_out}" - -# Create AppDir structure -ensure_directory "${APPDIR_BIN}" - -if [ "${_kind_arg_value}" != "${_ZIP_KIND}" ]; then - # We also create usr/share/icons, as some packages require this. - # See: https://github.com/kuiperzone/Publish-AppImage/issues/7 - ensure_directory "${APPDIR_SHARE}/icons" -fi - -echo - -# Publish dotnet -if [ "${_publish_cmd}" != "" ]; then - exec_or_die "${_publish_cmd}" - echo -fi - -# Post-publish -if [ "${POST_PUBLISH}" != "" ]; then - - exec_or_die "${POST_PUBLISH}" - echo - -fi - -# Application file must exist! -if [ ! -f "${APPRUN_TARGET}" ]; then - echo "Expected application file not found: ${APPRUN_TARGET}" - echo - exit 1 -fi - -if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then - - echo - - # Create desktop - if [ "${_desktop}" != "" ]; then - _file="${APPDIR_ROOT}/${APP_ID}.desktop" - echo "Creating: ${_file}" - echo -e "${_desktop}" > "${_file}" - assert_result - fi - - if [ "${_appxml}" != "" ]; then - _dir="${APPDIR_SHARE}/metainfo" - _file="${_dir}/${APP_ID}.appdata.xml" - echo "Creating: ${_file}" - ensure_directory "${_dir}" - echo -e "${_appxml}" > "${_file}" - assert_result - - if [ "${_desktop}" != "" ]; then - # Copy of desktop under "applications" - # Needed for launchable in appinfo.xml (if used) - # See https://github.com/AppImage/AppImageKit/issues/603 - _dir="${APPDIR_SHARE}/applications" - _file="${_dir}/${APP_ID}.desktop" - echo "Creating: ${_file}" - ensure_directory "${_dir}" - echo -e "${_desktop}" > "${_file}" - assert_result - fi - fi - - # Copy icon - if [ "${APP_ICON_SRC}" != "" ]; then - - _icon_ext="${APP_ICON_SRC##*.}" - - if [ "${_icon_ext}" != "" ]; then - _icon_ext=".${_icon_ext}" - fi - - _temp="${APPDIR_ROOT}/${APP_ID}${_icon_ext}" - echo "Creating: ${_temp}" - - cp "${APP_ICON_SRC}" "${_temp}" - assert_result - fi - - # AppRun - _temp="${APPDIR_ROOT}/AppRun" - - if [ ! -f "${_temp}" ]; then - - echo "Creating: ${_temp}" - ln -s "${_local_run}" "${_temp}" - assert_result - fi -fi - -# Build package -echo -exec_or_die "${_package_cmd}" -echo - -echo "OUTPUT OK: ${_package_out}" -echo - -if [ "${_packrun_cmd}" != "" ]; then - echo "RUNNING ..." - exec_or_die "${_packrun_cmd}" - echo -fi - -exit 0 \ No newline at end of file diff --git a/build/resources/appimage/publish-appimage.conf b/build/resources/appimage/publish-appimage.conf deleted file mode 100644 index ea44ee3b..00000000 --- a/build/resources/appimage/publish-appimage.conf +++ /dev/null @@ -1,140 +0,0 @@ -################################################################################ -# BASH FORMAT CONFIG: Publish-AppImage for .NET -# WEBPAGE : https://kuiper.zone/publish-appimage-dotnet/ -################################################################################ - - -######################################## -# Application -######################################## - -# Mandatory application (file) name. This must be the base name of the main -# runnable file to be created by the publish/build process. It should NOT -# include any directory part or extension, i.e. do not append ".exe" or ".dll" -# for dotnet. Example: "MyApp" -APP_MAIN="sourcegit" - -# Mandatory application ID in reverse DNS form, i.e. "tld.my-domain.MyApp". -# Exclude any ".desktop" post-fix. Note that reverse DNS form is necessary -# for compatibility with Freedesktop.org metadata. -APP_ID="com.sourcegit-scm.SourceGit" - -# Mandatory icon source file relative to this file (appimagetool seems to -# require this). Use .svg or .png only. PNG should be one of standard sizes, -# i.e, 128x128 or 256x256 pixels. Example: "Assets/app.svg" -APP_ICON_SRC="sourcegit.png" - -# Optional Freedesktop.org metadata source file relative to this file. It is not essential -# (leave empty) but will be used by appimagetool for repository information if provided. -# See for information: https://docs.appimage.org/packaging-guide/optional/appstream.html -# NB. The file may embed bash variables defined in this file and those listed below -# (these will be substituted during the build). Examples include: "${APP_ID}" -# and "". -# $ISO_DATE : date of build, i.e. "2021-10-29", -# $APP_VERSION : application version (if provided), -# Example: "Assets/appdata.xml". -APP_XML_SRC="sourcegit.appdata.xml" - - -######################################## -# Desktop Entry -######################################## - -# Mandatory friendly name of the application. -DE_NAME="SourceGit" - -# Mandatory category(ies), separated with semicolon, in which the entry should be -# shown. See https://specifications.freedesktop.org/menu-spec/latest/apa.html -# Examples: "Development", "Graphics", "Network", "Utility" etc. -DE_CATEGORIES="Utility" - -# Optional short comment text (single line). -# Example: "Perform calculations" -DE_COMMENT="Open-source GUI client for git users" - -# Optional keywords, separated with semicolon. Values are not meant for -# display and should not be redundant with the value of DE_NAME. -DE_KEYWORDS="" - -# Flag indicating whether the program runs in a terminal window. Use true or false only. -DE_TERMINAL_FLAG=false - -# Optional name-value text to be appended to the Desktop Entry file, thus providing -# additional metadata. Name-values should not be redundant with values above and -# are to be terminated with new line ("\n"). -# Example: "Comment[fr]=Effectue des calculs compliqués\nMimeType=image/x-foo" -DE_EXTEND="" - - -######################################## -# Dotnet Publish -######################################## - -# Optional path relative to this file in which to find the dotnet project (.csproj) -# or solution (.sln) file, or the directory containing it. If empty (default), a single -# project or solution file is expected under the same directory as this file. -# IMPORTANT. If set to "null", dotnet publish is disabled (it is NOT called). Instead, -# only POST_PUBLISH is called. Example: "Source/MyProject" -DOTNET_PROJECT_PATH="../../../src/SourceGit.csproj" - -# Optional arguments suppled to "dotnet publish". Do NOT include "-r" (runtime) or version here as they will -# be added (see also $APP_VERSION). Typically you want as a minimum: "-c Release --self-contained true". -# Additional useful arguments include: -# "-p:DebugType=None -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishTrimmed=true -p:TrimMode=link" -# Refer: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish -DOTNET_PUBLISH_ARGS="-c Release -p:DebugType=None -p:DebugSymbols=false" - - -######################################## -# 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 diff --git a/build/resources/appimage/runtime-x86_64 b/build/resources/appimage/runtime-x86_64 deleted file mode 100755 index 0c9535a1..00000000 Binary files a/build/resources/appimage/runtime-x86_64 and /dev/null differ diff --git a/build/resources/appimage/sourcegit.appdata.xml b/build/resources/appimage/sourcegit.appdata.xml index ca304b4b..012c82d3 100644 --- a/build/resources/appimage/sourcegit.appdata.xml +++ b/build/resources/appimage/sourcegit.appdata.xml @@ -1,6 +1,6 @@ - com.sourcegit-scm.SourceGit + com.sourcegit_scm.SourceGit MIT MIT SourceGit @@ -8,8 +8,9 @@ Open-source GUI client for git users - com.sourcegit-scm.SourceGit.desktop + https://github.com/sourcegit-scm/sourcegit + com.sourcegit_scm.SourceGit.desktop - com.sourcegit-scm.SourceGit.desktop + com.sourcegit_scm.SourceGit.desktop - \ No newline at end of file + diff --git a/build/resources/deb/DEBIAN/control b/build/resources/deb/DEBIAN/control index 44dbf397..7cfed330 100755 --- a/build/resources/deb/DEBIAN/control +++ b/build/resources/deb/DEBIAN/control @@ -1,5 +1,5 @@ Package: sourcegit -Version: 8.18 +Version: 8.23 Priority: optional Depends: libx11-6, libice6, libsm6 Architecture: amd64 diff --git a/build/resources/deb/DEBIAN/postinst b/build/resources/deb/DEBIAN/postinst deleted file mode 100755 index 56aba83b..00000000 --- a/build/resources/deb/DEBIAN/postinst +++ /dev/null @@ -1,5 +0,0 @@ -#!bin/sh - -echo 'Create link on /usr/bin' -ln -s /opt/sourcegit/sourcegit /usr/bin/sourcegit -exit 0 \ No newline at end of file diff --git a/build/resources/deb/DEBIAN/postrm b/build/resources/deb/DEBIAN/postrm deleted file mode 100755 index 5a600118..00000000 --- a/build/resources/deb/DEBIAN/postrm +++ /dev/null @@ -1,4 +0,0 @@ -#!bin/sh - -rm -f /usr/bin/sourcegit -exit 0 \ No newline at end of file diff --git a/build/resources/rpm/SPECS/build.spec b/build/resources/rpm/SPECS/build.spec index ddafcfb8..86a7cfdd 100644 --- a/build/resources/rpm/SPECS/build.spec +++ b/build/resources/rpm/SPECS/build.spec @@ -14,24 +14,23 @@ Requires: libSM.so.6 Open-source & Free Git Gui Client %install -mkdir -p $RPM_BUILD_ROOT/opt/sourcegit -mkdir -p $RPM_BUILD_ROOT/usr/share/applications -mkdir -p $RPM_BUILD_ROOT/usr/share/icons -cp -r ../../_common/applications $RPM_BUILD_ROOT/usr/share/ -cp -r ../../_common/icons $RPM_BUILD_ROOT/usr/share/ -cp -f ../../../SourceGit/* $RPM_BUILD_ROOT/opt/sourcegit/ -chmod 755 -R $RPM_BUILD_ROOT/opt/sourcegit -chmod 755 $RPM_BUILD_ROOT/usr/share/applications/sourcegit.desktop +mkdir -p %{buildroot}/opt/sourcegit +mkdir -p %{buildroot}/%{_bindir} +mkdir -p %{buildroot}/usr/share/applications +mkdir -p %{buildroot}/usr/share/icons +cp -f ../../../SourceGit/* %{buildroot}/opt/sourcegit/ +ln -sf ../../opt/sourcegit/sourcegit %{buildroot}/%{_bindir} +cp -r ../../_common/applications %{buildroot}/%{_datadir} +cp -r ../../_common/icons %{buildroot}/%{_datadir} +chmod 755 -R %{buildroot}/opt/sourcegit +chmod 755 %{buildroot}/%{_datadir}/applications/sourcegit.desktop %files -/opt/sourcegit -/usr/share - -%post -ln -s /opt/sourcegit/sourcegit /usr/bin/sourcegit - -%postun -rm -f /usr/bin/sourcegit +%dir /opt/sourcegit/ +/opt/sourcegit/* +/usr/share/applications/sourcegit.desktop +/usr/share/icons/* +%{_bindir}/sourcegit %changelog -# skip \ No newline at end of file +# skip diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh new file mode 100755 index 00000000..04309018 --- /dev/null +++ b/build/scripts/package.linux.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -e + +if [ -z "$VERSION" ]; then + echo "Provide the version as environment variable VERSION" + exit 1 +fi + +if [ -z "$RUNTIME" ]; then + echo "Provide the runtime as environment variable RUNTIME" + exit 1 +fi + +arch= +appimage_arch= +target= +case "$RUNTIME" in + linux-x64) + arch=amd64 + appimage_arch=x86_64 + target=x86_64;; + linux-arm64) + arch=arm64 + appimage_arch=arm_aarch64 + target=aarch64;; + *) + echo "Unknown runtime $RUNTIME" + exit 1;; +esac + +APPIMAGETOOL_URL=https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + +cd build + +if [ ! -f "appimagetool" ]; then + curl -o appimagetool -L "$APPIMAGETOOL_URL" + chmod +x appimagetool +fi + +rm -f SourceGit/*.dbg + +mkdir -p SourceGit.AppDir/opt +mkdir -p SourceGit.AppDir/usr/share/metainfo +mkdir -p SourceGit.AppDir/usr/share/applications + +cp -r SourceGit SourceGit.AppDir/opt/sourcegit +desktop-file-install resources/_common/applications/sourcegit.desktop --dir SourceGit.AppDir/usr/share/applications \ + --set-icon com.sourcegit_scm.SourceGit --set-key=Exec --set-value=AppRun +mv SourceGit.AppDir/usr/share/applications/{sourcegit,com.sourcegit_scm.SourceGit}.desktop +cp resources/appimage/sourcegit.png SourceGit.AppDir/com.sourcegit_scm.SourceGit.png +ln -rsf SourceGit.AppDir/opt/sourcegit/sourcegit SourceGit.AppDir/AppRun +ln -rsf SourceGit.AppDir/usr/share/applications/com.sourcegit_scm.SourceGit.desktop SourceGit.AppDir +cp resources/appimage/sourcegit.appdata.xml SourceGit.AppDir/usr/share/metainfo/com.sourcegit_scm.SourceGit.appdata.xml + +ARCH="$appimage_arch" ./appimagetool -v SourceGit.AppDir "sourcegit-$VERSION.linux.$arch.AppImage" + +mkdir -p resources/deb/opt/sourcegit/ +mkdir -p resources/deb/usr/bin +mkdir -p resources/deb/usr/share/applications +mkdir -p resources/deb/usr/share/icons +cp -f SourceGit/* resources/deb/opt/sourcegit +ln -sf ../../opt/sourcegit/sourcegit resources/deb/usr/bin +cp -r resources/_common/applications resources/deb/usr/share +cp -r resources/_common/icons resources/deb/usr/share +sed -i -e "s/^Version:.*/Version: $VERSION/" -e "s/^Architecture:.*/Architecture: $arch/" resources/deb/DEBIAN/control +dpkg-deb --root-owner-group --build resources/deb "sourcegit_$VERSION-1_$arch.deb" + +rpmbuild -bb --target="$target" resources/rpm/SPECS/build.spec --define "_topdir $(pwd)/resources/rpm" --define "_version $VERSION" +mv "resources/rpm/RPMS/$target/sourcegit-$VERSION-1.$target.rpm" ./ diff --git a/build/scripts/package.osx-app.sh b/build/scripts/package.osx-app.sh new file mode 100755 index 00000000..80e66a08 --- /dev/null +++ b/build/scripts/package.osx-app.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +if [ -z "$VERSION" ]; then + echo "Provide the version as environment variable VERSION" + exit 1 +fi + +if [ -z "$RUNTIME" ]; then + echo "Provide the runtime as environment variable RUNTIME" + exit 1 +fi + +cd build + +mkdir -p SourceGit.app/Contents/Resources +mv SourceGit SourceGit.app/Contents/MacOS +cp resources/app/App.icns SourceGit.app/Contents/Resources/App.icns +sed "s/SOURCE_GIT_VERSION/$VERSION/g" resources/app/App.plist > SourceGit.app/Contents/Info.plist + +zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit.app -x "*/*\.dsym/*" diff --git a/build/scripts/package.windows-portable.sh b/build/scripts/package.windows-portable.sh new file mode 100755 index 00000000..cc5ee1bd --- /dev/null +++ b/build/scripts/package.windows-portable.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +if [ -z "$VERSION" ]; then + echo "Provide the version as environment variable VERSION" + exit 1 +fi + +if [ -z "$RUNTIME" ]; then + echo "Provide the runtime as environment variable RUNTIME" + exit 1 +fi + +cd build + +rm -rf SourceGit/*.pdb + +zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index f6e3cd88..5d7b114e 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -58,6 +58,7 @@ namespace SourceGit typeof(GridLengthConverter), ] )] + [JsonSerializable(typeof(Models.ExternalToolPaths))] [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] [JsonSerializable(typeof(Models.JetBrainsState))] [JsonSerializable(typeof(Models.ThemeOverrides))] diff --git a/src/App.axaml b/src/App.axaml index 87f915a5..1f3a66e0 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -33,6 +33,7 @@ + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index a8f1e32e..a1eaee6a 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -44,6 +44,8 @@ namespace SourceGit [STAThread] public static void Main(string[] args) { + Native.OS.SetupDataDir(); + AppDomain.CurrentDomain.UnhandledException += (_, e) => { LogException(e.ExceptionObject as Exception); @@ -107,6 +109,11 @@ namespace SourceGit dialog.ShowDialog(toplevel); }); + public static readonly SimpleCommand OpenAppDataDirCommand = new SimpleCommand(() => + { + Native.OS.OpenInFileManager(Native.OS.DataDir); + }); + public static readonly SimpleCommand OpenAboutCommand = new SimpleCommand(() => { var toplevel = GetTopLevel() as Window; @@ -521,6 +528,8 @@ namespace SourceGit private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop) { Native.OS.SetupEnternalTools(); + Models.AvatarManager.Instance.Start(); + Models.AutoFetchManager.Instance.Start(); string startupRepo = null; if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs index 6232fc17..b629f82d 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Commit : Command { - public Commit(string repo, string message, bool autoStage, bool amend, bool allowEmpty = false) + public Commit(string repo, string message, bool amend, bool allowEmpty = false) { var file = Path.GetTempFileName(); File.WriteAllText(file, message); @@ -13,8 +13,6 @@ namespace SourceGit.Commands Context = repo; TraitErrorAsOutput = true; Args = $"commit --file=\"{file}\""; - if (autoStage) - Args += " --all"; if (amend) Args += " --amend --no-edit"; if (allowEmpty) diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index df5a1c2b..07622821 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; namespace SourceGit.Commands { @@ -26,7 +23,7 @@ namespace SourceGit.Commands Args += remote; - AutoFetch.MarkFetched(repo); + Models.AutoFetchManager.Instance.MarkFetched(repo); } public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action outputHandler) @@ -46,110 +43,4 @@ namespace SourceGit.Commands private readonly Action _outputHandler; } - - public class AutoFetch - { - public static bool IsEnabled - { - get; - set; - } = false; - - public static int Interval - { - get => _interval; - set - { - if (value < 1) - return; - _interval = value; - lock (_lock) - { - foreach (var job in _jobs) - { - job.Value.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(_interval)); - } - } - } - } - - class Job - { - public Fetch Cmd = null; - public DateTime NextRunTimepoint = DateTime.MinValue; - } - - static AutoFetch() - { - Task.Run(() => - { - while (true) - { - if (!IsEnabled) - { - Thread.Sleep(10000); - continue; - } - - var now = DateTime.Now; - var uptodate = new List(); - lock (_lock) - { - foreach (var job in _jobs) - { - if (job.Value.NextRunTimepoint.Subtract(now).TotalSeconds <= 0) - { - uptodate.Add(job.Value); - } - } - } - - foreach (var job in uptodate) - { - job.Cmd.Exec(); - job.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)); - } - - Thread.Sleep(2000); - } - }); - } - - public static void AddRepository(string repo) - { - var job = new Job - { - Cmd = new Fetch(repo, "--all", true, false, null) { RaiseError = false }, - NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)), - }; - - lock (_lock) - { - _jobs[repo] = job; - } - } - - public static void RemoveRepository(string repo) - { - lock (_lock) - { - _jobs.Remove(repo); - } - } - - public static void MarkFetched(string repo) - { - lock (_lock) - { - if (_jobs.TryGetValue(repo, out var value)) - { - value.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)); - } - } - } - - private static readonly Dictionary _jobs = new Dictionary(); - private static readonly object _lock = new object(); - private static int _interval = 10; - } } diff --git a/src/Models/AutoFetchManager.cs b/src/Models/AutoFetchManager.cs new file mode 100644 index 00000000..7f1e4da6 --- /dev/null +++ b/src/Models/AutoFetchManager.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace SourceGit.Models +{ + public class AutoFetchManager + { + public static AutoFetchManager Instance + { + get + { + if (_instance == null) + _instance = new AutoFetchManager(); + + return _instance; + } + } + + public class Job + { + public Commands.Fetch Cmd = null; + public DateTime NextRunTimepoint = DateTime.MinValue; + } + + public bool IsEnabled + { + get; + set; + } = false; + + public int Interval + { + get => _interval; + set + { + _interval = Math.Max(1, value); + + lock (_lock) + { + foreach (var job in _jobs) + job.Value.NextRunTimepoint = DateTime.Now.AddMinutes(_interval * 1.0); + } + } + } + + private static AutoFetchManager _instance = null; + private Dictionary _jobs = new Dictionary(); + private object _lock = new object(); + private int _interval = 10; + + public void Start() + { + Task.Run(() => + { + while (true) + { + if (!IsEnabled) + { + Thread.Sleep(10000); + continue; + } + + var now = DateTime.Now; + var uptodate = new List(); + lock (_lock) + { + foreach (var job in _jobs) + { + if (job.Value.NextRunTimepoint.Subtract(now).TotalSeconds <= 0) + uptodate.Add(job.Value); + } + } + + foreach (var job in uptodate) + { + job.Cmd.Exec(); + job.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)); + } + + Thread.Sleep(2000); + } + + // ReSharper disable once FunctionNeverReturns + }); + } + + public void AddRepository(string repo) + { + var job = new Job + { + Cmd = new Commands.Fetch(repo, "--all", true, false, null) { RaiseError = false }, + NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)), + }; + + lock (_lock) + { + _jobs[repo] = job; + } + } + + public void RemoveRepository(string repo) + { + lock (_lock) + { + _jobs.Remove(repo); + } + } + + public void MarkFetched(string repo) + { + lock (_lock) + { + if (_jobs.TryGetValue(repo, out var value)) + value.NextRunTimepoint = DateTime.Now.AddMinutes(Interval * 1.0); + } + } + } +} diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index e85de1fd..313553f9 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -20,15 +20,31 @@ namespace SourceGit.Models void OnAvatarResourceChanged(string email); } - public static partial class AvatarManager + public partial class AvatarManager { - public static string SelectedServer + public static AvatarManager Instance { - get; - set; - } = "https://www.gravatar.com/avatar/"; + get + { + if (_instance == null) + _instance = new AvatarManager(); - static AvatarManager() + return _instance; + } + } + + private static AvatarManager _instance = null; + + [GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@users\.noreply\.github\.com$")] + private static partial Regex REG_GITHUB_USER_EMAIL(); + + private object _synclock = new object(); + private string _storePath; + private List _avatars = new List(); + private Dictionary _resources = new Dictionary(); + private HashSet _requesting = new HashSet(); + + public void Start() { _storePath = Path.Combine(Native.OS.DataDir, "avatars"); if (!Directory.Exists(_storePath)) @@ -62,7 +78,7 @@ namespace SourceGit.Models var matchGithubUser = REG_GITHUB_USER_EMAIL().Match(email); var url = matchGithubUser.Success ? $"https://avatars.githubusercontent.com/{matchGithubUser.Groups[2].Value}" : - $"{SelectedServer}{md5}?d=404"; + $"https://www.gravatar.com/avatar/{md5}?d=404"; var localFile = Path.Combine(_storePath, md5); var img = null as Bitmap; @@ -105,20 +121,22 @@ namespace SourceGit.Models NotifyResourceChanged(email); }); } + + // ReSharper disable once FunctionNeverReturns }); } - public static void Subscribe(IAvatarHost host) + public void Subscribe(IAvatarHost host) { _avatars.Add(host); } - public static void Unsubscribe(IAvatarHost host) + public void Unsubscribe(IAvatarHost host) { _avatars.Remove(host); } - public static Bitmap Request(string email, bool forceRefetch) + public Bitmap Request(string email, bool forceRefetch) { if (forceRefetch) { @@ -167,7 +185,7 @@ namespace SourceGit.Models return null; } - private static string GetEmailHash(string email) + private string GetEmailHash(string email) { var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim(); var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered)); @@ -177,21 +195,12 @@ namespace SourceGit.Models return builder.ToString(); } - private static void NotifyResourceChanged(string email) + private void NotifyResourceChanged(string email) { foreach (var avatar in _avatars) { avatar.OnAvatarResourceChanged(email); } } - - private static readonly object _synclock = new object(); - private static readonly string _storePath; - private static readonly List _avatars = new List(); - private static readonly Dictionary _resources = new Dictionary(); - private static readonly HashSet _requesting = new HashSet(); - - [GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@users\.noreply\.github\.com$")] - private static partial Regex REG_GITHUB_USER_EMAIL(); } } diff --git a/src/Models/CommitLink.cs b/src/Models/CommitLink.cs new file mode 100644 index 00000000..955779a8 --- /dev/null +++ b/src/Models/CommitLink.cs @@ -0,0 +1,8 @@ +namespace SourceGit.Models +{ + public class CommitLink + { + public string Name { get; set; } = null; + public string URLPrefix { get; set; } = null; + } +} diff --git a/src/Models/CommitTemplate.cs b/src/Models/CommitTemplate.cs new file mode 100644 index 00000000..d67d4839 --- /dev/null +++ b/src/Models/CommitTemplate.cs @@ -0,0 +1,22 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models +{ + public class CommitTemplate : ObservableObject + { + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + + public string Content + { + get => _content; + set => SetProperty(ref _content, value); + } + + private string _name = string.Empty; + private string _content = string.Empty; + } +} diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index 500ff549..e7cecaa3 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -19,8 +19,8 @@ namespace SourceGit.Models public class TextInlineRange { public int Start { get; set; } - public int Count { get; set; } - public TextInlineRange(int p, int n) { Start = p; Count = n; } + public int End { get; set; } + public TextInlineRange(int p, int n) { Start = p; End = p + n - 1; } } public class TextDiffLine diff --git a/src/Models/ExternalTool.cs b/src/Models/ExternalTool.cs index 5ea8c744..45682dab 100644 --- a/src/Models/ExternalTool.cs +++ b/src/Models/ExternalTool.cs @@ -79,6 +79,12 @@ namespace SourceGit.Models public string LaunchCommand { get; set; } } + public class ExternalToolPaths + { + [JsonPropertyName("tools")] + public Dictionary Tools { get; set; } = new Dictionary(); + } + public class ExternalToolsFinder { public List Founded @@ -87,42 +93,60 @@ namespace SourceGit.Models private set; } = new List(); - public void TryAdd(string name, string icon, string args, string env, Func finder) + public ExternalToolsFinder() { - var path = Environment.GetEnvironmentVariable(env); - if (string.IsNullOrEmpty(path) || !File.Exists(path)) + var customPathsConfig = Path.Combine(Native.OS.DataDir, "external_editors.json"); + try { - path = finder(); - if (string.IsNullOrEmpty(path) || !File.Exists(path)) - return; + if (File.Exists(customPathsConfig)) + _customPaths = JsonSerializer.Deserialize(File.ReadAllText(customPathsConfig), JsonCodeGen.Default.ExternalToolPaths); + } + catch + { + // Ignore } - Founded.Add(new ExternalTool(name, icon, path, args)); + if (_customPaths == null) + _customPaths = new ExternalToolPaths(); + } + + public void TryAdd(string name, string icon, string args, string key, Func finder) + { + if (_customPaths.Tools.TryGetValue(key, out var customPath) && File.Exists(customPath)) + { + Founded.Add(new ExternalTool(name, icon, customPath, args)); + } + else + { + var path = finder(); + if (!string.IsNullOrEmpty(path) && File.Exists(path)) + Founded.Add(new ExternalTool(name, icon, path, args)); + } } public void VSCode(Func platformFinder) { - TryAdd("Visual Studio Code", "vscode", "\"{0}\"", "VSCODE_PATH", platformFinder); + TryAdd("Visual Studio Code", "vscode", "\"{0}\"", "VSCODE", platformFinder); } public void VSCodeInsiders(Func platformFinder) { - TryAdd("Visual Studio Code - Insiders", "vscode_insiders", "\"{0}\"", "VSCODE_INSIDERS_PATH", platformFinder); + TryAdd("Visual Studio Code - Insiders", "vscode_insiders", "\"{0}\"", "VSCODE_INSIDERS", platformFinder); } public void VSCodium(Func platformFinder) { - TryAdd("VSCodium", "codium", "\"{0}\"", "VSCODIUM_PATH", platformFinder); + TryAdd("VSCodium", "codium", "\"{0}\"", "VSCODIUM", platformFinder); } public void Fleet(Func platformFinder) { - TryAdd("Fleet", "fleet", "\"{0}\"", "FLEET_PATH", platformFinder); + TryAdd("Fleet", "fleet", "\"{0}\"", "FLEET", platformFinder); } public void SublimeText(Func platformFinder) { - TryAdd("Sublime Text", "sublime_text", "\"{0}\"", "SUBLIME_TEXT_PATH", platformFinder); + TryAdd("Sublime Text", "sublime_text", "\"{0}\"", "SUBLIME_TEXT", platformFinder); } public void FindJetBrainsFromToolbox(Func platformFinder) @@ -146,5 +170,7 @@ namespace SourceGit.Models } } } + + private ExternalToolPaths _customPaths = null; } } diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs index 127cfa98..618fa166 100644 --- a/src/Models/IssueTrackerRule.cs +++ b/src/Models/IssueTrackerRule.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Text.RegularExpressions; - +using Avalonia.Controls.Documents; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.Models @@ -10,6 +10,7 @@ namespace SourceGit.Models public int Start { get; set; } = 0; public int Length { get; set; } = 0; public string URL { get; set; } = ""; + public Run Link { get; set; } = null; public bool Intersect(int start, int length) { diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index e068deda..2aa69cb5 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -58,10 +58,12 @@ namespace SourceGit.Models if (URL.StartsWith("http", StringComparison.Ordinal)) { - if (URL.EndsWith(".git")) - url = URL.Substring(0, URL.Length - 4); + // Try to remove the user before host and `.git` extension. + var uri = new Uri(URL.EndsWith(".git", StringComparison.Ordinal) ? URL.Substring(0, URL.Length - 4) : URL); + if (uri.Port != 80 && uri.Port != 443) + url = $"{uri.Scheme}://{uri.Host}:{uri.Port}{uri.LocalPath}"; else - url = URL; + url = $"{uri.Scheme}://{uri.Host}{uri.LocalPath}"; return true; } diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index bf15c4f4..788e00a8 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -70,6 +70,12 @@ namespace SourceGit.Models set; } = new AvaloniaList(); + public AvaloniaList CommitTemplates + { + get; + set; + } = new AvaloniaList(); + public AvaloniaList CommitMessages { get; diff --git a/src/Models/TextMateHelper.cs b/src/Models/TextMateHelper.cs index 0ae46c90..4fb6ad93 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -1,24 +1,102 @@ using System; +using System.Collections.Generic; using System.IO; using Avalonia; +using Avalonia.Platform; using Avalonia.Styling; using AvaloniaEdit; using AvaloniaEdit.TextMate; using TextMateSharp.Grammars; +using TextMateSharp.Internal.Grammars.Reader; +using TextMateSharp.Internal.Types; +using TextMateSharp.Registry; +using TextMateSharp.Themes; namespace SourceGit.Models { + public class RegistryOptionsWrapper : IRegistryOptions + { + public RegistryOptionsWrapper(ThemeName defaultTheme) + { + _backend = new RegistryOptions(defaultTheme); + _extraGrammars = new List(); + + string[] extraGrammarFiles = ["toml.json"]; + foreach (var file in extraGrammarFiles) + { + var asset = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Grammars/{file}", + UriKind.RelativeOrAbsolute)); + + try + { + var grammar = GrammarReader.ReadGrammarSync(new StreamReader(asset)); + _extraGrammars.Add(grammar); + } + catch + { + // ignore + } + } + } + + public IRawTheme GetTheme(string scopeName) + { + return _backend.GetTheme(scopeName); + } + + public IRawGrammar GetGrammar(string scopeName) + { + var grammar = _extraGrammars.Find(x => x.GetScopeName().Equals(scopeName, StringComparison.Ordinal)); + return grammar ?? _backend.GetGrammar(scopeName); + } + + public ICollection GetInjections(string scopeName) + { + return _backend.GetInjections(scopeName); + } + + public IRawTheme GetDefaultTheme() + { + return _backend.GetDefaultTheme(); + } + + public IRawTheme LoadTheme(ThemeName name) + { + return _backend.LoadTheme(name); + } + + public string GetScopeByFileName(string filename) + { + var extension = Path.GetExtension(filename); + var grammar = _extraGrammars.Find(x => x.GetScopeName().EndsWith(extension, StringComparison.OrdinalIgnoreCase)); + if (grammar != null) + return grammar.GetScopeName(); + + if (extension == ".h") + extension = ".cpp"; + else if (extension == ".resx" || extension == ".plist" || extension == ".manifest") + extension = ".xml"; + else if (extension == ".command") + extension = ".sh"; + + return _backend.GetScopeByExtension(extension); + } + + private readonly RegistryOptions _backend; + private readonly List _extraGrammars; + } + public static class TextMateHelper { public static TextMate.Installation CreateForEditor(TextEditor editor) { if (Application.Current?.ActualThemeVariant == ThemeVariant.Dark) - return editor.InstallTextMate(new RegistryOptions(ThemeName.DarkPlus)); + return editor.InstallTextMate(new RegistryOptionsWrapper(ThemeName.DarkPlus)); - return editor.InstallTextMate(new RegistryOptions(ThemeName.LightPlus)); + return editor.InstallTextMate(new RegistryOptionsWrapper(ThemeName.LightPlus)); } public static void SetThemeByApp(TextMate.Installation installation) @@ -26,26 +104,18 @@ namespace SourceGit.Models if (installation == null) return; - if (installation.RegistryOptions is RegistryOptions reg) + if (installation.RegistryOptions is RegistryOptionsWrapper reg) { - if (Application.Current?.ActualThemeVariant == ThemeVariant.Dark) - installation.SetTheme(reg.LoadTheme(ThemeName.DarkPlus)); - else - installation.SetTheme(reg.LoadTheme(ThemeName.LightPlus)); + var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark; + installation.SetTheme(reg.LoadTheme(isDark ? ThemeName.DarkPlus : ThemeName.LightPlus)); } } public static void SetGrammarByFileName(TextMate.Installation installation, string filePath) { - if (installation is { RegistryOptions: RegistryOptions reg }) + if (installation is { RegistryOptions: RegistryOptionsWrapper reg }) { - var ext = Path.GetExtension(filePath); - if (ext == ".h") - ext = ".cpp"; - else if (ext == ".resx" || ext == ".plist") - ext = ".xml"; - - installation.SetGrammar(reg.GetScopeByExtension(ext)); + installation.SetGrammar(reg.GetScopeByFileName(filePath)); GC.Collect(); } } diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index dbcd43aa..9d444dae 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -5,7 +5,6 @@ using System.IO; using System.Runtime.Versioning; using Avalonia; -using Avalonia.Dialogs; using Avalonia.Media; namespace SourceGit.Native @@ -47,9 +46,6 @@ namespace SourceGit.Native { EnableIme = true, }); - - // Free-desktop file picker has an extra black background panel. - builder.UseManagedSystemDialogs(); } public string FindGitExecutable() diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index 4fc9998c..e034c674 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -53,7 +53,7 @@ namespace SourceGit.Native { if (Directory.Exists(path)) { - Process.Start("open", path); + Process.Start("open", $"\"{path}\""); } else if (File.Exists(path)) { diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 8c2a3ada..0e1b8522 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -21,9 +21,9 @@ namespace SourceGit.Native void OpenWithDefaultEditor(string file); } - public static readonly string DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SourceGit"); + public static string DataDir { get; private set; } = string.Empty; public static string GitExecutable { get; set; } = string.Empty; - public static List ExternalTools { get; set; } = new List(); + public static List ExternalTools { get; set; } = []; static OS() { @@ -70,10 +70,19 @@ namespace SourceGit.Native public static void SetupApp(AppBuilder builder) { + _backend.SetupApp(builder); + } + + public static void SetupDataDir() + { + var osAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (string.IsNullOrEmpty(osAppDataDir)) + DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sourcegit"); + else + DataDir = Path.Combine(osAppDataDir, "SourceGit"); + if (!Directory.Exists(DataDir)) Directory.CreateDirectory(DataDir); - - _backend.SetupApp(builder); } public static void SetupEnternalTools() diff --git a/src/Resources/Grammars/toml.json b/src/Resources/Grammars/toml.json new file mode 100644 index 00000000..86c2ef87 --- /dev/null +++ b/src/Resources/Grammars/toml.json @@ -0,0 +1,343 @@ +{ + "version": "1.0.0", + "scopeName": "source.toml", + "uuid": "8b4e5008-c50d-11ea-a91b-54ee75aeeb97", + "information_for_contributors": [ + "Originally was maintained by aster (galaster@foxmail.com). This notice is only kept here for the record, please don't send e-mails about bugs and other issues." + ], + "patterns": [ + { + "include": "#commentDirective" + }, + { + "include": "#comment" + }, + { + "include": "#table" + }, + { + "include": "#entryBegin" + }, + { + "include": "#value" + } + ], + "repository": { + "comment": { + "captures": { + "1": { + "name": "comment.line.number-sign.toml" + }, + "2": { + "name": "punctuation.definition.comment.toml" + } + }, + "comment": "Comments", + "match": "\\s*((#).*)$" + }, + "commentDirective": { + "captures": { + "1": { + "name": "meta.preprocessor.toml" + }, + "2": { + "name": "punctuation.definition.meta.preprocessor.toml" + } + }, + "comment": "Comments", + "match": "\\s*((#):.*)$" + }, + "table": { + "patterns": [ + { + "name": "meta.table.toml", + "match": "^\\s*(\\[)\\s*((?:(?:(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+'))\\s*\\.?\\s*)+)\\s*(\\])", + "captures": { + "1": { + "name": "punctuation.definition.table.toml" + }, + "2": { + "patterns": [ + { + "match": "(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+')", + "name": "support.type.property-name.table.toml" + }, + { + "match": "\\.", + "name": "punctuation.separator.dot.toml" + } + ] + }, + "3": { + "name": "punctuation.definition.table.toml" + } + } + }, + { + "name": "meta.array.table.toml", + "match": "^\\s*(\\[\\[)\\s*((?:(?:(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+'))\\s*\\.?\\s*)+)\\s*(\\]\\])", + "captures": { + "1": { + "name": "punctuation.definition.array.table.toml" + }, + "2": { + "patterns": [ + { + "match": "(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+')", + "name": "support.type.property-name.array.toml" + }, + { + "match": "\\.", + "name": "punctuation.separator.dot.toml" + } + ] + }, + "3": { + "name": "punctuation.definition.array.table.toml" + } + } + }, + { + "begin": "(\\{)", + "end": "(\\})", + "name": "meta.table.inline.toml", + "beginCaptures": { + "1": { + "name": "punctuation.definition.table.inline.toml" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.table.inline.toml" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "match": ",", + "name": "punctuation.separator.table.inline.toml" + }, + { + "include": "#entryBegin" + }, + { + "include": "#value" + } + ] + } + ] + }, + "entryBegin": { + "name": "meta.entry.toml", + "match": "\\s*((?:(?:(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+'))\\s*\\.?\\s*)+)\\s*(=)", + "captures": { + "1": { + "patterns": [ + { + "match": "(?:[A-Za-z0-9_+-]+)|(?:\"[^\"]+\")|(?:'[^']+')", + "name": "support.type.property-name.toml" + }, + { + "match": "\\.", + "name": "punctuation.separator.dot.toml" + } + ] + }, + "2": { + "name": "punctuation.eq.toml" + } + } + }, + "value": { + "patterns": [ + { + "name": "string.quoted.triple.basic.block.toml", + "begin": "\"\"\"", + "end": "\"\"\"", + "patterns": [ + { + "match": "\\\\([btnfr\"\\\\\\n/ ]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})", + "name": "constant.character.escape.toml" + }, + { + "match": "\\\\[^btnfr/\"\\\\\\n]", + "name": "invalid.illegal.escape.toml" + } + ] + }, + { + "name": "string.quoted.single.basic.line.toml", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "match": "\\\\([btnfr\"\\\\\\n/ ]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})", + "name": "constant.character.escape.toml" + }, + { + "match": "\\\\[^btnfr/\"\\\\\\n]", + "name": "invalid.illegal.escape.toml" + } + ] + }, + { + "name": "string.quoted.triple.literal.block.toml", + "begin": "'''", + "end": "'''" + }, + { + "name": "string.quoted.single.literal.line.toml", + "begin": "'", + "end": "'" + }, + { + "captures": { + "1": { + "name": "constant.other.time.datetime.offset.toml" + } + }, + "match": "(?M512 57c251 0 455 204 455 455S763 967 512 967 57 763 57 512 261 57 512 57zm181 274c-11-11-29-11-40 0L512 472 371 331c-11-11-29-11-40 0-11 11-11 29 0 40L471 512 331 653c-11 11-11 29 0 40 11 11 29 11 40 0l141-141 141 141c11 11 29 11 40 0 11-11 11-29 0-40L552 512l141-141c11-11 11-29 0-40z M797 829a49 49 0 1049 49 49 49 0 00-49-49zm147-114A49 49 0 10992 764a49 49 0 00-49-49zM928 861a49 49 0 1049 49A49 49 0 00928 861zm-5-586L992 205 851 64l-71 71a67 67 0 00-94 0l235 235a67 67 0 000-94zm-853 128a32 32 0 00-32 50 1291 1291 0 0075 112L288 552c20 0 25 21 8 37l-93 86a1282 1282 0 00120 114l100-32c19-6 28 15 14 34l-40 55c26 19 53 36 82 53a89 89 0 00115-20 1391 1391 0 00256-485l-188-188s-306 224-595 198z M1280 704c0 141-115 256-256 256H288C129 960 0 831 0 672c0-126 80-232 192-272A327 327 0 01192 384c0-177 143-320 320-320 119 0 222 64 277 160C820 204 857 192 896 192c106 0 192 86 192 192 0 24-5 48-13 69C1192 477 1280 580 1280 704zm-493-128H656V352c0-18-14-32-32-32h-96c-18 0-32 14-32 32v224h-131c-29 0-43 34-23 55l211 211c12 12 33 12 45 0l211-211c20-20 6-55-23-55z + M853 102H171C133 102 102 133 102 171v683C102 891 133 922 171 922h683C891 922 922 891 922 853V171C922 133 891 102 853 102zM390 600l-48 48L205 512l137-137 48 48L301 512l88 88zM465 819l-66-18L559 205l66 18L465 819zm218-171L634 600 723 512l-88-88 48-48L819 512 683 649z M796 471A292 292 0 00512 256a293 293 0 00-284 215H0v144h228A293 293 0 00512 832a291 291 0 00284-217H1024V471h-228M512 688A146 146 0 01366 544A145 145 0 01512 400c80 0 146 63 146 144A146 146 0 01512 688 M645 448l64 64 220-221L704 64l-64 64 115 115H128v90h628zM375 576l-64-64-220 224L314 960l64-64-116-115H896v-90H262z M608 0q48 0 88 23t63 63 23 87v70h55q35 0 67 14t57 38 38 57 14 67V831q0 34-14 66t-38 57-57 38-67 13H426q-34 0-66-13t-57-38-38-57-14-66v-70h-56q-34 0-66-14t-57-38-38-57-13-67V174q0-47 23-87T109 23 196 0h412m175 244H426q-46 0-86 22T278 328t-26 85v348H608q47 0 86-22t63-62 25-85l1-348m-269 318q18 0 31 13t13 31-13 31-31 13-31-13-13-31 13-31 31-13m0-212q13 0 22 9t11 22v125q0 14-9 23t-22 10-23-7-11-22l-1-126q0-13 10-23t23-10z @@ -33,6 +34,7 @@ M1024 896v128H0V704h128v192h768V704h128v192zM576 555 811 320 896 405l-384 384-384-384L213 320 448 555V0h128v555z M959 320H960v640A64 64 0 01896 1024H192A64 64 0 01128 960V64A64 64 0 01192 0H640v321h320L959 320zM320 544c0 17 14 32 32 32h384A32 32 0 00768 544c0-17-14-32-32-32H352A32 32 0 00320 544zm0 128c0 17 14 32 32 32h384a32 32 0 0032-32c0-17-14-32-32-32H352a32 32 0 00-32 32zm0 128c0 17 14 32 32 32h384a32 32 0 0032-32c0-17-14-32-32-32H352a32 32 0 00-32 32z M683 85l213 213v598a42 42 0 01-42 42H170A43 43 0 01128 896V128C128 104 147 85 170 85H683zm-213 384H341v85h128v128h85v-128h128v-85h-128V341h-85v128z + M949 727l-217-231a33 33 0 00-48 0 33 33 0 000 48l157 172H389a35 35 0 00-35 35c0 19 16 34 35 34h452l-160 179a34 34 0 005 54c14 10 33 7 45-5l219-237a33 33 0 000-49zM719 196h131c-24-91-95-160-185-185v131c0 27 25 54 54 54zM129 846l1-747s-7-37 36-33h359v52s-7 76 32 133a191 191 0 00146 84h91v126h66v-191H719a126 126 0 01-127-127V0H155c-51 0-91 40-91 91v767c0 51 40 91 91 91h193v-66H155c0-0-26 4-26-36z M416 832H128V128h384v192C512 355 541 384 576 384L768 384v32c0 19 13 32 32 32S832 435 832 416v-64c0-6 0-19-6-25l-256-256c-6-6-19-6-25-6H128A64 64 0 0064 128v704C64 867 93 896 129 896h288c19 0 32-13 32-32S435 832 416 832zM576 172 722 320H576V172zM736 512C614 512 512 614 512 736S614 960 736 960s224-102 224-224S858 512 736 512zM576 736C576 646 646 576 736 576c32 0 58 6 83 26l-218 218c-19-26-26-51-26-83zm160 160c-32 0-64-13-96-32l224-224c19 26 32 58 32 96 0 90-70 160-160 160z M896 320c0-19-6-32-19-45l-192-192c-13-13-26-19-45-19H192c-38 0-64 26-64 64v768c0 38 26 64 64 64h640c38 0 64-26 64-64V320zm-256 384H384c-19 0-32-13-32-32s13-32 32-32h256c19 0 32 13 32 32s-13 32-32 32zm166-384H640V128l192 192h-26z M599 425 599 657 425 832 425 425 192 192 832 192Z @@ -59,6 +61,7 @@ M40 9 15 23 15 31 9 28 9 20 34 5 24 0 0 14 0 34 25 48 25 28 49 14zM26 29 26 48 49 34 49 15z M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z + M 968 418 l -95 94 c -59 59 -146 71 -218 37 L 874 331 a 64 64 0 0 0 0 -90 L 783 150 a 64 64 0 0 0 -90 0 L 475 368 c -34 -71 -22 -159 37 -218 l 94 -94 c 75 -75 196 -75 271 0 l 90 90 c 75 75 75 196 0 271 z M 332 693 a 64 64 0 0 1 0 -90 l 271 -271 c 25 -25 65 -25 90 0 s 25 65 0 90 L 422 693 a 64 64 0 0 1 -90 0 z M 151 783 l 90 90 a 64 64 0 0 0 90 0 l 218 -218 c 34 71 22 159 -37 218 l -86 94 a 192 192 0 0 1 -271 0 l -98 -98 a 192 192 0 0 1 0 -271 l 94 -86 c 59 -59 146 -71 218 -37 L 151 693 a 64 64 0 0 0 0 90 z M0 33h1024v160H0zM0 432h1024v160H0zM0 831h1024v160H0z M512 0C233 0 7 223 0 500C6 258 190 64 416 64c230 0 416 200 416 448c0 53 43 96 96 96s96-43 96-96c0-283-229-512-512-512zm0 1023c279 0 505-223 512-500c-6 242-190 436-416 436c-230 0-416-200-416-448c0-53-43-96-96-96s-96 43-96 96c0 283 229 512 512 512z M976 0h-928A48 48 0 000 48v652a48 48 0 0048 48h416V928H200a48 48 0 000 96h624a48 48 0 000-96H560v-180h416a48 48 0 0048-48V48A48 48 0 00976 0zM928 652H96V96h832v556z diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 4a0abd37..649dc7c9 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -6,9 +6,9 @@ Über SourceGit • Erstellt mit © 2024 sourcegit-scm - • Text Editor von + • Texteditor von • Monospace-Schriftarten von - • Quelltext findest du unter + • Quelltext findest du auf Open Source & freier Git GUI Client Worktree hinzufügen Was auschecken: @@ -29,7 +29,7 @@ Wähle die anzuwendende .patch-Datei Ignoriere Leerzeichenänderungen Keine Warnungen - Schaltet die Warnung vor nachgestellte Leerzeichen aus + Keine Warnung vor Leerzeichen am Zeilenende Patch anwenden Warnen Gibt eine Warnung für ein paar solcher Fehler aus, aber wendet es an @@ -41,12 +41,12 @@ Archiv erstellen SourceGit Askpass ALS UNVERÄNDERT ANGENOMMENE DATEIEN - KEINE UNVERÄNDERT ANGENOMMENEN DATEIEN GEFUNDEN + KEINE ALS UNVERÄNDERT ANGENOMMENEN DATEIEN ENTFERNEN BINÄRE DATEI NICHT UNTERSTÜTZT!!! Blame BLAME WIRD BEI DIESER DATEI NICHT UNTERSTÜTZT!!! - ${0}$ auschecken... + Auscheken von ${0}$... Mit Branch vergleichen Mit HEAD vergleichen Mit Worktree vergleichen @@ -166,6 +166,7 @@ Art: Mit Anmerkung Ohne Anmerkung + Halte Strg gedrückt, um direkt auszuführen Ausschneiden Branch löschen Branch: @@ -218,7 +219,7 @@ Fetch Alle Remotes fetchen Ohne Tags fetchen - Alle toten remote Branches entfernen + Alle verwaisten Branches entfernen Remote: Remote-Änderungen fetchen Als unverändert annehmen @@ -240,7 +241,7 @@ Datei Historie FILTER Git-Flow - Entwicklungs-Branch: + Development-Branch: Feature: Feature-Prefix: FLOW - Finish Feature @@ -251,7 +252,7 @@ Hotfix-Prefix: Git-Flow initialisieren Branch behalten - Produktions-Branch: + Production-Branch: Release: Release-Prefix: Feature starten... @@ -286,9 +287,9 @@ LFS Objekte pushen Pushe große Dateien in der Warteschlange zum Git LFS Endpunkt Remote: - Verfolge '{0}' benannte Dateien + Verfolge alle '{0}' Dateien Verfolge alle *{0} Dateien - Historien + Verlauf Wechsle zwischen horizontalem und vertikalem Layout Wechsle zwischen Kurven- und Konturgraphenmodus AUTOR @@ -314,9 +315,9 @@ Ausgewählte Änderungen stagen/unstagen Commit-Suchmodus Wechsle zu 'Änderungen' - Wechsle zu 'Historien' + Wechsle zu 'Verlauf' Wechsle zu 'Stashes' - TEXT EDITOR + TEXTEDITOR Suchpanel schließen Suche nächste Übereinstimmung Suche vorherige Übereinstimmung @@ -334,8 +335,8 @@ Interaktiver Rebase Ziel Branch: Auf: - Hoch schieben - Runter schieben + Hochschieben + Runterschieben Source Git FEHLER INFO @@ -347,14 +348,15 @@ Name: Git wurde NICHT konfiguriert. Gehe bitte zuerst in die [Einstellungen] und konfiguriere Git. BENACHRICHTIGUNG + App-Daten Ordner öffnen ORDNER AUSWÄHLEN Öffne mit... Optional. Neue Seite erstellen Lesezeichen Tab schließen - Schließe andere Tabs - Schließe Rechte Tabs + Andere Tabs schließen + Rechte Tabs schließen Kopiere Repository-Pfad Repositories Einfügen @@ -368,20 +370,19 @@ Leztes Jahr Vor {0} Jahren Einstellungen - ERSCHEINUNGSBILD + DARSTELLUNG Standardschriftart Standardschriftgröße Monospace-Schriftart - Verwende nur die Monospace-Schriftart im Text Editor + Verwende nur die Monospace-Schriftart im Texteditor Design Design-Anpassungen ALLGEMEIN - Avatar Server Beim Starten nach Updates suchen Sprache Commit-Historie Zuletzt geöffnete Tabs beim Starten wiederherstellen - Commit-Nachricht Hinweislänge + Längenvorgabe für Commit-Nachrichten Fixe Tab-Breite in Titelleiste Sichtbare Vergleichskontextzeilen GIT @@ -403,12 +404,12 @@ Tag-Signierung GPG Format GPG Installationspfad - Gebe Installationspfad zu installiertem GPG Programm an + Installationspfad zum GPG Programm Benutzer Signierungsschlüssel GPG Benutzer Signierungsschlüssel DIFF/MERGE TOOL Installationspfad - Gebe Installationspfad zum Diff/Merge Tool an + Installationspfad zum Diff/Merge Tool Tool Remote löschen Ziel: @@ -480,7 +481,7 @@ Aktualisiern REMOTES REMOTE HINZUFÜGEN - LÖSEN + KONFLIKTE BEHEBEN Commit suchen Suche über Dateiname @@ -488,6 +489,7 @@ SHA Autor & Committer Suche Branches & Tags + Zeige Tags als Baum Statistiken SUBMODULE SUBMODUL HINZUFÜGEN @@ -585,16 +587,13 @@ Ignoriere nur diese Datei Amend Auto-Stage - Weise den Befehl an automatisch Dateien zu stagen die verändert und modifiziert wurden, aber für Git unbekannte Dateien sind davon unberührt. Du kannst diese Datei jetzt stagen. COMMIT COMMIT & PUSH STRG + Enter KONFLIKTE ERKANNT DATEI KONFLIKTE GELÖST - LETZTE COMMIT-NACHRICHTEN NICHT-VERFOLGTE DATEIEN INKLUDIEREN - NACHRICHTEN HISTORIE KEINE BISHERIGEN COMMIT-NACHRICHTEN GESTAGED UNSTAGEN diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 364494f0..57e199ac 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -64,6 +64,8 @@ Branch Compare Bytes CANCEL + Reset to This Revision + Reset to Parent Revision CHANGE DISPLAY MODE Show as File and Dir List Show as Path List @@ -122,6 +124,9 @@ Enter commit subject Description Repository Configure + COMMIT TEMPLATE + Template Name: + Template Content: Email Address Email address GIT @@ -163,6 +168,7 @@ Kind: annotated lightweight + Hold Ctrl to start directly Cut Delete Branch Branch: @@ -194,13 +200,13 @@ NEW Syntax Highlighting Line Word Wrap - Open In Merge Tool + Open in Merge Tool Decrease Number of Visible Lines Increase Number of Visible Lines SELECT FILE TO VIEW CHANGES Show hidden symbols Swap - Open In Merge Tool + Open in Merge Tool Discard Changes All local changes in working copy. Changes: @@ -344,6 +350,7 @@ Name: Git has NOT been configured. Please to go [Preference] and configure it first. NOTICE + Open App Data Dir SELECT FOLDER Open With... Optional. @@ -373,7 +380,6 @@ Theme Theme Overrides GENERAL - Avatar Server Check for updates on startup Language History Commits @@ -583,17 +589,16 @@ Ignore this file only Amend Auto-Stage - Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected. You can stage this file now. COMMIT COMMIT & PUSH + Template/Histories CTRL + Enter CONFLICTS DETECTED FILE CONFLICTS ARE RESOLVED - RECENT INPUT MESSAGES INCLUDE UNTRACKED FILES - MESSAGE HISTORIES NO RECENT INPUT MESSAGES + NO COMMIT TEMPLATES STAGED UNSTAGE UNSTAGE ALL @@ -601,6 +606,7 @@ STAGE STAGE ALL VIEW ASSUME UNCHANGED + Template: ${0}$ Right-click the selected file(s), and make your choice to resolve conflicts. WORKTREE Copy Path diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index ef0967ec..637f9a21 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -67,19 +67,21 @@ Comparar Branch Bytes CANCELAR + Resetar para Esta Revisão + Resetar to Revisão Pai ALTERAR MODO DE EXIBIÇÃO - Mostrar como Lista de Arquivos e Diretórios + Mostrar como Grade Mostrar como Lista de Caminhos - Mostrar como Árvore de Sistema de Arquivos - Checar Branch - Checar Commit + Mostrar como Árvore de Arquivos do Sistema + Checkout Branch + Checkout Commit Aviso: Ao fazer o checkout de um commit, seu Head ficará desanexado Commit: Branch: Alterações Locais: Descartar - Não Fazer Nada - Guardar & Reaplicar + Nada + Stash & Reaplicar Cherry-Pick Este Commit Commit: Commitar todas as alterações @@ -100,7 +102,8 @@ Comparar com HEAD Comparar com Worktree Copiar Informações - Copiar SHARebase Interativo ${0}$ até Aqui + Copiar SHA + Rebase Interativo ${0}$ até Aqui Rebase ${0}$ até Aqui Resetar ${0}$ até Aqui Reverter Commit @@ -124,8 +127,20 @@ Insira o assunto do commit Descrição Configurar Repositório + TEMPLATE DE COMMIT + Nome do Template: + Conteúdo do Template: Endereço de Email Endereço de email + GIT + RASTREADOR DE PROBLEMAS + Adicionar Regra de Exemplo do Github + Adicionar Regra de Exemplo do Jira + Nova Regra + Expressão Regex de Issue: + Nome da Regra: + URL de Resultado: + Por favor, use $1, $2 para acessar os valores de grupos do regex. Proxy HTTP Proxy HTTP usado por este repositório Nome de Usuário @@ -156,6 +171,7 @@ Tipo: anotada leve + Pressione Ctrl para iniciar diretamente Recortar Excluir Branch Branch: @@ -337,6 +353,7 @@ Nome: O Git NÃO foi configurado. Por favor, vá para [Preferências] e configure primeiro. AVISO + Abrir Pasta de Dados do Aplicativo SELECIONAR PASTA Abrir Com... Opcional. @@ -362,10 +379,10 @@ Fonte Padrão Tamanho da Fonte Padrão Fonte Monoespaçada + Usar apenas fonte monoespaçada no editor de texto Tema Sobrescrever Tema GERAL - Servidor de Avatar Verificar atualizações na inicialização Idioma Commits do Histórico @@ -477,6 +494,7 @@ SHA Autor & Committer Pesquisar Branches & Tags + Mostrar Tags como Árvore Estatísticas SUBMÓDULOS ADICIONAR SUBMÓDULO @@ -554,6 +572,7 @@ Submódulo: Usar opção --remote Aviso + Página de Boas-vindas Criar Grupo Raíz Criar Subgrupo Clonar Repositório @@ -573,17 +592,16 @@ Ignorar apenas este arquivo Corrigir Auto-Stage - Informe ao comando para automaticamente stagear arquivos que foram modificados e excluídos, mas novos arquivos que você não informou ao Git não serão afetados. Você pode stagear este arquivo agora. COMMIT COMMIT & PUSH + Template/Histories CTRL + Enter CONFLITOS DETECTADOS CONFLITOS DE ARQUIVOS RESOLVIDOS - MENSAGENS RECENTES DE ENTRADA INCLUIR ARQUIVOS NÃO RASTREADOS - HISTÓRICO DE MENSAGENS NENHUMA MENSAGEM DE ENTRADA RECENTE + NENHUM TEMPLATE DE COMMIT STAGED DESSTAGEAR DESSTAGEAR TODOS @@ -591,6 +609,7 @@ STAGEAR STAGEAR TODOS VER SUPOR NÃO ALTERADO + Template: ${0}$ Clique com o botão direito nos arquivos selecionados e escolha como resolver conflitos. WORKTREE Copiar Caminho diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 58e915db..3aa37333 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -67,6 +67,8 @@ 分支比较 字节 取 消 + 重置文件到该版本 + 重置文件到上一版本 切换变更显示模式 文件名+路径列表模式 全路径列表模式 @@ -125,6 +127,9 @@ 填写提交信息主题 详细描述 仓库配置 + 提交信息模板 + 模板名 : + 模板内容 : 电子邮箱 邮箱地址 GIT配置 @@ -166,6 +171,7 @@ 类型 : 附注标签 轻量标签 + 按住Ctrl键点击将以默认参数运行 剪切 删除分支确认 分支名 : @@ -347,6 +353,7 @@ 名称 : GIT尚未配置。请打开【偏好设置】配置GIT路径。 系统提示 + 浏览应用数据目录 选择文件夹 打开文件... 选填。 @@ -376,7 +383,6 @@ 主题 主题自定义 通用配置 - 头像服务 启动时检测软件更新 显示语言 最大历史提交数 @@ -584,18 +590,17 @@ 忽略同目录下所有文件 忽略本文件 修补(--amend) - 自动暂存(--all) - 提交前自动将修改过和删除的文件加入暂存区,但新增文件需要手动添加。 + 自动暂存 现在您已可将其加入暂存区中 提交 提交并推送 + 历史输入/模板 CTRL + Enter 检测到冲突 文件冲突已解决 - 最近输入的提交信息 显示未跟踪文件 - 历史提交信息 没有提交信息记录 + 没有可应用的提交信息模板 已暂存 从暂存区移除选中 从暂存区移除所有 @@ -603,6 +608,7 @@ 暂存选中 暂存所有 查看忽略变更文件 + 模板:${0}$ 请选中冲突文件,打开右键菜单,选择合适的解决方式 本地工作树 复制工作树路径 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 167933da..682335b7 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -67,6 +67,8 @@ 分支比較 位元組 取 消 + 重置檔案到該版本 + 重置檔案到上一版本 切換變更顯示模式 檔名+路徑列表模式 全路徑列表模式 @@ -125,6 +127,9 @@ 填寫提交信息主題 詳細描述 倉庫配置 + 提交資訊範本 + 範本名稱 : + 範本內容 : 電子郵箱 郵箱地址 GIT配置 @@ -166,6 +171,7 @@ 型別 : 附註標籤 輕量標籤 + 按住Ctrl鍵點擊將以預設參數運行 剪下 刪除分支確認 分支名 : @@ -347,6 +353,7 @@ 名稱 : GIT尚未配置。請開啟【偏好設定】配置GIT路徑。 系統提示 + 瀏覽程式資料目錄 選擇資料夾 開啟檔案... 選填。 @@ -376,7 +383,6 @@ 主題 主題自訂 通用配置 - 頭像服務 啟動時檢測軟體更新 顯示語言 最大歷史提交數 @@ -584,18 +590,17 @@ 忽略同路徑下所有檔案 忽略本檔案 修補(--amend) - 自動暫存(--all) - 提交前自動將修改過和刪除的檔案加入暫存區,但新增檔案需要手動添加。 + 自動暫存 現在您已可將其加入暫存區中 提交 提交併推送 + 歷史輸入/範本 CTRL + Enter 檢測到衝突 檔案衝突已解決 - 最近輸入的提交資訊 顯示未跟蹤檔案 - 歷史提交資訊 沒有提交資訊記錄 + 沒有可應用的提交資訊範本 已暫存 從暫存區移除選中 從暫存區移除所有 @@ -603,6 +608,7 @@ 暫存選中 暫存所有 檢視忽略變更檔案 + 範本:${0}$ 請選中衝突檔案,開啟右鍵選單,選擇合適的解決方式 本地工作樹 拷贝工作樹路徑 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index f40e88f6..e9477212 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -251,8 +251,12 @@ + + @@ -277,12 +281,9 @@ - - + + + + + - + + + + + - - - - - + - - - - - + - - - - - + + diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index c1fb8a4a..6ee82a71 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -24,12 +24,13 @@ true true link - Size + Speed + @@ -37,15 +38,16 @@ - - - - - + + + + + - + + diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index 33988725..71f96a90 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -36,11 +36,6 @@ namespace SourceGit.ViewModels get => Backend is Models.Branch; } - public string TrackStatus - { - get => Backend is Models.Branch { IsLocal: true } branch ? branch.TrackStatus.ToString() : string.Empty; - } - public FontWeight NameFontWeight { get => Backend is Models.Branch { IsCurrent: true } ? FontWeight.Bold : FontWeight.Regular; diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index ae0e2cb9..f6c01259 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -66,7 +66,7 @@ namespace SourceGit.ViewModels if (value == null || value.Count != 1) DiffContext = null; else - DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value[0]), _diffContext); + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, value[0]), _diffContext); } } } @@ -89,15 +89,35 @@ namespace SourceGit.ViewModels set => SetProperty(ref _viewRevisionFileContent, value); } + public AvaloniaList WebLinks + { + get; + private set; + } = new AvaloniaList(); + public AvaloniaList IssueTrackerRules { - get => _issueTrackerRules; + get => _repo.Settings?.IssueTrackerRules; } - public CommitDetail(string repo, AvaloniaList issueTrackerRules) + public CommitDetail(Repository repo) { _repo = repo; - _issueTrackerRules = issueTrackerRules; + + foreach (var remote in repo.Remotes) + { + if (remote.TryGetVisitURL(out var url)) + { + if (url.StartsWith("https://github.com/", StringComparison.Ordinal)) + WebLinks.Add(new Models.CommitLink() { Name = "Github", URLPrefix = $"{url}/commit/" }); + else if (url.StartsWith("https://gitlab.com/", StringComparison.Ordinal)) + WebLinks.Add(new Models.CommitLink() { Name = "GitLab", URLPrefix = $"{url}/-/commit/" }); + else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal)) + WebLinks.Add(new Models.CommitLink() { Name = "Gitee", URLPrefix = $"{url}/commit/" }); + else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal)) + WebLinks.Add(new Models.CommitLink() { Name = "Bitbucket", URLPrefix = $"{url}/commits/" }); + } + } } public void Cleanup() @@ -118,8 +138,7 @@ namespace SourceGit.ViewModels public void NavigateTo(string commitSHA) { - var repo = App.FindOpenedRepository(_repo); - repo?.NavigateToCommit(commitSHA); + _repo?.NavigateToCommit(commitSHA); } public void ClearSearchChangeFilter() @@ -129,7 +148,7 @@ namespace SourceGit.ViewModels public List GetRevisionFilesUnderFolder(string parentFolder) { - return new Commands.QueryRevisionObjects(_repo, _commit.SHA, parentFolder).Result(); + return new Commands.QueryRevisionObjects(_repo.FullPath, _commit.SHA, parentFolder).Result(); } public void ViewRevisionFile(Models.Object file) @@ -145,13 +164,13 @@ namespace SourceGit.ViewModels case Models.ObjectType.Blob: Task.Run(() => { - var isBinary = new Commands.IsBinary(_repo, _commit.SHA, file.Path).Result(); + var isBinary = new Commands.IsBinary(_repo.FullPath, _commit.SHA, file.Path).Result(); if (isBinary) { var ext = Path.GetExtension(file.Path); if (IMG_EXTS.Contains(ext)) { - var stream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path); + var stream = Commands.QueryFileContent.Run(_repo.FullPath, _commit.SHA, file.Path); var bitmap = stream.Length > 0 ? new Bitmap(stream) : null; Dispatcher.UIThread.Invoke(() => { @@ -160,7 +179,7 @@ namespace SourceGit.ViewModels } else { - var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result(); + var size = new Commands.QueryFileSize(_repo.FullPath, file.Path, _commit.SHA).Result(); Dispatcher.UIThread.Invoke(() => { ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size }; @@ -170,7 +189,7 @@ namespace SourceGit.ViewModels return; } - var contentStream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path); + var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _commit.SHA, file.Path); var content = new StreamReader(contentStream).ReadToEnd(); var matchLFS = REG_LFS_FORMAT().Match(content); if (matchLFS.Success) @@ -191,7 +210,7 @@ namespace SourceGit.ViewModels case Models.ObjectType.Commit: Task.Run(() => { - var submoduleRoot = Path.Combine(_repo, file.Path); + var submoduleRoot = Path.Combine(_repo.FullPath, file.Path); var commit = new Commands.QuerySingleCommit(submoduleRoot, file.SHA).Result(); if (commit != null) { @@ -237,10 +256,49 @@ namespace SourceGit.ViewModels var toolPath = Preference.Instance.ExternalMergeToolPath; var opt = new Models.DiffOption(_commit, change); - Task.Run(() => Commands.MergeTool.OpenForDiff(_repo, toolType, toolPath, opt)); + Task.Run(() => Commands.MergeTool.OpenForDiff(_repo.FullPath, toolType, toolPath, opt)); ev.Handled = true; }; menu.Items.Add(diffWithMerger); + menu.Items.Add(new MenuItem { Header = "-" }); + + var fullPath = Path.Combine(_repo.FullPath, change.Path); + if (File.Exists(fullPath)) + { + var resetToThisRevision = new MenuItem(); + resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); + resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToThisRevision.Click += (_, ev) => + { + new Commands.Checkout(_repo.FullPath).FileWithRevision(change.Path, $"{_commit.SHA}"); + ev.Handled = true; + }; + + var resetToFirstParent = new MenuItem(); + resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); + resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToFirstParent.IsEnabled = _commit.Parents.Count > 0 && change.Index != Models.ChangeState.Added && change.Index != Models.ChangeState.Renamed; + resetToFirstParent.Click += (_, ev) => + { + new Commands.Checkout(_repo.FullPath).FileWithRevision(change.Path, $"{_commit.SHA}~1"); + ev.Handled = true; + }; + + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = App.CreateMenuIcon("Icons.Explore"); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(fullPath, true); + ev.Handled = true; + }; + + menu.Items.Add(resetToThisRevision); + menu.Items.Add(resetToFirstParent); + menu.Items.Add(new MenuItem { Header = "-" }); + menu.Items.Add(explore); + menu.Items.Add(new MenuItem { Header = "-" }); + } if (change.Index != Models.ChangeState.Deleted) { @@ -249,7 +307,7 @@ namespace SourceGit.ViewModels history.Icon = App.CreateMenuIcon("Icons.Histories"); history.Click += (_, ev) => { - var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path, _issueTrackerRules) }; + var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) }; window.Show(); ev.Handled = true; }; @@ -259,26 +317,13 @@ namespace SourceGit.ViewModels blame.Icon = App.CreateMenuIcon("Icons.Blame"); blame.Click += (_, ev) => { - var window = new Views.Blame() { DataContext = new Blame(_repo, change.Path, _commit.SHA) }; + var window = new Views.Blame() { DataContext = new Blame(_repo.FullPath, change.Path, _commit.SHA) }; window.Show(); ev.Handled = true; }; - var full = Path.GetFullPath(Path.Combine(_repo, change.Path)); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(full); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(full, true); - ev.Handled = true; - }; - - menu.Items.Add(new MenuItem { Header = "-" }); menu.Items.Add(history); menu.Items.Add(blame); - menu.Items.Add(explore); menu.Items.Add(new MenuItem { Header = "-" }); } @@ -307,34 +352,25 @@ namespace SourceGit.ViewModels public ContextMenu CreateRevisionFileContextMenu(Models.Object file) { - var history = new MenuItem(); - history.Header = App.Text("FileHistory"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, ev) => + var fullPath = Path.Combine(_repo.FullPath, file.Path); + + var resetToThisRevision = new MenuItem(); + resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); + resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToThisRevision.IsEnabled = File.Exists(fullPath); + resetToThisRevision.Click += (_, ev) => { - var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path, _issueTrackerRules) }; - window.Show(); + new Commands.Checkout(_repo.FullPath).FileWithRevision(file.Path, $"{_commit.SHA}"); ev.Handled = true; }; - var blame = new MenuItem(); - blame.Header = App.Text("Blame"); - blame.Icon = App.CreateMenuIcon("Icons.Blame"); - blame.IsEnabled = file.Type == Models.ObjectType.Blob; - blame.Click += (_, ev) => - { - var window = new Views.Blame() { DataContext = new Blame(_repo, file.Path, _commit.SHA) }; - window.Show(); - ev.Handled = true; - }; - - var full = Path.GetFullPath(Path.Combine(_repo, file.Path)); var explore = new MenuItem(); explore.Header = App.Text("RevealFile"); explore.Icon = App.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = File.Exists(fullPath); explore.Click += (_, ev) => { - Native.OS.OpenInFileManager(full, file.Type == Models.ObjectType.Blob); + Native.OS.OpenInFileManager(fullPath, file.Type == Models.ObjectType.Blob); ev.Handled = true; }; @@ -353,12 +389,33 @@ namespace SourceGit.ViewModels if (selected.Count == 1) { var saveTo = Path.Combine(selected[0].Path.LocalPath, Path.GetFileName(file.Path)); - Commands.SaveRevisionFile.Run(_repo, _commit.SHA, file.Path, saveTo); + Commands.SaveRevisionFile.Run(_repo.FullPath, _commit.SHA, file.Path, saveTo); } ev.Handled = true; }; + var history = new MenuItem(); + history.Header = App.Text("FileHistory"); + history.Icon = App.CreateMenuIcon("Icons.Histories"); + history.Click += (_, ev) => + { + var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path) }; + window.Show(); + ev.Handled = true; + }; + + var blame = new MenuItem(); + blame.Header = App.Text("Blame"); + blame.Icon = App.CreateMenuIcon("Icons.Blame"); + blame.IsEnabled = file.Type == Models.ObjectType.Blob; + blame.Click += (_, ev) => + { + var window = new Views.Blame() { DataContext = new Blame(_repo.FullPath, file.Path, _commit.SHA) }; + window.Show(); + ev.Handled = true; + }; + var copyPath = new MenuItem(); copyPath.Header = App.Text("CopyPath"); copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); @@ -378,10 +435,14 @@ namespace SourceGit.ViewModels }; var menu = new ContextMenu(); - menu.Items.Add(history); - menu.Items.Add(blame); + menu.Items.Add(resetToThisRevision); + menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(explore); menu.Items.Add(saveAs); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(history); + menu.Items.Add(blame); + menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copyPath); menu.Items.Add(copyFileName); return menu; @@ -406,9 +467,9 @@ namespace SourceGit.ViewModels Task.Run(() => { - var fullMessage = new Commands.QueryCommitFullMessage(_repo, _commit.SHA).Result(); + var fullMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, _commit.SHA).Result(); var parent = _commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : _commit.Parents[0]; - var cmdChanges = new Commands.CompareRevisions(_repo, parent, _commit.SHA) { Cancel = _cancelToken }; + var cmdChanges = new Commands.CompareRevisions(_repo.FullPath, parent, _commit.SHA) { Cancel = _cancelToken }; var changes = cmdChanges.Result(); var visible = changes; if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) @@ -463,8 +524,7 @@ namespace SourceGit.ViewModels ".ico", ".bmp", ".jpg", ".png", ".jpeg" }; - private string _repo; - private AvaloniaList _issueTrackerRules = null; + private Repository _repo = null; private int _activePageIndex = 0; private Models.Commit _commit = null; private string _fullMessage = string.Empty; diff --git a/src/ViewModels/EditRemote.cs b/src/ViewModels/EditRemote.cs index 0004cca4..0a514324 100644 --- a/src/ViewModels/EditRemote.cs +++ b/src/ViewModels/EditRemote.cs @@ -93,11 +93,8 @@ namespace SourceGit.ViewModels public static ValidationResult ValidateSSHKey(string sshkey, ValidationContext ctx) { - if (ctx.ObjectInstance is EditRemote edit && edit.UseSSH) + if (ctx.ObjectInstance is EditRemote { _useSSH: true } && !string.IsNullOrEmpty(sshkey)) { - if (string.IsNullOrEmpty(sshkey)) - return new ValidationResult("SSH private key is required"); - if (!File.Exists(sshkey)) return new ValidationResult("Given SSH private key can NOT be found!"); } diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 79696a7e..e1284b2f 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -1,8 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Avalonia.Collections; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -35,7 +33,7 @@ namespace SourceGit.ViewModels } else { - DiffContext = new DiffContext(_repo, new Models.DiffOption(value, _file), _diffContext); + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(value, _file), _diffContext); DetailContext.Commit = value; } } @@ -54,15 +52,15 @@ namespace SourceGit.ViewModels set => SetProperty(ref _detailContext, value); } - public FileHistories(string repo, string file, AvaloniaList issueTrackerRules) + public FileHistories(Repository repo, string file) { _repo = repo; _file = file; - _detailContext = new CommitDetail(repo, issueTrackerRules); + _detailContext = new CommitDetail(repo); Task.Run(() => { - var commits = new Commands.QueryCommits(_repo, $"-n 10000 -- \"{file}\"", false).Result(); + var commits = new Commands.QueryCommits(_repo.FullPath, $"-n 10000 -- \"{file}\"", false).Result(); Dispatcher.UIThread.Invoke(() => { IsLoading = false; @@ -73,7 +71,7 @@ namespace SourceGit.ViewModels }); } - private readonly string _repo = null; + private readonly Repository _repo = null; private readonly string _file = null; private bool _isLoading = true; private List _commits = null; diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 5a943e04..86a211fa 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Platform.Storage; using Avalonia.VisualTree; @@ -11,6 +10,11 @@ namespace SourceGit.ViewModels { public class Histories : ObservableObject { + public Repository Repo + { + get => _repo; + } + public bool IsLoading { get => _isLoading; @@ -55,11 +59,6 @@ namespace SourceGit.ViewModels set => SetProperty(ref _detailContext, value); } - public AvaloniaList IssueTrackerRules - { - get => _repo.Settings.IssueTrackerRules; - } - public Histories(Repository repo) { _repo = repo; @@ -99,7 +98,7 @@ namespace SourceGit.ViewModels } else { - var commitDetail = new CommitDetail(_repo.FullPath, _repo.Settings.IssueTrackerRules); + var commitDetail = new CommitDetail(_repo); commitDetail.Commit = commit; DetailContext = commitDetail; } @@ -127,7 +126,7 @@ namespace SourceGit.ViewModels } else { - var commitDetail = new CommitDetail(_repo.FullPath, _repo.Settings.IssueTrackerRules); + var commitDetail = new CommitDetail(_repo); commitDetail.Commit = commit; DetailContext = commitDetail; } @@ -249,7 +248,7 @@ namespace SourceGit.ViewModels reword.Icon = App.CreateMenuIcon("Icons.Edit"); reword.Click += (_, e) => { - if (_repo.WorkingCopyChangesCount > 0) + if (_repo.LocalChangesCount > 0) { App.RaiseException(_repo.FullPath, "You have local changes. Please run stash or discard first."); return; @@ -267,7 +266,7 @@ namespace SourceGit.ViewModels squash.IsEnabled = commit.Parents.Count == 1; squash.Click += (_, e) => { - if (_repo.WorkingCopyChangesCount > 0) + if (_repo.LocalChangesCount > 0) { App.RaiseException(_repo.FullPath, "You have local changes. Please run stash or discard first."); return; @@ -328,7 +327,7 @@ namespace SourceGit.ViewModels interactiveRebase.IsVisible = current.Head != commit.SHA; interactiveRebase.Click += (_, e) => { - if (_repo.WorkingCopyChangesCount > 0) + if (_repo.LocalChangesCount > 0) { App.RaiseException(_repo.FullPath, "You have local changes. Please run stash or discard first."); return; @@ -385,7 +384,7 @@ namespace SourceGit.ViewModels }; menu.Items.Add(compareWithHead); - if (_repo.WorkingCopyChangesCount > 0) + if (_repo.LocalChangesCount > 0) { var compareWithWorktree = new MenuItem(); compareWithWorktree.Header = App.Text("CommitCM.CompareWithWorktree"); diff --git a/src/ViewModels/InteractiveRebase.cs b/src/ViewModels/InteractiveRebase.cs index 32417e01..0c8838e0 100644 --- a/src/ViewModels/InteractiveRebase.cs +++ b/src/ViewModels/InteractiveRebase.cs @@ -114,7 +114,7 @@ namespace SourceGit.ViewModels Current = current; On = on; IsLoading = true; - DetailContext = new CommitDetail(repoPath, repo.Settings.IssueTrackerRules); + DetailContext = new CommitDetail(repo); Task.Run(() => { diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 71584098..7661b28a 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -141,7 +141,7 @@ namespace SourceGit.ViewModels var last = Pages[0]; if (last.Data is Repository repo) { - Commands.AutoFetch.RemoveRepository(repo.FullPath); + Models.AutoFetchManager.Instance.RemoveRepository(repo.FullPath); repo.Close(); last.Node = new RepositoryNode() { Id = Guid.NewGuid().ToString() }; @@ -245,7 +245,7 @@ namespace SourceGit.ViewModels }; repo.Open(); - Commands.AutoFetch.AddRepository(repo.FullPath); + Models.AutoFetchManager.Instance.AddRepository(repo.FullPath); if (page == null) { @@ -371,7 +371,7 @@ namespace SourceGit.ViewModels { if (page.Data is Repository repo) { - Commands.AutoFetch.RemoveRepository(repo.FullPath); + Models.AutoFetchManager.Instance.RemoveRepository(repo.FullPath); repo.Close(); } diff --git a/src/ViewModels/Preference.cs b/src/ViewModels/Preference.cs index 5a7ca874..33b5f99c 100644 --- a/src/ViewModels/Preference.cs +++ b/src/ViewModels/Preference.cs @@ -128,19 +128,6 @@ namespace SourceGit.ViewModels set => SetProperty(ref _layout, value); } - public string AvatarServer - { - get => Models.AvatarManager.SelectedServer; - set - { - if (Models.AvatarManager.SelectedServer != value) - { - Models.AvatarManager.SelectedServer = value; - OnPropertyChanged(); - } - } - } - public int MaxHistoryCommits { get => _maxHistoryCommits; @@ -262,9 +249,7 @@ namespace SourceGit.ViewModels set { if (Native.OS.SetShell(value)) - { OnPropertyChanged(); - } } } @@ -276,12 +261,12 @@ namespace SourceGit.ViewModels public bool GitAutoFetch { - get => Commands.AutoFetch.IsEnabled; + get => Models.AutoFetchManager.Instance.IsEnabled; set { - if (Commands.AutoFetch.IsEnabled != value) + if (Models.AutoFetchManager.Instance.IsEnabled != value) { - Commands.AutoFetch.IsEnabled = value; + Models.AutoFetchManager.Instance.IsEnabled = value; OnPropertyChanged(); } } @@ -289,15 +274,15 @@ namespace SourceGit.ViewModels public int? GitAutoFetchInterval { - get => Commands.AutoFetch.Interval; + get => Models.AutoFetchManager.Instance.Interval; set { - if (value is null or < 1) + if (value is null || value < 1) return; - if (Commands.AutoFetch.Interval != value) + if (Models.AutoFetchManager.Instance.Interval != value) { - Commands.AutoFetch.Interval = (int)value; + Models.AutoFetchManager.Instance.Interval = (int)value; OnPropertyChanged(); } } @@ -336,7 +321,7 @@ namespace SourceGit.ViewModels { get; set; - } = new List(); + } = []; public int LastActiveTabIdx { diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 5ef34d5b..e304be0b 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -142,14 +142,16 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _submodules, value); } - public int WorkingCopyChangesCount + public int LocalChangesCount { - get => _workingCopy == null ? 0 : _workingCopy.Count; + get => _localChangesCount; + private set => SetProperty(ref _localChangesCount, value); } public int StashesCount { - get => _stashesPage == null ? 0 : _stashesPage.Stashes.Count; + get => _stashesCount; + private set => SetProperty(ref _stashesCount, value); } public bool IncludeUntracked @@ -350,6 +352,9 @@ namespace SourceGit.ViewModels _stashesPage = null; _inProgressContext = null; + _localChangesCount = 0; + _stashesCount = 0; + _remotes.Clear(); _branches.Clear(); _localBranchTrees.Clear(); @@ -420,7 +425,7 @@ namespace SourceGit.ViewModels return menu; } - public void Fetch() + public void Fetch(bool autoStart) { if (!PopupHost.CanCreatePopup()) return; @@ -431,10 +436,13 @@ namespace SourceGit.ViewModels return; } - PopupHost.ShowPopup(new Fetch(this)); + if (autoStart) + PopupHost.ShowAndStartPopup(new Fetch(this)); + else + PopupHost.ShowPopup(new Fetch(this)); } - public void Pull() + public void Pull(bool autoStart) { if (!PopupHost.CanCreatePopup()) return; @@ -445,10 +453,13 @@ namespace SourceGit.ViewModels return; } - PopupHost.ShowPopup(new Pull(this, null)); + if (autoStart) + PopupHost.ShowAndStartPopup(new Pull(this, null)); + else + PopupHost.ShowPopup(new Pull(this, null)); } - public void Push() + public void Push(bool autoStart) { if (!PopupHost.CanCreatePopup()) return; @@ -465,7 +476,10 @@ namespace SourceGit.ViewModels return; } - PopupHost.ShowPopup(new Push(this, null)); + if (autoStart) + PopupHost.ShowAndStartPopup(new Push(this, null)); + else + PopupHost.ShowPopup(new Push(this, null)); } public void ApplyPatch() @@ -607,15 +621,9 @@ namespace SourceGit.ViewModels Task.Run(RefreshCommits); } - public void StashAll() + public void StashAll(bool autoStart) { - if (PopupHost.CanCreatePopup()) - { - var changes = new List(); - changes.AddRange(_workingCopy.Unstaged); - changes.AddRange(_workingCopy.Staged); - PopupHost.ShowPopup(new StashChanges(this, changes, true)); - } + _workingCopy?.StashAll(autoStart); } public void GotoResolve() @@ -812,7 +820,7 @@ namespace SourceGit.ViewModels { InProgressContext = inProgress; HasUnsolvedConflicts = hasUnsolvedConflict; - OnPropertyChanged(nameof(WorkingCopyChangesCount)); + LocalChangesCount = changes.Count; }); } @@ -823,7 +831,8 @@ namespace SourceGit.ViewModels { if (_stashesPage != null) _stashesPage.Stashes = stashes; - OnPropertyChanged(nameof(StashesCount)); + + StashesCount = stashes.Count; }); } @@ -856,7 +865,7 @@ namespace SourceGit.ViewModels if (branch.IsLocal) { - if (WorkingCopyChangesCount > 0) + if (_localChangesCount > 0) PopupHost.ShowPopup(new Checkout(this, branch.Name)); else PopupHost.ShowAndStartPopup(new Checkout(this, branch.Name)); @@ -1193,7 +1202,7 @@ namespace SourceGit.ViewModels var discard = new MenuItem(); discard.Header = App.Text("BranchCM.DiscardAll"); discard.Icon = App.CreateMenuIcon("Icons.Undo"); - discard.IsEnabled = _workingCopy.Count > 0; + discard.IsEnabled = _localChangesCount > 0; discard.Click += (_, e) => { if (PopupHost.CanCreatePopup()) @@ -1297,7 +1306,7 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); - if (WorkingCopyChangesCount > 0) + if (_localChangesCount > 0) { var compareWithWorktree = new MenuItem(); compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); @@ -1320,7 +1329,7 @@ namespace SourceGit.ViewModels var compareWithBranch = CreateMenuItemToCompareBranches(branch); if (compareWithBranch != null) { - if (WorkingCopyChangesCount == 0) + if (_localChangesCount == 0) menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(compareWithBranch); @@ -1597,7 +1606,7 @@ namespace SourceGit.ViewModels } var hasCompare = false; - if (WorkingCopyChangesCount > 0) + if (_localChangesCount > 0) { var compareWithWorktree = new MenuItem(); compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); @@ -1959,6 +1968,9 @@ namespace SourceGit.ViewModels private int _selectedViewIndex = 0; private object _selectedView = null; + private int _localChangesCount = 0; + private int _stashesCount = 0; + private bool _isSearching = false; private bool _isSearchLoadingVisible = false; private bool _isSearchCommitSuggestionOpen = false; diff --git a/src/ViewModels/RepositoryConfigure.cs b/src/ViewModels/RepositoryConfigure.cs index f252a075..b72345ee 100644 --- a/src/ViewModels/RepositoryConfigure.cs +++ b/src/ViewModels/RepositoryConfigure.cs @@ -42,6 +42,17 @@ namespace SourceGit.ViewModels set => SetProperty(ref _httpProxy, value); } + public AvaloniaList CommitTemplates + { + get => _repo.Settings.CommitTemplates; + } + + public Models.CommitTemplate SelectedCommitTemplate + { + get => _selectedCommitTemplate; + set => SetProperty(ref _selectedCommitTemplate, value); + } + public AvaloniaList IssueTrackerRules { get => _repo.Settings.IssueTrackerRules; @@ -77,6 +88,20 @@ namespace SourceGit.ViewModels HttpProxy = string.Empty; } + public void AddCommitTemplate() + { + var template = new Models.CommitTemplate() { Name = "New Template" }; + _repo.Settings.CommitTemplates.Add(template); + SelectedCommitTemplate = template; + } + + public void RemoveSelectedCommitTemplate() + { + if (_selectedCommitTemplate != null) + _repo.Settings.CommitTemplates.Remove(_selectedCommitTemplate); + SelectedCommitTemplate = null; + } + public void AddSampleGithubIssueTracker() { foreach (var remote in _repo.Remotes) @@ -106,7 +131,8 @@ namespace SourceGit.ViewModels public void RemoveSelectedIssueTracker() { - _repo.Settings.RemoveIssueTracker(_selectedIssueTrackerRule); + if (_selectedIssueTrackerRule != null) + _repo.Settings.RemoveIssueTracker(_selectedIssueTrackerRule); SelectedIssueTrackerRule = null; } @@ -141,6 +167,7 @@ namespace SourceGit.ViewModels private readonly Repository _repo = null; private readonly Dictionary _cached = null; private string _httpProxy; + private Models.CommitTemplate _selectedCommitTemplate = null; private Models.IssueTrackerRule _selectedIssueTrackerRule = null; } } diff --git a/src/ViewModels/Reword.cs b/src/ViewModels/Reword.cs index ce2f4d6c..7ec873c8 100644 --- a/src/ViewModels/Reword.cs +++ b/src/ViewModels/Reword.cs @@ -39,7 +39,7 @@ namespace SourceGit.ViewModels return Task.Run(() => { - var succ = new Commands.Commit(_repo.FullPath, _message, false, true, true).Exec(); + var succ = new Commands.Commit(_repo.FullPath, _message, true, true).Exec(); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; }); diff --git a/src/ViewModels/Squash.cs b/src/ViewModels/Squash.cs index a24f5152..f0f4b8bb 100644 --- a/src/ViewModels/Squash.cs +++ b/src/ViewModels/Squash.cs @@ -43,7 +43,7 @@ namespace SourceGit.ViewModels { var succ = new Commands.Reset(_repo.FullPath, Parent.SHA, "--soft").Exec(); if (succ) - succ = new Commands.Commit(_repo.FullPath, _message, false, true).Exec(); + succ = new Commands.Commit(_repo.FullPath, _message, true).Exec(); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; }); diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 3e93d82b..e34f06c5 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -177,8 +177,6 @@ namespace SourceGit.ViewModels } } - public int Count => _count; - public object DetailContext { get => _detailContext; @@ -317,6 +315,17 @@ namespace SourceGit.ViewModels dialog.ShowDialog(toplevel); } + public void StashAll(bool autoStart) + { + if (!PopupHost.CanCreatePopup()) + return; + + if (autoStart) + PopupHost.ShowAndStartPopup(new StashChanges(_repo, _cached, true)); + else + PopupHost.ShowPopup(new StashChanges(_repo, _cached, true)); + } + public void StageSelected() { StageChanges(_selectedUnstaged); @@ -556,7 +565,7 @@ namespace SourceGit.ViewModels history.Icon = App.CreateMenuIcon("Icons.Histories"); history.Click += (_, e) => { - var window = new Views.FileHistories() { DataContext = new FileHistories(_repo.FullPath, change.Path, _repo.Settings.IssueTrackerRules) }; + var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) }; window.Show(); e.Handled = true; }; @@ -1118,34 +1127,62 @@ namespace SourceGit.ViewModels public ContextMenu CreateContextMenuForCommitMessages() { var menu = new ContextMenu(); - if (_repo.Settings.CommitMessages.Count == 0) + + var templateCount = _repo.Settings.CommitTemplates.Count; + if (templateCount == 0) { - var empty = new MenuItem(); - empty.Header = App.Text("WorkingCopy.NoCommitHistories"); - empty.IsEnabled = false; - menu.Items.Add(empty); - return menu; + menu.Items.Add(new MenuItem() + { + Header = App.Text("WorkingCopy.NoCommitTemplates"), + Icon = App.CreateMenuIcon("Icons.Code"), + IsEnabled = false + }); + } + else + { + for (int i = 0; i < templateCount; i++) + { + var template = _repo.Settings.CommitTemplates[i]; + var item = new MenuItem(); + item.Header = new Views.NameHighlightedTextBlock("WorkingCopy.UseCommitTemplate", template.Name); + item.Icon = App.CreateMenuIcon("Icons.Code"); + item.Click += (_, e) => + { + CommitMessage = template.Content; + e.Handled = true; + }; + menu.Items.Add(item); + } } - var tip = new MenuItem(); - tip.Header = App.Text("WorkingCopy.HasCommitHistories"); - tip.IsEnabled = false; - menu.Items.Add(tip); menu.Items.Add(new MenuItem() { Header = "-" }); - foreach (var message in _repo.Settings.CommitMessages) + var historiesCount = _repo.Settings.CommitMessages.Count; + if (historiesCount == 0) { - var dump = message; - - var item = new MenuItem(); - item.Header = dump; - item.Click += (_, e) => + menu.Items.Add(new MenuItem() { - CommitMessage = dump; - e.Handled = true; - }; + Header = App.Text("WorkingCopy.NoCommitHistories"), + Icon = App.CreateMenuIcon("Icons.Histories"), + IsEnabled = false + }); + } + else + { + for (int i = 0; i < historiesCount; i++) + { + var message = _repo.Settings.CommitMessages[i]; + var item = new MenuItem(); + item.Header = message; + item.Icon = App.CreateMenuIcon("Icons.Histories"); + item.Click += (_, e) => + { + CommitMessage = message; + e.Handled = true; + }; - menu.Items.Add(item); + menu.Items.Add(item); + } } return menu; @@ -1245,9 +1282,10 @@ namespace SourceGit.ViewModels return; } + var autoStage = AutoStageBeforeCommit; if (!_useAmend) { - if (AutoStageBeforeCommit) + if (autoStage) { if (_count == 0) { @@ -1269,26 +1307,28 @@ namespace SourceGit.ViewModels _repo.Settings.PushCommitMessage(_commitMessage); _repo.SetWatcherEnabled(false); - var autoStage = AutoStageBeforeCommit; Task.Run(() => { - var succ = new Commands.Commit(_repo.FullPath, _commitMessage, autoStage, _useAmend).Exec(); + var succ = true; + if (autoStage && _unstaged.Count > 0) + succ = new Commands.Add(_repo.FullPath).Exec(); + + if (succ) + succ = new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec(); + Dispatcher.UIThread.Post(() => { if (succ) { - SelectedStaged = []; CommitMessage = string.Empty; UseAmend = false; if (autoPush) - { PopupHost.ShowAndStartPopup(new Push(_repo, null)); - } } + _repo.MarkWorkingCopyDirtyManually(); _repo.SetWatcherEnabled(true); - IsCommitting = false; }); }); diff --git a/src/Views/Avatar.cs b/src/Views/Avatar.cs index e48f972e..24ac229f 100644 --- a/src/Views/Avatar.cs +++ b/src/Views/Avatar.cs @@ -39,7 +39,7 @@ namespace SourceGit.Views refetch.Click += (_, _) => { if (User != null) - Models.AvatarManager.Request(User.Email, true); + Models.AvatarManager.Instance.Request(User.Email, true); }; ContextMenu = new ContextMenu(); @@ -54,7 +54,7 @@ namespace SourceGit.Views return; var corner = (float)Math.Max(2, Bounds.Width / 16); - var img = Models.AvatarManager.Request(User.Email, false); + var img = Models.AvatarManager.Instance.Request(User.Email, false); if (img != null) { var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); @@ -72,21 +72,19 @@ namespace SourceGit.Views public void OnAvatarResourceChanged(string email) { if (User.Email.Equals(email, StringComparison.Ordinal)) - { InvalidateVisual(); - } } protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); - Models.AvatarManager.Subscribe(this); + Models.AvatarManager.Instance.Subscribe(this); } protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); - Models.AvatarManager.Unsubscribe(this); + Models.AvatarManager.Instance.Unsubscribe(this); } private static void OnUserPropertyChanged(Avatar avatar, AvaloniaPropertyChangedEventArgs e) diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs index d873fdce..6e500e8e 100644 --- a/src/Views/Blame.axaml.cs +++ b/src/Views/Blame.axaml.cs @@ -40,6 +40,9 @@ namespace SourceGit.Views foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var lineNumber = line.FirstDocumentLine.LineNumber; if (lineNumber > _editor.BlameData.LineInfos.Count) break; @@ -151,6 +154,9 @@ namespace SourceGit.Views foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var lineNumber = line.FirstDocumentLine.LineNumber; if (lineNumber >= _editor.BlameData.LineInfos.Count) break; diff --git a/src/Views/BranchCompare.axaml b/src/Views/BranchCompare.axaml index 0e6d4b86..b87570e2 100644 --- a/src/Views/BranchCompare.axaml +++ b/src/Views/BranchCompare.axaml @@ -59,7 +59,7 @@ - + @@ -83,7 +83,7 @@ - + diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml index 366814fe..59b0b609 100644 --- a/src/Views/BranchTree.axaml +++ b/src/Views/BranchTree.axaml @@ -83,20 +83,13 @@ FontWeight="{Binding NameFontWeight}"/> - - - + FontFamilyProperty = + TextBlock.FontFamilyProperty.AddOwner(); + + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + public static readonly StyledProperty FontSizeProperty = + TextBlock.FontSizeProperty.AddOwner(); + + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground), Brushes.White); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background), Brushes.White); + + public IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + static BranchTreeNodeTrackStatusPresenter() + { + AffectsMeasure( + FontSizeProperty, + FontFamilyProperty, + ForegroundProperty); + + AffectsRender( + ForegroundProperty, + BackgroundProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (_label != null) + { + context.DrawRectangle(Background, null, new RoundedRect(new Rect(0, 0, _label.Width + 18, 18), new CornerRadius(9))); + context.DrawText(_label, new Point(9, 9 - _label.Height * 0.5)); + } + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + InvalidateMeasure(); + InvalidateVisual(); + } + + protected override Size MeasureOverride(Size availableSize) + { + _label = null; + + if (DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + { + var status = branch.TrackStatus.ToString(); + if (!string.IsNullOrEmpty(status)) + { + _label = new FormattedText( + status, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily), + FontSize, + Foreground); + } + } + + return _label != null ? new Size(_label.Width + 18, 18) : new Size(0, 0); + } + + private FormattedText _label = null; + } + public partial class BranchTree : UserControl { public static readonly StyledProperty> NodesProperty = diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index d2c57810..70da7fcd 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -54,7 +54,17 @@ - + + + + + + + + @@ -71,6 +81,7 @@ Text="{Binding Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange" TextDecorations="Underline" + Cursor="Hand" Margin="0,0,16,0" PointerPressed="OnParentSHAPressed"/> diff --git a/src/Views/CommitBaseInfo.axaml.cs b/src/Views/CommitBaseInfo.axaml.cs index 86451dfe..f7c44f17 100644 --- a/src/Views/CommitBaseInfo.axaml.cs +++ b/src/Views/CommitBaseInfo.axaml.cs @@ -2,20 +2,12 @@ using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; namespace SourceGit.Views { public partial class CommitBaseInfo : UserControl { - public static readonly StyledProperty CanNavigateProperty = - AvaloniaProperty.Register(nameof(CanNavigate), true); - - public bool CanNavigate - { - get => GetValue(CanNavigateProperty); - set => SetValue(CanNavigateProperty, value); - } - public static readonly StyledProperty MessageProperty = AvaloniaProperty.Register(nameof(Message), string.Empty); @@ -25,6 +17,15 @@ namespace SourceGit.Views set => SetValue(MessageProperty, value); } + public static readonly StyledProperty> WebLinksProperty = + AvaloniaProperty.Register>(nameof(WebLinks)); + + public AvaloniaList WebLinks + { + get => GetValue(WebLinksProperty); + set => SetValue(WebLinksProperty, value); + } + public static readonly StyledProperty> IssueTrackerRulesProperty = AvaloniaProperty.Register>(nameof(IssueTrackerRules)); @@ -39,11 +40,43 @@ namespace SourceGit.Views InitializeComponent(); } + private void OnOpenWebLink(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.CommitDetail detail) + { + var links = WebLinks; + if (links.Count > 1) + { + var menu = new ContextMenu(); + + foreach (var link in links) + { + var url = $"{link.URLPrefix}{detail.Commit.SHA}"; + var item = new MenuItem() { Header = link.Name }; + item.Click += (_, ev) => + { + Native.OS.OpenBrowser(url); + ev.Handled = true; + }; + + menu.Items.Add(item); + } + + (sender as Control)?.OpenContextMenu(menu); + } + else if (links.Count == 1) + { + var url = $"{links[0].URLPrefix}{detail.Commit.SHA}"; + Native.OS.OpenBrowser(url); + } + } + + e.Handled = true; + } + private void OnParentSHAPressed(object sender, PointerPressedEventArgs e) { - if (sender is Control { DataContext: string sha } && - DataContext is ViewModels.CommitDetail detail && - CanNavigate) + if (DataContext is ViewModels.CommitDetail detail && sender is Control { DataContext: string sha }) { detail.NavigateTo(sha); } diff --git a/src/Views/CommitDetail.axaml b/src/Views/CommitDetail.axaml index af733f49..432fa737 100644 --- a/src/Views/CommitDetail.axaml +++ b/src/Views/CommitDetail.axaml @@ -21,6 +21,7 @@ diff --git a/src/Views/CommitMessagePresenter.cs b/src/Views/CommitMessagePresenter.cs index 116442d9..c00fb50c 100644 --- a/src/Views/CommitMessagePresenter.cs +++ b/src/Views/CommitMessagePresenter.cs @@ -6,6 +6,7 @@ using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Input; +using Avalonia.Utilities; namespace SourceGit.Views { @@ -38,6 +39,8 @@ namespace SourceGit.Views if (change.Property == MessageProperty || change.Property == IssueTrackerRulesProperty) { Inlines.Clear(); + _matches = null; + ClearHoveredIssueLink(); var message = Message; if (string.IsNullOrEmpty(message)) @@ -61,6 +64,7 @@ namespace SourceGit.Views } matches.Sort((l, r) => l.Start - r.Start); + _matches = matches; int pos = 0; foreach (var match in matches) @@ -68,12 +72,9 @@ namespace SourceGit.Views if (match.Start > pos) Inlines.Add(new Run(message.Substring(pos, match.Start - pos))); - var link = new TextBlock(); - link.SetValue(TextProperty, message.Substring(match.Start, match.Length)); - link.SetValue(ToolTip.TipProperty, match.URL); - link.Classes.Add("issue_link"); - link.PointerPressed += OnLinkPointerPressed; - Inlines.Add(link); + match.Link = new Run(message.Substring(match.Start, match.Length)); + match.Link.Classes.Add("issue_link"); + Inlines.Add(match.Link); pos = match.Start + match.Length; } @@ -83,16 +84,71 @@ namespace SourceGit.Views } } - private void OnLinkPointerPressed(object sender, PointerPressedEventArgs e) + protected override void OnPointerMoved(PointerEventArgs e) { - if (sender is TextBlock text) - { - var tooltip = text.GetValue(ToolTip.TipProperty) as string; - if (!string.IsNullOrEmpty(tooltip)) - Native.OS.OpenBrowser(tooltip); + base.OnPointerMoved(e); - e.Handled = true; + if (e.Pointer.Captured == null && _matches != null) + { + var padding = Padding; + var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); + point = new Point( + MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)), + MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Height, 0))); + + var pos = TextLayout.HitTestPoint(point).TextPosition; + foreach (var match in _matches) + { + if (!match.Intersect(pos, 1)) + continue; + + if (match == _lastHover) + return; + + _lastHover = match; + //_lastHover.Link.Classes.Add("issue_link_hovered"); + + SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); + ToolTip.SetTip(this, match.URL); + ToolTip.SetIsOpen(this, true); + return; + } + + ClearHoveredIssueLink(); } } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (_lastHover != null) + { + e.Pointer.Capture(null); + Native.OS.OpenBrowser(_lastHover.URL); + e.Handled = true; + return; + } + + base.OnPointerPressed(e); + } + + protected override void OnPointerExited(PointerEventArgs e) + { + base.OnPointerExited(e); + ClearHoveredIssueLink(); + } + + private void ClearHoveredIssueLink() + { + if (_lastHover != null) + { + ToolTip.SetTip(this, null); + SetCurrentValue(CursorProperty, Cursor.Parse("IBeam")); + //_lastHover.Link.Classes.Remove("issue_link_hovered"); + _lastHover = null; + } + } + + private List _matches = null; + private Models.IssueTrackerMatch _lastHover = null; } } diff --git a/src/Views/CommitRefsPresenter.cs b/src/Views/CommitRefsPresenter.cs index f941d4a5..da842182 100644 --- a/src/Views/CommitRefsPresenter.cs +++ b/src/Views/CommitRefsPresenter.cs @@ -191,9 +191,11 @@ namespace SourceGit.Views requiredWidth += label.Width + 16 /* icon */ + 8 /* label margin */ + 4 /* item right margin */; } + InvalidateVisual(); return new Size(requiredWidth, 16); } + InvalidateVisual(); return new Size(0, 0); } diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml index b573a32d..9e927205 100644 --- a/src/Views/DiffView.axaml +++ b/src/Views/DiffView.axaml @@ -176,7 +176,7 @@ - + @@ -190,7 +190,7 @@ - + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 91190315..0033b93a 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -77,7 +77,7 @@ FontSize="10" VerticalAlignment="Center"/> - l.Start - r.Start); + _matches = matches; int pos = 0; foreach (var match in matches) @@ -215,32 +219,82 @@ namespace SourceGit.Views if (match.Start > pos) Inlines.Add(new Run(subject.Substring(pos, match.Start - pos))); - var link = new TextBlock(); - link.SetValue(TextProperty, subject.Substring(match.Start, match.Length)); - link.SetValue(ToolTip.TipProperty, match.URL); - link.Classes.Add("issue_link"); - link.PointerPressed += OnLinkPointerPressed; - Inlines.Add(link); + match.Link = new Run(subject.Substring(match.Start, match.Length)); + match.Link.Classes.Add("issue_link"); + Inlines.Add(match.Link); pos = match.Start + match.Length; } if (pos < subject.Length) Inlines.Add(new Run(subject.Substring(pos))); + + InvalidateTextLayout(); } } - private void OnLinkPointerPressed(object sender, PointerPressedEventArgs e) + protected override void OnPointerMoved(PointerEventArgs e) { - if (sender is TextBlock text) - { - var tooltip = text.GetValue(ToolTip.TipProperty) as string; - if (!string.IsNullOrEmpty(tooltip)) - Native.OS.OpenBrowser(tooltip); + base.OnPointerMoved(e); - e.Handled = true; + if (_matches != null) + { + var padding = Padding; + var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); + point = new Point( + MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)), + MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Height, 0))); + + var textPosition = TextLayout.HitTestPoint(point).TextPosition; + foreach (var match in _matches) + { + if (!match.Intersect(textPosition, 1)) + continue; + + if (match == _lastHover) + return; + + _lastHover = match; + //_lastHover.Link.Classes.Add("issue_link_hovered"); + + SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); + ToolTip.SetTip(this, match.URL); + ToolTip.SetIsOpen(this, true); + e.Handled = true; + return; + } + + ClearHoveredIssueLink(); } } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + if (_lastHover != null) + Native.OS.OpenBrowser(_lastHover.URL); + } + + protected override void OnPointerExited(PointerEventArgs e) + { + base.OnPointerExited(e); + ClearHoveredIssueLink(); + } + + private void ClearHoveredIssueLink() + { + if (_lastHover != null) + { + ToolTip.SetTip(this, null); + SetCurrentValue(CursorProperty, Cursor.Parse("Arrow")); + //_lastHover.Link.Classes.Remove("issue_link_hovered"); + _lastHover = null; + } + } + + private List _matches = null; + private Models.IssueTrackerMatch _lastHover = null; } public class CommitTimeTextBlock : TextBlock @@ -541,6 +595,15 @@ namespace SourceGit.Views set => SetValue(CurrentBranchProperty, value); } + public static readonly StyledProperty> IssueTrackerRulesProperty = + AvaloniaProperty.Register>(nameof(IssueTrackerRules)); + + public AvaloniaList IssueTrackerRules + { + get => GetValue(IssueTrackerRulesProperty); + set => SetValue(IssueTrackerRulesProperty, value); + } + public static readonly StyledProperty NavigationIdProperty = AvaloniaProperty.Register(nameof(NavigationId)); @@ -550,16 +613,6 @@ namespace SourceGit.Views set => SetValue(NavigationIdProperty, value); } - public AvaloniaList IssueTrackerRules - { - get - { - if (DataContext is ViewModels.Histories histories) - return histories.IssueTrackerRules; - return null; - } - } - static Histories() { NavigationIdProperty.Changed.AddClassHandler((h, _) => diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml index 4a3ff4c5..90a46717 100644 --- a/src/Views/Launcher.axaml +++ b/src/Views/Launcher.axaml @@ -41,6 +41,11 @@ + + + + + diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index dc9f91d2..e73ad1a9 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -20,6 +20,11 @@ namespace SourceGit.Views InitializeComponent(); } + public bool HasKeyModifier(KeyModifiers modifier) + { + return _unhandledModifiers.HasFlag(modifier); + } + protected override void OnOpened(EventArgs e) { base.OnOpened(e); @@ -147,6 +152,27 @@ namespace SourceGit.Views } base.OnKeyDown(e); + + // Record unhandled key modifers. + if (!e.Handled) + { + _unhandledModifiers = e.KeyModifiers; + + if (!_unhandledModifiers.HasFlag(KeyModifiers.Alt) && (e.Key == Key.LeftAlt || e.Key == Key.RightAlt)) + _unhandledModifiers |= KeyModifiers.Alt; + + if (!_unhandledModifiers.HasFlag(KeyModifiers.Control) && (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl)) + _unhandledModifiers |= KeyModifiers.Control; + + if (!_unhandledModifiers.HasFlag(KeyModifiers.Shift) && (e.Key == Key.LeftShift || e.Key == Key.RightShift)) + _unhandledModifiers |= KeyModifiers.Shift; + } + } + + protected override void OnKeyUp(KeyEventArgs e) + { + base.OnKeyUp(e); + _unhandledModifiers = KeyModifiers.None; } protected override void OnClosing(WindowClosingEventArgs e) @@ -178,5 +204,7 @@ namespace SourceGit.Views e.Handled = true; } + + private KeyModifiers _unhandledModifiers = KeyModifiers.None; } } diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index 79eda516..6b34a1f1 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -57,7 +57,7 @@ - + - - - https://www.gravatar.com/avatar/ - https://cravatar.cn/avatar/ - - - - - - - - - + - - - diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 9c111eb7..d60f729b 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -91,15 +91,14 @@ - - - + @@ -107,15 +106,14 @@ - - - + @@ -670,7 +668,8 @@ - diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 0e500b2c..a83d24bd 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -1,12 +1,109 @@ using System; +using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Media; namespace SourceGit.Views { + public class CounterPresenter : Control + { + public static readonly StyledProperty CountProperty = + AvaloniaProperty.Register(nameof(Count), 0); + + public int Count + { + get => GetValue(CountProperty); + set => SetValue(CountProperty, value); + } + + public static readonly StyledProperty FontFamilyProperty = + TextBlock.FontFamilyProperty.AddOwner(); + + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + public static readonly StyledProperty FontSizeProperty = + TextBlock.FontSizeProperty.AddOwner(); + + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground), Brushes.White); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background), Brushes.White); + + public IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + static CounterPresenter() + { + AffectsMeasure( + FontSizeProperty, + FontFamilyProperty, + ForegroundProperty, + CountProperty); + + AffectsRender( + ForegroundProperty, + BackgroundProperty, + CountProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (_label != null) + { + context.DrawRectangle(Background, null, new RoundedRect(new Rect(0, 0, _label.Width + 18, 18), new CornerRadius(9))); + context.DrawText(_label, new Point(9, 9 - _label.Height * 0.5)); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + if (Count > 0) + { + _label = new FormattedText( + Count.ToString(), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily), + FontSize, + Foreground); + } + else + { + _label = null; + } + + return _label != null ? new Size(_label.Width + 18, 18) : new Size(0, 0); + } + + private FormattedText _label = null; + } + public partial class Repository : UserControl { public Repository() @@ -164,7 +261,7 @@ namespace SourceGit.Views if (!IsLoaded) return; - var leftHeight = LeftSidebarGroups.Bounds.Height - 28.0 * 5; + var leftHeight = LeftSidebarGroups.Bounds.Height - 28.0 * 5 - 4; var localBranchRows = vm.IsLocalBranchGroupExpanded ? LocalBranchTree.Rows.Count : 0; var remoteBranchRows = vm.IsRemoteGroupExpanded ? RemoteBranchTree.Rows.Count : 0; var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index 79bd7a85..5ecaee22 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -112,6 +112,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/RepositoryToolbar.axaml b/src/Views/RepositoryToolbar.axaml index 2f7a1d48..b76cfd63 100644 --- a/src/Views/RepositoryToolbar.axaml +++ b/src/Views/RepositoryToolbar.axaml @@ -31,19 +31,47 @@ - + + + + + + + + - + + + + + + + + - + + + + + + + + - + + + + + + + + diff --git a/src/Views/RepositoryToolbar.axaml.cs b/src/Views/RepositoryToolbar.axaml.cs index 57fed44b..27ac43cd 100644 --- a/src/Views/RepositoryToolbar.axaml.cs +++ b/src/Views/RepositoryToolbar.axaml.cs @@ -1,5 +1,7 @@ using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace SourceGit.Views { @@ -40,6 +42,34 @@ namespace SourceGit.Views } } + private void Fetch(object _, RoutedEventArgs e) + { + var launcher = this.FindAncestorOfType(); + (DataContext as ViewModels.Repository)?.Fetch(launcher?.HasKeyModifier(KeyModifiers.Control) ?? false); + e.Handled = true; + } + + private void Pull(object _, RoutedEventArgs e) + { + var launcher = this.FindAncestorOfType(); + (DataContext as ViewModels.Repository)?.Pull(launcher?.HasKeyModifier(KeyModifiers.Control) ?? false); + e.Handled = true; + } + + private void Push(object _, RoutedEventArgs e) + { + var launcher = this.FindAncestorOfType(); + (DataContext as ViewModels.Repository)?.Push(launcher?.HasKeyModifier(KeyModifiers.Control) ?? false); + e.Handled = true; + } + + private void StashAll(object _, RoutedEventArgs e) + { + var launcher = this.FindAncestorOfType(); + (DataContext as ViewModels.Repository)?.StashAll(launcher?.HasKeyModifier(KeyModifiers.Control) ?? false); + e.Handled = true; + } + private void OpenGitFlowMenu(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.Repository repo) diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index 1d264362..f6303b45 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -24,7 +24,7 @@ - + diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index 5066195d..aef71e19 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -73,7 +73,7 @@ - + diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index d5f6194a..464339d8 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -41,8 +41,8 @@ namespace SourceGit.Views Math.Abs(Height - old.Height) > 0.001 || StartIdx != old.StartIdx || EndIdx != old.EndIdx || - Combined != Combined || - IsOldSide != IsOldSide; + Combined != old.Combined || + IsOldSide != old.IsOldSide; } } @@ -92,6 +92,9 @@ namespace SourceGit.Views var typeface = view.CreateTypeface(); foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > lines.Count) break; @@ -160,7 +163,7 @@ namespace SourceGit.Views var width = textView.Bounds.Width; foreach (var line in textView.VisualLines) { - if (line.FirstDocumentLine == null) + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) continue; var index = line.FirstDocumentLine.LineNumber; @@ -172,8 +175,47 @@ namespace SourceGit.Views if (bg == null) continue; - var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - textView.VerticalOffset; - drawingContext.DrawRectangle(bg, null, new Rect(0, y, width, line.Height)); + var startY = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineTop) - textView.VerticalOffset; + var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - textView.VerticalOffset; + drawingContext.DrawRectangle(bg, null, new Rect(0, startY, width, endY - startY)); + + if (info.Highlights.Count > 0) + { + var highlightBG = info.Type == Models.TextDiffLineType.Added ? _presenter.AddedHighlightBrush : _presenter.DeletedHighlightBrush; + var processingIdxStart = 0; + var processingIdxEnd = 0; + var nextHightlight = 0; + + foreach (var tl in line.TextLines) + { + processingIdxEnd += tl.Length; + + var y = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineTop) - textView.VerticalOffset; + var h = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineBottom) - textView.VerticalOffset - y; + + while (nextHightlight < info.Highlights.Count) + { + var highlight = info.Highlights[nextHightlight]; + if (highlight.Start >= processingIdxEnd) + break; + + var start = line.GetVisualColumn(highlight.Start < processingIdxStart ? processingIdxStart : highlight.Start); + var end = line.GetVisualColumn(highlight.End >= processingIdxEnd ? processingIdxEnd : highlight.End + 1); + + var x = line.GetTextLineVisualXPosition(tl, start) - textView.HorizontalOffset; + var w = line.GetTextLineVisualXPosition(tl, end) - textView.HorizontalOffset - x; + var rect = new Rect(x, y, w, h); + drawingContext.DrawRectangle(highlightBG, null, rect); + + if (highlight.End >= processingIdxEnd) + break; + + nextHightlight++; + } + + processingIdxStart = processingIdxEnd; + } + } } } @@ -217,20 +259,6 @@ namespace SourceGit.Views v.TextRunProperties.SetForegroundBrush(_presenter.IndicatorForeground); v.TextRunProperties.SetTypeface(new Typeface(_presenter.FontFamily, FontStyle.Italic)); }); - - return; - } - - if (info.Highlights.Count > 0) - { - var bg = info.Type == Models.TextDiffLineType.Added ? _presenter.AddedHighlightBrush : _presenter.DeletedHighlightBrush; - foreach (var highlight in info.Highlights) - { - ChangeLinePart(line.Offset + highlight.Start, line.Offset + highlight.Start + highlight.Count, v => - { - v.TextRunProperties.SetBackgroundBrush(bg); - }); - } } } @@ -394,7 +422,7 @@ namespace SourceGit.Views if (chunk == null || (!chunk.Combined && chunk.IsOldSide != IsOld)) return; - var color = (Color)this.FindResource("SystemAccentColor"); + var color = (Color)this.FindResource("SystemAccentColor")!; var brush = new SolidColorBrush(color, 0.1); var pen = new Pen(color.ToUInt32()); var rect = new Rect(0, chunk.Y, Bounds.Width, chunk.Height); @@ -409,6 +437,7 @@ namespace SourceGit.Views base.OnLoaded(e); TextArea.TextView.ContextRequested += OnTextViewContextRequested; + TextArea.TextView.PointerEntered += OnTextViewPointerEntered; TextArea.TextView.PointerMoved += OnTextViewPointerMoved; TextArea.TextView.PointerWheelChanged += OnTextViewPointerWheelChanged; @@ -420,6 +449,7 @@ namespace SourceGit.Views base.OnUnloaded(e); TextArea.TextView.ContextRequested -= OnTextViewContextRequested; + TextArea.TextView.PointerEntered -= OnTextViewPointerEntered; TextArea.TextView.PointerMoved -= OnTextViewPointerMoved; TextArea.TextView.PointerWheelChanged -= OnTextViewPointerWheelChanged; @@ -480,6 +510,12 @@ namespace SourceGit.Views e.Handled = true; } + private void OnTextViewPointerEntered(object sender, PointerEventArgs e) + { + if (EnableChunkSelection && sender is TextView view) + UpdateSelectedChunk(e.GetPosition(view).Y + view.VerticalOffset); + } + private void OnTextViewPointerMoved(object sender, PointerEventArgs e) { if (EnableChunkSelection && sender is TextView view) @@ -675,12 +711,7 @@ namespace SourceGit.Views var firstLineIdx = view.VisualLines[0].FirstDocumentLine.LineNumber - 1; var lastLineIdx = view.VisualLines[^1].FirstDocumentLine.LineNumber - 1; - if (endIdx < firstLineIdx) - { - TrySetChunk(null); - return; - } - else if (startIdx > lastLineIdx) + if (endIdx < firstLineIdx || startIdx > lastLineIdx) { TrySetChunk(null); return; @@ -711,6 +742,9 @@ namespace SourceGit.Views var lineIdx = -1; foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > diff.Lines.Count) break; @@ -853,12 +887,7 @@ namespace SourceGit.Views var firstLineIdx = view.VisualLines[0].FirstDocumentLine.LineNumber - 1; var lastLineIdx = view.VisualLines[^1].FirstDocumentLine.LineNumber - 1; - if (endIdx < firstLineIdx) - { - TrySetChunk(null); - return; - } - else if (startIdx > lastLineIdx) + if (endIdx < firstLineIdx || startIdx > lastLineIdx) { TrySetChunk(null); return; @@ -895,6 +924,9 @@ namespace SourceGit.Views var lineIdx = -1; foreach (var line in view.VisualLines) { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > lines.Count) break; @@ -1129,7 +1161,7 @@ namespace SourceGit.Views SetCurrentValue(SelectedChunkProperty, null); } - private void OnStageChunk(object sender, RoutedEventArgs e) + private void OnStageChunk(object _1, RoutedEventArgs _2) { var chunk = SelectedChunk; if (chunk == null) @@ -1187,7 +1219,7 @@ namespace SourceGit.Views repo.SetWatcherEnabled(true); } - private void OnUnstageChunk(object sender, RoutedEventArgs e) + private void OnUnstageChunk(object _1, RoutedEventArgs _2) { var chunk = SelectedChunk; if (chunk == null) @@ -1241,7 +1273,7 @@ namespace SourceGit.Views repo.SetWatcherEnabled(true); } - private void OnDiscardChunk(object sender, RoutedEventArgs e) + private void OnDiscardChunk(object _1, RoutedEventArgs _2) { var chunk = SelectedChunk; if (chunk == null) diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 4727ce20..b8cab0e8 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -175,11 +175,13 @@ - + Classes="no_border" + Margin="4,0,0,0" Padding="0" + Click="OnOpenCommitMessagePicker"> + + + + + Content="{DynamicResource Text.WorkingCopy.AutoStage}"/>
Open-source GUI client for git users