diff --git a/.editorconfig b/.editorconfig index dedc5722..56725e7b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -100,7 +100,7 @@ dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal dotnet_naming_style.camel_case_underscore_style.required_prefix = _ -dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case # use accessibility modifiers dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion @@ -206,6 +206,9 @@ dotnet_diagnostic.CA1854.severity = warning #CA2211:Non-constant fields should not be visible dotnet_diagnostic.CA2211.severity = error +# IDE0005: remove used namespace using +dotnet_diagnostic.IDE0005.severity = error + # Wrapping preferences csharp_wrap_before_ternary_opsigns = false @@ -292,3 +295,12 @@ indent_size = 2 end_of_line = lf [*.{cmd,bat}] end_of_line = crlf + +# Package manifests +[{*.spec,control}] +end_of_line = lf + +# YAML files +[*.{yml,yaml}] +indent_size = 2 +end_of_line = lf diff --git a/.gitattributes b/.gitattributes index 7410eb08..bd1dfea9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,10 +3,12 @@ *.png binary *.ico binary *.sh text eol=lf +*.spec text eol=lf +control text eol=lf *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf *.json text .gitattributes export-ignore -.gitignore export-ignore \ No newline at end of file +.gitignore export-ignore diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad62efb6..12792cf6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,20 +19,31 @@ jobs: os: macos-latest runtime: osx-arm64 - name : Linux - os: ubuntu-20.04 + os: ubuntu-latest runtime: linux-x64 + container: ubuntu:20.04 - name : Linux (arm64) - os: ubuntu-20.04 + os: ubuntu-latest runtime: linux-arm64 + container: ubuntu:20.04 name: Build ${{ matrix.name }} runs-on: ${{ matrix.os }} + container: ${{ matrix.container || '' }} steps: + - name: Install common CLI tools + if: ${{ startsWith(matrix.runtime, 'linux-') }} + run: | + export DEBIAN_FRONTEND=noninteractive + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime + apt-get update + apt-get install -y sudo + sudo apt-get install -y curl wget git unzip zip libicu66 tzdata clang - name: Checkout sources uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Configure arm64 packages if: ${{ matrix.runtime == 'linux-arm64' }} run: | @@ -47,7 +58,7 @@ jobs: if: ${{ matrix.runtime == 'linux-arm64' }} run: | sudo apt-get update - sudo apt-get install clang llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64 + sudo apt-get install -y llvm gcc-aarch64-linux-gnu - name: Build run: dotnet build -c Release - name: Publish diff --git a/.github/workflows/localization-check.yml b/.github/workflows/localization-check.yml new file mode 100644 index 00000000..8dcd61c8 --- /dev/null +++ b/.github/workflows/localization-check.yml @@ -0,0 +1,41 @@ +name: Localization Check +on: + push: + branches: [ develop ] + paths: + - 'src/Resources/Locales/**' + workflow_dispatch: + workflow_call: + +jobs: + localization-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Install dependencies + run: npm install fs-extra@11.2.0 path@0.12.7 xml2js@0.6.2 + + - name: Run localization check + run: node build/scripts/localization-check.js + + - name: Commit changes + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + if [ -n "$(git status --porcelain)" ]; then + git add TRANSLATION.md src/Resources/Locales/*.axaml + git commit -m 'doc: Update translation status and sort locale files' + git push + else + echo "No changes to commit" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 53affe3d..2dfc97fd 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -3,16 +3,16 @@ on: workflow_call: inputs: version: - description: Source Git package version + description: SourceGit package version required: true type: string jobs: - windows-portable: - name: Package portable Windows app - runs-on: ubuntu-latest + windows: + name: Package Windows + runs-on: windows-2019 strategy: matrix: - runtime: [win-x64, win-arm64] + runtime: [ win-x64, win-arm64 ] steps: - name: Checkout sources uses: actions/checkout@v4 @@ -22,10 +22,11 @@ jobs: name: sourcegit.${{ matrix.runtime }} path: build/SourceGit - name: Package + shell: bash env: VERSION: ${{ inputs.version }} RUNTIME: ${{ matrix.runtime }} - run: ./build/scripts/package.windows-portable.sh + run: ./build/scripts/package.windows.sh - name: Upload package artifact uses: actions/upload-artifact@v4 with: @@ -36,7 +37,7 @@ jobs: with: name: sourcegit.${{ matrix.runtime }} osx-app: - name: Package OSX app + name: Package macOS runs-on: macos-latest strategy: matrix: @@ -69,6 +70,7 @@ jobs: linux: name: Package Linux runs-on: ubuntu-latest + container: ubuntu:20.04 strategy: matrix: runtime: [linux-x64, linux-arm64] @@ -77,9 +79,10 @@ jobs: 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 + export DEBIAN_FRONTEND=noninteractive + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime + apt-get update + apt-get install -y curl wget git dpkg-dev fakeroot tzdata zip unzip desktop-file-utils rpm libfuse2 file build-essential binutils - name: Download build uses: actions/download-artifact@v4 with: @@ -89,6 +92,7 @@ jobs: env: VERSION: ${{ inputs.version }} RUNTIME: ${{ matrix.runtime }} + APPIMAGE_EXTRACT_AND_RUN: 1 run: | mkdir build/SourceGit tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c19103e3..e61e608b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ github.ref_name }} VERSION: ${{ needs.version.outputs.version }} - run: gh release create "$TAG" -t "Release $VERSION" --notes-from-tag + run: gh release create "$TAG" -t "$VERSION" --notes-from-tag - name: Download artifacts uses: actions/download-artifact@v4 with: diff --git a/.gitignore b/.gitignore index 95bde92d..e686a534 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,10 @@ ehthumbs_vista.db bin/ obj/ +# ignore ci node files +node_modules/ +package.json +package-lock.json build/resources/ build/SourceGit/ @@ -32,4 +36,6 @@ build/*.tar.gz build/*.deb build/*.rpm build/*.AppImage -SourceGit.app/ \ No newline at end of file +SourceGit.app/ +build.command +src/Properties/launchSettings.json diff --git a/LICENSE b/LICENSE index dceab2d8..442ce085 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2024 sourcegit +Copyright (c) 2025 sourcegit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index c2dee8c5..f9ba3072 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,26 @@ -# SourceGit +# SourceGit - Opensource Git GUI client. -Opensource Git GUI client. +[![stars](https://img.shields.io/github/stars/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/stargazers) +[![forks](https://img.shields.io/github/forks/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/forks) +[![license](https://img.shields.io/github/license/sourcegit-scm/sourcegit.svg)](LICENSE) +[![latest](https://img.shields.io/github/v/release/sourcegit-scm/sourcegit.svg)](https://github.com/sourcegit-scm/sourcegit/releases/latest) +[![downloads](https://img.shields.io/github/downloads/sourcegit-scm/sourcegit/total)](https://github.com/sourcegit-scm/sourcegit/releases) ## Highlights * Supports Windows/macOS/Linux * Opensource/Free * Fast -* English/Français/Deutsch/Português/Русский/简体中文/繁體中文 +* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil) * Built-in light/dark themes * Customize theme * Visual commit graph * Supports SSH access with each remote * GIT commands with GUI * Clone/Fetch/Pull/Push... - * Merge/Rebase/Reset/Revert/Amend/Cherry-pick... - * Amend/Reword - * Interactive rebase (Basic) + * Merge/Rebase/Reset/Revert/Cherry-pick... + * Amend/Reword/Squash + * Interactive rebase * Branches * Remotes * Tags @@ -31,32 +35,40 @@ Opensource Git GUI client. * Revision Diffs * Branch Diff * Image Diff - Side-By-Side/Swipe/Blend +* Git command logs * Search commits * GitFlow * Git LFS +* Bisect * Issue Link * Workspace +* Custom Action * Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama)) > [!WARNING] > **Linux** only tested on **Debian 12** on both **X11** & **Wayland**. +## Translation Status + +You can find the current translation status in [TRANSLATION.md](https://github.com/sourcegit-scm/sourcegit/blob/develop/TRANSLATION.md) + ## How to Use -**To use this tool, you need to install Git(>=2.23.0) first.** +**To use this tool, you need to install Git(>=2.25.1) first.** -You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [Github Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits. +You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [GitHub Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits. 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` | +| Windows | `%APPDATA%\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. +> * You can open this data storage directory from the main menu `Open Data Storage Directory`. +> * You can create a `data` folder next to the `SourceGit` executable to force this app to store data (user settings, downloaded avatars and crash logs) into it (Portable-Mode). Only works on Windows. For **Windows** users: @@ -67,60 +79,91 @@ For **Windows** users: ``` > [!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: +* You can install the latest stable by `scoop` with follow commands: ```shell scoop bucket add extras scoop install sourcegit ``` -* Portable versions can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) +* Pre-built binaries can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) For **macOS** users: -* Download `sourcegit_x.y.osx-x64.zip` or `sourcegit_x.y.osx-arm64.zip` from Releases. `x64` for Intel and `arm64` for Apple Silicon. -* Move `SourceGit.app` to `Applications` folder. -* Make sure your mac trusts all software from anywhere. For more information, search `spctl --master-disable`. +* Thanks [@ybeapps](https://github.com/ybeapps) for making `SourceGit` available on `Homebrew`. You can simply install it with following command: + ```shell + brew tap ybeapps/homebrew-sourcegit + brew install --cask --no-quarantine sourcegit + ``` +* If you want to install `SourceGit.app` from GitHub Release manually, you need run following command to make sure it works: + ```shell + sudo xattr -cr /Applications/SourceGit.app + ``` * Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your mac. -* You may need to run `sudo xattr -cr /Applications/SourceGit.app` to make sure the software works. +* You can run `echo $PATH > ~/Library/Application\ Support/SourceGit/PATH` to generate a custom PATH env file to introduce `PATH` env to SourceGit. For **Linux** users: -* `xdg-open` must be installed to support open native file manager. -* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your linux. +* Thanks [@aikawayataro](https://github.com/aikawayataro) for providing `rpm` and `deb` repositories, hosted on [Codeberg](https://codeberg.org/yataro/-/packages). + + `deb` how to: + ```shell + curl https://codeberg.org/api/packages/yataro/debian/repository.key | sudo tee /etc/apt/keyrings/sourcegit.asc + echo "deb [signed-by=/etc/apt/keyrings/sourcegit.asc, arch=amd64,arm64] https://codeberg.org/api/packages/yataro/debian generic main" | sudo tee /etc/apt/sources.list.d/sourcegit.list + sudo apt update + sudo apt install sourcegit + ``` + + `rpm` how to: + ```shell + curl https://codeberg.org/api/packages/yataro/rpm.repo | sed -e 's/gpgcheck=1/gpgcheck=0/' > sourcegit.repo + + # Fedora 41 and newer + sudo dnf config-manager addrepo --from-repofile=./sourcegit.repo + # Fedora 40 and earlier + sudo dnf config-manager --add-repo ./sourcegit.repo + + sudo dnf install sourcegit + ``` + + If your distribution isn't using `dnf`, please refer to the documentation of your distribution on how to add an `rpm` repository. +* `AppImage` files can be found on [AppImage hub](https://appimage.github.io/SourceGit/), `xdg-open` (`xdg-utils`) must be installed to support open native file manager. +* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your Linux. * Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI. +* If you can NOT type accented characters, such as `ê`, `ó`, try to set the environment variable `AVALONIA_IM_MODULE` to `none`. ## OpenAI -This software supports using OpenAI or other AI service that has an OpenAI comaptible HTTP API to generate commit message. You need configurate the service in `Preference` window. +This software supports using OpenAI or other AI service that has an OpenAI compatible HTTP API to generate commit message. You need configurate the service in `Preference` window. For `OpenAI`: -* `Server` must be `https://api.openai.com/v1/chat/completions` +* `Server` must be `https://api.openai.com/v1` For other AI service: -* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`. For example, when using `Ollama`, it should be `http://localhost:11434/v1/chat/completions` instead of `http://localhost:11434/api/generate` +* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate` * The `API Key` is optional that depends on the service ## External Tools This app supports open repository in external tools listed in the table below. -| 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 | -| Zed | NO | YES | YES | ZED | +| Tool | Windows | macOS | Linux | +|-------------------------------|---------|-------|-------| +| Visual Studio Code | YES | YES | YES | +| Visual Studio Code - Insiders | YES | YES | YES | +| VSCodium | YES | YES | YES | +| Fleet | YES | YES | YES | +| Sublime Text | YES | YES | YES | +| Zed | NO | YES | YES | +| Visual Studio | YES | NO | NO | > [!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: +> To solve this problem you can add a file named `external_editors.json` in app data storage directory and provide the path directly. For example: ```json { "tools": { - "VSCODE": "D:\\VSCode\\Code.exe" + "Visual Studio Code": "D:\\VSCode\\Code.exe" } } ``` @@ -146,6 +189,19 @@ This app supports open repository in external tools listed in the table below. Everyone is welcome to submit a PR. Please make sure your PR is based on the latest `develop` branch and the target branch of PR is `develop`. +In short, here are the commands to get started once [.NET tools are installed](https://dotnet.microsoft.com/en-us/download): + +```sh +dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org +dotnet restore +dotnet build +dotnet run --project src/SourceGit.csproj +``` + Thanks to all the people who contribute. -[![Contributors](https://contrib.rocks/image?repo=sourcegit-scm/sourcegit&columns=10)](https://github.com/sourcegit-scm/sourcegit/graphs/contributors) +[![Contributors](https://contrib.rocks/image?repo=sourcegit-scm/sourcegit&columns=20)](https://github.com/sourcegit-scm/sourcegit/graphs/contributors) + +## Third-Party Components + +For detailed license information, see [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md). diff --git a/SourceGit.sln b/SourceGit.sln index 10c94e36..624322f8 100644 --- a/SourceGit.sln +++ b/SourceGit.sln @@ -13,9 +13,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F45A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{67B6D05F-A000-40BA-ADB4-C9065F880D7B}" ProjectSection(SolutionItems) = preProject + .github\workflows\build.yml = .github\workflows\build.yml .github\workflows\ci.yml = .github\workflows\ci.yml .github\workflows\package.yml = .github\workflows\package.yml .github\workflows\release.yml = .github\workflows\release.yml + .github\workflows\localization-check.yml = .github\workflows\localization-check.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}" @@ -58,6 +60,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DEBIAN", "DEBIAN", "{F101849D-BDB7-40D4-A516-751150C3CCFC}" ProjectSection(SolutionItems) = preProject build\resources\deb\DEBIAN\control = build\resources\deb\DEBIAN\control + build\resources\deb\DEBIAN\preinst = build\resources\deb\DEBIAN\preinst + build\resources\deb\DEBIAN\prerm = build\resources\deb\DEBIAN\prerm EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rpm", "rpm", "{9BA0B044-0CC9-46F8-B551-204F149BF45D}" @@ -75,9 +79,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "appimage", "appimage", "{5D EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C54D4001-9940-477C-A0B6-E795ED0A3209}" ProjectSection(SolutionItems) = preProject + build\scripts\localization-check.js = build\scripts\localization-check.js 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 + build\scripts\package.windows.sh = build\scripts\package.windows.sh EndProjectSection EndProject Global diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md new file mode 100644 index 00000000..2338263c --- /dev/null +++ b/THIRD-PARTY-LICENSES.md @@ -0,0 +1,86 @@ +# Third-Party Licenses + +This project incorporates components from the following third parties: + +## Packages + +### AvaloniaUI + +- **Source**: https://github.com/AvaloniaUI/Avalonia +- **Version**: 11.2.5 +- **License**: MIT License +- **License Link**: https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md + +### AvaloniaEdit + +- **Source**: https://github.com/AvaloniaUI/AvaloniaEdit +- **Version**: 11.2.0 +- **License**: MIT License +- **License Link**: https://github.com/AvaloniaUI/AvaloniaEdit/blob/master/LICENSE + +### LiveChartsCore.SkiaSharpView.Avalonia + +- **Source**: https://github.com/beto-rodriguez/LiveCharts2 +- **Version**: 2.0.0-rc5.4 +- **License**: MIT License +- **License Link**: https://github.com/beto-rodriguez/LiveCharts2/blob/master/LICENSE + +### TextMateSharp + +- **Source**: https://github.com/danipen/TextMateSharp +- **Version**: 1.0.66 +- **License**: MIT License +- **License Link**: https://github.com/danipen/TextMateSharp/blob/master/LICENSE.md + +### OpenAI .NET SDK + +- **Source**: https://github.com/openai/openai-dotnet +- **Version**: 2.2.0-beta2 +- **License**: MIT License +- **License Link**: https://github.com/openai/openai-dotnet/blob/main/LICENSE + +### Azure.AI.OpenAI + +- **Source**: https://github.com/Azure/azure-sdk-for-net +- **Version**: 2.2.0-beta2 +- **License**: MIT License +- **License Link**: https://github.com/Azure/azure-sdk-for-net/blob/main/LICENSE.txt + +## Fonts + +### JetBrainsMono + +- **Source**: https://github.com/JetBrains/JetBrainsMono +- **Commit**: v2.304 +- **License**: SIL Open Font License, Version 1.1 +- **License Link**: https://github.com/JetBrains/JetBrainsMono/blob/v2.304/OFL.txt + +## Grammar Files + +### haxe-TmLanguage + +- **Source**: https://github.com/vshaxe/haxe-TmLanguage +- **Commit**: ddad8b4c6d0781ac20be0481174ec1be772c5da5 +- **License**: MIT License +- **License Link**: https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md + +### coc-toml + +- **Source**: https://github.com/kkiyama117/coc-toml +- **Commit**: aac3e0c65955c03314b2733041b19f903b7cc447 +- **License**: MIT License +- **License Link**: https://github.com/kkiyama117/coc-toml/blob/aac3e0c65955c03314b2733041b19f903b7cc447/LICENSE + +### eclipse-buildship + +- **Source**: https://github.com/eclipse/buildship +- **Commit**: 6bb773e7692f913dec27105129ebe388de34e68b +- **License**: Eclipse Public License 1.0 +- **License Link**: https://github.com/eclipse-buildship/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/README.md + +### vscode-jsp-lang + +- **Source**: https://github.com/samuel-weinhardt/vscode-jsp-lang +- **Commit**: 0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355 +- **License**: MIT License +- **License Link**: https://github.com/samuel-weinhardt/vscode-jsp-lang/blob/0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355/LICENSE diff --git a/TRANSLATION.md b/TRANSLATION.md new file mode 100644 index 00000000..051440f0 --- /dev/null +++ b/TRANSLATION.md @@ -0,0 +1,511 @@ +# Translation Status + +This document shows the translation status of each locale file in the repository. + +## Details + +### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) + +### ![de__DE](https://img.shields.io/badge/de__DE-96.14%25-yellow) + +
+Missing keys in de_DE.axaml + +- Text.Avatar.Load +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitDetail.Changes.Count +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Pull.RecurseSubmodules +- Text.Repository.ClearStashes +- Text.Repository.ShowSubmodulesAsTree +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.WorkingCopy.ResetAuthor + +
+ +### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen) + +### ![fr__FR](https://img.shields.io/badge/fr__FR-92.03%25-yellow) + +
+Missing keys in fr_FR.axaml + +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitDetail.Changes.Count +- Text.CommitMessageTextBox.SubjectCount +- Text.Configure.Git.PreferredMergeMode +- Text.ConfirmEmptyCommit.Continue +- Text.ConfirmEmptyCommit.NoLocalChanges +- Text.ConfirmEmptyCommit.StageAllThenCommit +- Text.ConfirmEmptyCommit.WithLocalChanges +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Pull.RecurseSubmodules +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.ConfirmCommitWithFilter +- Text.WorkingCopy.Conflicts.OpenExternalMergeTool +- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts +- Text.WorkingCopy.Conflicts.UseMine +- Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.ResetAuthor + +
+ +### ![it__IT](https://img.shields.io/badge/it__IT-97.38%25-yellow) + +
+Missing keys in it_IT.axaml + +- Text.Avatar.Load +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitDetail.Changes.Count +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Pull.RecurseSubmodules +- Text.Repository.ClearStashes +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Submodule.Deinit +- Text.WorkingCopy.ResetAuthor + +
+ +### ![ja__JP](https://img.shields.io/badge/ja__JP-91.78%25-yellow) + +
+Missing keys in ja_JP.axaml + +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.CompareWithCurrent +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitDetail.Changes.Count +- Text.CommitMessageTextBox.SubjectCount +- Text.Configure.Git.PreferredMergeMode +- Text.ConfirmEmptyCommit.Continue +- Text.ConfirmEmptyCommit.NoLocalChanges +- Text.ConfirmEmptyCommit.StageAllThenCommit +- Text.ConfirmEmptyCommit.WithLocalChanges +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Pull.RecurseSubmodules +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes +- Text.Repository.FilterCommits +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.ConfirmCommitWithFilter +- Text.WorkingCopy.Conflicts.OpenExternalMergeTool +- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts +- Text.WorkingCopy.Conflicts.UseMine +- Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.ResetAuthor + +
+ +### ![pt__BR](https://img.shields.io/badge/pt__BR-83.81%25-yellow) + +
+Missing keys in pt_BR.axaml + +- Text.AIAssistant.Regen +- Text.AIAssistant.Use +- Text.ApplyStash +- Text.ApplyStash.DropAfterApply +- Text.ApplyStash.RestoreIndex +- Text.ApplyStash.Stash +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.CustomAction +- Text.BranchCM.MergeMultiBranches +- Text.BranchCM.ResetToSelectedCommit +- Text.BranchUpstreamInvalid +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.Clone.RecurseSubmodules +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitCM.Merge +- Text.CommitCM.MergeMultiple +- Text.CommitDetail.Changes.Count +- Text.CommitDetail.Files.Search +- Text.CommitDetail.Info.Children +- Text.CommitMessageTextBox.SubjectCount +- Text.Configure.CustomAction.Scope.Branch +- Text.Configure.CustomAction.WaitForExit +- Text.Configure.Git.PreferredMergeMode +- Text.Configure.IssueTracker.AddSampleGiteeIssue +- Text.Configure.IssueTracker.AddSampleGiteePullRequest +- Text.ConfirmEmptyCommit.Continue +- Text.ConfirmEmptyCommit.NoLocalChanges +- Text.ConfirmEmptyCommit.StageAllThenCommit +- Text.ConfirmEmptyCommit.WithLocalChanges +- Text.CopyFullPath +- Text.CreateBranch.Name.WarnSpace +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.DeleteRepositoryNode.Path +- Text.DeleteRepositoryNode.TipForGroup +- Text.DeleteRepositoryNode.TipForRepository +- Text.Diff.First +- Text.Diff.Last +- Text.Diff.Submodule.Deleted +- Text.Diff.UseBlockNavigation +- Text.Fetch.Force +- Text.FileCM.ResolveUsing +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.Clone +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.InProgress.CherryPick.Head +- Text.InProgress.Merge.Operating +- Text.InProgress.Rebase.StoppedAt +- Text.InProgress.Revert.Head +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Merge.Source +- Text.MergeMultiple +- Text.MergeMultiple.CommitChanges +- Text.MergeMultiple.Strategy +- Text.MergeMultiple.Targets +- Text.Preferences.AI.Streaming +- Text.Preferences.Appearance.EditorTabWidth +- Text.Preferences.General.DateFormat +- Text.Preferences.General.ShowChildren +- Text.Preferences.General.ShowTagsInGraph +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Preferences.Git.SSLVerify +- Text.Pull.RecurseSubmodules +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes +- Text.Repository.FilterCommits +- Text.Repository.HistoriesLayout +- Text.Repository.HistoriesLayout.Horizontal +- Text.Repository.HistoriesLayout.Vertical +- Text.Repository.HistoriesOrder +- Text.Repository.Notifications.Clear +- Text.Repository.OnlyHighlightCurrentBranchInHistories +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.Skip +- Text.Repository.Tags.OrderByCreatorDate +- Text.Repository.Tags.OrderByName +- Text.Repository.Tags.Sort +- Text.Repository.UseRelativeTimeInHistories +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.SetUpstream +- Text.SetUpstream.Local +- Text.SetUpstream.Unset +- Text.SetUpstream.Upstream +- Text.SHALinkCM.NavigateTo +- Text.Stash.AutoRestore +- Text.Stash.AutoRestore.Tip +- Text.StashCM.SaveAsPatch +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.CommitToEdit +- Text.WorkingCopy.ConfirmCommitWithFilter +- Text.WorkingCopy.Conflicts.OpenExternalMergeTool +- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts +- Text.WorkingCopy.Conflicts.UseMine +- Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.ResetAuthor +- Text.WorkingCopy.SignOff + +
+ +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.75%25-yellow) + +
+Missing keys in ru_RU.axaml + +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream + +
+ +### ![ta__IN](https://img.shields.io/badge/ta__IN-91.91%25-yellow) + +
+Missing keys in ta_IN.axaml + +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.CompareWithCurrent +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitDetail.Changes.Count +- Text.CommitMessageTextBox.SubjectCount +- Text.Configure.Git.PreferredMergeMode +- Text.ConfirmEmptyCommit.Continue +- Text.ConfirmEmptyCommit.NoLocalChanges +- Text.ConfirmEmptyCommit.StageAllThenCommit +- Text.ConfirmEmptyCommit.WithLocalChanges +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Pull.RecurseSubmodules +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.UpdateSubmodules.Target +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.Conflicts.OpenExternalMergeTool +- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts +- Text.WorkingCopy.Conflicts.UseMine +- Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.ResetAuthor + +
+ +### ![uk__UA](https://img.shields.io/badge/uk__UA-93.15%25-yellow) + +
+Missing keys in uk_UA.axaml + +- Text.Avatar.Load +- Text.Bisect +- Text.Bisect.Abort +- Text.Bisect.Bad +- Text.Bisect.Detecting +- Text.Bisect.Good +- Text.Bisect.Skip +- Text.Bisect.WaitingForRange +- Text.BranchCM.ResetToSelectedCommit +- Text.Checkout.RecurseSubmodules +- Text.Checkout.WithFastForward +- Text.Checkout.WithFastForward.Upstream +- Text.CommitCM.CopyAuthor +- Text.CommitCM.CopyCommitter +- Text.CommitCM.CopySubject +- Text.CommitDetail.Changes.Count +- Text.CommitMessageTextBox.SubjectCount +- Text.ConfigureWorkspace.Name +- Text.CreateBranch.OverwriteExisting +- Text.DeinitSubmodule +- Text.DeinitSubmodule.Force +- Text.DeinitSubmodule.Path +- Text.Diff.Submodule.Deleted +- Text.GitFlow.FinishWithPush +- Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Preferences.Git.IgnoreCRAtEOLInDiff +- Text.Pull.RecurseSubmodules +- Text.Repository.BranchSort +- Text.Repository.BranchSort.ByCommitterDate +- Text.Repository.BranchSort.ByName +- Text.Repository.ClearStashes +- Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree +- Text.Repository.ViewLogs +- Text.Repository.Visit +- Text.ResetWithoutCheckout +- Text.ResetWithoutCheckout.MoveTo +- Text.ResetWithoutCheckout.Target +- Text.Submodule.Deinit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL +- Text.ViewLogs +- Text.ViewLogs.Clear +- Text.ViewLogs.CopyLog +- Text.ViewLogs.Delete +- Text.WorkingCopy.ResetAuthor + +
+ +### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) + +### ![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen) \ No newline at end of file diff --git a/VERSION b/VERSION index 66182729..d3e094ba 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.35 \ No newline at end of file +2025.22 \ No newline at end of file diff --git a/build/README.md b/build/README.md index a75f4d73..17305edf 100644 --- a/build/README.md +++ b/build/README.md @@ -5,11 +5,11 @@ ## How to build this project manually -1. Make sure [.NET SDK 8](https://dotnet.microsoft.com/en-us/download) is installed on your machine. +1. Make sure [.NET SDK 9](https://dotnet.microsoft.com/en-us/download) is installed on your machine. 2. Clone this project 3. Run the follow command under the project root dir ```sh dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj ``` > [!NOTE] -> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replece the `$DESTINATION_FOLDER` with the real path that will store the output executable files. +> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replace the `$DESTINATION_FOLDER` with the real path that will store the output executable files. diff --git a/build/resources/_common/applications/sourcegit.desktop b/build/resources/_common/applications/sourcegit.desktop index ff7ef135..bcf9c813 100644 --- a/build/resources/_common/applications/sourcegit.desktop +++ b/build/resources/_common/applications/sourcegit.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Name=Source Git +Name=SourceGit Comment=Open-source & Free Git GUI Client Exec=/opt/sourcegit/sourcegit Icon=/usr/share/icons/sourcegit.png diff --git a/build/resources/deb/DEBIAN/control b/build/resources/deb/DEBIAN/control index 7cfed330..71786b43 100755 --- a/build/resources/deb/DEBIAN/control +++ b/build/resources/deb/DEBIAN/control @@ -1,7 +1,8 @@ Package: sourcegit -Version: 8.23 +Version: 2025.10 Priority: optional -Depends: libx11-6, libice6, libsm6 +Depends: libx11-6, libice6, libsm6, libicu | libicu76 | libicu74 | libicu72 | libicu71 | libicu70 | libicu69 | libicu68 | libicu67 | libicu66 | libicu65 | libicu63 | libicu60 | libicu57 | libicu55 | libicu52, xdg-utils Architecture: amd64 +Installed-Size: 60440 Maintainer: longshuang@msn.cn Description: Open-source & Free Git GUI Client diff --git a/build/resources/deb/DEBIAN/preinst b/build/resources/deb/DEBIAN/preinst new file mode 100755 index 00000000..a93f8090 --- /dev/null +++ b/build/resources/deb/DEBIAN/preinst @@ -0,0 +1,32 @@ +#!/bin/sh + +set -e + +# summary of how this script can be called: +# * `install' +# * `install' +# * `upgrade' +# * `abort-upgrade' +# for details, see http://www.debian.org/doc/debian-policy/ + +case "$1" in + install|upgrade) + # Check if SourceGit is running and stop it + if pgrep -f '/opt/sourcegit/sourcegit' > /dev/null; then + echo "Stopping running SourceGit instance..." + pkill -f '/opt/sourcegit/sourcegit' || true + # Give the process a moment to terminate + sleep 1 + fi + ;; + + abort-upgrade) + ;; + + *) + echo "preinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/build/resources/deb/DEBIAN/prerm b/build/resources/deb/DEBIAN/prerm new file mode 100755 index 00000000..c2c9e4f0 --- /dev/null +++ b/build/resources/deb/DEBIAN/prerm @@ -0,0 +1,35 @@ +#!/bin/sh + +set -e + +# summary of how this script can be called: +# * `remove' +# * `upgrade' +# * `failed-upgrade' +# * `remove' `in-favour' +# * `deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +case "$1" in + remove|upgrade|deconfigure) + if pgrep -f '/opt/sourcegit/sourcegit' > /dev/null; then + echo "Stopping running SourceGit instance..." + pkill -f '/opt/sourcegit/sourcegit' || true + # Give the process a moment to terminate + sleep 1 + fi + ;; + + failed-upgrade) + ;; + + *) + echo "prerm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/build/resources/rpm/SPECS/build.spec b/build/resources/rpm/SPECS/build.spec index 289cbe39..2a684837 100644 --- a/build/resources/rpm/SPECS/build.spec +++ b/build/resources/rpm/SPECS/build.spec @@ -5,8 +5,10 @@ Summary: Open-source & Free Git Gui Client License: MIT URL: https://sourcegit-scm.github.io/ Source: https://github.com/sourcegit-scm/sourcegit/archive/refs/tags/v%_version.tar.gz -Requires: libX11 -Requires: libSM +Requires: libX11.so.6()(%{__isa_bits}bit) +Requires: libSM.so.6()(%{__isa_bits}bit) +Requires: libicu +Requires: xdg-utils %define _build_id_links none diff --git a/build/scripts/localization-check.js b/build/scripts/localization-check.js new file mode 100644 index 00000000..8d636b5b --- /dev/null +++ b/build/scripts/localization-check.js @@ -0,0 +1,83 @@ +const fs = require('fs-extra'); +const path = require('path'); +const xml2js = require('xml2js'); + +const repoRoot = path.join(__dirname, '../../'); +const localesDir = path.join(repoRoot, 'src/Resources/Locales'); +const enUSFile = path.join(localesDir, 'en_US.axaml'); +const outputFile = path.join(repoRoot, 'TRANSLATION.md'); + +const parser = new xml2js.Parser(); + +async function parseXml(filePath) { + const data = await fs.readFile(filePath); + return parser.parseStringPromise(data); +} + +async function filterAndSortTranslations(localeData, enUSKeys, enUSData) { + const strings = localeData.ResourceDictionary['x:String']; + // Remove keys that don't exist in English file + const filtered = strings.filter(item => enUSKeys.has(item.$['x:Key'])); + + // Sort based on the key order in English file + const enUSKeysArray = enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']); + filtered.sort((a, b) => { + const aIndex = enUSKeysArray.indexOf(a.$['x:Key']); + const bIndex = enUSKeysArray.indexOf(b.$['x:Key']); + return aIndex - bIndex; + }); + + return filtered; +} + +async function calculateTranslationRate() { + const enUSData = await parseXml(enUSFile); + const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key'])); + const files = (await fs.readdir(localesDir)).filter(file => file !== 'en_US.axaml' && file.endsWith('.axaml')); + + const lines = []; + + lines.push('# Translation Status'); + lines.push('This document shows the translation status of each locale file in the repository.'); + lines.push(`## Details`); + lines.push(`### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)`); + + for (const file of files) { + const locale = file.replace('.axaml', '').replace('_', '__'); + const filePath = path.join(localesDir, file); + const localeData = await parseXml(filePath); + const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key'])); + const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key)); + + // Sort and clean up extra translations + const sortedAndCleaned = await filterAndSortTranslations(localeData, enUSKeys, enUSData); + localeData.ResourceDictionary['x:String'] = sortedAndCleaned; + + // Save the updated file + const builder = new xml2js.Builder({ + headless: true, + renderOpts: { pretty: true, indent: ' ' } + }); + let xmlStr = builder.buildObject(localeData); + + // Add an empty line before the first x:String + xmlStr = xmlStr.replace(' 0) { + const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100; + const badgeColor = progress >= 75 ? 'yellow' : 'red'; + + lines.push(`### ![${locale}](https://img.shields.io/badge/${locale}-${progress.toFixed(2)}%25-${badgeColor})`); + lines.push(`
\nMissing keys in ${file}\n\n${missingKeys.map(key => `- ${key}`).join('\n')}\n\n
`) + } else { + lines.push(`### ![${locale}](https://img.shields.io/badge/${locale}-%E2%88%9A-brightgreen)`); + } + } + + const content = lines.join('\n\n'); + console.log(content); + await fs.writeFile(outputFile, content, 'utf8'); +} + +calculateTranslationRate().catch(err => console.error(err)); diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh index 5ffd5a27..1b4adbdc 100755 --- a/build/scripts/package.linux.sh +++ b/build/scripts/package.linux.sh @@ -5,16 +5,6 @@ set -o set -u set pipefail -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= @@ -66,8 +56,15 @@ cp -f SourceGit/* resources/deb/opt/sourcegit ln -rsf resources/deb/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" +# Calculate installed size in KB +installed_size=$(du -sk resources/deb | cut -f1) +# Update the control file +sed -i -e "s/^Version:.*/Version: $VERSION/" \ + -e "s/^Architecture:.*/Architecture: $arch/" \ + -e "s/^Installed-Size:.*/Installed-Size: $installed_size/" \ + resources/deb/DEBIAN/control +# Build deb package with gzip compression +dpkg-deb -Zgzip --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 index 4a50a860..2d43e24a 100755 --- a/build/scripts/package.osx-app.sh +++ b/build/scripts/package.osx-app.sh @@ -5,16 +5,6 @@ set -o set -u set pipefail -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 diff --git a/build/scripts/package.windows-portable.sh b/build/scripts/package.windows-portable.sh deleted file mode 100755 index 9ba29216..00000000 --- a/build/scripts/package.windows-portable.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -o -set -u -set pipefail - -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/build/scripts/package.windows.sh b/build/scripts/package.windows.sh new file mode 100755 index 00000000..c22a9d35 --- /dev/null +++ b/build/scripts/package.windows.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e +set -o +set -u +set pipefail + +cd build + +rm -rf SourceGit/*.pdb + +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then + powershell -Command "Compress-Archive -Path SourceGit -DestinationPath \"sourcegit_$VERSION.$RUNTIME.zip\" -Force" +else + zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit +fi diff --git a/global.json b/global.json index b5b37b60..a27a2b82 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "9.0.0", "rollForward": "latestMajor", "allowPrerelease": false } diff --git a/src/App.Commands.cs b/src/App.Commands.cs index 8a485029..22e9fb51 100644 --- a/src/App.Commands.cs +++ b/src/App.Commands.cs @@ -25,12 +25,34 @@ namespace SourceGit private Action _action = null; } - public static readonly Command OpenPreferenceCommand = new Command(_ => OpenDialog(new Views.Preference())); - public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys())); + public static bool IsCheckForUpdateCommandVisible + { + get + { +#if DISABLE_UPDATE_DETECTION + return false; +#else + return true; +#endif + } + } + + public static readonly Command OpenPreferencesCommand = new Command(_ => ShowWindow(new Views.Preferences(), false)); + public static readonly Command OpenHotkeysCommand = new Command(_ => ShowWindow(new Views.Hotkeys(), false)); public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir)); - public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About())); - public static readonly Command CheckForUpdateCommand = new Command(_ => Check4Update(true)); + public static readonly Command OpenAboutCommand = new Command(_ => ShowWindow(new Views.About(), false)); + public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true)); public static readonly Command QuitCommand = new Command(_ => Quit(0)); - public static readonly Command CopyTextBlockCommand = new Command(p => CopyTextBlock(p as TextBlock)); + public static readonly Command CopyTextBlockCommand = new Command(p => + { + var textBlock = p as TextBlock; + if (textBlock == null) + return; + + if (textBlock.Inlines is { Count: > 0 } inlines) + CopyText(inlines.Text); + else if (!string.IsNullOrEmpty(textBlock.Text)) + CopyText(textBlock.Text); + }); } } diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 70567af5..9cad0792 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -46,11 +46,9 @@ namespace SourceGit [JsonSerializable(typeof(Models.ExternalToolPaths))] [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] [JsonSerializable(typeof(Models.JetBrainsState))] - [JsonSerializable(typeof(Models.OpenAIChatRequest))] - [JsonSerializable(typeof(Models.OpenAIChatResponse))] [JsonSerializable(typeof(Models.ThemeOverrides))] [JsonSerializable(typeof(Models.Version))] [JsonSerializable(typeof(Models.RepositorySettings))] - [JsonSerializable(typeof(ViewModels.Preference))] + [JsonSerializable(typeof(ViewModels.Preferences))] internal partial class JsonCodeGen : JsonSerializerContext { } } diff --git a/src/App.axaml b/src/App.axaml index b1fe303b..186022d5 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -14,10 +14,15 @@ + + + + + @@ -30,10 +35,10 @@ - - + + - + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 682ec5fc..8e579373 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Avalonia; @@ -22,6 +25,7 @@ namespace SourceGit { public partial class App : Application { + #region App Entry Point [STAThread] public static void Main(string[] args) { @@ -34,15 +38,14 @@ namespace SourceGit TaskScheduler.UnobservedTaskException += (_, e) => { - LogException(e.Exception); e.SetObserved(); }; try { - if (TryLaunchedAsRebaseTodoEditor(args, out int exitTodo)) + if (TryLaunchAsRebaseTodoEditor(args, out int exitTodo)) Environment.Exit(exitTodo); - else if (TryLaunchedAsRebaseMessageEditor(args, out int exitMessage)) + else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage)) Environment.Exit(exitMessage); else BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); @@ -59,6 +62,10 @@ namespace SourceGit builder.UsePlatformDetect(); builder.LogToTrace(); builder.WithInterFont(); + builder.With(new FontManagerOptions() + { + DefaultFamilyName = "fonts:Inter#Inter" + }); builder.ConfigureFonts(manager => { var monospace = new EmbeddedFontCollection( @@ -71,38 +78,71 @@ namespace SourceGit return builder; } - public override void Initialize() + public static void LogException(Exception ex) { - AvaloniaXamlLoader.Load(this); + if (ex == null) + return; - var pref = ViewModels.Preference.Instance; - pref.PropertyChanged += (_, _) => pref.Save(); + var builder = new StringBuilder(); + builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n"); + builder.Append("----------------------------\n"); + builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n"); + builder.Append($"OS: {Environment.OSVersion}\n"); + builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n"); + builder.Append($"Source: {ex.Source}\n"); + builder.Append($"Thread Name: {Thread.CurrentThread.Name ?? "Unnamed"}\n"); + builder.Append($"User: {Environment.UserName}\n"); + builder.Append($"App Start Time: {Process.GetCurrentProcess().StartTime}\n"); + builder.Append($"Exception Time: {DateTime.Now}\n"); + builder.Append($"Memory Usage: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024} MB\n"); + builder.Append($"---------------------------\n\n"); + builder.Append(ex); - SetLocale(pref.Locale); - SetTheme(pref.Theme, pref.ThemeOverrides); - SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor); + var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); + File.WriteAllText(file, builder.ToString()); } + #endregion - public override void OnFrameworkInitializationCompleted() + #region Utility Functions + public static void ShowWindow(object data, bool showAsDialog) { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + var impl = (Views.ChromelessWindow target, bool isDialog) => { - BindingPlugins.DataValidators.RemoveAt(0); + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + if (isDialog) + target.ShowDialog(owner); + else + target.Show(owner); + } + else + { + target.Show(); + } + }; - if (TryLaunchedAsCoreEditor(desktop)) - return; - - if (TryLaunchedAsAskpass(desktop)) - return; - - TryLaunchedAsNormal(desktop); + if (data is Views.ChromelessWindow window) + { + impl(window, showAsDialog); + return; } - } - public static void OpenDialog(Window window) - { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) - window.ShowDialog(owner); + var dataTypeName = data.GetType().FullName; + if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal)) + return; + + var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views."); + var viewType = Type.GetType(viewTypeName); + if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow))) + return; + + window = Activator.CreateInstance(viewType) as Views.ChromelessWindow; + if (window != null) + { + window.DataContext = data; + impl(window, showAsDialog); + } } public static void RaiseException(string context, string message) @@ -200,6 +240,9 @@ namespace SourceGit app._fontsOverrides = null; } + defaultFont = app.FixFontFamilyName(defaultFont); + monospaceFont = app.FixFontFamilyName(monospaceFont); + var resDic = new ResourceDictionary(); if (!string.IsNullOrEmpty(defaultFont)) resDic.Add("Fonts.Default", new FontFamily(defaultFont)); @@ -223,7 +266,7 @@ namespace SourceGit if (onlyUseMonospaceFontInEditor) { if (string.IsNullOrEmpty(defaultFont)) - resDic.Add("Fonts.Primary", new FontFamily("fonts:Inter#Inter, $Default")); + resDic.Add("Fonts.Primary", new FontFamily("fonts:Inter#Inter")); else resDic.Add("Fonts.Primary", new FontFamily(defaultFont)); } @@ -258,7 +301,7 @@ namespace SourceGit return await clipboard.GetTextAsync(); } } - return default; + return null; } public static string Text(string key, params object[] args) @@ -280,8 +323,7 @@ namespace SourceGit icon.Height = 12; icon.Stretch = Stretch.Uniform; - var geo = Current?.FindResource(key) as StreamGeometry; - if (geo != null) + if (Current?.FindResource(key) is StreamGeometry geo) icon.Data = geo; return icon; @@ -295,26 +337,11 @@ namespace SourceGit return null; } - public static ViewModels.Launcher GetLauncer() + public static ViewModels.Launcher GetLauncher() { return Current is App app ? app._launcher : null; } - public static ViewModels.Repository FindOpenedRepository(string repoPath) - { - if (Current is App app && app._launcher != null) - { - foreach (var page in app._launcher.Pages) - { - var id = page.Node.Id.Replace("\\", "/"); - if (id == repoPath && page.Data is ViewModels.Repository repo) - return repo; - } - } - - return null; - } - public static void Quit(int exitCode) { if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) @@ -327,94 +354,68 @@ namespace SourceGit Environment.Exit(exitCode); } } + #endregion - private static void CopyTextBlock(TextBlock textBlock) + #region Overrides + public override void Initialize() { - if (textBlock == null) - return; + AvaloniaXamlLoader.Load(this); - if (textBlock.Inlines is { Count: > 0 } inlines) - CopyText(inlines.Text); - else if (!string.IsNullOrEmpty(textBlock.Text)) - CopyText(textBlock.Text); + var pref = ViewModels.Preferences.Instance; + pref.PropertyChanged += (_, _) => pref.Save(); + + SetLocale(pref.Locale); + SetTheme(pref.Theme, pref.ThemeOverrides); + SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor); } - private static void LogException(Exception ex) + public override void OnFrameworkInitializationCompleted() { - if (ex == null) - return; - - var builder = new StringBuilder(); - builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n"); - builder.Append("----------------------------\n"); - builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n"); - builder.Append($"OS: {Environment.OSVersion.ToString()}\n"); - builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n"); - builder.Append($"Source: {ex.Source}\n"); - builder.Append($"---------------------------\n\n"); - builder.Append(ex.StackTrace); - while (ex.InnerException != null) + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - ex = ex.InnerException; - builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n"); - builder.Append(ex.StackTrace); + BindingPlugins.DataValidators.RemoveAt(0); + + // Disable tooltip if window is not active. + ToolTip.ToolTipOpeningEvent.AddClassHandler((c, e) => + { + var topLevel = TopLevel.GetTopLevel(c); + if (topLevel is not Window { IsActive: true }) + e.Cancel = true; + }); + + if (TryLaunchAsCoreEditor(desktop)) + return; + + if (TryLaunchAsAskpass(desktop)) + return; + + _ipcChannel = new Models.IpcChannel(); + if (!_ipcChannel.IsFirstInstance) + { + var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty; + if (!string.IsNullOrEmpty(arg)) + { + if (arg.StartsWith('"') && arg.EndsWith('"')) + arg = arg.Substring(1, arg.Length - 2).Trim(); + + if (arg.Length > 0 && !Path.IsPathFullyQualified(arg)) + arg = Path.GetFullPath(arg); + } + + _ipcChannel.SendToFirstInstance(arg); + Environment.Exit(0); + } + else + { + _ipcChannel.MessageReceived += TryOpenRepository; + desktop.Exit += (_, _) => _ipcChannel.Dispose(); + TryLaunchAsNormal(desktop); + } } - - var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); - File.WriteAllText(file, builder.ToString()); } + #endregion - private static void Check4Update(bool manually = false) - { - Task.Run(async () => - { - try - { - // Fetch lastest release information. - var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) }; - var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json"); - - // Parse json into Models.Version. - var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version); - if (ver == null) - return; - - // Check if already up-to-date. - if (!ver.IsNewVersion) - { - if (manually) - ShowSelfUpdateResult(new Models.AlreadyUpToDate()); - return; - } - - // Should not check ignored tag if this is called manually. - if (!manually) - { - var pref = ViewModels.Preference.Instance; - if (ver.TagName == pref.IgnoreUpdateTag) - return; - } - - ShowSelfUpdateResult(ver); - } - catch (Exception e) - { - if (manually) - ShowSelfUpdateResult(e); - } - }); - } - - private static void ShowSelfUpdateResult(object data) - { - Dispatcher.UIThread.Post(() => - { - OpenDialog(new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } }); - }); - } - - private static bool TryLaunchedAsRebaseTodoEditor(string[] args, out int exitCode) + private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) { exitCode = -1; @@ -467,39 +468,57 @@ namespace SourceGit return true; } - private static bool TryLaunchedAsRebaseMessageEditor(string[] args, out int exitCode) + private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCode) { exitCode = -1; if (args.Length <= 1 || !args[0].Equals("--rebase-message-editor", StringComparison.Ordinal)) return false; + exitCode = 0; + var file = args[1]; var filename = Path.GetFileName(file); if (!filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase)) return true; - var jobsFile = Path.Combine(Path.GetDirectoryName(file)!, "sourcegit_rebase_jobs.json"); - if (!File.Exists(jobsFile)) + var gitDir = Path.GetDirectoryName(file)!; + var origHeadFile = Path.Combine(gitDir, "rebase-merge", "orig-head"); + var ontoFile = Path.Combine(gitDir, "rebase-merge", "onto"); + var doneFile = Path.Combine(gitDir, "rebase-merge", "done"); + var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json"); + if (!File.Exists(ontoFile) || !File.Exists(origHeadFile) || !File.Exists(doneFile) || !File.Exists(jobsFile)) return true; + var origHead = File.ReadAllText(origHeadFile).Trim(); + var onto = File.ReadAllText(ontoFile).Trim(); var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); - var doneFile = Path.Combine(Path.GetDirectoryName(file)!, "rebase-merge", "done"); - if (!File.Exists(doneFile)) + if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead)) return true; - var done = File.ReadAllText(doneFile).Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); - if (done.Length > collection.Jobs.Count) + var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + if (done.Length == 0) return true; - var job = collection.Jobs[done.Length - 1]; - File.WriteAllText(file, job.Message); + var current = done[^1].Trim(); + var match = REG_REBASE_TODO().Match(current); + if (!match.Success) + return true; + + var sha = match.Groups[1].Value; + foreach (var job in collection.Jobs) + { + if (job.SHA.StartsWith(sha)) + { + File.WriteAllText(file, job.Message); + break; + } + } - exitCode = 0; return true; } - private bool TryLaunchedAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) + private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) { var args = desktop.Args; if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal)) @@ -507,14 +526,18 @@ namespace SourceGit var file = args[1]; if (!File.Exists(file)) + { desktop.Shutdown(-1); - else - desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file); + return true; + } + var editor = new Views.StandaloneCommitMessageEditor(); + editor.SetFile(file); + desktop.MainWindow = editor; return true; } - private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop) + private bool TryLaunchAsAskpass(IClassicDesktopStyleApplicationLifetime desktop) { var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS"); if (launchAsAskpass is not "TRUE") @@ -523,30 +546,158 @@ namespace SourceGit var args = desktop.Args; if (args?.Length > 0) { - desktop.MainWindow = new Views.Askpass(args[0]); + var askpass = new Views.Askpass(); + askpass.TxtDescription.Text = args[0]; + desktop.MainWindow = askpass; return true; } return false; } - private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop) + private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) { - Native.OS.SetupEnternalTools(); + Native.OS.SetupExternalTools(); Models.AvatarManager.Instance.Start(); string startupRepo = null; if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) startupRepo = desktop.Args[0]; + var pref = ViewModels.Preferences.Instance; + pref.SetCanModify(); + _launcher = new ViewModels.Launcher(startupRepo); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; + desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; - var pref = ViewModels.Preference.Instance; +#if !DISABLE_UPDATE_DETECTION if (pref.ShouldCheck4UpdateOnStartup()) Check4Update(); +#endif } + private void TryOpenRepository(string repo) + { + if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo)) + { + var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd(); + if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) + { + Dispatcher.UIThread.Invoke(() => + { + var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); + ViewModels.Welcome.Instance.Refresh(); + _launcher?.OpenRepositoryInTab(node, null); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher wnd }) + wnd.BringToTop(); + }); + + return; + } + } + + Dispatcher.UIThread.Invoke(() => + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher launcher }) + launcher.BringToTop(); + }); + } + + private void Check4Update(bool manually = false) + { + Task.Run(async () => + { + try + { + // Fetch latest release information. + var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) }; + var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json"); + + // Parse JSON into Models.Version. + var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version); + if (ver == null) + return; + + // Check if already up-to-date. + if (!ver.IsNewVersion) + { + if (manually) + ShowSelfUpdateResult(new Models.AlreadyUpToDate()); + return; + } + + // Should not check ignored tag if this is called manually. + if (!manually) + { + var pref = ViewModels.Preferences.Instance; + if (ver.TagName == pref.IgnoreUpdateTag) + return; + } + + ShowSelfUpdateResult(ver); + } + catch (Exception e) + { + if (manually) + ShowSelfUpdateResult(new Models.SelfUpdateFailed(e)); + } + }); + } + + private void ShowSelfUpdateResult(object data) + { + Dispatcher.UIThread.Post(() => + { + ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true); + }); + } + + private string FixFontFamilyName(string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + var parts = input.Split(','); + var trimmed = new List(); + + foreach (var part in parts) + { + var t = part.Trim(); + if (string.IsNullOrEmpty(t)) + continue; + + // Collapse multiple spaces into single space + var prevChar = '\0'; + var sb = new StringBuilder(); + + foreach (var c in t) + { + if (c == ' ' && prevChar == ' ') + continue; + sb.Append(c); + prevChar = c; + } + + var name = sb.ToString(); + if (name.Contains('#', StringComparison.Ordinal)) + { + if (!name.Equals("fonts:Inter#Inter", StringComparison.Ordinal) && + !name.Equals("fonts:SourceGit#JetBrains Mono", StringComparison.Ordinal)) + continue; + } + + trimmed.Add(name); + } + + return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; + } + + [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")] + private static partial Regex REG_REBASE_TODO(); + + private Models.IpcChannel _ipcChannel = null; private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; private ResourceDictionary _themeOverrides = null; diff --git a/src/App.manifest b/src/App.manifest index b3bc3bdf..11a2ff11 100644 --- a/src/App.manifest +++ b/src/App.manifest @@ -1,7 +1,7 @@  diff --git a/src/Commands/Add.cs b/src/Commands/Add.cs index 7134ec4a..210eb4b2 100644 --- a/src/Commands/Add.cs +++ b/src/Commands/Add.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Text; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Add : Command { @@ -11,21 +8,19 @@ namespace SourceGit.Commands Context = repo; Args = includeUntracked ? "add ." : "add -u ."; } - - public Add(string repo, List changes) + + public Add(string repo, Models.Change change) { WorkingDirectory = repo; Context = repo; + Args = $"add -- \"{change.Path}\""; + } - var builder = new StringBuilder(); - builder.Append("add --"); - foreach (var c in changes) - { - builder.Append(" \""); - builder.Append(c.Path); - builder.Append("\""); - } - Args = builder.ToString(); + public Add(string repo, string pathspecFromFile) + { + WorkingDirectory = repo; + Context = repo; + Args = $"add --pathspec-from-file=\"{pathspecFromFile}\""; } } } diff --git a/src/Commands/Archive.cs b/src/Commands/Archive.cs index d4f6241c..5e0919f7 100644 --- a/src/Commands/Archive.cs +++ b/src/Commands/Archive.cs @@ -1,23 +1,12 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Archive : Command { - public Archive(string repo, string revision, string saveTo, Action outputHandler) + public Archive(string repo, string revision, string saveTo) { WorkingDirectory = repo; Context = repo; Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}"; - TraitErrorAsOutput = true; - _outputHandler = outputHandler; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } } diff --git a/src/Commands/AssumeUnchanged.cs b/src/Commands/AssumeUnchanged.cs index 1898122a..28f78280 100644 --- a/src/Commands/AssumeUnchanged.cs +++ b/src/Commands/AssumeUnchanged.cs @@ -1,75 +1,14 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace SourceGit.Commands +namespace SourceGit.Commands { - public partial class AssumeUnchanged + public class AssumeUnchanged : Command { - [GeneratedRegex(@"^(\w)\s+(.+)$")] - private static partial Regex REG_PARSE(); - - class ViewCommand : Command + public AssumeUnchanged(string repo, string file, bool bAdd) { - public ViewCommand(string repo) - { - WorkingDirectory = repo; - Args = "ls-files -v"; - RaiseError = false; - } + var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged"; - public List Result() - { - Exec(); - return _outs; - } - - protected override void OnReadline(string line) - { - var match = REG_PARSE().Match(line); - if (!match.Success) - return; - - if (match.Groups[1].Value == "h") - { - _outs.Add(match.Groups[2].Value); - } - } - - private readonly List _outs = new List(); + WorkingDirectory = repo; + Context = repo; + Args = $"update-index {mode} -- \"{file}\""; } - - class ModCommand : Command - { - public ModCommand(string repo, string file, bool bAdd) - { - var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged"; - - WorkingDirectory = repo; - Context = repo; - Args = $"update-index {mode} -- \"{file}\""; - } - } - - public AssumeUnchanged(string repo) - { - _repo = repo; - } - - public List View() - { - return new ViewCommand(_repo).Result(); - } - - public void Add(string file) - { - new ModCommand(_repo, file, true).Exec(); - } - - public void Remove(string file) - { - new ModCommand(_repo, file, false).Exec(); - } - - private readonly string _repo; } } diff --git a/src/Commands/Bisect.cs b/src/Commands/Bisect.cs new file mode 100644 index 00000000..a3bf1a97 --- /dev/null +++ b/src/Commands/Bisect.cs @@ -0,0 +1,13 @@ +namespace SourceGit.Commands +{ + public class Bisect : Command + { + public Bisect(string repo, string subcmd) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + Args = $"bisect {subcmd}"; + } + } +} diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs index de221b07..1fc51fa4 100644 --- a/src/Commands/Blame.cs +++ b/src/Commands/Blame.cs @@ -21,10 +21,17 @@ namespace SourceGit.Commands public Models.BlameData Result() { - var succ = Exec(); - if (!succ) + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return _result; + + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - return new Models.BlameData(); + ParseLine(line); + + if (_result.IsBinary) + break; } if (_needUnifyCommitSHA) @@ -42,14 +49,9 @@ namespace SourceGit.Commands return _result; } - protected override void OnReadline(string line) + private void ParseLine(string line) { - if (_result.IsBinary) - return; - if (string.IsNullOrEmpty(line)) - return; - - if (line.IndexOf('\0', StringComparison.Ordinal) >= 0) + if (line.Contains('\0', StringComparison.Ordinal)) { _result.IsBinary = true; _result.LineInfos.Clear(); @@ -65,7 +67,7 @@ namespace SourceGit.Commands var commit = match.Groups[1].Value; var author = match.Groups[2].Value; var timestamp = int.Parse(match.Groups[3].Value); - var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString("yyyy/MM/dd"); + var when = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString(_dateFormat); var info = new Models.BlameLineInfo() { @@ -87,6 +89,7 @@ namespace SourceGit.Commands private readonly Models.BlameData _result = new Models.BlameData(); private readonly StringBuilder _content = new StringBuilder(); + private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly; private string _lastSHA = string.Empty; private bool _needUnifyCommitSHA = false; private int _minSHALen = 64; diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 890b54ee..0d1b1f8f 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -1,30 +1,52 @@ -namespace SourceGit.Commands +using System.Text; + +namespace SourceGit.Commands { public static class Branch { - public static bool Create(string repo, string name, string basedOn) + public static string ShowCurrent(string repo) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.Args = $"branch {name} {basedOn}"; + cmd.Args = $"branch --show-current"; + return cmd.ReadToEnd().StdOut.Trim(); + } + + public static bool Create(string repo, string name, string basedOn, bool force, Models.ICommandLog log) + { + var builder = new StringBuilder(); + builder.Append("branch "); + if (force) + builder.Append("-f "); + builder.Append(name); + builder.Append(" "); + builder.Append(basedOn); + + var cmd = new Command(); + cmd.WorkingDirectory = repo; + cmd.Context = repo; + cmd.Args = builder.ToString(); + cmd.Log = log; return cmd.Exec(); } - public static bool Rename(string repo, string name, string to) + public static bool Rename(string repo, string name, string to, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"branch -M {name} {to}"; + cmd.Log = log; return cmd.Exec(); } - public static bool SetUpstream(string repo, string name, string upstream) + public static bool SetUpstream(string repo, string name, string upstream, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; + cmd.Log = log; if (string.IsNullOrEmpty(upstream)) cmd.Args = $"branch {name} --unset-upstream"; @@ -34,22 +56,27 @@ return cmd.Exec(); } - public static bool DeleteLocal(string repo, string name) + public static bool DeleteLocal(string repo, string name, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"branch -D {name}"; + cmd.Log = log; return cmd.Exec(); } - public static bool DeleteRemote(string repo, string remote, string name) + public static bool DeleteRemote(string repo, string remote, string name, Models.ICommandLog log) { + bool exists = new Remote(repo).HasBranch(remote, name); + if (exists) + return new Push(repo, remote, $"refs/heads/{name}", true) { Log = log }.Exec(); + var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - cmd.Args = $"push {remote} --delete {name}"; + cmd.Args = $"branch -D -r {remote}/{name}"; + cmd.Log = log; return cmd.Exec(); } } diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index 306d62ff..d2876740 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; namespace SourceGit.Commands @@ -12,19 +11,37 @@ namespace SourceGit.Commands Context = repo; } - public bool Branch(string branch, Action onProgress) + public bool Branch(string branch, bool force) { - Args = $"checkout --recurse-submodules --progress {branch}"; - TraitErrorAsOutput = true; - _outputHandler = onProgress; + var builder = new StringBuilder(); + builder.Append("checkout --progress "); + if (force) + builder.Append("--force "); + builder.Append(branch); + + Args = builder.ToString(); return Exec(); } - public bool Branch(string branch, string basedOn, Action onProgress) + public bool Branch(string branch, string basedOn, bool force, bool allowOverwrite) { - Args = $"checkout --recurse-submodules --progress -b {branch} {basedOn}"; - TraitErrorAsOutput = true; - _outputHandler = onProgress; + var builder = new StringBuilder(); + builder.Append("checkout --progress "); + if (force) + builder.Append("--force "); + builder.Append(allowOverwrite ? "-B " : "-b "); + builder.Append(branch); + builder.Append(" "); + builder.Append(basedOn); + + Args = builder.ToString(); + return Exec(); + } + + public bool Commit(string commitId, bool force) + { + var option = force ? "--force" : string.Empty; + Args = $"checkout {option} --detach --progress {commitId}"; return Exec(); } @@ -61,20 +78,5 @@ namespace SourceGit.Commands Args = $"checkout --no-overlay {revision} -- \"{file}\""; return Exec(); } - - public bool Commit(string commitId, Action onProgress) - { - Args = $"checkout --detach --progress {commitId}"; - TraitErrorAsOutput = true; - _outputHandler = onProgress; - return Exec(); - } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private Action _outputHandler; } } diff --git a/src/Commands/Clean.cs b/src/Commands/Clean.cs index a10e5873..6ed74999 100644 --- a/src/Commands/Clean.cs +++ b/src/Commands/Clean.cs @@ -1,31 +1,12 @@ -using System.Collections.Generic; -using System.Text; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Clean : Command { - public Clean(string repo, bool includeIgnored) + public Clean(string repo) { WorkingDirectory = repo; Context = repo; - Args = includeIgnored ? "clean -qfdx" : "clean -qfd"; - } - - public Clean(string repo, List files) - { - var builder = new StringBuilder(); - builder.Append("clean -qfd --"); - foreach (var f in files) - { - builder.Append(" \""); - builder.Append(f); - builder.Append("\""); - } - - WorkingDirectory = repo; - Context = repo; - Args = builder.ToString(); + Args = "clean -qfdx"; } } } diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index 683b8846..efec264b 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -1,18 +1,13 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Clone : Command { - private readonly Action _notifyProgress; - - public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action ouputHandler) + public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs) { Context = ctx; WorkingDirectory = path; - TraitErrorAsOutput = true; SSHKey = sshKey; - Args = "clone --progress --verbose --recurse-submodules "; + Args = "clone --progress --verbose "; if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} "; @@ -21,13 +16,6 @@ namespace SourceGit.Commands if (!string.IsNullOrEmpty(localName)) Args += localName; - - _notifyProgress = ouputHandler; - } - - protected override void OnReadline(string line) - { - _notifyProgress?.Invoke(line); } } } diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index d774fa09..975922fc 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using Avalonia.Threading; @@ -10,11 +11,6 @@ namespace SourceGit.Commands { public partial class Command { - public class CancelToken - { - public bool Requested { get; set; } = false; - } - public class ReadToEndResult { public bool IsSuccess { get; set; } = false; @@ -30,81 +26,51 @@ namespace SourceGit.Commands } public string Context { get; set; } = string.Empty; - public CancelToken Cancel { get; set; } = null; + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; public string WorkingDirectory { get; set; } = null; public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode public string SSHKey { get; set; } = string.Empty; public string Args { get; set; } = string.Empty; public bool RaiseError { get; set; } = true; - public bool TraitErrorAsOutput { get; set; } = false; + public Models.ICommandLog Log { get; set; } = null; public bool Exec() { + Log?.AppendLine($"$ git {Args}\n"); + var start = CreateGitStartInfo(); var errs = new List(); var proc = new Process() { StartInfo = start }; - var isCancelled = false; - proc.OutputDataReceived += (_, e) => - { - if (Cancel != null && Cancel.Requested) - { - isCancelled = true; - proc.CancelErrorRead(); - proc.CancelOutputRead(); - if (!proc.HasExited) - proc.Kill(true); - return; - } - - if (e.Data != null) - OnReadline(e.Data); - }; - - proc.ErrorDataReceived += (_, e) => - { - if (Cancel != null && Cancel.Requested) - { - isCancelled = true; - proc.CancelErrorRead(); - proc.CancelOutputRead(); - if (!proc.HasExited) - proc.Kill(true); - return; - } - - if (string.IsNullOrEmpty(e.Data)) - return; - if (TraitErrorAsOutput) - OnReadline(e.Data); - - // Ignore progress messages - if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal)) - return; - if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) - return; - if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal)) - return; - if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal)) - return; - if (REG_PROGRESS().IsMatch(e.Data)) - return; - errs.Add(e.Data); - }; + proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs); + proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs); + var dummy = null as Process; + var dummyProcLock = new object(); try { proc.Start(); + + // It not safe, please only use `CancellationToken` in readonly commands. + if (CancellationToken.CanBeCanceled) + { + dummy = proc; + CancellationToken.Register(() => + { + lock (dummyProcLock) + { + if (dummy is { HasExited: false }) + dummy.Kill(); + } + }); + } } catch (Exception e) { if (RaiseError) - { - Dispatcher.UIThread.Invoke(() => - { - App.RaiseException(Context, e.Message); - }); - } + Dispatcher.UIThread.Post(() => App.RaiseException(Context, e.Message)); + + Log?.AppendLine(string.Empty); return false; } @@ -112,18 +78,27 @@ namespace SourceGit.Commands proc.BeginErrorReadLine(); proc.WaitForExit(); + if (dummy != null) + { + lock (dummyProcLock) + { + dummy = null; + } + } + int exitCode = proc.ExitCode; proc.Close(); + Log?.AppendLine(string.Empty); - if (!isCancelled && exitCode != 0 && errs.Count > 0) + if (!CancellationToken.IsCancellationRequested && exitCode != 0) { if (RaiseError) { - Dispatcher.UIThread.Invoke(() => - { - App.RaiseException(Context, string.Join("\n", errs)); - }); + var errMsg = string.Join("\n", errs).Trim(); + if (!string.IsNullOrEmpty(errMsg)) + Dispatcher.UIThread.Post(() => App.RaiseException(Context, errMsg)); } + return false; } @@ -162,11 +137,6 @@ namespace SourceGit.Commands return rs; } - protected virtual void OnReadline(string line) - { - // Implemented by derived class - } - private ProcessStartInfo CreateGitStartInfo() { var start = new ProcessStartInfo(); @@ -191,9 +161,12 @@ namespace SourceGit.Commands if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey)) start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'"); - // Force using en_US.UTF-8 locale to avoid GCM crash + // Force using en_US.UTF-8 locale if (OperatingSystem.IsLinux()) - start.Environment.Add("LANG", "en_US.UTF-8"); + { + start.Environment.Add("LANG", "C"); + start.Environment.Add("LC_ALL", "C"); + } // Force using this app as git editor. switch (Editor) @@ -219,6 +192,28 @@ namespace SourceGit.Commands return start; } + private void HandleOutput(string line, List errs) + { + line ??= string.Empty; + Log?.AppendLine(line); + + // Lines to hide in error message. + if (line.Length > 0) + { + if (line.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal) || + line.StartsWith("remote: Counting objects:", StringComparison.Ordinal) || + line.StartsWith("remote: Compressing objects:", StringComparison.Ordinal) || + line.StartsWith("Filtering content:", StringComparison.Ordinal) || + line.StartsWith("hint:", StringComparison.Ordinal)) + return; + + if (REG_PROGRESS().IsMatch(line)) + return; + } + + errs.Add(line); + } + [GeneratedRegex(@"\d+%")] private static partial Regex REG_PROGRESS(); } diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs index b629f82d..1585e7e3 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -4,19 +4,36 @@ namespace SourceGit.Commands { public class Commit : Command { - public Commit(string repo, string message, bool amend, bool allowEmpty = false) + public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor) { - var file = Path.GetTempFileName(); - File.WriteAllText(file, message); + _tmpFile = Path.GetTempFileName(); + File.WriteAllText(_tmpFile, message); WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; - Args = $"commit --file=\"{file}\""; + Args = $"commit --allow-empty --file=\"{_tmpFile}\""; + if (signOff) + Args += " --signoff"; if (amend) - Args += " --amend --no-edit"; - if (allowEmpty) - Args += " --allow-empty"; + Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit"; } + + public bool Run() + { + var succ = Exec(); + + try + { + File.Delete(_tmpFile); + } + catch + { + // Ignore + } + + return succ; + } + + private readonly string _tmpFile; } } diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index c4674c8e..c88e087a 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -6,8 +6,10 @@ namespace SourceGit.Commands { public partial class CompareRevisions : Command { - [GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")] + [GeneratedRegex(@"^([MADC])\s+(.+)$")] private static partial Regex REG_FORMAT(); + [GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")] + private static partial Regex REG_RENAME_FORMAT(); public CompareRevisions(string repo, string start, string end) { @@ -18,18 +20,44 @@ namespace SourceGit.Commands Args = $"diff --name-status {based} {end}"; } + public CompareRevisions(string repo, string start, string end, string path) + { + WorkingDirectory = repo; + Context = repo; + + var based = string.IsNullOrEmpty(start) ? "-R" : start; + Args = $"diff --name-status {based} {end} -- \"{path}\""; + } + public List Result() { - Exec(); - _changes.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal)); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return _changes; + + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + ParseLine(line); + + _changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); return _changes; } - protected override void OnReadline(string line) + private void ParseLine(string line) { var match = REG_FORMAT().Match(line); if (!match.Success) + { + match = REG_RENAME_FORMAT().Match(line); + if (match.Success) + { + var renamed = new Models.Change() { Path = match.Groups[1].Value }; + renamed.Set(Models.ChangeState.Renamed); + _changes.Add(renamed); + } + return; + } var change = new Models.Change() { Path = match.Groups[2].Value }; var status = match.Groups[1].Value; @@ -48,10 +76,6 @@ namespace SourceGit.Commands change.Set(Models.ChangeState.Deleted); _changes.Add(change); break; - case 'R': - change.Set(Models.ChangeState.Renamed); - _changes.Add(change); - break; case 'C': change.Set(Models.ChangeState.Copied); _changes.Add(change); diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs index 2fb83325..49e8fcb7 100644 --- a/src/Commands/Config.cs +++ b/src/Commands/Config.cs @@ -29,7 +29,7 @@ namespace SourceGit.Commands var rs = new Dictionary(); if (output.IsSuccess) { - var lines = output.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var idx = line.IndexOf('=', StringComparison.Ordinal); diff --git a/src/Commands/CountLocalChangesWithoutUntracked.cs b/src/Commands/CountLocalChangesWithoutUntracked.cs index afb62840..a704f313 100644 --- a/src/Commands/CountLocalChangesWithoutUntracked.cs +++ b/src/Commands/CountLocalChangesWithoutUntracked.cs @@ -8,7 +8,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "status -uno --ignore-submodules=dirty --porcelain"; + Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain"; } public int Result() @@ -16,7 +16,7 @@ namespace SourceGit.Commands var rs = ReadToEnd(); if (rs.IsSuccess) { - var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); return lines.Length; } diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index b9b6e064..6af0a3cc 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text.RegularExpressions; @@ -8,6 +8,10 @@ namespace SourceGit.Commands { [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] private static partial Regex REG_INDICATOR(); + + [GeneratedRegex(@"^index\s([0-9a-f]{6,40})\.\.([0-9a-f]{6,40})(\s[1-9]{6})?")] + private static partial Regex REG_HASH_CHANGE(); + private const string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/"; private const string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/"; private const string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/"; @@ -24,34 +28,48 @@ namespace SourceGit.Commands Context = repo; if (ignoreWhitespace) - Args = $"diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}"; + Args = $"diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}"; + else if (Models.DiffOption.IgnoreCRAtEOL) + Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}"; else - Args = $"diff --patch --ignore-cr-at-eol --unified={unified} {opt}"; + Args = $"diff --no-ext-diff --patch --unified={unified} {opt}"; } public Models.DiffResult Result() { - Exec(); + var rs = ReadToEnd(); + var start = 0; + var end = rs.StdOut.IndexOf('\n', start); + while (end > 0) + { + var line = rs.StdOut.Substring(start, end - start); + ParseLine(line); - if (_result.IsBinary || _result.IsLFS) + start = end + 1; + end = rs.StdOut.IndexOf('\n', start); + } + + if (start < rs.StdOut.Length) + ParseLine(rs.StdOut.Substring(start)); + + if (_result.IsBinary || _result.IsLFS || _result.TextDiff.Lines.Count == 0) { _result.TextDiff = null; } else { ProcessInlineHighlights(); - - if (_result.TextDiff.Lines.Count == 0) - _result.TextDiff = null; - else - _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine); + _result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine); } return _result; } - protected override void OnReadline(string line) + private void ParseLine(string line) { + if (_result.IsBinary) + return; + if (line.StartsWith("old mode ", StringComparison.Ordinal)) { _result.OldMode = line.Substring(9); @@ -64,8 +82,17 @@ namespace SourceGit.Commands return; } - if (_result.IsBinary) + if (line.StartsWith("deleted file mode ", StringComparison.Ordinal)) + { + _result.OldMode = line.Substring(18); return; + } + + if (line.StartsWith("new file mode ", StringComparison.Ordinal)) + { + _result.NewMode = line.Substring(14); + return; + } if (_result.IsLFS) { @@ -78,7 +105,7 @@ namespace SourceGit.Commands } else if (line.StartsWith("-size ", StringComparison.Ordinal)) { - _result.LFSDiff.Old.Size = long.Parse(line.Substring(6)); + _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); } } else if (ch == '+') @@ -89,36 +116,52 @@ namespace SourceGit.Commands } else if (line.StartsWith("+size ", StringComparison.Ordinal)) { - _result.LFSDiff.New.Size = long.Parse(line.Substring(6)); + _result.LFSDiff.New.Size = long.Parse(line.AsSpan(6)); } } else if (line.StartsWith(" size ", StringComparison.Ordinal)) { - _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6)); + _result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6)); } return; } if (_result.TextDiff.Lines.Count == 0) { - var match = REG_INDICATOR().Match(line); - if (!match.Success) + if (line.StartsWith("Binary", StringComparison.Ordinal)) { - if (line.StartsWith("Binary", StringComparison.Ordinal)) - _result.IsBinary = true; + _result.IsBinary = true; return; } - _oldLine = int.Parse(match.Groups[1].Value); - _newLine = int.Parse(match.Groups[2].Value); - _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0)); + if (string.IsNullOrEmpty(_result.OldHash)) + { + var match = REG_HASH_CHANGE().Match(line); + if (!match.Success) + return; + + _result.OldHash = match.Groups[1].Value; + _result.NewHash = match.Groups[2].Value; + } + else + { + var match = REG_INDICATOR().Match(line); + if (!match.Success) + return; + + _oldLine = int.Parse(match.Groups[1].Value); + _newLine = int.Parse(match.Groups[2].Value); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); + _result.TextDiff.Lines.Add(_last); + } } else { if (line.Length == 0) { ProcessInlineHighlights(); - _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine); + _result.TextDiff.Lines.Add(_last); _oldLine++; _newLine++; return; @@ -134,7 +177,8 @@ namespace SourceGit.Commands return; } - _deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0); + _deleted.Add(_last); _oldLine++; } else if (ch == '+') @@ -146,7 +190,8 @@ namespace SourceGit.Commands return; } - _added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine); + _added.Add(_last); _newLine++; } else if (ch != '\\') @@ -157,7 +202,8 @@ namespace SourceGit.Commands { _oldLine = int.Parse(match.Groups[1].Value); _newLine = int.Parse(match.Groups[2].Value); - _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0); + _result.TextDiff.Lines.Add(_last); } else { @@ -168,11 +214,16 @@ namespace SourceGit.Commands return; } - _result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine)); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine); + _result.TextDiff.Lines.Add(_last); _oldLine++; _newLine++; } } + else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal)) + { + _last.NoNewLineEndOfFile = true; + } } } @@ -223,6 +274,7 @@ namespace SourceGit.Commands private readonly Models.DiffResult _result = new Models.DiffResult(); private readonly List _deleted = new List(); private readonly List _added = new List(); + private Models.TextDiffLine _last = null; private int _oldLine = 0; private int _newLine = 0; } diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index a279bb84..f36ca6c9 100644 --- a/src/Commands/Discard.cs +++ b/src/Commands/Discard.cs @@ -1,39 +1,95 @@ using System; using System.Collections.Generic; +using System.IO; + +using Avalonia.Threading; namespace SourceGit.Commands { public static class Discard { - public static void All(string repo, bool includeIgnored) + /// + /// Discard all local changes (unstaged & staged) + /// + /// + /// + /// + public static void All(string repo, bool includeIgnored, Models.ICommandLog log) { - new Restore(repo).Exec(); - new Clean(repo, includeIgnored).Exec(); + var changes = new QueryLocalChanges(repo).Result(); + try + { + foreach (var c in changes) + { + if (c.WorkTree == Models.ChangeState.Untracked || + c.WorkTree == Models.ChangeState.Added || + c.Index == Models.ChangeState.Added || + c.Index == Models.ChangeState.Renamed) + { + var fullPath = Path.Combine(repo, c.Path); + if (Directory.Exists(fullPath)) + Directory.Delete(fullPath, true); + else + File.Delete(fullPath); + } + } + } + catch (Exception e) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); + }); + } + + new Reset(repo, "HEAD", "--hard") { Log = log }.Exec(); + + if (includeIgnored) + new Clean(repo) { Log = log }.Exec(); } - public static void Changes(string repo, List changes) + /// + /// Discard selected changes (only unstaged). + /// + /// + /// + /// + public static void Changes(string repo, List changes, Models.ICommandLog log) { - var needClean = new List(); - var needCheckout = new List(); + var restores = new List(); - foreach (var c in changes) + try { - if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) - needClean.Add(c.Path); - else - needCheckout.Add(c.Path); + foreach (var c in changes) + { + if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) + { + var fullPath = Path.Combine(repo, c.Path); + if (Directory.Exists(fullPath)) + Directory.Delete(fullPath, true); + else + File.Delete(fullPath); + } + else + { + restores.Add(c.Path); + } + } + } + catch (Exception e) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); + }); } - for (int i = 0; i < needClean.Count; i += 10) + if (restores.Count > 0) { - var count = Math.Min(10, needClean.Count - i); - new Clean(repo, needClean.GetRange(i, count)).Exec(); - } - - for (int i = 0; i < needCheckout.Count; i += 10) - { - var count = Math.Min(10, needCheckout.Count - i); - new Restore(repo, needCheckout.GetRange(i, count), "--worktree --recurse-submodules").Exec(); + var pathSpecFile = Path.GetTempFileName(); + File.WriteAllLines(pathSpecFile, restores); + new Restore(repo, pathSpecFile, false) { Log = log }.Exec(); + File.Delete(pathSpecFile); } } } diff --git a/src/Commands/ExecuteCustomAction.cs b/src/Commands/ExecuteCustomAction.cs new file mode 100644 index 00000000..e59bc068 --- /dev/null +++ b/src/Commands/ExecuteCustomAction.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics; +using System.Text; + +using Avalonia.Threading; + +namespace SourceGit.Commands +{ + public static class ExecuteCustomAction + { + public static void Run(string repo, string file, string args) + { + var start = new ProcessStartInfo(); + start.FileName = file; + start.Arguments = args; + start.UseShellExecute = false; + start.CreateNoWindow = true; + start.WorkingDirectory = repo; + + try + { + Process.Start(start); + } + catch (Exception e) + { + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message)); + } + } + + public static void RunAndWait(string repo, string file, string args, Models.ICommandLog log) + { + var start = new ProcessStartInfo(); + start.FileName = file; + start.Arguments = args; + start.UseShellExecute = false; + start.CreateNoWindow = true; + start.RedirectStandardOutput = true; + start.RedirectStandardError = true; + start.StandardOutputEncoding = Encoding.UTF8; + start.StandardErrorEncoding = Encoding.UTF8; + start.WorkingDirectory = repo; + + log?.AppendLine($"$ {file} {args}\n"); + + var proc = new Process() { StartInfo = start }; + var builder = new StringBuilder(); + + proc.OutputDataReceived += (_, e) => + { + if (e.Data != null) + log?.AppendLine(e.Data); + }; + + proc.ErrorDataReceived += (_, e) => + { + if (e.Data != null) + { + log?.AppendLine(e.Data); + builder.AppendLine(e.Data); + } + }; + + try + { + proc.Start(); + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + proc.WaitForExit(); + + var exitCode = proc.ExitCode; + if (exitCode != 0) + { + var errMsg = builder.ToString().Trim(); + if (!string.IsNullOrEmpty(errMsg)) + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, errMsg)); + } + } + catch (Exception e) + { + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, e.Message)); + } + + proc.Close(); + } + } +} diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 94b7fde9..edf2a6dd 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -1,44 +1,31 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Fetch : Command { - public Fetch(string repo, string remote, bool prune, bool noTags, Action outputHandler) + public Fetch(string repo, string remote, bool noTags, bool force) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); Args = "fetch --progress --verbose "; - if (prune) - Args += "--prune "; - if (noTags) Args += "--no-tags "; else + Args += "--tags "; + + if (force) Args += "--force "; Args += remote; } - public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action outputHandler) + public Fetch(string repo, Models.Branch local, Models.Branch remote) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; - SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - Args = $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}"; + SSHKey = new Config(repo).Get($"remote.{remote.Remote}.sshkey"); + Args = $"fetch --progress --verbose {remote.Remote} {remote.Name}:{local.Name}"; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } } diff --git a/src/Commands/FormatPatch.cs b/src/Commands/FormatPatch.cs index 2c7359c0..bf850d60 100644 --- a/src/Commands/FormatPatch.cs +++ b/src/Commands/FormatPatch.cs @@ -6,7 +6,8 @@ { WorkingDirectory = repo; Context = repo; - Args = $"format-patch {commit} -1 -o \"{saveTo}\""; + Editor = EditorType.None; + Args = $"format-patch {commit} -1 --output=\"{saveTo}\""; } } } diff --git a/src/Commands/GC.cs b/src/Commands/GC.cs index 393b915e..0b27f487 100644 --- a/src/Commands/GC.cs +++ b/src/Commands/GC.cs @@ -1,23 +1,12 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class GC : Command { - public GC(string repo, Action outputHandler) + public GC(string repo) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; Args = "gc --prune=now"; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } } diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs index e71fb0b9..df61fdd2 100644 --- a/src/Commands/GenerateCommitMessage.cs +++ b/src/Commands/GenerateCommitMessage.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Text; using System.Threading; +using Avalonia.Threading; + namespace SourceGit.Commands { /// @@ -20,100 +22,78 @@ namespace SourceGit.Commands } } - public GenerateCommitMessage(string repo, List changes, CancellationToken cancelToken, Action onProgress) + public GenerateCommitMessage(Models.OpenAIService service, string repo, List changes, CancellationToken cancelToken, Action onResponse) { + _service = service; _repo = repo; _changes = changes; _cancelToken = cancelToken; - _onProgress = onProgress; + _onResponse = onResponse; } - public string Result() + public void Exec() { try { - var summaries = new List(); + _onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n"); + + var responseBuilder = new StringBuilder(); + var summaryBuilder = new StringBuilder(); foreach (var change in _changes) { if (_cancelToken.IsCancellationRequested) - return ""; + return; - _onProgress?.Invoke($"Analyzing {change.Path}..."); - var summary = GenerateChangeSummary(change); - summaries.Add(summary); + responseBuilder.Append("- "); + summaryBuilder.Append("- "); + + var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd(); + if (rs.IsSuccess) + { + _service.Chat( + _service.AnalyzeDiffPrompt, + $"Here is the `git diff` output: {rs.StdOut}", + _cancelToken, + update => + { + responseBuilder.Append(update); + summaryBuilder.Append(update); + + _onResponse?.Invoke($"Waiting for pre-file analyzing to completed...\n\n{responseBuilder}"); + }); + } + + responseBuilder.Append("\n"); + summaryBuilder.Append("(file: "); + summaryBuilder.Append(change.Path); + summaryBuilder.Append(")\n"); } if (_cancelToken.IsCancellationRequested) - return ""; + return; - _onProgress?.Invoke($"Generating commit message..."); - var builder = new StringBuilder(); - builder.Append(GenerateSubject(string.Join("", summaries))); - builder.Append("\n"); - foreach (var summary in summaries) - { - builder.Append("\n- "); - builder.Append(summary.Trim()); - } - - return builder.ToString(); + var responseBody = responseBuilder.ToString(); + var subjectBuilder = new StringBuilder(); + _service.Chat( + _service.GenerateSubjectPrompt, + $"Here are the summaries changes:\n{summaryBuilder}", + _cancelToken, + update => + { + subjectBuilder.Append(update); + _onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}"); + }); } catch (Exception e) { - App.RaiseException(_repo, $"Failed to generate commit message: {e}"); - return ""; + Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}")); } } - private string GenerateChangeSummary(Models.Change change) - { - var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd(); - var diff = rs.IsSuccess ? rs.StdOut : "unknown change"; - - var prompt = new StringBuilder(); - prompt.AppendLine("You are an expert developer specialist in creating commits."); - prompt.AppendLine("Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:"); - prompt.AppendLine("- Do not use any code snippets, imports, file routes or bullets points."); - prompt.AppendLine("- Do not mention the route of file that has been change."); - prompt.AppendLine("- Simply describe the MAIN GOAL of the changes."); - prompt.AppendLine("- Output directly the summary in plain text.`"); - - var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here is the `git diff` output: {diff}", _cancelToken); - if (rsp != null && rsp.Choices.Count > 0) - return rsp.Choices[0].Message.Content; - - return string.Empty; - } - - private string GenerateSubject(string summary) - { - var prompt = new StringBuilder(); - prompt.AppendLine("You are an expert developer specialist in creating commits messages."); - prompt.AppendLine("Your only goal is to retrieve a single commit message."); - prompt.AppendLine("Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:"); - prompt.AppendLine("- Assign the commit {type} according to the next conditions:"); - prompt.AppendLine(" feat: Only when adding a new feature."); - prompt.AppendLine(" fix: When fixing a bug."); - prompt.AppendLine(" docs: When updating documentation."); - prompt.AppendLine(" style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic."); - prompt.AppendLine(" test: When adding or updating tests. "); - prompt.AppendLine(" chore: When making changes to the build process or auxiliary tools and libraries. "); - prompt.AppendLine(" revert: When undoing a previous commit."); - prompt.AppendLine(" refactor: When restructuring code without changing its external behavior, or is any of the other refactor types."); - prompt.AppendLine("- Do not add any issues numeration, explain your output nor introduce your answer."); - prompt.AppendLine("- Output directly only one commit message in plain text with the next format: {type}: {commit_message}."); - prompt.AppendLine("- Be as concise as possible, keep the message under 50 characters."); - - var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here are the summaries changes: {summary}", _cancelToken); - if (rsp != null && rsp.Choices.Count > 0) - return rsp.Choices[0].Message.Content; - - return string.Empty; - } - + private Models.OpenAIService _service; private string _repo; private List _changes; private CancellationToken _cancelToken; - private Action _onProgress; + private Action _onResponse; } } diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs index 5e05ed83..1d33fa3a 100644 --- a/src/Commands/GitFlow.cs +++ b/src/Commands/GitFlow.cs @@ -1,52 +1,12 @@ -using System; -using System.Collections.Generic; - +using System.Text; using Avalonia.Threading; namespace SourceGit.Commands { public static class GitFlow { - public class BranchDetectResult + public static bool Init(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log) { - public bool IsGitFlowBranch { get; set; } = false; - public string Type { get; set; } = string.Empty; - public string Prefix { get; set; } = string.Empty; - } - - public static bool IsEnabled(string repo, List branches) - { - var localBrancheNames = new HashSet(); - foreach (var branch in branches) - { - if (branch.IsLocal) - localBrancheNames.Add(branch.Name); - } - - var config = new Config(repo).ListAll(); - if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master)) - return false; - - if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop)) - return false; - - return config.ContainsKey("gitflow.prefix.feature") && - config.ContainsKey("gitflow.prefix.release") && - config.ContainsKey("gitflow.prefix.hotfix"); - } - - public static bool Init(string repo, List branches, string master, string develop, string feature, string release, string hotfix, string version) - { - var current = branches.Find(x => x.IsCurrent); - - var masterBranch = branches.Find(x => x.Name == master); - if (masterBranch == null && current != null) - Branch.Create(repo, master, current.Head); - - var devBranch = branches.Find(x => x.Name == develop); - if (devBranch == null && current != null) - Branch.Create(repo, develop, current.Head); - var config = new Config(repo); config.Set("gitflow.branch.master", master); config.Set("gitflow.branch.develop", develop); @@ -61,104 +21,72 @@ namespace SourceGit.Commands init.WorkingDirectory = repo; init.Context = repo; init.Args = "flow init -d"; + init.Log = log; return init.Exec(); } - public static string GetPrefix(string repo, string type) + public static bool Start(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log) { - return new Config(repo).Get($"gitflow.prefix.{type}"); - } - - public static BranchDetectResult DetectType(string repo, List branches, string branch) - { - var rs = new BranchDetectResult(); - var localBrancheNames = new HashSet(); - foreach (var b in branches) - { - if (b.IsLocal) - localBrancheNames.Add(b.Name); - } - - var config = new Config(repo).ListAll(); - if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master)) - return rs; - - if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop)) - return rs; - - if (!config.TryGetValue("gitflow.prefix.feature", out var feature) || - !config.TryGetValue("gitflow.prefix.release", out var release) || - !config.TryGetValue("gitflow.prefix.hotfix", out var hotfix)) - return rs; - - if (branch.StartsWith(feature, StringComparison.Ordinal)) - { - rs.IsGitFlowBranch = true; - rs.Type = "feature"; - rs.Prefix = feature; - } - else if (branch.StartsWith(release, StringComparison.Ordinal)) - { - rs.IsGitFlowBranch = true; - rs.Type = "release"; - rs.Prefix = release; - } - else if (branch.StartsWith(hotfix, StringComparison.Ordinal)) - { - rs.IsGitFlowBranch = true; - rs.Type = "hotfix"; - rs.Prefix = hotfix; - } - - return rs; - } - - public static bool Start(string repo, string type, string name) - { - if (!SUPPORTED_BRANCH_TYPES.Contains(type)) - { - Dispatcher.UIThread.Post(() => - { - App.RaiseException(repo, "Bad branch type!!!"); - }); - - return false; - } - var start = new Command(); start.WorkingDirectory = repo; start.Context = repo; - start.Args = $"flow {type} start {name}"; + + switch (type) + { + case Models.GitFlowBranchType.Feature: + start.Args = $"flow feature start {name}"; + break; + case Models.GitFlowBranchType.Release: + start.Args = $"flow release start {name}"; + break; + case Models.GitFlowBranchType.Hotfix: + start.Args = $"flow hotfix start {name}"; + break; + default: + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!")); + return false; + } + + start.Log = log; return start.Exec(); } - public static bool Finish(string repo, string type, string name, bool keepBranch) + public static bool Finish(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log) { - if (!SUPPORTED_BRANCH_TYPES.Contains(type)) - { - Dispatcher.UIThread.Post(() => - { - App.RaiseException(repo, "Bad branch type!!!"); - }); + var builder = new StringBuilder(); + builder.Append("flow "); - return false; + switch (type) + { + case Models.GitFlowBranchType.Feature: + builder.Append("feature"); + break; + case Models.GitFlowBranchType.Release: + builder.Append("release"); + break; + case Models.GitFlowBranchType.Hotfix: + builder.Append("hotfix"); + break; + default: + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!")); + return false; } - var option = keepBranch ? "-k" : string.Empty; + builder.Append(" finish "); + if (squash) + builder.Append("--squash "); + if (push) + builder.Append("--push "); + if (keepBranch) + builder.Append("-k "); + builder.Append(name); + var finish = new Command(); finish.WorkingDirectory = repo; finish.Context = repo; - finish.Args = $"flow {type} finish {option} {name}"; + finish.Args = builder.ToString(); + finish.Log = log; return finish.Exec(); } - - private static readonly List SUPPORTED_BRANCH_TYPES = new List() - { - "feature", - "release", - "bugfix", - "hotfix", - "support", - }; } } diff --git a/src/Commands/GitIgnore.cs b/src/Commands/GitIgnore.cs index e666eba6..8b351f5e 100644 --- a/src/Commands/GitIgnore.cs +++ b/src/Commands/GitIgnore.cs @@ -8,7 +8,14 @@ namespace SourceGit.Commands { var file = Path.Combine(repo, ".gitignore"); if (!File.Exists(file)) + { File.WriteAllLines(file, [pattern]); + return; + } + + var org = File.ReadAllText(file); + if (!org.EndsWith('\n')) + File.AppendAllLines(file, ["", pattern]); else File.AppendAllLines(file, [pattern]); } diff --git a/src/Commands/IsBareRepository.cs b/src/Commands/IsBareRepository.cs new file mode 100644 index 00000000..f92d0888 --- /dev/null +++ b/src/Commands/IsBareRepository.cs @@ -0,0 +1,24 @@ +using System.IO; + +namespace SourceGit.Commands +{ + public class IsBareRepository : Command + { + public IsBareRepository(string path) + { + WorkingDirectory = path; + Args = "rev-parse --is-bare-repository"; + } + + public bool Result() + { + if (!Directory.Exists(Path.Combine(WorkingDirectory, "refs")) || + !Directory.Exists(Path.Combine(WorkingDirectory, "objects")) || + !File.Exists(Path.Combine(WorkingDirectory, "HEAD"))) + return false; + + var rs = ReadToEnd(); + return rs.IsSuccess && rs.StdOut.Trim() == "true"; + } + } +} diff --git a/src/Commands/IsBinary.cs b/src/Commands/IsBinary.cs index de59b5a4..af8f54bb 100644 --- a/src/Commands/IsBinary.cs +++ b/src/Commands/IsBinary.cs @@ -11,7 +11,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\""; + Args = $"diff {Models.Commit.EmptyTreeSHA1} {commit} --numstat -- \"{path}\""; RaiseError = false; } diff --git a/src/Commands/IsCommitSHA.cs b/src/Commands/IsCommitSHA.cs new file mode 100644 index 00000000..1b0c50e3 --- /dev/null +++ b/src/Commands/IsCommitSHA.cs @@ -0,0 +1,17 @@ +namespace SourceGit.Commands +{ + public class IsCommitSHA : Command + { + public IsCommitSHA(string repo, string hash) + { + WorkingDirectory = repo; + Args = $"cat-file -t {hash}"; + } + + public bool Result() + { + var rs = ReadToEnd(); + return rs.IsSuccess && rs.StdOut.Trim().Equals("commit"); + } + } +} diff --git a/src/Commands/IsConflictResolved.cs b/src/Commands/IsConflictResolved.cs index 13bc12ac..9b243451 100644 --- a/src/Commands/IsConflictResolved.cs +++ b/src/Commands/IsConflictResolved.cs @@ -10,5 +10,10 @@ Context = repo; Args = $"diff -a --ignore-cr-at-eol --check {opt}"; } + + public bool Result() + { + return ReadToEnd().IsSuccess; + } } } diff --git a/src/Commands/LFS.cs b/src/Commands/LFS.cs index c9ab7b41..18d2ba93 100644 --- a/src/Commands/LFS.cs +++ b/src/Commands/LFS.cs @@ -7,26 +7,18 @@ namespace SourceGit.Commands { public partial class LFS { - [GeneratedRegex(@"^(.+)\s+(\w+)\s+\w+:(\d+)$")] + [GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")] private static partial Regex REG_LOCK(); - class SubCmd : Command + private class SubCmd : Command { - public SubCmd(string repo, string args, Action onProgress) + public SubCmd(string repo, string args, Models.ICommandLog log) { WorkingDirectory = repo; Context = repo; Args = args; - TraitErrorAsOutput = true; - _outputHandler = onProgress; + Log = log; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } public LFS(string repo) @@ -44,35 +36,35 @@ namespace SourceGit.Commands return content.Contains("git lfs pre-push"); } - public bool Install() + public bool Install(Models.ICommandLog log) { - return new SubCmd(_repo, "lfs install --local", null).Exec(); + return new SubCmd(_repo, "lfs install --local", log).Exec(); } - public bool Track(string pattern, bool isFilenameMode = false) + public bool Track(string pattern, bool isFilenameMode, Models.ICommandLog log) { var opt = isFilenameMode ? "--filename" : ""; - return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", null).Exec(); + return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", log).Exec(); } - public void Fetch(string remote, Action outputHandler) + public void Fetch(string remote, Models.ICommandLog log) { - new SubCmd(_repo, $"lfs fetch {remote}", outputHandler).Exec(); + new SubCmd(_repo, $"lfs fetch {remote}", log).Exec(); } - public void Pull(string remote, Action outputHandler) + public void Pull(string remote, Models.ICommandLog log) { - new SubCmd(_repo, $"lfs pull {remote}", outputHandler).Exec(); + new SubCmd(_repo, $"lfs pull {remote}", log).Exec(); } - public void Push(string remote, Action outputHandler) + public void Push(string remote, Models.ICommandLog log) { - new SubCmd(_repo, $"lfs push {remote}", outputHandler).Exec(); + new SubCmd(_repo, $"lfs push {remote}", log).Exec(); } - public void Prune(Action outputHandler) + public void Prune(Models.ICommandLog log) { - new SubCmd(_repo, "lfs prune", outputHandler).Exec(); + new SubCmd(_repo, "lfs prune", log).Exec(); } public List Locks(string remote) @@ -82,7 +74,7 @@ namespace SourceGit.Commands var rs = cmd.ReadToEnd(); if (rs.IsSuccess) { - var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var match = REG_LOCK().Match(line); @@ -101,21 +93,21 @@ namespace SourceGit.Commands return locks; } - public bool Lock(string remote, string file) + public bool Lock(string remote, string file, Models.ICommandLog log) { - return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", null).Exec(); + return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", log).Exec(); } - public bool Unlock(string remote, string file, bool force) + public bool Unlock(string remote, string file, bool force, Models.ICommandLog log) { var opt = force ? "-f" : ""; - return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", null).Exec(); + return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", log).Exec(); } - public bool Unlock(string remote, long id, bool force) + public bool Unlock(string remote, long id, bool force, Models.ICommandLog log) { var opt = force ? "-f" : ""; - return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", null).Exec(); + return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", log).Exec(); } private readonly string _repo; diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs index cf2e285f..b08377b9 100644 --- a/src/Commands/Merge.cs +++ b/src/Commands/Merge.cs @@ -1,23 +1,36 @@ -using System; +using System.Collections.Generic; +using System.Text; namespace SourceGit.Commands { public class Merge : Command { - public Merge(string repo, string source, string mode, Action outputHandler) + public Merge(string repo, string source, string mode) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; Args = $"merge --progress {source} {mode}"; } - protected override void OnReadline(string line) + public Merge(string repo, List targets, bool autoCommit, string strategy) { - _outputHandler?.Invoke(line); - } + WorkingDirectory = repo; + Context = repo; - private readonly Action _outputHandler = null; + var builder = new StringBuilder(); + builder.Append("merge --progress "); + if (!string.IsNullOrEmpty(strategy)) + builder.Append($"--strategy={strategy} "); + if (!autoCommit) + builder.Append("--no-commit "); + + foreach (var t in targets) + { + builder.Append(t); + builder.Append(' '); + } + + Args = builder.ToString(); + } } } diff --git a/src/Commands/MergeTool.cs b/src/Commands/MergeTool.cs index d3478d59..fc6d0d75 100644 --- a/src/Commands/MergeTool.cs +++ b/src/Commands/MergeTool.cs @@ -13,15 +13,18 @@ namespace SourceGit.Commands cmd.Context = repo; cmd.RaiseError = true; + // NOTE: If no names are specified, 'git mergetool' will run the merge tool program on every file with merge conflicts. + var fileArg = string.IsNullOrEmpty(file) ? "" : $"\"{file}\""; + if (toolType == 0) { - cmd.Args = $"mergetool \"{file}\""; + cmd.Args = $"mergetool {fileArg}"; return cmd.Exec(); } if (!File.Exists(toolPath)) { - Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT found external merge tool in '{toolPath}'!")); + Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT find external merge tool in '{toolPath}'!")); return false; } @@ -32,7 +35,7 @@ namespace SourceGit.Commands return false; } - cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.Cmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit \"{file}\""; + cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{toolPath}\\\" {supported.Cmd}\" -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit {fileArg}"; return cmd.Exec(); } @@ -51,7 +54,7 @@ namespace SourceGit.Commands if (!File.Exists(toolPath)) { - Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT found external diff tool in '{toolPath}'!")); + Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT find external diff tool in '{toolPath}'!")); return false; } diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index a4efa4b6..698fbfce 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -1,31 +1,18 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Pull : Command { - public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action outputHandler) + public Pull(string repo, string remote, string branch, bool useRebase) { - _outputHandler = outputHandler; WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); - Args = "pull --verbose --progress --tags "; + Args = "pull --verbose --progress "; if (useRebase) - Args += "--rebase "; - if (noTags) - Args += "--no-tags "; + Args += "--rebase=true "; Args += $"{remote} {branch}"; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler; } } diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index 69b859ab..8a5fe33c 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -1,16 +1,11 @@ -using System; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Push : Command { - public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force, Action onProgress) + public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force) { - _outputHandler = onProgress; - WorkingDirectory = repo; Context = repo; - TraitErrorAsOutput = true; SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); Args = "push --progress --verbose "; @@ -26,7 +21,7 @@ namespace SourceGit.Commands Args += $"{remote} {local}:{remoteBranch}"; } - public Push(string repo, string remote, string tag, bool isDelete) + public Push(string repo, string remote, string refname, bool isDelete) { WorkingDirectory = repo; Context = repo; @@ -36,14 +31,7 @@ namespace SourceGit.Commands if (isDelete) Args += "--delete "; - Args += $"{remote} refs/tags/{tag}"; + Args += $"{remote} {refname}"; } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private readonly Action _outputHandler = null; } } diff --git a/src/Commands/QueryAssumeUnchangedFiles.cs b/src/Commands/QueryAssumeUnchangedFiles.cs new file mode 100644 index 00000000..b5c23b0b --- /dev/null +++ b/src/Commands/QueryAssumeUnchangedFiles.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands +{ + public partial class QueryAssumeUnchangedFiles : Command + { + [GeneratedRegex(@"^(\w)\s+(.+)$")] + private static partial Regex REG_PARSE(); + + public QueryAssumeUnchangedFiles(string repo) + { + WorkingDirectory = repo; + Args = "ls-files -v"; + RaiseError = false; + } + + public List Result() + { + var outs = new List(); + var rs = ReadToEnd(); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_PARSE().Match(line); + if (!match.Success) + continue; + + if (match.Groups[1].Value == "h") + outs.Add(match.Groups[2].Value); + } + + return outs; + } + } +} diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 95f97214..d0ecd322 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -14,22 +14,52 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "branch -l --all -v --format=\"%(refname)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; + Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; } - public List Result() + public List Result(out int localBranchesCount) { + localBranchesCount = 0; + var branches = new List(); var rs = ReadToEnd(); if (!rs.IsSuccess) return branches; - var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var remoteHeads = new Dictionary(); foreach (var line in lines) { var b = ParseLine(line); if (b != null) + { branches.Add(b); + if (!b.IsLocal) + remoteHeads.Add(b.FullName, b.Head); + else + localBranchesCount++; + } + } + + foreach (var b in branches) + { + if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream)) + { + if (remoteHeads.TryGetValue(b.Upstream, out var upstreamHead)) + { + b.IsUpstreamGone = false; + + if (b.TrackStatus == null) + b.TrackStatus = new QueryTrackStatus(WorkingDirectory, b.Head, upstreamHead).Result(); + } + else + { + b.IsUpstreamGone = true; + + if (b.TrackStatus == null) + b.TrackStatus = new Models.BranchTrackStatus(); + } + } } return branches; @@ -38,7 +68,7 @@ namespace SourceGit.Commands private Models.Branch ParseLine(string line) { var parts = line.Split('\0'); - if (parts.Length != 5) + if (parts.Length != 6) return null; var branch = new Models.Branch(); @@ -72,13 +102,16 @@ namespace SourceGit.Commands } branch.FullName = refName; - branch.Head = parts[1]; - branch.IsCurrent = parts[2] == "*"; - branch.Upstream = parts[3]; + branch.CommitterDate = ulong.Parse(parts[1]); + branch.Head = parts[2]; + branch.IsCurrent = parts[3] == "*"; + branch.Upstream = parts[4]; + branch.IsUpstreamGone = false; - if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal)) - branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result(); - else + if (!branch.IsLocal || + string.IsNullOrEmpty(branch.Upstream) || + string.IsNullOrEmpty(parts[5]) || + parts[5].Equals("=", StringComparison.Ordinal)) branch.TrackStatus = new Models.BranchTrackStatus(); return branch; diff --git a/src/Commands/QueryCommitChildren.cs b/src/Commands/QueryCommitChildren.cs new file mode 100644 index 00000000..4e99ce7a --- /dev/null +++ b/src/Commands/QueryCommitChildren.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.Commands +{ + public class QueryCommitChildren : Command + { + public QueryCommitChildren(string repo, string commit, int max) + { + WorkingDirectory = repo; + Context = repo; + _commit = commit; + Args = $"rev-list -{max} --parents --branches --remotes --ancestry-path ^{commit}"; + } + + public List Result() + { + var rs = ReadToEnd(); + var outs = new List(); + if (rs.IsSuccess) + { + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + if (line.Contains(_commit)) + outs.Add(line.Substring(0, 40)); + } + } + + return outs; + } + + private string _commit; + } +} diff --git a/src/Commands/QueryCommitFullMessage.cs b/src/Commands/QueryCommitFullMessage.cs index c8f1867d..36b6d1c7 100644 --- a/src/Commands/QueryCommitFullMessage.cs +++ b/src/Commands/QueryCommitFullMessage.cs @@ -6,7 +6,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"show --no-show-signature --pretty=format:%B -s {sha}"; + Args = $"show --no-show-signature --format=%B -s {sha}"; } public string Result() diff --git a/src/Commands/QueryCommitSignInfo.cs b/src/Commands/QueryCommitSignInfo.cs new file mode 100644 index 00000000..133949af --- /dev/null +++ b/src/Commands/QueryCommitSignInfo.cs @@ -0,0 +1,34 @@ +namespace SourceGit.Commands +{ + public class QueryCommitSignInfo : Command + { + public QueryCommitSignInfo(string repo, string sha, bool useFakeSignersFile) + { + WorkingDirectory = repo; + Context = repo; + + const string baseArgs = "show --no-show-signature --format=%G?%n%GS%n%GK -s"; + const string fakeSignersFileArg = "-c gpg.ssh.allowedSignersFile=/dev/null"; + Args = $"{(useFakeSignersFile ? fakeSignersFileArg : string.Empty)} {baseArgs} {sha}"; + } + + public Models.CommitSignInfo Result() + { + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return null; + + var raw = rs.StdOut.Trim().ReplaceLineEndings("\n"); + if (raw.Length <= 1) + return null; + + var lines = raw.Split('\n'); + return new Models.CommitSignInfo() + { + VerifyResult = lines[0][0], + Signer = lines[1], + Key = lines[2] + }; + } + } +} diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 76894412..9e1d9918 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -10,7 +10,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "log --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + limits; + Args = $"log --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}"; _findFirstMerged = needFindHead; } @@ -18,20 +18,20 @@ namespace SourceGit.Commands { string search = onlyCurrentBranch ? string.Empty : "--branches --remotes "; - if (method == Models.CommitSearchMethod.ByUser) + if (method == Models.CommitSearchMethod.ByAuthor) { - search += $"-i --author=\"{filter}\" --committer=\"{filter}\""; + search += $"-i --author=\"{filter}\""; } - else if (method == Models.CommitSearchMethod.ByFile) + else if (method == Models.CommitSearchMethod.ByCommitter) { - search += $"-- \"{filter}\""; + search += $"-i --committer=\"{filter}\""; } - else + else if (method == Models.CommitSearchMethod.ByMessage) { var argsBuilder = new StringBuilder(); argsBuilder.Append(search); - var words = filter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries); + var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries); foreach (var word in words) { var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal); @@ -41,10 +41,18 @@ namespace SourceGit.Commands search = argsBuilder.ToString(); } + else if (method == Models.CommitSearchMethod.ByFile) + { + search += $"-- \"{filter}\""; + } + else + { + search = $"-G\"{filter}\""; + } WorkingDirectory = repo; Context = repo; - Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search; + Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {search}"; _findFirstMerged = false; } @@ -112,15 +120,7 @@ namespace SourceGit.Commands if (data.Length < 8) return; - var idx = data.IndexOf(' ', StringComparison.Ordinal); - if (idx == -1) - { - _current.Parents.Add(data); - return; - } - - _current.Parents.Add(data.Substring(0, idx)); - _current.Parents.Add(data.Substring(idx + 1)); + _current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries)); } private void MarkFirstMerged() @@ -128,7 +128,7 @@ namespace SourceGit.Commands Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\""; var rs = ReadToEnd(); - var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); if (shas.Length == 0) return; diff --git a/src/Commands/QueryCommitsWithFullMessage.cs b/src/Commands/QueryCommitsForInteractiveRebase.cs similarity index 61% rename from src/Commands/QueryCommitsWithFullMessage.cs rename to src/Commands/QueryCommitsForInteractiveRebase.cs index 36e22b42..9f238319 100644 --- a/src/Commands/QueryCommitsWithFullMessage.cs +++ b/src/Commands/QueryCommitsForInteractiveRebase.cs @@ -3,18 +3,18 @@ using System.Collections.Generic; namespace SourceGit.Commands { - public class QueryCommitsWithFullMessage : Command + public class QueryCommitsForInteractiveRebase : Command { - public QueryCommitsWithFullMessage(string repo, string args) + public QueryCommitsForInteractiveRebase(string repo, string on) { _boundary = $"----- BOUNDARY OF COMMIT {Guid.NewGuid()} -----"; WorkingDirectory = repo; Context = repo; - Args = $"log --date-order --no-show-signature --decorate=full --pretty=format:\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {args}"; + Args = $"log --date-order --no-show-signature --decorate=full --format=\"%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%B%n{_boundary}\" {on}..HEAD"; } - public List Result() + public List Result() { var rs = ReadToEnd(); if (!rs.IsSuccess) @@ -29,7 +29,7 @@ namespace SourceGit.Commands switch (nextPartIdx) { case 0: - _current = new Models.CommitWithMessage(); + _current = new Models.InteractiveCommit(); _current.Commit.SHA = line; _commits.Add(_current); break; @@ -52,16 +52,28 @@ namespace SourceGit.Commands _current.Commit.CommitterTime = ulong.Parse(line); break; default: - if (line.Equals(_boundary, StringComparison.Ordinal)) - nextPartIdx = -1; + var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal); + if (boundary > end) + { + _current.Message = rs.StdOut.Substring(start, boundary - start - 1); + end = boundary + _boundary.Length; + } else - _current.Message += line; + { + _current.Message = rs.StdOut.Substring(start); + end = rs.StdOut.Length - 2; + } + + nextPartIdx = -1; break; } nextPartIdx++; start = end + 1; + if (start >= rs.StdOut.Length - 1) + break; + end = rs.StdOut.IndexOf('\n', start); } @@ -73,19 +85,11 @@ namespace SourceGit.Commands if (data.Length < 8) return; - var idx = data.IndexOf(' ', StringComparison.Ordinal); - if (idx == -1) - { - _current.Commit.Parents.Add(data); - return; - } - - _current.Commit.Parents.Add(data.Substring(0, idx)); - _current.Commit.Parents.Add(data.Substring(idx + 1)); + _current.Commit.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries)); } - private List _commits = new List(); - private Models.CommitWithMessage _current = null; - private string _boundary = ""; + private List _commits = []; + private Models.InteractiveCommit _current = null; + private readonly string _boundary; } } diff --git a/src/Commands/QueryCurrentRevisionFiles.cs b/src/Commands/QueryCurrentRevisionFiles.cs deleted file mode 100644 index 217ea20e..00000000 --- a/src/Commands/QueryCurrentRevisionFiles.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace SourceGit.Commands -{ - public class QueryCurrentRevisionFiles : Command - { - public QueryCurrentRevisionFiles(string repo) - { - WorkingDirectory = repo; - Context = repo; - Args = "ls-tree -r --name-only HEAD"; - } - - public string[] Result() - { - var rs = ReadToEnd(); - if (rs.IsSuccess) - return rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries); - - return []; - } - } -} diff --git a/src/Commands/QueryFileContent.cs b/src/Commands/QueryFileContent.cs index f887859c..83d0a575 100644 --- a/src/Commands/QueryFileContent.cs +++ b/src/Commands/QueryFileContent.cs @@ -35,5 +35,39 @@ namespace SourceGit.Commands return stream; } + + public static Stream FromLFS(string repo, string oid, long size) + { + var starter = new ProcessStartInfo(); + starter.WorkingDirectory = repo; + starter.FileName = Native.OS.GitExecutable; + starter.Arguments = $"lfs smudge"; + starter.UseShellExecute = false; + starter.CreateNoWindow = true; + starter.WindowStyle = ProcessWindowStyle.Hidden; + starter.RedirectStandardInput = true; + starter.RedirectStandardOutput = true; + + var stream = new MemoryStream(); + try + { + var proc = new Process() { StartInfo = starter }; + proc.Start(); + proc.StandardInput.WriteLine("version https://git-lfs.github.com/spec/v1"); + proc.StandardInput.WriteLine($"oid sha256:{oid}"); + proc.StandardInput.WriteLine($"size {size}"); + proc.StandardOutput.BaseStream.CopyTo(stream); + proc.WaitForExit(); + proc.Close(); + + stream.Position = 0; + } + catch (Exception e) + { + App.RaiseException(repo, $"Failed to query file content: {e}"); + } + + return stream; + } } } diff --git a/src/Commands/QueryFileSize.cs b/src/Commands/QueryFileSize.cs index c36984dd..30af7715 100644 --- a/src/Commands/QueryFileSize.cs +++ b/src/Commands/QueryFileSize.cs @@ -11,14 +11,11 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"ls-tree {revision} -l -- {file}"; + Args = $"ls-tree {revision} -l -- \"{file}\""; } public long Result() { - if (_result != 0) - return _result; - var rs = ReadToEnd(); if (rs.IsSuccess) { @@ -29,7 +26,5 @@ namespace SourceGit.Commands return 0; } - - private readonly long _result = 0; } } diff --git a/src/Commands/QueryGitCommonDir.cs b/src/Commands/QueryGitCommonDir.cs new file mode 100644 index 00000000..1076243e --- /dev/null +++ b/src/Commands/QueryGitCommonDir.cs @@ -0,0 +1,26 @@ +using System.IO; + +namespace SourceGit.Commands +{ + public class QueryGitCommonDir : Command + { + public QueryGitCommonDir(string workDir) + { + WorkingDirectory = workDir; + Args = "rev-parse --git-common-dir"; + RaiseError = false; + } + + public string Result() + { + var rs = ReadToEnd().StdOut; + if (string.IsNullOrEmpty(rs)) + return null; + + rs = rs.Trim(); + if (Path.IsPathRooted(rs)) + return rs; + return Path.GetFullPath(Path.Combine(WorkingDirectory, rs)); + } + } +} diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index bdef9bf8..788ed617 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Text.RegularExpressions; +using Avalonia.Threading; + namespace SourceGit.Commands { public partial class QueryLocalChanges : Command @@ -14,144 +16,150 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain"; + Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain"; } public List Result() { - Exec(); - return _changes; - } - - protected override void OnReadline(string line) - { - var match = REG_FORMAT().Match(line); - if (!match.Success) - return; - - var change = new Models.Change() { Path = match.Groups[2].Value }; - var status = match.Groups[1].Value; - - switch (status) + var outs = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) { - case " M": - change.Set(Models.ChangeState.None, Models.ChangeState.Modified); - break; - case " T": - change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged); - break; - case " A": - change.Set(Models.ChangeState.None, Models.ChangeState.Added); - break; - case " D": - change.Set(Models.ChangeState.None, Models.ChangeState.Deleted); - break; - case " R": - change.Set(Models.ChangeState.None, Models.ChangeState.Renamed); - break; - case " C": - change.Set(Models.ChangeState.None, Models.ChangeState.Copied); - break; - case "M": - change.Set(Models.ChangeState.Modified); - break; - case "MM": - change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified); - break; - case "MT": - change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged); - break; - case "MD": - change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted); - break; - case "T": - change.Set(Models.ChangeState.TypeChanged); - break; - case "TM": - change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified); - break; - case "TT": - change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged); - break; - case "TD": - change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted); - break; - case "A": - change.Set(Models.ChangeState.Added); - break; - case "AM": - change.Set(Models.ChangeState.Added, Models.ChangeState.Modified); - break; - case "AT": - change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged); - break; - case "AD": - change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted); - break; - case "D": - change.Set(Models.ChangeState.Deleted); - break; - case "R": - change.Set(Models.ChangeState.Renamed); - break; - case "RM": - change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified); - break; - case "RT": - change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged); - break; - case "RD": - change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted); - break; - case "C": - change.Set(Models.ChangeState.Copied); - break; - case "CM": - change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified); - break; - case "CT": - change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged); - break; - case "CD": - change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted); - break; - case "DR": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Renamed); - break; - case "DC": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Copied); - break; - case "DD": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Deleted); - break; - case "AU": - change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged); - break; - case "UD": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted); - break; - case "UA": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added); - break; - case "DU": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged); - break; - case "AA": - change.Set(Models.ChangeState.Added, Models.ChangeState.Added); - break; - case "UU": - change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged); - break; - case "??": - change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked); - break; - default: - return; + Dispatcher.UIThread.Post(() => App.RaiseException(Context, rs.StdErr)); + return outs; } - _changes.Add(change); - } + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT().Match(line); + if (!match.Success) + continue; - private readonly List _changes = new List(); + var change = new Models.Change() { Path = match.Groups[2].Value }; + var status = match.Groups[1].Value; + + switch (status) + { + case " M": + change.Set(Models.ChangeState.None, Models.ChangeState.Modified); + break; + case " T": + change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged); + break; + case " A": + change.Set(Models.ChangeState.None, Models.ChangeState.Added); + break; + case " D": + change.Set(Models.ChangeState.None, Models.ChangeState.Deleted); + break; + case " R": + change.Set(Models.ChangeState.None, Models.ChangeState.Renamed); + break; + case " C": + change.Set(Models.ChangeState.None, Models.ChangeState.Copied); + break; + case "M": + change.Set(Models.ChangeState.Modified); + break; + case "MM": + change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified); + break; + case "MT": + change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged); + break; + case "MD": + change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted); + break; + case "T": + change.Set(Models.ChangeState.TypeChanged); + break; + case "TM": + change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified); + break; + case "TT": + change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged); + break; + case "TD": + change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted); + break; + case "A": + change.Set(Models.ChangeState.Added); + break; + case "AM": + change.Set(Models.ChangeState.Added, Models.ChangeState.Modified); + break; + case "AT": + change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged); + break; + case "AD": + change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted); + break; + case "D": + change.Set(Models.ChangeState.Deleted); + break; + case "R": + change.Set(Models.ChangeState.Renamed); + break; + case "RM": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified); + break; + case "RT": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged); + break; + case "RD": + change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted); + break; + case "C": + change.Set(Models.ChangeState.Copied); + break; + case "CM": + change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified); + break; + case "CT": + change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged); + break; + case "CD": + change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted); + break; + case "DD": + change.ConflictReason = Models.ConflictReason.BothDeleted; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "AU": + change.ConflictReason = Models.ConflictReason.AddedByUs; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "UD": + change.ConflictReason = Models.ConflictReason.DeletedByThem; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "UA": + change.ConflictReason = Models.ConflictReason.AddedByThem; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "DU": + change.ConflictReason = Models.ConflictReason.DeletedByUs; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "AA": + change.ConflictReason = Models.ConflictReason.BothAdded; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "UU": + change.ConflictReason = Models.ConflictReason.BothModified; + change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted); + break; + case "??": + change.Set(Models.ChangeState.None, Models.ChangeState.Untracked); + break; + } + + if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None) + outs.Add(change); + } + + return outs; + } } } diff --git a/src/Commands/QueryRefsContainsCommit.cs b/src/Commands/QueryRefsContainsCommit.cs index e3b73ccc..cabe1b50 100644 --- a/src/Commands/QueryRefsContainsCommit.cs +++ b/src/Commands/QueryRefsContainsCommit.cs @@ -20,9 +20,12 @@ namespace SourceGit.Commands if (!output.IsSuccess) return rs; - var lines = output.StdOut.Split('\n'); + var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { + if (line.EndsWith("/HEAD", StringComparison.Ordinal)) + continue; + if (line.StartsWith("refs/heads/", StringComparison.Ordinal)) rs.Add(new() { Name = line.Substring("refs/heads/".Length), Type = Models.DecoratorType.LocalBranchHead }); else if (line.StartsWith("refs/remotes/", StringComparison.Ordinal)) diff --git a/src/Commands/QueryRemotes.cs b/src/Commands/QueryRemotes.cs index b5b41b4a..7afec74d 100644 --- a/src/Commands/QueryRemotes.cs +++ b/src/Commands/QueryRemotes.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; namespace SourceGit.Commands @@ -17,27 +18,31 @@ namespace SourceGit.Commands public List Result() { - Exec(); - return _loaded; - } + var outs = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return outs; - protected override void OnReadline(string line) - { - var match = REG_REMOTE().Match(line); - if (!match.Success) - return; - - var remote = new Models.Remote() + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - Name = match.Groups[1].Value, - URL = match.Groups[2].Value, - }; + var match = REG_REMOTE().Match(line); + if (!match.Success) + continue; - if (_loaded.Find(x => x.Name == remote.Name) != null) - return; - _loaded.Add(remote); + var remote = new Models.Remote() + { + Name = match.Groups[1].Value, + URL = match.Groups[2].Value, + }; + + if (outs.Find(x => x.Name == remote.Name) != null) + continue; + + outs.Add(remote); + } + + return outs; } - - private readonly List _loaded = new List(); } } diff --git a/src/Commands/QueryRevisionByRefName.cs b/src/Commands/QueryRevisionByRefName.cs new file mode 100644 index 00000000..7fb4ecfa --- /dev/null +++ b/src/Commands/QueryRevisionByRefName.cs @@ -0,0 +1,21 @@ +namespace SourceGit.Commands +{ + public class QueryRevisionByRefName : Command + { + public QueryRevisionByRefName(string repo, string refname) + { + WorkingDirectory = repo; + Context = repo; + Args = $"rev-parse {refname}"; + } + + public string Result() + { + var rs = ReadToEnd(); + if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut)) + return rs.StdOut.Trim(); + + return null; + } + } +} diff --git a/src/Commands/QueryRevisionFileNames.cs b/src/Commands/QueryRevisionFileNames.cs new file mode 100644 index 00000000..c6fd7373 --- /dev/null +++ b/src/Commands/QueryRevisionFileNames.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace SourceGit.Commands +{ + public class QueryRevisionFileNames : Command + { + public QueryRevisionFileNames(string repo, string revision) + { + WorkingDirectory = repo; + Context = repo; + Args = $"ls-tree -r -z --name-only {revision}"; + } + + public List Result() + { + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return []; + + var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries); + var outs = new List(); + foreach (var line in lines) + outs.Add(line); + return outs; + } + } +} diff --git a/src/Commands/QueryRevisionObjects.cs b/src/Commands/QueryRevisionObjects.cs index bcad9129..de3406e8 100644 --- a/src/Commands/QueryRevisionObjects.cs +++ b/src/Commands/QueryRevisionObjects.cs @@ -12,7 +12,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"ls-tree {sha}"; + Args = $"ls-tree -z {sha}"; if (!string.IsNullOrEmpty(parentFolder)) Args += $" -- \"{parentFolder}\""; @@ -20,11 +20,27 @@ namespace SourceGit.Commands public List Result() { - Exec(); + var rs = ReadToEnd(); + if (rs.IsSuccess) + { + var start = 0; + var end = rs.StdOut.IndexOf('\0', start); + while (end > 0) + { + var line = rs.StdOut.Substring(start, end - start); + Parse(line); + start = end + 1; + end = rs.StdOut.IndexOf('\0', start); + } + + if (start < rs.StdOut.Length) + Parse(rs.StdOut.Substring(start)); + } + return _objects; } - protected override void OnReadline(string line) + private void Parse(string line) { var match = REG_FORMAT().Match(line); if (!match.Success) diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs index 1e1c9ea4..35289ec5 100644 --- a/src/Commands/QuerySingleCommit.cs +++ b/src/Commands/QuerySingleCommit.cs @@ -8,7 +8,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"show --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}"; + Args = $"show --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s -s {sha}"; } public Models.Commit Result() diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs index f5c9659f..78980401 100644 --- a/src/Commands/QueryStagedChangesWithAmend.cs +++ b/src/Commands/QueryStagedChangesWithAmend.cs @@ -6,87 +6,87 @@ namespace SourceGit.Commands { public partial class QueryStagedChangesWithAmend : Command { - [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMTUX])\d{0,6}\t(.*)$")] + [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMT])\d{0,6}\t(.*)$")] private static partial Regex REG_FORMAT1(); [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")] private static partial Regex REG_FORMAT2(); - public QueryStagedChangesWithAmend(string repo) + public QueryStagedChangesWithAmend(string repo, string parent) { WorkingDirectory = repo; Context = repo; - Args = "diff-index --cached -M HEAD^"; + Args = $"diff-index --cached -M {parent}"; + _parent = parent; } public List Result() { var rs = ReadToEnd(); - if (rs.IsSuccess) + if (!rs.IsSuccess) + return []; + + var changes = new List(); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - var changes = new List(); - var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) + var match = REG_FORMAT2().Match(line); + if (match.Success) { - var match = REG_FORMAT2().Match(line); - if (match.Success) + var change = new Models.Change() { - var change = new Models.Change() + Path = match.Groups[3].Value, + DataForAmend = new Models.ChangeDataForAmend() { - Path = match.Groups[3].Value, - DataForAmend = new Models.ChangeDataForAmend() - { - FileMode = match.Groups[1].Value, - ObjectHash = match.Groups[2].Value, - }, - }; - change.Set(Models.ChangeState.Renamed); - changes.Add(change); - continue; - } - - match = REG_FORMAT1().Match(line); - if (match.Success) - { - var change = new Models.Change() - { - Path = match.Groups[4].Value, - DataForAmend = new Models.ChangeDataForAmend() - { - FileMode = match.Groups[1].Value, - ObjectHash = match.Groups[2].Value, - }, - }; - - var type = match.Groups[3].Value; - switch (type) - { - case "A": - change.Set(Models.ChangeState.Added); - break; - case "C": - change.Set(Models.ChangeState.Copied); - break; - case "D": - change.Set(Models.ChangeState.Deleted); - break; - case "M": - change.Set(Models.ChangeState.Modified); - break; - case "T": - change.Set(Models.ChangeState.TypeChanged); - break; - case "U": - change.Set(Models.ChangeState.Unmerged); - break; - } - changes.Add(change); - } + FileMode = match.Groups[1].Value, + ObjectHash = match.Groups[2].Value, + ParentSHA = _parent, + }, + }; + change.Set(Models.ChangeState.Renamed); + changes.Add(change); + continue; } - return changes; + match = REG_FORMAT1().Match(line); + if (match.Success) + { + var change = new Models.Change() + { + Path = match.Groups[4].Value, + DataForAmend = new Models.ChangeDataForAmend() + { + FileMode = match.Groups[1].Value, + ObjectHash = match.Groups[2].Value, + ParentSHA = _parent, + }, + }; + + var type = match.Groups[3].Value; + switch (type) + { + case "A": + change.Set(Models.ChangeState.Added); + break; + case "C": + change.Set(Models.ChangeState.Copied); + break; + case "D": + change.Set(Models.ChangeState.Deleted); + break; + case "M": + change.Set(Models.ChangeState.Modified); + break; + case "T": + change.Set(Models.ChangeState.TypeChanged); + break; + } + changes.Add(change); + } } - return []; + return changes; } + + private readonly string _parent; } } diff --git a/src/Commands/QueryStashChanges.cs b/src/Commands/QueryStashChanges.cs deleted file mode 100644 index 3b8d2db6..00000000 --- a/src/Commands/QueryStashChanges.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace SourceGit.Commands -{ - public partial class QueryStashChanges : Command - { - [GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")] - private static partial Regex REG_FORMAT(); - - public QueryStashChanges(string repo, string sha) - { - WorkingDirectory = repo; - Context = repo; - Args = $"diff --name-status --pretty=format: {sha}^ {sha}"; - } - - public List Result() - { - Exec(); - return _changes; - } - - protected override void OnReadline(string line) - { - var match = REG_FORMAT().Match(line); - if (!match.Success) - return; - - var change = new Models.Change() { Path = match.Groups[2].Value }; - var status = match.Groups[1].Value; - - switch (status[0]) - { - case 'M': - change.Set(Models.ChangeState.Modified); - _changes.Add(change); - break; - case 'A': - change.Set(Models.ChangeState.Added); - _changes.Add(change); - break; - case 'D': - change.Set(Models.ChangeState.Deleted); - _changes.Add(change); - break; - case 'R': - change.Set(Models.ChangeState.Renamed); - _changes.Add(change); - break; - case 'C': - change.Set(Models.ChangeState.Copied); - _changes.Add(change); - break; - } - } - - private readonly List _changes = new List(); - } -} diff --git a/src/Commands/QueryStashes.cs b/src/Commands/QueryStashes.cs index 6d089f8e..b4067aaf 100644 --- a/src/Commands/QueryStashes.cs +++ b/src/Commands/QueryStashes.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace SourceGit.Commands { @@ -8,41 +9,65 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "stash list --pretty=format:%H%n%ct%n%gd%n%s"; + Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s"; } public List Result() { - Exec(); - return _stashes; - } + var outs = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return outs; - protected override void OnReadline(string line) - { - switch (_nextLineIdx) + var nextPartIdx = 0; + var start = 0; + var end = rs.StdOut.IndexOf('\n', start); + while (end > 0) { - case 0: - _current = new Models.Stash() { SHA = line }; - _stashes.Add(_current); - break; - case 1: - _current.Time = ulong.Parse(line); - break; - case 2: - _current.Name = line; - break; - case 3: - _current.Message = line; - break; + var line = rs.StdOut.Substring(start, end - start); + + switch (nextPartIdx) + { + case 0: + _current = new Models.Stash() { SHA = line }; + outs.Add(_current); + break; + case 1: + ParseParent(line); + break; + case 2: + _current.Time = ulong.Parse(line); + break; + case 3: + _current.Name = line; + break; + case 4: + _current.Message = line; + break; + } + + nextPartIdx++; + if (nextPartIdx > 4) + nextPartIdx = 0; + + start = end + 1; + end = rs.StdOut.IndexOf('\n', start); } - _nextLineIdx++; - if (_nextLineIdx > 3) - _nextLineIdx = 0; + if (start < rs.StdOut.Length) + _current.Message = rs.StdOut.Substring(start); + + return outs; + } + + private void ParseParent(string data) + { + if (data.Length < 8) + return; + + _current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries)); } - private readonly List _stashes = new List(); private Models.Stash _current = null; - private int _nextLineIdx = 0; } } diff --git a/src/Commands/QuerySubmodules.cs b/src/Commands/QuerySubmodules.cs index 5fd6e3d5..663c0ea0 100644 --- a/src/Commands/QuerySubmodules.cs +++ b/src/Commands/QuerySubmodules.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; @@ -6,12 +7,12 @@ namespace SourceGit.Commands { public partial class QuerySubmodules : Command { - [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")] - private static partial Regex REG_FORMAT1(); - [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")] - private static partial Regex REG_FORMAT2(); - [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")] + [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] private static partial Regex REG_FORMAT_STATUS(); + [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")] + private static partial Regex REG_FORMAT_DIRTY(); + [GeneratedRegex(@"^submodule\.(\S*)\.(\w+)=(.*)$")] + private static partial Regex REG_FORMAT_MODULE_INFO(); public QuerySubmodules(string repo) { @@ -24,55 +25,118 @@ namespace SourceGit.Commands { var submodules = new List(); var rs = ReadToEnd(); - if (!rs.IsSuccess) - return submodules; - var builder = new StringBuilder(); - var lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var map = new Dictionary(); + var needCheckLocalChanges = false; foreach (var line in lines) { - var match = REG_FORMAT1().Match(line); + var match = REG_FORMAT_STATUS().Match(line); if (match.Success) { - var path = match.Groups[1].Value; - builder.Append($"\"{path}\" "); - submodules.Add(new Models.Submodule() { Path = path }); - continue; - } + var stat = match.Groups[1].Value; + var sha = match.Groups[2].Value; + var path = match.Groups[3].Value; - match = REG_FORMAT2().Match(line); - if (match.Success) - { - var path = match.Groups[1].Value; - builder.Append($"\"{path}\" "); - submodules.Add(new Models.Submodule() { Path = path }); + var module = new Models.Submodule() { Path = path, SHA = sha }; + switch (stat[0]) + { + case '-': + module.Status = Models.SubmoduleStatus.NotInited; + break; + case '+': + module.Status = Models.SubmoduleStatus.RevisionChanged; + break; + case 'U': + module.Status = Models.SubmoduleStatus.Unmerged; + break; + default: + module.Status = Models.SubmoduleStatus.Normal; + needCheckLocalChanges = true; + break; + } + + map.Add(path, module); + submodules.Add(module); } } if (submodules.Count > 0) { - Args = $"status -uno --porcelain -- {builder}"; + Args = "config --file .gitmodules --list"; + rs = ReadToEnd(); + if (rs.IsSuccess) + { + var modules = new Dictionary(); + lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT_MODULE_INFO().Match(line); + if (match.Success) + { + var name = match.Groups[1].Value; + var key = match.Groups[2].Value; + var val = match.Groups[3].Value; + + if (!modules.TryGetValue(name, out var m)) + { + m = new ModuleInfo(); + modules.Add(name, m); + } + + if (key.Equals("path", StringComparison.Ordinal)) + m.Path = val; + else if (key.Equals("url", StringComparison.Ordinal)) + m.URL = val; + } + } + + foreach (var kv in modules) + { + if (map.TryGetValue(kv.Value.Path, out var m)) + m.URL = kv.Value.URL; + } + } + } + + if (needCheckLocalChanges) + { + var builder = new StringBuilder(); + foreach (var kv in map) + { + if (kv.Value.Status == Models.SubmoduleStatus.Normal) + { + builder.Append('"'); + builder.Append(kv.Key); + builder.Append("\" "); + } + } + + Args = $"--no-optional-locks status --porcelain -- {builder}"; rs = ReadToEnd(); if (!rs.IsSuccess) return submodules; - var dirty = new HashSet(); - lines = rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries); + lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { - var match = REG_FORMAT_STATUS().Match(line); + var match = REG_FORMAT_DIRTY().Match(line); if (match.Success) { var path = match.Groups[1].Value; - dirty.Add(path); + if (map.TryGetValue(path, out var m)) + m.Status = Models.SubmoduleStatus.Modified; } } - - foreach (var submodule in submodules) - submodule.IsDirty = dirty.Contains(submodule.Path); } return submodules; } + + private class ModuleInfo + { + public string Path { get; set; } = string.Empty; + public string URL { get; set; } = string.Empty; + } } } diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index 3aa20dc2..4b706439 100644 --- a/src/Commands/QueryTags.cs +++ b/src/Commands/QueryTags.cs @@ -11,7 +11,7 @@ namespace SourceGit.Commands Context = repo; WorkingDirectory = repo; - Args = $"tag -l --sort=-creatordate --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%00%(contents:subject)%0a%0a%(contents:body)\""; + Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; } public List Result() @@ -25,15 +25,21 @@ namespace SourceGit.Commands foreach (var record in records) { var subs = record.Split('\0', StringSplitOptions.None); - if (subs.Length != 4) + if (subs.Length != 6) continue; - var message = subs[3].Trim(); + var name = subs[0].Substring(10); + var message = subs[5].Trim(); + if (!string.IsNullOrEmpty(message) && message.Equals(name, StringComparison.Ordinal)) + message = null; + tags.Add(new Models.Tag() { - Name = subs[0].Substring(10), - SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2], - Message = string.IsNullOrEmpty(message) ? null : message, + Name = name, + IsAnnotated = subs[1].Equals("tag", StringComparison.Ordinal), + SHA = string.IsNullOrEmpty(subs[3]) ? subs[2] : subs[3], + CreatorDate = ulong.Parse(subs[4]), + Message = message, }); } diff --git a/src/Commands/QueryTrackStatus.cs b/src/Commands/QueryTrackStatus.cs index 0bf9746f..e7e1f1c9 100644 --- a/src/Commands/QueryTrackStatus.cs +++ b/src/Commands/QueryTrackStatus.cs @@ -19,7 +19,7 @@ namespace SourceGit.Commands if (!rs.IsSuccess) return status; - var lines = rs.StdOut.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (line[0] == '>') diff --git a/src/Commands/QueryUpdatableSubmodules.cs b/src/Commands/QueryUpdatableSubmodules.cs new file mode 100644 index 00000000..03f4a24d --- /dev/null +++ b/src/Commands/QueryUpdatableSubmodules.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands +{ + public partial class QueryUpdatableSubmodules : Command + { + [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] + private static partial Regex REG_FORMAT_STATUS(); + + public QueryUpdatableSubmodules(string repo) + { + WorkingDirectory = repo; + Context = repo; + Args = "submodule status"; + } + + public List Result() + { + var submodules = new List(); + var rs = ReadToEnd(); + + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT_STATUS().Match(line); + if (match.Success) + { + var stat = match.Groups[1].Value; + var path = match.Groups[3].Value; + if (!stat.StartsWith(' ')) + submodules.Add(path); + } + } + + return submodules; + } + } +} diff --git a/src/Commands/Remote.cs b/src/Commands/Remote.cs index f2e8d09e..beaf412b 100644 --- a/src/Commands/Remote.cs +++ b/src/Commands/Remote.cs @@ -45,5 +45,14 @@ Args = "remote set-url" + (isPush ? " --push " : " ") + $"{name} {url}"; return Exec(); } + + public bool HasBranch(string remote, string branch) + { + SSHKey = new Config(WorkingDirectory).Get($"remote.{remote}.sshkey"); + Args = $"ls-remote {remote} {branch}"; + + var rs = ReadToEnd(); + return rs.IsSuccess && rs.StdOut.Trim().Length > 0; + } } } diff --git a/src/Commands/Reset.cs b/src/Commands/Reset.cs index da272135..6a54533b 100644 --- a/src/Commands/Reset.cs +++ b/src/Commands/Reset.cs @@ -1,33 +1,7 @@ -using System.Collections.Generic; -using System.Text; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Reset : Command { - public Reset(string repo) - { - WorkingDirectory = repo; - Context = repo; - Args = "reset"; - } - - public Reset(string repo, List changes) - { - WorkingDirectory = repo; - Context = repo; - - var builder = new StringBuilder(); - builder.Append("reset --"); - foreach (var c in changes) - { - builder.Append(" \""); - builder.Append(c.Path); - builder.Append("\""); - } - Args = builder.ToString(); - } - public Reset(string repo, string revision, string mode) { WorkingDirectory = repo; diff --git a/src/Commands/Restore.cs b/src/Commands/Restore.cs index 7a363543..663ea975 100644 --- a/src/Commands/Restore.cs +++ b/src/Commands/Restore.cs @@ -1,29 +1,52 @@ -using System.Collections.Generic; -using System.Text; +using System.Text; namespace SourceGit.Commands { public class Restore : Command { - public Restore(string repo) + /// + /// Only used for single staged change. + /// + /// + /// + public Restore(string repo, Models.Change stagedChange) { WorkingDirectory = repo; Context = repo; - Args = "restore . --source=HEAD --staged --worktree --recurse-submodules"; + + var builder = new StringBuilder(); + builder.Append("restore --staged -- \""); + builder.Append(stagedChange.Path); + builder.Append('"'); + + if (stagedChange.Index == Models.ChangeState.Renamed) + { + builder.Append(" \""); + builder.Append(stagedChange.OriginalPath); + builder.Append('"'); + } + + Args = builder.ToString(); } - public Restore(string repo, List files, string extra) + /// + /// Restore changes given in a path-spec file. + /// + /// + /// + /// + public Restore(string repo, string pathspecFile, bool isStaged) { WorkingDirectory = repo; Context = repo; - StringBuilder builder = new StringBuilder(); + var builder = new StringBuilder(); builder.Append("restore "); - if (!string.IsNullOrEmpty(extra)) - builder.Append(extra).Append(" "); - builder.Append("--"); - foreach (var f in files) - builder.Append(' ').Append('"').Append(f).Append('"'); + builder.Append(isStaged ? "--staged " : "--worktree --recurse-submodules "); + builder.Append("--pathspec-from-file=\""); + builder.Append(pathspecFile); + builder.Append('"'); + Args = builder.ToString(); } } diff --git a/src/Commands/SaveChangesAsPatch.cs b/src/Commands/SaveChangesAsPatch.cs index 409127ba..b10037a1 100644 --- a/src/Commands/SaveChangesAsPatch.cs +++ b/src/Commands/SaveChangesAsPatch.cs @@ -9,7 +9,7 @@ namespace SourceGit.Commands { public static class SaveChangesAsPatch { - public static bool Exec(string repo, List changes, bool isUnstaged, string saveTo) + public static bool ProcessLocalChanges(string repo, List changes, bool isUnstaged, string saveTo) { using (var sw = File.Create(saveTo)) { @@ -23,6 +23,33 @@ namespace SourceGit.Commands return true; } + public static bool ProcessRevisionCompareChanges(string repo, List changes, string baseRevision, string targetRevision, string saveTo) + { + using (var sw = File.Create(saveTo)) + { + foreach (var change in changes) + { + if (!ProcessSingleChange(repo, new Models.DiffOption(baseRevision, targetRevision, change), sw)) + return false; + } + } + + return true; + } + + public static bool ProcessStashChanges(string repo, List opts, string saveTo) + { + using (var sw = File.Create(saveTo)) + { + foreach (var opt in opts) + { + if (!ProcessSingleChange(repo, opt, sw)) + return false; + } + } + return true; + } + private static bool ProcessSingleChange(string repo, Models.DiffOption opt, FileStream writer) { var starter = new ProcessStartInfo(); diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index 99e89093..550844ef 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -13,12 +13,8 @@ namespace SourceGit.Commands var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result(); if (isLFSFiltered) { - var tmpFile = saveTo + ".tmp"; - if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile)) - { - ExecCmd(repo, $"lfs smudge", saveTo, tmpFile); - } - File.Delete(tmpFile); + var pointerStream = QueryFileContent.Run(repo, revision, file); + ExecCmd(repo, $"lfs smudge", saveTo, pointerStream); } else { @@ -26,7 +22,7 @@ namespace SourceGit.Commands } } - private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null) + private static bool ExecCmd(string repo, string args, string outputFile, Stream input = null) { var starter = new ProcessStartInfo(); starter.WorkingDirectory = repo; @@ -45,21 +41,8 @@ namespace SourceGit.Commands { var proc = new Process() { StartInfo = starter }; proc.Start(); - - if (inputFile != null) - { - using (StreamReader sr = new StreamReader(inputFile)) - { - while (true) - { - var line = sr.ReadLine(); - if (line == null) - break; - proc.StandardInput.WriteLine(line); - } - } - } - + if (input != null) + proc.StandardInput.Write(new StreamReader(input).ReadToEnd()); proc.StandardOutput.BaseStream.CopyTo(sw); proc.WaitForExit(); var rs = proc.ExitCode == 0; diff --git a/src/Commands/Stash.cs b/src/Commands/Stash.cs index fbdb6bd4..7d1a269b 100644 --- a/src/Commands/Stash.cs +++ b/src/Commands/Stash.cs @@ -11,69 +11,84 @@ namespace SourceGit.Commands Context = repo; } - public bool Push(string message) + public bool Push(string message, bool includeUntracked = true, bool keepIndex = false) { - Args = $"stash push -m \"{message}\""; + var builder = new StringBuilder(); + builder.Append("stash push "); + if (includeUntracked) + builder.Append("--include-untracked "); + if (keepIndex) + builder.Append("--keep-index "); + builder.Append("-m \""); + builder.Append(message); + builder.Append("\""); + + Args = builder.ToString(); return Exec(); } - public bool Push(List changes, string message, bool onlyStaged) + public bool Push(string message, List changes, bool keepIndex) { - var pathsBuilder = new StringBuilder(); + var builder = new StringBuilder(); + builder.Append("stash push --include-untracked "); + if (keepIndex) + builder.Append("--keep-index "); + builder.Append("-m \""); + builder.Append(message); + builder.Append("\" -- "); - if (onlyStaged) - { - foreach (var c in changes) - pathsBuilder.Append($"\"{c.Path}\" "); - - var paths = pathsBuilder.ToString(); - Args = $"stash push --staged -m \"{message}\" -- {paths}"; - } - else - { - var needAdd = new List(); - foreach (var c in changes) - { - pathsBuilder.Append($"\"{c.Path}\" "); - - if (c.WorkTree == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) - { - needAdd.Add(c); - if (needAdd.Count > 10) - { - new Add(WorkingDirectory, needAdd).Exec(); - needAdd.Clear(); - } - } - } - if (needAdd.Count > 0) - { - new Add(WorkingDirectory, needAdd).Exec(); - needAdd.Clear(); - } - - var paths = pathsBuilder.ToString(); - Args = $"stash push -m \"{message}\" -- {paths}"; - } + foreach (var c in changes) + builder.Append($"\"{c.Path}\" "); + Args = builder.ToString(); return Exec(); } - public bool Apply(string name) + public bool Push(string message, string pathspecFromFile, bool keepIndex) { - Args = $"stash apply -q {name}"; + var builder = new StringBuilder(); + builder.Append("stash push --include-untracked --pathspec-from-file=\""); + builder.Append(pathspecFromFile); + builder.Append("\" "); + if (keepIndex) + builder.Append("--keep-index "); + builder.Append("-m \""); + builder.Append(message); + builder.Append("\""); + + Args = builder.ToString(); + return Exec(); + } + + public bool PushOnlyStaged(string message, bool keepIndex) + { + var builder = new StringBuilder(); + builder.Append("stash push --staged "); + if (keepIndex) + builder.Append("--keep-index "); + builder.Append("-m \""); + builder.Append(message); + builder.Append("\""); + Args = builder.ToString(); + return Exec(); + } + + public bool Apply(string name, bool restoreIndex) + { + var opts = restoreIndex ? "--index" : string.Empty; + Args = $"stash apply -q {opts} \"{name}\""; return Exec(); } public bool Pop(string name) { - Args = $"stash pop -q {name}"; + Args = $"stash pop -q --index \"{name}\""; return Exec(); } public bool Drop(string name) { - Args = $"stash drop -q {name}"; + Args = $"stash drop -q \"{name}\""; return Exec(); } diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index adfcc574..e11c1740 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -1,14 +1,14 @@ -using System; +using System; namespace SourceGit.Commands { public class Statistics : Command { - public Statistics(string repo) + public Statistics(string repo, int max) { WorkingDirectory = repo; Context = repo; - Args = $"log --date-order --branches --remotes -40000 --pretty=format:\"%ct$%aN\""; + Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN±%aE"; } public Models.Statistics Result() @@ -40,7 +40,7 @@ namespace SourceGit.Commands if (dateEndIdx == -1) return; - var dateStr = line.Substring(0, dateEndIdx); + var dateStr = line.AsSpan(0, dateEndIdx); if (double.TryParse(dateStr, out var date)) statistics.AddCommit(line.Substring(dateEndIdx + 1), date); } diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs index 9a273703..025d035a 100644 --- a/src/Commands/Submodule.cs +++ b/src/Commands/Submodule.cs @@ -1,4 +1,5 @@ -using System; +using System.Collections.Generic; +using System.Text; namespace SourceGit.Commands { @@ -10,10 +11,9 @@ namespace SourceGit.Commands Context = repo; } - public bool Add(string url, string relativePath, bool recursive, Action outputHandler) + public bool Add(string url, string relativePath, bool recursive) { - _outputHandler = outputHandler; - Args = $"submodule add {url} \"{relativePath}\""; + Args = $"-c protocol.file.allow=always submodule add \"{url}\" \"{relativePath}\""; if (!Exec()) return false; @@ -29,38 +29,38 @@ namespace SourceGit.Commands } } - public bool Update(string module, bool init, bool recursive, bool useRemote, Action outputHandler) + public bool Update(List modules, bool init, bool recursive, bool useRemote = false) { - Args = "submodule update"; + var builder = new StringBuilder(); + builder.Append("submodule update"); if (init) - Args += " --init"; + builder.Append(" --init"); if (recursive) - Args += " --recursive"; + builder.Append(" --recursive"); if (useRemote) - Args += " --remote"; - if (!string.IsNullOrEmpty(module)) - Args += $" -- \"{module}\""; + builder.Append(" --remote"); + if (modules.Count > 0) + { + builder.Append(" --"); + foreach (var module in modules) + builder.Append($" \"{module}\""); + } - _outputHandler = outputHandler; + Args = builder.ToString(); return Exec(); } - public bool Delete(string relativePath) + public bool Deinit(string module, bool force) { - Args = $"submodule deinit -f \"{relativePath}\""; - if (!Exec()) - return false; - - Args = $"rm -rf \"{relativePath}\""; + Args = force ? $"submodule deinit -f -- \"{module}\"" : $"submodule deinit -- \"{module}\""; return Exec(); } - protected override void OnReadline(string line) + public bool Delete(string module) { - _outputHandler?.Invoke(line); + Args = $"rm -rf \"{module}\""; + return Exec(); } - - private Action _outputHandler; } } diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs index fa11e366..017afea0 100644 --- a/src/Commands/Tag.cs +++ b/src/Commands/Tag.cs @@ -1,59 +1,51 @@ -using System.Collections.Generic; -using System.IO; +using System.IO; namespace SourceGit.Commands { public static class Tag { - public static bool Add(string repo, string name, string basedOn) + public static bool Add(string repo, string name, string basedOn, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.Args = $"tag {name} {basedOn}"; + cmd.Args = $"tag --no-sign {name} {basedOn}"; + cmd.Log = log; return cmd.Exec(); } - public static bool Add(string repo, string name, string basedOn, string message, bool sign) + public static bool Add(string repo, string name, string basedOn, string message, bool sign, Models.ICommandLog log) { var param = sign ? "--sign -a" : "--no-sign -a"; var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"tag {param} {name} {basedOn} "; + cmd.Log = log; if (!string.IsNullOrEmpty(message)) { string tmp = Path.GetTempFileName(); File.WriteAllText(tmp, message); cmd.Args += $"-F \"{tmp}\""; - } - else - { - cmd.Args += $"-m {name}"; + + var succ = cmd.Exec(); + File.Delete(tmp); + return succ; } + cmd.Args += $"-m {name}"; return cmd.Exec(); } - public static bool Delete(string repo, string name, List remotes) + public static bool Delete(string repo, string name, Models.ICommandLog log) { var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; cmd.Args = $"tag --delete {name}"; - if (!cmd.Exec()) - return false; - - if (remotes != null) - { - foreach (var r in remotes) - { - new Push(repo, r.Name, name, true).Exec(); - } - } - - return true; + cmd.Log = log; + return cmd.Exec(); } } } diff --git a/src/Commands/UnstageChangesForAmend.cs b/src/Commands/UnstageChangesForAmend.cs index c930f136..19def067 100644 --- a/src/Commands/UnstageChangesForAmend.cs +++ b/src/Commands/UnstageChangesForAmend.cs @@ -23,13 +23,11 @@ namespace SourceGit.Commands _patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append("\t"); _patchBuilder.Append(c.OriginalPath); - _patchBuilder.Append("\n"); } else if (c.Index == Models.ChangeState.Added) { _patchBuilder.Append("0 0000000000000000000000000000000000000000\t"); _patchBuilder.Append(c.Path); - _patchBuilder.Append("\n"); } else if (c.Index == Models.ChangeState.Deleted) { @@ -37,7 +35,6 @@ namespace SourceGit.Commands _patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append("\t"); _patchBuilder.Append(c.Path); - _patchBuilder.Append("\n"); } else { @@ -46,8 +43,9 @@ namespace SourceGit.Commands _patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append("\t"); _patchBuilder.Append(c.Path); - _patchBuilder.Append("\n"); } + + _patchBuilder.Append("\n"); } } diff --git a/src/Commands/Version.cs b/src/Commands/Version.cs deleted file mode 100644 index ed7c6892..00000000 --- a/src/Commands/Version.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace SourceGit.Commands -{ - public class Version : Command - { - public Version() - { - Args = "--version"; - RaiseError = false; - } - - public string Query() - { - var rs = ReadToEnd(); - if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) - return string.Empty; - return rs.StdOut.Trim().Substring("git version ".Length); - } - } -} diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs index 7516b1e3..1198a443 100644 --- a/src/Commands/Worktree.cs +++ b/src/Commands/Worktree.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; namespace SourceGit.Commands { @@ -20,12 +21,13 @@ namespace SourceGit.Commands var last = null as Models.Worktree; if (rs.IsSuccess) { - var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (line.StartsWith("worktree ", StringComparison.Ordinal)) { last = new Models.Worktree() { FullPath = line.Substring(9).Trim() }; + last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath); worktrees.Add(last); } else if (line.StartsWith("bare", StringComparison.Ordinal)) @@ -54,7 +56,7 @@ namespace SourceGit.Commands return worktrees; } - public bool Add(string fullpath, string name, bool createNew, string tracking, Action outputHandler) + public bool Add(string fullpath, string name, bool createNew, string tracking) { Args = "worktree add "; @@ -73,15 +75,15 @@ namespace SourceGit.Commands if (!string.IsNullOrEmpty(tracking)) Args += tracking; + else if (!string.IsNullOrEmpty(name) && !createNew) + Args += name; - _outputHandler = outputHandler; return Exec(); } - public bool Prune(Action outputHandler) + public bool Prune() { Args = "worktree prune -v"; - _outputHandler = outputHandler; return Exec(); } @@ -97,22 +99,14 @@ namespace SourceGit.Commands return Exec(); } - public bool Remove(string fullpath, bool force, Action outputHandler) + public bool Remove(string fullpath, bool force) { if (force) Args = $"worktree remove -f \"{fullpath}\""; else Args = $"worktree remove \"{fullpath}\""; - _outputHandler = outputHandler; return Exec(); } - - protected override void OnReadline(string line) - { - _outputHandler?.Invoke(line); - } - - private Action _outputHandler = null; } } diff --git a/src/Converters/BoolConverters.cs b/src/Converters/BoolConverters.cs index 2d738700..3563fb37 100644 --- a/src/Converters/BoolConverters.cs +++ b/src/Converters/BoolConverters.cs @@ -1,4 +1,5 @@ using Avalonia.Data.Converters; +using Avalonia.Media; namespace SourceGit.Converters { @@ -6,5 +7,8 @@ namespace SourceGit.Converters { public static readonly FuncValueConverter ToPageTabWidth = new FuncValueConverter(x => x ? 200 : double.NaN); + + public static readonly FuncValueConverter IsBoldToFontWeight = + new FuncValueConverter(x => x ? FontWeight.Bold : FontWeight.Normal); } } diff --git a/src/Converters/FilterModeConverters.cs b/src/Converters/FilterModeConverters.cs new file mode 100644 index 00000000..c486af5e --- /dev/null +++ b/src/Converters/FilterModeConverters.cs @@ -0,0 +1,22 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace SourceGit.Converters +{ + public static class FilterModeConverters + { + public static readonly FuncValueConverter ToBorderBrush = + new FuncValueConverter(v => + { + switch (v) + { + case Models.FilterMode.Included: + return Brushes.Green; + case Models.FilterMode.Excluded: + return Brushes.Red; + default: + return Brushes.Transparent; + } + }); + } +} diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 17a88da2..f21c5d24 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -23,10 +23,10 @@ namespace SourceGit.Converters new FuncValueConverter(v => v != 1); public static readonly FuncValueConverter IsSubjectLengthBad = - new FuncValueConverter(v => v > ViewModels.Preference.Instance.SubjectGuideLength); + new FuncValueConverter(v => v > ViewModels.Preferences.Instance.SubjectGuideLength); public static readonly FuncValueConverter IsSubjectLengthGood = - new FuncValueConverter(v => v <= ViewModels.Preference.Instance.SubjectGuideLength); + new FuncValueConverter(v => v <= ViewModels.Preferences.Instance.SubjectGuideLength); public static readonly FuncValueConverter ToTreeMargin = new FuncValueConverter(v => new Thickness(v * 16, 0, 0, 0)); diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs index 81cac8b7..6f3ae98b 100644 --- a/src/Converters/ListConverters.cs +++ b/src/Converters/ListConverters.cs @@ -7,8 +7,11 @@ namespace SourceGit.Converters { public static class ListConverters { + public static readonly FuncValueConverter Count = + new FuncValueConverter(v => v == null ? "0" : $"{v.Count}"); + public static readonly FuncValueConverter ToCount = - new FuncValueConverter(v => v == null ? " (0)" : $" ({v.Count})"); + new FuncValueConverter(v => v == null ? "(0)" : $"({v.Count})"); public static readonly FuncValueConverter IsNullOrEmpty = new FuncValueConverter(v => v == null || v.Count == 0); diff --git a/src/Converters/ObjectConverters.cs b/src/Converters/ObjectConverters.cs new file mode 100644 index 00000000..f7c57764 --- /dev/null +++ b/src/Converters/ObjectConverters.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace SourceGit.Converters +{ + public static class ObjectConverters + { + public class IsTypeOfConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null || parameter == null) + return false; + + return value.GetType().IsAssignableTo((Type)parameter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return new NotImplementedException(); + } + } + + public static readonly IsTypeOfConverter IsTypeOf = new IsTypeOfConverter(); + } +} diff --git a/src/Converters/PathConverters.cs b/src/Converters/PathConverters.cs index dd7cfa49..ac1e61e5 100644 --- a/src/Converters/PathConverters.cs +++ b/src/Converters/PathConverters.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Avalonia.Data.Converters; @@ -22,7 +22,7 @@ namespace SourceGit.Converters var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; if (v.StartsWith(home, StringComparison.Ordinal)) - return "~" + v.Substring(prefixLen); + return $"~{v.AsSpan(prefixLen)}"; return v; }); diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index 585e0f02..bcadfae9 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -1,13 +1,12 @@ using System; using System.Globalization; -using System.Text.RegularExpressions; using Avalonia.Data.Converters; using Avalonia.Styling; namespace SourceGit.Converters { - public static partial class StringConverters + public static class StringConverters { public class ToLocaleConverter : IValueConverter { @@ -68,22 +67,6 @@ namespace SourceGit.Converters public static readonly FuncValueConverter ToShortSHA = new FuncValueConverter(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v)); - public static readonly FuncValueConverter UnderRecommendGitVersion = - new(v => - { - var match = REG_GIT_VERSION().Match(v ?? ""); - if (match.Success) - { - var major = int.Parse(match.Groups[1].Value); - var minor = int.Parse(match.Groups[2].Value); - var build = int.Parse(match.Groups[3].Value); - - return new Version(major, minor, build) < MINIMAL_GIT_VERSION; - } - - return true; - }); - public static readonly FuncValueConverter TrimRefsPrefix = new FuncValueConverter(v => { @@ -96,9 +79,10 @@ namespace SourceGit.Converters return v; }); - [GeneratedRegex(@"^[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")] - private static partial Regex REG_GIT_VERSION(); + public static readonly FuncValueConverter ContainsSpaces = + new FuncValueConverter(v => v != null && v.Contains(' ')); - private static readonly Version MINIMAL_GIT_VERSION = new Version(2, 23, 0); + public static readonly FuncValueConverter IsNotNullOrWhitespace = + new FuncValueConverter(v => v != null && v.Trim().Length > 0); } } diff --git a/src/Models/ApplyWhiteSpaceMode.cs b/src/Models/ApplyWhiteSpaceMode.cs index 6fbce0b2..aad45f57 100644 --- a/src/Models/ApplyWhiteSpaceMode.cs +++ b/src/Models/ApplyWhiteSpaceMode.cs @@ -2,14 +2,22 @@ { public class ApplyWhiteSpaceMode { + public static readonly ApplyWhiteSpaceMode[] Supported = + [ + new ApplyWhiteSpaceMode("No Warn", "Turns off the trailing whitespace warning", "nowarn"), + new ApplyWhiteSpaceMode("Warn", "Outputs warnings for a few such errors, but applies", "warn"), + new ApplyWhiteSpaceMode("Error", "Raise errors and refuses to apply the patch", "error"), + new ApplyWhiteSpaceMode("Error All", "Similar to 'error', but shows more", "error-all"), + ]; + public string Name { get; set; } public string Desc { get; set; } public string Arg { get; set; } public ApplyWhiteSpaceMode(string n, string d, string a) { - Name = App.Text(n); - Desc = App.Text(d); + Name = n; + Desc = d; Arg = a; } } diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index 313553f9..fa07975d 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -17,7 +17,7 @@ namespace SourceGit.Models { public interface IAvatarHost { - void OnAvatarResourceChanged(string email); + void OnAvatarResourceChanged(string email, Bitmap image); } public partial class AvatarManager @@ -35,14 +35,15 @@ namespace SourceGit.Models private static AvatarManager _instance = null; - [GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@users\.noreply\.github\.com$")] + [GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")] private static partial Regex REG_GITHUB_USER_EMAIL(); - private object _synclock = new object(); + private readonly Lock _synclock = new(); private string _storePath; private List _avatars = new List(); private Dictionary _resources = new Dictionary(); private HashSet _requesting = new HashSet(); + private HashSet _defaultAvatars = new HashSet(); public void Start() { @@ -50,8 +51,8 @@ namespace SourceGit.Models if (!Directory.Exists(_storePath)) Directory.CreateDirectory(_storePath); - var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/github.png", UriKind.RelativeOrAbsolute)); - _resources.Add("noreply@github.com", new Bitmap(icon)); + LoadDefaultAvatar("noreply@github.com", "github.png"); + LoadDefaultAvatar("unrealbot@epicgames.com", "unreal.png"); Task.Run(() => { @@ -118,7 +119,7 @@ namespace SourceGit.Models Dispatcher.UIThread.InvokeAsync(() => { _resources[email] = img; - NotifyResourceChanged(email); + NotifyResourceChanged(email, img); }); } @@ -140,17 +141,16 @@ namespace SourceGit.Models { if (forceRefetch) { - if (email.Equals("noreply@github.com", StringComparison.Ordinal)) + if (_defaultAvatars.Contains(email)) return null; - if (_resources.ContainsKey(email)) - _resources.Remove(email); + _resources.Remove(email); var localFile = Path.Combine(_storePath, GetEmailHash(email)); if (File.Exists(localFile)) File.Delete(localFile); - NotifyResourceChanged(email); + NotifyResourceChanged(email, null); } else { @@ -178,29 +178,61 @@ namespace SourceGit.Models lock (_synclock) { - if (!_requesting.Contains(email)) - _requesting.Add(email); + _requesting.Add(email); } return null; } + public void SetFromLocal(string email, string file) + { + try + { + Bitmap image = null; + + using (var stream = File.OpenRead(file)) + { + image = Bitmap.DecodeToWidth(stream, 128); + } + + if (image == null) + return; + + _resources[email] = image; + + _requesting.Remove(email); + + var store = Path.Combine(_storePath, GetEmailHash(email)); + File.Copy(file, store, true); + NotifyResourceChanged(email, image); + } + catch + { + // ignore + } + } + + private void LoadDefaultAvatar(string key, string img) + { + var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute)); + _resources.Add(key, new Bitmap(icon)); + _defaultAvatars.Add(key); + } + private string GetEmailHash(string email) { var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim(); - var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(lowered)); - var builder = new StringBuilder(); + var hash = MD5.HashData(Encoding.Default.GetBytes(lowered)); + var builder = new StringBuilder(hash.Length * 2); foreach (var c in hash) builder.Append(c.ToString("x2")); return builder.ToString(); } - private void NotifyResourceChanged(string email) + private void NotifyResourceChanged(string email, Bitmap image) { foreach (var avatar in _avatars) - { - avatar.OnAvatarResourceChanged(email); - } + avatar.OnAvatarResourceChanged(email, image); } } } diff --git a/src/Models/Bisect.cs b/src/Models/Bisect.cs new file mode 100644 index 00000000..2ed8beb2 --- /dev/null +++ b/src/Models/Bisect.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public enum BisectState + { + None = 0, + WaitingForRange, + Detecting, + } + + [Flags] + public enum BisectCommitFlag + { + None = 0, + Good = 1 << 0, + Bad = 1 << 1, + } + + public class Bisect + { + public HashSet Bads + { + get; + set; + } = []; + + public HashSet Goods + { + get; + set; + } = []; + } +} diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index ac6b8c67..7146da3f 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -7,6 +7,8 @@ namespace SourceGit.Models public List Ahead { get; set; } = new List(); public List Behind { get; set; } = new List(); + public bool IsVisible => Ahead.Count > 0 || Behind.Count > 0; + public override string ToString() { if (Ahead.Count == 0 && Behind.Count == 0) @@ -21,10 +23,17 @@ namespace SourceGit.Models } } + public enum BranchSortMode + { + Name = 0, + CommitterDate, + } + public class Branch { public string Name { get; set; } public string FullName { get; set; } + public ulong CommitterDate { get; set; } public string Head { get; set; } public bool IsLocal { get; set; } public bool IsCurrent { get; set; } @@ -32,6 +41,7 @@ namespace SourceGit.Models public string Upstream { get; set; } public BranchTrackStatus TrackStatus { get; set; } public string Remote { get; set; } + public bool IsUpstreamGone { get; set; } public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}"; } diff --git a/src/Models/Change.cs b/src/Models/Change.cs index 36fe20ac..129678be 100644 --- a/src/Models/Change.cs +++ b/src/Models/Change.cs @@ -18,14 +18,27 @@ namespace SourceGit.Models Deleted, Renamed, Copied, - Unmerged, - Untracked + Untracked, + Conflicted, + } + + public enum ConflictReason + { + None, + BothDeleted, + AddedByUs, + DeletedByThem, + AddedByThem, + DeletedByUs, + BothAdded, + BothModified, } public class ChangeDataForAmend { public string FileMode { get; set; } = ""; public string ObjectHash { get; set; } = ""; + public string ParentSHA { get; set; } = ""; } public class Change @@ -35,20 +48,14 @@ namespace SourceGit.Models public string Path { get; set; } = ""; public string OriginalPath { get; set; } = ""; public ChangeDataForAmend DataForAmend { get; set; } = null; + public ConflictReason ConflictReason { get; set; } = ConflictReason.None; - public bool IsConflit - { - get - { - if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged) - return true; - if (Index == ChangeState.Added && WorkTree == ChangeState.Added) - return true; - if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted) - return true; - return false; - } - } + public bool IsConflicted => WorkTree == ChangeState.Conflicted; + public string ConflictMarker => CONFLICT_MARKERS[(int)ConflictReason]; + public string ConflictDesc => CONFLICT_DESCS[(int)ConflictReason]; + + public string WorkTreeDesc => TYPE_DESCS[(int)WorkTree]; + public string IndexDesc => TYPE_DESCS[(int)Index]; public void Set(ChangeState index, ChangeState workTree = ChangeState.None) { @@ -76,8 +83,44 @@ namespace SourceGit.Models if (Path[0] == '"') Path = Path.Substring(1, Path.Length - 2); + if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"') OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2); } + + private static readonly string[] TYPE_DESCS = + [ + "Unknown", + "Modified", + "Type Changed", + "Added", + "Deleted", + "Renamed", + "Copied", + "Untracked", + "Conflict" + ]; + private static readonly string[] CONFLICT_MARKERS = + [ + string.Empty, + "DD", + "AU", + "UD", + "UA", + "DU", + "AA", + "UU" + ]; + private static readonly string[] CONFLICT_DESCS = + [ + string.Empty, + "Both deleted", + "Added by us", + "Deleted by them", + "Added by them", + "Deleted by us", + "Both added", + "Both modified" + ]; } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 534cf5bb..f0f4b39b 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -8,13 +8,19 @@ namespace SourceGit.Models { public enum CommitSearchMethod { - ByUser, + BySHA = 0, + ByAuthor, + ByCommitter, ByMessage, ByFile, + ByContent, } public class Commit { + // As retrieved by: git mktree Parents { get; set; } = new List(); - public List Decorators { get; set; } = new List(); + public List Parents { get; set; } = new(); + public List Decorators { get; set; } = new(); public bool HasDecorators => Decorators.Count > 0; - public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); - public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); - public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd"); + public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); + public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); + public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly); + public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly); public bool IsMerged { get; set; } = false; public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; @@ -42,7 +49,7 @@ namespace SourceGit.Models public int Color { get; set; } = 0; public double Opacity => IsMerged ? 1 : OpacityForNotMerged; public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular; - public Thickness Margin { get; set; } = new Thickness(0); + public Thickness Margin { get; set; } = new(0); public IBrush Brush => CommitGraph.Pens[Color].Brush; public void ParseDecorators(string data) @@ -106,14 +113,14 @@ namespace SourceGit.Models if (l.Type != r.Type) return (int)l.Type - (int)r.Type; else - return string.Compare(l.Name, r.Name, StringComparison.Ordinal); + return NumericSort.Compare(l.Name, r.Name); }); } } - public class CommitWithMessage + public class CommitFullMessage { - public Commit Commit { get; set; } = new Commit(); - public string Message { get; set; } = ""; + public string Message { get; set; } = string.Empty; + public InlineElementCollector Inlines { get; set; } = new(); } } diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 31f5a40e..01488656 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -25,10 +25,11 @@ namespace SourceGit.Models s_penCount = colors.Count; } - public class Path(int color) + public class Path(int color, bool isMerged) { public List Points { get; } = []; public int Color { get; } = color; + public bool IsMerged { get; } = isMerged; } public class Link @@ -37,6 +38,7 @@ namespace SourceGit.Models public Point Control; public Point End; public int Color; + public bool IsMerged; } public enum DotType @@ -51,6 +53,7 @@ namespace SourceGit.Models public DotType Type; public Point Center; public int Color; + public bool IsMerged; } public List Paths { get; } = []; @@ -61,14 +64,14 @@ namespace SourceGit.Models { const double unitWidth = 12; const double halfWidth = 6; - const double unitHeight = 28; - const double halfHeight = 14; + const double unitHeight = 1; + const double halfHeight = 0.5; var temp = new CommitGraph(); var unsolved = new List(); var ended = new List(); var offsetY = -halfHeight; - var colorIdx = 0; + var colorPicker = new ColorPicker(); foreach (var commit in commits) { @@ -108,7 +111,6 @@ namespace SourceGit.Models } isMerged = isMerged || l.IsMerged; - major.IsMerged = isMerged; } else { @@ -119,28 +121,35 @@ namespace SourceGit.Models // Remove ended curves from unsolved foreach (var l in ended) + { + colorPicker.Recycle(l.Path.Color); unsolved.Remove(l); + } ended.Clear(); - // Create new curve for branch head + // If no path found, create new curve for branch head + // Otherwise, create new curve for new merged commit if (major == null) { offsetX += unitWidth; if (commit.Parents.Count > 0) { - major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY)); + major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY)); unsolved.Add(major); temp.Paths.Add(major.Path); } - - colorIdx = (colorIdx + 1) % s_penCount; + } + else if (isMerged && !major.IsMerged && commit.Parents.Count > 0) + { + major.ReplaceMerged(); + temp.Paths.Add(major.Path); } // Calculate link position of this commit. var position = new Point(major?.LastX ?? offsetX, offsetY); var dotColor = major?.Path.Color ?? 0; - var anchor = new Dot() { Center = position, Color = dotColor }; + var anchor = new Dot() { Center = position, Color = dotColor, IsMerged = isMerged }; if (commit.IsCurrentHead) anchor.Type = DotType.Head; else if (commit.Parents.Count > 1) @@ -158,16 +167,20 @@ namespace SourceGit.Models var parent = unsolved.Find(x => x.Next.Equals(parentHash, StringComparison.Ordinal)); if (parent != null) { - // Try to change the merge state of linked graph - if (isMerged) - parent.IsMerged = true; + if (isMerged && !parent.IsMerged) + { + parent.Goto(parent.LastX, offsetY + halfHeight, halfHeight); + parent.ReplaceMerged(); + temp.Paths.Add(parent.Path); + } temp.Links.Add(new Link { Start = position, End = new Point(parent.LastX, offsetY + halfHeight), Control = new Point(parent.LastX, position.Y), - Color = parent.Path.Color + Color = parent.Path.Color, + IsMerged = isMerged, }); } else @@ -175,10 +188,9 @@ namespace SourceGit.Models offsetX += unitWidth; // Create new curve for parent commit that not includes before - var l = new PathHelper(parentHash, isMerged, colorIdx, position, new Point(offsetX, position.Y + halfHeight)); + var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight)); unsolved.Add(l); temp.Paths.Add(l.Path); - colorIdx = (colorIdx + 1) % s_penCount; } } } @@ -205,32 +217,53 @@ namespace SourceGit.Models return temp; } + private class ColorPicker + { + public int Next() + { + if (_colorsQueue.Count == 0) + { + for (var i = 0; i < s_penCount; i++) + _colorsQueue.Enqueue(i); + } + + return _colorsQueue.Dequeue(); + } + + public void Recycle(int idx) + { + if (!_colorsQueue.Contains(idx)) + _colorsQueue.Enqueue(idx); + } + + private Queue _colorsQueue = new Queue(); + } + private class PathHelper { - public Path Path { get; } + public Path Path { get; private set; } public string Next { get; set; } - public bool IsMerged { get; set; } public double LastX { get; private set; } + public bool IsMerged => Path.IsMerged; + public PathHelper(string next, bool isMerged, int color, Point start) { Next = next; - IsMerged = isMerged; LastX = start.X; _lastY = start.Y; - Path = new Path(color); + Path = new Path(color, isMerged); Path.Points.Add(start); } public PathHelper(string next, bool isMerged, int color, Point start, Point to) { Next = next; - IsMerged = isMerged; LastX = to.X; _lastY = to.Y; - Path = new Path(color); + Path = new Path(color, isMerged); Path.Points.Add(start); Path.Points.Add(to); } @@ -310,6 +343,19 @@ namespace SourceGit.Models _lastY = y; } + /// + /// End the current path and create a new from the end. + /// + public void ReplaceMerged() + { + var color = Path.Color; + Add(LastX, _lastY); + + Path = new Path(color, true); + Path.Points.Add(new Point(LastX, _lastY)); + _endY = 0; + } + private void Add(double x, double y) { if (_endY < y) @@ -327,7 +373,6 @@ namespace SourceGit.Models private static readonly List s_defaultPenColors = [ Colors.Orange, Colors.ForestGreen, - Colors.Gray, Colors.Turquoise, Colors.Olive, Colors.Magenta, diff --git a/src/Models/CommitLink.cs b/src/Models/CommitLink.cs index 955779a8..2891e5d6 100644 --- a/src/Models/CommitLink.cs +++ b/src/Models/CommitLink.cs @@ -1,8 +1,49 @@ -namespace SourceGit.Models +using System; +using System.Collections.Generic; + +namespace SourceGit.Models { public class CommitLink { public string Name { get; set; } = null; public string URLPrefix { get; set; } = null; + + public CommitLink(string name, string prefix) + { + Name = name; + URLPrefix = prefix; + } + + public static List Get(List remotes) + { + var outs = new List(); + + foreach (var remote in remotes) + { + if (remote.TryGetVisitURL(out var url)) + { + var trimmedUrl = url.AsSpan(); + if (url.EndsWith(".git")) + trimmedUrl = url.AsSpan(0, url.Length - 4); + + if (url.StartsWith("https://github.com/", StringComparison.Ordinal)) + outs.Add(new($"Github ({trimmedUrl.Slice(19)})", $"{url}/commit/")); + else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal)) + outs.Add(new($"GitLab ({trimmedUrl.Slice(trimmedUrl.Slice(15).IndexOf('/') + 16)})", $"{url}/-/commit/")); + else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal)) + outs.Add(new($"Gitee ({trimmedUrl.Slice(18)})", $"{url}/commit/")); + else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal)) + outs.Add(new($"BitBucket ({trimmedUrl.Slice(22)})", $"{url}/commits/")); + else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal)) + outs.Add(new($"Codeberg ({trimmedUrl.Slice(21)})", $"{url}/commit/")); + else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal)) + outs.Add(new($"Gitea ({trimmedUrl.Slice(18)})", $"{url}/commit/")); + else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal)) + outs.Add(new($"sourcehut ({trimmedUrl.Slice(18)})", $"{url}/commit/")); + } + } + + return outs; + } } } diff --git a/src/Models/CommitSignInfo.cs b/src/Models/CommitSignInfo.cs new file mode 100644 index 00000000..44b95e61 --- /dev/null +++ b/src/Models/CommitSignInfo.cs @@ -0,0 +1,60 @@ +using Avalonia.Media; + +namespace SourceGit.Models +{ + public class CommitSignInfo + { + public char VerifyResult { get; init; } = 'N'; + public string Signer { get; init; } = string.Empty; + public string Key { get; init; } = string.Empty; + public bool HasSigner => !string.IsNullOrEmpty(Signer); + + public IBrush Brush + { + get + { + switch (VerifyResult) + { + case 'G': + case 'U': + return Brushes.Green; + case 'X': + case 'Y': + case 'R': + return Brushes.DarkOrange; + case 'B': + case 'E': + return Brushes.Red; + default: + return Brushes.Transparent; + } + } + } + + public string ToolTip + { + get + { + switch (VerifyResult) + { + case 'G': + return "Good signature."; + case 'U': + return "Good signature with unknown validity."; + case 'X': + return "Good signature but has expired."; + case 'Y': + return "Good signature made by expired key."; + case 'R': + return "Good signature made by a revoked key."; + case 'B': + return "Bad signature."; + case 'E': + return "Signature cannot be checked."; + default: + return "No signature."; + } + } + } + } +} diff --git a/src/Models/CommitTemplate.cs b/src/Models/CommitTemplate.cs index 135d8ac9..3f331543 100644 --- a/src/Models/CommitTemplate.cs +++ b/src/Models/CommitTemplate.cs @@ -1,17 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; +using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.Models { - public partial class CommitTemplate : ObservableObject + public class CommitTemplate : ObservableObject { - [GeneratedRegex(@"\$\{files(\:\d+)?\}")] - private static partial Regex REG_COMMIT_TEMPLATE_FILES(); - public string Name { get => _name; @@ -24,54 +18,10 @@ namespace SourceGit.Models set => SetProperty(ref _content, value); } - public string Apply(List changes) + public string Apply(Branch branch, List changes) { - var content = _content.Replace("${files_num}", $"{changes.Count}"); - var matches = REG_COMMIT_TEMPLATE_FILES().Matches(content); - if (matches.Count == 0) - return content; - - var builder = new StringBuilder(); - var last = 0; - for (int i = 0; i < matches.Count; i++) - { - var match = matches[i]; - if (!match.Success) - continue; - - var start = match.Index; - if (start != last) - builder.Append(content.Substring(last, start - last)); - - var countStr = match.Groups[1].Value; - var paths = new List(); - var more = string.Empty; - if (countStr is { Length: <= 1 }) - { - foreach (var c in changes) - paths.Add(c.Path); - } - else - { - var count = Math.Min(int.Parse(countStr.Substring(1)), changes.Count); - for (int j = 0; j < count; j++) - paths.Add(changes[j].Path); - - if (count < changes.Count) - more = $" and {changes.Count - count} other files"; - } - - builder.Append(string.Join(", ", paths)); - if (!string.IsNullOrEmpty(more)) - builder.Append(more); - - last = start + match.Length; - } - - if (last != content.Length - 1) - builder.Append(content.Substring(last)); - - return builder.ToString(); + var te = new TemplateEngine(); + return te.Eval(_content, branch, changes); } private string _name = string.Empty; diff --git a/src/Models/ConventionalCommitType.cs b/src/Models/ConventionalCommitType.cs index 4fb61d87..531a16c0 100644 --- a/src/Models/ConventionalCommitType.cs +++ b/src/Models/ConventionalCommitType.cs @@ -4,23 +4,28 @@ namespace SourceGit.Models { public class ConventionalCommitType { - public string Type { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; + public string Name { get; set; } + public string Type { get; set; } + public string Description { get; set; } - public static readonly List Supported = new List() - { - new ConventionalCommitType("feat", "Adding a new feature"), - new ConventionalCommitType("fix", "Fixing a bug"), - new ConventionalCommitType("docs", "Updating documentation"), - new ConventionalCommitType("style", "Elements or code styles without changing the code logic"), - new ConventionalCommitType("test", "Adding or updating tests"), - new ConventionalCommitType("chore", "Making changes to the build process or auxiliary tools and libraries"), - new ConventionalCommitType("revert", "Undoing a previous commit"), - new ConventionalCommitType("refactor", "Restructuring code without changing its external behavior") - }; + public static readonly List Supported = [ + new("Features", "feat", "Adding a new feature"), + new("Bug Fixes", "fix", "Fixing a bug"), + new("Work In Progress", "wip", "Still being developed and not yet complete"), + new("Reverts", "revert", "Undoing a previous commit"), + new("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"), + new("Performance Improvements", "perf", "Improves performance"), + new("Builds", "build", "Changes that affect the build system or external dependencies"), + new("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"), + new("Documentations", "docs", "Updating documentation"), + new("Styles", "style", "Elements or code styles without changing the code logic"), + new("Tests", "test", "Adding or updating tests"), + new("Chores", "chore", "Other changes that don't modify src or test files"), + ]; - public ConventionalCommitType(string type, string description) + public ConventionalCommitType(string name, string type, string description) { + Name = name; Type = type; Description = description; } diff --git a/src/Models/Count.cs b/src/Models/Count.cs new file mode 100644 index 00000000..d48b0c08 --- /dev/null +++ b/src/Models/Count.cs @@ -0,0 +1,19 @@ +using System; + +namespace SourceGit.Models +{ + public class Count : IDisposable + { + public int Value { get; set; } = 0; + + public Count(int value) + { + Value = value; + } + + public void Dispose() + { + // Ignore + } + } +} diff --git a/src/Models/CustomAction.cs b/src/Models/CustomAction.cs new file mode 100644 index 00000000..a614961a --- /dev/null +++ b/src/Models/CustomAction.cs @@ -0,0 +1,50 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models +{ + public enum CustomActionScope + { + Repository, + Commit, + Branch, + } + + public class CustomAction : ObservableObject + { + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + + public CustomActionScope Scope + { + get => _scope; + set => SetProperty(ref _scope, value); + } + + public string Executable + { + get => _executable; + set => SetProperty(ref _executable, value); + } + + public string Arguments + { + get => _arguments; + set => SetProperty(ref _arguments, value); + } + + public bool WaitForExit + { + get => _waitForExit; + set => SetProperty(ref _waitForExit, value); + } + + private string _name = string.Empty; + private CustomActionScope _scope = CustomActionScope.Repository; + private string _executable = string.Empty; + private string _arguments = string.Empty; + private bool _waitForExit = true; + } +} diff --git a/src/Models/DateTimeFormat.cs b/src/Models/DateTimeFormat.cs new file mode 100644 index 00000000..16276c40 --- /dev/null +++ b/src/Models/DateTimeFormat.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public class DateTimeFormat + { + public string DateOnly { get; set; } + public string DateTime { get; set; } + + public string Example + { + get => _example.ToString(DateTime); + } + + public DateTimeFormat(string dateOnly, string dateTime) + { + DateOnly = dateOnly; + DateTime = dateTime; + } + + public static int ActiveIndex + { + get; + set; + } = 0; + + public static DateTimeFormat Active + { + get => Supported[ActiveIndex]; + } + + public static readonly List Supported = new List + { + new DateTimeFormat("yyyy/MM/dd", "yyyy/MM/dd, HH:mm:ss"), + new DateTimeFormat("yyyy.MM.dd", "yyyy.MM.dd, HH:mm:ss"), + new DateTimeFormat("yyyy-MM-dd", "yyyy-MM-dd, HH:mm:ss"), + new DateTimeFormat("MM/dd/yyyy", "MM/dd/yyyy, HH:mm:ss"), + new DateTimeFormat("MM.dd.yyyy", "MM.dd.yyyy, HH:mm:ss"), + new DateTimeFormat("MM-dd-yyyy", "MM-dd-yyyy, HH:mm:ss"), + new DateTimeFormat("dd/MM/yyyy", "dd/MM/yyyy, HH:mm:ss"), + new DateTimeFormat("dd.MM.yyyy", "dd.MM.yyyy, HH:mm:ss"), + new DateTimeFormat("dd-MM-yyyy", "dd-MM-yyyy, HH:mm:ss"), + new DateTimeFormat("MMM d yyyy", "MMM d yyyy, HH:mm:ss"), + new DateTimeFormat("d MMM yyyy", "d MMM yyyy, HH:mm:ss"), + }; + + private static readonly DateTime _example = new DateTime(2025, 1, 31, 8, 0, 0, DateTimeKind.Local); + } +} diff --git a/src/Models/DealWithLocalChanges.cs b/src/Models/DealWithLocalChanges.cs deleted file mode 100644 index f308a90c..00000000 --- a/src/Models/DealWithLocalChanges.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SourceGit.Models -{ - public enum DealWithLocalChanges - { - DoNothing, - StashAndReaply, - Discard, - } -} diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs index 98387e7f..69f93980 100644 --- a/src/Models/DiffOption.cs +++ b/src/Models/DiffOption.cs @@ -5,6 +5,15 @@ namespace SourceGit.Models { public class DiffOption { + /// + /// Enable `--ignore-cr-at-eol` by default? + /// + public static bool IgnoreCRAtEOL + { + get; + set; + } = true; + public Change WorkingCopyChange => _workingCopyChange; public bool IsUnstaged => _isUnstaged; public List Revisions => _revisions; @@ -40,7 +49,7 @@ namespace SourceGit.Models else { if (change.DataForAmend != null) - _extra = "--cached HEAD^"; + _extra = $"--cached {change.DataForAmend.ParentSHA}"; else _extra = "--cached"; @@ -56,7 +65,7 @@ namespace SourceGit.Models /// public DiffOption(Commit commit, Change change) { - var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^"; + var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; _revisions.Add(baseRevision); _revisions.Add(commit.SHA); _path = change.Path; @@ -70,7 +79,7 @@ namespace SourceGit.Models /// public DiffOption(Commit commit, string file) { - var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^"; + var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; _revisions.Add(baseRevision); _revisions.Add(commit.SHA); _path = file; @@ -115,6 +124,6 @@ namespace SourceGit.Models private readonly string _path; private readonly string _orgPath = string.Empty; private readonly string _extra = string.Empty; - private readonly List _revisions = new List(); + private readonly List _revisions = []; } } diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index e7cecaa3..b2d91310 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; @@ -30,6 +30,7 @@ namespace SourceGit.Models public int OldLineNumber { get; set; } = 0; public int NewLineNumber { get; set; } = 0; public List Highlights { get; set; } = new List(); + public bool NoNewLineEndOfFile { get; set; } = false; public string OldLine => OldLineNumber == 0 ? string.Empty : OldLineNumber.ToString(); public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString(); @@ -63,7 +64,7 @@ namespace SourceGit.Models { public string File { get; set; } = string.Empty; public List Lines { get; set; } = new List(); - public Vector SyncScrollOffset { get; set; } = Vector.Zero; + public Vector ScrollOffset { get; set; } = Vector.Zero; public int MaxLineNumber = 0; public string Repo { get; set; } = null; @@ -146,7 +147,7 @@ namespace SourceGit.Models public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) { var isTracked = !string.IsNullOrEmpty(fileBlobGuid); - var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000"; + var fileGuid = isTracked ? fileBlobGuid : "00000000"; var builder = new StringBuilder(); builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n'); @@ -674,11 +675,25 @@ namespace SourceGit.Models { public bool IsBinary { get; set; } = false; public bool IsLFS { get; set; } = false; + public string OldHash { get; set; } = string.Empty; + public string NewHash { get; set; } = string.Empty; public string OldMode { get; set; } = string.Empty; public string NewMode { get; set; } = string.Empty; public TextDiff TextDiff { get; set; } = null; public LFSDiff LFSDiff { get; set; } = null; - public string FileModeChange => string.IsNullOrEmpty(OldMode) ? string.Empty : $"{OldMode} → {NewMode}"; + public string FileModeChange + { + get + { + if (string.IsNullOrEmpty(OldMode) && string.IsNullOrEmpty(NewMode)) + return string.Empty; + + var oldDisplay = string.IsNullOrEmpty(OldMode) ? "0" : OldMode; + var newDisplay = string.IsNullOrEmpty(NewMode) ? "0" : NewMode; + + return $"{oldDisplay} → {newDisplay}"; + } + } } } diff --git a/src/Models/DirtyState.cs b/src/Models/DirtyState.cs new file mode 100644 index 00000000..2b9d898d --- /dev/null +++ b/src/Models/DirtyState.cs @@ -0,0 +1,12 @@ +using System; + +namespace SourceGit.Models +{ + [Flags] + public enum DirtyState + { + None = 0, + HasLocalChanges = 1 << 0, + HasPendingPullOrPush = 1 << 1, + } +} diff --git a/src/Models/ExternalMerger.cs b/src/Models/ExternalMerger.cs index 9855f9d3..fe67ad6a 100644 --- a/src/Models/ExternalMerger.cs +++ b/src/Models/ExternalMerger.cs @@ -39,9 +39,11 @@ namespace SourceGit.Models new ExternalMerger(4, "tortoise_merge", "Tortoise Merge", "TortoiseMerge.exe;TortoiseGitMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", "-base:\"$LOCAL\" -theirs:\"$REMOTE\""), new ExternalMerger(5, "kdiff3", "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMerger(6, "beyond_compare", "Beyond Compare", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "-u -e \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e -sw \"$LOCAL\" \"$REMOTE\""), new ExternalMerger(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger(10, "plastic_merge", "Plastic SCM", "mergetool.exe", "-s=\"$REMOTE\" -b=\"$BASE\" -d=\"$LOCAL\" -r=\"$MERGED\" --automatic", "-s=\"$LOCAL\" -d=\"$REMOTE\""), + new ExternalMerger(11, "meld", "Meld", "Meld.exe", "\"$LOCAL\" \"$BASE\" \"$REMOTE\" --output \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), }; } else if (OperatingSystem.IsMacOS()) @@ -65,7 +67,7 @@ namespace SourceGit.Models new ExternalMerger(2, "vscode_insiders", "Visual Studio Code - Insiders", "/usr/share/code-insiders/code-insiders", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), new ExternalMerger(3, "kdiff3", "KDiff3", "/usr/bin/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMerger(4, "beyond_compare", "Beyond Compare", "/usr/bin/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(5, "meld", "Meld", "/usr/bin/meld", "\"$LOCAL\" \"$BASE\" \"$REMOTE\" -output \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger(5, "meld", "Meld", "/usr/bin/meld", "\"$LOCAL\" \"$BASE\" \"$REMOTE\" --output \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), new ExternalMerger(6, "codium", "VSCodium", "/usr/share/codium/bin/codium", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), new ExternalMerger(7, "p4merge", "P4Merge", "/usr/local/bin/p4merge", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""), }; diff --git a/src/Models/ExternalTool.cs b/src/Models/ExternalTool.cs index b26a9a90..103e91bc 100644 --- a/src/Models/ExternalTool.cs +++ b/src/Models/ExternalTool.cs @@ -13,15 +13,13 @@ namespace SourceGit.Models public class ExternalTool { public string Name { get; private set; } - public string Executable { get; private set; } - public string OpenCmdArgs { get; private set; } public Bitmap IconImage { get; private set; } = null; - public ExternalTool(string name, string icon, string executable, string openCmdArgs) + public ExternalTool(string name, string icon, string execFile, Func execArgsGenerator = null) { Name = name; - Executable = executable; - OpenCmdArgs = openCmdArgs; + _execFile = execFile; + _execArgsGenerator = execArgsGenerator ?? (repo => $"\"{repo}\""); try { @@ -40,11 +38,14 @@ namespace SourceGit.Models Process.Start(new ProcessStartInfo() { WorkingDirectory = repo, - FileName = Executable, - Arguments = string.Format(OpenCmdArgs, repo), + FileName = _execFile, + Arguments = _execArgsGenerator.Invoke(repo), UseShellExecute = false, }); } + + private string _execFile = string.Empty; + private Func _execArgsGenerator = null; } public class JetBrainsState @@ -110,48 +111,48 @@ namespace SourceGit.Models _customPaths = new ExternalToolPaths(); } - public void TryAdd(string name, string icon, string args, string key, Func finder) + public void TryAdd(string name, string icon, Func finder, Func execArgsGenerator = null) { - if (_customPaths.Tools.TryGetValue(key, out var customPath) && File.Exists(customPath)) + if (_customPaths.Tools.TryGetValue(name, out var customPath) && File.Exists(customPath)) { - Founded.Add(new ExternalTool(name, icon, customPath, args)); + Founded.Add(new ExternalTool(name, icon, customPath, execArgsGenerator)); } else { var path = finder(); if (!string.IsNullOrEmpty(path) && File.Exists(path)) - Founded.Add(new ExternalTool(name, icon, path, args)); + Founded.Add(new ExternalTool(name, icon, path, execArgsGenerator)); } } public void VSCode(Func platformFinder) { - TryAdd("Visual Studio Code", "vscode", "\"{0}\"", "VSCODE", platformFinder); + TryAdd("Visual Studio Code", "vscode", platformFinder); } public void VSCodeInsiders(Func platformFinder) { - TryAdd("Visual Studio Code - Insiders", "vscode_insiders", "\"{0}\"", "VSCODE_INSIDERS", platformFinder); + TryAdd("Visual Studio Code - Insiders", "vscode_insiders", platformFinder); } public void VSCodium(Func platformFinder) { - TryAdd("VSCodium", "codium", "\"{0}\"", "VSCODIUM", platformFinder); + TryAdd("VSCodium", "codium", platformFinder); } public void Fleet(Func platformFinder) { - TryAdd("Fleet", "fleet", "\"{0}\"", "FLEET", platformFinder); + TryAdd("Fleet", "fleet", platformFinder); } public void SublimeText(Func platformFinder) { - TryAdd("Sublime Text", "sublime_text", "\"{0}\"", "SUBLIME_TEXT", platformFinder); + TryAdd("Sublime Text", "sublime_text", platformFinder); } public void Zed(Func platformFinder) { - TryAdd("Zed", "zed", "\"{0}\"", "ZED", platformFinder); + TryAdd("Zed", "zed", platformFinder); } public void FindJetBrainsFromToolbox(Func platformFinder) @@ -170,8 +171,7 @@ namespace SourceGit.Models Founded.Add(new ExternalTool( $"{tool.DisplayName} {tool.DisplayVersion}", supported_icons.Contains(tool.ProductCode) ? $"JetBrains/{tool.ProductCode}" : "JetBrains/JB", - Path.Combine(tool.InstallLocation, tool.LaunchCommand), - "\"{0}\"")); + Path.Combine(tool.InstallLocation, tool.LaunchCommand))); } } } diff --git a/src/Models/Filter.cs b/src/Models/Filter.cs new file mode 100644 index 00000000..af4569fa --- /dev/null +++ b/src/Models/Filter.cs @@ -0,0 +1,60 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models +{ + public enum FilterType + { + LocalBranch = 0, + LocalBranchFolder, + RemoteBranch, + RemoteBranchFolder, + Tag, + } + + public enum FilterMode + { + None = 0, + Included, + Excluded, + } + + public class Filter : ObservableObject + { + public string Pattern + { + get => _pattern; + set => SetProperty(ref _pattern, value); + } + + public FilterType Type + { + get; + set; + } = FilterType.LocalBranch; + + public FilterMode Mode + { + get => _mode; + set => SetProperty(ref _mode, value); + } + + public bool IsBranch + { + get => Type != FilterType.Tag; + } + + public Filter() + { + } + + public Filter(string pattern, FilterType type, FilterMode mode) + { + _pattern = pattern; + _mode = mode; + Type = type; + } + + private string _pattern = string.Empty; + private FilterMode _mode = FilterMode.None; + } +} diff --git a/src/Models/GitFlow.cs b/src/Models/GitFlow.cs new file mode 100644 index 00000000..5d26072b --- /dev/null +++ b/src/Models/GitFlow.cs @@ -0,0 +1,46 @@ +namespace SourceGit.Models +{ + public enum GitFlowBranchType + { + None = 0, + Feature, + Release, + Hotfix, + } + + public class GitFlow + { + public string Master { get; set; } = string.Empty; + public string Develop { get; set; } = string.Empty; + public string FeaturePrefix { get; set; } = string.Empty; + public string ReleasePrefix { get; set; } = string.Empty; + public string HotfixPrefix { get; set; } = string.Empty; + + public bool IsValid + { + get + { + return !string.IsNullOrEmpty(Master) && + !string.IsNullOrEmpty(Develop) && + !string.IsNullOrEmpty(FeaturePrefix) && + !string.IsNullOrEmpty(ReleasePrefix) && + !string.IsNullOrEmpty(HotfixPrefix); + } + } + + public string GetPrefix(GitFlowBranchType type) + { + switch (type) + { + case GitFlowBranchType.Feature: + return FeaturePrefix; + case GitFlowBranchType.Release: + return ReleasePrefix; + case GitFlowBranchType.Hotfix: + return HotfixPrefix; + default: + return string.Empty; + } + } + } +} diff --git a/src/Models/GitVersions.cs b/src/Models/GitVersions.cs new file mode 100644 index 00000000..8aae63a3 --- /dev/null +++ b/src/Models/GitVersions.cs @@ -0,0 +1,20 @@ +namespace SourceGit.Models +{ + public static class GitVersions + { + /// + /// The minimal version of Git that required by this app. + /// + public static readonly System.Version MINIMAL = new(2, 25, 1); + + /// + /// The minimal version of Git that supports the `stash push` command with the `--pathspec-from-file` option. + /// + public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new(2, 26, 0); + + /// + /// The minimal version of Git that supports the `stash push` command with the `--staged` option. + /// + public static readonly System.Version STASH_PUSH_ONLY_STAGED = new(2, 35, 0); + } +} diff --git a/src/Models/Hyperlink.cs b/src/Models/Hyperlink.cs deleted file mode 100644 index 81dc980e..00000000 --- a/src/Models/Hyperlink.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace SourceGit.Models -{ - public class Hyperlink - { - public int Start { get; set; } = 0; - public int Length { get; set; } = 0; - public string Link { get; set; } = ""; - public bool IsCommitSHA { get; set; } = false; - - public Hyperlink(int start, int length, string link, bool isCommitSHA = false) - { - Start = start; - Length = length; - Link = link; - IsCommitSHA = isCommitSHA; - } - - public bool Intersect(int start, int length) - { - if (start == Start) - return true; - - if (start < Start) - return start + length > Start; - - return start < Start + Length; - } - } -} diff --git a/src/Models/ICommandLog.cs b/src/Models/ICommandLog.cs new file mode 100644 index 00000000..34ec7031 --- /dev/null +++ b/src/Models/ICommandLog.cs @@ -0,0 +1,7 @@ +namespace SourceGit.Models +{ + public interface ICommandLog + { + void AppendLine(string line); + } +} diff --git a/src/Models/IRepository.cs b/src/Models/IRepository.cs index 12b1adba..2fc7c612 100644 --- a/src/Models/IRepository.cs +++ b/src/Models/IRepository.cs @@ -2,8 +2,7 @@ { public interface IRepository { - string FullPath { get; set; } - string GitDir { get; set; } + bool MayHaveSubmodules(); void RefreshBranches(); void RefreshWorktrees(); diff --git a/src/Models/ImageDecoder.cs b/src/Models/ImageDecoder.cs new file mode 100644 index 00000000..6fe0f428 --- /dev/null +++ b/src/Models/ImageDecoder.cs @@ -0,0 +1,10 @@ +namespace SourceGit.Models +{ + public enum ImageDecoder + { + None = 0, + Builtin, + Pfim, + Tiff, + } +} diff --git a/src/Models/InlineElement.cs b/src/Models/InlineElement.cs new file mode 100644 index 00000000..ea7bcee8 --- /dev/null +++ b/src/Models/InlineElement.cs @@ -0,0 +1,37 @@ +namespace SourceGit.Models +{ + public enum InlineElementType + { + Keyword = 0, + Link, + CommitSHA, + Code, + } + + public class InlineElement + { + public InlineElementType Type { get; } + public int Start { get; } + public int Length { get; } + public string Link { get; } + + public InlineElement(InlineElementType type, int start, int length, string link) + { + Type = type; + Start = start; + Length = length; + Link = link; + } + + public bool IsIntersecting(int start, int length) + { + if (start == Start) + return true; + + if (start < Start) + return start + length > Start; + + return start < Start + Length; + } + } +} diff --git a/src/Models/InlineElementCollector.cs b/src/Models/InlineElementCollector.cs new file mode 100644 index 00000000..d81aaf8d --- /dev/null +++ b/src/Models/InlineElementCollector.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public class InlineElementCollector + { + public int Count => _implementation.Count; + public InlineElement this[int index] => _implementation[index]; + + public InlineElement Intersect(int start, int length) + { + foreach (var elem in _implementation) + { + if (elem.IsIntersecting(start, length)) + return elem; + } + + return null; + } + + public void Add(InlineElement element) + { + _implementation.Add(element); + } + + public void Sort() + { + _implementation.Sort((l, r) => l.Start.CompareTo(r.Start)); + } + + public void Clear() + { + _implementation.Clear(); + } + + private readonly List _implementation = []; + } +} diff --git a/src/Models/InteractiveRebase.cs b/src/Models/InteractiveRebase.cs index 0980587a..d1710d4a 100644 --- a/src/Models/InteractiveRebase.cs +++ b/src/Models/InteractiveRebase.cs @@ -12,6 +12,12 @@ namespace SourceGit.Models Drop, } + public class InteractiveCommit + { + public Commit Commit { get; set; } = new Commit(); + public string Message { get; set; } = string.Empty; + } + public class InteractiveRebaseJob { public string SHA { get; set; } = string.Empty; @@ -21,6 +27,8 @@ namespace SourceGit.Models public class InteractiveRebaseJobCollection { + public string OrigHead { get; set; } = string.Empty; + public string Onto { get; set; } = string.Empty; public List Jobs { get; set; } = new List(); } } diff --git a/src/Models/IpcChannel.cs b/src/Models/IpcChannel.cs new file mode 100644 index 00000000..c2a6c6c7 --- /dev/null +++ b/src/Models/IpcChannel.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +namespace SourceGit.Models +{ + public class IpcChannel : IDisposable + { + public bool IsFirstInstance + { + get => _isFirstInstance; + } + + public event Action MessageReceived; + + public IpcChannel() + { + try + { + _singletonLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + _isFirstInstance = true; + _server = new NamedPipeServerStream( + "SourceGitIPCChannel" + Environment.UserName, + PipeDirection.In, + -1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + _cancellationTokenSource = new CancellationTokenSource(); + Task.Run(StartServer); + } + catch + { + _isFirstInstance = false; + } + } + + public void SendToFirstInstance(string cmd) + { + try + { + using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) + { + client.Connect(1000); + if (!client.IsConnected) + return; + + using (var writer = new StreamWriter(client)) + { + writer.WriteLine(cmd); + writer.Flush(); + } + + if (OperatingSystem.IsWindows()) + client.WaitForPipeDrain(); + else + Thread.Sleep(1000); + } + } + catch + { + // IGNORE + } + } + + public void Dispose() + { + _cancellationTokenSource?.Cancel(); + _singletonLock?.Dispose(); + } + + private async void StartServer() + { + using var reader = new StreamReader(_server); + + while (!_cancellationTokenSource.IsCancellationRequested) + { + try + { + await _server.WaitForConnectionAsync(_cancellationTokenSource.Token); + + if (!_cancellationTokenSource.IsCancellationRequested) + { + var line = await reader.ReadToEndAsync(_cancellationTokenSource.Token); + MessageReceived?.Invoke(line?.Trim()); + } + + _server.Disconnect(); + } + catch + { + if (!_cancellationTokenSource.IsCancellationRequested && _server.IsConnected) + _server.Disconnect(); + } + } + } + + private FileStream _singletonLock = null; + private bool _isFirstInstance = false; + private NamedPipeServerStream _server = null; + private CancellationTokenSource _cancellationTokenSource = null; + } +} diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs index 29487a16..40c84b9e 100644 --- a/src/Models/IssueTrackerRule.cs +++ b/src/Models/IssueTrackerRule.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using CommunityToolkit.Mvvm.ComponentModel; @@ -46,7 +45,7 @@ namespace SourceGit.Models set => SetProperty(ref _urlTemplate, value); } - public void Matches(List outs, string message) + public void Matches(InlineElementCollector outs, string message) { if (_regex == null || string.IsNullOrEmpty(_urlTemplate)) return; @@ -60,17 +59,7 @@ namespace SourceGit.Models var start = match.Index; var len = match.Length; - var intersect = false; - foreach (var exist in outs) - { - if (exist.Intersect(start, len)) - { - intersect = true; - break; - } - } - - if (intersect) + if (outs.Intersect(start, len) != null) continue; var link = _urlTemplate; @@ -81,8 +70,7 @@ namespace SourceGit.Models link = link.Replace($"${j}", group.Value); } - var range = new Hyperlink(start, len, link); - outs.Add(range); + outs.Add(new InlineElement(InlineElementType.Link, start, len, link)); } } diff --git a/src/Models/LFSObject.cs b/src/Models/LFSObject.cs index 0f281253..8bc2dda2 100644 --- a/src/Models/LFSObject.cs +++ b/src/Models/LFSObject.cs @@ -1,8 +1,22 @@ -namespace SourceGit.Models +using System.Text.RegularExpressions; + +namespace SourceGit.Models { - public class LFSObject + public partial class LFSObject { + [GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")] + private static partial Regex REG_FORMAT(); + public string Oid { get; set; } = string.Empty; public long Size { get; set; } = 0; + + public static LFSObject Parse(string content) + { + var match = REG_FORMAT().Match(content); + if (match.Success) + return new() { Oid = match.Groups[1].Value, Size = long.Parse(match.Groups[2].Value) }; + + return null; + } } } diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs index 0d9e5f69..1788a9b2 100644 --- a/src/Models/Locales.cs +++ b/src/Models/Locales.cs @@ -8,13 +8,18 @@ namespace SourceGit.Models public string Key { get; set; } public static readonly List Supported = new List() { - new Locale("English", "en_US"), new Locale("Deutsch", "de_DE"), + new Locale("English", "en_US"), + new Locale("Español", "es_ES"), new Locale("Français", "fr_FR"), + new Locale("Italiano", "it_IT"), new Locale("Português (Brasil)", "pt_BR"), + new Locale("Українська", "uk_UA"), new Locale("Русский", "ru_RU"), new Locale("简体中文", "zh_CN"), new Locale("繁體中文", "zh_TW"), + new Locale("日本語", "ja_JP"), + new Locale("தமிழ் (Tamil)", "ta_IN"), }; public Locale(string name, string key) diff --git a/src/Models/MergeMode.cs b/src/Models/MergeMode.cs index 15e3f7e9..5dc70030 100644 --- a/src/Models/MergeMode.cs +++ b/src/Models/MergeMode.cs @@ -5,8 +5,9 @@ public static readonly MergeMode[] Supported = [ new MergeMode("Default", "Fast-forward if possible", ""), + new MergeMode("Fast-forward", "Refuse to merge when fast-forward is not possible", "--ff-only"), new MergeMode("No Fast-forward", "Always create a merge commit", "--no-ff"), - new MergeMode("Squash", "Use '--squash'", "--squash"), + new MergeMode("Squash", "Squash merge", "--squash"), new MergeMode("Don't commit", "Merge without commit", "--no-ff --no-commit"), ]; diff --git a/src/Models/MergeStrategy.cs b/src/Models/MergeStrategy.cs new file mode 100644 index 00000000..ab1d446b --- /dev/null +++ b/src/Models/MergeStrategy.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public class MergeStrategy + { + public string Name { get; internal set; } + public string Desc { get; internal set; } + public string Arg { get; internal set; } + + public static List ForMultiple { get; private set; } = [ + new MergeStrategy("Default", "Let Git automatically select a strategy", string.Empty), + new MergeStrategy("Octopus", "Attempt merging multiple heads", "octopus"), + new MergeStrategy("Ours", "Record the merge without modifying the tree", "ours"), + ]; + + public MergeStrategy(string n, string d, string a) + { + Name = n; + Desc = d; + Arg = a; + } + } +} diff --git a/src/Models/Notification.cs b/src/Models/Notification.cs index 2261e327..473947b0 100644 --- a/src/Models/Notification.cs +++ b/src/Models/Notification.cs @@ -4,10 +4,5 @@ { public bool IsError { get; set; } = false; public string Message { get; set; } = string.Empty; - - public void CopyMessage() - { - App.CopyText(Message); - } } } diff --git a/src/Models/NumericSort.cs b/src/Models/NumericSort.cs new file mode 100644 index 00000000..baaf3da4 --- /dev/null +++ b/src/Models/NumericSort.cs @@ -0,0 +1,52 @@ +using System; + +namespace SourceGit.Models +{ + public static class NumericSort + { + public static int Compare(string s1, string s2) + { + int len1 = s1.Length; + int len2 = s2.Length; + + int marker1 = 0; + int marker2 = 0; + + while (marker1 < len1 && marker2 < len2) + { + char c1 = s1[marker1]; + char c2 = s2[marker2]; + + bool isDigit1 = char.IsDigit(c1); + bool isDigit2 = char.IsDigit(c2); + if (isDigit1 != isDigit2) + return c1.CompareTo(c2); + + int subLen1 = 1; + while (marker1 + subLen1 < len1 && char.IsDigit(s1[marker1 + subLen1]) == isDigit1) + subLen1++; + + int subLen2 = 1; + while (marker2 + subLen2 < len2 && char.IsDigit(s2[marker2 + subLen2]) == isDigit2) + subLen2++; + + string sub1 = s1.Substring(marker1, subLen1); + string sub2 = s2.Substring(marker2, subLen2); + + marker1 += subLen1; + marker2 += subLen2; + + int result; + if (isDigit1) + result = (subLen1 == subLen2) ? string.CompareOrdinal(sub1, sub2) : (subLen1 - subLen2); + else + result = string.Compare(sub1, sub2, StringComparison.OrdinalIgnoreCase); + + if (result != 0) + return result; + } + + return len1 - len2; + } + } +} diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs index 5ab0c7ee..22fbcd51 100644 --- a/src/Models/OpenAI.cs +++ b/src/Models/OpenAI.cs @@ -1,136 +1,235 @@ using System; +using System.ClientModel; using System.Collections.Generic; -using System.Net.Http; using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using System.Threading; +using Azure.AI.OpenAI; +using CommunityToolkit.Mvvm.ComponentModel; +using OpenAI; +using OpenAI.Chat; namespace SourceGit.Models { - public class OpenAIChatMessage + public partial class OpenAIResponse { - [JsonPropertyName("role")] - public string Role + public OpenAIResponse(Action onUpdate) { - get; - set; + _onUpdate = onUpdate; } - [JsonPropertyName("content")] - public string Content + public void Append(string text) { - get; - set; + var buffer = text; + + if (_thinkTail.Length > 0) + { + _thinkTail.Append(buffer); + buffer = _thinkTail.ToString(); + _thinkTail.Clear(); + } + + buffer = REG_COT().Replace(buffer, ""); + + var startIdx = buffer.IndexOf('<', StringComparison.Ordinal); + if (startIdx >= 0) + { + if (startIdx > 0) + OnReceive(buffer.Substring(0, startIdx)); + + var endIdx = buffer.IndexOf(">", startIdx + 1, StringComparison.Ordinal); + if (endIdx <= startIdx) + { + if (buffer.Length - startIdx <= 15) + _thinkTail.Append(buffer.Substring(startIdx)); + else + OnReceive(buffer.Substring(startIdx)); + } + else if (endIdx < startIdx + 15) + { + var tag = buffer.Substring(startIdx + 1, endIdx - startIdx - 1); + if (_thinkTags.Contains(tag)) + _thinkTail.Append(buffer.Substring(startIdx)); + else + OnReceive(buffer.Substring(startIdx)); + } + else + { + OnReceive(buffer.Substring(startIdx)); + } + } + else + { + OnReceive(buffer); + } } + + public void End() + { + if (_thinkTail.Length > 0) + { + OnReceive(_thinkTail.ToString()); + _thinkTail.Clear(); + } + } + + private void OnReceive(string text) + { + if (!_hasTrimmedStart) + { + text = text.TrimStart(); + if (string.IsNullOrEmpty(text)) + return; + + _hasTrimmedStart = true; + } + + _onUpdate.Invoke(text); + } + + [GeneratedRegex(@"<(think|thought|thinking|thought_chain)>.*?", RegexOptions.Singleline)] + private static partial Regex REG_COT(); + + private Action _onUpdate = null; + private StringBuilder _thinkTail = new StringBuilder(); + private HashSet _thinkTags = ["think", "thought", "thinking", "thought_chain"]; + private bool _hasTrimmedStart = false; } - public class OpenAIChatChoice + public class OpenAIService : ObservableObject { - [JsonPropertyName("index")] - public int Index + public string Name { - get; - set; + get => _name; + set => SetProperty(ref _name, value); } - [JsonPropertyName("message")] - public OpenAIChatMessage Message + public string Server { - get; - set; + get => _server; + set => SetProperty(ref _server, value); } - } - public class OpenAIChatResponse - { - [JsonPropertyName("choices")] - public List Choices + public string ApiKey { - get; - set; - } = []; - } + get => _apiKey; + set => SetProperty(ref _apiKey, value); + } - public class OpenAIChatRequest - { - [JsonPropertyName("model")] public string Model { - get; - set; + get => _model; + set => SetProperty(ref _model, value); } - [JsonPropertyName("messages")] - public List Messages + public bool Streaming { - get; - set; - } = []; - - public void AddMessage(string role, string content) - { - Messages.Add(new OpenAIChatMessage { Role = role, Content = content }); - } - } - - public static class OpenAI - { - public static string Server - { - get; - set; + get => _streaming; + set => SetProperty(ref _streaming, value); } - public static string ApiKey + public string AnalyzeDiffPrompt { - get; - set; + get => _analyzeDiffPrompt; + set => SetProperty(ref _analyzeDiffPrompt, value); } - public static string Model + public string GenerateSubjectPrompt { - get; - set; + get => _generateSubjectPrompt; + set => SetProperty(ref _generateSubjectPrompt, value); } - public static bool IsValid + public OpenAIService() { - get => !string.IsNullOrEmpty(Server) && !string.IsNullOrEmpty(Model); + AnalyzeDiffPrompt = """ + You are an expert developer specialist in creating commits. + Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules: + - Do not use any code snippets, imports, file routes or bullets points. + - Do not mention the route of file that has been change. + - Write clear, concise, and descriptive messages that explain the MAIN GOAL made of the changes. + - Use the present tense and active voice in the message, for example, "Fix bug" instead of "Fixed bug.". + - Use the imperative mood, which gives the message a sense of command, e.g. "Add feature" instead of "Added feature". + - Avoid using general terms like "update" or "change", be specific about what was updated or changed. + - Avoid using terms like "The main goal of", just output directly the summary in plain text + """; + + GenerateSubjectPrompt = """ + You are an expert developer specialist in creating commits messages. + Your only goal is to retrieve a single commit message. + Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules: + - Assign the commit {type} according to the next conditions: + feat: Only when adding a new feature. + fix: When fixing a bug. + docs: When updating documentation. + style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic. + test: When adding or updating tests. + chore: When making changes to the build process or auxiliary tools and libraries. + revert: When undoing a previous commit. + refactor: When restructuring code without changing its external behavior, or is any of the other refactor types. + - Do not add any issues numeration, explain your output nor introduce your answer. + - Output directly only one commit message in plain text with the next format: {type}: {commit_message}. + - Be as concise as possible, keep the message under 50 characters. + """; } - public static OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation) + public void Chat(string prompt, string question, CancellationToken cancellation, Action onUpdate) { - var chat = new OpenAIChatRequest() { Model = Model }; - chat.AddMessage("system", prompt); - chat.AddMessage("user", question); + var server = new Uri(_server); + var key = new ApiKeyCredential(_apiKey); + var client = null as ChatClient; + if (_server.Contains("openai.azure.com/", StringComparison.Ordinal)) + { + var azure = new AzureOpenAIClient(server, key); + client = azure.GetChatClient(_model); + } + else + { + var openai = new OpenAIClient(key, new() { Endpoint = server }); + client = openai.GetChatClient(_model); + } - var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) }; - if (!string.IsNullOrEmpty(ApiKey)) - client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}"); + var messages = new List(); + messages.Add(_model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt)); + messages.Add(new UserChatMessage(question)); - var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json"); try { - var task = client.PostAsync(Server, req, cancellation); - task.Wait(); + var rsp = new OpenAIResponse(onUpdate); - var rsp = task.Result; - if (!rsp.IsSuccessStatusCode) - throw new Exception($"AI service returns error code {rsp.StatusCode}"); + if (_streaming) + { + var updates = client.CompleteChatStreaming(messages, null, cancellation); - var reader = rsp.Content.ReadAsStringAsync(cancellation); - reader.Wait(); + foreach (var update in updates) + { + if (update.ContentUpdate.Count > 0) + rsp.Append(update.ContentUpdate[0].Text); + } + } + else + { + var completion = client.CompleteChat(messages, null, cancellation); - return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse); + if (completion.Value.Content.Count > 0) + rsp.Append(completion.Value.Content[0].Text); + } + + rsp.End(); } catch { - if (cancellation.IsCancellationRequested) - return null; - - throw; + if (!cancellation.IsCancellationRequested) + throw; } } + + private string _name; + private string _server; + private string _apiKey; + private string _model; + private bool _streaming = true; + private string _analyzeDiffPrompt; + private string _generateSubjectPrompt; } } diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index dcf30ddc..6e36cfb9 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -1,22 +1,26 @@ using System; +using System.IO; using System.Text.RegularExpressions; namespace SourceGit.Models { public partial class Remote { - [GeneratedRegex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/~%]+/[\w\-\.%]+(\.git)?$")] + [GeneratedRegex(@"^https?://[^/]+/.+[^/\.]$")] private static partial Regex REG_HTTPS(); - [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/~%]+/[\w\-\.%]+(\.git)?$")] + [GeneratedRegex(@"^git://[^/]+/.+[^/\.]$")] + private static partial Regex REG_GIT(); + [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")] private static partial Regex REG_SSH1(); - [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/~]+/[\w\-\.]+(\.git)?$")] + [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/([a-zA-z0-9~%][\w\-\./~%]*)?[a-zA-Z0-9](\.git)?$")] private static partial Regex REG_SSH2(); - [GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~]+/[\w\-\.]+)\.git$")] + [GeneratedRegex(@"^git@([\w\.\-]+):([\w\-/~%]+/[\w\-\.%]+)\.git$")] private static partial Regex REG_TO_VISIT_URL_CAPTURE(); private static readonly Regex[] URL_FORMATS = [ REG_HTTPS(), + REG_GIT(), REG_SSH1(), REG_SSH2(), ]; @@ -29,13 +33,10 @@ namespace SourceGit.Models if (string.IsNullOrWhiteSpace(url)) return false; - for (int i = 1; i < URL_FORMATS.Length; i++) - { - if (URL_FORMATS[i].IsMatch(url)) - return true; - } + if (REG_SSH1().IsMatch(url)) + return true; - return false; + return REG_SSH2().IsMatch(url); } public static bool IsValidURL(string url) @@ -49,7 +50,10 @@ namespace SourceGit.Models return true; } - return false; + return url.StartsWith("file://", StringComparison.Ordinal) || + url.StartsWith("./", StringComparison.Ordinal) || + url.StartsWith("../", StringComparison.Ordinal) || + Directory.Exists(url); } public bool TryGetVisitURL(out string url) diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index f2fd1bc6..a54956d3 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -1,4 +1,8 @@ -using Avalonia.Collections; +using System; +using System.Collections.Generic; +using System.Text; + +using Avalonia.Collections; namespace SourceGit.Models { @@ -10,11 +14,59 @@ namespace SourceGit.Models set; } = string.Empty; - public DealWithLocalChanges DealWithLocalChangesOnCheckoutBranch + public bool EnableReflog { get; set; - } = DealWithLocalChanges.DoNothing; + } = false; + + public bool EnableFirstParentInHistories + { + get; + set; + } = false; + + public bool EnableTopoOrderInHistories + { + get; + set; + } = false; + + public bool OnlyHighlightCurrentBranchInHistories + { + get; + set; + } = false; + + public BranchSortMode LocalBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + + public BranchSortMode RemoteBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + + public TagSortMode TagSortMode + { + get; + set; + } = TagSortMode.CreatorDate; + + public bool IncludeUntrackedInLocalChanges + { + get; + set; + } = true; + + public bool EnableForceOnFetch + { + get; + set; + } = false; public bool FetchWithoutTags { @@ -22,30 +74,12 @@ namespace SourceGit.Models set; } = false; - public DealWithLocalChanges DealWithLocalChangesOnPull - { - get; - set; - } = DealWithLocalChanges.DoNothing; - public bool PreferRebaseInsteadOfMerge { get; set; } = true; - public bool FetchWithoutTagsOnPull - { - get; - set; - } = false; - - public bool FetchAllBranchesOnPull - { - get; - set; - } = true; - public bool CheckSubmodulesOnPush { get; @@ -58,11 +92,17 @@ namespace SourceGit.Models set; } = false; - public DealWithLocalChanges DealWithLocalChangesOnCreateBranch + public bool PushToRemoteWhenCreateTag { get; set; - } = DealWithLocalChanges.DoNothing; + } = true; + + public bool PushToRemoteWhenDeleteTag + { + get; + set; + } = false; public bool CheckoutBranchOnCreateBranch { @@ -70,35 +110,41 @@ namespace SourceGit.Models set; } = true; - public bool AutoStageBeforeCommit + public bool UpdateSubmodulesOnCheckoutBranch { get; set; - } = false; + } = true; - public AvaloniaList Filters + public AvaloniaList HistoriesFilters { get; set; - } = new AvaloniaList(); + } = []; public AvaloniaList CommitTemplates { get; set; - } = new AvaloniaList(); + } = []; public AvaloniaList CommitMessages { get; set; - } = new AvaloniaList(); + } = []; public AvaloniaList IssueTrackerRules { get; set; - } = new AvaloniaList(); + } = []; + + public AvaloniaList CustomActions + { + get; + set; + } = []; public bool EnableAutoFetch { @@ -112,8 +158,253 @@ namespace SourceGit.Models set; } = 10; + public bool EnableSignOffForCommit + { + get; + set; + } = false; + + public bool IncludeUntrackedWhenStash + { + get; + set; + } = true; + + public bool OnlyStagedWhenStash + { + get; + set; + } = false; + + public bool KeepIndexWhenStash + { + get; + set; + } = false; + + public bool AutoRestoreAfterStash + { + get; + set; + } = false; + + public string PreferredOpenAIService + { + get; + set; + } = "---"; + + public bool IsLocalBranchesExpandedInSideBar + { + get; + set; + } = true; + + public bool IsRemotesExpandedInSideBar + { + get; + set; + } = false; + + public bool IsTagsExpandedInSideBar + { + get; + set; + } = false; + + public bool IsSubmodulesExpandedInSideBar + { + get; + set; + } = false; + + public bool IsWorktreeExpandedInSideBar + { + get; + set; + } = false; + + public List ExpandedBranchNodesInSideBar + { + get; + set; + } = []; + + public int PreferredMergeMode + { + get; + set; + } = 0; + + public string LastCommitMessage + { + get; + set; + } = string.Empty; + + public Dictionary CollectHistoriesFilters() + { + var map = new Dictionary(); + foreach (var filter in HistoriesFilters) + map.Add(filter.Pattern, filter.Mode); + return map; + } + + public bool UpdateHistoriesFilter(string pattern, FilterType type, FilterMode mode) + { + // Clear all filters when there's a filter that has different mode. + if (mode != FilterMode.None) + { + var clear = false; + foreach (var filter in HistoriesFilters) + { + if (filter.Mode != mode) + { + clear = true; + break; + } + } + + if (clear) + { + HistoriesFilters.Clear(); + HistoriesFilters.Add(new Filter(pattern, type, mode)); + return true; + } + } + else + { + for (int i = 0; i < HistoriesFilters.Count; i++) + { + var filter = HistoriesFilters[i]; + if (filter.Type == type && filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + { + HistoriesFilters.RemoveAt(i); + return true; + } + } + + return false; + } + + foreach (var filter in HistoriesFilters) + { + if (filter.Type != type) + continue; + + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + return false; + } + + HistoriesFilters.Add(new Filter(pattern, type, mode)); + return true; + } + + public void RemoveChildrenBranchFilters(string pattern) + { + var dirty = new List(); + var prefix = $"{pattern}/"; + + foreach (var filter in HistoriesFilters) + { + if (filter.Type == FilterType.Tag) + continue; + + if (filter.Pattern.StartsWith(prefix, StringComparison.Ordinal)) + dirty.Add(filter); + } + + foreach (var filter in dirty) + HistoriesFilters.Remove(filter); + } + + public string BuildHistoriesFilter() + { + var includedRefs = new List(); + var excludedBranches = new List(); + var excludedRemotes = new List(); + var excludedTags = new List(); + foreach (var filter in HistoriesFilters) + { + if (filter.Type == FilterType.LocalBranch) + { + if (filter.Mode == FilterMode.Included) + includedRefs.Add(filter.Pattern); + else if (filter.Mode == FilterMode.Excluded) + excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); + } + else if (filter.Type == FilterType.LocalBranchFolder) + { + if (filter.Mode == FilterMode.Included) + includedRefs.Add($"--branches={filter.Pattern.AsSpan(11)}/*"); + else if (filter.Mode == FilterMode.Excluded) + excludedBranches.Add($"--exclude=\"{filter.Pattern.AsSpan(11)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); + } + else if (filter.Type == FilterType.RemoteBranch) + { + if (filter.Mode == FilterMode.Included) + includedRefs.Add(filter.Pattern); + else if (filter.Mode == FilterMode.Excluded) + excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}\" --decorate-refs-exclude=\"{filter.Pattern}\""); + } + else if (filter.Type == FilterType.RemoteBranchFolder) + { + if (filter.Mode == FilterMode.Included) + includedRefs.Add($"--remotes={filter.Pattern.AsSpan(13)}/*"); + else if (filter.Mode == FilterMode.Excluded) + excludedRemotes.Add($"--exclude=\"{filter.Pattern.AsSpan(13)}/*\" --decorate-refs-exclude=\"{filter.Pattern}/*\""); + } + else if (filter.Type == FilterType.Tag) + { + if (filter.Mode == FilterMode.Included) + includedRefs.Add($"refs/tags/{filter.Pattern}"); + else if (filter.Mode == FilterMode.Excluded) + excludedTags.Add($"--exclude=\"{filter.Pattern}\" --decorate-refs-exclude=\"refs/tags/{filter.Pattern}\""); + } + } + + var builder = new StringBuilder(); + if (includedRefs.Count > 0) + { + foreach (var r in includedRefs) + { + builder.Append(r); + builder.Append(' '); + } + } + else if (excludedBranches.Count + excludedRemotes.Count + excludedTags.Count > 0) + { + foreach (var b in excludedBranches) + { + builder.Append(b); + builder.Append(' '); + } + + builder.Append("--exclude=HEAD --branches "); + + foreach (var r in excludedRemotes) + { + builder.Append(r); + builder.Append(' '); + } + + builder.Append("--exclude=origin/HEAD --remotes "); + + foreach (var t in excludedTags) + { + builder.Append(t); + builder.Append(' '); + } + + builder.Append("--tags "); + } + + return builder.ToString(); + } + public void PushCommitMessage(string message) { + message = message.Trim().ReplaceLineEndings("\n"); var existIdx = CommitMessages.IndexOf(message); if (existIdx == 0) return; @@ -130,39 +421,13 @@ namespace SourceGit.Models CommitMessages.Insert(0, message); } - public IssueTrackerRule AddNewIssueTracker() + public IssueTrackerRule AddIssueTracker(string name, string regex, string url) { var rule = new IssueTrackerRule() { - Name = "New Issue Tracker", - RegexString = "#(\\d+)", - URLTemplate = "https://xxx/$1", - }; - - IssueTrackerRules.Add(rule); - return rule; - } - - public IssueTrackerRule AddGithubIssueTracker(string repoURL) - { - var rule = new IssueTrackerRule() - { - Name = "Github ISSUE", - RegexString = "#(\\d+)", - URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://github.com/username/repository/issues/$1" : $"{repoURL}/issues/$1", - }; - - IssueTrackerRules.Add(rule); - return rule; - } - - public IssueTrackerRule AddJiraIssueTracker() - { - var rule = new IssueTrackerRule() - { - Name = "Jira Tracker", - RegexString = "PROJ-(\\d+)", - URLTemplate = "https://jira.yourcompany.com/browse/PROJ-$1", + Name = name, + RegexString = regex, + URLTemplate = url, }; IssueTrackerRules.Add(rule); @@ -174,5 +439,32 @@ namespace SourceGit.Models if (rule != null) IssueTrackerRules.Remove(rule); } + + public CustomAction AddNewCustomAction() + { + var act = new CustomAction() { Name = "Unnamed Action" }; + CustomActions.Add(act); + return act; + } + + public void RemoveCustomAction(CustomAction act) + { + if (act != null) + CustomActions.Remove(act); + } + + public void MoveCustomActionUp(CustomAction act) + { + var idx = CustomActions.IndexOf(act); + if (idx > 0) + CustomActions.Move(idx - 1, idx); + } + + public void MoveCustomActionDown(CustomAction act) + { + var idx = CustomActions.IndexOf(act); + if (idx < CustomActions.Count - 1) + CustomActions.Move(idx + 1, idx); + } } } diff --git a/src/Models/ResetMode.cs b/src/Models/ResetMode.cs index 735533c2..827ccaa9 100644 --- a/src/Models/ResetMode.cs +++ b/src/Models/ResetMode.cs @@ -6,23 +6,25 @@ namespace SourceGit.Models { public static readonly ResetMode[] Supported = [ - new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", Brushes.Green), - new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", Brushes.Orange), - new ResetMode("Merge", "Reset while keeping unmerged changes", "--merge", Brushes.Purple), - new ResetMode("Keep", "Reset while keeping local modifications", "--keep", Brushes.Purple), - new ResetMode("Hard", "Discard all changes", "--hard", Brushes.Red), + new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", "S", Brushes.Green), + new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", "M", Brushes.Orange), + new ResetMode("Merge", "Reset while keeping unmerged changes", "--merge", "G", Brushes.Purple), + new ResetMode("Keep", "Reset while keeping local modifications", "--keep", "K", Brushes.Purple), + new ResetMode("Hard", "Discard all changes", "--hard", "H", Brushes.Red), ]; public string Name { get; set; } public string Desc { get; set; } public string Arg { get; set; } + public string Key { get; set; } public IBrush Color { get; set; } - public ResetMode(string n, string d, string a, IBrush b) + public ResetMode(string n, string d, string a, string k, IBrush b) { Name = n; Desc = d; Arg = a; + Key = k; Color = b; } } diff --git a/src/Models/RevisionFile.cs b/src/Models/RevisionFile.cs index f1f5265f..29a23efa 100644 --- a/src/Models/RevisionFile.cs +++ b/src/Models/RevisionFile.cs @@ -1,4 +1,6 @@ -using Avalonia.Media.Imaging; +using System.Globalization; +using System.IO; +using Avalonia.Media.Imaging; namespace SourceGit.Models { @@ -9,10 +11,17 @@ namespace SourceGit.Models public class RevisionImageFile { - public Bitmap Image { get; set; } = null; - public long FileSize { get; set; } = 0; - public string ImageType { get; set; } = string.Empty; + public Bitmap Image { get; } + public long FileSize { get; } + public string ImageType { get; } public string ImageSize => Image != null ? $"{Image.PixelSize.Width} x {Image.PixelSize.Height}" : "0 x 0"; + + public RevisionImageFile(string file, Bitmap img, long size) + { + Image = img; + FileSize = size; + ImageType = Path.GetExtension(file)!.Substring(1).ToUpper(CultureInfo.CurrentCulture); + } } public class RevisionTextFile @@ -29,6 +38,6 @@ namespace SourceGit.Models public class RevisionSubmodule { public Commit Commit { get; set; } = null; - public string FullMessage { get; set; } = string.Empty; + public CommitFullMessage FullMessage { get; set; } = null; } } diff --git a/src/Models/Version.cs b/src/Models/SelfUpdate.cs similarity index 65% rename from src/Models/Version.cs rename to src/Models/SelfUpdate.cs index 35c21778..e02f80d8 100644 --- a/src/Models/Version.cs +++ b/src/Models/SelfUpdate.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System; +using System.Reflection; using System.Text.Json.Serialization; namespace SourceGit.Models @@ -32,5 +33,24 @@ namespace SourceGit.Models } } - public class AlreadyUpToDate { } + public class AlreadyUpToDate + { + } + + public class SelfUpdateFailed + { + public string Reason + { + get; + private set; + } + + public SelfUpdateFailed(Exception e) + { + if (e.InnerException is { } inner) + Reason = inner.Message; + else + Reason = e.Message; + } + } } diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs index 8ab25788..7dfb2237 100644 --- a/src/Models/ShellOrTerminal.cs +++ b/src/Models/ShellOrTerminal.cs @@ -41,6 +41,9 @@ namespace SourceGit.Models { new ShellOrTerminal("mac-terminal", "Terminal", ""), new ShellOrTerminal("iterm2", "iTerm", ""), + new ShellOrTerminal("warp", "Warp", ""), + new ShellOrTerminal("ghostty", "Ghostty", ""), + new ShellOrTerminal("kitty", "kitty", "") }; } else @@ -55,6 +58,8 @@ namespace SourceGit.Models new ShellOrTerminal("mate-terminal", "MATE Terminal", "mate-terminal"), new ShellOrTerminal("foot", "Foot", "foot"), new ShellOrTerminal("wezterm", "WezTerm", "wezterm"), + new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis"), + new ShellOrTerminal("kitty", "kitty", "kitty"), new ShellOrTerminal("custom", "Custom", ""), }; } diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs index 06da763a..369ab145 100644 --- a/src/Models/Stash.cs +++ b/src/Models/Stash.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace SourceGit.Models { @@ -6,9 +7,10 @@ namespace SourceGit.Models { public string Name { get; set; } = ""; public string SHA { get; set; } = ""; + public List Parents { get; set; } = []; public ulong Time { get; set; } = 0; public string Message { get; set; } = ""; - public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); + public string TimeStr => DateTime.UnixEpoch.AddSeconds(Time).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); } } diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs index b669eb55..a86380c3 100644 --- a/src/Models/Statistics.cs +++ b/src/Models/Statistics.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using LiveChartsCore; using LiveChartsCore.Defaults; @@ -10,54 +11,47 @@ using SkiaSharp; namespace SourceGit.Models { - public enum StaticsticsMode + public enum StatisticsMode { All, ThisMonth, ThisWeek, } - public class StaticsticsAuthor(string name, int count) + public class StatisticsAuthor(User user, int count) { - public string Name { get; set; } = name; - public int Count { get; set; } = count; - } - - public class StaticsticsSample(DateTime time, int count) - { - public DateTime Time { get; set; } = time; + public User User { get; set; } = user; public int Count { get; set; } = count; } public class StatisticsReport { - public static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; - public int Total { get; set; } = 0; - public List Authors { get; set; } = new List(); - public List Series { get; set; } = new List(); - public List XAxes { get; set; } = new List(); - public List YAxes { get; set; } = new List(); + public List Authors { get; set; } = new(); + public List Series { get; set; } = new(); + public List XAxes { get; set; } = new(); + public List YAxes { get; set; } = new(); + public StatisticsAuthor SelectedAuthor { get => _selectedAuthor; set => ChangeAuthor(value); } - public StatisticsReport(StaticsticsMode mode, DateTime start) + public StatisticsReport(StatisticsMode mode, DateTime start) { _mode = mode; - YAxes = [new Axis() + YAxes.Add(new Axis() { TextSize = 10, MinLimit = 0, SeparatorsPaint = new SolidColorPaint(new SKColor(0x40808080)) { StrokeThickness = 1 } - }]; + }); - if (mode == StaticsticsMode.ThisWeek) + if (mode == StatisticsMode.ThisWeek) { for (int i = 0; i < 7; i++) _mapSamples.Add(start.AddDays(i), 0); XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(1), v => WEEKDAYS[(int)v.DayOfWeek]) { TextSize = 10 }); } - else if (mode == StaticsticsMode.ThisMonth) + else if (mode == StatisticsMode.ThisMonth) { var now = DateTime.Now; var maxDays = DateTime.DaysInMonth(now.Year, now.Month); @@ -72,12 +66,12 @@ namespace SourceGit.Models } } - public void AddCommit(DateTime time, string author) + public void AddCommit(DateTime time, User author) { Total++; - var normalized = DateTime.MinValue; - if (_mode == StaticsticsMode.ThisWeek || _mode == StaticsticsMode.ThisMonth) + DateTime normalized; + if (_mode == StatisticsMode.ThisWeek || _mode == StatisticsMode.ThisMonth) normalized = time.Date; else normalized = new DateTime(time.Year, time.Month, 1).ToLocalTime(); @@ -91,10 +85,30 @@ namespace SourceGit.Models _mapUsers[author] = vu + 1; else _mapUsers.Add(author, 1); + + if (_mapUserSamples.TryGetValue(author, out var vus)) + { + if (vus.TryGetValue(normalized, out var n)) + vus[normalized] = n + 1; + else + vus.Add(normalized, 1); + } + else + { + _mapUserSamples.Add(author, new Dictionary + { + { normalized, 1 } + }); + } } public void Complete() { + foreach (var kv in _mapUsers) + Authors.Add(new StatisticsAuthor(kv.Key, kv.Value)); + + Authors.Sort((l, r) => r.Count - l.Count); + var samples = new List(); foreach (var kv in _mapSamples) samples.Add(new DateTimePoint(kv.Key, kv.Value)); @@ -109,64 +123,111 @@ namespace SourceGit.Models } ); - foreach (var kv in _mapUsers) - Authors.Add(new StaticsticsAuthor(kv.Key, kv.Value)); - - Authors.Sort((l, r) => r.Count - l.Count); - _mapUsers.Clear(); _mapSamples.Clear(); } public void ChangeColor(uint color) { - if (Series is [ColumnSeries series]) - series.Fill = new SolidColorPaint(new SKColor(color)); + _fillColor = color; + + var fill = new SKColor(color); + + if (Series.Count > 0 && Series[0] is ColumnSeries total) + total.Fill = new SolidColorPaint(_selectedAuthor == null ? fill : fill.WithAlpha(51)); + + if (Series.Count > 1 && Series[1] is ColumnSeries user) + user.Fill = new SolidColorPaint(fill); } - private StaticsticsMode _mode = StaticsticsMode.All; - private Dictionary _mapUsers = new Dictionary(); - private Dictionary _mapSamples = new Dictionary(); + public void ChangeAuthor(StatisticsAuthor author) + { + if (author == _selectedAuthor) + return; + + _selectedAuthor = author; + Series.RemoveRange(1, Series.Count - 1); + if (author == null || !_mapUserSamples.TryGetValue(author.User, out var userSamples)) + { + ChangeColor(_fillColor); + return; + } + + var samples = new List(); + foreach (var kv in userSamples) + samples.Add(new DateTimePoint(kv.Key, kv.Value)); + + Series.Add( + new ColumnSeries() + { + Values = samples, + Stroke = null, + Fill = null, + Padding = 1, + } + ); + + ChangeColor(_fillColor); + } + + private static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; + private StatisticsMode _mode; + private Dictionary _mapUsers = new(); + private Dictionary _mapSamples = new(); + private Dictionary> _mapUserSamples = new(); + private StatisticsAuthor _selectedAuthor = null; + private uint _fillColor = 255; } public class Statistics { - public StatisticsReport All { get; set; } - public StatisticsReport Month { get; set; } - public StatisticsReport Week { get; set; } + public StatisticsReport All { get; } + public StatisticsReport Month { get; } + public StatisticsReport Week { get; } public Statistics() { - _today = DateTime.Now.ToLocalTime().Date; - _thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24); - _thisMonthStart = _today.AddDays(1 - _today.Day); + var today = DateTime.Now.ToLocalTime().Date; + var weekOffset = (7 + (int)today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7; + _thisWeekStart = today.AddDays(-weekOffset); + _thisMonthStart = today.AddDays(1 - today.Day); - All = new StatisticsReport(StaticsticsMode.All, DateTime.MinValue); - Month = new StatisticsReport(StaticsticsMode.ThisMonth, _thisMonthStart); - Week = new StatisticsReport(StaticsticsMode.ThisWeek, _thisWeekStart); + All = new StatisticsReport(StatisticsMode.All, DateTime.MinValue); + Month = new StatisticsReport(StatisticsMode.ThisMonth, _thisMonthStart); + Week = new StatisticsReport(StatisticsMode.ThisWeek, _thisWeekStart); } public void AddCommit(string author, double timestamp) { + var emailIdx = author.IndexOf('±', StringComparison.Ordinal); + var email = author.Substring(emailIdx + 1).ToLower(CultureInfo.CurrentCulture); + if (!_users.TryGetValue(email, out var user)) + { + user = User.FindOrAdd(author); + _users.Add(email, user); + } + var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); if (time >= _thisWeekStart) - Week.AddCommit(time, author); + Week.AddCommit(time, user); if (time >= _thisMonthStart) - Month.AddCommit(time, author); + Month.AddCommit(time, user); - All.AddCommit(time, author); + All.AddCommit(time, user); } public void Complete() { + _users.Clear(); + All.Complete(); Month.Complete(); Week.Complete(); } - private readonly DateTime _today; private readonly DateTime _thisMonthStart; private readonly DateTime _thisWeekStart; + private readonly Dictionary _users = new(); } } diff --git a/src/Models/Submodule.cs b/src/Models/Submodule.cs index ce00ac02..ca73a8de 100644 --- a/src/Models/Submodule.cs +++ b/src/Models/Submodule.cs @@ -1,8 +1,20 @@ namespace SourceGit.Models { + public enum SubmoduleStatus + { + Normal = 0, + NotInited, + RevisionChanged, + Unmerged, + Modified, + } + public class Submodule { - public string Path { get; set; } = ""; - public bool IsDirty { get; set; } = false; + public string Path { get; set; } = string.Empty; + public string SHA { get; set; } = string.Empty; + public string URL { get; set; } = string.Empty; + public SubmoduleStatus Status { get; set; } = SubmoduleStatus.Normal; + public bool IsDirty => Status > SubmoduleStatus.NotInited; } } diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 2ec9e093..87944637 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -1,10 +1,27 @@ -namespace SourceGit.Models +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models { - public class Tag + public enum TagSortMode { - public string Name { get; set; } - public string SHA { get; set; } - public string Message { get; set; } - public bool IsFiltered { get; set; } + CreatorDate = 0, + Name, + } + + public class Tag : ObservableObject + { + public string Name { get; set; } = string.Empty; + public bool IsAnnotated { get; set; } = false; + public string SHA { get; set; } = string.Empty; + public ulong CreatorDate { get; set; } = 0; + public string Message { get; set; } = string.Empty; + + public FilterMode FilterMode + { + get => _filterMode; + set => SetProperty(ref _filterMode, value); + } + + private FilterMode _filterMode = FilterMode.None; } } diff --git a/src/Models/TemplateEngine.cs b/src/Models/TemplateEngine.cs new file mode 100644 index 00000000..c54f55fb --- /dev/null +++ b/src/Models/TemplateEngine.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace SourceGit.Models +{ + public class TemplateEngine + { + private class Context(Branch branch, IReadOnlyList changes) + { + public Branch branch = branch; + public IReadOnlyList changes = changes; + } + + private class Text(string text) + { + public string text = text; + } + + private class Variable(string name) + { + public string name = name; + } + + private class SlicedVariable(string name, int count) + { + public string name = name; + public int count = count; + } + + private class RegexVariable(string name, Regex regex, string replacement) + { + public string name = name; + public Regex regex = regex; + public string replacement = replacement; + } + + private const char ESCAPE = '\\'; + private const char VARIABLE_ANCHOR = '$'; + private const char VARIABLE_START = '{'; + private const char VARIABLE_END = '}'; + private const char VARIABLE_SLICE = ':'; + private const char VARIABLE_REGEX = '/'; + private const char NEWLINE = '\n'; + private const RegexOptions REGEX_OPTIONS = RegexOptions.Singleline | RegexOptions.IgnoreCase; + + public string Eval(string text, Branch branch, IReadOnlyList changes) + { + Reset(); + + _chars = text.ToCharArray(); + Parse(); + + var context = new Context(branch, changes); + var sb = new StringBuilder(); + sb.EnsureCapacity(text.Length); + foreach (var token in _tokens) + { + switch (token) + { + case Text text_token: + sb.Append(text_token.text); + break; + case Variable var_token: + sb.Append(EvalVariable(context, var_token)); + break; + case SlicedVariable sliced_var: + sb.Append(EvalVariable(context, sliced_var)); + break; + case RegexVariable regex_var: + sb.Append(EvalVariable(context, regex_var)); + break; + } + } + + return sb.ToString(); + } + + private void Reset() + { + _pos = 0; + _chars = []; + _tokens.Clear(); + } + + private char? Next() + { + var c = Peek(); + if (c is not null) + { + _pos++; + } + return c; + } + + private char? Peek() + { + return (_pos >= _chars.Length) ? null : _chars[_pos]; + } + + private int? Integer() + { + var start = _pos; + while (Peek() is char c && c >= '0' && c <= '9') + { + _pos++; + } + if (start >= _pos) + return null; + + var chars = new ReadOnlySpan(_chars, start, _pos - start); + return int.Parse(chars); + } + + private void Parse() + { + // text token start + var tok = _pos; + bool esc = false; + while (Next() is char c) + { + if (esc) + { + esc = false; + continue; + } + switch (c) + { + case ESCAPE: + // allow to escape only \ and $ + if (Peek() is char nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR)) + { + esc = true; + FlushText(tok, _pos - 1); + tok = _pos; + } + break; + case VARIABLE_ANCHOR: + // backup the position + var bak = _pos; + var variable = TryParseVariable(); + if (variable is null) + { + // no variable found, rollback + _pos = bak; + } + else + { + // variable found, flush a text token + FlushText(tok, bak - 1); + _tokens.Add(variable); + tok = _pos; + } + break; + } + } + // flush text token + FlushText(tok, _pos); + } + + private void FlushText(int start, int end) + { + int len = end - start; + if (len <= 0) + return; + var text = new string(_chars, start, len); + _tokens.Add(new Text(text)); + } + + private object TryParseVariable() + { + if (Next() != VARIABLE_START) + return null; + int name_start = _pos; + while (Next() is char c) + { + // name character, continue advancing + if (IsNameChar(c)) + continue; + + var name_end = _pos - 1; + // not a name character but name is empty, cancel + if (name_start >= name_end) + return null; + var name = new string(_chars, name_start, name_end - name_start); + + return c switch + { + // variable + VARIABLE_END => new Variable(name), + // sliced variable + VARIABLE_SLICE => TryParseSlicedVariable(name), + // regex variable + VARIABLE_REGEX => TryParseRegexVariable(name), + _ => null, + }; + } + + return null; + } + + private object TryParseSlicedVariable(string name) + { + int? n = Integer(); + if (n is null) + return null; + if (Next() != VARIABLE_END) + return null; + + return new SlicedVariable(name, (int)n); + } + + private object TryParseRegexVariable(string name) + { + var regex = ParseRegex(); + if (regex == null) + return null; + var replacement = ParseReplacement(); + if (replacement == null) + return null; + + return new RegexVariable(name, regex, replacement); + } + + private Regex ParseRegex() + { + var sb = new StringBuilder(); + var tok = _pos; + var esc = false; + while (Next() is char c) + { + if (esc) + { + esc = false; + continue; + } + switch (c) + { + case ESCAPE: + // allow to escape only / as \ and { used frequently in regexes + if (Peek() == VARIABLE_REGEX) + { + esc = true; + sb.Append(_chars, tok, _pos - 1 - tok); + tok = _pos; + } + break; + case VARIABLE_REGEX: + // goto is fine + goto Loop_exit; + case NEWLINE: + // no newlines allowed + return null; + } + } + Loop_exit: + sb.Append(_chars, tok, _pos - 1 - tok); + + try + { + var pattern = sb.ToString(); + if (pattern.Length == 0) + return null; + var regex = new Regex(pattern, REGEX_OPTIONS); + + return regex; + } + catch (RegexParseException) + { + return null; + } + } + + private string ParseReplacement() + { + var sb = new StringBuilder(); + var tok = _pos; + var esc = false; + while (Next() is char c) + { + if (esc) + { + esc = false; + continue; + } + switch (c) + { + case ESCAPE: + // allow to escape only } + if (Peek() == VARIABLE_END) + { + esc = true; + sb.Append(_chars, tok, _pos - 1 - tok); + tok = _pos; + } + break; + case VARIABLE_END: + // goto is fine + goto Loop_exit; + case NEWLINE: + // no newlines allowed + return null; + } + } + Loop_exit: + sb.Append(_chars, tok, _pos - 1 - tok); + + var replacement = sb.ToString(); + + return replacement; + } + + private static bool IsNameChar(char c) + { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; + } + + // (?) notice or log if variable is not found + private static string EvalVariable(Context context, string name) + { + if (!s_variables.TryGetValue(name, out var getter)) + { + return string.Empty; + } + return getter(context); + } + + private static string EvalVariable(Context context, Variable variable) + { + return EvalVariable(context, variable.name); + } + + private static string EvalVariable(Context context, SlicedVariable variable) + { + if (!s_slicedVariables.TryGetValue(variable.name, out var getter)) + { + return string.Empty; + } + return getter(context, variable.count); + } + + private static string EvalVariable(Context context, RegexVariable variable) + { + var str = EvalVariable(context, variable.name); + if (string.IsNullOrEmpty(str)) + return str; + return variable.regex.Replace(str, variable.replacement); + } + + private int _pos = 0; + private char[] _chars = []; + private readonly List _tokens = []; + + private delegate string VariableGetter(Context context); + + private static readonly IReadOnlyDictionary s_variables = new Dictionary() { + // legacy variables + {"branch_name", GetBranchName}, + {"files_num", GetFilesCount}, + {"files", GetFiles}, + // + {"BRANCH", GetBranchName}, + {"FILES_COUNT", GetFilesCount}, + {"FILES", GetFiles}, + }; + + private static string GetBranchName(Context context) + { + return context.branch.Name; + } + + private static string GetFilesCount(Context context) + { + return context.changes.Count.ToString(); + } + + private static string GetFiles(Context context) + { + var paths = new List(); + foreach (var c in context.changes) + paths.Add(c.Path); + return string.Join(", ", paths); + } + + private delegate string VariableSliceGetter(Context context, int count); + + private static readonly IReadOnlyDictionary s_slicedVariables = new Dictionary() { + // legacy variables + {"files", GetFilesSliced}, + // + {"FILES", GetFilesSliced}, + }; + + private static string GetFilesSliced(Context context, int count) + { + var sb = new StringBuilder(); + var paths = new List(); + var max = Math.Min(count, context.changes.Count); + for (int i = 0; i < max; i++) + paths.Add(context.changes[i].Path); + + sb.AppendJoin(", ", paths); + if (max < context.changes.Count) + sb.Append($" and {context.changes.Count - max} other files"); + + return sb.ToString(); + } + } +} diff --git a/src/Models/TextInlineChange.cs b/src/Models/TextInlineChange.cs index c96d839f..15901d03 100644 --- a/src/Models/TextInlineChange.cs +++ b/src/Models/TextInlineChange.cs @@ -9,7 +9,7 @@ namespace SourceGit.Models public int AddedStart { get; set; } public int AddedCount { get; set; } - class Chunk + private class Chunk { public int Hash; public bool Modified; @@ -25,7 +25,7 @@ namespace SourceGit.Models } } - enum Edit + private enum Edit { None, DeletedRight, @@ -34,7 +34,7 @@ namespace SourceGit.Models AddedLeft, } - class EditResult + private class EditResult { public Edit State; public int DeleteStart; @@ -204,11 +204,10 @@ namespace SourceGit.Models for (int i = 0; i <= half; i++) { - for (int j = -i; j <= i; j += 2) { var idx = j + half; - int o, n; + int o; if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1])) { o = forward[idx + 1]; @@ -220,7 +219,7 @@ namespace SourceGit.Models rs.State = Edit.DeletedRight; } - n = o - j; + var n = o - j; var startX = o; var startY = n; @@ -258,7 +257,7 @@ namespace SourceGit.Models for (int j = -i; j <= i; j += 2) { var idx = j + half; - int o, n; + int o; if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1])) { o = reverse[idx + 1] - 1; @@ -270,7 +269,7 @@ namespace SourceGit.Models rs.State = Edit.AddedLeft; } - n = o - (j + delta); + var n = o - (j + delta); var endX = o; var endY = n; @@ -312,8 +311,7 @@ namespace SourceGit.Models private static void AddChunk(List chunks, Dictionary hashes, string data, int start) { - int hash; - if (hashes.TryGetValue(data, out hash)) + if (hashes.TryGetValue(data, out var hash)) { chunks.Add(new Chunk(hash, start, data.Length)); } diff --git a/src/Models/TextMateHelper.cs b/src/Models/TextMateHelper.cs index 0eb5e489..b7efae72 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -21,10 +21,11 @@ namespace SourceGit.Models { private static readonly ExtraGrammar[] s_extraGrammars = [ - new ExtraGrammar("source.toml", ".toml", "toml.json"), - new ExtraGrammar("source.kotlin", ".kotlin", "kotlin.json"), - new ExtraGrammar("source.hx", ".hx", "haxe.json"), - new ExtraGrammar("source.hxml", ".hxml", "hxml.json"), + new ExtraGrammar("source.toml", [".toml"], "toml.json"), + new ExtraGrammar("source.kotlin", [".kotlin", ".kt", ".kts"], "kotlin.json"), + new ExtraGrammar("source.hx", [".hx"], "haxe.json"), + new ExtraGrammar("source.hxml", [".hxml"], "hxml.json"), + new ExtraGrammar("text.html.jsp", [".jsp", ".jspf", ".tag"], "jsp.json"), ]; public static string GetScope(string file, RegistryOptions reg) @@ -36,13 +37,14 @@ namespace SourceGit.Models extension = ".xml"; else if (extension == ".command") extension = ".sh"; - else if (extension == ".kt" || extension == ".kts") - extension = ".kotlin"; foreach (var grammar in s_extraGrammars) { - if (grammar.Extension.Equals(extension, StringComparison.OrdinalIgnoreCase)) - return grammar.Scope; + foreach (var ext in grammar.Extensions) + { + if (ext.Equals(extension, StringComparison.OrdinalIgnoreCase)) + return grammar.Scope; + } } return reg.GetScopeByExtension(extension); @@ -71,10 +73,10 @@ namespace SourceGit.Models return reg.GetGrammar(scopeName); } - private record ExtraGrammar(string Scope, string Extension, string File) + private record ExtraGrammar(string Scope, List Extensions, string File) { public readonly string Scope = Scope; - public readonly string Extension = Extension; + public readonly List Extensions = Extensions; public readonly string File = File; } } diff --git a/src/Models/User.cs b/src/Models/User.cs index 850bcf2f..0b4816fe 100644 --- a/src/Models/User.cs +++ b/src/Models/User.cs @@ -26,11 +26,7 @@ namespace SourceGit.Models public override bool Equals(object obj) { - if (obj == null || !(obj is User)) - return false; - - var other = obj as User; - return Name == other.Name && Email == other.Email; + return obj is User other && Name == other.Name && Email == other.Email; } public override int GetHashCode() @@ -43,6 +39,11 @@ namespace SourceGit.Models return _caches.GetOrAdd(data, key => new User(key)); } + public override string ToString() + { + return $"{Name} <{Email}>"; + } + private static ConcurrentDictionary _caches = new ConcurrentDictionary(); private readonly int _hash; } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 6665250c..a3cfc329 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -8,12 +8,12 @@ namespace SourceGit.Models { public class Watcher : IDisposable { - public Watcher(IRepository repo) + public Watcher(IRepository repo, string fullpath, string gitDir) { _repo = repo; _wcWatcher = new FileSystemWatcher(); - _wcWatcher.Path = _repo.FullPath; + _wcWatcher.Path = fullpath; _wcWatcher.Filter = "*"; _wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; _wcWatcher.IncludeSubdirectories = true; @@ -23,15 +23,8 @@ namespace SourceGit.Models _wcWatcher.Deleted += OnWorkingCopyChanged; _wcWatcher.EnableRaisingEvents = true; - // If this repository is a worktree repository, just watch the main repository's gitdir. - var gitDirNormalized = _repo.GitDir.Replace("\\", "/"); - var worktreeIdx = gitDirNormalized.IndexOf(".git/worktrees/", StringComparison.Ordinal); - var repoWatchDir = _repo.GitDir; - if (worktreeIdx > 0) - repoWatchDir = _repo.GitDir.Substring(0, worktreeIdx + 4); - _repoWatcher = new FileSystemWatcher(); - _repoWatcher.Path = repoWatchDir; + _repoWatcher.Path = gitDir; _repoWatcher.Filter = "*"; _repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; _repoWatcher.IncludeSubdirectories = true; @@ -72,6 +65,11 @@ namespace SourceGit.Models _updateBranch = DateTime.Now.ToFileTime() - 1; } + public void MarkTagDirtyManually() + { + _updateTags = DateTime.Now.ToFileTime() - 1; + } + public void MarkWorkingCopyDirtyManually() { _updateWC = DateTime.Now.ToFileTime() - 1; @@ -113,22 +111,17 @@ namespace SourceGit.Models if (_updateTags > 0) { _updateTags = 0; - Task.Run(() => - { - _repo.RefreshTags(); - _repo.RefreshBranches(); - _repo.RefreshCommits(); - }); - } - else - { - Task.Run(() => - { - _repo.RefreshBranches(); - _repo.RefreshCommits(); - }); + Task.Run(_repo.RefreshTags); } + if (_updateSubmodules > 0 || _repo.MayHaveSubmodules()) + { + _updateSubmodules = 0; + Task.Run(_repo.RefreshSubmodules); + } + + Task.Run(_repo.RefreshBranches); + Task.Run(_repo.RefreshCommits); Task.Run(_repo.RefreshWorkingCopyChanges); Task.Run(_repo.RefreshWorktrees); } @@ -142,33 +135,48 @@ namespace SourceGit.Models if (_updateSubmodules > 0 && now > _updateSubmodules) { _updateSubmodules = 0; - _repo.RefreshSubmodules(); + Task.Run(_repo.RefreshSubmodules); } if (_updateStashes > 0 && now > _updateStashes) { _updateStashes = 0; - _repo.RefreshStashes(); + Task.Run(_repo.RefreshStashes); } if (_updateTags > 0 && now > _updateTags) { _updateTags = 0; - _repo.RefreshTags(); - _repo.RefreshCommits(); + Task.Run(_repo.RefreshTags); + Task.Run(_repo.RefreshCommits); } } private void OnRepositoryChanged(object o, FileSystemEventArgs e) { - if (string.IsNullOrEmpty(e.Name) || e.Name.EndsWith(".lock", StringComparison.Ordinal)) + if (string.IsNullOrEmpty(e.Name)) return; - var name = e.Name.Replace("\\", "/"); - if (name.StartsWith("modules", StringComparison.Ordinal) && name.EndsWith("HEAD", StringComparison.Ordinal)) + var name = e.Name.Replace('\\', '/').TrimEnd('/'); + if (name.Contains("fsmonitor--daemon/", StringComparison.Ordinal) || + name.EndsWith(".lock", StringComparison.Ordinal) || + name.StartsWith("lfs/", StringComparison.Ordinal)) + return; + + if (name.StartsWith("modules", StringComparison.Ordinal)) { - _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); - _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + if (name.EndsWith("/HEAD", StringComparison.Ordinal) || + name.EndsWith("/ORIG_HEAD", StringComparison.Ordinal)) + { + _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); + _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + } + } + else if (name.Equals("MERGE_HEAD", StringComparison.Ordinal) || + name.Equals("AUTO_MERGE", StringComparison.Ordinal)) + { + if (_repo.MayHaveSubmodules()) + _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); } else if (name.StartsWith("refs/tags", StringComparison.Ordinal)) { @@ -179,17 +187,12 @@ namespace SourceGit.Models _updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); } else if (name.Equals("HEAD", StringComparison.Ordinal) || + name.Equals("BISECT_START", StringComparison.Ordinal) || name.StartsWith("refs/heads/", StringComparison.Ordinal) || name.StartsWith("refs/remotes/", StringComparison.Ordinal) || (name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal))) { _updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime(); - - lock (_submodules) - { - if (_submodules.Count > 0) - _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); - } } else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal)) { @@ -202,11 +205,22 @@ namespace SourceGit.Models if (string.IsNullOrEmpty(e.Name)) return; - var name = e.Name.Replace("\\", "/"); - if (name == ".git" || name.StartsWith(".git/", StringComparison.Ordinal)) + var name = e.Name.Replace('\\', '/').TrimEnd('/'); + if (name.Equals(".git", StringComparison.Ordinal) || + name.StartsWith(".git/", StringComparison.Ordinal) || + name.EndsWith("/.git", StringComparison.Ordinal)) return; - lock (_submodules) + if (name.StartsWith(".vs/", StringComparison.Ordinal)) + return; + + if (name.Equals(".gitmodules", StringComparison.Ordinal)) + { + _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); + return; + } + + lock (_lockSubmodule) { foreach (var submodule in _submodules) { @@ -232,7 +246,7 @@ namespace SourceGit.Models private long _updateStashes = 0; private long _updateTags = 0; - private object _lockSubmodule = new object(); + private readonly Lock _lockSubmodule = new(); private List _submodules = new List(); } } diff --git a/src/Models/Worktree.cs b/src/Models/Worktree.cs index f9ba14e4..26f88a8a 100644 --- a/src/Models/Worktree.cs +++ b/src/Models/Worktree.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.Models { @@ -6,6 +7,7 @@ namespace SourceGit.Models { public string Branch { get; set; } = string.Empty; public string FullPath { get; set; } = string.Empty; + public string RelativePath { get; set; } = string.Empty; public string Head { get; set; } = string.Empty; public bool IsBare { get; set; } = false; public bool IsDetached { get; set; } = false; @@ -21,15 +23,15 @@ namespace SourceGit.Models get { if (IsDetached) - return $"(deteched HEAD at {Head.Substring(10)})"; + return $"detached HEAD at {Head.AsSpan(10)}"; - if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal)) - return $"({Branch.Substring(11)})"; + if (Branch.StartsWith("refs/heads/", StringComparison.Ordinal)) + return Branch.Substring(11); - if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal)) - return $"({Branch.Substring(13)})"; + if (Branch.StartsWith("refs/remotes/", StringComparison.Ordinal)) + return Branch.Substring(13); - return $"({Branch})"; + return Branch; } } diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index 55b7b43b..3f6de903 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -5,6 +5,8 @@ using System.IO; using System.Runtime.Versioning; using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform; namespace SourceGit.Native { @@ -13,10 +15,22 @@ namespace SourceGit.Native { public void SetupApp(AppBuilder builder) { - builder.With(new X11PlatformOptions() + builder.With(new X11PlatformOptions() { EnableIme = true }); + } + + public void SetupWindow(Window window) + { + if (OS.UseSystemWindowFrame) { - EnableIme = true, - }); + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; + window.ExtendClientAreaToDecorationsHint = false; + } + else + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; + window.ExtendClientAreaToDecorationsHint = true; + window.Classes.Add("custom_window_frame"); + } } public string FindGitExecutable() @@ -68,13 +82,16 @@ namespace SourceGit.Native { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var cwd = string.IsNullOrEmpty(workdir) ? home : workdir; + var terminal = OS.ShellOrTerminal; var startInfo = new ProcessStartInfo(); startInfo.WorkingDirectory = cwd; - startInfo.FileName = OS.ShellOrTerminal; + startInfo.FileName = terminal; - if (OS.ShellOrTerminal.EndsWith("wezterm", StringComparison.OrdinalIgnoreCase)) + if (terminal.EndsWith("wezterm", StringComparison.OrdinalIgnoreCase)) startInfo.Arguments = $"start --cwd \"{cwd}\""; + else if (terminal.EndsWith("ptyxis", StringComparison.OrdinalIgnoreCase)) + startInfo.Arguments = $"--new-window --working-directory=\"{cwd}\""; try { @@ -103,8 +120,8 @@ namespace SourceGit.Native private string FindExecutable(string filename) { var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - var pathes = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); - foreach (var path in pathes) + var paths = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); + foreach (var path in paths) { var test = Path.Combine(path, filename); if (File.Exists(test)) diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index 316e509c..b76d239a 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -3,9 +3,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.Versioning; -using System.Text; using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform; namespace SourceGit.Native { @@ -19,35 +20,39 @@ namespace SourceGit.Native DisableDefaultApplicationMenuItems = true, }); - { - var startInfo = new ProcessStartInfo(); - startInfo.FileName = "zsh"; - startInfo.Arguments = "--login -c \"echo $PATH\""; - startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - startInfo.RedirectStandardOutput = true; - startInfo.StandardOutputEncoding = Encoding.UTF8; + // Fix `PATH` env on macOS. + var path = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrEmpty(path)) + path = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"; + else if (!path.Contains("/opt/homebrew/", StringComparison.Ordinal)) + path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path; - try - { - var proc = new Process() { StartInfo = startInfo }; - proc.Start(); - var pathData = proc.StandardOutput.ReadToEnd(); - proc.WaitForExit(); - if (proc.ExitCode == 0) - Environment.SetEnvironmentVariable("PATH", pathData); - proc.Close(); - } - catch - { - // Ignore error. - } + var customPathFile = Path.Combine(OS.DataDir, "PATH"); + if (File.Exists(customPathFile)) + { + var env = File.ReadAllText(customPathFile).Trim(); + if (!string.IsNullOrEmpty(env)) + path = env; } + + Environment.SetEnvironmentVariable("PATH", path); + } + + public void SetupWindow(Window window) + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome; + window.ExtendClientAreaToDecorationsHint = true; } public string FindGitExecutable() { - return File.Exists("/usr/bin/git") ? "/usr/bin/git" : string.Empty; + var gitPathVariants = new List() { + "/usr/bin/git", "/usr/local/bin/git", "/opt/homebrew/bin/git", "/opt/homebrew/opt/git/bin/git" + }; + foreach (var path in gitPathVariants) + if (File.Exists(path)) + return path; + return string.Empty; } public string FindTerminal(Models.ShellOrTerminal shell) @@ -58,6 +63,12 @@ namespace SourceGit.Native return "Terminal"; case "iterm2": return "iTerm"; + case "warp": + return "Warp"; + case "ghostty": + return "Ghostty"; + case "kitty": + return "kitty"; } return string.Empty; diff --git a/src/Native/OS.cs b/src/Native/OS.cs index bc9c5403..ad6f8104 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -1,16 +1,21 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Text; +using System.Text.RegularExpressions; using Avalonia; +using Avalonia.Controls; namespace SourceGit.Native { - public static class OS + public static partial class OS { public interface IBackend { void SetupApp(AppBuilder builder); + void SetupWindow(Window window); string FindGitExecutable(); string FindTerminal(Models.ShellOrTerminal shell); @@ -22,10 +27,54 @@ namespace SourceGit.Native void OpenWithDefaultEditor(string file); } - public static string DataDir { get; private set; } = string.Empty; - public static string GitExecutable { get; set; } = string.Empty; - public static string ShellOrTerminal { get; set; } = string.Empty; - public static List ExternalTools { get; set; } = []; + public static string DataDir + { + get; + private set; + } = string.Empty; + + public static string GitExecutable + { + get => _gitExecutable; + set + { + if (_gitExecutable != value) + { + _gitExecutable = value; + UpdateGitVersion(); + } + } + } + + public static string GitVersionString + { + get; + private set; + } = string.Empty; + + public static Version GitVersion + { + get; + private set; + } = new Version(0, 0, 0); + + public static string ShellOrTerminal + { + get; + set; + } = string.Empty; + + public static List ExternalTools + { + get; + set; + } = []; + + public static bool UseSystemWindowFrame + { + get => OperatingSystem.IsLinux() && _enableSystemWindowFrame; + set => _enableSystemWindowFrame = value; + } static OS() { @@ -54,6 +103,17 @@ namespace SourceGit.Native public static void SetupDataDir() { + if (OperatingSystem.IsWindows()) + { + var execFile = Process.GetCurrentProcess().MainModule!.FileName; + var portableDir = Path.Combine(Path.GetDirectoryName(execFile), "data"); + if (Directory.Exists(portableDir)) + { + DataDir = portableDir; + return; + } + } + var osAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); if (string.IsNullOrEmpty(osAppDataDir)) DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sourcegit"); @@ -64,11 +124,16 @@ namespace SourceGit.Native Directory.CreateDirectory(DataDir); } - public static void SetupEnternalTools() + public static void SetupExternalTools() { ExternalTools = _backend.FindExternalTools(); } + public static void SetupForWindow(Window window) + { + _backend.SetupWindow(window); + } + public static string FindGitExecutable() { return _backend.FindGitExecutable(); @@ -110,6 +175,69 @@ namespace SourceGit.Native _backend.OpenWithDefaultEditor(file); } + public static string GetAbsPath(string root, string sub) + { + var fullpath = Path.Combine(root, sub); + if (OperatingSystem.IsWindows()) + return fullpath.Replace('/', '\\'); + + return fullpath; + } + + private static void UpdateGitVersion() + { + if (string.IsNullOrEmpty(_gitExecutable) || !File.Exists(_gitExecutable)) + { + GitVersionString = string.Empty; + GitVersion = new Version(0, 0, 0); + return; + } + + var start = new ProcessStartInfo(); + start.FileName = _gitExecutable; + start.Arguments = "--version"; + start.UseShellExecute = false; + start.CreateNoWindow = true; + start.RedirectStandardOutput = true; + start.RedirectStandardError = true; + start.StandardOutputEncoding = Encoding.UTF8; + start.StandardErrorEncoding = Encoding.UTF8; + + var proc = new Process() { StartInfo = start }; + try + { + proc.Start(); + + var rs = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + if (proc.ExitCode == 0 && !string.IsNullOrWhiteSpace(rs)) + { + GitVersionString = rs.Trim(); + + var match = REG_GIT_VERSION().Match(GitVersionString); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var build = int.Parse(match.Groups[3].Value); + GitVersion = new Version(major, minor, build); + GitVersionString = GitVersionString.Substring(11).Trim(); + } + } + } + catch + { + // Ignore errors + } + + proc.Close(); + } + + [GeneratedRegex(@"^git version[\s\w]*(\d+)\.(\d+)[\.\-](\d+).*$")] + private static partial Regex REG_GIT_VERSION(); + private static IBackend _backend = null; + private static string _gitExecutable = string.Empty; + private static bool _enableSystemWindowFrame = false; } } diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index 6ca0bbb0..07cf51fb 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -8,26 +8,33 @@ using System.Text; using Avalonia; using Avalonia.Controls; +using Avalonia.Platform; +using Avalonia.Threading; namespace SourceGit.Native { [SupportedOSPlatform("windows")] internal class Windows : OS.IBackend { - [StructLayout(LayoutKind.Sequential)] - internal struct RTL_OSVERSIONINFOEX + internal struct RECT { - internal uint dwOSVersionInfoSize; - internal uint dwMajorVersion; - internal uint dwMinorVersion; - internal uint dwBuildNumber; - internal uint dwPlatformId; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] - internal string szCSDVersion; + public int left; + public int top; + public int right; + public int bottom; } - [DllImport("ntdll")] - private static extern int RtlGetVersion(ref RTL_OSVERSIONINFOEX lpVersionInformation); + [StructLayout(LayoutKind.Sequential)] + internal struct MARGINS + { + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; + } + + [DllImport("dwmapi.dll")] + private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)] private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs); @@ -41,41 +48,96 @@ namespace SourceGit.Native [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)] private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags); + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); + public void SetupApp(AppBuilder builder) { // Fix drop shadow issue on Windows 10 - RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); - v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(); - if (RtlGetVersion(ref v) == 0 && (v.dwMajorVersion < 10 || v.dwBuildNumber < 22000)) + if (!OperatingSystem.IsWindowsVersionAtLeast(10, 22000, 0)) { Window.WindowStateProperty.Changed.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); Control.LoadedEvent.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); } } + public void SetupWindow(Window window) + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; + window.ExtendClientAreaToDecorationsHint = true; + window.Classes.Add("fix_maximized_padding"); + + Win32Properties.AddWndProcHookCallback(window, (IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool handled) => + { + // Custom WM_NCHITTEST + if (msg == 0x0084) + { + handled = true; + + if (window.WindowState == WindowState.FullScreen || window.WindowState == WindowState.Maximized) + return 1; // HTCLIENT + + var p = IntPtrToPixelPoint(lParam); + GetWindowRect(hWnd, out var rcWindow); + + var borderThickness = (int)(4 * window.RenderScaling); + int y = 1; + int x = 1; + if (p.X >= rcWindow.left && p.X < rcWindow.left + borderThickness) + x = 0; + else if (p.X < rcWindow.right && p.X >= rcWindow.right - borderThickness) + x = 2; + + if (p.Y >= rcWindow.top && p.Y < rcWindow.top + borderThickness) + y = 0; + else if (p.Y < rcWindow.bottom && p.Y >= rcWindow.bottom - borderThickness) + y = 2; + + var zone = y * 3 + x; + switch (zone) + { + case 0: + return 13; // HTTOPLEFT + case 1: + return 12; // HTTOP + case 2: + return 14; // HTTOPRIGHT + case 3: + return 10; // HTLEFT + case 4: + return 1; // HTCLIENT + case 5: + return 11; // HTRIGHT + case 6: + return 16; // HTBOTTOMLEFT + case 7: + return 15; // HTBOTTOM + default: + return 17; // HTBOTTOMRIGHT + } + } + + return IntPtr.Zero; + }); + } + public string FindGitExecutable() { var reg = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryView.Registry64); - var git = reg.OpenSubKey("SOFTWARE\\GitForWindows"); - if (git != null && git.GetValue("InstallPath") is string installPath) - { + var git = reg.OpenSubKey(@"SOFTWARE\GitForWindows"); + if (git?.GetValue("InstallPath") is string installPath) return Path.Combine(installPath, "bin", "git.exe"); - } var builder = new StringBuilder("git.exe", 259); if (!PathFindOnPath(builder, null)) - { return null; - } var exePath = builder.ToString(); if (!string.IsNullOrEmpty(exePath)) - { return exePath; - } return null; } @@ -113,7 +175,7 @@ namespace SourceGit.Native break; case "cmd": - return "C:\\Windows\\System32\\cmd.exe"; + return @"C:\Windows\System32\cmd.exe"; case "wt": var wtFinder = new StringBuilder("wt.exe", 512); if (PathFindOnPath(wtFinder, null)) @@ -131,15 +193,16 @@ namespace SourceGit.Native finder.VSCode(FindVSCode); finder.VSCodeInsiders(FindVSCodeInsiders); finder.VSCodium(FindVSCodium); - finder.Fleet(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\Programs\\Fleet\\Fleet.exe"); - finder.FindJetBrainsFromToolbox(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\JetBrains\\Toolbox"); + finder.Fleet(() => $@"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Programs\Fleet\Fleet.exe"); + finder.FindJetBrainsFromToolbox(() => $@"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\JetBrains\Toolbox"); finder.SublimeText(FindSublimeText); + finder.TryAdd("Visual Studio", "vs", FindVisualStudio, GenerateCommandlineArgsForVisualStudio); return finder.Founded; } public void OpenBrowser(string url) { - var info = new ProcessStartInfo("cmd", $"/c start {url}"); + var info = new ProcessStartInfo("cmd", $"/c start \"\" \"{url}\""); info.CreateNoWindow = true; Process.Start(info); } @@ -201,10 +264,23 @@ namespace SourceGit.Native private void FixWindowFrameOnWin10(Window w) { - if (w.WindowState == WindowState.Maximized || w.WindowState == WindowState.FullScreen) - w.SystemDecorations = SystemDecorations.Full; - else if (w.WindowState == WindowState.Normal) - w.SystemDecorations = SystemDecorations.BorderOnly; + // Schedule the DWM frame extension to run in the next render frame + // to ensure proper timing with the window initialization sequence + Dispatcher.UIThread.InvokeAsync(() => + { + var platformHandle = w.TryGetPlatformHandle(); + if (platformHandle == null) + return; + + var margins = new MARGINS { cxLeftWidth = 1, cxRightWidth = 1, cyTopHeight = 1, cyBottomHeight = 1 }; + DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins); + }, DispatcherPriority.Render); + } + + private PixelPoint IntPtrToPixelPoint(IntPtr param) + { + var v = IntPtr.Size == 4 ? param.ToInt32() : (int)(param.ToInt64() & 0xFFFFFFFF); + return new PixelPoint((short)(v & 0xffff), (short)(v >> 16)); } #region EXTERNAL_EDITOR_FINDER @@ -217,9 +293,7 @@ namespace SourceGit.Native // VSCode (system) var systemVScode = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1"); if (systemVScode != null) - { return systemVScode.GetValue("DisplayIcon") as string; - } var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.CurrentUser, @@ -228,9 +302,7 @@ namespace SourceGit.Native // VSCode (user) var vscode = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{771FD6B0-FA20-440A-A002-3B3BAC16DC50}_is1"); if (vscode != null) - { return vscode.GetValue("DisplayIcon") as string; - } return string.Empty; } @@ -244,9 +316,7 @@ namespace SourceGit.Native // VSCode - Insiders (system) var systemVScodeInsiders = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1"); if (systemVScodeInsiders != null) - { return systemVScodeInsiders.GetValue("DisplayIcon") as string; - } var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.CurrentUser, @@ -255,9 +325,7 @@ namespace SourceGit.Native // VSCode - Insiders (user) var vscodeInsiders = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{217B4C08-948D-4276-BFBB-BEE930AE5A2C}_is1"); if (vscodeInsiders != null) - { return vscodeInsiders.GetValue("DisplayIcon") as string; - } return string.Empty; } @@ -269,11 +337,9 @@ namespace SourceGit.Native Microsoft.Win32.RegistryView.Registry64); // VSCodium (system) - var systemVScodium = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1"); - if (systemVScodium != null) - { - return systemVScodium.GetValue("DisplayIcon") as string; - } + var systemVSCodium = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1"); + if (systemVSCodium != null) + return systemVSCodium.GetValue("DisplayIcon") as string; var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.CurrentUser, @@ -282,9 +348,7 @@ namespace SourceGit.Native // VSCodium (user) var vscodium = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1"); if (vscodium != null) - { return vscodium.GetValue("DisplayIcon") as string; - } return string.Empty; } @@ -313,6 +377,25 @@ namespace SourceGit.Native return string.Empty; } + + private string FindVisualStudio() + { + var localMachine = Microsoft.Win32.RegistryKey.OpenBaseKey( + Microsoft.Win32.RegistryHive.LocalMachine, + Microsoft.Win32.RegistryView.Registry64); + + // Get default class for VisualStudio.Launcher.sln - the handler for *.sln files + if (localMachine.OpenSubKey(@"SOFTWARE\Classes\VisualStudio.Launcher.sln\CLSID") is Microsoft.Win32.RegistryKey launcher) + { + // Get actual path to the executable + if (launcher.GetValue(string.Empty) is string CLSID && + localMachine.OpenSubKey(@$"SOFTWARE\Classes\CLSID\{CLSID}\LocalServer32") is Microsoft.Win32.RegistryKey devenv && + devenv.GetValue(string.Empty) is string localServer32) + return localServer32!.Trim('\"'); + } + + return string.Empty; + } #endregion private void OpenFolderAndSelectFile(string folderPath) @@ -328,5 +411,34 @@ namespace SourceGit.Native ILFree(pidl); } } + + private string GenerateCommandlineArgsForVisualStudio(string repo) + { + var sln = FindVSSolutionFile(new DirectoryInfo(repo), 4); + return string.IsNullOrEmpty(sln) ? $"\"{repo}\"" : $"\"{sln}\""; + } + + private string FindVSSolutionFile(DirectoryInfo dir, int leftDepth) + { + var files = dir.GetFiles(); + foreach (var f in files) + { + if (f.Name.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) + return f.FullName; + } + + if (leftDepth <= 0) + return null; + + var subDirs = dir.GetDirectories(); + foreach (var subDir in subDirs) + { + var first = FindVSSolutionFile(subDir, leftDepth - 1); + if (!string.IsNullOrEmpty(first)) + return first; + } + + return null; + } } } diff --git a/src/Resources/Grammars/haxe.json b/src/Resources/Grammars/haxe.json index 12acc538..3f78154d 100644 --- a/src/Resources/Grammars/haxe.json +++ b/src/Resources/Grammars/haxe.json @@ -1,7 +1,9 @@ { "information_for_contributors": [ "This file has been copied from https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/haxe.tmLanguage", - "and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage" + "and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage", + "The original file was licensed under the MIT License", + "https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md" ], "fileTypes": [ "hx", @@ -2485,4 +2487,4 @@ "name": "variable.other.hx" } } -} \ No newline at end of file +} diff --git a/src/Resources/Grammars/hxml.json b/src/Resources/Grammars/hxml.json index 829c403e..3be42577 100644 --- a/src/Resources/Grammars/hxml.json +++ b/src/Resources/Grammars/hxml.json @@ -1,7 +1,9 @@ { "information_for_contributors": [ "This file has been copied from https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/hxml.tmLanguage", - "and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage" + "and converted to JSON using https://marketplace.visualstudio.com/items?itemName=pedro-w.tmlanguage", + "The original file was licensed under the MIT License", + "https://github.com/vshaxe/haxe-TmLanguage/blob/ddad8b4c6d0781ac20be0481174ec1be772c5da5/LICENSE.md" ], "fileTypes": [ "hxml" @@ -67,4 +69,4 @@ ], "scopeName": "source.hxml", "uuid": "CB1B853A-C4C8-42C3-BA70-1B1605BE51C1" -} \ No newline at end of file +} diff --git a/src/Resources/Grammars/jsp.json b/src/Resources/Grammars/jsp.json new file mode 100644 index 00000000..2fbfd97c --- /dev/null +++ b/src/Resources/Grammars/jsp.json @@ -0,0 +1,100 @@ +{ + "information_for_contributors": [ + "This file has been copied from https://github.com/samuel-weinhardt/vscode-jsp-lang/blob/0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355/syntaxes/jsp.tmLanguage.json", + "The original file was licensed under the MIT License", + "https://github.com/samuel-weinhardt/vscode-jsp-lang/blob/0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355/LICENSE" + ], + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Jakarta Server Pages", + "fileTypes": ["jsp", "jspf", "tag"], + "scopeName": "text.html.jsp", + "patterns": [ + { "include": "#comment" }, + { "include": "#directive" }, + { "include": "#expression" }, + { "include": "text.html.derivative" } + ], + "injections": { + "L:text.html.jsp -comment -meta.tag.directive.jsp -meta.tag.scriptlet.jsp": { + "patterns": [ + { "include": "#scriptlet" } + ], + "comment": "allow scriptlets anywhere except comments and nested" + }, + "L:meta.attribute (string.quoted.single.html | string.quoted.double.html) -string.template.expression.jsp": { + "patterns": [ + { "include": "#expression" }, + { "include": "text.html.derivative" } + ], + "comment": "allow expressions and tags within HTML attributes (not nested)" + } + }, + "repository": { + "comment": { + "name": "comment.block.jsp", + "begin": "<%--", + "end": "--%>" + }, + "directive": { + "name": "meta.tag.directive.jsp", + "begin": "(<)(%@)", + "end": "(%)(>)", + "beginCaptures": { + "1": { "name": "punctuation.definition.tag.jsp" }, + "2": { "name": "entity.name.tag.jsp" } + }, + "endCaptures": { + "1": { "name": "entity.name.tag.jsp" }, + "2": { "name": "punctuation.definition.tag.jsp" } + }, + "patterns": [ + { + "match": "\\b(attribute|include|page|tag|taglib|variable)\\b(?!\\s*=)", + "name": "keyword.control.directive.jsp" + }, + { "include": "text.html.basic#attribute" } + ] + }, + "scriptlet": { + "name": "meta.tag.scriptlet.jsp", + "contentName": "meta.embedded.block.java", + "begin": "(<)(%[\\s!=])", + "end": "(%)(>)", + "beginCaptures": { + "1": { "name": "punctuation.definition.tag.jsp" }, + "2": { "name": "entity.name.tag.jsp" } + }, + "endCaptures": { + "1": { "name": "entity.name.tag.jsp" }, + "2": { "name": "punctuation.definition.tag.jsp" } + }, + "patterns": [ + { + "match": "\\{(?=\\s*(%>|$))", + "comment": "consume trailing curly brackets for fragmented scriptlets" + }, + { "include": "source.java" } + ] + }, + "expression": { + "name": "string.template.expression.jsp", + "contentName": "meta.embedded.block.java", + "begin": "[$#]\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.definition.template-expression.begin.jsp" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.template-expression.end.jsp" } + }, + "patterns": [ + { "include": "#escape" }, + { "include": "source.java" } + ] + }, + "escape": { + "match": "\\\\.", + "name": "constant.character.escape.jsp" + } + } +} diff --git a/src/Resources/Grammars/kotlin.json b/src/Resources/Grammars/kotlin.json index e8f844d0..2857f717 100644 --- a/src/Resources/Grammars/kotlin.json +++ b/src/Resources/Grammars/kotlin.json @@ -1,6 +1,8 @@ { "information_for_contributors": [ - "This file has been copied from https://github.com/eclipse/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/org.eclipse.buildship.kotlindsl.provider/kotlin.tmLanguage.json" + "This file has been copied from https://github.com/eclipse/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/org.eclipse.buildship.kotlindsl.provider/kotlin.tmLanguage.json", + "The original file was licensed under the Eclipse Public License, Version 1.0", + "https://github.com/eclipse-buildship/buildship/blob/6bb773e7692f913dec27105129ebe388de34e68b/README.md" ], "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "name": "Kotlin", @@ -698,4 +700,4 @@ "name": "variable.language.this.kotlin" } } -} \ No newline at end of file +} diff --git a/src/Resources/Grammars/toml.json b/src/Resources/Grammars/toml.json index 86c2ef87..6be4678f 100644 --- a/src/Resources/Grammars/toml.json +++ b/src/Resources/Grammars/toml.json @@ -3,7 +3,10 @@ "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." + "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.", + "This file has been copied from https://github.com/kkiyama117/coc-toml/blob/main/toml.tmLanguage.json", + "The original file was licensed under the MIT License", + "https://github.com/kkiyama117/coc-toml/blob/main/LICENSE" ], "patterns": [ { diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index 1a84597e..001c7ee7 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -1,21 +1,28 @@ - M951 419a255 255 0 00-22-209 258 258 0 00-278-124A259 259 0 00213 178a255 255 0 00-171 124 258 258 0 0032 303 255 255 0 0022 210 258 258 0 00278 124A255 255 0 00566 1024a258 258 0 00246-179 256 256 0 00171-124 258 258 0 00-32-302zM566 957a191 191 0 01-123-44l6-3 204-118a34 34 0 0017-29v-287l86 50a3 3 0 012 2v238a192 192 0 01-192 192zM154 781a191 191 0 01-23-129l6 4 204 118a33 33 0 0033 0l249-144v99a3 3 0 01-1 3L416 851a192 192 0 01-262-70zM100 337a191 191 0 01101-84V495a33 33 0 0017 29l248 143-86 50a3 3 0 01-3 0l-206-119A192 192 0 01100 336zm708 164-249-145L645 307a3 3 0 013 0l206 119a192 192 0 01-29 346v-242a34 34 0 00-17-28zm86-129-6-4-204-119a33 33 0 00-33 0L401 394V294a3 3 0 011-3l206-119a192 192 0 01285 199zm-539 176-86-50a3 3 0 01-2-2V259a192 192 0 01315-147l-6 3-204 118a34 34 0 00-17 29zm47-101 111-64 111 64v128l-111 64-111-64z + M41 512c0-128 46-241 138-333C271 87 384 41 512 41s241 46 333 138c92 92 138 205 138 333s-46 241-138 333c-92 92-205 138-333 138s-241-46-333-138C87 753 41 640 41 512zm87 0c0 108 36 195 113 271s164 113 271 113c108 0 195-36 271-113s113-164 113-271-36-195-113-271c-77-77-164-113-271-113-108 0-195 36-271 113C164 317 128 404 128 512zm256 148V292l195 113L768 512l-195 113-195 113v-77zm148-113-61 36V440l61 36 61 36-61 36z + M304 464a128 128 0 01128-128c71 0 128 57 128 128v224a32 32 0 01-64 0V592h-128v95a32 32 0 01-64 0v-224zm64 1v64h128v-64a64 64 0 00-64-64c-35 0-64 29-64 64zM688 337c18 0 32 14 32 32v319a32 32 0 01-32 32c-18 0-32-14-32-32v-319a32 32 0 0132-32zM84 911l60-143A446 446 0 0164 512C64 265 265 64 512 64s448 201 448 448-201 448-448 448c-54 0-105-9-153-27l-242 22a32 32 0 01-32-44zm133-150-53 126 203-18 13 5c41 15 85 23 131 23 212 0 384-172 384-384S724 128 512 128 128 300 128 512c0 82 26 157 69 220l20 29z M296 392h64v64h-64zM296 582v160h128V582h-64v-62h-64v62zm80 48v64h-32v-64h32zM360 328h64v64h-64zM296 264h64v64h-64zM360 456h64v64h-64zM360 200h64v64h-64zM855 289 639 73c-6-6-14-9-23-9H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V311c0-9-3-17-9-23zM790 326H602V138L790 326zm2 562H232V136h64v64h64v-64h174v216c0 23 19 42 42 42h216v494z + M851 755q0 23-16 39l-78 78q-16 16-39 16t-39-16l-168-168-168 168q-16 16-39 16t-39-16l-78-78q-16-16-16-39t16-39l168-168-168-168q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 168 168-168q16-16 39-16t39 16l78 78q16 16 16 39t-16 39l-168 168 168 168q16 16 16 39z M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z + M128 384a43 43 0 0043-43V213a43 43 0 0143-43h128a43 43 0 000-85H213a128 128 0 00-128 128v128a43 43 0 0043 43zm213 469H213a43 43 0 01-43-43v-128a43 43 0 00-85 0v128a128 128 0 00128 128h128a43 43 0 000-85zm384-299a43 43 0 000-85h-49A171 171 0 00555 347V299a43 43 0 00-85 0v49A171 171 0 00347 469H299a43 43 0 000 85h49A171 171 0 00469 677V725a43 43 0 0085 0v-49A171 171 0 00677 555zm-213 43a85 85 0 1185-85 85 85 0 01-85 85zm384 43a43 43 0 00-43 43v128a43 43 0 01-43 43h-128a43 43 0 000 85h128a128 128 0 00128-128v-128a43 43 0 00-43-43zM811 85h-128a43 43 0 000 85h128a43 43 0 0143 43v128a43 43 0 0085 0V213a128 128 0 00-128-128z M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z M832 64H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V96c0-18-14-32-32-32zM736 596 624 502 506 596V131h230v318z + M509 546 780 275 871 366 509 728 147 366 238 275zM509 728h-362v128h724v-128z M757 226a143 143 0 00-55 276 96 96 0 01-88 59h-191a187 187 0 00-96 27V312a143 143 0 10-96 0v399a143 143 0 10103 2 96 96 0 0188-59h191a191 191 0 00187-151 143 143 0 00-43-279zM280 130a48 48 0 110 96 48 48 0 010-96zm0 764a48 48 0 110-96 48 48 0 010 96zM757 417a48 48 0 110-96 48 48 0 010 96z M896 128h-64V64c0-35-29-64-64-64s-64 29-64 64v64h-64c-35 0-64 29-64 64s29 64 64 64h64v64c0 35 29 64 64 64s64-29 64-64V256h64c35 0 64-29 64-64s-29-64-64-64zm-204 307C673 481 628 512 576 512H448c-47 0-90 13-128 35V372C394 346 448 275 448 192c0-106-86-192-192-192S64 86 64 192c0 83 54 154 128 180v280c-74 26-128 97-128 180c0 106 86 192 192 192s192-86 192-192c0-67-34-125-84-159c22-20 52-33 84-33h128c122 0 223-85 249-199c-19 4-37 7-57 7c-26 0-51-5-76-13zM256 128c35 0 64 29 64 64s-29 64-64 64s-64-29-64-64s29-64 64-64zm0 768c-35 0-64-29-64-64s29-64 64-64s64 29 64 64s-29 64-64 64z - M378 116l265 0 0 47-265 0 0-47ZM888 116 748 116l0 47 124 0c18 0 33 15 33 33l0 93L115 290l0-93c0-18 15-33 33-33l124 0 0-47L132 116c-35 0-64 29-64 64l0 714c0 35 29 64 64 64l757 0c35 0 64-29 64-64l-0-714C952 145 924 116 888 116zM905 337l0 540c0 18-15 33-33 33L148 910c-18 0-33-15-33-33L115 337 905 337zM301 65l47 0 0 170-47 0 0-170ZM673 65l47 0 0 170-47 0 0-170ZM358 548l0 231 53 0L411 459l-35 0-3 4c-18 26-41 49-70 68l-4 3 0 54 13-8C331 569 346 559 358 548zM618 727c-10 6-24 8-42 5-16-3-28-18-35-46l-2-9-48 13 2 8c6 30 18 52 36 65 17 13 36 20 55 21 3 0 7 0 10 0 15 0 28-2 40-7 14-6 27-13 37-23 10-10 18-22 23-37 5-14 8-28 8-42 1-14-1-27-4-39l-0-0c-3-12-8-24-15-36-7-13-19-23-35-30-15-7-31-11-47-11-11-0-23 1-36 5 4-15 8-32 11-52l114 0 0-49L536 464l-1 7c-25 116-32 145-33 150l-3 10 46 5 3-4c8-11 18-18 31-21 13-3 25-3 35-0 10 3 18 9 24 18 7 9 10 20 11 34 1 14-2 26-6 37C636 711 629 720 618 727z M512 597m-1 0a1 1 0 103 0a1 1 0 10-3 0ZM810 393 732 315 448 600 293 444 214 522l156 156 78 78 362-362z + M512 32C246 32 32 250 32 512s218 480 480 480 480-218 480-480S774 32 512 32zm269 381L496 698c-26 26-61 26-83 0L243 528c-26-26-26-61 0-83s61-26 83 0l128 128 240-240c26-26 61-26 83 0 26 19 26 54 3 80z M747 467c29 0 56 4 82 12v-363c0-47-38-84-84-84H125c-47 0-84 38-84 84v707c0 47 38 84 84 84h375a287 287 0 01-43-152c0-160 129-289 289-289zm-531-250h438c19 0 34 15 34 34s-15 34-34 34H216c-19 0-34-15-34-34s15-34 34-34zm0 179h263c19 0 34 15 34 34s-15 34-34 34H216c-19 0-34-15-34-34s15-34 34-34zm131 247h-131c-19 0-34-15-34-34s15-34 34-34h131c19 0 34 15 34 34s-15 34-34 34zM747 521c-130 0-236 106-236 236S617 992 747 992s236-106 236-236S877 521 747 521zm11 386v-65h-130c-12 0-22-10-22-22s10-22 22-22h260l-130 108zm108-192H606l130-108v65h130c12 0 22 10 22 22s-10 22-22 22z M529 511c115 0 212 79 239 185h224a62 62 0 017 123l-7 0-224 0a247 247 0 01-479 0H65a62 62 0 01-7-123l7-0h224a247 247 0 01239-185zm0 124a124 124 0 100 247 124 124 0 000-247zm0-618c32 0 58 24 61 55l0 7V206c89 11 165 45 225 103a74 74 0 0122 45l0 9v87a62 62 0 01-123 7l-0-7v-65l-6-4c-43-33-97-51-163-53l-17-0c-74 0-133 18-180 54l-6 4v65a62 62 0 01-55 61l-7 0a62 62 0 01-61-55l-0-7V362c0-20 8-39 23-53 60-58 135-92 224-103V79c0-34 28-62 62-62z + M512 926c-229 0-414-186-414-414S283 98 512 98s414 186 414 414-186 414-414 414zm0-73c189 0 341-153 341-341S701 171 512 171 171 323 171 512s153 341 341 341zm-6-192L284 439l52-52 171 171 171-171L728 439l-222 222z 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 + M591 907A85 85 0 01427 875h114a299 299 0 0050 32zM725 405c130 0 235 105 235 235s-105 235-235 235-235-105-235-235 105-235 235-235zM512 64a43 43 0 0143 43v24c126 17 229 107 264 225A298 298 0 00725 341l-4 0A235 235 0 00512 213l-5 0c-125 4-224 104-228 229l-0 6v167a211 211 0 01-26 101l-4 7-14 23h211a298 298 0 0050 85l-276-0a77 77 0 01-66-39c-13-22-14-50-2-73l2-4 22-36c10-17 16-37 17-57l0-7v-167C193 287 313 153 469 131V107a43 43 0 0139-43zm345 505L654 771a149 149 0 00202-202zM725 491a149 149 0 00-131 220l202-202A149 149 0 00725 491z 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 + M684 736 340 736l0-53 344 1-0 53zM552 565l-213-2 0-53 212 2-0 53zM684 392 340 392l0-53 344 1-0 53zM301 825c-45 0-78-9-100-27-22-18-33-43-33-75v-116c0-22-4-37-12-45-7-9-20-13-40-13v-61c19 0 32-4 40-12 8-9 12-24 12-46v-116c0-32 11-57 33-75 22-18 56-27 100-27h24v61h-24a35 35 0 00-27 12 41 41 0 00-11 29v116c0 35-10 60-31 75a66 66 0 01-31 14c11 2 22 6 31 14 20 17 31 42 31 75v116c0 12 4 22 11 29 7 8 16 12 27 12h24v61h-24zM701 764h24c10 0 19-4 27-12a41 41 0 0011-29v-116c0-33 10-58 31-75 9-7 19-12 31-14a66 66 0 01-31-14c-20-15-31-40-31-75v-116a41 41 0 00-11-29 35 35 0 00-27-12h-24v-61h24c45 0 78 9 100 27 22 18 33 43 33 75v116c0 22 4 37 11 46 8 8 21 12 40 12v61c-19 0-33 4-40 13-7 8-11 23-11 45v116c0 32-11 57-33 75-22 18-55 27-100 27h-24v-61z M128 854h768v86H128zM390 797c13 13 29 19 48 19s35-6 45-19l291-288c26-22 26-64 0-90L435 83l-61 61L426 192l-272 269c-22 22-22 64 0 90l237 246zm93-544 211 211-32 32H240l243-243zM707 694c0 48 38 86 86 86 48 0 86-38 86-86 0-22-10-45-26-61L794 576l-61 61c-13 13-26 35-26 58z - 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 + M0 512M1024 512M512 0M512 1024M796 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 M796 561a5 5 0 014 7l-39 90a5 5 0 004 7h100a5 5 0 014 8l-178 247a5 5 0 01-9-4l32-148a5 5 0 00-5-6h-89a5 5 0 01-4-7l86-191a5 5 0 014-3h88zM731 122a73 73 0 0173 73v318a54 54 0 00-8-1H731V195H244v634h408l-16 73H244a73 73 0 01-73-73V195a73 73 0 0173-73h488zm-219 366v73h-195v-73h195zm146-146v73H317v-73h341z 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 @@ -31,7 +38,7 @@ M469 235V107h85v128h-85zm-162-94 85 85-60 60-85-85 60-60zm469 60-85 85-60-60 85-85 60 60zm-549 183A85 85 0 01302 341H722a85 85 0 0174 42l131 225A85 85 0 01939 652V832a85 85 0 01-85 85H171a85 85 0 01-85-85v-180a85 85 0 0112-43l131-225zM722 427H302l-100 171h255l10 29a59 59 0 002 5c2 4 5 9 9 14 8 9 18 17 34 17 16 0 26-7 34-17a72 72 0 0011-18l0-0 10-29h255l-100-171zM853 683H624a155 155 0 01-12 17C593 722 560 747 512 747c-48 0-81-25-99-47a155 155 0 01-12-17H171v149h683v-149z M576 832C576 867 547 896 512 896 477 896 448 867 448 832 448 797 477 768 512 768 547 768 576 797 576 832ZM512 256C477 256 448 285 448 320L448 640C448 675 477 704 512 704 547 704 576 675 576 640L576 320C576 285 547 256 512 256ZM1024 896C1024 967 967 1024 896 1024L128 1024C57 1024 0 967 0 896 0 875 5 855 14 837L14 837 398 69 398 69C420 28 462 0 512 0 562 0 604 28 626 69L1008 835C1018 853 1024 874 1024 896ZM960 896C960 885 957 875 952 865L952 864 951 863 569 98C557 77 536 64 512 64 488 64 466 77 455 99L452 105 92 825 93 825 71 867C66 876 64 886 64 896 64 931 93 960 128 960L896 960C931 960 960 931 960 896Z M928 128l-416 0-32-64-352 0-64 128 896 0zM904 704l75 0 45-448-1024 0 64 640 484 0c-105-38-180-138-180-256 0-150 122-272 272-272s272 122 272 272c0 22-3 43-8 64zM1003 914l-198-175c17-29 27-63 27-99 0-106-86-192-192-192s-192 86-192 192 86 192 192 192c36 0 70-10 99-27l175 198c23 27 62 28 87 3l6-6c25-25 23-64-3-87zM640 764c-68 0-124-56-124-124s56-124 124-124 124 56 124 124-56 124-124 124z - M520 168C291 168 95 311 16 512c79 201 275 344 504 344 229 0 425-143 504-344-79-201-275-344-504-344zm0 573c-126 0-229-103-229-229s103-229 229-229c126 0 229 103 229 229s-103 229-229 229zm0-367c-76 0-137 62-137 137s62 137 137 137S657 588 657 512s-62-137-137-137z + M0 512M1024 512M512 0M512 1024M520 168C291 168 95 311 16 512c79 201 275 344 504 344 229 0 425-143 504-344-79-201-275-344-504-344zm0 573c-126 0-229-103-229-229s103-229 229-229c126 0 229 103 229 229s-103 229-229 229zm0-367c-76 0-137 62-137 137s62 137 137 137S657 588 657 512s-62-137-137-137z M734 128c-33-19-74-8-93 25l-41 70c-28-6-58-9-90-9-294 0-445 298-445 298s82 149 231 236l-31 54c-19 33-8 74 25 93 33 19 74 8 93-25L759 222C778 189 767 147 734 128zM305 512c0-115 93-208 207-208 14 0 27 1 40 4l-37 64c-1 0-2 0-2 0-77 0-140 63-140 140 0 26 7 51 20 71l-37 64C324 611 305 564 305 512zM771 301 700 423c13 27 20 57 20 89 0 110-84 200-192 208l-51 89c12 1 24 2 36 2 292 0 446-298 446-298S895 388 771 301z M826 498 538 250c-11-9-26-1-26 14v496c0 15 16 23 26 14L826 526c8-7 8-21 0-28zm-320 0L218 250c-11-9-26-1-26 14v496c0 15 16 23 26 14L506 526c4-4 6-9 6-14 0-5-2-10-6-14z M1024 896v128H0V704h128v192h768V704h128v192zM576 555 811 320 896 405l-384 384-384-384L213 320 448 555V0h128v555z @@ -41,6 +48,7 @@ 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 + M505 74c-145 3-239 68-239 68-12 8-15 25-7 37 9 13 25 15 38 6 0 0 184-136 448 2 12 7 29 3 36-10 8-13 3-29-12-37-71-38-139-56-199-63-23-3-44-3-65-3m17 111c-254-3-376 201-376 201-8 12-5 29 7 37 12 8 29 4 39-10 0 0 103-178 329-175 226 3 325 173 325 173 8 12 24 17 37 9 14-8 17-24 9-37 0 0-117-195-370-199m-31 106c-72 5-140 31-192 74C197 449 132 603 204 811c5 14 20 21 34 17 14-5 21-20 16-34-66-191-7-316 79-388 84-69 233-85 343-17 54 34 96 93 118 151 22 58 20 114 3 141-18 28-54 38-86 30-32-8-58-31-59-80-1-73-58-118-118-125-57-7-123 24-140 92-32 125 49 302 238 361 14 4 29-3 34-17 4-14-3-29-18-34-163-51-225-206-202-297 10-41 46-55 84-52 37 4 69 26 69 73 2 70 48 117 100 131 52 13 112-3 144-52 33-50 28-120 3-188-26-68-73-136-140-178a356 356 0 00-213-52m15 104v0c-76 3-152 42-195 125-56 106-31 215 7 293 38 79 90 131 90 131 10 11 27 11 38 0s11-26 0-38c0 0-46-47-79-116s-54-157-8-244c48-90 133-111 208-90 76 22 140 88 138 186-2 15 9 28 24 29 15 1 27-10 29-27 3-122-79-210-176-239a246 246 0 00-75-9m9 213c-15 0-26 13-26 27 0 0 1 63 36 124 36 61 112 119 244 107 15-1 26-13 25-28-1-15-14-26-30-25-116 11-165-33-193-81-28-47-29-98-29-98a27 27 0 00-27-27z m211 611a142 142 0 00-90-4v-190a142 142 0 0090-4v198zm0 262v150h-90v-146a142 142 0 0090-4zm0-723a142 142 0 00-90-4v-146h90zm-51 246a115 115 0 11115-115 115 115 0 01-115 115zm0 461a115 115 0 11115-115 115 115 0 01-115 115zm256-691h563v90h-563zm0 461h563v90h-563zm0-282h422v90h-422zm0 474h422v90h-422z M853 267H514c-4 0-6-2-9-4l-38-66c-13-21-38-36-64-36H171c-41 0-75 34-75 75v555c0 41 34 75 75 75h683c41 0 75-34 75-75V341c0-41-34-75-75-75zm-683-43h233c4 0 6 2 9 4l38 66c13 21 38 36 64 36H853c6 0 11 4 11 11v75h-704V235c0-6 4-11 11-11zm683 576H171c-6 0-11-4-11-11V480h704V789c0 6-4 11-11 11z M1088 227H609L453 78a11 11 0 00-7-3H107a43 43 0 00-43 43v789a43 43 0 0043 43h981a43 43 0 0043-43V270a43 43 0 00-43-43zM757 599c0 5-5 9-10 9h-113v113c0 5-4 9-9 9h-56c-5 0-9-4-9-9V608h-113c-5 0-10-4-10-9V543c0-5 5-9 10-9h113V420c0-5 4-9 9-9h56c5 0 9 4 9 9V533h113c5 0 10 4 10 9v56z @@ -49,6 +57,7 @@ M939 94v710L512 998 85 805V94h-64A21 21 0 010 73v-0C0 61 10 51 21 51h981c12 0 21 10 21 21v0c0 12-10 21-21 21h-64zm-536 588L512 624l109 58c6 3 13 4 20 3a32 32 0 0026-37l-21-122 88-87c5-5 8-11 9-18a32 32 0 00-27-37l-122-18-54-111a32 32 0 00-57 0l-54 111-122 18c-7 1-13 4-18 9a33 33 0 001 46l88 87-21 122c-1 7-0 14 3 20a32 32 0 0043 14z M236 542a32 32 0 109 63l86-12a180 180 0 0022 78l-71 47a32 32 0 1035 53l75-50a176 176 0 00166 40L326 529zM512 16C238 16 16 238 16 512s222 496 496 496 496-222 496-496S786 16 512 16zm0 896c-221 0-400-179-400-400a398 398 0 0186-247l561 561A398 398 0 01512 912zm314-154L690 622a179 179 0 004-29l85 12a32 32 0 109-63l-94-13v-49l94-13a32 32 0 10-9-63l-87 12a180 180 0 00-20-62l71-47A32 32 0 10708 252l-75 50a181 181 0 00-252 10l-115-115A398 398 0 01512 112c221 0 400 179 400 400a398 398 0 01-86 247z M884 159l-18-18a43 43 0 00-38-12l-235 43a166 166 0 00-101 60L400 349a128 128 0 00-148 47l-120 171a21 21 0 005 29l17 12a128 128 0 00178-32l27-38 124 124-38 27a128 128 0 00-32 178l12 17a21 21 0 0029 5l171-120a128 128 0 0047-148l117-92A166 166 0 00853 431l43-235a43 43 0 00-12-38zm-177 249a64 64 0 110-90 64 64 0 010 90zm-373 312a21 21 0 010 30l-139 139a21 21 0 01-30 0l-30-30a21 21 0 010-30l139-139a21 21 0 0130 0z + M525 0C235 0 0 235 0 525c0 232 150 429 359 498 26 5 36-11 36-25 0-12-1-54-1-97-146 31-176-63-176-63-23-61-58-76-58-76-48-32 3-32 3-32 53 3 81 54 81 54 47 80 123 57 153 43 4-34 18-57 33-70-116-12-239-57-239-259 0-57 21-104 54-141-5-13-23-67 5-139 0 0 44-14 144 54 42-11 87-17 131-17s90 6 131 17C756 203 801 217 801 217c29 72 10 126 5 139 34 37 54 83 54 141 0 202-123 246-240 259 19 17 36 48 36 97 0 70-1 127-1 144 0 14 10 30 36 25 209-70 359-266 359-498C1050 235 814 0 525 0z M590 74 859 342V876c0 38-31 68-68 68H233c-38 0-68-31-68-68V142c0-38 31-68 68-68h357zm-12 28H233a40 40 0 00-40 38L193 142v734a40 40 0 0038 40L233 916h558a40 40 0 0040-38L831 876V354L578 102zM855 371h-215c-46 0-83-36-84-82l0-2V74h28v213c0 30 24 54 54 55l2 0h215v28zM57 489m28 0 853 0q28 0 28 28l0 284q0 28-28 28l-853 0q-28 0-28-28l0-284q0-28 28-28ZM157 717c15 0 29-6 37-13v-51h-41v22h17v18c-2 2-6 3-10 3-21 0-30-13-30-34 0-21 12-34 28-34 9 0 15 4 20 9l14-17C184 610 172 603 156 603c-29 0-54 21-54 57 0 37 24 56 54 56zM245 711v-108h-34v108h34zm69 0v-86H341V603H262v22h28V711h24zM393 711v-108h-34v108h34zm66 6c15 0 29-6 37-13v-51h-41v22h17v18c-2 2-6 3-10 3-21 0-30-13-30-34 0-21 12-34 28-34 9 0 15 4 20 9l14-17C485 610 474 603 458 603c-29 0-54 21-54 57 0 37 24 56 54 56zm88-6v-36c0-13-2-28-3-40h1l10 24 25 52H603v-108h-23v36c0 13 2 28 3 40h-1l-10-24L548 603H523v108h23zM677 717c30 0 51-22 51-57 0-36-21-56-51-56-30 0-51 20-51 56 0 36 21 57 51 57zm3-23c-16 0-26-12-26-32 0-19 10-31 26-31 16 0 26 11 26 31S696 694 680 694zm93 17v-38h13l21 38H836l-25-43c12-5 19-15 19-31 0-26-20-34-44-34H745v108h27zm16-51H774v-34h15c16 0 25 4 25 16s-9 18-25 18zM922 711v-22h-43v-23h35v-22h-35V625h41V603H853v108h68z M30 271l241 0 0-241-241 0 0 241zM392 271l241 0 0-241-241 0 0 241zM753 30l0 241 241 0 0-241-241 0zM30 632l241 0 0-241-241 0 0 241zM392 632l241 0 0-241-241 0 0 241zM753 632l241 0 0-241-241 0 0 241zM30 994l241 0 0-241-241 0 0 241zM392 994l241 0 0-241-241 0 0 241zM753 994l241 0 0-241-241 0 0 241z M0 512M1024 512M512 0M512 1024M955 323q0 23-16 39l-414 414-78 78q-16 16-39 16t-39-16l-78-78-207-207q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 169 375-375q16-16 39-16t39 16l78 78q16 16 16 39z @@ -61,9 +70,10 @@ M412 66C326 132 271 233 271 347c0 17 1 34 4 50-41-48-98-79-162-83a444 444 0 00-46 196c0 207 142 382 337 439h2c19 0 34 15 34 33 0 11-6 21-14 26l1 14C183 973 0 763 0 511 0 272 166 70 393 7A35 35 0 01414 0c19 0 34 15 34 33a33 33 0 01-36 33zm200 893c86-66 141-168 141-282 0-17-1-34-4-50 41 48 98 79 162 83a444 444 0 0046-196c0-207-142-382-337-439h-2a33 33 0 01-34-33c0-11 6-21 14-26L596 0C841 51 1024 261 1024 513c0 239-166 441-393 504A35 35 0 01610 1024a33 33 0 01-34-33 33 33 0 0136-33zM512 704a192 192 0 110-384 192 192 0 010 384z M512 64A447 447 0 0064 512c0 248 200 448 448 448s448-200 448-448S760 64 512 64zM218 295h31c54 0 105 19 145 55 13 12 13 31 3 43a35 35 0 01-22 10 36 36 0 01-21-7 155 155 0 00-103-39h-31a32 32 0 01-31-31c0-18 13-31 30-31zm31 433h-31a32 32 0 01-31-31c0-16 13-31 31-31h31A154 154 0 00403 512 217 217 0 01620 295h75l-93-67a33 33 0 01-7-43 33 33 0 0143-7l205 148-205 148a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67H620a154 154 0 00-154 154c0 122-97 220-217 220zm390 118a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67h-75c-52 0-103-19-143-54-12-12-13-31-1-43a30 30 0 0142-3 151 151 0 00102 39h75L602 599a33 33 0 01-7-43 33 33 0 0143-7l205 148-203 151z M922 39H102A65 65 0 0039 106v609a65 65 0 0063 68h94v168a34 34 0 0019 31 30 30 0 0012 3 30 30 0 0022-10l182-192H922a65 65 0 0063-68V106A65 65 0 00922 39zM288 378h479a34 34 0 010 68H288a34 34 0 010-68zm0-135h479a34 34 0 010 68H288a34 34 0 010-68zm0 270h310a34 34 0 010 68H288a34 34 0 010-68z - M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z - M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zm-725 64h725c6 0 11 4 11 11v288h-747V192c0-6 4-11 11-11zm725 661H149c-6 0-11-4-11-11V544h747V832c0 6-4 11-11 11z + M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z 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 + M892 251c-5-11-18-18-30-18H162c-12 0-23 7-30 18-5 11-5 26 2 35l179 265v320c0 56 44 102 99 102h200c55 0 99-46 99-102v-320l179-266c9-11 9-24 4-34zm-345 540c0 18-14 35-34 35-18 0-34-14-34-35v-157c0-18 14-34 34-34 18 0 34 14 34 34v157zM512 205c18 0 34-14 34-35V87c0-20-16-35-34-35s-34 14-34 35v84c1 20 16 34 34 34zM272 179c5 18 23 30 40 24 17-6 28-24 23-42l-25-51c-5-18-23-30-40-24s-28 24-23 42L272 179zM777 127c5-18-6-36-23-42-17-6-35 5-40 24l-25 51c-5 18 6 37 23 42 17 6 35-5 40-24l25-52z + M416 192m32 0 448 0q32 0 32 32l0 0q0 32-32 32l-448 0q-32 0-32-32l0 0q0-32 32-32ZM416 448m32 0 448 0q32 0 32 32l0 0q0 32-32 32l-448 0q-32 0-32-32l0 0q0-32 32-32ZM416 704m32 0 448 0q32 0 32 32l0 0q0 32-32 32l-448 0q-32 0-32-32l0 0q0-32 32-32ZM96 320l128-192 128 192h-256zM96 640l128 192 128-192h-256zM190 320h64v320H190z 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 @@ -71,22 +81,24 @@ 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 M832 464h-68V240a128 128 0 00-128-128h-248a128 128 0 00-128 128v224H192c-18 0-32 14-32 32v384c0 18 14 32 32 32h640c18 0 32-14 32-32v-384c0-18-14-32-32-32zm-292 237v53a8 8 0 01-8 8h-40a8 8 0 01-8-8v-53a48 48 0 1156 0zm152-237H332V240a56 56 0 0156-56h248a56 56 0 0156 56v224z - M887 774 625 511l263-260c11-11 11-28 0-39l-75-75c-5-5-12-8-20-8-7 0-14 3-20 8L512 396 250 137c-5-5-12-8-20-8-7 0-14 3-20 8L136 212c-11 11-11 28 0 39l263 260L137 774c-5 5-8 12-8 20 0 7 3 14 8 20l75 75c5 5 12 8 20 8 7 0 14-3 20-8L512 626l261 262c5 5 12 8 20 8 7 0 14-3 20-8l75-75c5-5 8-12 8-20C895 786 892 779 887 774z - M1024 750v110c0 50-41 91-91 91h-841A92 92 0 010 859v-110C0 699 41 658 91 658h841c50 0 91 41 91 91z - M0 4 0 20 16 20 0 4M4 0 20 0 20 16 4 0z + M908 366h-25V248a18 18 0 00-0-2 20 20 0 00-5-13L681 7 681 7a19 19 0 00-4-3c-0-0-1-1-1-1a29 29 0 00-4-2L671 1a24 24 0 00-5-1H181a40 40 0 00-40 40v326h-25c-32 0-57 26-57 57v298c0 32 26 57 57 57h25v204c0 22 18 40 40 40H843a40 40 0 0040-40v-204h25c32 0 57-26 57-57V424a57 57 0 00-57-57zM181 40h465v205c0 11 9 20 20 20h177v101H181V40zm413 527c0 89-54 143-134 143-81 0-128-61-128-138 0-82 52-143 132-143 84 0 129 63 129 138zm-440 139V433h62v220h108v52h-170zm690 267H181v-193H843l0 193zm18-280a305 305 0 01-91 15c-50 0-86-12-111-37-25-23-39-59-38-99 0-90 66-142 155-142 35 0 62 7 76 13l-13 49c-15-6-33-12-63-12-51 0-90 29-90 88 0 56 35 89 86 89 14 0 25-2 30-4v-57h-42v-48h101v143zM397 570c0 53 25 91 66 91 42 0 65-40 65-92 0-49-23-91-66-91-42 0-66 40-66 93z M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z M824 645V307c0-56-46-102-102-102h-102V102l-154 154 154 154V307h102v338c-46 20-82 67-82 123 0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123zm-51 195c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72zM384 256c0-72-61-133-133-133-72 0-133 61-133 133 0 56 36 102 82 123v266C154 666 118 712 118 768c0 72 61 133 133 133 72 0 133-61 133-133 0-56-36-102-82-123V379C348 358 384 312 384 256zM323 768c0 41-31 72-72 72-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72zM251 328c-41 0-72-31-72-72s31-72 72-72c41 0 72 31 72 72s-31 72-72 72z M896 64H128C96 64 64 96 64 128v768c0 32 32 64 64 64h768c32 0 64-32 64-64V128c0-32-32-64-64-64z m-64 736c0 16-17 32-32 32H224c-18 0-32-12-32-32V224c0-16 16-32 32-32h576c15 0 32 16 32 32v576zM512 384c-71 0-128 57-128 128s57 128 128 128 128-57 128-128-57-128-128-128z + M0 512M1024 512M512 0M512 1024M813 448c-46 0-83 37-83 83 0 46 37 83 83 83 46 0 83-37 83-83 0-46-37-83-83-83zM211 448C165 448 128 485 128 531c0 46 37 83 83 83 46 0 83-37 83-83 0-46-37-83-83-83zM512 448c-46 0-83 37-83 83 0 46 37 83 83 83 46 0 83-37 83-83C595 485 558 448 512 448z M299 811 299 725 384 725 384 811 299 811M469 811 469 725 555 725 555 811 469 811M640 811 640 725 725 725 725 811 640 811M299 640 299 555 384 555 384 640 299 640M469 640 469 555 555 555 555 640 469 640M640 640 640 555 725 555 725 640 640 640M299 469 299 384 384 384 384 469 299 469M469 469 469 384 555 384 555 469 469 469M640 469 640 384 725 384 725 469 640 469M299 299 299 213 384 213 384 299 299 299M469 299 469 213 555 213 555 299 469 299M640 299 640 213 725 213 725 299 640 299Z M64 363l0 204 265 0L329 460c0-11 6-18 14-20C349 437 355 437 362 441c93 60 226 149 226 149 33 22 34 60 0 82 0 0-133 89-226 149-14 9-32-3-32-18l-1-110L64 693l0 117c0 41 34 75 75 75l746 0c41 0 75-34 75-74L960 364c0-0 0-1 0-1L64 363zM64 214l0 75 650 0-33-80c-16-38-62-69-103-69l-440 0C97 139 64 173 64 214z M683 409v204L1024 308 683 0v191c-413 0-427 526-427 526c117-229 203-307 427-307zm85 492H102V327h153s38-63 114-122H51c-28 0-51 27-51 61v697c0 34 23 61 51 61h768c28 0 51-27 51-61V614l-102 100v187z + M841 627A43 43 0 00811 555h-299v85h196l-183 183A43 43 0 00555 896h299v-85h-196l183-183zM299 170H213v512H85l171 171 171-171H299zM725 128h-85c-18 0-34 11-40 28l-117 313h91L606 384h154l32 85h91l-117-313A43 43 0 00725 128zm-88 171 32-85h26l32 85h-90z + M512 0a512 512 0 01512 512 57 57 0 01-114 0 398 398 0 10-398 398 57 57 0 010 114A512 512 0 01512 0zm367 600 121 120a57 57 0 01-80 81l-40-40V967a57 57 0 01-50 57l-7 0a57 57 0 01-57-57v-205l-40 40a57 57 0 01-75 5l-5-5a57 57 0 01-0-80l120-121a80 80 0 01113-0zM512 272a57 57 0 0157 57V499h114a57 57 0 0156 50L740 556a57 57 0 01-57 57H512a57 57 0 01-57-57v-228a57 57 0 0150-57L512 272z M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z M544 85c49 0 90 37 95 85h75a96 96 0 0196 89L811 267a32 32 0 01-28 32L779 299a32 32 0 01-32-28L747 267a32 32 0 00-28-32L715 235h-91a96 96 0 01-80 42H395c-33 0-62-17-80-42L224 235a32 32 0 00-32 28L192 267v576c0 16 12 30 28 32l4 0h128a32 32 0 0132 28l0 4a32 32 0 01-32 32h-128a96 96 0 01-96-89L128 843V267a96 96 0 0189-96L224 171h75a96 96 0 0195-85h150zm256 256a96 96 0 0196 89l0 7v405a96 96 0 01-89 96L800 939h-277a96 96 0 01-96-89L427 843v-405a96 96 0 0189-96L523 341h277zm-256-192H395a32 32 0 000 64h150a32 32 0 100-64z m186 532 287 0 0 287c0 11 9 20 20 20s20-9 20-20l0-287 287 0c11 0 20-9 20-20s-9-20-20-20l-287 0 0-287c0-11-9-20-20-20s-20 9-20 20l0 287-287 0c-11 0-20 9-20 20s9 20 20 20z M432 0h160c27 0 48 21 48 48v336h175c36 0 53 43 28 68L539 757c-15 15-40 15-55 0L180 452c-25-25-7-68 28-68H384V48c0-27 21-48 48-48zm592 752v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h293l98 98c40 40 105 40 145 0l98-98H976c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z M592 768h-160c-27 0-48-21-48-48V384h-175c-36 0-53-43-28-68L485 11c15-15 40-15 55 0l304 304c25 25 7 68-28 68H640v336c0 27-21 48-48 48zm432-16v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h272v16c0 62 50 112 112 112h160c62 0 112-50 112-112v-16h272c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z + M563 555c0 28-23 51-51 51-28 0-51-23-51-51L461 113c0-28 23-51 51-51s51 23 51 51L563 555 563 555zM85 535c0-153 81-287 201-362 24-15 55-8 70 16C371 214 363 245 340 260 248 318 187 419 187 535c0 180 146 325 325 325 180-0 325-146 325-325 0-119-64-223-160-280-24-14-32-46-18-70 14-24 46-32 70-18 125 74 210 211 210 367 0 236-191 427-427 427C276 963 85 772 85 535 M277 85a149 149 0 00-43 292v230a32 32 0 0064 0V555h267A160 160 0 00725 395v-12a149 149 0 10-64-5v17a96 96 0 01-96 96H299V383A149 149 0 00277 85zM228 720a32 32 0 00-37-52 150 150 0 00-53 68 32 32 0 1060 23 85 85 0 0130-39zm136-52a32 32 0 00-37 52 86 86 0 0130 39 32 32 0 1060-23 149 149 0 00-53-68zM204 833a32 32 0 10-55 32 149 149 0 0063 58 32 32 0 0028-57 85 85 0 01-36-33zm202 32a32 32 0 00-55-32 85 85 0 01-36 33 32 32 0 0028 57 149 149 0 0063-58z - M854 234a171 171 0 00-171 171v51c13-5 28-7 43-7h35a136 136 0 01136 136v93a198 198 0 01-198 198 101 101 0 01-101-101V405a256 256 0 01256-256h21a21 21 0 0121 21v43a21 21 0 01-21 21h-21zM213 456c13-5 28-7 43-7h35a136 136 0 01136 136v93a198 198 0 01-198 198 101 101 0 01-101-101V405a256 256 0 01256-256h21a21 21 0 0121 21v43a21 21 0 01-21 21h-21a171 171 0 00-171 171v51z + M467 556c0-0 0-1 0-1C467 555 467 556 467 556zM467 556c0-0 0-0 0-0C467 556 467 556 467 556zM467 556c-0 0-0 0-0 0C467 557 467 557 467 556zM468 549C468 532 468 541 468 549L468 549zM468 549c0 1-0 1-0 2C468 551 468 550 468 549zM468 552c-0 1-0 2-0 3C467 554 468 553 468 552zM736 549C736 532 736 541 736 549L736 549zM289 378l0 179 89 0c-1 80-89 89-89 89l45 45c0 0 129-15 134-134L467 378 289 378zM959 244l0 536c0 99-80 179-179 179L244 959c-99 0-179-80-179-179L65 244c0-99 80-179 179-179l536 0C879 65 959 145 959 244zM869 289c0-74-60-134-134-134L289 155c-74 0-134 60-134 134l0 447c0 74 60 134 134 134l447 0c74 0 134-60 134-134L869 289zM557 557l89 0c-1 80-89 89-89 89l45 45c0 0 129-15 134-134L735 378 557 378 557 557z m224 154a166 166 0 00-166 166v192a166 166 0 00166 166h64v-76h-64a90 90 0 01-90-90v-192a90 90 0 0190-90h320a90 90 0 0190 90v192a90 90 0 01-90 90h-128v77h128a166 166 0 00166-167v-192a166 166 0 00-166-166h-320zm166 390a90 90 0 0190-90h128v-76h-128a166 166 0 00-166 166v192a166 166 0 00166 166h320a166 166 0 00166-166v-192a166 166 0 00-166-166h-64v77h64a90 90 0 0190 90v192a90 90 0 01-90 90h-320a90 90 0 01-90-90v-192z M512 128M706 302a289 289 0 00-173 44 27 27 0 1029 46 234 234 0 01125-36c23 0 45 3 66 9 93 28 161 114 161 215C914 704 813 805 687 805H337C211 805 110 704 110 580c0-96 61-178 147-210C282 263 379 183 495 183a245 245 0 01210 119z M364 512h67v108h108v67h-108v108h-67v-108h-108v-67h108v-108zm298-64A107 107 0 01768 555C768 614 720 660 660 660h-108v-54h-108v-108h-94v108h-94c4-21 22-47 44-51l-1-12a75 75 0 0171-75a128 128 0 01239-7a106 106 0 0153-14z @@ -108,12 +120,14 @@ M558 545 790 403c24-15 31-47 16-71-15-24-46-31-70-17L507 457 277 315c-24-15-56-7-71 17-15 24-7 56 17 71l232 143V819c0 28 23 51 51 51 28 0 51-23 51-51V545h0zM507 0l443 256v512L507 1024 63 768v-512L507 0z M770 320a41 41 0 00-56-14l-252 153L207 306a41 41 0 10-43 70l255 153 2 296a41 41 0 0082 0l-2-295 255-155a41 41 0 0014-56zM481 935a42 42 0 01-42 0L105 741a42 42 0 01-21-36v-386a42 42 0 0121-36L439 89a42 42 0 0142 0l335 193a42 42 0 0121 36v87h84v-87a126 126 0 00-63-109L523 17a126 126 0 00-126 0L63 210a126 126 0 00-63 109v386a126 126 0 0063 109l335 193a126 126 0 00126 0l94-54-42-72zM1029 700h-126v-125a42 42 0 00-84 0v126h-126a42 42 0 000 84h126v126a42 42 0 1084 0v-126h126a42 42 0 000-84z M416 587c21 0 37 17 37 37v299A37 37 0 01416 960h-299a37 37 0 01-37-37v-299c0-21 17-37 37-37h299zm448 0c21 0 37 17 37 37v299A37 37 0 01864 960h-299a37 37 0 01-37-37v-299c0-21 17-37 37-37h299zM758 91l183 189a37 37 0 010 52l-182 188a37 37 0 01-53 1l-183-189a37 37 0 010-52l182-188a37 37 0 0153-1zM416 139c21 0 37 17 37 37v299A37 37 0 01416 512h-299a37 37 0 01-37-37v-299c0-21 17-37 37-37h299z + M653 435l-26 119H725c9 0 13 4 13 13v47c0 9-4 13-13 13h-107l-21 115c0 9-4 13-13 13h-47c-9 0-13-4-13-13l21-111H427l-21 115c0 9-4 13-13 13H346c-9 0-13-4-13-13l21-107h-85c-4-9-9-21-13-34v-38c0-9 4-13 13-13h98l26-119H294c-9 0-13-4-13-13V375c0-9 4-13 13-13h115l13-81c0-9 4-13 13-13h43c9 0 13 4 13 13L469 363h119l13-81c0-9 4-13 13-13h47c9 0 13 4 13 13l-13 77h85c9 0 13 4 13 13v47c0 9-4 13-13 13h-98v4zM512 0C230 0 0 230 0 512c0 145 60 282 166 375L90 1024H512c282 0 512-230 512-512S794 0 512 0zm-73 559h124l26-119h-128l-21 119z M875 128h-725A107 107 0 0043 235v555A107 107 0 00149 896h725a107 107 0 00107-107v-555A107 107 0 00875 128zm-115 640h-183v-58l25-3c15 0 19-8 14-24l-22-61H419l-28 82 39 2V768h-166v-58l18-3c18-2 22-11 26-24l125-363-40-4V256h168l160 448 39 3zM506 340l-72 218h145l-71-218h-2z M177 156c-22 5-33 17-36 37c-10 57-33 258-13 278l445 445c23 23 61 23 84 0l246-246c23-23 23-61 0-84l-445-445C437 120 231 145 177 156zM331 344c-26 26-69 26-95 0c-26-26-26-69 0-95s69-26 95 0C357 276 357 318 331 344z M683 537h-144v-142h-142V283H239a44 44 0 00-41 41v171a56 56 0 0014 34l321 321a41 41 0 0058 0l174-174a41 41 0 000-58zm-341-109a41 41 0 110-58a41 41 0 010 58zM649 284V142h-69v142h-142v68h142v142h69v-142h142v-68h-142z M996 452 572 28A96 96 0 00504 0H96C43 0 0 43 0 96v408a96 96 0 0028 68l424 424c37 37 98 37 136 0l408-408c37-37 37-98 0-136zM224 320c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96zm1028 268L844 996c-37 37-98 37-136 0l-1-1L1055 647c34-34 53-79 53-127s-19-93-53-127L663 0h97a96 96 0 0168 28l424 424c37 37 37 98 0 136z M765 118 629 239l-16 137-186 160 54 59 183-168 144 4 136-129 47-43-175-12L827 67zM489 404c-66 0-124 55-124 125s54 121 124 121c66 0 120-55 120-121H489l23-121c-8-4-16-4-23-4zM695 525c0 114-93 207-206 207s-206-94-206-207 93-207 206-207c16 0 27 0 43 4l43-207c-27-4-54-8-85-8-229 0-416 188-416 419s187 419 416 419c225 0 408-180 416-403v-12l-210-4z M144 112h736c18 0 32 14 32 32v736c0 18-14 32-32 32H144c-18 0-32-14-32-32V144c0-18 14-32 32-32zm112 211v72a9 9 0 003 7L386 509 259 615a9 9 0 00-3 7v72a9 9 0 0015 7L493 516a9 9 0 000-14l-222-186a9 9 0 00-15 7zM522 624a10 10 0 00-10 10v60a10 10 0 0010 10h237a10 10 0 0010-10v-60a10 10 0 00-10-10H522z + M170 831 513 489 855 831 960 726 512 278 64 726 170 831zM512 278h448v-128h-896v128h448z M897 673v13c0 51-42 93-93 93h-10c-1 0-2 0-2 0H220c-23 0-42 19-42 42v13c0 23 19 42 42 42h552c14 0 26 12 26 26 0 14-12 26-26 26H220c-51 0-93-42-93-93v-13c0-51 42-93 93-93h20c1-0 2-0 2-0h562c23 0 42-19 42-42v-13c0-11-5-22-13-29-8-7-17-11-28-10H660c-14 0-26-12-26-26 0-14 12-26 26-26h144c24-1 47 7 65 24 18 17 29 42 29 67zM479 98c-112 0-203 91-203 203 0 44 14 85 38 118l132 208c15 24 50 24 66 0l133-209c23-33 37-73 37-117 0-112-91-203-203-203zm0 327c-68 0-122-55-122-122s55-122 122-122 122 55 122 122-55 122-122 122z M912 800a48 48 0 1 1 0 96h-416a48 48 0 1 1 0-96h416z m-704-704A112 112 0 0 1 256 309.184V480h80a48 48 0 0 1 0 96H256v224h81.664a48 48 0 1 1 0 96H256a96 96 0 0 1-96-96V309.248A112 112 0 0 1 208 96z m704 384a48 48 0 1 1 0 96h-416a48 48 0 0 1 0-96h416z m0-320a48 48 0 1 1 0 96h-416a48 48 0 0 1 0-96h416z M30 0 30 30 0 15z @@ -121,6 +135,8 @@ M762 1024C876 818 895 504 448 514V768L64 384l384-384v248c535-14 595 472 314 776z M832 464H332V240c0-31 25-56 56-56h248c31 0 56 25 56 56v68c0 4 4 8 8 8h56c4 0 8-4 8-8v-68c0-71-57-128-128-128H388c-71 0-128 57-128 128v224h-68c-18 0-32 14-32 32v384c0 18 14 32 32 32h640c18 0 32-14 32-32V496c0-18-14-32-32-32zM540 701v53c0 4-4 8-8 8h-40c-4 0-8-4-8-8v-53c-12-9-20-23-20-39 0-27 22-48 48-48s48 22 48 48c0 16-8 30-20 39z M170 831l343-342L855 831l105-105-448-448L64 726 170 831z + M667 607c-3-2-7-14-0-38 73-77 118-187 118-290C784 115 668 0 508 0 348 0 236 114 236 278c0 104 45 215 119 292 7 24-2 33-8 35C274 631 0 725 0 854L0 1024l1024 0 0-192C989 714 730 627 667 607L667 607z + M880 128A722 722 0 01555 13a77 77 0 00-85 0 719 719 0 01-325 115c-40 4-71 38-71 80v369c0 246 329 446 439 446 110 0 439-200 439-446V207c0-41-31-76-71-80zM465 692a36 36 0 01-53 0L305 579a42 42 0 010-57 36 36 0 0153 0l80 85L678 353a36 36 0 0153 0 42 42 0 01-0 57L465 692z M812 864h-29V654c0-21-11-40-28-52l-133-88 134-89c18-12 28-31 28-52V164h28c18 0 32-14 32-32s-14-32-32-32H212c-18 0-32 14-32 32s14 32 32 32h30v210c0 21 11 40 28 52l133 88-134 89c-18 12-28 31-28 52V864H212c-18 0-32 14-32 32s14 32 32 32h600c18 0 32-14 32-32s-14-32-32-32zM441 566c18-12 28-31 28-52s-11-40-28-52L306 373V164h414v209l-136 90c-18 12-28 31-28 52 0 21 11 40 28 52l135 89V695c-9-7-20-13-32-19-30-15-93-41-176-41-63 0-125 14-175 38-12 6-22 12-31 18v-36l136-90z M0 512M1024 512M512 0M512 1024M762 412v100h-500v-100h-150v200h800v-200h-150z M519 459 222 162a37 37 0 10-52 52l297 297L169 809a37 37 0 1052 52l297-297 297 297a37 37 0 1052-52l-297-297 297-297a37 37 0 10-52-52L519 459z diff --git a/src/Resources/Images/ExternalToolIcons/plastic_merge.png b/src/Resources/Images/ExternalToolIcons/plastic_merge.png new file mode 100644 index 00000000..0d82fc86 Binary files /dev/null and b/src/Resources/Images/ExternalToolIcons/plastic_merge.png differ diff --git a/src/Resources/Images/ShellIcons/ghostty.png b/src/Resources/Images/ShellIcons/ghostty.png new file mode 100644 index 00000000..e394a517 Binary files /dev/null and b/src/Resources/Images/ShellIcons/ghostty.png differ diff --git a/src/Resources/Images/ShellIcons/kitty.png b/src/Resources/Images/ShellIcons/kitty.png new file mode 100644 index 00000000..465c2863 Binary files /dev/null and b/src/Resources/Images/ShellIcons/kitty.png differ diff --git a/src/Resources/Images/ShellIcons/ptyxis.png b/src/Resources/Images/ShellIcons/ptyxis.png new file mode 100644 index 00000000..9202f6e1 Binary files /dev/null and b/src/Resources/Images/ShellIcons/ptyxis.png differ diff --git a/src/Resources/Images/ShellIcons/warp.png b/src/Resources/Images/ShellIcons/warp.png new file mode 100644 index 00000000..7d604d8e Binary files /dev/null and b/src/Resources/Images/ShellIcons/warp.png differ diff --git a/src/Resources/Images/github.png b/src/Resources/Images/github.png index 3a7abb16..d3c211da 100644 Binary files a/src/Resources/Images/github.png and b/src/Resources/Images/github.png differ diff --git a/src/Resources/Images/unreal.png b/src/Resources/Images/unreal.png new file mode 100644 index 00000000..01ceeb31 Binary files /dev/null and b/src/Resources/Images/unreal.png differ diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 7cc9fd63..032e5ce7 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -2,41 +2,34 @@ + Info Über SourceGit - • Erstellt mit - • Grafik gerendert durch - © 2024 sourcegit-scm - • Texteditor von - • Monospace-Schriftarten von - • Quelltext findest du auf Open Source & freier Git GUI Client Worktree hinzufügen - Was auschecken: - Existierender Branch - Neuen Branch erstellen Ordner: Pfad für diesen Worktree. Relativer Pfad wird unterstützt. Branch Name: Optional. Standard ist der Zielordnername. Branch verfolgen: Remote-Branch verfolgen + Was auschecken: + Neuen Branch erstellen + Existierender Branch OpenAI Assistent + Neu generieren Verwende OpenAI, um Commit-Nachrichten zu generieren + Als Commit-Nachricht verwenden Patch - Fehler - Fehler werfen und anwenden des Patches verweigern - Alle Fehler - Ähnlich wie 'Fehler', zeigt aber mehr an Patch-Datei: Wähle die anzuwendende .patch-Datei Ignoriere Leerzeichenänderungen - Keine Warnungen - Keine Warnung vor Leerzeichen am Zeilenende Patch anwenden - Warnen - Gibt eine Warnung für ein paar solcher Fehler aus, aber wendet es an Leerzeichen: + Stash anwenden + Nach dem Anwenden löschen + Änderungen des Index wiederherstellen + Stash: Archivieren... Speichere Archiv in: Wähle Archivpfad aus @@ -46,45 +39,55 @@ ALS UNVERÄNDERT ANGENOMMENE DATEIEN KEINE ALS UNVERÄNDERT ANGENOMMENEN DATEIEN ENTFERNEN + Aktualisieren BINÄRE DATEI NICHT UNTERSTÜTZT!!! + Bisect + Abbrechen + Schlecht + Bisecting. Ist der aktuelle HEAD gut oder fehlerhaft? + Gut + Überspringen + Bisecting. Aktuellen Commit als gut oder schlecht markieren und einen anderen auschecken. Blame BLAME WIRD BEI DIESER DATEI NICHT UNTERSTÜTZT!!! - Auscheken von ${0}$... - Mit Branch vergleichen - Mit HEAD vergleichen + Auschecken von ${0}$... + Mit ${0}$ vergleichen Mit Worktree vergleichen Branch-Namen kopieren + Benutzerdefinierte Aktion Lösche ${0}$... Lösche alle ausgewählten {0} Branches - Alle Änderungen verwerfen Fast-Forward zu ${0}$ + Fetche ${0}$ in ${1}$ hinein... Git Flow - Abschließen ${0}$ Merge ${0}$ in ${1}$ hinein... + Merge ausgewählte {0} Branches in aktuellen hinein Pull ${0}$ Pull ${0}$ in ${1}$ hinein... Push ${0}$ Rebase ${0}$ auf ${1}$... Benenne ${0}$ um... - Setze verfolgten Branch - Upstream Verfolgung aufheben + Setze verfolgten Branch... Branch Vergleich + Ungültiger upstream! Bytes ABBRECHEN - Auf diese Revision zurücksetzen Auf Vorgänger-Revision zurücksetzen + Auf diese Revision zurücksetzen + Generiere Commit-Nachricht ANZEIGE MODUS ÄNDERN Zeige als Datei- und Ordnerliste Zeige als Pfadliste Zeige als Dateisystembaum Branch auschecken Commit auschecken - Warnung: Beim Auschecken eines Commits wird dein HEAD losgelöst (detached) sein! Commit: - Branch: + Warnung: Beim Auschecken eines Commits wird dein HEAD losgelöst (detached) sein! Lokale Änderungen: Verwerfen - Nichts tun Stashen & wieder anwenden + Alle Submodule updaten + Branch: Cherry Pick Quelle an Commit-Nachricht anhängen Commit(s): @@ -99,17 +102,24 @@ Lokaler Name: Repository-Name. Optional. Übergeordneter Ordner: + Submodule initialisieren und aktualisieren Repository URL: SCHLIESSEN Editor + Commit auschecken Diesen Commit cherry-picken Mehrere cherry-picken - Commit auschecken Mit HEAD vergleichen Mit Worktree vergleichen - Info kopieren - SHA kopieren + Author + Committer + Information + SHA + Betreff + Benutzerdefinierte Aktion Interactives Rebase von ${0}$ auf diesen Commit + Merge in ${0}$ hinein + Merge ... Rebase von ${0}$ auf diesen Commit Reset ${0}$ auf diesen Commit Commit rückgängig machen @@ -121,10 +131,12 @@ Änderungen durchsuchen... DATEIEN LFS DATEI + Dateien durchsuchen... Submodule INFORMATION AUTOR GEÄNDERT + NACHFOLGER COMMITTER Prüfe Refs, die diesen Commit enthalten COMMIT ENTHALTEN IN @@ -134,47 +146,78 @@ REFS SHA Im Browser öffnen - Commit-Nachricht Details + Betreff + Commit-Nachricht Repository Einstellungen COMMIT TEMPLATE - Template Name: Template Inhalt: + Template Name: + BENUTZERDEFINIERTE AKTION + Argumente: + ${REPO} - Repository Pfad; ${SHA} - SHA-Wert des selektierten Commits + Ausführbare Datei: + Name: + Geltungsbereich: + Branch + Commit + Repository + Auf Beenden der Aktion warten Email Adresse Email Adresse GIT Remotes automatisch fetchen Minute(n) Standard Remote + Bevorzugter Merge Modus TICKETSYSTEM + Beispiel Azure DevOps Rule hinzufügen + Beispiel für Gitee Issue Regel einfügen + Beispiel für Gitee Pull Request Regel einfügen Beispiel für Github-Regel hinzufügen + Beispiel für Gitlab Issue Regel einfügen + Beispiel für Gitlab Merge Request einfügen Beispiel für Jira-Regel hinzufügen Neue Regel Ticketnummer Regex-Ausdruck: Name: Ergebnis-URL: Verwende bitte $1, $2 um auf Regex-Gruppenwerte zuzugreifen. + OPEN AI + Bevorzugter Service: + Der ausgewählte 'Bevorzugte Service' wird nur in diesem Repository gesetzt und verwendet. Wenn keiner gesetzt ist und mehrere Servies verfügbar sind wird ein Kontextmenü zur Auswahl angezeigt. HTTP Proxy HTTP Proxy für dieses Repository Benutzername Benutzername für dieses Repository Arbeitsplätze Farbe + Name Zuletzt geöffnete Tabs beim Starten wiederherstellen + WEITER + Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty)? + ALLES STAGEN & COMMITTEN + Leerer Commit erkannt! Möchtest du trotzdem fortfahren (--allow-empty) oder alle Änderungen stagen und dann committen? + Konventionelle Commit-Hilfe + Breaking Change: + Geschlossenes Ticket: + Änderungen im Detail: + Gültigkeitsbereich: + Kurzbeschreibung: + Typ der Änderung: Kopieren Kopiere gesamten Text - COMMIT-NACHRICHT KOPIEREN + Ganzen Pfad kopieren Pfad kopieren - Dateinamen kopieren Branch erstellen... Basierend auf: Erstellten Branch auschecken Lokale Änderungen: Verwerfen - Nichts tun Stashen & wieder anwenden Neuer Branch-Name: Branch-Namen eingeben. + Leerzeichen werden durch Bindestriche ersetzt. Lokalen Branch erstellen Tag erstellen... Neuer Tag auf: @@ -198,7 +241,10 @@ Du versuchst mehrere Branches auf einmal zu löschen. Kontrolliere noch einmal vor dem Fortfahren! Remote löschen Remote: + Pfad: Ziel: + Alle Nachfolger werden aus der Liste entfernt. + Dadurch wird es nur aus der Liste entfernt, nicht von der Festplatte! Bestätige löschen von Gruppe Bestätige löschen von Repository Lösche Submodul @@ -211,11 +257,14 @@ ALT Kopieren Dateimodus geändert + Erste Differenz Ignoriere Leerzeichenänderungen + Letzte Differenz LFS OBJEKT ÄNDERUNG Nächste Änderung KEINE ÄNDERUNG ODER NUR ZEILEN-ENDE ÄNDERUNGEN Vorherige Änderung + Als Patch speichern Zeige versteckte Symbole Nebeneinander SUBMODUL @@ -223,7 +272,9 @@ Seiten wechseln Syntax Hervorhebung Zeilenumbruch + Aktiviere Block-Navigation Öffne in Merge Tool + Alle Zeilen anzeigen Weniger Zeilen anzeigen Mehr Zeilen anzeigen WÄHLE EINE DATEI AUS UM ÄNDERUNGEN ANZUZEIGEN @@ -239,11 +290,12 @@ Ziel: Ausgewählte Gruppe bearbeiten Ausgewähltes Repository bearbeiten - Fast-Forward (ohne Auschecken) + Führe benutzerdefinierte Aktion aus + Name der Aktion: Fetch Alle Remotes fetchen + Aktiviere '--force' Option Ohne Tags fetchen - Alle verwaisten Branches entfernen Remote: Remote-Änderungen fetchen Als unverändert annehmen @@ -251,6 +303,7 @@ Verwerfe {0} Dateien... Verwerfe Änderungen in ausgewählten Zeilen Öffne externes Merge Tool + Löse mit ${0}$ Als Patch speichern... Stagen {0} Dateien stagen @@ -260,12 +313,11 @@ Unstage {0} Dateien unstagen Änderungen in ausgewählten Zeilen unstagen - "Ihre" verwenden (checkout --theirs) "Meine" verwenden (checkout --ours) + "Ihre" verwenden (checkout --theirs) Datei Historie - INHALT ÄNDERUNGEN - FILTER + INHALT Git-Flow Development-Branch: Feature: @@ -295,8 +347,8 @@ Eigenes Muster: Verfolgungsmuster zu Git LFS hinzufügen Fetch - LFS Objekte fetchen Führt `git lfs fetch` aus um Git LFS Objekte herunterzuladen. Das aktualisiert nicht die Arbeitskopie. + LFS Objekte fetchen Installiere Git LFS Hooks Sperren anzeigen Keine gesperrten Dateien @@ -308,39 +360,46 @@ Prune Führt `git lfs prune` aus um alte LFS Dateien von lokalem Speicher zu löschen Pull - LFS Objekte pullen Führt `git lfs pull` aus um alle Git LFS Dasteien für aktuellen Ref & Checkout herunterzuladen + LFS Objekte pullen Push - LFS Objekte pushen Pushe große Dateien in der Warteschlange zum Git LFS Endpunkt + LFS Objekte pushen Remote: Verfolge alle '{0}' Dateien Verfolge alle *{0} Dateien Verlauf - Wechsle zwischen horizontalem und vertikalem Layout AUTOR AUTOR ZEITPUNKT GRAPH & COMMIT-NACHRICHT SHA COMMIT ZEITPUNKT {0} COMMITS AUSGEWÄHLT + Halte 'Strg' oder 'Umschalt', um mehrere Commits auszuwählen. + Halte ⌘ oder ⇧, um mehrere Commits auszuwählen + TIPPS: Tastaturkürzel Referenz GLOBAL Aktuelles Popup schließen + Klone neues Repository Aktuellen Tab schließen - Zum vorherigen Tab wechseln Zum nächsten Tab wechseln + Zum vorherigen Tab wechseln Neuen Tab erstellen - Einstellungen öffnen + Einstellungen öffnen REPOSITORY Gestagte Änderungen committen Gestagte Änderungen committen und pushen Alle Änderungen stagen und committen + Neuen Branch basierend auf ausgewählten Commit erstellen Ausgewählte Änderungen verwerfen + Fetch, wird direkt ausgeführt Dashboard Modus (Standard) + Commit-Suchmodus + Pull, wird direkt ausgeführt + Push, wird direkt ausgeführt Erzwinge Neuladen des Repositorys Ausgewählte Änderungen stagen/unstagen - Commit-Suchmodus Wechsle zu 'Änderungen' Wechsle zu 'Verlauf' Wechsle zu 'Stashes' @@ -349,24 +408,34 @@ Suche nächste Übereinstimmung Suche vorherige Übereinstimmung Öffne Suchpanel + Verwerfen Stagen Unstagen - Verwerfen Initialisiere Repository Pfad: - Cherry-Pick wird durchgeführt. Drücke 'Abbrechen' um den originalen HEAD wiederherzustellen. - Merge request wird durchgeführt. Drücke 'Abbrechen' um den originalen HEAD wiederherzustellen. - Rebase wird durchgeführt. Drücke 'Abbrechen' um den originalen HEAD wiederherzustellen. - Revert wird durchgeführt. Drücke 'Abbrechen' um den originalen HEAD wiederherzustellen. + Cherry-Pick wird durchgeführt. + Verarbeite commit + Merge request wird durchgeführt. + Verarbeite + Rebase wird durchgeführt. + Angehalten bei + Revert wird durchgeführt. + Reverte commit Interaktiver Rebase - Ziel Branch: Auf: + Ziel Branch: + Link kopieren + In Browser öffnen FEHLER INFO Branch mergen Ziel-Branch: Merge Option: - Quell-Branch: + Quelle: + Merge (mehrere) + Alle Änderungen committen + Strategie: + Ziele: Bewege Repository Knoten Wähle Vorgänger-Knoten für: Name: @@ -382,74 +451,85 @@ Kopiere Repository-Pfad Repositories Einfügen - Gerade eben - Vor {0} Minuten - Vor {0} Stunden - Gestern Vor {0} Tagen + Vor 1 Stunde + Vor {0} Stunden + Gerade eben Letzter Monat - Vor {0} Monaten Leztes Jahr + Vor {0} Minuten + Vor {0} Monaten Vor {0} Jahren - Einstellungen - OPEN AI - Server - API Schlüssel - Modell - DARSTELLUNG - Standardschriftart - Standardschriftgröße - Monospace-Schriftart - Verwende die Monospace-Schriftart nur im Texteditor - Design - Design-Anpassungen - Fixe Tab-Breite in Titelleiste - Verwende nativen Fensterrahmen - DIFF/MERGE TOOL - Installationspfad - Installationspfad zum Diff/Merge Tool - Tool - ALLGEMEIN - Beim Starten nach Updates suchen - Sprache - Commit-Historie - Zeige Autor Zeitpunkt anstatt Commit Zeitpunkt - Längenvorgabe für Commit-Nachrichten - GIT - Aktiviere Auto-CRLF - Clone Standardordner - Benutzer Email - Globale Git Benutzer Email - Installationspfad - Benutzername - Globaler Git Benutzername - Git Version - Diese App setzt Git (>= 2.23.0) voraus - GPG SIGNIERUNG - Commit-Signierung - Tag-Signierung - GPG Format - GPG Installationspfad - Installationspfad zum GPG Programm - Benutzer Signierungsschlüssel - GPG Benutzer Signierungsschlüssel - EINBINDUNGEN - SHELL/TERMINAL - Shell/Terminal - Pfad + Gestern + Einstellungen + OPEN AI + Analysierung des Diff Befehl + API Schlüssel + Generiere Nachricht Befehl + Modell + Name + Server + Streaming aktivieren + DARSTELLUNG + Standardschriftart + Editor Tab Breite + Schriftgröße + Standard + Texteditor + Monospace-Schriftart + Verwende die Monospace-Schriftart nur im Texteditor + Design + Design-Anpassungen + Fixe Tab-Breite in Titelleiste + Verwende nativen Fensterrahmen + DIFF/MERGE TOOL + Installationspfad + Installationspfad zum Diff/Merge Tool + Tool + ALLGEMEIN + Beim Starten nach Updates suchen + Datumsformat + Sprache + Commit-Historie + Zeige Autor Zeitpunkt anstatt Commit Zeitpunkt + Zeige Nachfolger in den Commit Details + Zeige Tags im Commit Graph + Längenvorgabe für Commit-Nachrichten + GIT + Aktiviere Auto-CRLF + Klon Standardordner + Benutzer Email + Globale Git Benutzer Email + Aktivere --prune beim fetchen + Aktiviere --ignore-cr-at-eol beim Unterschied + Diese App setzt Git (>= 2.25.1) voraus + Installationspfad + Aktiviere HTTP SSL Verifizierung + Benutzername + Globaler Git Benutzername + Git Version + GPG SIGNIERUNG + Commit-Signierung + GPG Format + GPG Installationspfad + Installationspfad zum GPG Programm + Tag-Signierung + Benutzer Signierungsschlüssel + GPG Benutzer Signierungsschlüssel + EINBINDUNGEN + SHELL/TERMINAL + Pfad + Shell/Terminal Remote löschen Ziel: Worktrees löschen - Worktree Informationen in `$GIT_DIR/worktrees` löschen + Worktree Informationen in `$GIT_COMMON_DIR/worktrees` löschen Pull Remote-Branch: - Alle Branches fetchen Lokaler Branch: Lokale Änderungen: Verwerfen - Nichts tun Stashen & wieder anwenden - Ohne Tags fetchen Remote: Pull (Fetch & Merge) Rebase anstatt Merge verwenden @@ -471,7 +551,6 @@ Lokale Änderungen stashen & wieder anwenden Auf: Rebase: - Aktualisieren Remote hinzufügen Remote bearbeiten Name: @@ -484,7 +563,6 @@ Fetch Im Browser öffnen Prune - Ziel: Bestätige das entfernen des Worktrees Aktiviere `--force` Option Ziel: @@ -494,38 +572,64 @@ Branch: ABBRECHEN Änderungen automatisch von Remote fetchen... + Sortieren + Nach Commit Datum + Nach Name Aufräumen (GC & Prune) Führt `git gc` auf diesem Repository aus. - Alles löschen + Filter aufheben Repository Einstellungen WEITER + Benutzerdefinierte Aktionen + Keine benutzerdefinierten Aktionen + Alle Änderungen verwerfen + Aktiviere '--reflog' Option Öffne im Datei-Browser Suche Branches/Tags/Submodule - GEFILTERT: + Sichtbarkeit im Graphen + Aufheben + Im Graph ausblenden + Im Graph filtern + Aktiviere '--first-parent' Option + LAYOUT + Horizontal + Vertikal + COMMIT SORTIERUNG + Commit Zeitpunkt + Topologie LOKALE BRANCHES Zum HEAD wechseln - Aktiviere '--first-parent' Option Erstelle Branch + BENACHRICHTIGUNGEN LÖSCHEN + Nur aktuellen Branch im Graphen hervorheben Öffne in {0} Öffne in externen Tools Aktualisiern REMOTES REMOTE HINZUFÜGEN - KONFLIKTE BEHEBEN Commit suchen + Autor + Committer + Inhalt Dateiname Commit-Nachricht SHA - Autor & Committer Aktueller Branch Zeige Tags als Baum + ÜBERSPRINGEN Statistiken SUBMODULE SUBMODUL HINZUFÜGEN SUBMODUL AKTUALISIEREN TAGS NEUER TAG + Nach Erstellungsdatum + Nach Namen + Sortiere Öffne im Terminal + Verwende relative Zeitangaben in Verlauf + Logs ansehen + Öffne '{0}' im Browser WORKTREES WORKTREE HINZUFÜGEN PRUNE @@ -553,13 +657,22 @@ Diese Version überspringen Software Update Es sind momentan kein Updates verfügbar. + Setze verfolgten Branch + Branch: + Upstream Verfolgung aufheben + Upstream: + SHA kopieren + Zum Commit wechseln Squash Commits In: SSH privater Schlüssel: Pfad zum privaten SSH Schlüssel START Stash + Automatisch wiederherstellen nach dem Stashen + Die Arbeitsdateien bleiben unverändert, aber ein Stash wird gespeichert. Inklusive nicht-verfolgter Dateien + Behalte gestagte Dateien Name: Optional. Name dieses Stashes Nur gestagte Änderungen @@ -567,7 +680,7 @@ Lokale Änderungen stashen Anwenden Entfernen - Anwenden und entfernen + Als Path speichern... Stash entfernen Entfernen: Stashes @@ -576,11 +689,11 @@ Statistiken COMMITS COMMITTER + ÜBERSICHT MONAT WOCHE - COMMITS: AUTOREN: - ÜBERSICHT + COMMITS: SUBMODULE Submodul hinzufügen Relativen Pfad kopieren @@ -591,15 +704,21 @@ Submodul löschen OK Tag-Namen kopieren + Tag-Nachricht kopieren Lösche ${0}$... + Merge ${0}$ in ${1}$ hinein... Pushe ${0}$... - URL: Submodule aktualisieren Alle Submodule Initialisiere wenn nötig Rekursiv Submodul: Verwende `--remote` Option + URL: + Logs + ALLES LÖSCHEN + Kopieren + Löschen Warnung Willkommensseite Erstelle Gruppe @@ -612,7 +731,7 @@ Öffne alle Repositories Öffne Repository Öffne Terminal - Clone Standardordner erneut nach Repositories durchsuchen + Klon Standardordner erneut nach Repositories durchsuchen Suche Repositories... Sortieren Änderungen @@ -622,27 +741,33 @@ Ignoriere Dateien im selben Ordner Ignoriere nur diese Datei Amend - Auto-Stage Du kannst diese Datei jetzt stagen. COMMIT COMMIT & PUSH Template/Historie Klick-Ereignis auslösen + Commit (Bearbeitung) Alle Änderungen stagen und committen + Du hast {0} Datei(en) gestaged, aber nur {1} werden angezeigt ({2} sind herausgefiltert). Willst du trotzdem fortfahren? KONFLIKTE ERKANNT + EXTERNES MERGE-TOOL ÖFFNEN + ALLE KONFLIKTE IN EXTERNEM MERGE-TOOL ÖFFNEN DATEI KONFLIKTE GELÖST + MEINE VERSION VERWENDEN + DEREN VERSION VERWENDEN NICHT-VERFOLGTE DATEIEN INKLUDIEREN KEINE BISHERIGEN COMMIT-NACHRICHTEN KEINE COMMIT TEMPLATES + Rechtsklick auf selektierte Dateien und wähle die Konfliktlösungen aus. + SignOff GESTAGED UNSTAGEN ALLES UNSTAGEN UNSTAGED STAGEN ALLES STAGEN - ALS UNVERÄNDERT ANGENOMMENE ANZEIGEN + ALS UNVERÄNDERT ANGENOMMENE ANZEIGEN Template: ${0}$ - Rechtsklick auf selektierte Dateien und wähle die Konfliktlösungen aus. ARBEITSPLATZ: Arbeitsplätze konfigurieren... WORKTREE diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index bfb0ace4..ee035551 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1,39 +1,31 @@ About About SourceGit - • Build with - • Chart is rendered by - © 2024 sourcegit-scm - • TextEditor from - • Monospace fonts come from - • Source code can be found at Opensource & Free Git GUI Client Add Worktree - What to Checkout: - Existing Branch - Create New Branch Location: Path for this worktree. Relative path is supported. Branch Name: Optional. Default is the destination folder name. Track Branch: Tracking remote branch - OpenAI Assistant - Use OpenAI to generate commit message + What to Checkout: + Create New Branch + Existing Branch + AI Assistant + RE-GENERATE + Use AI to generate commit message + APPLY AS COMMIT MESSAGE Patch - Error - Raise errors and refuses to apply the patch - Error All - Similar to 'error', but shows more Patch File: Select .patch file to apply Ignore whitespace changes - No Warn - Turns off the trailing whitespace warning Apply Patch - Warn - Outputs warnings for a few such errors, but applies Whitespace: + Apply Stash + Delete after applying + Reinstate the index's changes + Stash: Archive... Save Archive To: Select archive file path @@ -43,45 +35,59 @@ FILES ASSUME UNCHANGED NO FILES ASSUMED AS UNCHANGED REMOVE + Load Image... + Refresh BINARY FILE NOT SUPPORTED!!! + Bisect + Abort + Bad + Bisecting. Is current HEAD good or bad? + Good + Skip + Bisecting. Mark current commit as good or bad and checkout another one. Blame BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout ${0}$... - Compare with Branch - Compare with HEAD + Compare with ${0}$ Compare with Worktree Copy Branch Name + Custom Action Delete ${0}$... Delete selected {0} branches - Discard all changes Fast-Forward to ${0}$ + Fetch ${0}$ into ${1}$... Git Flow - Finish ${0}$ Merge ${0}$ into ${1}$... + Merge selected {0} branches into current Pull ${0}$ Pull ${0}$ into ${1}$... Push ${0}$ Rebase ${0}$ on ${1}$... Rename ${0}$... - Set Tracking Branch - Unset Upstream + Reset ${0}$ to ${1}$... + Set Tracking Branch... Branch Compare + Invalid upstream! Bytes CANCEL - Reset to This Revision Reset to Parent Revision + Reset to This Revision + Generate commit message CHANGE DISPLAY MODE Show as File and Dir List Show as Path List Show as Filesystem Tree Checkout Branch Checkout Commit - Warning: By doing a commit checkout, your Head will be detached Commit: - Branch: + Warning: By doing a commit checkout, your Head will be detached Local Changes: Discard - Do Nothing Stash & Reapply + Update all submodules + Branch: + Checkout & Fast-Forward + Fast-Forward to: Cherry Pick Append source to commit message Commit(s): @@ -96,32 +102,42 @@ Local Name: Repository name. Optional. Parent Folder: + Initialize & update submodules Repository URL: CLOSE Editor - Cherry-Pick This Commit - Cherry-Pick ... Checkout Commit + Cherry-Pick Commit + Cherry-Pick ... Compare with HEAD Compare with Worktree - Copy Info - Copy SHA - Interactive Rebase ${0}$ to Here - Rebase ${0}$ to Here + Author + Committer + Information + SHA + Subject + Custom Action + Interactively Rebase ${0}$ on Here + Merge to ${0}$ + Merge ... + Rebase ${0}$ on Here Reset ${0}$ to Here Revert Commit Reword Save as Patch... - Squash Into Parent - Squash Child Commits to Here + Squash into Parent + Squash Children into Here CHANGES + changed file(s) Search Changes... FILES LFS File + Search Files... Submodule INFORMATION AUTHOR CHANGED + CHILDREN COMMITTER Check refs that contains this commit COMMIT IS CONTAINED BY @@ -131,33 +147,58 @@ REFS SHA Open in Browser - Enter commit subject Description + SUBJECT + Enter commit subject Repository Configure COMMIT TEMPLATE - Template Name: Template Content: + Template Name: + CUSTOM ACTION + Arguments: + ${REPO} - Repository's path; ${BRANCH} - Selected branch; ${SHA} - Selected commit's SHA + Executable File: + Name: + Scope: + Branch + Commit + Repository + Wait for action exit Email Address Email address GIT Fetch remotes automatically Minute(s) Default Remote + Preferred Merge Mode ISSUE TRACKER + Add Sample Azure DevOps Rule + Add Sample Gitee Issue Rule + Add Sample Gitee Pull Request Rule Add Sample Github Rule + Add Sample GitLab Issue Rule + Add Sample GitLab Merge Request Rule Add Sample Jira Rule New Rule Issue Regex Expression: Rule Name: Result URL: Please use $1, $2 to access regex groups values. + AI + Preferred Service: + If the 'Preferred Service' is set, SourceGit will only use it in this repository. Otherwise, if there is more than one service available, a context menu to choose one of them will be shown. HTTP Proxy HTTP proxy used by this repository User Name User name for this repository Workspaces Color + Name Restore tabs on startup + CONTINUE + Empty commit detected! Do you want to continue (--allow-empty)? + STAGE ALL & COMMIT + Empty commit detected! Do you want to continue (--allow-empty) or stage all then commit? Conventional Commit Helper Breaking Change: Closed Issue: @@ -167,19 +208,19 @@ Type of Change: Copy Copy All Text - COPY MESSAGE + Copy Full Path Copy Path - Copy File Name Create Branch... Based On: Check out the created branch Local Changes: Discard - Do Nothing Stash & Reapply New Branch Name: Enter branch name. + Spaces will be replaced with dashes. Create Local Branch + Overwrite existing branch Create Tag... New Tag At: GPG signing @@ -194,6 +235,9 @@ lightweight Hold Ctrl to start directly Cut + De-initialize Submodule + Force de-init even if it contains local changes. + Submodule: Delete Branch Branch: You are about to delete a remote branch!!! @@ -202,7 +246,10 @@ You are trying to delete multiple branches at one time. Be sure to double-check before taking action! Delete Remote Remote: + Path: Target: + All children will be removed from list. + This will only remove it from list, not from disk! Confirm Deleting Group Confirm Deleting Repository Delete Submodule @@ -215,19 +262,25 @@ OLD Copy File Mode Changed - Ignore Whitespace Change + First Difference + Ignore All Whitespace Changes + Last Difference LFS OBJECT CHANGE Next Difference NO CHANGES OR ONLY EOL CHANGES Previous Difference + Save as Patch Show hidden symbols Side-By-Side Diff SUBMODULE + DELETED NEW Swap Syntax Highlighting Line Word Wrap + Enable Block-Navigation Open in Merge Tool + Show All Lines Decrease Number of Visible Lines Increase Number of Visible Lines SELECT FILE TO VIEW CHANGES @@ -236,18 +289,19 @@ All local changes in working copy. Changes: Include ignored files - Total {0} changes will be discard + {0} changes will be discarded You can't undo this action!!! Bookmark: New Name: Target: Edit Selected Group Edit Selected Repository - Fast-Forward (without checkout) + Run Custom Action + Action Name: Fetch Fetch all remotes + Force override local refs Fetch without tags - Prune remote dead branches Remote: Fetch Remote Changes Assume unchanged @@ -255,7 +309,8 @@ Discard {0} files... Discard Changes in Selected Line(s) Open External Merge Tool - Save As Patch... + Resolve Using ${0}$ + Save as Patch... Stage Stage {0} files Stage Changes in Selected Line(s) @@ -264,12 +319,11 @@ Unstage Unstage {0} files Unstage Changes in Selected Line(s) - Use Theirs (checkout --theirs) Use Mine (checkout --ours) + Use Theirs (checkout --theirs) File History - CONTENT CHANGE - FILTER + CONTENT Git-Flow Development Branch: Feature: @@ -278,6 +332,8 @@ FLOW - Finish Hotfix FLOW - Finish Release Target: + Push to remote(s) after performing finish + Squash during merge Hotfix: Hotfix Prefix: Initialize Git-Flow @@ -299,8 +355,8 @@ Custom Pattern: Add Track Pattern to Git LFS Fetch - Fetch LFS Objects Run `git lfs fetch` to download Git LFS objects. This does not update the working copy. + Fetch LFS Objects Install Git LFS hooks Show Locks No Locked Files @@ -312,42 +368,48 @@ Prune Run `git lfs prune` to delete old LFS files from local storage Pull - Pull LFS Objects Run `git lfs pull` to download all Git LFS files for current ref & checkout + Pull LFS Objects Push - Push LFS Objects Push queued large files to the Git LFS endpoint + Push LFS Objects Remote: Track files named '{0}' Track all *{0} files - Histories - Switch Horizontal/Vertical Layout + HISTORY AUTHOR AUTHOR TIME GRAPH & SUBJECT SHA COMMIT TIME SELECTED {0} COMMITS - Holding 'Ctrl' or 'Shift' to select multiple commits. - Holding ⌘ or ⇧ to select multiple commits. + Hold 'Ctrl' or 'Shift' to select multiple commits. + Hold ⌘ or ⇧ to select multiple commits. TIPS: Keyboard Shortcuts Reference GLOBAL Cancel current popup + Clone new repository Close current page - Go to previous page Go to next page + Go to previous page Create new page - Open preference dialog + Open Preferences dialog + Switch active workspace + Switch active page REPOSITORY Commit staged changes Commit and push staged changes Stage all changes and commit + Creates a new branch based on selected commit Discard selected changes + Fetch, starts directly Dashboard mode (Default) + Commit search mode + Pull, starts directly + Push, starts directly Force to reload this repository Stage/Unstage selected changes - Commit search mode Switch to 'Changes' Switch to 'Histories' Switch to 'Stashes' @@ -355,31 +417,44 @@ Close search panel Find next match Find previous match + Open with external diff/merge tool Open search panel + Discard Stage Unstage - Discard Initialize Repository Path: - Cherry-Pick in progress. Press 'Abort' to restore original HEAD. - Merge request in progress. Press 'Abort' to restore original HEAD. - Rebase in progress. Press 'Abort' to restore original HEAD. - Revert in progress. Press 'Abort' to restore original HEAD. + Cherry-Pick in progress. + Processing commit + Merge in progress. + Merging + Rebase in progress. + Stopped at + Revert in progress. + Reverting commit Interactive Rebase - Target Branch: On: + Target Branch: + Copy Link + Open in Browser ERROR NOTICE + Workspaces + Pages Merge Branch Into: Merge Option: - Source Branch: + Source: + Merge (Multiple) + Commit all changes + Strategy: + Targets: Move Repository Node Select parent node for: Name: - Git has NOT been configured. Please to go [Preference] and configure it first. - Open App Data Dir - Open With... + Git has NOT been configured. Please to go [Preferences] and configure it first. + Open Data Storage Directory + Open with... Optional. Create New Page Bookmark @@ -389,74 +464,86 @@ Copy Repository Path Repositories Paste - Just now - {0} minutes ago - {0} hours ago - Yesterday {0} days ago + 1 hour ago + {0} hours ago + Just now Last month - {0} months ago Last year + {0} minutes ago + {0} months ago {0} years ago - Preference - OPEN AI - Server - API Key - Model - APPEARANCE - Default Font - Default Font Size - Monospace Font - Only use monospace font in text editor - Theme - Theme Overrides - Use fixed tab width in titlebar - Use native window frame - DIFF/MERGE TOOL - Install Path - Input path for diff/merge tool - Tool - GENERAL - Check for updates on startup - Language - History Commits - Show author time intead of commit time in graph - Subject Guide Length - GIT - Enable Auto CRLF - Default Clone Dir - User Email - Global git user email - Install Path - User Name - Global git user name - Git version - Git (>= 2.23.0) is required by this app - GPG SIGNING - Commit GPG signing - Tag GPG signing - GPG Format - Program Install Path - Input path for installed gpg program - User Signing Key - User's gpg signing key - INTEGRATION - SHELL/TERMINAL - Shell/Terminal - Path + Yesterday + Preferences + AI + Analyze Diff Prompt + API Key + Generate Subject Prompt + Model + Name + Server + Enable Streaming + APPEARANCE + Default Font + Editor Tab Width + Font Size + Default + Editor + Monospace Font + Use monospace font only in text editor + Theme + Theme Overrides + Use fixed tab width in titlebar + Use native window frame + DIFF/MERGE TOOL + Install Path + Input path for diff/merge tool + Tool + GENERAL + Check for updates on startup + Date Format + Language + History Commits + Show author time instead of commit time in graph + Show children in the commit details + Show tags in commit graph + Subject Guide Length + GIT + Enable Auto CRLF + Default Clone Dir + User Email + Global git user email + Enable --prune on fetch + Enable --ignore-cr-at-eol in diff + Git (>= 2.25.1) is required by this app + Install Path + Enable HTTP SSL Verify + User Name + Global git user name + Git version + GPG SIGNING + Commit GPG signing + GPG Format + Program Install Path + Input path for installed gpg program + Tag GPG signing + User Signing Key + User's gpg signing key + INTEGRATION + SHELL/TERMINAL + Path + Shell/Terminal Prune Remote Target: Prune Worktrees - Prune worktree information in `$GIT_DIR/worktrees` + Prune worktree information in `$GIT_COMMON_DIR/worktrees` Pull - Branch: - Fetch all branches + Remote Branch: Into: Local Changes: Discard - Do Nothing Stash & Reapply - Fetch without tags + Update all submodules Remote: Pull (Fetch & Merge) Use rebase instead of merge @@ -478,7 +565,6 @@ Stash & reapply local changes On: Rebase: - Refresh Add Remote Edit Remote Name: @@ -491,7 +577,6 @@ Fetch Open In Browser Prune - Target: Confirm to Remove Worktree Enable `--force` Option Target: @@ -501,47 +586,77 @@ Branch: ABORT Auto fetching changes from remotes... + Sort + By Committer Date + By Name Cleanup(GC & Prune) Run `git gc` command for this repository. Clear all + Clear Configure this repository CONTINUE + Custom Actions + No Custom Actions + Discard all changes Enable '--reflog' Option - Open In File Browser + Open in File Browser Search Branches/Tags/Submodules - FILTERED BY: - LOCAL BRANCHES - Navigate To HEAD + Visibility in Graph + Unset + Hide in commit graph + Filter in commit graph Enable '--first-parent' Option + LAYOUT + Horizontal + Vertical + COMMITS ORDER + Commit Date + Topologically + LOCAL BRANCHES + Navigate to HEAD Create Branch - Open In {0} - Open In External Tools + CLEAR NOTIFICATIONS + Only highlight current branch in graph + Open in {0} + Open in External Tools Refresh REMOTES - ADD REMOTE - RESOLVE + Add Remote Search Commit + Author + Committer + Content File Message SHA - Author & Committer Current Branch + Show Submodules as Tree Show Tags as Tree + SKIP Statistics SUBMODULES - ADD SUBMODULE - UPDATE SUBMODULE + Add Submodule + Update Submodule TAGS - NEW TAG - Open In Terminal + New Tag + By Creator Date + By Name + Sort + Open in Terminal + Use relative time in histories + View Logs + Visit '{0}' in Browser WORKTREES - ADD WORKTREE - PRUNE + Add Worktree + Prune Git Repository URL Reset Current Branch To Revision Reset Mode: Move To: Current Branch: + Reset Branch (Without Checkout) + Move To: + Branch: Reveal in File Explorer Revert Commit Commit: @@ -561,13 +676,22 @@ Skip This Version Software Update There are currently no updates available. + Set Tracking Branch + Branch: + Unset upstream + Upstream: + Copy SHA + Go to Squash Commits Into: SSH Private Key: Private SSH key store path START Stash + Auto-restore after stashing + Your working files remain unchanged, but a stash is saved. Include untracked files + Keep staged files Message: Optional. Name of this stash Only staged changes @@ -575,40 +699,52 @@ Stash Local Changes Apply Drop - Pop + Save as Patch... Drop Stash Drop: - Stashes + STASHES CHANGES STASHES Statistics COMMITS COMMITTER + OVERVIEW MONTH WEEK - COMMITS: AUTHORS: - OVERVIEW + COMMITS: SUBMODULES Add Submodule Copy Relative Path + De-initialize Submodule Fetch nested submodules Open Submodule Repository Relative Path: Relative folder to store this module. Delete Submodule + STATUS + modified + not initialized + revision changed + unmerged + URL OK Copy Tag Name Copy Tag Message Delete ${0}$... + Merge ${0}$ into ${1}$... Push ${0}$... - URL: Update Submodules All submodules Initialize as needed Recursively Submodule: Use --remote option + URL: + Logs + CLEAR ALL + Copy + Delete Warning Welcome Page Create Group @@ -621,37 +757,44 @@ Open All Repositories Open Repository Open Terminal - Rescan Repositories in Default Clone Dir + Rescan Repositories in Default Clone Dir Search Repositories... Sort - Changes + LOCAL CHANGES Git Ignore Ignore all *{0} files Ignore *{0} files in the same folder Ignore files in the same folder Ignore this file only Amend - Auto-Stage You can stage this file now. COMMIT COMMIT & PUSH Template/Histories Trigger click event + Commit (Edit) Stage all changes and commit + You have staged {0} file(s) but only {1} file(s) displayed ({2} files are filtered out). Do you want to continue? CONFLICTS DETECTED + OPEN EXTERNAL MERGETOOL + OPEN ALL CONFLICTS IN EXTERNAL MERGETOOL FILE CONFLICTS ARE RESOLVED + USE MINE + USE THEIRS INCLUDE UNTRACKED FILES NO RECENT INPUT MESSAGES NO COMMIT TEMPLATES + Reset Author + Right-click the selected file(s), and make your choice to resolve conflicts. + SignOff STAGED UNSTAGE UNSTAGE ALL UNSTAGED STAGE STAGE ALL - VIEW ASSUME UNCHANGED + VIEW ASSUME UNCHANGED Template: ${0}$ - Right-click the selected file(s), and make your choice to resolve conflicts. WORKSPACE: Configure Workspaces... WORKTREE diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml new file mode 100644 index 00000000..4f1a4452 --- /dev/null +++ b/src/Resources/Locales/es_ES.axaml @@ -0,0 +1,809 @@ + + + + + + Acerca de + Acerca de SourceGit + Cliente Git GUI de código abierto y gratuito + Agregar Worktree + Ubicación: + Ruta para este worktree. Se admite ruta relativa. + Nombre de la Rama: + Opcional. Por defecto es el nombre de la carpeta de destino. + Rama de Seguimiento: + Seguimiento de rama remota + Qué Checkout: + Crear Nueva Rama + Rama Existente + Asistente OpenAI + RE-GENERAR + Usar OpenAI para generar mensaje de commit + APLICAR CÓMO MENSAJE DE COMMIT + Aplicar Parche + Archivo del Parche: + Seleccionar archivo .patch para aplicar + Ignorar cambios de espacios en blanco + Aplicar Parche + Espacios en Blanco: + Aplicar Stash + Borrar después de aplicar + Restaurar los cambios del índice + Stash: + Archivar... + Guardar Archivo en: + Seleccionar ruta del archivo + Revisión: + Archivar + SourceGit Askpass + ARCHIVOS ASUMIDOS COMO SIN CAMBIOS + NO HAY ARCHIVOS ASUMIDOS COMO SIN CAMBIOS + REMOVER + Cargar Imagen... + Refrescar + ¡ARCHIVO BINARIO NO SOPORTADO! + Bisect + Abortar + Malo + Bisecting. ¿Es el HEAD actual bueno o malo? + Bueno + Saltar + Bisecting. Marcar el commit actual cómo bueno o malo y revisar otro. + Blame + ¡BLAME EN ESTE ARCHIVO NO SOPORTADO! + Checkout ${0}$... + Comparar con ${0}$ + Comparar con Worktree + Copiar Nombre de Rama + Acción personalizada + Eliminar ${0}$... + Eliminar {0} ramas seleccionadas + Fast-Forward a ${0}$ + Fetch ${0}$ en ${1}$... + Git Flow - Finalizar ${0}$ + Merge ${0}$ en ${1}$... + Hacer merge de las ramas {0} seleccionadas hacia la rama actual + Pull ${0}$ + Pull ${0}$ en ${1}$... + Push ${0}$ + Rebase ${0}$ en ${1}$... + Renombrar ${0}$... + Resetear ${0}$ a ${1}$... + Establecer Rama de Seguimiento... + Comparar Ramas + ¡Upstream inválido! + Bytes + CANCELAR + Resetear a Revisión Padre + Resetear a Esta Revisión + Generar mensaje de commit + CAMBIAR MODO DE VISUALIZACIÓN + Mostrar como Lista de Archivos y Directorios + Mostrar como Lista de Rutas + Mostrar como Árbol de Sistema de Archivos + Checkout Rama + Checkout Commit + Commit: + Advertencia: Al hacer un checkout de commit, tu Head se separará + Cambios Locales: + Descartar + Stash & Reaplicar + Actualizar todos los submódulos + Rama: + Checkout & Fast-Forward + Fast-Forward a: + Cherry Pick + Añadir fuente al mensaje de commit + Commit(s): + Commit todos los cambios + Mainline: + Normalmente no puedes cherry-pick un merge porque no sabes qué lado del merge debe considerarse la línea principal. Esta opción permite que cherry-pick reproduzca el cambio en relación con el padre especificado. + Limpiar Stashes + Estás intentando limpiar todos los stashes. ¿Estás seguro de continuar? + Clonar Repositorio Remoto + Parámetros Adicionales: + Argumentos adicionales para clonar el repositorio. Opcional. + Nombre Local: + Nombre del repositorio. Opcional. + Carpeta Padre: + Inicializar y actualizar submódulos + URL del Repositorio: + CERRAR + Editor + Checkout Commit + Cherry-Pick Este Commit + Cherry-Pick ... + Comparar con HEAD + Comparar con Worktree + Autor + Committer + Información + SHA + Asunto + Acción personalizada + Rebase Interactivo ${0}$ hasta Aquí + Merge a ${0}$ + Merge ... + Rebase ${0}$ hasta Aquí + Reset ${0}$ hasta Aquí + Revertir Commit + Reescribir + Guardar como Parche... + Squash en Parent + Squash Commits Hijos hasta Aquí + CAMBIOS + archivo(s) modificado(s) + Buscar Cambios... + ARCHIVOS + Archivo LFS + Buscar Archivos... + Submódulo + INFORMACIÓN + AUTOR + CAMBIADO + HIJOS + COMMITTER + Ver refs que contienen este commit + COMMIT ESTÁ CONTENIDO EN + Muestra solo los primeros 100 cambios. Ver todos los cambios en la pestaña CAMBIOS. + MENSAJE + PADRES + REFS + SHA + Abrir en Navegador + Descripción + ASUNTO + Introducir asunto del commit + Configurar Repositorio + PLANTILLA DE COMMIT + Contenido de la Plantilla: + Nombre de la Plantilla: + ACCIÓN PERSONALIZADA + Argumentos: + ${REPO} - Ruta del repositorio; ${SHA} - SHA del commit seleccionado + Archivo Ejecutable: + Nombre: + Alcance: + Rama + Commit + Repositorio + Esperar la acción de salida + Dirección de Email + Dirección de email + GIT + Fetch remotos automáticamente + Minuto(s) + Remoto por Defecto + Modo preferido de Merge + SEGUIMIENTO DE INCIDENCIAS + Añadir Regla de Ejemplo para Azure DevOps + Añadir Regla de Ejemplo para Incidencias de Gitee + Añadir Regla de Ejemplo para Pull Requests de Gitee + Añadir Regla de Ejemplo para Github + Añadir Regla de Ejemplo para Incidencias de GitLab + Añadir Regla de Ejemplo para Merge Requests de GitLab + Añadir Regla de Ejemplo para Jira + Nueva Regla + Expresión Regex para Incidencias: + Nombre de la Regla: + URL Resultante: + Por favor, use $1, $2 para acceder a los valores de los grupos regex. + OPEN AI + Servicio Preferido: + Si el 'Servicio Preferido' está establecido, SourceGit sólo lo usará en este repositorio. De lo contrario, si hay más de un servicio disponible, se mostrará un menú de contexto para elegir uno. + Proxy HTTP + Proxy HTTP utilizado por este repositorio + Nombre de Usuario + Nombre de usuario para este repositorio + Espacios de Trabajo + Color + Nombre + Restaurar pestañas al iniciar + CONTINUAR + ¡Commit vacío detectado! ¿Quieres continuar (--allow-empty)? + HACER STAGE A TODO & COMMIT + ¡Commit vacío detectado! ¿Quieres continuar (--allow-empty) o hacer stage a todo y después commit? + Asistente de Commit Convencional + Cambio Importante: + Incidencia Cerrada: + Detalles del Cambio: + Alcance: + Descripción Corta: + Tipo de Cambio: + Copiar + Copiar Todo el Texto + Copiar Ruta Completa + Copiar Ruta + Crear Rama... + Basado En: + Checkout de la rama creada + Cambios Locales: + Descartar + Stash & Reaplicar + Nombre de la Nueva Rama: + Introduzca el nombre de la rama. + Los espacios serán reemplazados con guiones. + Crear Rama Local + Sobrescribir la rama existente + Crear Etiqueta... + Nueva Etiqueta En: + Firma GPG + Mensaje de la Etiqueta: + Opcional. + Nombre de la Etiqueta: + Formato recomendado: v1.0.0-alpha + Push a todos los remotos después de crear + Crear Nueva Etiqueta + Tipo: + anotada + ligera + Mantenga Ctrl para iniciar directamente + Cortar + Desinicializar Submódulo + Forzar desinicialización incluso si contiene cambios locales. + Submódulo: + Eliminar Rama + Rama: + ¡Estás a punto de eliminar una rama remota! + También eliminar la rama remota ${0}$ + Eliminar Múltiples Ramas + Estás intentando eliminar múltiples ramas a la vez. ¡Asegúrate de revisar antes de tomar acción! + Eliminar Remoto + Remoto: + Ruta: + Destino: + Todos los hijos serán removidos de la lista. + ¡Esto solo lo removera de la lista, no del disco! + Confirmar Eliminación de Grupo + Confirmar Eliminación de Repositorio + Eliminar Submódulo + Ruta del Submódulo: + Eliminar Etiqueta + Etiqueta: + Eliminar de los repositorios remotos + DIFERENCIA BINARIA + NUEVO + ANTIGUO + Copiar + Modo de Archivo Cambiado + Primera Diferencia + Ignorar Cambio de Espacios en Blanco + Última Diferencia + CAMBIO DE OBJETO LFS + Siguiente Diferencia + SIN CAMBIOS O SOLO CAMBIOS DE EOL + Diferencia Anterior + Guardar como Parche + Mostrar símbolos ocultos + Diferencia Lado a Lado + SUBMÓDULO + BORRADO + NUEVO + Intercambiar + Resaltado de Sintaxis + Ajuste de Línea + Habilitar navegación en bloque + Abrir en Herramienta de Merge + Mostrar Todas las Líneas + Disminuir Número de Líneas Visibles + Aumentar Número de Líneas Visibles + SELECCIONA ARCHIVO PARA VER CAMBIOS + Abrir en Herramienta de Merge + Descartar Cambios + Todos los cambios locales en la copia de trabajo. + Cambios: + Incluir archivos ignorados + Total {0} cambios serán descartados + ¡No puedes deshacer esta acción! + Marcador: + Nuevo Nombre: + Destino: + Editar Grupo Seleccionado + Editar Repositorio Seleccionado + Ejecutar Acción Personalizada + Nombre de la Acción: + Fetch + Fetch todos los remotos + Utilizar opción '--force' + Fetch sin etiquetas + Remoto: + Fetch Cambios Remotos + Asumir sin cambios + Descartar... + Descartar {0} archivos... + Descartar Cambios en Línea(s) Seleccionada(s) + Abrir Herramienta de Merge Externa + Resolver usando ${0}$ + Guardar como Parche... + Stage + Stage {0} archivos + Stage Cambios en Línea(s) Seleccionada(s) + Stash... + Stash {0} archivos... + Unstage + Unstage {0} archivos + Unstage Cambios en Línea(s) Seleccionada(s) + Usar Míos (checkout --ours) + Usar Suyos (checkout --theirs) + Historial de Archivos + CAMBIO + CONTENIDO + Git-Flow + Rama de Desarrollo: + Feature: + Prefijo de Feature: + FLOW - Finalizar Feature + FLOW - Finalizar Hotfix + FLOW - Finalizar Release + Destino: + Push al/los remoto(s) después de Finalizar + Squash durante el merge + Hotfix: + Prefijo de Hotfix: + Inicializar Git-Flow + Mantener rama + Rama de Producción: + Release: + Prefijo de Release: + Iniciar Feature... + FLOW - Iniciar Feature + Iniciar Hotfix... + FLOW - Iniciar Hotfix + Introducir nombre + Iniciar Release... + FLOW - Iniciar Release + Prefijo de Etiqueta de Versión: + Git LFS + Añadir Patrón de Seguimiento... + El patrón es el nombre del archivo + Patrón Personalizado: + Añadir Patrón de Seguimiento a Git LFS + Fetch + Ejecuta `git lfs fetch` para descargar objetos Git LFS. Esto no actualiza la copia de trabajo. + Fetch Objetos LFS + Instalar hooks de Git LFS + Mostrar Bloqueos + No hay archivos bloqueados + Bloquear + Mostrar solo mis bloqueos + Bloqueos LFS + Desbloquear + Forzar Desbloqueo + Prune + Ejecuta `git lfs prune` para eliminar archivos LFS antiguos del almacenamiento local + Pull + Ejecuta `git lfs pull` para descargar todos los archivos Git LFS para la referencia actual y hacer checkout + Pull Objetos LFS + Push + Push archivos grandes en cola al endpoint de Git LFS + Push Objetos LFS + Remoto: + Seguir archivos llamados '{0}' + Seguir todos los archivos *{0} + Historias + AUTOR + HORA DEL AUTOR + GRÁFICO & ASUNTO + SHA + FECHA DE COMMIT + {0} COMMITS SELECCIONADOS + Mantén 'Ctrl' o 'Shift' para seleccionar múltiples commits. + Mantén ⌘ o ⇧ para seleccionar múltiples commits. + CONSEJOS: + Referencia de Atajos de Teclado + GLOBAL + Cancelar popup actual + Clonar repositorio nuevo + Cerrar página actual + Ir a la siguiente página + Ir a la página anterior + Crear nueva página + Abrir diálogo de preferencias + Cambiar espacio de trabajo activo + Cambiar página activa + REPOSITORIO + Commit cambios staged + Commit y push cambios staged + Stage todos los cambios y commit + Crea una nueva rama basada en el commit seleccionado + Descartar cambios seleccionados + Fetch, empieza directamente + Modo Dashboard (Por Defecto) + Modo de búsqueda de commits + Pull, empieza directamente + Push, empieza directamente + Forzar a recargar este repositorio + Stage/Unstage cambios seleccionados + Cambiar a 'Cambios' + Cambiar a 'Historias' + Cambiar a 'Stashes' + EDITOR DE TEXTO + Cerrar panel de búsqueda + Buscar siguiente coincidencia + Buscar coincidencia anterior + Abrir con herramienta diff/merge externa + Abrir panel de búsqueda + Descartar + Stage + Unstage + Inicializar Repositorio + Ruta: + Cherry-Pick en progreso. + Procesando commit + Merge en progreso. + Haciendo merge + Rebase en progreso. + Pausado en + Revert en progreso. + Haciendo revert del commit + Rebase Interactivo + En: + Rama Objetivo: + Copiar Enlace + Abrir en el Navegador + ERROR + AVISO + Espacios de trabajo + Páginas + Merge Rama + En: + Opción de Merge: + Rama Fuente: + Merge (Multiplo) + Commit todos los cambios + Estrategia: + Destino: + Mover Nodo del Repositorio + Seleccionar nodo padre para: + Nombre: + Git NO ha sido configurado. Por favor, ve a [Preferencias] y configúralo primero. + Abrir Directorio de Datos de la App + Abrir Con... + Opcional. + Crear Nueva Página + Marcador + Cerrar Pestaña + Cerrar Otras Pestañas + Cerrar Pestañas a la Derecha + Copiar Ruta del Repositorio + Repositorios + Pegar + Hace {0} días + Hace 1 hora + Hace {0} horas + Justo ahora + Último mes + Último año + Hace {0} minutos + Hace {0} meses + Hace {0} años + Ayer + Preferencias + OPEN AI + Analizar Diff Prompt + Clave API + Generar Subject Prompt + Modelo + Nombre + Servidor + Activar Transmisión + APARIENCIA + Fuente por defecto + Ancho de la Pestaña del Editor + Tamaño de fuente + Por defecto + Editor + Fuente Monospace + Usar solo fuente monospace en el editor de texto + Tema + Sobreescritura de temas + Usar ancho de pestaña fijo en la barra de título + Usar marco de ventana nativo + HERRAMIENTA DIFF/MERGE + Ruta de instalación + Introducir ruta para la herramienta diff/merge + Herramienta + GENERAL + Buscar actualizaciones al iniciar + Formato de Fecha + Idioma + Commits en el historial + Mostrar hora del autor en lugar de la hora del commit en el gráfico + Mostrar hijos en los detalles de commit + Mostrar etiquetas en el gráfico de commit + Longitud de la guía del asunto + GIT + Habilitar Auto CRLF + Directorio de clonado por defecto + Email de usuario + Email global del usuario git + Habilitar --prune para fetch + Habilitar --ignore-cr-at-eol en diff + Se requiere Git (>= 2.25.1) para esta aplicación + Ruta de instalación + Habilitar verificación HTTP SSL + Nombre de usuario + Nombre global del usuario git + Versión de Git + FIRMA GPG + Firma GPG en commit + Formato GPG + Ruta de instalación del programa + Introducir ruta para el programa gpg instalado + Firma GPG en etiqueta + Clave de firma del usuario + Clave de firma gpg del usuario + INTEGRACIÓN + SHELL/TERMINAL + Ruta + Shell/Terminal + Podar Remoto + Destino: + Podar Worktrees + Podar información de worktree en `$GIT_COMMON_DIR/worktrees` + Pull + Rama Remota: + En: + Cambios Locales: + Descartar + Stash & Reaplicar + Actualizar todos los submódulos + Remoto: + Pull (Fetch & Merge) + Usar rebase en lugar de merge + Push + Asegurarse de que los submódulos se hayan hecho push + Forzar push + Rama Local: + Remoto: + Push Cambios al Remoto + Rama Remota: + Establecer como rama de seguimiento + Push todas las etiquetas + Push Etiqueta al Remoto + Push a todos los remotos + Remoto: + Etiqueta: + Salir + Rebase Rama Actual + Stash & reaplicar cambios locales + En: + Rebase: + Añadir Remoto + Editar Remoto + Nombre: + Nombre remoto + URL del Repositorio: + URL del repositorio git remoto + Copiar URL + Borrar... + Editar... + Fetch + Abrir En Navegador + Podar (Prune) + Confirmar para Eliminar Worktree + Utilizar Opción `--force` + Destino: + Renombrar Rama + Nuevo Nombre: + Nombre único para esta rama + Rama: + ABORTAR + Auto fetching cambios desde remotos... + Ordenar + Por Fecha de Committer + Por Nombre + Limpiar (GC & Prune) + Ejecutar comando `git gc` para este repositorio. + Limpiar todo + Limpiar + Configurar este repositorio + CONTINUAR + Acciones Personalizadas + No hay ninguna Acción Personalizada + Descartar todos los cambios + Habilitar Opción '--reflog' + Abrir en el Explorador + Buscar Ramas/Etiquetas/Submódulos + Visibilidad en el Gráfico + Desestablecer + Ocultar en el Gráfico de Commits + Filtrar en el Gráfico de Commits + Habilitar Opción '--first-parent' + DISPOSICIÓN + Horizontal + Vertical + ORDEN DE COMMITS + Fecha de Commit + Topológicamente + RAMAS LOCALES + Navegar a HEAD + Crear Rama + LIMPIAR NOTIFICACIONES + Resaltar solo la rama actual en el gráfico + Abrir en {0} + Abrir en Herramientas Externas + Refrescar + REMOTOS + AÑADIR REMOTO + Buscar Commit + Autor + Committer + Contenido + Archivo + Mensaje + SHA + Rama Actual + Mostrar Submódulos como Árbol + Mostrar Etiquetas como Árbol + OMITIR + Estadísticas + SUBMÓDULOS + AÑADIR SUBMÓDULO + ACTUALIZAR SUBMÓDULO + ETIQUETAS + NUEVA ETIQUETA + Por Fecha de Creación + Por Nombre + Ordenar + Abrir en Terminal + Usar tiempo relativo en las historias + Ver Logs + Visitar '{0}' en el Navegador + WORKTREES + AÑADIR WORKTREE + PRUNE + URL del Repositorio Git + Resetear Rama Actual a Revisión + Modo de Reset: + Mover a: + Rama Actual: + Resetear Rama (Sin hacer Checkout) + Mover A: + Rama: + Revelar en el Explorador de Archivos + Revertir Commit + Commit: + Commit revertir cambios + Reescribir Mensaje de Commit + Usa 'Shift+Enter' para introducir una nueva línea. 'Enter' es el atajo del botón OK + Ejecutando. Por favor espera... + GUARDAR + Guardar Como... + ¡El parche se ha guardado exitosamente! + Escanear Repositorios + Directorio Raíz: + Buscar Actualizaciones... + Nueva versión de este software disponible: + ¡Error al buscar actualizaciones! + Descargar + Omitir Esta Versión + Actualización de Software + Actualmente no hay actualizaciones disponibles. + Establecer Rama de Seguimiento + Rama: + Desestablecer upstream + Upstream: + Copiar SHA + Ir a + Squash Commits + En: + Clave Privada SSH: + Ruta de almacenamiento de la clave privada SSH + INICIAR + Stash + Restaurar automáticamente después del stashing + Tus archivos de trabajo permanecen sin cambios, pero se guarda un stash. + Incluir archivos no rastreados + Mantener archivos staged + Mensaje: + Opcional. Nombre de este stash + Solo cambios staged + ¡Tanto los cambios staged como los no staged de los archivos seleccionados serán stashed! + Stash Cambios Locales + Aplicar + Eliminar + Guardar como Parche... + Eliminar Stash + Eliminar: + Stashes + CAMBIOS + STASHES + Estadísticas + COMMITS + COMMITTER + GENERAL + MES + SEMANA + AUTORES: + COMMITS: + SUBMÓDULOS + Añadir Submódulo + Copiar Ruta Relativa + Desinicializar Submódulo + Fetch submódulos anidados + Abrir Repositorio del Submódulo + Ruta Relativa: + Carpeta relativa para almacenar este módulo. + Eliminar Submódulo + ESTADO + modificado + no inicializado + revisión cambiada + unmerged + URL + OK + Copiar Nombre de la Etiqueta + Copiar Mensaje de la Etiqueta + Eliminar ${0}$... + Merge ${0}$ en ${1}$... + Push ${0}$... + Actualizar Submódulos + Todos los submódulos + Inicializar según sea necesario + Recursivamente + Submódulo: + Usar opción --remote + URL: + Logs + LIMPIAR TODO + Copiar + Borrar + Advertencia + Página de Bienvenida + Crear Grupo + Crear Sub-Grupo + Clonar Repositorio + Eliminar + SOPORTA ARRASTRAR Y SOLTAR CARPETAS. SOPORTA AGRUPACIÓN PERSONALIZADA. + Editar + Mover a Otro Grupo + Abrir Todos Los Repositorios + Abrir Repositorio + Abrir Terminal + Reescanear Repositorios en el Directorio de Clonado por Defecto + Buscar Repositorios... + Ordenar + Cambios + Git Ignore + Ignorar todos los archivos *{0} + Ignorar archivos *{0} en la misma carpeta + Ignorar archivos en la misma carpeta + Ignorar solo este archivo + Enmendar + Puedes hacer stage a este archivo ahora. + COMMIT + COMMIT & PUSH + Plantilla/Historias + Activar evento de clic + Commit (Editar) + Hacer stage a todos los cambios y commit + Tienes {0} archivo(s) en stage, pero solo {1} archivo(s) mostrado(s) ({2} archivo(s) están filtrados). ¿Quieres continuar? + CONFLICTOS DETECTADOS + ABRIR HERRAMIENTA DE MERGE EXTERNA + ABRIR TODOS LOS CONFLICTOS EN HERRAMIENTA DE MERGE EXTERNA + LOS CONFLICTOS DE ARCHIVOS ESTÁN RESUELTOS + USAR MÍOS + USAR SUYOS + INCLUIR ARCHIVOS NO RASTREADOS + NO HAY MENSAJES DE ENTRADA RECIENTES + NO HAY PLANTILLAS DE COMMIT + Restablecer Autor + Haz clic derecho en el(los) archivo(s) seleccionado(s) y elige tu opción para resolver conflictos. + Firmar + STAGED + UNSTAGE + UNSTAGE TODO + UNSTAGED + STAGE + STAGE TODO + VER ASSUME UNCHANGED + Plantilla: ${0}$ + ESPACIO DE TRABAJO: + Configura Espacios de Trabajo... + WORKTREE + Copiar Ruta + Bloquear + Eliminar + Desbloquear + diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 8e56248d..635d74c2 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -2,13 +2,9 @@ + À propos - • Compilé avec - © 2024 sourcegit-scm - • TextEditor de - • Les polices Monospace proviennent de À propos de SourceGit - • Le code source est disponible sur Client Git Open Source et Gratuit Ajouter un Worktree Emplacement : @@ -17,23 +13,23 @@ Optionnel. Nom du dossier de destination par défaut. Suivre la branche : Suivi de la branche distante - What to Checkout: + Que récupérer : Créer une nouvelle branche Branche existante + Assistant IA + RE-GÉNÉRER + Utiliser l'IA pour générer un message de commit + APPLIQUER COMME MESSAGE DE COMMIT Appliquer - Erreur - Soulever les erreurs et refuser d'appliquer le patch - Toutes les erreurs - Similaire à 'Erreur', mais plus détaillé Fichier de patch : Selectionner le fichier .patch à appliquer Ignorer les changements d'espaces blancs - Pas d'avertissement - Désactiver l'avertissement sur les espaces blancs terminaux Appliquer le patch - Avertissement - Affiche des avertissements pour ce type d'erreurs tout en appliquant le patch Espaces blancs : + Appliquer le Stash + Supprimer après application + Rétablir les changements de l'index + Stash: Archiver... Enregistrer l'archive sous : Sélectionnez le chemin du fichier d'archive @@ -43,81 +39,93 @@ FICHIERS PRÉSUMÉS INCHANGÉS PAS DE FICHIERS PRÉSUMÉS INCHANGÉS SUPPRIMER + Rafraîchir FICHIER BINAIRE NON SUPPORTÉ !!! Blâme LE BLÂME SUR CE FICHIER N'EST PAS SUPPORTÉ!!! - Checkout ${0}$... - Comparer avec la branche - Comparer avec HEAD + Récupérer ${0}$... + Comparer avec ${0}$ Comparer avec le worktree Copier le nom de la branche + Action personnalisée Supprimer ${0}$... Supprimer {0} branches sélectionnées - Rejeter tous les changements Fast-Forward vers ${0}$ + Fetch ${0}$ vers ${1}$... Git Flow - Terminer ${0}$ Fusionner ${0}$ dans ${1}$... + Fusionner les {0} branches sélectionnées dans celle en cours Tirer ${0}$ Tirer ${0}$ dans ${1}$... Pousser ${0}$ Rebaser ${0}$ sur ${1}$... Renommer ${0}$... - Définir la branche de suivi - Ne plus suivre la branche distante + Définir la branche de suivi... Comparer les branches + Branche en amont invalide! Octets ANNULER Réinitialiser à la révision parente Réinitialiser à cette révision + Générer un message de commit CHANGER LE MODE D'AFFICHAGE Afficher comme liste de dossiers/fichiers Afficher comme liste de chemins Afficher comme arborescence - Checkout Branch - Checkout ce commit + Récupérer la branche + Récupérer ce commit Commit : - Avertissement: un checkout vers un commit aboutiera vers un HEAD détaché + Avertissement: une récupération vers un commit aboutiera vers un HEAD détaché Changements locaux : Annuler - Ne rien faire Mettre en stash et réappliquer Branche : Cherry-Pick de ce commit + Ajouter la source au message de commit Commit : Commit tous les changements - Cherry Pick + Ligne principale : + Habituellement, on ne peut pas cherry-pick un commit car on ne sait pas quel côté devrait être considéré comme principal. Cette option permet de rejouer les changements relatifs au parent spécifié. Supprimer les stashes Vous essayez de supprimer tous les stashes. Êtes-vous sûr de vouloir continuer ? + Cloner repository distant Paramètres supplémentaires : Arguments additionnels au clônage. Optionnel. Nom local : Nom de dépôt. Optionnel. Dossier parent : + Initialiser et mettre à jour les sous-modules URL du dépôt : - Cloner le dépôt distant FERMER Éditeur - Changer de commit + Récupérer ce commit Cherry-Pick ce commit + Cherry-Pick ... Comparer avec HEAD Comparer avec le worktree - Copier les informations - Copier le SHA + Informations + SHA + Action personnalisée Rebase interactif de ${0}$ ici + Fusionner dans ${0}$ + Fusionner ... Rebaser ${0}$ ici Réinitialiser ${0}$ ici Annuler le commit Reformuler Enregistrer en tant que patch... Squash dans le parent + Squash les commits enfants ici CHANGEMENTS Rechercher les changements... FICHIERS Fichier LFS + Rechercher des fichiers... Sous-module INFORMATIONS AUTEUR CHANGÉ + ENFANTS COMMITTER Vérifier les références contenant ce commit LE COMMIT EST CONTENU PAR @@ -126,43 +134,73 @@ PARENTS REFS SHA + Ouvrir dans le navigateur Description Entrez le message du commit Configurer le dépôt MODÈLE DE COMMIT Contenu de modèle: Nom de modèle: + ACTION PERSONNALISÉE + Arguments : + ${REPO} - Chemin du repository; ${SHA} - SHA du commit sélectionné + Fichier exécutable : + Nom : + Portée : + Branche + Commit + Repository + Attendre la fin de l'action Adresse e-mail Adresse e-mail GIT Fetch les dépôts distants automatiquement minute(s) + Dépôt par défaut SUIVI DES PROBLÈMES + Ajouter une règle d'exemple Azure DevOps + Ajouter une règle d'exemple Gitee + Ajouter une règle d'exemple pour Pull Request Gitee Ajouter une règle d'exemple Github + Ajouter une règle d'exemple pour Incidents GitLab + Ajouter une règle d'exemple pour Merge Request GitLab Ajouter une règle d'exemple Jira Nouvelle règle Issue Regex Expression: Nom de règle : - Veuillez utiliser $1, $2 pour accéder aux valeurs des groupes regex. URL résultant: + Veuillez utiliser $1, $2 pour accéder aux valeurs des groupes regex. + IA + Service préféré: + Si le 'Service préféré' est défini, SourceGit l'utilisera seulement dans ce repository. Sinon, si plus d'un service est disponible, un menu contextuel permettant de choisir l'un d'eux sera affiché. Proxy HTTP Proxy HTTP utilisé par ce dépôt Nom d'utilisateur Nom d'utilisateur pour ce dépôt + Espaces de travail + Couleur + Nom + Restaurer les onglets au démarrage + Assistant Commits Conventionnels + Changement Radical : + Incident Clos : + Détail des Modifications : + Portée : + Courte Description : + Type de Changement : Copier Copier tout le texte - Copier le nom de fichier - COPIER LE MESSAGE + Copier le chemin complet Copier le chemin Créer une branche... Basé sur : - Passer à la branche créée + Récupérer la branche créée Changements locaux : Rejeter - Ne rien faire Stash & Réappliquer Nom de la nouvelle branche : Entrez le nom de la branche. + Les espaces seront remplacés par des tirets. Créer une branche locale Créer un tag... Nouveau tag à : @@ -186,7 +224,10 @@ Vous essayez de supprimer plusieurs branches à la fois. Assurez-vous de revérifier avant de procéder ! Supprimer Remote Remote : + Chemin: Cible : + Tous les enfants seront retirés de la liste. + Cela le supprimera uniquement de la liste, pas du disque ! Confirmer la suppression du groupe Confirmer la suppression du dépôt Supprimer le sous-module @@ -199,10 +240,14 @@ ANCIEN Copier Mode de fichier changé + Première différence + Ignorer les changements d'espaces + Dernière différence CHANGEMENT D'OBJET LFS Différence suivante PAS DE CHANGEMENT OU SEULEMENT EN FIN DE LIGNE Différence précédente + Enregistrer en tant que patch Afficher les caractères invisibles Diff côte-à-côte SOUS-MODULE @@ -210,7 +255,9 @@ Permuter Coloration syntaxique Retour à la ligne + Activer la navigation par blocs Ouvrir dans l'outil de fusion + Voir toutes les lignes Réduit le nombre de ligne visibles Augmente le nombre de ligne visibles SÉLECTIONNEZ UN FICHIER POUR VOIR LES CHANGEMENTS @@ -218,6 +265,7 @@ Rejeter les changements Tous les changements dans la copie de travail. Changements : + Inclure les fichiers ignorés {0} changements seront rejetés Vous ne pouvez pas annuler cette action !!! Signet : @@ -225,11 +273,12 @@ Cible : Éditer le groupe sélectionné Éditer le dépôt sélectionné - Fast-Forward (sans checkout) + Lancer action personnalisée + Nom de l'action : Fetch Fetch toutes les branches distantes + Outrepasser les vérifications de refs Fetch sans les tags - Élaguer les branches mortes distantes Remote : Récupérer les changements distants Présumer inchangé @@ -237,6 +286,7 @@ Rejeter {0} fichiers... Rejeter les changements dans les lignes sélectionnées Ouvrir l'outil de fusion externe + Résoudre en utilisant ${0}$ Enregistrer en tant que patch... Indexer Indexer {0} fichiers @@ -249,79 +299,90 @@ Utiliser les miennes (checkout --ours) Utiliser les leurs (checkout --theirs) Historique du fichier + MODIFICATION CONTENU - FILTRER Git-Flow - Development Branch: + Branche de développement : Feature: Feature Prefix: - FLOW - Finish Feature - FLOW - Finish Hotfix - FLOW - Finish Release - Target: + FLOW - Terminer Feature + FLOW - Terminer Hotfix + FLOW - Terminer Release + Cible: Hotfix: Hotfix Prefix: - Initialize Git-Flow - Keep branch - Production Branch: - Release: - Release Prefix: - Start Feature... - FLOW - Start Feature - Start Hotfix... - FLOW - Start Hotfix - Enter name - Start Release... - FLOW - Start Release - Version Tag Prefix: + Initialiser Git-Flow + Garder la branche + Branche de production : + Release : + Release Prefix : + Commencer Feature... + FLOW - Commencer Feature + Commencer Hotfix... + FLOW - Commencer Hotfix + Saisir le nom + Commencer Release... + FLOW - Commencer Release + Préfixe Tag de Version : Git LFS - Add Track Pattern... - Pattern is file name - Custom Pattern: - Add Track Pattern to Git LFS + Ajouter un pattern de suivi... + Le pattern est un nom de fichier + Pattern personnalisé : + Ajouter un pattern de suivi à Git LFS Fetch - Fetch LFS Objects - Run `git lfs fetch` to download Git LFS objects. This does not update the working copy. - Install Git LFS hooks - Show Locks - No Locked Files - Lock - LFS Locks - Unlock - Force Unlock - Prune - Run `git lfs prune` to delete old LFS files from local storage + Lancer `git lfs fetch` pour télécharger les objets Git LFS. Cela ne met pas à jour la copie de travail. + Fetch les objets LFS + Installer les hooks Git LFS + Afficher les verrous + Pas de fichiers verrouillés + Verrouiller + Afficher seulement mes verrous + Verrous LFS + Déverouiller + Forcer le déverouillage + Elaguer + Lancer `git lfs prune` pour supprimer les anciens fichier LFS du stockage local Pull - Pull LFS Objects - Run `git lfs pull` to download all Git LFS files for current ref & checkout - Push - Push LFS Objects - Push queued large files to the Git LFS endpoint - Remote: - Track files named '{0}' - Track all *{0} files + Lancer `git lfs pull` pour télécharger tous les fichier Git LFS de la référence actuelle & récupérer + Pull les objets LFS + Pousser + Transférer les fichiers volumineux en file d'attente vers le point de terminaison Git LFS + Pousser les objets LFS + Dépôt : + Suivre les fichiers appelés '{0}' + Suivre tous les fichiers *{0} Historique - Basculer entre dispositions Horizontal/Vertical AUTEUR + HEURE DE L'AUTEUR GRAPHE & SUJET SHA HEURE DE COMMIT {0} COMMITS SÉLECTIONNÉS + Maintenir 'Ctrl' ou 'Shift' enfoncée pour sélectionner plusieurs commits. + Maintenir ⌘ ou ⇧ enfoncée pour sélectionner plusieurs commits. + CONSEILS: Référence des raccourcis clavier GLOBAL Annuler le popup en cours + Cloner un nouveau dépôt Fermer la page en cours - Aller à la page précédente Aller à la page suivante + Aller à la page précédente Créer une nouvelle page - Ouvrir le dialogue des préférences + Ouvrir le dialogue des préférences DÉPÔT Commit les changements de l'index Commit et pousser les changements de l'index + Ajouter tous les changements et commit + Créer une nouvelle branche basée sur le commit actuel + Rejeter les changements sélectionnés + Fetch, démarre directement Mode tableau de bord (Défaut) + Recherche de commit + Pull, démarre directement + Push, démarre directement Forcer le rechargement du dépôt Ajouter/Retirer les changements sélectionnés de l'index - Recherche de commit Basculer vers 'Changements' Basculer vers 'Historique' Basculer vers 'Stashes' @@ -330,238 +391,306 @@ Trouver la prochaine correspondance Trouver la correspondance précédente Ouvrir le panneau de recherche - Stage - Retirer de l'index Rejeter - Initialize Repository - Path: - Cherry-Pick in progress. Press 'Abort' to restore original HEAD. - Merge request in progress. Press 'Abort' to restore original HEAD. - Rebase in progress. Press 'Abort' to restore original HEAD. - Revert in progress. Press 'Abort' to restore original HEAD. - Interactive Rebase - Target Branch: - On: - ERROR + Indexer + Retirer de l'index + Initialiser le repository + Chemin : + Cherry-Pick en cours. + Traitement du commit + Merge request en cours. + Fusionnement + Rebase en cours. + Arrêté à + Annulation en cours. + Annulation du commit + Rebase interactif + Sur : + Branche cible : + Copier le lien + Ouvrir dans le navigateur + ERREUR NOTICE - Merge Branch - Into: - Merge Option: - Source Branch: + Merger la branche + Dans : + Option de merge: + Source: + Fusionner (Plusieurs) + Commit tous les changement + Stratégie: + Cibles: + Déplacer le noeud du repository + Sélectionnier le noeud parent pour : Nom : Git n'a PAS été configuré. Veuillez d'abord le faire dans le menu Préférence. Ouvrir le dossier AppData - Open With... - Optional. + Ouvrir avec... + Optionnel. Créer un nouvel onglet Bookmark Fermer l'onglet Fermer les autres onglets Fermer les onglets à droite - Copy Repository Path + Copier le chemin vers le dépôt Dépôts - Paste - A l'instant - il y a {0} minutes - il y a {0} heures - Hier + Coller il y a {0} jours + il y a 1 heure + il y a {0} heures + A l'instant Le mois dernier - il y a {0} mois L'an dernier + il y a {0} minutes + il y a {0} mois il y a {0} ans - Préférences - APPARENCE - Police par défaut - Taille de police par défaut - Police monospace - N'utiliser que des polices monospace pour l'éditeur de texte - Thème - Dérogations de thème - Utiliser des onglets de taille fixe dans la barre de titre - Utiliser un cadre de fenêtre natif - OUTIL DIFF/MERGE - Chemin d'installation - Saisir le chemin d'installation de l'outil diff/merge - Outil - GÉNÉRAL - Vérifier les mises à jour au démarrage - Language - Historique de commits - Guide de longueur du sujet - GIT - Activer auto CRLF - Répertoire de clônage par défaut - E-mail utilsateur - E-mail utilsateur global - Chemin d'installation - Nom d'utilisateur - Nom d'utilisateur global - Version de Git - Cette application requière Git (>= 2.23.0) - SIGNATURE GPG - Signature GPG de commit - Signature GPG de tag - Format GPG - Chemin d'installation du programme - Saisir le chemin d'installation vers le programme GPG - Clé de signature de l'utilisateur - Clé de signature GPG de l'utilisateur - Élaguer une branche distant + Hier + Préférences + IA + Analyser Diff Prompt + Clé d'API + Générer le sujet de Prompt + Modèle + Nom + Serveur + Activer le streaming + APPARENCE + Police par défaut + Largeur de tab dans l'éditeur + Taille de police + Défaut + Éditeur + Police monospace + N'utiliser que des polices monospace pour l'éditeur de texte + Thème + Dérogations de thème + Utiliser des onglets de taille fixe dans la barre de titre + Utiliser un cadre de fenêtre natif + OUTIL DIFF/MERGE + Chemin d'installation + Saisir le chemin d'installation de l'outil diff/merge + Outil + GÉNÉRAL + Vérifier les mises à jour au démarrage + Format de date + Language + Historique de commits + Afficher l'heure de l'auteur au lieu de l'heure de validation dans le graphique + Afficher les enfants dans les détails du commit + Afficher les tags dans le graphique des commits + Guide de longueur du sujet + GIT + Activer auto CRLF + Répertoire de clônage par défaut + E-mail utilsateur + E-mail utilsateur global + Activer --prune pour fetch + Cette application requière Git (>= 2.25.1) + Chemin d'installation + Activer la vérification HTTP SSL + Nom d'utilisateur + Nom d'utilisateur global + Version de Git + SIGNATURE GPG + Signature GPG de commit + Format GPG + Chemin d'installation du programme + Saisir le chemin d'installation vers le programme GPG + Signature GPG de tag + Clé de signature de l'utilisateur + Clé de signature GPG de l'utilisateur + INTEGRATION + SHELL/TERMINAL + Chemin + Shell/Terminal + Élaguer une branche distant Cible : Élaguer les Worktrees - Élaguer les information de worktree dans `$GIT_DIR/worktrees` + Élaguer les information de worktree dans `$GIT_COMMON_DIR/worktrees` Pull - Branche : - Fetch toutes les branches + Branche distante : Dans : Changements locaux : Rejeter - Ne rien faire Stash & Réappliquer - Fetch sans les tags - Distant : + Dépôt distant : Pull (Fetch & Merge) - Use rebase instead of merge - Push - Make sure submodules have been pushed - Force push - Local Branch: - Remote: - Push Changes To Remote - Remote Branch: - Set as tracking branch - Push all tags - Push Tag To Remote - Push to all remotes - Remote: - Tag: - Quit - Rebase Current Branch - Stash & reapply local changes - On: - Rebase: - Refresh - Add Remote - Edit Remote - Name: - Remote name - Repository URL: - Remote git repository URL - Copy URL - Delete... - Edit... + Utiliser rebase au lieu de merge + Pousser + Assurez-vous que les submodules ont été poussés + Poussage forcé + Branche locale : + Dépôt distant : + Pousser les changements vers le dépôt distant + Branche distante : + Définir comme branche de suivi + Pousser tous les tags + Pousser les tags vers le dépôt distant + Pousser tous les dépôts distants + Dépôt distant : + Tag : + Quitter + Rebase la branche actuelle + Stash & réappliquer changements locaux + Sur : + Rebase : + Ajouter dépôt distant + Modifier dépôt distant + Nom : + Nom du dépôt distant + URL du repository : + URL du dépôt distant + Copier l'URL + Supprimer... + Editer... Fetch - Open In Browser - Prune - Target: - Confirm to Remove Worktree - Enable `--force` Option - Target: - Rename Branch - New Name: - Unique name for this branch - Branch: + Ouvrir dans le navigateur + Elaguer + Confirmer la suppression du Worktree + Activer l'option `--force` + Cible : + la branche + Nouveau nom : + Nom unique pour cette branche + Branche : ABORT - Cleanup(GC & Prune) - Run `git gc` command for this repository. - Clear all - Configure this repository - CONTINUE - Ouvrir dans l'explorateur Windows - Search Branches/Tags/Submodules - FILTERED BY: - LOCAL BRANCHES - Navigate To HEAD - Enable '--first-parent' Option - Create Branch - Open In {0} - Open In External Tools - Refresh - REMOTES - ADD REMOTE - RESOLVE - Search Commit - File + Fetch automatique des changements depuis les dépôts... + Nettoyage(GC & Elaguage) + Lancer `git gc` pour ce repository. + Tout effacer + Configurer ce repository + CONTINUER + Actions personnalisées + Pas d'actions personnalisées + Rejeter tous les changements + Activer l'option '--reflog' + Ouvrir dans l'explorateur de fichiers + Rechercher Branches/Tags/Submodules + Visibilité dans le graphique + Réinitialiser + Cacher dans le graphique des commits + Filtrer dans le graphique des commits + Activer l'option '--first-parent' + DISPOSITION + Horizontal + Vertical + ORDRE DES COMMITS + Date du commit + Topologiquement + BRANCHES LOCALES + Naviguer vers le HEAD + Créer une branche + EFFACER LES NOTIFICATIONS + Mettre la branche courante en surbrillance dans le graph + Ouvrir dans {0} + Ouvrir dans un outil externe + Rafraîchir + DEPOTS DISTANTS + AJOUTER DEPOT DISTANT + Rechercher un commit + Auteur + Committer + Fichier Message SHA - Author & Committer - Show Tags as Tree - Statistics + Branche actuelle + Voir les Tags en tant qu'arbre + PASSER + Statistiques SUBMODULES - ADD SUBMODULE - UPDATE SUBMODULE + AJOUTER SUBMODULE + METTRE A JOUR SUBMODULE TAGS - NEW TAG + NOUVEAU TAG + Par date de créateur + Par nom + Trier Ouvrir dans un terminal + Utiliser le temps relatif dans les historiques WORKTREES - ADD WORKTREE - PRUNE - Git Repository URL - Reset Current Branch To Revision + AJOUTER WORKTREE + ELAGUER + URL du repository Git + Reset branche actuelle à la révision Reset Mode: - Move To: - Current Branch: + Déplacer vers : + Branche actuelle : Ouvrir dans l'explorateur de fichier - Revert le Commit + Annuler le Commit Commit : - Commit les changements du revert + Commit les changements de l'annulation Reformuler le message de commit Utiliser 'Maj+Entrée' pour insérer une nouvelle ligne. 'Entrée' est la touche pour valider En exécution. Veuillez patienter... SAUVEGARDER Sauvegarder en tant que... Le patch a été sauvegardé ! - Vérifier les mises à jour... + Analyser les repositories + Dossier racine : + Rechercher des mises à jour... Une nouvelle version du logiciel est disponible : La vérification de mise à jour à échouée ! Télécharger Passer cette version Mise à jour du logiciel Il n'y a pas de mise à jour pour le moment. - Squash Commits - SSH Private Key: - Private SSH key store path + Définir la branche suivie + Branche: + Retirer la branche amont + En amont: + Copier le SHA + Aller à + Squash les commits + Dans : + Clé privée SSH : + Chemin du magasin de clés privées SSH START Stash - Include untracked files - Message: + Auto-restauration après le stash + Vos fichiers de travail restent inchangés, mais une sauvegarde est enregistrée. + Inclure les fichiers non-suivis + Garder les fichiers indexés + Message : Optionnel. Nom de ce stash + Seulement les changements indexés + Les modifications indexées et non-indexées des fichiers sélectionnés seront stockées!!! Stash les changements locaux Appliquer Effacer - Extraire + Sauver comme Patch... Effacer le Stash Effacer : Stashes CHANGEMENTS STASHES - Statistics + Statistiques COMMITS COMMITTER - MONTH - WEEK + APERCU + MOIS + SEMAINE + AUTEURS : COMMITS: - AUTHORS: - SUBMODULES - Add Submodule - Copy Relative Path - Fetch nested submodules - Open Submodule Repository + SOUS-MODULES + Ajouter un sous-module + Copier le chemin relatif + Fetch les sous-modules imbriqués + Ouvrir le dépôt de sous-module Relative Path: Relative folder to store this module. - Delete Submodule + Supprimer le sous-module OK - Copy Tag Name - Delete ${0}$... - Push ${0}$... - URL : + Copier le nom du Tag + Copier le message du tag + Supprimer ${0}$... + Fusionner ${0}$ dans ${1}$... + Pousser ${0}$... Actualiser les sous-modules Tous les sous-modules Initialiser au besoin Récursivement Sous-module : Utiliser l'option --remote + URL : Avertissement Page d'accueil Créer un groupe @@ -570,9 +699,11 @@ Supprimer GLISSER / DEPOSER DE DOSSIER SUPPORTÉ. GROUPAGE PERSONNALISÉ SUPPORTÉ. Éditer + Déplacer vers un autre groupe Ouvrir tous les dépôts Ouvrir un dépôt Ouvrir le terminal + Réanalyser les repositories dans le dossier de clonage par défaut Chercher des dépôts... Trier Changements @@ -582,25 +713,30 @@ Ignorer les fichiers dans le même dossier N'ignorer que ce fichier Amender - Auto-Index Vous pouvez indexer ce fichier. COMMIT - COMMIT & PUSH + COMMIT & POUSSER Modèles/Historiques + Trigger click event + Commit (Modifier) + Indexer tous les changements et commit CONFLITS DÉTECTÉS LES CONFLITS DE FICHIER SONT RÉSOLUS INCLURE LES FICHIERS NON-SUIVIS PAS DE MESSAGE D'ENTRÉE RÉCENT PAS DE MODÈLES DE COMMIT + Faites un clique droit sur les fichiers sélectionnés et faites vos choix pour la résoluion des conflits. + SignOff INDEXÉ RETIRER DE L'INDEX RETIRER TOUT DE L'INDEX NON INDEXÉ INDEXER INDEXER TOUT - VOIR LES FICHIERS PRÉSUMÉS INCHANGÉS + VOIR LES FICHIERS PRÉSUMÉS INCHANGÉS Modèle: ${0}$ - Faites un clique droit sur les fichiers sélectionnés et faites vos choix pour la résoluion des conflits. + ESPACE DE TRAVAIL : + Configurer les espaces de travail... WORKTREE Copier le chemin Verrouiller diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml new file mode 100644 index 00000000..3aef0043 --- /dev/null +++ b/src/Resources/Locales/it_IT.axaml @@ -0,0 +1,788 @@ + + + + + + Informazioni + Informazioni su SourceGit + Client GUI Git open source e gratuito + Aggiungi Worktree + Posizione: + Percorso per questo worktree. Supportato il percorso relativo. + Nome Branch: + Facoltativo. Predefinito è il nome della cartella di destinazione. + Traccia Branch: + Traccia branch remoto + Di cosa fare il checkout: + Crea nuovo branch + Branch esistente + Assistente AI + RIGENERA + Usa AI per generare il messaggio di commit + APPLICA COME MESSAGGIO DI COMMIT + Applica + File Patch: + Seleziona file .patch da applicare + Ignora modifiche agli spazi + Applica Patch + Spazi: + Applica lo stash + Rimuovi dopo aver applicato + Ripristina le modifiche all'indice + Stash: + Archivia... + Salva Archivio In: + Seleziona il percorso del file archivio + Revisione: + Archivia + Richiedi Password SourceGit + FILE ASSUNTI COME INVARIATI + NESSUN FILE ASSUNTO COME INVARIATO + RIMUOVI + Aggiorna + FILE BINARIO NON SUPPORTATO!!! + Biseca + Annulla + Cattiva + Bisecando. La HEAD corrente è buona o cattiva? + Buona + Salta + Bisecando. Marca il commit corrente come buono o cattivo e fai checkout di un altro. + Attribuisci + L'ATTRIBUZIONE SU QUESTO FILE NON È SUPPORTATA!!! + Checkout ${0}$... + Confronta con ${0}$ + Confronta con Worktree + Copia Nome Branch + Azione personalizzata + Elimina ${0}$... + Elimina i {0} branch selezionati + Avanzamento Veloce a ${0}$ + Recupera ${0}$ in ${1}$... + Git Flow - Completa ${0}$ + Unisci ${0}$ in ${1}$... + Unisci i {0} branch selezionati in quello corrente + Scarica ${0}$ + Scarica ${0}$ in ${1}$... + Invia ${0}$ + Riallinea ${0}$ su ${1}$... + Rinomina ${0}$... + Imposta Branch di Tracciamento... + Confronto Branch + Upstream non valido + Byte + ANNULLA + Ripristina la Revisione Padre + Ripristina Questa Revisione + Genera messaggio di commit + CAMBIA MODALITÀ DI VISUALIZZAZIONE + Mostra come elenco di file e cartelle + Mostra come elenco di percorsi + Mostra come albero del filesystem + Checkout Branch + Checkout Commit + Commit: + Avviso: Effettuando un checkout del commit, la tua HEAD sarà separata + Modifiche Locali: + Scarta + Stasha e Ripristina + Aggiorna tutti i sottomoduli + Branch: + Cherry Pick + Aggiungi sorgente al messaggio di commit + Commit(s): + Conferma tutte le modifiche + Mainline: + Di solito non è possibile fare cherry-pick sdi una unione perché non si sa quale lato deve essere considerato il mainline. Questa opzione consente di riprodurre la modifica relativa al genitore specificato. + Cancella Stash + Stai per cancellare tutti gli stash. Sei sicuro di voler continuare? + Clona Repository Remoto + Parametri Extra: + Argomenti addizionali per clonare il repository. Facoltativo. + Nome Locale: + Nome del repository. Facoltativo. + Cartella Principale: + Inizializza e aggiorna i sottomoduli + URL del Repository: + CHIUDI + Editor + Checkout Commit + Cherry-Pick Questo Commit + Cherry-Pick... + Confronta con HEAD + Confronta con Worktree + Autore + Committer + Informazioni + SHA + Oggetto + Azione Personalizzata + Riallinea Interattivamente ${0}$ fino a Qui + Unisci a ${0}$ + Unisci ... + Riallinea ${0}$ fino a Qui + Ripristina ${0}$ fino a Qui + Annulla Commit + Modifica + Salva come Patch... + Compatta nel Genitore + Compatta Commit Figli fino a Qui + MODIFICHE + Cerca Modifiche... + FILE + File LFS + Cerca File... + Sottomodulo + INFORMAZIONI + AUTORE + MODIFICATO + FIGLI + CHI HA COMMITTATO + Controlla i riferimenti che contengono questo commit + IL COMMIT È CONTENUTO DA + Mostra solo le prime 100 modifiche. Vedi tutte le modifiche nella scheda MODIFICHE. + MESSAGGIO + GENITORI + RIFERIMENTI + SHA + Apri nel Browser + Descrizione + OGGETTO + Inserisci l'oggetto del commit + Configura Repository + TEMPLATE DI COMMIT + Contenuto Template: + Nome Template: + AZIONE PERSONALIZZATA + Argomenti: + ${REPO} - Percorso del repository; ${SHA} - SHA del commit selezionato + File Eseguibile: + Nome: + Ambito: + Branch + Commit + Repository + Attendi la fine dell'azione + Indirizzo Email + Indirizzo email + GIT + Recupera automaticamente i remoti + Minuto/i + Remoto Predefinito + Modalità di Merge Preferita + TRACCIAMENTO ISSUE + Aggiungi una regola di esempio per Azure DevOps + Aggiungi una regola di esempio per un Issue Gitee + Aggiungi una regola di esempio per un Pull Request Gitee + Aggiungi una regola di esempio per GitHub + Aggiungi una regola di esempio per Issue GitLab + Aggiungi una regola di esempio per una Merge Request GitLab + Aggiungi una regola di esempio per Jira + Nuova Regola + Espressione Regex Issue: + Nome Regola: + URL Risultato: + Utilizza $1, $2 per accedere ai valori dei gruppi regex. + AI + Servizio preferito: + Se il 'Servizio Preferito' é impostato, SourceGit utilizzerà solo quello per questo repository. Altrimenti, se ci sono più servizi disponibili, verrà mostrato un menu contestuale per sceglierne uno. + Proxy HTTP + Proxy HTTP usato da questo repository + Nome Utente + Nome utente per questo repository + Spazi di Lavoro + Colore + Nome + Ripristina schede all'avvio + CONTINUA + Trovato un commit vuoto! Vuoi procedere (--allow-empty)? + STAGE DI TUTTO E COMMITTA + Trovato un commit vuoto! Vuoi procedere (--allow-empty) o fare lo stage di tutto e committare? + Guida Commit Convenzionali + Modifica Sostanziale: + Issue Chiusa: + Dettaglio Modifiche: + Ambito: + Descrizione Breve: + Tipo di Modifica: + Copia + Copia Tutto il Testo + Copia Intero Percorso + Copia Percorso + Crea Branch... + Basato Su: + Checkout del Branch Creato + Modifiche Locali: + Scarta + Stasha e Ripristina + Nome Nuovo Branch: + Inserisci il nome del branch. + Gli spazi verranno rimpiazzati con dei trattini. + Crea Branch Locale + Crea Tag... + Nuovo Tag Su: + Firma con GPG + Messaggio Tag: + Facoltativo. + Nome Tag: + Formato consigliato: v1.0.0-alpha + Invia a tutti i remoti dopo la creazione + Crea Nuovo Tag + Tipo: + annotato + leggero + Tieni premuto Ctrl per avviare direttamente + Taglia + Elimina Branch + Branch: + Stai per eliminare un branch remoto!!! + Elimina anche il branch remoto ${0}$ + Elimina Branch Multipli + Stai per eliminare più branch contemporaneamente. Controlla attentamente prima di procedere! + Elimina Remoto + Remoto: + Percorso: + Destinazione: + Tutti i figli verranno rimossi dalla lista. + Lo rimuoverà solamente dalla lista, non dal disco! + Conferma Eliminazione Gruppo + Conferma Eliminazione Repository + Elimina Sottomodulo + Percorso Sottomodulo: + Elimina Tag + Tag: + Elimina dai repository remoti + DIFF BINARIO + NUOVO + VECCHIO + Copia + Modalità File Modificata + Prima differenza + Ignora Modifiche agli Spazi + Ultima differenza + MODIFICA OGGETTO LFS + Differenza Successiva + NESSUNA MODIFICA O SOLO CAMBIAMENTI DI FINE LINEA + Differenza Precedente + Salva come Patch + Mostra Simboli Nascosti + Diff Affiancato + SOTTOMODULO + NUOVO + Scambia + Evidenziazione Sintassi + Avvolgimento delle Parole + Abilita la navigazione a blocchi + Apri nello Strumento di Merge + Mostra Tutte le Righe + Diminuisci Numero di Righe Visibili + Aumenta Numero di Righe Visibili + SELEZIONA UN FILE PER VISUALIZZARE LE MODIFICHE + Apri nello Strumento di Merge + Scarta Modifiche + Tutte le modifiche locali nella copia di lavoro. + Modifiche: + Includi file ignorati + Un totale di {0} modifiche saranno scartate + Questa azione non può essere annullata!!! + Segnalibro: + Nuovo Nome: + Destinazione: + Modifica Gruppo Selezionato + Modifica Repository Selezionato + Esegui Azione Personalizzata + Nome Azione: + Recupera + Recupera da tutti i remoti + Forza la sovrascrittura dei riferimenti locali + Recupera senza tag + Remoto: + Recupera Modifiche Remote + Presumi invariato + Scarta... + Scarta {0} file... + Scarta Modifiche nelle Righe Selezionate + Apri Strumento di Merge Esterno + Risolvi Usando ${0}$ + Salva come Patch... + Stage + Stage di {0} file + Stage delle Modifiche nelle Righe Selezionate + Stasha... + Stasha {0} file... + Rimuovi da Stage + Rimuovi da Stage {0} file + Rimuovi le Righe Selezionate da Stage + Usa Il Mio (checkout --ours) + Usa Il Loro (checkout --theirs) + Cronologia File + MODIFICA + CONTENUTO + Git-Flow + Branch di Sviluppo: + Feature: + Prefisso Feature: + FLOW - Completa Feature + FLOW - Completa Hotfix + FLOW - Completa Rilascio + Target: + Invia al remote dopo aver finito + Esegui squash durante il merge + Hotfix: + Prefisso Hotfix: + Inizializza Git-Flow + Mantieni branch + Branch di Produzione: + Rilascio: + Prefisso Rilascio: + Inizia Feature... + FLOW - Inizia Feature + Inizia Hotfix... + FLOW - Inizia Hotfix + Inserisci nome + Inizia Rilascio... + FLOW - Inizia Rilascio + Prefisso Tag Versione: + Git LFS + Aggiungi Modello di Tracciamento... + Il modello è un nome file + Modello Personalizzato: + Aggiungi Modello di Tracciamento a Git LFS + Recupera + Esegui `git lfs fetch` per scaricare gli oggetti Git LFS. Questo non aggiorna la copia di lavoro. + Recupera Oggetti LFS + Installa hook di Git LFS + Mostra Blocchi + Nessun File Bloccato + Blocca + Mostra solo i miei blocchi + Blocchi LFS + Sblocca + Forza Sblocco + Elimina + Esegui `git lfs prune` per eliminare vecchi file LFS dallo storage locale + Scarica + Esegui `git lfs pull` per scaricare tutti i file LFS per il ref corrente e fare il checkout + Scarica Oggetti LFS + Invia + Invia grandi file in coda al punto finale di Git LFS + Invia Oggetti LFS + Remoto: + Traccia file con nome '{0}' + Traccia tutti i file *{0} + STORICO + AUTORE + ORA AUTORE + GRAFICO E OGGETTO + SHA + ORA COMMIT + {0} COMMIT SELEZIONATI + Tieni premuto 'Ctrl' o 'Shift' per selezionare più commit. + Tieni premuto ⌘ o ⇧ per selezionare più commit. + SUGGERIMENTI: + Riferimento Scorciatoie da Tastiera + GLOBALE + Annulla il popup corrente + Clona una nuova repository + Chiudi la pagina corrente + Vai alla pagina successiva + Vai alla pagina precedente + Crea una nuova pagina + Apri la finestra delle preferenze + REPOSITORY + Committa le modifiche in tsage + Committa e invia le modifiche in stage + Fai lo stage di tutte le modifiche e committa + Crea un nuovo branch dal commit selezionato + Scarta le modifiche selezionate + Recupera, avvia direttamente + Modalità Dashboard (Predefinita) + Modalità ricerca commit + Scarica, avvia direttamente + Invia, avvia direttamente + Forza l'aggiornamento di questo repository + Aggiungi/Rimuovi da stage le modifiche selezionate + Passa a 'Modifiche' + Passa a 'Storico' + Passa a 'Stashes' + EDITOR TESTO + Chiudi il pannello di ricerca + Trova il prossimo risultato + Trova il risultato precedente + Apri con uno strumento di diff/merge esterno + Apri il pannello di ricerca + Scarta + Aggiungi in stage + Rimuovi + Inizializza Repository + Percorso: + Cherry-Pick in corso. + Elaborando il commit + Unione in corso. + Unendo + Riallineamento in corso. + Interrotto a + Ripristino in corso. + Ripristinando il commit + Riallinea Interattivamente + Su: + Branch di destinazione: + Copia il Link + Apri nel Browser + ERRORE + AVVISO + Unisci Branch + In: + Opzione di Unione: + Sorgente: + Unione (multipla) + Commit di tutte le modifiche + Strategia: + Obiettivi: + Sposta Nodo Repository + Seleziona nodo padre per: + Nome: + Git NON è configurato. Prima vai su [Preferenze] per configurarlo. + Apri Cartella Dati App + Apri con... + Opzionale. + Crea Nuova Pagina + Segnalibro + Chiudi Tab + Chiudi Altri Tab + Chiudi i Tab a Destra + Copia Percorso Repository + Repository + Incolla + {0} giorni fa + 1 ora fa + {0} ore fa + Proprio ora + Il mese scorso + L'anno scorso + {0} minuti fa + {0} mesi fa + {0} anni fa + Ieri + Preferenze + AI + Analizza il Prompt Differenza + Chiave API + Genera Prompt Oggetto + Modello + Nome + Server + Abilita streaming + ASPETTO + Font Predefinito + Larghezza della Tab Editor + Dimensione Font + Dimensione Font Predefinita + Dimensione Font Editor + Font Monospaziato + Usa solo font monospaziato nell'editor + Tema + Sostituzioni Tema + Usa larghezza fissa per i tab nella barra del titolo + Usa cornice finestra nativa + STRUMENTO DI DIFFERENZA/UNIONE + Percorso Installazione + Inserisci il percorso per lo strumento di differenza/unione + Strumento + GENERALE + Controlla aggiornamenti all'avvio + Formato data + Lingua + Numero massimo di commit nella cronologia + Mostra nel grafico l'orario dell'autore anziché quello del commit + Mostra i figli nei dettagli del commit + Mostra i tag nel grafico dei commit + Lunghezza Guida Oggetto + GIT + Abilita Auto CRLF + Cartella predefinita per cloni + Email Utente + Email utente Git globale + Abilita --prune durante il fetch + Abilita --ignore-cr-at-eol nel diff + Questa applicazione richiede Git (>= 2.25.1) + Percorso Installazione + Abilita la verifica HTTP SSL + Nome Utente + Nome utente Git globale + Versione di Git + FIRMA GPG + Firma GPG per commit + Formato GPG + Percorso Programma Installato + Inserisci il percorso per il programma GPG installato + Firma GPG per tag + Chiave Firma Utente + Chiave GPG dell'utente per la firma + INTEGRAZIONE + SHELL/TERMINALE + Percorso + Shell/Terminale + Potatura Remota + Destinazione: + Potatura Worktrees + Potatura delle informazioni di worktree in `$GIT_COMMON_DIR/worktrees` + Scarica + Branch Remoto: + In: + Modifiche Locali: + Scarta + Stasha e Riapplica + Remoto: + Scarica (Recupera e Unisci) + Riallineare anziché unire + Invia + Assicurati che i sottomoduli siano stati inviati + Forza l'invio + Branch Locale: + Remoto: + Invia modifiche al remoto + Branch Remoto: + Imposta come branch di tracking + Invia tutti i tag + Invia Tag al Remoto + Invia a tutti i remoti + Remoto: + Tag: + Esci + Riallinea Branch Corrente + Stasha e Riapplica modifiche locali + Su: + Riallinea: + Aggiungi Remoto + Modifica Remoto + Nome: + Nome del remoto + URL del Repository: + URL del repository Git remoto + Copia URL + Elimina... + Modifica... + Recupera + Apri nel Browser + Pota + Conferma Rimozione Worktree + Abilita opzione `--force` + Destinazione: + Rinomina Branch + Nuovo Nome: + Nome univoco per questo branch + Branch: + ANNULLA + Recupero automatico delle modifiche dai remoti... + Ordina + Per data del committer + Per nome + Pulizia (GC e Potatura) + Esegui il comando `git gc` per questo repository. + Cancella tutto + Configura questo repository + CONTINUA + Azioni Personalizzate + Nessuna Azione Personalizzata + Scarta tutte le modifiche + Abilita opzione '--reflog' + Apri nell'Esplora File + Cerca Branch/Tag/Sottomodulo + Visibilità nel grafico + Non impostato + Nascondi nel grafico dei commit + Filtra nel grafico dei commit + Abilita opzione '--first-parent' + LAYOUT + Orizzontale + Verticale + Ordine dei commit + Per data del commit + Topologicamente + BRANCH LOCALI + Vai a HEAD + Crea Branch + CANCELLA LE NOTIFICHE + Evidenzia nel grafico solo il branch corrente + Apri in {0} + Apri in Strumenti Esterni + Aggiorna + REMOTI + AGGIUNGI REMOTO + Cerca Commit + Autore + Committer + Contenuto + File + Messaggio + SHA + Branch Corrente + Mostra i Sottomoduli Come Albero + Mostra Tag come Albero + SALTA + Statistiche + SOTTOMODULI + AGGIUNGI SOTTOMODULI + AGGIORNA SOTTOMODULI + TAG + NUOVO TAG + Per data di creazione + Per nome + Ordina + Apri nel Terminale + Usa tempo relativo nello storico + Visualizza i Log + Visita '{0}' nel Browser + WORKTREE + AGGIUNGI WORKTREE + POTATURA + URL del Repository Git + Reset Branch Corrente alla Revisione + Modalità Reset: + Sposta a: + Branch Corrente: + Mostra nell'Esplora File + Ripristina Commit + Commit: + Commit delle modifiche di ripristino + Modifica Messaggio di Commit + Usa 'Shift+Enter' per inserire una nuova riga. 'Enter' è il tasto rapido per il pulsante OK + In esecuzione. Attendere... + SALVA + Salva come... + La patch è stata salvata con successo! + Scansiona Repository + Cartella Principale: + Controlla Aggiornamenti... + È disponibile una nuova versione del software: + Errore durante il controllo degli aggiornamenti! + Scarica + Salta questa versione + Aggiornamento Software + Non ci sono aggiornamenti disponibili. + Imposta il Branch + Branch: + Rimuovi upstream + Upstream: + Copia SHA + Vai a + Compatta Commit + In: + Chiave Privata SSH: + Percorso per la chiave SSH privata + AVVIA + Stasha + Auto-ripristino dopo lo stash + I tuoi file di lavoro rimangono inalterati, ma viene salvato uno stash. + Includi file non tracciati + Mantieni file in stage + Messaggio: + Opzionale. Nome di questo stash + Solo modifiche in stage + Sia le modifiche in stage che quelle non in stage dei file selezionati saranno stashate!!! + Stasha Modifiche Locali + Applica + Elimina + Salva come Patch... + Elimina Stash + Elimina: + STASH + MODIFICHE + STASH + Statistiche + COMMIT + COMMITTER + PANORAMICA + MESE + SETTIMANA + AUTORI: + COMMIT: + SOTTOMODULI + Aggiungi Sottomodulo + Copia Percorso Relativo + Recupera sottomoduli annidati + Apri Repository del Sottomodulo + Percorso Relativo: + Cartella relativa per memorizzare questo modulo. + Elimina Sottomodulo + STATO + modificato + non inizializzato + revisione cambiata + non unito + URL + OK + Copia Nome Tag + Copia Messaggio Tag + Elimina ${0}$... + Unisci ${0}$ in ${1}$... + Invia ${0}$... + Aggiorna Sottomoduli + Tutti i sottomoduli + Inizializza se necessario + Ricorsivamente + Sottomodulo: + Usa opzione --remote + URL: + Log + CANCELLA TUTTO + Copia + Elimina + Avviso + Pagina di Benvenuto + Crea Gruppo + Crea Sottogruppo + Clona Repository + Elimina + TRASCINA E RILASCIA CARTELLA SUPPORTATO. RAGGRUPPAMENTI PERSONALIZZATI SUPPORTATI. + Modifica + Sposta in un Altro Gruppo + Apri Tutti i Repository + Apri Repository + Apri Terminale + Riscansiona Repository nella Cartella Clone Predefinita + Cerca Repository... + Ordina + MODIFICHE LOCALI + Git Ignore + Ignora tutti i file *{0} + Ignora i file *{0} nella stessa cartella + Ignora i file nella stessa cartella + Ignora solo questo file + Modifica + Puoi aggiungere in stage questo file ora. + COMMIT + COMMIT E INVIA + Template/Storico + Attiva evento click + Commit (Modifica) + Stage di tutte le modifiche e fai il commit + Hai stageato {0} file ma solo {1} file mostrati ({2} file sono stati filtrati). Vuoi procedere? + CONFLITTI RILEVATI + APRI STRUMENTO DI MERGE ESTERNO + APRI TUTTI I CONFLITTI NELLO STRUMENTO DI MERGE ESTERNO + CONFLITTI NEI FILE RISOLTI + USO IL MIO + USO IL LORO + INCLUDI FILE NON TRACCIATI + NESSUN MESSAGGIO RECENTE INSERITO + NESSUN TEMPLATE DI COMMIT + Clicca con il tasto destro sul(i) file selezionato, quindi scegli come risolvere i conflitti. + SignOff + IN STAGE + RIMUOVI DA STAGE + RIMUOVI TUTTO DA STAGE + NON IN STAGE + FAI LO STAGE + FAI LO STAGE DI TUTTO + VISUALIZZA COME NON MODIFICATO + Template: ${0}$ + WORKSPACE: + Configura Workspaces... + WORKTREE + Copia Percorso + Blocca + Rimuovi + Sblocca + diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml new file mode 100644 index 00000000..918a6b4d --- /dev/null +++ b/src/Resources/Locales/ja_JP.axaml @@ -0,0 +1,743 @@ + + + + + + 概要 + SourceGitについて + オープンソース & フリーなGit GUIクライアント + ワークツリーを追加 + 場所: + ワークツリーのパスを入力してください。相対パスも使用することができます。 + ブランチの名前: + 任意。デフォルトでは宛先フォルダ名が使用されます。 + 追跡するブランチ: + 追跡中のリモートブランチ + チェックアウトする内容: + 新しいブランチを作成 + 既存のブランチ + OpenAI アシスタント + 再生成 + OpenAIを使用してコミットメッセージを生成 + コミットメッセージとして適用 + 適用 + パッチファイル: + 適用する .patchファイルを選択 + 空白文字の変更を無視 + パッチを適用 + 空白文字: + スタッシュを適用 + 適用後に削除 + インデックスの変更を復元 + スタッシュ: + アーカイブ... + アーカイブの保存先: + アーカイブファイルのパスを選択 + リビジョン: + アーカイブ + SourceGit Askpass + 変更されていないとみなされるファイル + 変更されていないとみなされるファイルはありません + 削除 + 更新 + バイナリファイルはサポートされていません!!! + Blame + BLAMEではこのファイルはサポートされていません!!! + ${0}$ をチェックアウトする... + ワークツリーと比較 + ブランチ名をコピー + カスタムアクション + ${0}$を削除... + 選択中の{0}個のブランチを削除 + ${0}$ へ早送りする + ${0}$ から ${1}$ へフェッチする + Git Flow - Finish ${0}$ + ${0}$ を ${1}$ にマージする... + 選択中の{0}個のブランチを現在のブランチにマージする + ${0}$ をプルする + ${0}$ を ${1}$ にプルする... + ${0}$ をプッシュする + ${0}$ を ${1}$ でリベースする... + ${0}$ をリネームする... + トラッキングブランチを設定... + ブランチの比較 + 無効な上流ブランチ! + バイト + キャンセル + 親リビジョンにリセット + このリビジョンにリセット + コミットメッセージを生成 + 変更表示の切り替え + ファイルとディレクトリのリストを表示 + パスのリストを表示 + ファイルシステムのツリーを表示 + ブランチをチェックアウト + コミットをチェックアウト + コミット: + 警告: コミットをチェックアウトするとHEADが切断されます + ローカルの変更: + 破棄 + スタッシュして再適用 + ブランチ: + チェリーピック + ソースをコミットメッセージに追加 + コミット(複数可): + すべての変更をコミット + メインライン: + 通常、マージをチェリーピックすることはできません。どちらのマージ元をメインラインとして扱うべきかが分からないためです。このオプションを使用すると、指定した親に対して変更を再適用する形でチェリーピックを実行できます。 + スタッシュをクリア + すべてのスタッシュをクリアします。続行しますか? + リモートリポジトリをクローン + 追加の引数: + リポジトリをクローンする際の追加パラメータ(任意)。 + ローカル名: + リポジトリの名前(任意)。 + 親フォルダ: + サブモジュールを初期化して更新 + リポジトリのURL: + 閉じる + エディタ + コミットをチェックアウト + このコミットをチェリーピック + チェリーピック... + HEADと比較 + ワークツリーと比較 + 情報 + SHA + カスタムアクション + ${0}$ ブランチをここにインタラクティブリベース + ${0}$ にマージ + マージ... + ${0}$ をここにリベース + ${0}$ ブランチをここにリセット + コミットを戻す + 書き直す + パッチとして保存... + 親にスカッシュ + 子コミットをここにスカッシュ + 変更 + 変更を検索... + ファイル + LFSファイル + ファイルを検索... + サブモジュール + コミットの情報 + 著者 + 変更 + + コミッター + このコミットを含む参照を確認 + コミットが含まれるか確認 + 最初の100件の変更のみが表示されています。すべての変更は'変更'タブで確認できます。 + メッセージ + + 参照 + SHA + ブラウザで開く + 説明 + コミットのタイトルを入力 + リポジトリの設定 + コミットテンプレート + テンプレート内容: + テンプレート名: + カスタムアクション + 引数: + ${REPO} - リポジトリのパス; ${BRANCH} - 選択中のブランチ; ${SHA} - 選択中のコミットのSHA + 実行ファイル: + 名前: + スコープ: + ブランチ + コミット + リポジトリ + アクションの終了を待機 + Eメールアドレス + Eメールアドレス + GIT + 自動的にリモートからフェッチ 間隔: + 分(s) + リモートの初期値 + ISSUEトラッカー + サンプルのAzure DevOpsルールを追加 + サンプルのGitee Issueルールを追加 + サンプルのGiteeプルリクエストルールを追加 + サンプルのGithubルールを追加 + サンプルのGitLab Issueルールを追加 + サンプルのGitLabマージリクエストルールを追加 + サンプルのJiraルールを追加 + 新しいルール + Issueの正規表現: + ルール名: + リザルトURL: + 正規表現のグループ値に$1, $2を使用してください。 + AI + 優先するサービス: + 優先するサービスが設定されている場合、SourceGitはこのリポジトリでのみそれを使用します。そうでない場合で複数サービスが利用できる場合は、そのうちの1つを選択するためのコンテキストメニューが表示されます。 + HTTP プロキシ + このリポジトリで使用するHTTPプロキシ + ユーザー名 + このリポジトリにおけるユーザー名 + ワークスペース + + 名前 + 起動時にタブを復元 + Conventional Commitヘルパー + 破壊的変更: + 閉じたIssue: + 詳細な変更: + スコープ: + 短い説明: + 変更の種類: + コピー + すべてのテキストをコピー + 絶対パスをコピー + パスをコピー + ブランチを作成... + 派生元: + 作成したブランチにチェックアウト + ローカルの変更: + 破棄 + スタッシュして再適用 + 新しいブランチの名前: + ブランチの名前を入力 + スペースはダッシュに置き換えられます。 + ローカルブランチを作成 + タグを作成... + 付与されるコミット: + GPG署名を使用 + タグメッセージ: + 任意。 + タグの名前: + 推奨フォーマット: v1.0.0-alpha + 作成後にすべてのリモートにプッシュ + 新しいタグを作成 + 種類: + 注釈付き + 軽量 + Ctrlキーを押しながらクリックで実行 + 切り取り + ブランチを削除 + ブランチ: + リモートブランチを削除しようとしています!!! + もしリモートブランチを削除する場合、${0}$も削除します。 + 複数のブランチを削除 + 一度に複数のブランチを削除しようとしています! 操作を行う前に再度確認してください! + リモートを削除 + リモート: + パス: + 対象: + すべての子ノードがリストから削除されます。 + これはリストからのみ削除され、ディスクには保存されません! + グループを削除 + リポジトリを削除 + サブモジュールを削除 + サブモジュールのパス: + タグを削除 + タグ: + リモートリポジトリから削除 + バイナリの差分 + NEW + OLD + コピー + ファイルモードが変更されました + 先頭の差分 + 空白の変更を無視 + 最後の差分 + LFSオブジェクトの変更 + 次の差分 + 変更がない、もしくはEOLの変更のみ + 前の差分 + パッチとして保存 + 隠されたシンボルを表示 + 差分の分割表示 + サブモジュール + 新規 + スワップ + シンタックスハイライト + 行の折り返し + ブロックナビゲーションを有効化 + マージツールで開く + すべての行を表示 + 表示する行数を減らす + 表示する行数を増やす + ファイルを選択すると、変更内容が表示されます + マージツールで開く + 変更を破棄 + ワーキングディレクトリのすべての変更を破棄 + 変更: + 無視したファイルを含める + {0}個の変更を破棄します。 + この操作を元に戻すことはできません!!! + ブックマーク: + 新しい名前: + 対象: + 選択中のグループを編集 + 選択中のリポジトリを編集 + カスタムアクションを実行 + アクション名: + フェッチ + すべてのリモートをフェッチ + ローカル参照を強制的に上書き + タグなしでフェッチ + リモート: + リモートの変更をフェッチ + 変更されていないとみなされる + 破棄... + {0}個のファイルを破棄... + 選択された行の変更を破棄 + 外部マージツールで開く + ${0}$ を使用して解決 + パッチとして保存... + ステージ + {0}個のファイルをステージ... + 選択された行の変更をステージ + スタッシュ... + {0}個のファイルをスタッシュ... + アンステージ + {0}個のファイルをアンステージ... + 選択された行の変更をアンステージ + 自分の変更を使用 (checkout --ours) + 相手の変更を使用 (checkout --theirs) + ファイルの履歴 + 変更 + コンテンツ + Git-Flow + 開発ブランチ: + Feature: + Feature プレフィックス: + FLOW - Finish Feature + FLOW - Finish Hotfix + FLOW - Finish Release + 対象: + Hotfix: + Hotfix プレフィックス: + Git-Flowを初期化 + ブランチを保持 + プロダクション ブランチ: + Release: + Release プレフィックス: + Start Feature... + FLOW - Start Feature + Start Hotfix... + FLOW - Start Hotfix + 名前を入力 + Start Release... + FLOW - Start Release + Versionタグ プレフィックス: + Git LFS + トラックパターンを追加... + パターンをファイル名として扱う + カスタム パターン: + Git LFSにトラックパターンを追加 + フェッチ + `git lfs fetch`を実行して、Git LFSオブジェクトをダウンロードします。ワーキングコピーは更新されません。 + LFSオブジェクトをフェッチ + Git LFSフックをインストール + ロックを表示 + ロックされているファイルはありません + ロック + 私のロックのみ表示 + LFSロック + ロック解除 + 強制的にロック解除 + 削除 + `git lfs prune`を実行して、ローカルの保存領域から古いLFSファイルを削除します。 + プル + `git lfs pull`を実行して、現在の参照とチェックアウトのすべてのGit LFSファイルをダウンロードします。 + LFSオブジェクトをプル + プッシュ + キュー内の大容量ファイルをGit LFSエンドポイントにプッシュします。 + LFSオブジェクトをプッシュ + リモート: + {0}という名前のファイルをトラック + すべての*{0}ファイルをトラック + 履歴 + 著者 + 著者時間 + グラフ & コミットのタイトル + SHA + 日時 + {0} コミットを選択しました + 'Ctrl'キーまたは'Shift'キーを押すと、複数のコミットを選択できます。 + ⌘ または ⇧ キーを押して複数のコミットを選択します。 + TIPS: + キーボードショートカットを確認 + 総合 + 現在のポップアップをキャンセル + 新しくリポジトリをクローン + 現在のページを閉じる + 次のページに移動 + 前のページに移動 + 新しいページを作成 + 設定ダイアログを開く + リポジトリ + ステージ済みの変更をコミット + ステージ済みの変更をコミットしてプッシュ + 全ての変更をステージしてコミット + 選択中のコミットから新たなブランチを作成 + 選択した変更を破棄 + 直接フェッチを実行 + ダッシュボードモード (初期値) + コミット検索モード + 直接プルを実行 + 直接プッシュを実行 + 現在のリポジトリを強制的に再読み込み + 選択中の変更をステージ/アンステージ + '変更'に切り替える + '履歴'に切り替える + 'スタッシュ'に切り替える + テキストエディタ + 検索パネルを閉じる + 次のマッチを検索 + 前のマッチを検索 + 検索パネルを開く + 破棄 + ステージ + アンステージ + リポジトリの初期化 + パス: + チェリーピックが進行中です。'中止'を押すと元のHEADが復元されます。 + コミットを処理中 + マージリクエストが進行中です。'中止'を押すと元のHEADが復元されます。 + マージ中 + リベースが進行中です。'中止'を押すと元のHEADが復元されます。 + 停止しました + 元に戻す処理が進行中です。'中止'を押すと元のHEADが復元されます。 + コミットを元に戻しています + インタラクティブ リベース + On: + 対象のブランチ: + リンクをコピー + ブラウザで開く + エラー + 通知 + ブランチのマージ + 宛先: + マージオプション: + ソースブランチ: + マージ (複数) + すべての変更をコミット + マージ戦略: + 対象: + リポジトリノードの移動 + 親ノードを選択: + 名前: + Gitが設定されていません。まず[設定]に移動して設定を行ってください。 + アプリケーションデータのディレクトリを開く + 外部ツールで開く... + 任意。 + 新しいページを開く + ブックマーク + タブを閉じる + 他のタブを閉じる + 右のタブを閉じる + リポジトリパスをコピー + リポジトリ + 貼り付け + {0} 日前 + 1 時間前 + {0} 時間前 + たった今 + 先月 + 昨年 + {0} 分前 + {0} ヶ月前 + {0} 年前 + 昨日 + 設定 + AI + 差分分析プロンプト + APIキー + タイトル生成プロンプト + モデル + 名前 + サーバー + ストリーミングを有効化 + 外観 + デフォルトのフォント + エディタのタブ幅 + フォントサイズ + デフォルト + エディタ + 等幅フォント + テキストエディタでは等幅フォントのみを使用する + テーマ + テーマの上書き + タイトルバーの固定タブ幅を使用 + ネイティブウィンドウフレームを使用 + 差分/マージ ツール + インストール パス + 差分/マージ ツールのパスを入力 + ツール + 総合 + 起動時にアップデートを確認 + 日時のフォーマット + 言語 + コミット履歴 + グラフにコミット時間の代わりに著者の時間を表示する + コミット詳細に子コミットを表示 + コミットグラフにタグを表示 + コミットタイトル枠の大きさ + GIT + 自動CRLFを有効化 + デフォルトのクローンディレクトリ + ユーザー Eメールアドレス + グローバルgitのEメールアドレス + フェッチ時に--pruneを有効化 + Git (>= 2.25.1) はこのアプリで必要です + インストール パス + HTTP SSL 検証を有効にする + ユーザー名 + グローバルのgitユーザー名 + Gitバージョン + GPG 署名 + コミットにGPG署名を行う + GPGフォーマット + プログラムのインストールパス + インストールされたgpgプログラムのパスを入力 + タグにGPG署名を行う + ユーザー署名キー + ユーザーのGPG署名キー + 統合 + シェル/ターミナル + パス + シェル/ターミナル + リモートを削除 + 対象: + 作業ツリーを削除 + `$GIT_DIR/worktrees` の作業ツリー情報を削除 + プル + ブランチ: + 宛先: + ローカルの変更: + 破棄 + スタッシュして再適用 + リモート: + プル (フェッチ & マージ) + マージの代わりにリベースを使用 + プッシュ + サブモジュールがプッシュされていることを確認 + 強制的にプッシュ + ローカル ブランチ: + リモート: + 変更をリモートにプッシュ + リモート ブランチ: + 追跡ブランチとして設定 + すべてのタグをプッシュ + リモートにタグをプッシュ + すべてのリモートにプッシュ + リモート: + タグ: + 終了 + 現在のブランチをリベース + ローカルの変更をスタッシュして再適用 + On: + リベース: + リモートを追加 + リモートを編集 + 名前: + リモートの名前 + リポジトリのURL: + リモートのgitリポジトリのURL + URLをコピー + 削除... + 編集... + フェッチ + ブラウザで開く + 削除 + ワークツリーの削除を確認 + `--force` オプションを有効化 + 対象: + ブランチの名前を編集 + 新しい名前: + このブランチにつける一意な名前 + ブランチ: + 中止 + リモートから変更を自動取得中... + クリーンアップ(GC & Prune) + このリポジトリに対して`git gc`コマンドを実行します。 + すべてのフィルターをクリア + リポジトリの設定 + 続ける + カスタムアクション + カスタムアクションがありません + すべての変更を破棄 + `--reflog` オプションを有効化 + ファイルブラウザーで開く + ブランチ/タグ/サブモジュールを検索 + 解除 + コミットグラフで非表示 + コミットグラフでフィルター + `--first-parent` オプションを有効化 + レイアウト + 水平 + 垂直 + コミットの並び順 + 日時 + トポロジカルソート + ローカル ブランチ + HEADに移動 + ブランチを作成 + 通知をクリア + グラフで現在のブランチを強調表示 + {0} で開く + 外部ツールで開く + 更新 + リモート + リモートを追加 + コミットを検索 + 著者 + コミッター + ファイル + メッセージ + SHA + 現在のブランチ + タグをツリーとして表示 + スキップ + 統計 + サブモジュール + サブモジュールを追加 + サブモジュールを更新 + タグ + 新しいタグを作成 + 作成者日時 + 名前 + ソート + ターミナルで開く + 履歴に相対時間を使用 + ワークツリー + ワークツリーを追加 + 削除 + GitリポジトリのURL + 現在のブランチをリビジョンにリセット + リセットモード: + 移動先: + 現在のブランチ: + ファイルエクスプローラーで表示 + コミットを戻す + コミット: + コミットの変更を戻す + コミットメッセージを書き直す + 改行には'Shift+Enter'キーを使用します。 'Enter"はOKボタンのホットキーとして機能します。 + 実行中です。しばらくお待ちください... + 保存 + 名前を付けて保存... + パッチが正常に保存されました! + リポジトリをスキャン + ルートディレクトリ: + 更新を確認 + 新しいバージョンのソフトウェアが利用可能です: + 更新の確認に失敗しました! + ダウンロード + このバージョンをスキップ + ソフトウェアの更新 + 利用可能なアップデートはありません + トラッキングブランチを設定 + ブランチ: + 上流ブランチを解除 + 上流ブランチ: + SHAをコピー + Go to + スカッシュコミット + 宛先: + SSH プライベートキー: + プライベートSSHキーストアのパス + スタート + スタッシュ + スタッシュ後に自動で復元 + 作業ファイルは変更されず、スタッシュが保存されます。 + 追跡されていないファイルを含める + ステージされたファイルを保持 + メッセージ: + オプション. このスタッシュの名前を入力 + ステージされた変更のみ + 選択したファイルの、ステージされた変更とステージされていない変更の両方がスタッシュされます!!! + ローカルの変更をスタッシュ + 適用 + 破棄 + パッチとして保存 + スタッシュを破棄 + 破棄: + スタッシュ + 変更 + スタッシュ + 統計 + コミット + コミッター + 概要 + 月間 + 週間 + 著者: + コミット: + サブモジュール + サブモジュールを追加 + 相対パスをコピー + ネストされたサブモジュールを取得する + サブモジュールのリポジトリを開く + 相対パス: + このモジュールを保存するフォルダの相対パス + サブモジュールを削除 + OK + タグ名をコピー + タグメッセージをコピー + ${0}$ を削除... + ${0}$ を ${1}$ にマージ... + ${0}$ をプッシュ... + サブモジュールを更新 + すべてのサブモジュール + 必要に応じて初期化 + 再帰的に更新 + サブモジュール: + --remoteオプションを使用 + URL: + 警告 + ようこそ + グループを作成 + サブグループを作成 + リポジトリをクローンする + 削除 + ドラッグ & ドロップでフォルダを追加できます. グループを作成したり、変更したりできます。 + 編集 + 別のグループに移動 + すべてのリポジトリを開く + リポジトリを開く + ターミナルを開く + デフォルトのクローンディレクトリ内のリポジトリを再スキャン + リポジトリを検索... + ソート + 変更 + Git Ignore + すべての*{0}ファイルを無視 + 同じフォルダ内の*{0}ファイルを無視 + 同じフォルダ内のファイルを無視 + このファイルのみを無視 + Amend + このファイルを今すぐステージできます。 + コミット + コミットしてプッシュ + メッセージのテンプレート/履歴 + クリックイベントをトリガー + コミット (Edit) + すべての変更をステージしてコミット + 競合が検出されました + ファイルの競合は解決されました + 追跡されていないファイルを含める + 最近の入力メッセージはありません + コミットテンプレートはありません + 選択したファイルを右クリックし、競合を解決する操作を選択してください。 + サインオフ + ステージしたファイル + ステージを取り消し + すべてステージを取り消し + 未ステージのファイル + ステージへ移動 + すべてステージへ移動 + 変更されていないとみなしたものを表示 + テンプレート: ${0}$ + ワークスペース: + ワークスペースを設定... + ワークツリー + パスをコピー + ロック + 削除 + ロック解除 + diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 8a970730..f448a908 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -2,37 +2,27 @@ + Sobre Sobre o SourceGit - • Construído com - © 2024 sourcegit-scm - • Editor de Texto de - • Fontes monoespaçadas de - • Código-fonte pode ser encontrado em Cliente Git GUI Livre e de Código Aberto Adicionar Worktree - O que Checar: - Branch Existente - Criar Nova Branch Localização: Caminho para este worktree. Caminho relativo é suportado. - Nome da Branch: + Nome do Branch: Opcional. O padrão é o nome da pasta de destino. Rastrear Branch: - Rastreando branch remota + Rastreando branch remoto + O que Checar: + Criar Novo Branch + Branch Existente + Assietente IA + Utilizar IA para gerar mensagem de commit Patch - Erro - Erros levantados e se recusa a aplicar o patch - Erro Total - Semelhante a 'erro', mas mostra mais Arquivo de Patch: Selecione o arquivo .patch para aplicar Ignorar mudanças de espaço em branco - Sem Aviso - Desativa o aviso de espaço em branco no final Aplicar Patch - Aviso - Emite avisos para alguns erros, mas aplica Espaço em Branco: Arquivar... Salvar Arquivo Como: @@ -40,51 +30,53 @@ Revisão: Arquivar SourceGit Askpass - ARQUIVOS ASSUMIDOS COMO INALTERADOS - NENHUM ARQUIVO ASSUMIDO COMO INALTERADO + ARQUIVOS CONSIDERADOS SEM ALTERAÇÕES + NENHUM ARQUIVO CONSIDERADO SEM ALTERAÇÕES REMOVER + Atualizar ARQUIVO BINÁRIO NÃO SUPORTADO!!! - Responsabilizar - RESPONSABILIZAÇÃO PARA ESTE ARQUIVO NÃO SUPORTADA!!! - Checar ${0}$... - Comparar com Branch - Comparar com HEAD + Blame + BLAME NESTE ARQUIVO NÃO É SUPORTADO!!! + Checkout ${0}$... + Comparar com ${0}$ Comparar com Worktree - Copiar Nome da Branch + Copiar Nome do Branch Excluir ${0}$... - Excluir {0} branches selecionadas - Descartar todas as alterações - Avançar para ${0}$ + Excluir {0} branches selecionados + Fast-Forward para ${0}$ + Buscar ${0}$ em ${1}$... Git Flow - Finalizar ${0}$ Mesclar ${0}$ em ${1}$... Puxar ${0}$ Puxar ${0}$ para ${1}$... - Empurrar ${0}$ + Subir ${0}$ Rebase ${0}$ em ${1}$... Renomear ${0}$... - Definir Branch de Rastreamento - Desfazer Upstream - Comparar Branch + Definir Branch de Rastreamento... + Comparação de Branches Bytes CANCELAR + Resetar para Revisão Pai Resetar para Esta Revisão - Resetar to Revisão Pai + Gerar mensagem de commit ALTERAR MODO DE EXIBIÇÃO - Mostrar como Grade - Mostrar como Lista de Caminhos - Mostrar como Árvore de Arquivos do Sistema + Exibir como Lista de Arquivos e Diretórios + Exibir como Lista de Caminhos + Exibir como Árvore de Sistema de Arquivos Checkout Branch Checkout Commit - Aviso: Ao fazer o checkout de um commit, seu Head ficará desanexado Commit: - Branch: + Aviso: Ao fazer o checkout de um commit, seu Head ficará desanexado Alterações Locais: Descartar - Nada Stash & Reaplicar + Branch: Cherry-Pick + Adicionar origem à mensagem de commit Commit(s): Commitar todas as alterações + Mainline: + Geralmente você não pode fazer cherry-pick de um merge commit porque você não sabe qual lado do merge deve ser considerado na mainline. Esta opção permite ao cherry-pick reaplicar a mudança relativa ao parent especificado. Limpar Stashes Você está tentando limpar todas as stashes. Tem certeza que deseja continuar? Clonar Repositório Remoto @@ -96,12 +88,14 @@ URL do Repositório: FECHAR Editor - Cherry-Pick Este Commit Checar Commit + Cherry-Pick este commit + Cherry-Pick ... Comparar com HEAD Comparar com Worktree - Copiar Informações - Copiar SHA + Informações + SHA + Ação customizada Rebase Interativo ${0}$ até Aqui Rebase ${0}$ até Aqui Resetar ${0}$ até Aqui @@ -109,6 +103,7 @@ Modificar Mensagem Salvar como Patch... Mesclar ao Commit Pai + Mesclar commits filhos para este ALTERAÇÕES Buscar Alterações... ARQUIVOS @@ -118,47 +113,74 @@ AUTOR ALTERADO COMMITTER + Verificar referências que contenham este commit + COMMIT EXISTE EM Mostra apenas as primeiras 100 alterações. Veja todas as alterações na aba ALTERAÇÕES. MENSAGEM PAIS REFERÊNCIAS SHA - Insira o assunto do commit + Abrir no navegador Descrição + Insira o assunto do commit Configurar Repositório TEMPLATE DE COMMIT - Nome do Template: Conteúdo do Template: - Endereço de Email + Nome do Template: + AÇÃO CUSTOMIZADA + Argumentos: + ${REPO} - Caminho do repositório; ${SHA} - SHA do commit selecionado + Caminho do executável: + Nome: + Escopo: + Commit + Repositório + Endereço de email Endereço de email GIT Buscar remotos automaticamente Minuto(s) + Remoto padrão RASTREADOR DE PROBLEMAS + Adicionar Regra de Exemplo do Azure DevOps Adicionar Regra de Exemplo do Github + Adicionar Regra de Exemplo do GitLab + Adicionar regra de exemplo de Merge Request do GitLab 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. + IA + Serviço desejado: + Se o 'Serviço desejado' for definido, SourceGit usará ele neste Repositório. Senão, caso haja mais de um serviço disponível, será exibido um menu para seleção. Proxy HTTP Proxy HTTP usado por este repositório Nome de Usuário Nome de usuário para este repositório + Workspaces + Cor + Nome + Restaurar abas ao inicializar + Assistente de Conventional Commit + Breaking Change: + Ticket encerrado: + Detalhes: + Escopo: + Breve resumo: + Tipo de mudança: Copiar - COPIAR MENSAGEM + Copiar todo o texto Copiar Caminho - Copiar Nome do Arquivo Criar Branch... Baseado Em: - Checar a branch criada + Checar o branch criado Alterações Locais: Descartar - Não Fazer Nada Guardar & Reaplicar - Nome da Nova Branch: - Insira o nome da branch. + Nome do Novo Branch: + Insira o nome do branch. Criar Branch Local Criar Tag... Nova Tag Em: @@ -177,9 +199,9 @@ Excluir Branch Branch: Você está prestes a excluir uma branch remota!!! - Também excluir branch remota ${0}$ - Excluir Múltiplas Branches - Você está tentando excluir várias branches de uma vez. Certifique-se de verificar antes de agir! + Também excluir branch remoto ${0}$ + Excluir Múltiplos Branches + Você está tentando excluir vários branches de uma vez. Certifique-se de verificar antes de agir! Excluir Remoto Remoto: Alvo: @@ -195,11 +217,13 @@ ANTIGO Copiar Modo de Arquivo Alterado + Ignorar mudanças de espaço em branco MUDANÇA DE OBJETO LFS Próxima Diferença SEM MUDANÇAS OU APENAS MUDANÇAS DE EOL Diferença Anterior - Mostrar símbolos ocultos + Salvar como um Patch + Exibir símbolos ocultos Diferença Lado a Lado SUBMÓDULO NOVO @@ -207,6 +231,7 @@ Realce de Sintaxe Quebra de Linha Abrir na Ferramenta de Mesclagem + Exibir todas as linhas Diminuir Número de Linhas Visíveis Aumentar Número de Linhas Visíveis SELECIONE O ARQUIVO PARA VISUALIZAR AS MUDANÇAS @@ -214,6 +239,7 @@ Descartar Alterações Todas as alterações locais na cópia de trabalho. Alterações: + Incluir arquivos ignorados Um total de {0} alterações será descartado Você não pode desfazer esta ação!!! Favorito: @@ -221,11 +247,11 @@ Alvo: Editar Grupo Selecionado Editar Repositório Selecionado - Fast-Forward (sem checkout) + Executar ação customizada + Nome da ação: Buscar Buscar todos os remotos Buscar sem tags - Prune remotos mortos Remoto: Buscar Alterações Remotas Assumir não alterado @@ -242,10 +268,11 @@ Desfazer Preparação Desfazer Preparação de {0} arquivos Desfazer Preparação nas Linhas Selecionadas - Usar Deles (checkout --theirs) Usar Meu (checkout --ours) + Usar Deles (checkout --theirs) Histórico de Arquivos - FILTRO + MUDANÇA + CONTEUDO Git-Flow Branch de Desenvolvimento: Feature: @@ -275,48 +302,58 @@ Padrão Personalizado: Adicionar Padrão de Rastreamento ao Git LFS Buscar - Buscar Objetos LFS Execute `git lfs fetch` para baixar objetos Git LFS. Isso não atualiza a cópia de trabalho. + Buscar Objetos LFS Instalar hooks do Git LFS - Mostrar Locks + Exibir bloqueios Sem Arquivos Bloqueados Bloquear - Locks LFS + Exibir apenas meus bloqueios + Bloqueios LFS Desbloquear Forçar Desbloqueio Prune Execute `git lfs prune` para excluir arquivos LFS antigos do armazenamento local Puxar - Puxar Objetos LFS Execute `git lfs pull` para baixar todos os arquivos Git LFS para a referência atual e checkout + Puxar Objetos LFS Enviar - Enviar Objetos LFS Envie arquivos grandes enfileirados para o endpoint Git LFS + Enviar Objetos LFS Remoto: Rastrear arquivos nomeados '{0}' Rastrear todos os arquivos *{0} Históricos - Alternar Layout Horizontal/Vertical AUTOR + DATA DO AUTOR GRÁFICO & ASSUNTO SHA HORA DO COMMIT SELECIONADO {0} COMMITS + Segure 'Ctrl' ou 'Shift' para selecionar múltiplos commits. + Segure ⌘ ou ⇧ para selecionar múltiplos commits. + DICAS: Referência de Atalhos de Teclado GLOBAL Cancelar popup atual Fechar página atual - Ir para a página anterior Ir para a próxima página + Ir para a página anterior Criar nova página - Abrir diálogo de preferências + Abrir diálogo de preferências REPOSITÓRIO Commitar mudanças preparadas Commitar e enviar mudanças preparadas + Preparar todas as mudanças e commitar + Cria um novo branch partindo do commit selecionado + Descartar mudanças selecionadas + Buscar, imediatamente Modo de Dashboard (Padrão) + Modo de busca de commits + Puxar, imediatamente + Enviar, imediatamente Forçar recarregamento deste repositório Preparar/Despreparar mudanças selecionadas - Modo de busca de commits Alternar para 'Mudanças' Alternar para 'Históricos' Alternar para 'Stashes' @@ -325,24 +362,27 @@ Encontrar próxima correspondência Encontrar correspondência anterior Abrir painel de busca + Descartar Preparar Despreparar - Descartar Inicializar Repositório Caminho: - Cherry-Pick em andamento. Pressione 'Abort' para restaurar o HEAD original. - Merge em andamento. Pressione 'Abort' para restaurar o HEAD original. - Rebase em andamento. Pressione 'Abort' para restaurar o HEAD original. - Revert em andamento. Pressione 'Abort' para restaurar o HEAD original. + Cherry-Pick em andamento. + Merge em andamento. + Rebase em andamento. + Revert em andamento. Rebase Interativo - Ramo Alvo: Em: + Ramo Alvo: + Copiar link + Abrir no navegador ERRO AVISO Mesclar Ramo Para: Opção de Mesclagem: - Ramo de Origem: + Mover nó do repositório + Selecionar nó pai para: Nome: O Git NÃO foi configurado. Por favor, vá para [Preferências] e configure primeiro. Abrir Pasta de Dados do Aplicativo @@ -356,68 +396,83 @@ Copiar Caminho do Repositório Repositórios Colar - Agora mesmo - {0} minutos atrás - {0} horas atrás - Ontem {0} dias atrás + 1 hora atrás + {0} horas atrás + Agora mesmo Mês passado - {0} meses atrás Ano passado + {0} minutos atrás + {0} meses atrás {0} anos atrás - Preferências - APARÊNCIA - Fonte Padrão - Tamanho da Fonte Padrão - Fonte Monoespaçada - Usar apenas fonte monoespaçada no editor de texto - Tema - Sobrescrever Tema - Usar largura fixa da aba na barra de título - FERRAMENTA DE DIF/MERGE - Caminho de Instalação - Insira o caminho para a ferramenta de dif/merge - Ferramenta - GERAL - Verificar atualizações na inicialização - Idioma - Commits do Histórico - Comprimento do Guia de Assunto - GIT - Habilitar Auto CRLF - Diretório Padrão de Clone - E-mail do Usuário - E-mail global do usuário git - Caminho de Instalação - Nome do Usuário - Nome global do usuário git - Versão do Git - Git (>= 2.23.0) é necessário para este aplicativo - ASSINATURA GPG - Assinatura GPG de Commit - Assinatura GPG de Tag - Formato GPG - Caminho de Instalação do Programa - Insira o caminho do programa gpg instalado - Chave de Assinatura do Usuário - Chave de assinatura gpg do usuário + Ontem + Preferências + INTELIGÊNCIA ARTIFICIAL + Prompt para Analisar Diff + Chave da API + Prompt para Gerar Título + Modelo + Nome + Servidor + APARÊNCIA + Fonte Padrão + Tamanho da Fonte + Padrão + Editor + Fonte Monoespaçada + Usar fonte monoespaçada apenas no editor de texto + Tema + Substituições de Tema + Usar largura fixa de aba na barra de título + Usar moldura de janela nativa + FERRAMENTA DE DIFF/MERGE + Caminho de Instalação + Insira o caminho para a ferramenta de diff/merge + Ferramenta + GERAL + Verificar atualizações na inicialização + Idioma + Commits do Histórico + Exibir data do autor em vez da data do commit no gráfico + Comprimento do Guia de Assunto + GIT + Habilitar Auto CRLF + Diretório de Clone Padrão + Email do Usuário + Email global do usuário git + Habilita --prune ao buscar + Git (>= 2.25.1) é necessário para este aplicativo + Caminho de Instalação + Nome do Usuário + Nome global do usuário git + Versão do Git + ASSINATURA GPG + Assinatura GPG de commit + Formato GPG + Caminho de Instalação do Programa + Insira o caminho para o programa gpg instalado + Assinatura GPG de tag + Chave de Assinatura do Usuário + Chave de assinatura gpg do usuário + INTEGRAÇÃO + SHELL/TERMINAL + Caminho + Shell/Terminal Prunar Remoto Alvo: Podar Worktrees - Podar informações de worktree em `$GIT_DIR/worktrees` + Podar informações de worktree em `$GIT_COMMON_DIR/worktrees` Puxar - Branch: - Buscar todos os branches + Branch Remoto: Para: Alterações Locais: Descartar - Não Fazer Nada Guardar & Reaplicar - Buscar sem tags Remoto: Puxar (Buscar & Mesclar) Usar rebase em vez de merge Empurrar + Certifica de que submodules foram enviadas Forçar push Branch Local: Remoto: @@ -434,7 +489,6 @@ Guardar & reaplicar alterações locais Em: Rebase: - Atualizar Adicionar Remoto Editar Remoto Nome: @@ -447,7 +501,6 @@ Buscar Abrir no Navegador Podar - Alvo: Confirmar Remoção de Worktree Habilitar Opção `--force` Alvo: @@ -456,30 +509,40 @@ Nome único para este branch Branch: ABORTAR + Buscando automaticamente mudanças dos remotos... Limpar (GC & Podar) Execute o comando `git gc` para este repositório. Limpar tudo Configurar este repositório CONTINUAR + Ações customizada + Nenhuma ação customizada + Descartar todas as alterações + Habilitar opção '--reflog' Abrir no Navegador de Arquivos Pesquisar Branches/Tags/Submódulos - FILTRADO POR: + Desfazer + Esconder no gráfico de commit + Incluir no gráfico de commit + Habilitar opção '--first-parent' + Data do Commit + Topologicamente BRANCHES LOCAIS Navegar para HEAD - Filtro do Primeiro Pai Criar Branch Abrir em {0} Abrir em Ferramentas Externas Atualizar REMOTOS ADICIONAR REMOTO - RESOLVER Pesquisar Commit + Autor + Committer Arquivo Mensagem SHA - Autor & Committer - Mostrar Tags como Árvore + Branch Atual + Exibir Tags como Árvore Estatísticas SUBMÓDULOS ADICIONAR SUBMÓDULO @@ -505,6 +568,8 @@ SALVAR Salvar Como... Patch salvo com sucesso! + Escanear Repositórios + Diretório Raiz: Verificar atualizações... Nova versão deste software disponível: Falha ao verificar atualizações! @@ -512,18 +577,22 @@ Ignorar esta versão Atualização de Software Não há atualizações disponíveis no momento. + Copiar SHA Squash Commits + Squash commits em: Chave SSH Privada: Caminho para a chave SSH privada INICIAR Stash Incluir arquivos não rastreados + Manter arquivos em stage Mensagem: Opcional. Nome deste stash + Apenas mudanças em stage + Tanto mudanças em stage e fora de stage dos arquivos selecionados serão enviadas para stash!!! Guardar Alterações Locais Aplicar Descartar - Pop Descartar Stash Descartar: Stashes @@ -532,10 +601,11 @@ Estatísticas COMMITS COMMITTER + VISÃO GERAL MÊS SEMANA - COMMITS: AUTORES: + COMMITS: SUBMÓDULOS Adicionar Submódulo Copiar Caminho Relativo @@ -546,15 +616,17 @@ Excluir Submódulo OK Copiar Nome da Tag + Copiar mensage da Tag Excluir ${0}$... + Mesclar ${0}$ em ${1}$... Enviar ${0}$... - URL: Atualizar Submódulos Todos os submódulos Inicializar conforme necessário Recursivamente Submódulo: Usar opção --remote + URL: Aviso Página de Boas-vindas Criar Grupo Raíz @@ -563,9 +635,11 @@ Excluir ARRASTAR E SOLTAR PASTAS SUPORTADO. AGRUPAMENTO PERSONALIZADO SUPORTADO. Editar + Mover para Outro Grupo Abrir Todos os Repositórios Abrir Repositório Abrir Terminal + Reescanear Repositórios no Diretório de Clone Padrão Buscar Repositórios... Ordenar Alterações @@ -575,28 +649,31 @@ Ignorar arquivos na mesma pasta Ignorar apenas este arquivo Corrigir - Auto-Stage Você pode stagear este arquivo agora. COMMIT - COMMIT & PUSH - Template/Histories + COMMITAR E ENVIAR + Modelo/Históricos + Acionar evento de clique + Preparar todas as mudanças e commitar CONFLITOS DETECTADOS - CONFLITOS DE ARQUIVOS RESOLVIDOS + CONFLITOS DE ARQUIVO RESOLVIDOS INCLUIR ARQUIVOS NÃO RASTREADOS - NENHUMA MENSAGEM DE ENTRADA RECENTE - NENHUM TEMPLATE DE COMMIT - STAGED - DESSTAGEAR - DESSTAGEAR TODOS - NÃO STAGED - STAGEAR - STAGEAR TODOS - VER SUPOR NÃO ALTERADO - Template: ${0}$ + SEM MENSAGENS DE ENTRADA RECENTES + SEM MODELOS DE COMMIT Clique com o botão direito nos arquivos selecionados e escolha como resolver conflitos. + STAGED + UNSTAGE + UNSTAGE TODOS + UNSTAGED + STAGE + STAGE TODOS + VER SUPOR NÃO ALTERADO + Template: ${0}$ + Workspaces: + Configurar workspaces... WORKTREE Copiar Caminho - Travar + Bloquear Remover - Destravar + Desbloquear diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 23ec125f..a625df98 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -2,384 +2,457 @@ + О программе О SourceGit - • Сборка с - • Диаграмма отображается с помощью - © 2024 sourcegit-scm - • Текстовый редактор от - • Моноширинные шрифты взяты из - • Исходный код можно найти по адресу Бесплатный графический клиент Git с исходным кодом - Добавить рабочее дерево - Что проверить: - Существующую ветку - Создать новую ветку + Добавить рабочий каталог Расположение: - Путь к этому рабочему дереву. Поддерживается относительный путью + Путь к рабочему каталогу (поддерживается относительный путь) Имя ветки: - Необязательно. По умолчанию используется имя целевой папки. + Имя целевого каталога по умолчанию. (необязательно) Отслеживание ветки: Отслеживание внешней ветки - OpenAI Ассистент - Использовать OpenAI для создания сообщения о фиксации + Переключиться на: + Создать новую ветку + Ветку из списка + Помощник OpenAI + ПЕРЕСОЗДАТЬ + Использовать OpenAI для создания сообщения о ревизии + ПРИМЕНИТЬ КАК СООБЩЕНИЕ РЕВИЗИИ Исправить - Ошибка - Выдает ошибки и отказывается применять исправление - Все ошибки - Аналогично "ошибке", но показывает больше - Файл исправлений: + Файл заплатки: Выберите файл .patch для применения Игнорировать изменения пробелов - Нет предупреждений - Отключает предупреждение о пробелах в конце - Применить исправление - Предупреждение - Выдает предупреждения о нескольких таких ошибках, но применяет + Применить заплатку Пробел: + Отложить + Удалить после применения + Восстановить изменения индекса + Отложенный: Архивировать... Сохранить архив в: Выберите путь к архивному файлу Ревизия: Архив Спросить разрешения SourceGit - ФАЙЛЫ СЧИТАЮТСЯ НЕИЗМЕНЕННЫМИ - НИ ОДИН ФАЙЛ НЕ СЧИТАЕТСЯ НЕИЗМЕНЕННЫМ + НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ + СПИСОК ПУСТ УДАЛИТЬ + Загрузить картинку... + Обновить ДВОИЧНЫЙ ФАЙЛ НЕ ПОДДЕРЖИВАЕТСЯ!!! - Обвинение - ОБВИНЕНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! - Проверить ${0}$... - Сравнить в веткой - Сравнить в заголовком - Сравнить в рабочим деревом + Раздвоить + О + Плохая + Раздвоение. Текущая ГОЛОВА (HEAD) хорошая или плохая? + Хорошая + Пропустить + Раздвоение. Сделать текущую ревизию хорошей или плохой и переключиться на другой. + Расследование + РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! + Переключиться на ${0}$... + Сравнить с ${0}$ + Сравнить с рабочим каталогом Копировать имя ветки + Изменить действие Удалить ${0}$... Удалить выбранные {0} ветки - Отклонить все изменения. Перемотать вперёд к ${0}$ - Поток Git - Завершение ${0}$ - Слить ${0}$ в ${1}$... - Забрать ${0}$ - Перетащить ${0}$ в ${1}$... + Извлечь ${0}$ в ${1}$... + Git-процесс - Завершение ${0}$ + Влить ${0}$ в ${1}$... + Влить {0} выделенных веток в текущую + Загрузить ${0}$ + Загрузить ${0}$ в ${1}$... Выложить ${0}$ Переместить ${0}$ на ${1}$... Переименовать ${0}$... - Установить отслеживание ветки - Отключить основной поток - Сравнение ветвей + Сбросить ${0}$ к ${1}$... + Отслеживать ветку... + Сравнение веток + Недопустимая основная ветка! Байты ОТМЕНА - Сбросить эту ревизию Сбросить родительскую ревизию + Сбросить эту ревизию + Произвести сообщение о ревизии ИЗМЕНИТЬ РЕЖИМ ОТОБРАЖЕНИЯ Показывать в виде списка файлов и каталогов - Показать в виде списка путей - Показать в виде дерева файловой системы - Проверить ветку - Проверить фиксацию - Предупреждение: При выполнении проверки фиксации ваша голова будет отсоединена - Фиксация: - Ветка: + Показывать в виде списка путей + Показывать в виде дерева файловой системы + Переключить ветку + Переключение ревизии + Ревизия: + Предупреждение: После переключения ревизии ваша Голова (HEAD) будет отсоединена Локальные изменения: Отклонить - Ничего не делать Отложить и примненить повторно + Обновить все подкаталоги + Ветка: Частичный выбор - Добавить источник для фиксации сообщения - Фиксация(и): - Фиксировать все изменения. + Добавить источник для ревизии сообщения + Ревизия(и): + Ревизия всех изменений. Основной: - Обычно вы не можете выделить слияние, потому что не знаете, какую сторону слияния следует считать основной. Эта опция позволяет отобразить изменение относительно указанного родительского элемента. + Обычно вы не можете выделить слияние, потому что не знаете, какую сторону слияния следует считать основной. Эта опция позволяет отобразить изменения относительно указанного родительского элемента. Очистить отложенные - Вы пытаетесь очистить все отложенные. Вы уверены, что будете продолжать? - Клонировать внешнее хранилище + Вы пытаетесь очистить все отложенные. Вы уверены, что хотите продолжить? + Клонировать внешний репозиторий Расширенные параметры: - Дополнительные аргументы для клонирования хранилища. Необязательно. + Дополнительные аргументы для клонирования репозитория. (необязательно). Локальное имя: - Имя хранилища. Необязательно. + Имя репозитория. (необязательно). Родительский каталог: - Адрес хранилища: + Создать и обновить подмодуль + Адрес репозитория: ЗАКРЫТЬ Редактор - Выбрать из списка эту фиксацию - Список выбора ... - Проверить фиксацию - Сравнить в заголовком - Сравнить с рабочим деревом - Копировать информацию - Копировать SHA - Интерактивное перемещение ${0}$ сюда + Переключиться на эту ревизию + Применить эту ревизию (cherry-pick) + Применить несколько ревизий ... + Сравнить c ГОЛОВОЙ (HEAD) + Сравнить с рабочим каталогом + Автор + Ревизор + Информацию + SHA + Субъект + Пользовательское действие + Интерактивное перемещение (rebase -i) ${0}$ сюда + Влить в ${0}$ + Влить ... Переместить ${0}$ сюда Сбросить ${0}$ сюда - Вернуть фиксацию - Переформулировать - Сохранить как исправление... - Уплотнить в родительскую - Уплотнить дочерную фиксацию сюда + Отменить ревизию + Изменить комментарий + Сохранить как заплатки... + Объединить с предыдущей ревизией + Объединить все следующие ревизии с этим ИЗМЕНЕНИЯ + изменённый(х) файл(ов) Найти изменения.... ФАЙЛЫ - Файл ХБФ + Файл LFS + Поиск файлов... Подмодуль ИНФОРМАЦИЯ АВТОР ИЗМЕНЁННЫЙ - ИСПОЛНИТЕЛЬ - Проверить ссылки, содержащие эту фиксацию - ФИКСАЦИЯ СОДЕРЖИТСЯ В + ДОЧЕРНИЙ + РЕВИЗОР (ИСПОЛНИТЕЛЬ) + Найти все ветки с этой ревизией + ВЕТКИ С ЭТОЙ РЕВИЗИЕЙ Отображаются только первые 100 изменений. Смотрите все изменения на вкладке ИЗМЕНЕНИЯ. СООБЩЕНИЕ РОДИТЕЛИ ССЫЛКИ SHA Открыть в браузере - Введите тему фиксации Описание - Настройка хранилища - ШАБЛОН ФИКСАЦИИ - Имя шаблона: - Шаблон содержания: + СУБЪЕКТ + Введите тему ревизии + Настройка репозитория + ШАБЛОН РЕВИЗИИ + Cодержание: + Название: + ПОЛЬЗОВАТЕЛЬСКОЕ ДЕЙСТВИЕ + Аргументы: + ${REPO} - Путь к репозиторию; ${SHA} - SHA ревизий + Исполняемый файл: + Имя: + Диапазон: + Ветка + Ревизия + Репозиторий + Ждать для выполения выхода Адрес электронной почты Адрес электронной почты GIT - Автоматическое извлечение внешних хранилищ + Автозагрузка изменений Минут(а/ы) - Удалённое хранилище по-умолчанию + Внешний репозиторий по умолчанию + Предпочтительный режим слияния ОТСЛЕЖИВАНИЕ ПРОБЛЕМ + Добавить пример правила Azure DevOps + Добавить пример правила для тем в Gitea + Добавить пример правила запроса скачивания из Gitea Добавить пример правила для Git + Добавить пример правила выдачи GitLab + Добавить пример правила запроса на слияние в GitLab Добавить пример правила Jira Новое правило Проблема с регулярным выражением: Имя правила: Адрес результата: Пожалуйста, используйте $1, $2 для доступа к значениям групп регулярных выражений. + ОТКРЫТЬ ИИ + Предпочтительный сервис: + Если «Предпочтительный сервис» установлен, SourceGit будет использовать только этот репозиторий. В противном случае, если доступно более одной услуги, будет отображено контекстное меню для выбора одной из них. HTTP-прокси - HTTP-прокси, используемый этим хранилищем - Имя пользовтаеля - Имя пользователя для этого хранилища + HTTP-прокси для репозитория + Имя пользователя + Имя пользователя репозитория Рабочие пространства - Имя Цвет + Имя Восстанавливать вкладки при запуске - Общепринятый помощник по фиксации изменений + ПРОДОЛЖИТЬ + Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty)? + Сформировать всё и зафиксировать ревизию + Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty) или отложить всё, затем зафиксировать ревизию? + Общепринятый помощник по ревизии Кардинальные изменения: Закрытая тема: Детали изменений: Область: - Коротнкое описание: + Короткое описание: Тип изменения: Копировать Копировать весь текст - КОПИРОВАТЬ СООБЩЕНИЕ + Копировать полный путь Копировать путь - Копировать имя файла Создать ветку... Основан на: - Проверить созданную ветку + Переключиться на созданную ветку Локальные изменения: Отклонить - Ничего не делать Отложить и применить повторно Имя новой ветки: - Ввести имя ветки. + Введите имя ветки. + Пробелы будут заменены на тире. Создать локальную ветку + Перезаписать существующую ветку Создать метку... Новая метка у: - Подпись GPG - Сообщение с меткой: + GPG подпись + Сообщение с меткой: Необязательно. Имя метки: Рекомендуемый формат: v1.0.0-alpha - Выложить на все внешние хранилища после создания + Выложить на все внешние репозитории после создания Создать новую метку Вид: - Аннотированный - Лёгкий - Удерживайте Ctrl, чтобы начать непосредственно + С примечаниями + Простой + Удерживайте Ctrl, чтобы сразу начать Вырезать + Удалить подмодуль + Принудительно удалить даже если содержит локальные изменения. + Подмодуль: Удалить ветку Ветка: - Вы собираетесь удалить удаленную ветку!!! - Также удалите удаленную ветку ${0}$ + Вы собираетесь удалить внешнюю ветку!!! + Также удалите внешнюю ветку ${0}$ Удаление нескольких веток Вы пытаетесь удалить несколько веток одновременно. Обязательно перепроверьте, прежде чем предпринимать какие-либо действия! - Удалить внешнее хранилище - Внешнее хранилище: + Удалить внешний репозиторий + Внешний репозиторий: + Путь: Цель: + Все дочерние элементы будут удалены из списка. + Будет удалён из списка. На диске останется. Подтвердите удаление группы - Подтвердите удаление хранилища + Подтвердите удаление репозитория Удалить подмодуль Путь подмодуля: Удалить метку Метка: - Удалить из внешнего хранилища - РАЗНИЦА БИНАРНИКОВ + Удалить из внешнего репозитория + СРАВНЕНИЕ БИНАРНИКОВ НОВЫЙ СТАРЫЙ Копировать Режим файла изменён - Игнорировать изменение пробелов - Показать скрытые символы - ИЗМЕНЕНИЕ ОБЪЕКТА ХБФ - Следующее различие + Первое сравнение + Игнорировать изменения пробелов + Последнее сравнение + ИЗМЕНЕНИЕ ОБЪЕКТА LFS + Следующее сравнение НИКАКИХ ИЗМЕНЕНИЙ ИЛИ МЕНЯЕТСЯ ТОЛЬКО EOL - Предыдущее различие - Различие бок о бок + Предыдущее сравнение + Сохранить как заплатку + Показывать скрытые символы + Сравнение рядом ПОДМОДУЛЬ + УДАЛЁН НОВЫЙ Обмен Подсветка синтаксиса Перенос слов в строке + Разрешить навигацию по блокам Открыть в инструменте слияния - Уменьшить количество видимых линий - Увеличить количество видимых линий - ВЫБРАТЬ ФАЙЛ ДЛЯ ПРОСМОТРА ИЗМЕНЕНИЙ + Показывать все строки + Уменьшить количество видимых строк + Увеличить количество видимых строк + ВЫБЕРИТЕ ФАЙЛ ДЛЯ ПРОСМОТРА ИЗМЕНЕНИЙ Открыть в инструменте слияния Отклонить изменения Все локальные изменения в рабочей копии. Изменения: Включить игнорируемые файлы - Всего {0} изменений будут отменены + {0} изменений будут отменены Вы не можете отменить это действие!!! Закладка: Новое имя: Цель: Редактировать выбранную группу - Редактировать выбранное хранилище - Быстрая перемотка вперёд (без проверки) + Редактировать выбранный репозиторий + Выполнить пользовательское действие + Имя действия: Извлечь - Извлечь все внешние хранилища + Извлечь все внешние репозитории + Разрешить опцию (--force) Извлечь без меток - Удалить внешние мёртвые ветки - Внешнее хранилище: + Внешний репозиторий: Извлечь внешние изменения - Допустить без изменений + Не отслеживать Отклонить... Отклонить {0} файлов... - Отклонить изменения в выбранной(ых) строке(ах) + Отменить изменения в выбранной(ых) строке(ах) Открыть расширенный инструмент слияния - Сохранить как исправление... - Подготовить - Подготовленные {0} файлы - Подготовленные изменения в выбранной(ых) строке(ах) + Взять версию ${0}$ + Сохранить как файл заплатки... + Сформировать + Сформированные {0} файлы + Сформированные изменения в выбранной(ых) строке(ах) Отложить... Отложить {0} файлов... - Снять подготовленный - Неподготовленные {0} файлы - Неподготовленные изменения в выбранной(ых) строке(ах) - Использовать их (checkout --theirs) + Расформировать + Несформированные {0} файлы + Несформированные изменения в выбранной(ых) строке(ах) Использовать мой (checkout --ours) + Использовать их (checkout --theirs) История файлов - СОДЕРЖИМОЕ ИЗМЕНИТЬ - ФИЛЬТР - Git-поток + СОДЕРЖИМОЕ + Git-процесс Ветка разработчика: Свойство: Свойство префикса: - ПОТОК - Свойства завершения - ПОТОК - Закончить исправление - ПОТОК - Завершить выпуск + ПРОЦЕСС - Свойства завершения + ПРОЦЕСС - Закончить исправление + ПРОЦЕСС - Завершить выпуск Цель: + Выложить на удалённый(ые) после завершения + Втиснуть при слиянии Исправление: Префикс исправлений: - Инициализировать Git-поток + Создать Git-процесс Держать ветку Производственная ветка: Выпуск: Префикс выпуска: Свойство запуска... - ПОТОК - Свойство запуска + ПРОЦЕСС - Свойство запуска Запуск исправлений... - ПОТОК - Запуск исправлений + ПРОЦЕСС - Запуск исправлений Ввести имя Запуск выпуска... - ПОТОК - Запуск выпуска + ПРОЦЕСС - Запуск выпуска Префикс метки версии: - Git хранилища больших файлов + Git LFS (хранилище больших файлов) Добавить шаблон отслеживания... Шаблон — это имя файла Изменить шаблон: - Добавить шаблон отслеживания в ХБФ Git + Добавить шаблон отслеживания в LFS Git Извлечь - Извлечь объекты ХБФ - Запустить `git lfs fetch", чтобы загрузить объекты ХБФ Git. При этом рабочая копия не обновляется. - Установить перехват ХБФ Git - Показать блокировки + Запустить (git lfs fetch), чтобы загрузить объекты LFS Git. При этом рабочая копия не обновляется. + Извлечь объекты LFS + Установить перехват LFS Git + Показывать блокировки Нет заблокированных файлов Блокировка Показывать только мои блокировки - Блокировки ХБФ + Блокировки LFS Разблокировать Принудительно разблокировать Обрезать - Запустите `git lfs prune", чтобы удалить старые файлы ХБФ из локального хранилища - Забрать - Забрать объекты ХБФ - Запустите `git lfs pull", чтобы загрузить все файлы ХБФ Git для текущей ссылки и проверить + Запустить (git lfs prune), чтобы удалить старые файлы LFS из локального хранилища + Загрузить + Запустить (git lfs pull), чтобы загрузить все файлы LFS Git для текущей ссылки и проверить + Загрузить объекты LFS Выложить - Выложить объекты ХБФ - Отправляйте большие файлы, помещенные в очередь, в конечную точку ХБФ Git + Отправляйте большие файлы, помещенные в очередь, в конечную точку LFS Git + Выложить объекты LFS Внешнее хранилище: - Отслеживать файлы с именем '{0}' + Отслеживать файлы с именем «{0}» Отслеживать все *{0} файлов Истории - Переключение горизонтального/вертикального расположения АВТОР ВРЕМЯ АВТОРА ГРАФ И СУБЪЕКТ SHA - ВРЕМЯ ФИКСАЦИИ - ВЫБРАННЫЕ {0} ФИКСАЦИИ - Удерживайте 'Ctrl' или 'Shift', чтобы выбрать несколько фиксаций. - Удерживайте ⌘ или ⇧, чтобы выбрать несколько фиксаций. + ВРЕМЯ РЕВИЗИИ + ВЫБРАННЫЕ {0} РЕВИЗИИ + Удерживайте Ctrl или Shift, чтобы выбрать несколько ревизий. + Удерживайте ⌘ или ⇧, чтобы выбрать несколько ревизий. ПОДСКАЗКИ: Ссылка на сочетания клавиш ОБЩЕЕ - Отменить текущее всплывающее окно - Закрыть текущее окно - Перейти на предыдущую страницу - Перейти на следующую страницу - Создать новую страницу - Открыть диалоговое окно настроек - ХРАНИЛИЩЕ - Зафиксировать подготовленные изменения - Зафиксировать и выложить подготовленные изменения - Подготовить все изменения и зафиксировать + Закрыть окно + Клонировать репозиторий + Закрыть вкладку + Перейти на следующую вкладку + Перейти на предыдущую вкладку + Создать новую вкладку + Открыть диалоговое окно настроек + Переключить активное рабочее место + Переключить активную страницу + РЕПОЗИТОРИЙ + Зафиксировать сформированные изменения + Зафиксировать и выложить сформированные изменения + Сформировать все изменения и зафиксировать + Создать новую ветку на основе выбранной ветки Отклонить выбранные изменения - Режим доски (по-умолчанию) - Принудительно перезагрузить этот хранилище - Подготовленные/Неподготовленные выбранные изменения - Режим поиска фиксаций - Переключить на 'Изменения' - Переключить на 'Истории' - Переключить на 'Отложенные' + Извлечение, запускается сразу + Режим доски (по умолчанию) + Режим поиска ревизий + Загрузить, запускается сразу + Выложить, запускается сразу + Принудительно перезагрузить репозиторий + Сформированные/Несформированные выбранные изменения + Переключить на «Изменения» + Переключить на «Истории» + Переключить на «Отложенные» ТЕКСТОВЫЙ РЕДАКТОР Закрыть панель поиска Найти следующее совпадение Найти предыдущее совпадение + Открыть с внешним инструментом сравнения/слияние Открыть панель поиска - Подготовить - Снять из подготовленных Отклонить - Инициализировать хранилище + Сформировать + Расформировать + Создать репозиторий Путь: - Выполняется частичный забор. Нажмите 'Отказ' для восстановления заголовка. - Выполняет запрос слияния. Нажмите 'Отказ' для восстановления заголовка. - Выполняется перенос. Нажмите 'Отказ' для восстановления заголовка. - Выполняется возврат. Нажмите 'Отказ' для восстановления заголовка. + Выполняется частичный перенос ревизий (cherry-pick). + Обрабтка ревизии. + Выполняется слияние. + Выполяется. + Выполняется перенос. + Остановлен на + Выполняется отмена ревизии. + Выполняется отмена Интерактивное перемещение - Целевая ветка: На: + Целевая ветка: + Копировать ссылку + Открыть в браузере ОШИБКА УВЕДОМЛЕНИЕ - Слить ветку + Рабочие места + Страницы + Влить ветку В: Опции слияния: - Исходная ветка: - Переместить узел хранилища - Выбрать родительский узел для: + Источник: + Влить несколько веток + Зафиксировать все изменения + Стратегия: + Цели: + Переместить репозиторий в другую группу + Выбрать группу для: Имя: Git НЕ был настроен. Пожалуйста, перейдите в [Настройки] и сначала настройте его. Открыть приложение каталогов данных @@ -390,242 +463,304 @@ Закрыть вкладку Закрыть другие вкладки Закрыть вкладки справа - Копировать путь хранилища - Хранилища + Копировать путь репозитория + Репозитории Вставить - Сейчас - {0} минут назад - {0} часов назад - Вчера {0} дней назад + 1 час назад + {0} часов назад + Сейчас Последний месяц + В прошлом году + {0} минут назад {0} месяцев назад - В пролому году {0} лет назад - Параметры - ОТКРЫТЬ ИИ - Сервер - Ключ API - Модель - ВИД - Шрифт по-умолчанию - Размер шрифта по-умолчанию - Моноширный шрифт - В текстовом редакторе используется только моноширный шрифт - Тема - Переопределение темы - Использовать фиксированную ширину табуляции в строке заголовка. - Использовать системное окно - ИНСТРУМЕНТ РАЗЛИЧИЙ/СЛИЯНИЯ - Путь установки - Введите путь для инструмента различия/слияния - Инструмент - ГЛАВНЫЙ - Проверить обновления при старте - Язык - История фиксаций - Показать время автора вместо времени фиксации на графике - Длина темы фиксации - GIT - Включить автозавершение CRLF - Каталог клонирования по-умолчанию - Электроная почта пользователя - Общая электроная почта пользователя git - Путь установки - Имя пользователя - общее имя пользователя git - Версия Git - Git (>= 2.23.0) требуется для этого приложения - ПОДПИСЬ GPG - Фиксация подписи GPG - Метка подписи GPG - Формат GPG - Путь установки программы - Введите путь для установленной программы GPG - Ключ подписи пользователя - Ключ подписи GPG пользователя - ВНЕДРЕНИЕ - ОБОЛОЧКА/ТЕРМИНАЛ - Оболочка/Терминал - Путь - Удалить внешнее хранилище + Вчера + Параметры + ОТКРЫТЬ ИИ + Запрос на анализ сравнения + Ключ API + Создать запрос на тему + Модель + Имя: + Сервер + Разрешить потоковую передачу + ВИД + Шрифт по умолчанию + Редактировать ширину вкладки + Размер шрифта + По умолчанию + Редактор + Моноширный шрифт + В текстовом редакторе используется только моноширный шрифт + Тема + Переопределение темы + Использовать фиксированную ширину табуляции в строке заголовка. + Использовать системное окно + ИНСТРУМЕНТ СРАВНЕНИЙ/СЛИЯНИЯ + Путь установки + Введите путь для инструмента сравнения/слияния + Инструмент + ОСНОВНЫЕ + Проверить обновления при старте + Формат даты + Язык + Максимальная длина истории + Показывать время автора вместо времени ревизии на графике + Показать наследника в деталях комментария + Показывать метки на графике + Длина темы ревизии + GIT + Включить автозавершение CRLF + Каталог клонирования по умолчанию + Электроная почта пользователя + Общая электроная почта пользователя git + Разрешить (--prune) при скачивании + Разрешить (--ignore-cr-at-eol) в сравнении + Для работы программы требуется версия Git (>= 2.25.1) + Путь установки + Разрешить верификацию HTTP SSL + Имя пользователя + Общее имя пользователя git + Версия Git + GPG ПОДПИСЬ + GPG подпись ревизии + Формат GPG + Путь установки программы + Введите путь для установленной программы GPG + GPG подпись метки + Ключ подписи пользователя + Ключ GPG подписи пользователя + ВНЕДРЕНИЕ + ОБОЛОЧКА/ТЕРМИНАЛ + Путь + Оболочка/Терминал + Удалить внешний репозиторий Цель: - Удалить рабочее дерево - Информация об обрезке рабочего дерева в `$GIT_DIR/worktrees` - Забрать - Ветка: - Извлечь все ветки + Удалить рабочий каталог + Информация об обрезке рабочего каталога в «$GIT_COMMON_DIR/worktrees» + Загрузить + Ветка внешнего репозитория: В: Локальные изменения: Отклонить - Ничего не делать Отложить и применить повторно - Забрать без меток - Внешнее хранилище: - Забрать (Получить и слить) + Обновить все подмодули + Внешний репозиторий: + Загрузить (Получить и слить) Использовать перемещение вместо слияния Выложить Убедитесь, что подмодули были вставлены Принудительно выложить Локальная ветка: - Внешнее хранилище: - Выложить изменения на внешнее хранилище - Ветка внешнего хранилища: - Установить в качестве ветки отслеживания + Внешний репозиторий: + Выложить изменения на внешний репозиторий + Ветка внешнего репозитория: + Отслеживать ветку Выложить все метки - Выложить метку на внешнее хранилище - Выложить на все внешние хранилища - Внешнее хранилище: + Выложить метку на внешний репозиторий + Выложить на все внешние репозитории + Внешний репозиторий: Метка: Выйти Перемещение текущей ветки Отложить и применить повторно локальные изменения На: Переместить: - Обновить - Добавить внешнее хранилище - Редактировать внешнее хранилище + Добавить внешний репозиторий + Редактировать внешний репозиторий Имя: - Имя внешнего хранилища - Адрес хранилища: - Адрес внешнего хранилища git + Имя внешнего репозитория + Адрес: + Адрес внешнего репозитория git Копировать адрес Удалить... Редактировать... Извлечь Открыть в браузере Удалить - Цель: - Подтвердить удаление рабочего дерева - Включить опцию `--force` + Подтвердить удаление рабочего каталога + Включить опцию (--force) Цель: Переименовать ветку Новое имя: Уникальное имя для данной ветки Ветка: Отказ - Автоматическое извлечение изменений с внешних хранилищ... + Автоматическое извлечение изменений с внешних репозиторий... + Сортировать + По дате ревизора (исполнителя) + По имени Очистить (Сбор мусора и удаление) - Запустить команду `git gc` для данного хранилища. + Запустить команду (git gc) для данного репозитория. Очистить всё - Настройка этого хранилища + Очистить + Настройка репозитория ПРОДОЛЖИТЬ + Изменить действия + Не изменять действия + Отклонить все изменения. + Разрешить опцию --reflog Открыть в файловом менеджере Поиск веток, меток и подмодулей - ОТФИЛЬТРОВАНО ОТ: + Видимость на графике + Не установлен (по умолчанию) + Скрыть в графе ревизии + Фильтр в графе ревизии + Включить опцию (--first-parent) + РАСПОЛОЖЕНИЕ + Горизонтально + Вертикально + ЗАПРОС РЕВИЗИЙ + Дата ревизии + Топологически ЛОКАЛЬНЫЕ ВЕТКИ - Навигация по заголовку - Включить опцию '--first-parent' + Навигация по ГОЛОВЕ (HEAD) Создать ветку + ОЧИСТКА УВЕДОМЛЕНИЙ + Выделять только текущую ветку на графике Открыть в {0} Открыть в расширенном инструменте Обновить - ВНЕШНИЕ ХРАНИЛИЩА - ДОБАВИТЬ ВНЕШНЕЕ ХРАНИЛИЩЕ - РАЗРЕШИТЬ - Поиск фиксации + ВНЕШНИЕ РЕПОЗИТОРИИ + ДОБАВИТЬ ВНЕШНИЙ РЕПОЗИТОРИЙ + Поиск ревизии + Автор + Ревизор + Содержимое Файл Сообщение SHA - Автор и исполнитель Текущая ветка - Показать метки как дерево - Статистики + Показывать подмодули как дерево + Показывать метки как катлог + ПРОПУСТИТЬ + Статистикa ПОДМОДУЛИ ДОБАВИТЬ ПОДМОДУЛЬ ОБНОВИТЬ ПОДМОДУЛЬ МЕТКИ НОВАЯ МЕТКА + По дате создания + По имени + Сортировать Открыть в терминале - РАБОЧИЕ ДЕРЕВЬЯ - ДОБАВИТЬ РАБОЧЕЕ ДЕРЕВО + Использовать относительное время в историях + Просмотр журналов + Посетить '{0}' в браузере + РАБОЧИЕ КАТАЛОГИ + ДОБАВИТЬ РАБОЧИЙ КАТАЛОГ ОБРЕЗАТЬ - Адрес хранилища Git - Сбросить текущую втеку до версии + Адрес репозитория Git + Сбросить текущую ветку до версии Режим сброса: Переместить в: Текущая ветка: - Раскрыть в файловом менеджере - Отменить фиксацию - Фиксация: - Отмена изменений фиксации - Переформулировать сообщение фиксации - Использовать "Shift+Enter" для ввода новой строки. "Enter" - это горячая клавиша кнопки OK + Сброс ветки (без переключения) + Переместить в: + Ветка: + Открыть в файловом менеджере + Отменить ревизию + Ревизия: + Отмена ревизии + Изменить комментарий ревизии + Используйте «Shift+Enter» для ввода новой строки. «Enter» - это горячая клавиша кнопки «OK» Запуск. Подождите пожалуйста... СОХРАНИТЬ Сохранить как... - Исправление успешно сохранено! - Сканирование хранилищ + Заплатка успешно сохранена! + Сканирование репозиторий Корневой каталог: Проверка для обновления... - Доступна новая версия этого программного обеспечения: + Доступна новая версия программного обеспечения: Не удалось проверить наличие обновлений! Загрузка Пропустить эту версию Обновление ПО В настоящее время обновления недоступны. - Уплотнить фиксации + Отслеживать ветку + Ветка: + Снять основную ветку + Основная ветка: + Копировать SHA + Перейти + Втиснуть ревизии В: - Частный ключ SSH: - Путь хранения частного ключа SSH - Только подготовленные изменения - Подготовленные так и подготовленные изменения выбранных файлов будут сохранены!!! + Приватный ключ SSH: + Путь хранения приватного ключа SSH ЗАПУСК Отложить + Автоматически восстанавливать после откладывания + Ваши рабочие файлы остаются неизменными, но отложенные сохранятся. Включить неотслеживаемые файлы + Хранить отложенные файлы Сообщение: - Необязательно. Имя этого тайника + Имя тайника (необязательно) + Только сформированные изменения + Сформированные так и несформированные изменения выбранных файлов будут сохранены!!! Отложить локальные изменения Принять Отбросить - Применить - Отрбосить тайник + Сохранить как заплатку... + Отбросить тайник Отбросить: Отложенные ИЗМЕНЕНИЯ ОТЛОЖЕННЫЕ - Статистики - ФИКСАЦИИ - ИСПОЛНИТЕЛИ + Статистика + РЕВИЗИИ + РЕВИЗОРЫ (ИСПОЛНИТЕЛИ) + ОБЗОР МЕСЯЦ НЕДЕЛЯ - ФИКСАЦИИ: АВТОРЫ: - ОБЗОР + РЕВИЗИИ: ПОДМОДУЛИ - Добавить подмодули + Добавить подмодули Копировать относительный путь + Удалить подмодуль Извлечение вложенных подмодулей - Открыть подмодуль хранилища - Относительный путь: - Относительный каталог для хранения подмодуля. + Открыть подмодуль репозитория + Каталог: + Относительный путь для хранения подмодуля. Удалить подмодуль + СОСТОЯНИЕ + изменён + не создан + ревизия изменена + не слита + URL-адрес ОК Копировать имя метки Копировать сообщение с метки Удалить ${0}$... + Влить ${0}$ в ${1}$... Выложить ${0}$... - Сетевой адрес: Обновление подмодулей Все подмодули - Инициализировать по необходимости + Создавать по необходимости Рекурсивно Подмодуль: - Использовать опцию --remote + Использовать опцию (--remote) + Сетевой адрес: + Журналы + ОЧИСТИТЬ ВСЁ + Копировать + Удалить Предупреждение Приветствие Создать группу Создать подгруппу - Клонировать хранилище + Клонировать репозиторий Удалить ПОДДЕРЖИВАЕТСЯ: ПЕРЕТАСКИВАНИЕ КАТАЛОГОВ, ПОЛЬЗОВАТЕЛЬСКАЯ ГРУППИРОВКА. Редактировать Перейти в другую группу - Открыть все хранилища - Открыть хранилище + Открыть все репозитории + Открыть репозиторий Открыть терминал - Повторное сканирование хранилищ в каталоге клонирования по-умолчанию - Поиск хранилищ... + Повторное сканирование репозиториев в каталоге клонирования по умолчанию + Поиск репозиториев... Сортировка Изменения Игнорировать Git @@ -634,30 +769,37 @@ Игнорировать файлы в том же каталоге Игнорировать только эти файлы Изменить - Автоподготовка - Теперь вы можете подготовитть этот файл. + Теперь вы можете сформировать этот файл. ЗАФИКСИРОВАТЬ ЗАФИКСИРОВАТЬ и ОТПРАВИТЬ Шаблон/Истории Запустить событие щелчка - Подготовить все изменения и зафиксировать + Зафиксировать (Редактировать) + Сформировать все изменения и зафиксировать + Вы сформировали {0} файл(ов), но отображается только {1} файл(ов) ({2} файл(ов) отфильтровано). Вы хотите продолжить? ОБНАРУЖЕНЫ КОНФЛИКТЫ + ОТКРЫТЬ ВНЕШНИЙ ИНСТРУМЕНТ СЛИЯНИЯ + ОТКРЫТЬ ВСЕ КОНФЛИКТЫ ВО ВНЕШНЕМ ИНСТРУМЕНТЕ СЛИЯНИЯ КОНФЛИКТЫ ФАЙЛОВ РАЗРЕШЕНЫ + ИСПОЛЬЗОВАТЬ МОИ + ИСПОЛЬЗОВАТЬ ИХ ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ НЕТ ПОСЛЕДНИХ ВХОДНЫХ СООБЩЕНИЙ - НЕТ ШАБЛОНОВ ФИКСАЦИИ - ПОДГОТОВЛЕННЫЕ - СНЯТЬ ПОДГОТОВЛЕННЫЙ - СНЯТЬ ВСЕ ПОДГОТОВЛЕННЫЕ - НЕПОДГОТОВЛЕННЫЕ - ПОДГОТОВИТЬ - ВСЕ ПОДГОТОВИТЬ - ВИД ПРЕДПОЛАГАЕТСЯ НЕИЗМЕННЫМ + НЕТ ШАБЛОНОВ РЕВИЗИИ + Сбросить автора + Щёлкните правой кнопкой мыши выбранный файл(ы) и разрешите конфликты. + Завершение работы + СФОРМИРОВАННЫЕ + РАСФОРМИРОВАТЬ + РАСФОРМИРОВАТЬ ВСЁ + НЕСФОРМИРОВАННЫЕ + СФОРМИРОВАТЬ + СФОРМИРОВАТЬ ВСЁ + ОТКРЫТЬ СПИСОК НЕОТСЛЕЖИВАЕМЫХ ФАЙЛОВ Шаблон: ${0}$ - Щёлкните правой кнопкой мыши выбранный файл(ы) и сделайте свой выбор для разрешения конфликтов. РАБОЧЕЕ ПРОСТРАНСТВО: Настройка рабочего пространства... - РАБОЧЕЕ ДЕРЕВО + РАБОЧИЙ КАТАЛОГ Копировать путь Заблокировать Удалить diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml new file mode 100644 index 00000000..abe53252 --- /dev/null +++ b/src/Resources/Locales/ta_IN.axaml @@ -0,0 +1,744 @@ + + + + + + பற்றி + மூலஅறிவிலி பற்றி + திறந்தமூல & கட்டற்ற அறிவிலி இடைமுக வாடிக்கயாளர் + பணிமரத்தைச் சேர் + இடம்: + இந்த பணிமரத்திற்கான பாதை. தொடர்புடைய பாதை ஆதரிக்கப்படுகிறது. + கிளை பெயர்: + விருப்பத்தேர்வு. இயல்புநிலை இலக்கு கோப்புறை பெயர். + கிளை கண்காணி: + தொலை கிளையைக் கண்காணித்தல் + என்ன சரிபார்க்க வேண்டும்: + புதிய கிளையை உருவாக்கு + ஏற்கனவே உள்ள கிளை + செநு உதவியாளர் + மறு-உருவாக்கு + உறுதிமொழி செய்தியை உருவாக்க செநுவைப் பயன்படுத்து + உறுதிமொழி செய்தி என இடு + ஒட்டு + ஒட்டு கோப்பு: + .ஒட்டு இடுவதற்கு கோப்பைத் தேர்ந்தெடு + வெள்ளைவெளி மாற்றங்களைப் புறக்கணி + ஒட்டு இடு + வெள்ளைவெளி: + பதுக்கிவைத்ததை இடு + பயன்படுத்திய பின் நீக்கு + குறியீட்டின் மாற்றங்களை மீண்டும் நிறுவு + பதுக்கிவை: + காப்பகம்... + இதற்கு காப்பகத்தை சேமி: + காப்பகக் கோப்பு பாதையைத் தேர்ந்தெடு + திருத்தம்: + காப்பகம் + மூலஅறிவிலி கடவுகேள் + கோப்புகள் மாற்றப்படவில்லை எனக் கருதப்படுகிறது + எந்த கோப்புகளும் மாற்றப்படவில்லை எனக் கருதப்படுகிறது + நீக்கு + புதுப்பி + இருமம் கோப்பு ஆதரிக்கப்படவில்லை!!! + குற்றச்சாட்டு + இந்த கோப்பில் குற்றம் சாட்ட ஆதரிக்கப்படவில்லை!!! + ${0}$ சரிபார்... + பணிமரத்துடன் ஒப்பிடுக + கிளை பெயரை நகலெடு + தனிப்பயன் செயல் + ${0}$ ஐ நீக்கு... + தேர்ந்தெடுக்கப்பட்ட {0} கிளைகளை நீக்கு + ${0}$ இதற்கு வேகமாக முன்னோக்கிச் செல் + ${0}$ ஐ ${1}$இல் பெறு... + அறிவிலி ஓட்டம் - முடி ${0}$ + ${0}$ ஐ ${1}$இல் இணை... + தேர்ந்தெடுக்கப்பட்ட {0} கிளைகளை தற்பொதையதில் இணை + இழு ${0}$ + இழு ${0}$ஐ ${1}$-க்குள்... + தள்ளு ${0}$ + மறுதளம் ${0}$ இதன்மேல் ${1}$... + மறுபெயரிடு ${0}$... + கண்காணிப்பு கிளையை அமை... + கிளை ஒப்பிடு + தவறான மேல்ஓடை! + எண்மங்கள் + விடு + பெற்றோர் திருத்தத்திற்கு மீட்டமை + இந்த திருத்தத்திற்கு மீட்டமை + உறுதிமொழி செய்தி உருவாக்கு + காட்சி பயன்முறையை மாற்று + கோப்பு மற்றும் கோப்புறை பட்டியலாக காட்டு + பாதை பட்டியலாகக் காட்டு + கோப்பு முறைமை மரமாகக் காட்டு + கிளை சரிபார் + உறுதிமொழி சரிபார் + உறுதிமொழி: + முன்னறிவிப்பு: ஒரு உறுதிமொழி சரிபார்பதன் மூலம், உங்கள் தலை பிரிக்கப்படும் + உள்ளக மாற்றங்கள்: + நிராகரி + பதுக்கிவை & மீண்டும் இடு + கிளை: + கனி பறி + உறுதிமொழி செய்திக்கு மூலத்தைச் சேர் + உறுதிமொழி(கள்): + அனைத்து மாற்றங்களையும் உறுதிமொழி + முதன்மைகோடு: + பொதுவாக நீங்கள் ஒரு ஒன்றிணையை கனி-பறிக்க முடியாது, ஏனெனில் இணைப்பின் எந்தப் பக்கத்தை முதன்மையாகக் கருத வேண்டும் என்பது உங்களுக்குத் தெரியாது. இந்த விருப்பம் குறிப்பிட்ட பெற்றோருடன் தொடர்புடைய மாற்றத்தை மீண்டும் இயக்க கனி-பறி அனுமதிக்கிறது. + பதுக்கிவைத்தையும் அழி + நீங்கள் அனைத்து பதுக்கிவைத்தையும் அழிக்க முயற்சிக்கிறீர்கள் தொடர விரும்புகிறீர்களா? + நகலி தொலை களஞ்சியம் + கூடுதல் அளவுருக்கள்: + நகலி களஞ்சியத்திற்கான கூடுதல் வாதங்கள். விருப்பத்தேர்வு. + உள்ளக பெயர்: + களஞ்சியப் பெயர். விருப்பத்தேர்வு. + பெற்றோர் கோப்புறை: + துவக்கு & துணை தொகுதிகளைப் புதுப்பி + களஞ்சிய முகவரி: + மூடு + திருத்தி + உறுதிமொழி சரிபார் + கனி-பறி உறுதிமொழி + கனி-பறி ... + தலையுடன் ஒப்பிடுக + பணிமரத்துடன் ஒப்பிடுக + தகவலை + பாகொவ-வை + தனிப்பயன் செயல் + இங்கே ${0}$ ஐ ஊடாடும் வகையில் மறுதளம் + ${0}$ இதற்கு ஒன்றிணை + ஒன்றிணை ... + இங்கே ${0}$ ஐ மறுதளம் + ${0}$ ஐ இங்கே மீட்டமை + உறுதிமொழி திரும்பபெறு + வேறுமொழி + ஒட்டாக சேமி... + பெற்றோர்களில் நொறுக்கு + நொறுக்கு குழந்தைகள் இங்கே சேர் + மாற்றங்கள் + மாற்றங்களைத் தேடு... + கோப்புகள் + பெகோஅ கோப்பு + கோப்புகளைத் தேடு... + துணைத்தொகுதி + தகவல் + ஆசிரியர் + மாற்றப்பட்டது + குழந்தைகள் + உறுதிமொழியாளர் + இந்த உறுதிமொழிடைக் கொண்ட குறிப்புகளைச் சரிபார் + உறுதிமொழி இதில் உள்ளது + முதல் 100 மாற்றங்களை மட்டும் காட்டுகிறது மாற்றங்கள் தாவலில் அனைத்து மாற்றங்களையும் காண்க. + செய்தி + பெற்றோர்கள் + குறிகள் + பாகொவ + உலாவியில் திற + விளக்கம் + உறுதிமொழி பொருளை உள்ளிடவும் + களஞ்சியம் உள்ளமை + உறுதிமொழி வளர்புரு + வார்ப்புரு உள்ளடக்கம்: + வார்ப்புரு பெயர்: + தனிப்பயன் செயல் + வாதங்கள்: + ${களஞ்சிய} - களஞ்சியத்தின் பாதை; ${கிளை} - தேர்ந்தெடுக்கப்பட்ட கிளை; ${பாகொவ} - தேர்ந்தெடுக்கப்பட்ட உறுதிமொழிடியின் பாகொவ + இயக்கக்கூடிய கோப்பு: + பெயர்: + நோக்கம்: + கிளை + உறுதிமொழி + களஞ்சியம் + செயல்பாட்டிலிருந்து வெளியேற காத்திரு + மின்னஞ்சல் முகவரி + மின்னஞ்சல் முகவரி + அறிவிலி + தொலைகளை தானாக எடு + நிமையங்கள் + இயல்புநிலை தொலை + சிக்கல் கண்காணி + மாதிரி அசூர் வளர்பணிகள் விதியைச் சேர் + மாதிரி அறிவிலிஈ சிக்கலுக்கான விதியைச் சேர் + மாதிரி அறிவிலிஈ இழு கோரிக்கை விதியைச் சேர் + மாதிரி அறிவிலிமையம் விதியைச் சேர் + மாதிரி அறிவிலிஆய்வு சிக்கலுக்கான விதியைச் சேர் + மாதிரி அறிவிலிஆய்வு இணைப்பு கோரிக்கை விதியைச் சேர் + மாதிரி சீரா விதியைச் சேர் + புதிய விதி + வழக்கவெளி வெளிப்பாடு வெளியீடு: + விதியின் பெயர்: + முடிவு முகவரி: + வழக்கவெளி குழுக்கள் மதிப்புகளை அணுக $1, $2 ஐப் பயன்படுத்து + செநு + விருப்பமான சேவை: + 'விருப்பமான சேவை' அமைக்கப்பட்டிருந்தால், மூலஅறிவிலி இந்த களஞ்சியத்தில் மட்டுமே அதைப் பயன்படுத்தும். இல்லையெனில், ஒன்றுக்கு மேற்பட்ட சேவைகள் இருந்தால், அவற்றில் ஒன்றைத் தேர்ந்தெடுப்பதற்கான சூழல் பட்டயல் காண்பிக்கப்படும். + உஉபநெ பதிலாள் + இந்த களஞ்சியத்தால் பயன்படுத்தப்படும் உஉபநெ பதிலாள் + பயனர் பெயர் + இந்த களஞ்சியத்திற்கான பயனர் பெயர் + பணியிடங்கள் + நிறம் + பெயர் + தாவல்களை மீட்டமை + வழக்கமான உறுதிமொழி உதவியாளர் + உடைக்கும் மாற்றம்: + மூடப்பட்ட வெளியீடு சிக்கல்: + மாற்ற விவரங்கள்: + நோக்கம்: + குறுகிய விளக்கம்: + மாற்ற வகை: + நகல் + அனைத்து உரையையும் நகலெடு + முழு பாதையை நகலெடு + நகல் பாதை + கிளையை உருவாக்கு... + இதன் அடிப்படையில்: + உருவாக்கப்பட்ட கிளையைப் சரிபார் + உள்ளக மாற்றங்கள்: + நிராகரி + பதுக்கிவை & மீண்டும் இடு + புதிய கிளை பெயர்: + கிளை பெயரை உள்ளிடவும். + இடைவெளிகள் கோடுகளால் மாற்றப்படும். + உள்ளக கிளையை உருவாக்கு + குறிச்சொல்லை உருவாக்கு... + இங்கு புதிய குறிச்சொல்: + சீபிசீ கையொப்பமிடுதல் + குறிச்சொல் செய்தி: + விருப்பத்தேர்வு. + குறிச்சொல் பெயர்: + பரிந்துரைக்கப்பட்ட வடிவம்: ப1.0.0-ஆனா + உருவாக்கப்பட்ட பிறகு அனைத்து தொலைகளுக்கும் தள்ளு + புதிய குறிசொல் உருவாக்கு + வகை: + annotated + குறைந்தஎடை + நேரடியாகத் தொடங்க கட்டுப்பாட்டை அழுத்திப் பிடி + வெட்டு + கிளையை நீக்கு + கிளை: + நீங்கள் ஒரு தொலை கிளையை நீக்கப் போகிறீர்கள்!!! + தொலை ${0}$ கிளையையும் நீக்கு + பல கிளைகளை நீக்கு + நீங்கள் ஒரே நேரத்தில் பல கிளைகளை நீக்க முயற்சிக்கிறீர்கள் நடவடிக்கை எடுப்பதற்கு முன் மீண்டும் சரிபார்! + தொலையை நீக்கு + தொலை: + பாதை: + இலக்கு: + எல்லா குழந்தைகளும் பட்டியலிலிருந்து நீக்கப்படுவார்கள். + இது பட்டியலிலிருந்து மட்டுமே அகற்றும், வட்டிலிருந்து அல்ல! + குழுவை நீக்குவதை உறுதிப்படுத்து + களஞ்சியத்தை நீக்குவதை உறுதிப்படுத்து + துணைத்தொகுதியை நீக்கு + துணைத்தொகுதி பாதை: + குறிச்சொல்லை நீக்கு + குறிசொல்: + தொலை களஞ்சியங்களிலிருந்து நீக்கு + இருமம் வேறுபாடு + புதிய + பழைய + நகல் + கோப்பு முறை மாற்றப்பட்டது + முதல் வேறுபாடு + வெள்ளைவெளி மாற்றத்தை புறக்கணி + கடைசி வேறுபாடு + பெகோஅ பொருள் மாற்றம் + அடுத்த வேறுபாடு + மாற்றங்கள் இல்லை அல்லது வரிமுடிவு மாற்றங்கள் மட்டும் + முந்தைய வேறுபாடு + ஒட்டாகச் சேமி + மறைக்கப்பட்ட சின்னங்களைக் காட்டு + பக்கவாட்டு வேறுபாடு + துணைத் தொகுதி + புதிய + இடமாற்று + தொடரியல் சிறப்பம்சமாக்கல் + வரி சொல் மடக்கு + தடுப்பு-வழிசெலுத்தலை இயக்கு + ஒன்றிணை கருவியில் திற + அனைத்து வரிகளையும் காட்டு + தெரியும் வரிகளின் எண்ணிக்கையைக் குறை + தெரியும் வரிகளின் எண்ணிக்கையை அதிகரி + மாற்றங்களைக் காண கோப்பைத் தேர்ந்தெடு + ஒன்றிணை கருவியில் திற + மாற்றங்களை நிராகரி + செயல்படும் நகலில் உள்ள அனைத்து உள்ளக மாற்றங்கள். + மாற்றங்கள்: + புறக்கணிக்கப்பட்ட கோப்புகளைச் சேர் + {0} மாற்றங்கள் நிராகரிக்கப்படும் + இந்தச் செயலை நீங்கள் செயல்தவிர்க்க முடியாது!!! + புத்தகக்குறி: + புதிய பெயர்: + இலக்கு: + தேர்ந்தெடுக்கப்பட்ட குழுவைத் திருத்து + தேர்ந்தெடுக்கப்பட்ட களஞ்சியத்தைத் திருத்து + தனிப்பயன் செயலை இயக்கு + செயல் பெயர்: + பெறு + எல்லா தொலைகளையும் பெறு + உள்ளக குறிப்புகளை கட்டாயமாக மீறு + குறிச்சொற்கள் இல்லாமல் பெறு + தொலை: + தொலை மாற்றங்களைப் பெறு + மாறாமல் என கருது + நிராகரி... + {0} கோப்புகளை நிராகரி... + தேர்ந்தெடுக்கப்பட்ட வரிகளில் மாற்றங்களை நிராகரி + வெளிப்புற இணைப்பு கருவியைத் திற + ${0}$ஐப் பயன்படுத்தி தீர் + ஒட்டு என சேமி... + நிலைபடுத்து + {0} fகோப்புகள் நிலைபடுத்து + தேர்ந்தெடுக்கப்பட்ட வரிகளில் மாற்றங்களை நிலைபடுத்து + பதுக்கிவை... + {0} கோப்புகள் பதுக்கிவை... + நிலைநீக்கு + நிலைநீக்கு {0} கோப்புகள் + தேர்ந்தெடுக்கப்பட்ட வரிகளில் மாற்றங்களை நிலைநீக்கு + என்னுடையதைப் பயன்படுத்து (சரிபார் --நமது) + அவர்களுடையதைப் பயன்படுத்து (சரிபார் --அவர்களது) + கோப்பு வரலாறு + மாற்றம் + உள்ளடக்கம் + அறிவிலி-ஓட்டம் + மேம்பாட்டு கிளை: + நற்பொருத்தம்: + நற்பொருத்தம் முன்னொட்டு: + ஓட்டம் - நற்பொருத்தம் முடி + ஓட்டம் - சூடானதிருத்தம் முடி + ஓட்டம் - வெளியீட்டை முடி + இலக்கு: + சூடானதிருத்தம்: + சூடானதிருத்தம் முன்னொட்டு: + அறிவிலி-ஓட்டம் துவக்கு + கிளையை வைத்திரு + உற்பத்தி கிளை: + வெளியீடு: + வெளியீடு முன்னொட்டு: + நற்பொருத்தம் தொடங்கு... + ஓட்டம் - நற்பொருத்தம் தொடங்கு + சூடானதிருத்தம் தொடங்கு... + ஓட்டம் - சூடானதிருத்தம் தொடங்கு + பெயரை உள்ளிடு + வெளியீட்டைத் தொடங்கு... + ஓட்டம் - வெளியீட்டைத் தொடங்கு + பதிப்பு குறிச்சொல் முன்னொட்டு: + அறிவிலி பெகோஅ + அறிவிலி கண்காணி வடிவத்தைச் சேர்... + வடிவம் என்பது கோப்பு பெயர் + தனிப்பயன் வடிவம்: + அறிவிலி பெகோஅ இல் கண்காணி வடிவங்களைச் சேர் + பெறு + அறிவிலி பெகோஅ பொருள்களைப் பதிவிறக்க `அறிவிலி பெகோஅ பெறு` ஐ இயக்கவும் இது செயல்படும் நகலை புதுப்பிக்காது. + அறிவிலி பெகோஅ பொருள்களைப் பெறு + அறிவிலி பெகோஅ கொக்கிகளை நிறுவு + பூட்டுகளைக் காட்டு + பூட்டப்பட்ட கோப்புகள் இல்லை + பூட்டு + எனது பூட்டுகளை மட்டும் காட்டு + பெகோஅ பூட்டுகள் + திற + கட்டாயம் திற + கத்தரி + உள்ளக சேமிப்பகத்திலிருந்து பழைய பெகோஅ கோப்புகளை நீக்க `அறிவிலி பெகோஅ கத்தரி` ஐ இயக்கு + இழு + தற்போதைய குறிக்கு அனைத்து அறிவிலி பெகோஅ கோப்புகளையும் பதிவிறக்கி சரிபார்க்க `அறிவிலி பெகோஅ இழு`ஐ இயக்கு + பெகோஅ பொருள்களை இழு + தள்ளு + வரிசைப்படுத்தப்பட்ட பெரிய கோப்புகளை அறிவிலி பெகோஅ முடிவுபுள்ளிக்கு தள்ளு + பெகோஅ பொருள்கள் தள்ளு + தொலை: + '{0}' என பெயரிடப்பட்ட கோப்புகளைக் கண்காணி + அனைத்து *{0} கோப்புகளையும் கண்காணி + வரலாறு + ஆசிரியர் + ஆசிரியர் நேரம் + வரைபடம் & பொருள் + பாகொவ + உறுதிமொழி நேரம் + தேர்ந்தெடுக்கப்பட்ட {0} உறுதிமொழிகள் + பல உறுதிமொழிகளைத் தேர்ந்தெடுக்க 'கட்டுப்பாடு' அல்லது 'உயர்த்து'ஐ அழுத்திப் பிடி. + பல உறுதிமொழிகளைத் தேர்ந்தெடுக்க ⌘ அல்லது ⇧ ஐ அழுத்திப் பிடி. + குறிப்புகள்: + விசைப்பலகை குறுக்குவழிகள் குறிப்பு + உலகளாவிய + தற்போதைய மேல்தோன்றலை Cancel + புதிய களஞ்சியத்தை நகலி செய் + தற்போதைய பக்கத்தை மூடு + அடுத்த பக்கத்திற்குச் செல் + முந்தைய பக்கத்திற்குச் செல் + புதிய பக்கத்தை உருவாக்கு + விருப்பத்தேர்வுகள் உரையாடலைத் திற + களஞ்சியம் + நிலைபடுத்திய மாற்றங்களை உறுதிமொழி + நிலைபடுத்திய மாற்றங்களை உறுதிமொழி மற்றும் தள்ளு + அனைத்து மாற்றங்களையும் நிலைபடுத்தி உறுதிமொழி + தேர்ந்தெடுக்கப்பட்ட உறுதிமொழியின் அடிப்படையில் ஒரு புதிய கிளையை உருவாக்குகிறது + தேர்ந்தெடுக்கப்பட்ட மாற்றங்களை நிராகரி + எடு, நேரடியாகத் தொடங்குகிறது + முகப்பலகை பயன்முறை (இயல்புநிலை) + உறுதிமொழி தேடல் பயன்முறை + இழு, நேரடியாகத் தொடங்குகிறது + தள்ளு, நேரடியாகத் தொடங்குகிறது + இந்த களஞ்சியத்தை மீண்டும் ஏற்ற கட்டாயப்படுத்து + தேர்ந்தெடுக்கப்பட்ட மாற்றங்களை நிலைபடுத்து/நிலைநீக்கு + 'மாற்றங்கள்' என்பதற்கு மாறு + 'வரலாறுகள்' என்பதற்கு மாறு + 'பதுகிவைத்தவை' என்பதற்கு மாறு + உரை திருத்தி + தேடல் பலகத்தை மூடு + அடுத்த பொருத்தத்தைக் கண்டறி + முந்தைய பொருத்தத்தைக் கண்டறி + தேடல் பலகத்தைத் திற + நிராகரி + நிலைபடுத்து + நிலைநீக்கு + களஞ்சியத்தைத் துவக்கு + பாதை: + கனி-பறி செயல்பாட்டில் உள்ளது. + உறுதிமொழி செயலாக்குதல் + இணைத்தல் செயல்பாட்டில் உள்ளது. + இணைத்தல் + மறுதளம் செயல்பாட்டில் உள்ளது + இல் நிறுத்தப்பட்டது + திரும்ப்பெறும் செயல்பாட்டில் உள்ளது. + திரும்பபெறும் உறுதிமொழி + ஊடாடும் மறுதளம் + மேல்: + இலக்கு கிளை: + இணைப்பை நகலெடு + உலாவியில் திற + பிழை + அறிவிப்பு + கிளையை ஒன்றிணை + Into: + இணைப்பு விருப்பம்: + இதனுள்: + ஒன்றிணை (பல) + அனைத்து மாற்றங்களையும் உறுதிமொழி + சூழ்ச்சிமுறை: + இலக்குகள்: + களஞ்சிய முனையை நகர்த்து + இதற்கான பெற்றோர் முனையைத் தேர்ந்தெடு + பெயர்: + அறிவிலி உள்ளமைக்கப்படவில்லை. [விருப்பத்தேர்வுகள்]க்குச் சென்று முதலில் அதை உள்ளமை. + தரவு சேமிப்பக கோப்பகத்தைத் திற + இதனுடன் திற... + விருப்பத்தேர்வு. + புதிய பக்கத்தை உருவாக்கு + புத்தகக்குறி + மூடு தாவல் + பிற தாவல்களை மூடு + வலதுபுறத்தில் உள்ள தாவல்களை மூடு + களஞ்சிய பாதை நகலெடு + களஞ்சியங்கள் + ஒட்டு + {0} நாட்களுக்கு முன்பு + 1 மணி நேரத்திற்கு முன்பு + {0} மணி நேரத்திற்கு முன்பு + சற்றுமுன் + கடந்த திங்கள் + கடந்த ஆண்டு + {0} நிமையங்களுக்கு முன்பு + {0} திங்களுக்கு முன்பு + {0} ஆண்டுகளுக்கு முன்பு + நேற்று + விருப்பத்தேர்வுகள் + செநு + வேறுபாடு உடனடியாக பகுப்பாய்வு செய் + பநிஇ திறவுகோல் + பொருள் உடனடியாக உருவாக்கு + மாதிரி + பெயர் + சேவையகம் + ஓடையை இயக்கு + தோற்றம் + இயல்புநிலை எழுத்துரு + திருத்தி தாவல் அகலம் + எழுத்துரு அளவு + இயல்புநிலை + திருத்தி + ஒற்றைவெளி எழுத்துரு + ஒற்றைவெளி எழுத்துருவை உரை திருத்தியில் மட்டும் பயன்படுத்து + கருப்பொருள் + கருப்பொருள் மேலெழுதப்படுகிறது + தலைப்புப்பட்டியில் நிலையான தாவல் அகலத்தைப் பயன்படுத்து + சொந்த சாளர சட்டத்தைப் பயன்படுத்து + வேறு/ஒன்றிணை கருவி + நிறுவல் பாதை + வேறு/ஒன்றிணை கருவிக்கான பாதை உள்ளிடு + கருவி + பொது + தொடக்கத்தில் புதுப்பிப்புகளைச் சரிபார் + தேதி வடிவம் + மொழி + வரலாற்று உறுதிமொழிகள் + வரைபடத்தில் உறுதிமொழி நேரத்திற்குப் பதிலாக ஆசிரியர் நேரத்தைக் காட்டு + உறுதிமொழி விவரங்களில் குழந்தைகளைக் காட்டு + உறுதிமொழி வரைபடத்தில் குறிச்சொற்களைக் காட்டு + பொருள் வழிகாட்டி நீளம் + அறிவிலி + தானியங்கி வரிமுடிவை இயக்கு + இயல்புநிலை நகலி அடைவு + பயனர் மின்னஞ்சல் + உலகளாவிய அறிவிலி பயனர் மின்னஞ்சல் + --prune எடுக்கும்போது இயக்கு + அறிவிலி (>= 2.25.1) இந்த பயன்பாட்டிற்கு தேவைப்படுகிறது + நிறுவல் பாதை + உஉபநெ பாகுஅ சரிபார்ப்பை இயக்கு + பயனர் பெயர் + உலகளாவிய அறிவிலி பயனர் பெயர் + அறிவிலி பதிப்பு + சிபிசி கையொப்பமிடுதல் + சிபிசி கையொப்பமிடுதல் உறுதிமொழி + சிபிசி வடிவம் + நிரல் நிறுவல் பாதை + நிறுவப்பட்ட சிபிசி நிரலுக்கான உள்ளீட்டு பாதை + சிபிசி கையொப்பமிடுதலை குறிச்சொலிடு + பயனர் கையொப்பமிடும் திறவுகோல் + பயனரின் கையொப்பமிடும் திறவுகோல் + ஒருங்கிணைப்பு + ஓடு/முனையம் + பாதை + ஓடு/முனையம் + தொலை கத்தரி + இலக்கு: + பணிமரங்கள் கத்தரி + `$GIT_COMMON_DIR/பணிமரங்கள்` இதில் பணிமரம் தகவலை கத்தரி + இழு + தொலை கிளை: + இதனுள்: + உள்ளக மாற்றங்கள்: + நிராகரி + பதுக்கிவை & மீண்டும் இடு + தொலை: + இழு (எடுத்து ஒன்றிணை) + ஒன்றிணை என்பதற்குப் பதிலாக மறுதளத்தைப் பயன்படுத்து + தள்ளு + துணைத் தொகுதிகள் தள்ளப்பட்டது என்பதை உறுதிசெய் + கட்டாயமாக தள்ளு + உள்ளக கிளை: + தொலை: + மாற்றங்களை தொலைக்கு தள்ளு + தொலை கிளை: + கண்காணிப்பு கிளையாக அமை + அனைத்து குறிச்சொற்களையும் தள்ளு + தொலைக்கு குறிச்சொல்லை தள்ளு + அனைத்து தொலைகளுக்கும் தள்ளு + தொலை: + குறிச்சொல்: + வெளியேறு + தற்போதைய கிளையை மறுதளம் செய் + உள்ளக மாற்றங்களை பதுக்கிவை & மீண்டும் இடு + மேல்: + மறுதளம்: + தொலையைச் சேர் + தொலையைத் திருத்து + பெயர்: + களஞ்சிய பெயர் + களஞ்சிய முகவரி: + தொலை அறிவிலி களஞ்சிய முகவரி: + முகவரியை நகலெடு + நீக்கு... + திருத்து... + பெறு + உலாவியில் திற + கத்தரித்தல் + பணிமரத்தை அகற்றுவதை உறுதிப்படுத்து + `--கட்டாயம்` விருப்பத்தை இயக்கு + இலக்கு: + கிளையை மறுபெயரிடு + புதிய பெயர்: + இந்தக் கிளைக்கான தனித்துவமான பெயர் + கிளை: + நிறுத்து + தொலைகளிலிருந்து மாற்றங்களைத் தானாகப் பெறுதல்... + சுத்தப்படுத்தல்(சீசி & கத்தரித்தல்) + இந்த களஞ்சியத்திற்கு `அறிவிலி சீசி` கட்டளையை இயக்கு. + அனைத்தையும் அழி + இந்த களஞ்சியத்தை உள்ளமை + தொடர்க + தனிப்பயன் செயல்கள் + தனிப்பயன் செயல்கள் இல்லை + எல்லா மாற்றங்களையும் நிராகரி + '--குறிபதிவு' விருப்பத்தை இயக்கு + கோப்பு உலாவியில் திற + கிளைகள்/குறிச்சொற்கள்/துணைத் தொகுதிகளைத் தேடு + வரைபடத்தில் தெரிவுநிலை + அமைவை நீக்கு + உறுதிமொழி வரைபடத்தில் மறை + உறுதிமொழி வரைபடத்தில் வடிகட்டு + '--first-parent' விருப்பம் இயக்கு + தளவமைப்பு + கிடைமட்டம் + செங்குத்து + உறுதிமொழி வரிசை + உறுதிமொழி தேதி + இடவியல் மூலமாக + உள்ளக கிளைகள் + தலைக்கு செல் + கிளையை உருவாக்கு + அறிவிப்புகளை அழி + வரைபடத்தில் தற்போதைய கிளையை மட்டும் முன்னிலை படுத்து + {0} இல் திற + வெளிப்புற கருவிகளில் திற + புதுப்பி + தொலைகள் + தொலையைச் சேர் + உறுதிமொழி தேடு + ஆசிரியர் + உறுதிமொழியாளர் + கோப்பு + செய்தி + பாகொவ + தற்போதைய கிளை + குறிச்சொற்களை மரமாகக் காட்டு + தவிர் + புள்ளிவிவரங்கள் + துணைத் தொகுதிகள் + துணைத் தொகுதியைச் சேர் + துணைத் தொகுதியைப் புதுப்பி + குறிசொற்கள் + புதிய குறிசொல் + படைப்பாளர் தேதியின்படி + பெயர் மூலம் + வரிசைப்படுத்து + முனையத்தில் திற + வரலாறுகளில் உறவு நேரத்தைப் பயன்படுத்து + பணிமரங்கள் + பணிமரத்தைச் சேர் + கத்தரித்தல் + அறிவிலி களஞ்சிய முகவரி + தற்போதைய கிளையை திருத்தத்திற்கு மீட்டமை + மீட்டமை பயன்முறை: + இதற்கு நகர்த்து: + தற்போதைய கிளை: + கோப்பு உலாவியில் வெளிப்படுத்து + பின்வாங்கு உறுதிமொழி + உறுதிமொழி: + பின்வாங்கு மாற்றங்களை உறுதிமொழி + மாறுசொல் உறுதிமொழி செய்தி + புதிய வரியை உள்ளிட 'உயர்த்து+நுழை' ஐப் பயன்படுத்தவும். 'நுழை' என்பது சரி பொத்தானின் சூடானவிசை ஆகும் + இயங்குகிறது. காத்திருக்கவும்... + சேமி + எனச் சேமி... + ஒட்டு வெற்றிகரமாக சேமிக்கப்பட்டது! + களஞ்சியங்களை வருடு + வேர் அடைவு: + புதுப்பிப்புகளைச் சரிபார்... + இந்த மென்பொருளின் புதிய பதிப்பு கிடைக்கிறது: + புதுப்பிப்புகளைச் சரிபார்க்க முடியவில்லை! + பதிவிறக்கம் + இந்தப் பதிப்பைத் தவிர் + மென்பொருள் புதுப்பி + தற்போது புதுப்பிப்புகள் எதுவும் கிடைக்கவில்லை. + கண்காணிப்பு கிளையை அமை + கிளை: + மேல்ஓடையை நீக்கு + மேல்ஓடை: + SHA ஐ நகலெடு + இதற்கு செல் + நொறுக்கு உறுதிமொழிகள் + இதில்: + பாஓடு தனியார் திறவுகோல்: + தனியார் பாஓடு திறவுகோல் கடை பாதை + தொடங்கு + பதுக்கிவை + பதுக்கிவைத்த பிறகு தானியங்கி மீட்டமை + உங்கள் செயல்படும் கோப்புகள் மாறாமல் இருக்கும், ஆனால் ஒரு பதுக்கிவைக்கப்படும். + கண்காணிக்கப்படாத கோப்புகளைச் சேர் + நிலைப்படுத்தப்பட்ட கோப்புகளை வைத்திரு + செய்தி: + விருப்பத்தேர்வு. இந்த பதுக்கலின் பெயர் + நிலைப்படுத்தப்பட்ட மாற்றங்கள் மட்டும் + தேர்ந்தெடுக்கப்பட்ட கோப்புகளின் நிலைப்படுத்தப்பட்ட மற்றும் நிலைப்படுத்தப்படாத மாற்றங்கள் இரண்டும் பதுக்கிவைக்கப்படும்!!! + உள்ளக மாற்றங்களை பதுக்கிவை + இடு + கைவிடு + ஒட்டாகச் சேமி... + பதுக்கிவைத்தவை கைவிடு + கைவிடு: + பதுக்கிவைத்தவைகள் + மாற்றங்கள் + பதுக்கிவைத்தவைகள் + புள்ளிவிவரங்கள் + உறுதிமொழிகள் + உறுதிமொழியாளர் + மேலோட்டப் பார்வை + திங்கள் + வாரம் + ஆசிரியர்கள்: + உறுதிமொழிகள்: + துணைத் தொகுதி + துணைத் தொகுதியைச் சேர் + உறவு பாதையை நகலெடு + உள்ளமைக்கப்பட்ட துணைத் தொகுதிகளை எடு + துணைத் தொகுதி களஞ்சியத்தைத் திற + உறவு பாதை: + இந்த தொகுதியை சேமிப்பதற்கான தொடர்புடைய கோப்புறை. + துணை தொகுதியை நீக்கு + சரி + குறிச்சொல் பெயரை நகலெடு + குறிச்சொல் செய்தியை நகலெடு + நீக்கு ${0}$... + ${0}$ இதை ${1}$ இல் இணை... + தள்ளு ${0}$... + துணைத்தொகுதிகளைப் புதுப்பி + அனைத்து துணைத்தொகுதிகள் + தேவைக்கேற்றப துவக்கு + சுழற்சி முறையில் + --தொலை விருப்பத்தைப் பயன்படுத்து + முகவரி: + முன்னறிவிப்பு + வரவேற்பு பக்கம் + குழுவை உருவாக்கு + துணைக் குழுவை உருவாக்கு + நகலி களஞ்சியம் + நீக்கு + கோப்புறையை இழுத்து & விடு ஆதரிக்கப்படுகிறது. தனிப்பயன் குழுவாக்க ஆதரவு. + திருத்து + வேறொரு குழுவிற்கு நகர்த்து + அனைத்து களஞ்சியங்களையும் திற + களஞ்சியத்தைத் திற + முனையத்தைத் திற + இயல்புநிலை நகலி அடைவில் களஞ்சியங்களை மீண்டும் வருடு + களஞ்சியங்களைத் தேடு... + வரிசைப்படுத்து + உள்ளக மாற்றங்கள் + அறிவிலி புறக்கணி + எல்லா *{0} கோப்புகளையும் புறக்கணி + ஒரே கோப்புறையில் *{0} கோப்புகளைப் புறக்கணி + ஒரே கோப்புறையில் கோப்புகளைப் புறக்கணி + இந்த கோப்பை மட்டும் புறக்கணி + பின்னொட்டு + இந்த கோப்பை இப்போது நீங்கள் நிலைப்படுத்தலாம். + உறுதிமொழி + உறுதிமொழி & தள்ளு + வளர்புரு/வரலாறுகள் + சொடுக்கு நிகழ்வைத் தூண்டு + உறுதிமொழி (திருத்து) + அனைத்து மாற்றங்களையும் நிலைப்படுத்தி உறுதிமொழி + நீங்கள் {0} கோப்புகளை நிலைப்படுத்தியுள்ளீர்கள், ஆனால் {1} கோப்புகள் மட்டுமே காட்டப்பட்டுள்ளன ({2} கோப்புகள் வடிகட்டப்பட்டுள்ளன). தொடர விரும்புகிறீர்களா? + மோதல்கள் கண்டறியப்பட்டது + கோப்பு மோதல்கள் தீர்க்கப்பட்டது + கண்காணிக்கப்படாத கோப்புகளைச் சேர் + அண்மைக் கால உள்ளீட்டு செய்திகள் இல்லை + உறுதிமொழி வளர்புருகள் இல்லை + தேர்ந்தெடுக்கப்பட்ட கோப்பு(களை) வலது சொடுக்கு செய்து, முரண்பாடுகளைத் தீர்க்க உங்கள் விருப்பத்தைத் தேர்ந்தெடு. + கையெழுத்திடு + நிலைபடுத்தியது + நிலைநீக்கு + அனைத்தும் நிலைநீக்கு + நிலைநீக்கு + நிலைபடுத்து + அனைத்தும் நிலைபடுத்து + மாறாதது எனநினைப்பதை பார் + வளர்புரு: ${0}$ + பணியிடம்: + பணியிடங்களை உள்ளமை... + பணிமரம் + பாதையை நகலெடு + பூட்டு + நீக்கு + திற + diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml new file mode 100644 index 00000000..096b4398 --- /dev/null +++ b/src/Resources/Locales/uk_UA.axaml @@ -0,0 +1,754 @@ + + + + + + Про програму + Про SourceGit + Безкоштовний Git GUI клієнт з відкритим кодом + Додати робоче дерево + Розташування: + Шлях для цього робочого дерева. Відносний шлях підтримується. + Назва гілки: + Необов'язково. За замовчуванням — назва кінцевої папки. + Відстежувати гілку: + Відстежувати віддалену гілку + Що перемкнути: + Створити нову гілку + Наявна гілка + AI Асистент + ПЕРЕГЕНЕРУВАТИ + Використати AI для генерації повідомлення коміту + ЗАСТОСУВАТИ ЯК ПОВІДОМЛЕННЯ КОМІТУ + Застосувати + Файл патчу: + Виберіть файл .patch для застосування + Ігнорувати зміни пробілів + Застосувати Патч + Пробіли: + Застосувати схованку + Видалити після застосування + Відновити зміни індексу + Схованка: + Архівувати... + Зберегти архів у: + Виберіть шлях до файлу архіву + Ревізія: + Архівувати + SourceGit Askpass + ФАЙЛИ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ + НЕМАЄ ФАЙЛІВ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ + ВИДАЛИТИ + Оновити + БІНАРНИЙ ФАЙЛ НЕ ПІДТРИМУЄТЬСЯ!!! + Автор рядка + ПОШУК АВТОРА РЯДКА ДЛЯ ЦЬОГО ФАЙЛУ НЕ ПІДТРИМУЄТЬСЯ!!! + Перейти на ${0}$... + Порівняти з ${0}$ + Порівняти з робочим деревом + Копіювати назву гілки + Спеціальна дія + Видалити ${0}$... + Видалити вибрані {0} гілок + Перемотати до ${0}$ + Отримати ${0}$ в ${1}$... + Git Flow - Завершити ${0}$ + Злиття ${0}$ в ${1}$... + Злити вибрані {0} гілок в поточну + Витягти ${0}$ + Витягти ${0}$ в ${1}$... + Надіслати ${0}$ + Перебазувати ${0}$ на ${1}$... + Перейменувати ${0}$... + Встановити відстежувану гілку... + Порівняти гілки + Недійсний upstream! + Байтів + СКАСУВАТИ + Скинути до батьківської ревізії + Скинути до цієї ревізії + Згенерувати повідомлення коміту + ЗМІНИТИ РЕЖИМ ВІДОБРАЖЕННЯ + Показати як список файлів та тек + Показати як список шляхів + Показати як дерево файлової системи + Перейти на гілку + Перейти на коміт + Коміт: + Попередження: Перехід на коміт призведе до стану "від'єднаний HEAD" + Локальні зміни: + Скасувати + Сховати та Застосувати + Гілка: + Cherry-pick + Додати джерело до повідомлення коміту + Коміт(и): + Закомітити всі зміни + Батьківський коміт: + Зазвичай неможливо cherry-pick злиття, бо невідомо, яку сторону злиття вважати батьківською (mainline). Ця опція дозволяє відтворити зміни відносно вказаного батьківського коміту. + Очистити схованки + Ви намагаєтеся очистити всі схованки. Ви впевнені? + Клонувати віддалене сховище + Додаткові параметри: + Додаткові аргументи для клонування сховища. Необов'язково. + Локальна назва: + Назва сховища. Необов'язково. + Батьківська тека: + Ініціалізувати та оновити підмодулі + URL сховища: + ЗАКРИТИ + Редактор + Перейти на коміт + Cherry-pick коміт + Cherry-pick ... + Порівняти з HEAD + Порівняти з робочим деревом + Iнформацію + SHA + Спеціальна дія + Інтерактивно перебазувати ${0}$ сюди + Злиття в ${0}$ + Злити ... + Перебазувати ${0}$ сюди + Скинути ${0}$ сюди + Скасувати коміт + Змінити повідомлення + Зберегти як патч... + Склеїти з батьківським комітом + Склеїти дочірні коміти сюди + ЗМІНИ + Пошук змін... + ФАЙЛИ + LFS Файл + Пошук файлів... + Підмодуль + ІНФОРМАЦІЯ + АВТОР + ЗМІНЕНО + ДОЧІРНІ + КОМІТЕР + Перевірити посилання, що містять цей коміт + КОМІТ МІСТИТЬСЯ В + Показано лише перші 100 змін. Дивіться всі зміни на вкладці ЗМІНИ. + ПОВІДОМЛЕННЯ + БАТЬКІВСЬКІ + ПОСИЛАННЯ (Refs) + SHA + Відкрити в браузері + Опис + Введіть тему коміту + Налаштування сховища + ШАБЛОН КОМІТУ + Зміст шаблону: + Назва шаблону: + СПЕЦІАЛЬНА ДІЯ + Аргументи: + ${REPO} - Шлях до сховища; ${BRANCH} - Вибрана гілка; ${SHA} - SHA вибраного коміту + Виконуваний файл: + Назва: + Область застосування: + Гілка + Коміт + Репозиторій + Чекати завершення дії + Адреса Email + Адреса електронної пошти + GIT + Автоматично отримувати зміни з віддалених сховищ + хвилин(и) + Віддалене сховище за замовчуванням + Бажаний режим злиття + ТРЕКЕР ЗАВДАНЬ + Додати приклад правила для Azure DevOps + Додати приклад правила для Gitee Issue + Додати приклад правила для Gitee Pull Request + Додати приклад правила для Github + Додати приклад правила для GitLab Issue + Додати приклад правила для GitLab Merge Request + Додати приклад правила для Jira + Нове правило + Регулярний вираз для завдання: + Назва правила: + URL результату: + Використовуйте $1, $2 для доступу до значень груп регулярного виразу. + AI + Бажаний сервіс: + Якщо 'Бажаний сервіс' встановлено, SourceGit буде використовувати лише його у цьому сховищі. Інакше, якщо доступно більше одного сервісу, буде показано контекстне меню для вибору. + HTTP Проксі + HTTP проксі, що використовується цим сховищем + Ім'я користувача + Ім'я користувача для цього сховища + Робочі простори + Колір + Відновлювати вкладки при запуску + ПРОДОВЖИТИ + Виявлено порожній коміт! Продовжити (--allow-empty)? + ІНДЕКСУВАТИ ВСЕ ТА ЗАКОМІТИТИ + Виявлено порожній коміт! Продовжити (--allow-empty) чи індексувати все та закомітити? + Допомога Conventional Commit + Зворотньо несумісні зміни: + Закрите завдання: + Детальні зміни: + Область застосування: + Короткий опис: + Тип зміни: + Копіювати + Копіювати весь текст + Копіювати повний шлях + Копіювати шлях + Створити гілку... + На основі: + Перейти на створену гілку + Локальні зміни: + Скасувати + Сховати та Застосувати + Назва нової гілки: + Введіть назву гілки. + Пробіли будуть замінені на тире. + Створити локальну гілку + Створити тег... + Новий тег для: + Підпис GPG + Повідомлення тегу: + Необов'язково. + Назва тегу: + Рекомендований формат: v1.0.0-alpha + Надіслати на всі віддалені сховища після створення + Створити Новий Тег + Тип: + анотований + легкий + Утримуйте Ctrl для запуску без діалогу + Вирізати + Видалити гілку + Гілка: + Ви збираєтеся видалити віддалену гілку!!! + Також видалити віддалену гілку ${0}$ + Видалити кілька гілок + Ви намагаєтеся видалити кілька гілок одночасно. Перевірте ще раз перед виконанням! + Видалити віддалене сховище + Віддалене сховище: + Шлях: + Ціль: + Усі дочірні елементи будуть видалені зі списку. + Це видалить сховище лише зі списку, а не з диска! + Підтвердити видалення групи + Підтвердити видалення сховища + Видалити підмодуль + Шлях до підмодуля: + Видалити тег + Тег: + Видалити з віддалених сховищ + РІЗНИЦЯ ДЛЯ БІНАРНИХ ФАЙЛІВ + НОВИЙ + СТАРИЙ + Копіювати + Змінено режим файлу + Перша відмінність + Ігнорувати зміни пробілів + Остання відмінність + ЗМІНА ОБ'ЄКТА LFS + Наступна відмінність + НЕМАЄ ЗМІН АБО ЛИШЕ ЗМІНИ КІНЦЯ РЯДКА + Попередня відмінність + Зберегти як патч + Показати приховані символи + Порівняння пліч-о-пліч + ПІДМОДУЛЬ + НОВИЙ + Поміняти місцями + Підсвітка синтаксису + Перенос слів + Увімкнути навігацію блоками + Відкрити в інструменті злиття + Показати всі рядки + Зменшити кількість видимих рядків + Збільшити кількість видимих рядків + ОБЕРІТЬ ФАЙЛ ДЛЯ ПЕРЕГЛЯДУ ЗМІН + Відкрити в інструменті злиття + Скасувати зміни + Усі локальні зміни в робочій копії. + Зміни: + Включити файли, які ігноруються + {0} змін будуть відхилені + Ви не можете скасувати цю дію!!! + Закладка: + Нова назва: + Ціль: + Редагувати вибрану групу + Редагувати вибраний репозиторій + Виконати спеціальну дію + Ім'я дії: + Витягти + Витягти всі віддалені сховища + Примусово перезаписати локальні refs + Витягти без тегів + Віддалений: + Витягти зміни з віддалених репозиторіїв + Вважати незмінними + Скасувати... + Скасувати {0} файлів... + Скасувати зміни в вибраних рядках + Відкрити зовнішній інструмент злиття + Розв'язати за допомогою ${0}$ + Зберегти як патч... + Стагнути + Стагнути {0} файлів + Стагнути зміни в вибраних рядках + Схованка... + Схованка {0} файлів... + Скинути стаг + Скинути {0} файлів + Скинути зміни в вибраних рядках + Використовувати Mine (checkout --ours) + Використовувати Theirs (checkout --theirs) + Історія файлу + ЗМІНА + ЗМІСТ + Git-Flow + Розробка гілки: + Функція: + Префікс функції: + FLOW - Завершити функцію + FLOW - Завершити гарячу поправку + FLOW - Завершити реліз + Ціль: + Гаряча поправка: + Префікс гарячої поправки: + Ініціалізувати Git-Flow + Залишити гілку + Гілка виробництва: + Реліз: + Префікс релізу: + Почати функцію... + FLOW - Почати функцію + Почати гарячу поправку... + FLOW - Почати гарячу поправку + Введіть назву + Почати реліз... + FLOW - Почати реліз + Тег версії Префікс: + Git LFS + Додати шаблон для відстеження... + Шаблон є ім'ям файлу + Спеціальний шаблон: + Додати шаблон для відстеження до Git LFS + Витягти + Запустіть `git lfs fetch`, щоб завантажити об'єкти Git LFS. Це не оновлює робочу копію. + Витягти об'єкти LFS + Встановити Git LFS hooks + Показати блокування + Немає заблокованих файлів + Заблокувати + Показати лише мої блокування + LFS блокування + Розблокувати + Примусово розблокувати + Принт + Запустіть `git lfs prune`, щоб видалити старі файли з локального сховища + Витягти + Запустіть `git lfs pull`, щоб завантажити всі файли Git LFS для поточної ref & checkout + Витягти об'єкти LFS + Надіслати + Надіслати чернетки великих файлів до кінця Git LFS + Надіслати об'єкти LFS + Віддалений: + Відстежувати файли, названі '{0}' + Відстежувати всі *{0} файли + ІСТОРІЯ + АВТОР + ЧАС АВТОРА + ГРАФ ТА ТЕМА + SHA + ЧАС КОМІТУ + ВИБРАНО {0} КОМІТІВ + Утримуйте 'Ctrl' або 'Shift' для вибору кількох комітів. + Утримуйте ⌘ або ⇧ для вибору кількох комітів. + ПОРАДИ: + Гарячі клавіші + ГЛОБАЛЬНІ + Скасувати поточне спливаюче вікно + Клонувати нове сховище + Закрити поточну вкладку + Перейти до наступної вкладки + Перейти до попередньої вкладки + Створити нову вкладку + Відкрити діалог Налаштування + СХОВИЩЕ + Закомітити проіндексовані зміни + Закомітити та надіслати проіндексовані зміни + Індексувати всі зміни та закомітити + Створити нову гілку на основі вибраного коміту + Скасувати вибрані зміни + Fetch, запускається без діалогу + Режим панелі керування (за замовчуванням) + Режим пошуку комітів + Pull, запускається без діалогу + Push, запускається без діалогу + Примусово перезавантажити це сховище + Індексувати/Видалити з індексу вибрані зміни + Перейти до 'Зміни' + Перейти до 'Історія' + Перейти до 'Схованки' + ТЕКСТОВИЙ РЕДАКТОР + Закрити панель пошуку + Знайти наступний збіг + Знайти попередній збіг + Відкрити панель пошуку + Скасувати + Індексувати + Видалити з індексу + Ініціалізувати сховище + Шлях: + Cherry-pick в процесі. + Обробка коміту + Злиття в процесі. + Виконується злиття + Перебазування в процесі. + Зупинено на + Скасування в процесі. + Скасування коміту + Інтерактивне перебазування + На: + Цільова гілка: + Копіювати посилання + Відкрити в браузері + ПОМИЛКА + ПОВІДОМЛЕННЯ + Злиття гілки + В: + Опція злиття: + Джерело: + Злиття (Кілька) + Закомітити всі зміни + Стратегія: + Цілі: + Перемістити вузол сховища + Виберіть батьківський вузол для: + Назва: + Git не налаштовано. Будь ласка, перейдіть до [Налаштування] та налаштуйте його. + Відкрити теку зберігання даних + Відкрити за допомогою... + Необов'язково. + Створити нову вкладку + Закладка + Закрити вкладку + Закрити інші вкладки + Закрити вкладки праворуч + Копіювати шлях до сховища + Сховища + Вставити + {0} днів тому + годину тому + {0} годин тому + Щойно + Минулого місяця + Минулого року + {0} хвилин тому + {0} місяців тому + {0} років тому + Вчора + Налаштування + AI + Промпт для аналізу різниці + Ключ API + Промпт для генерації теми + Модель + Назва + Сервер + Увімкнути потокове відтворення + ВИГЛЯД + Шрифт за замовчуванням + Ширина табуляції в редакторі + Розмір шрифту + За замовчуванням + Редактор + Моноширинний шрифт + Використовувати моноширинний шрифт лише в текстовому редакторі + Тема + Перевизначення теми + Використовувати фіксовану ширину вкладки в заголовку + Використовувати системну рамку вікна + ІНСТРУМЕНТ DIFF/MERGE + Шлях встановлення + Введіть шлях до інструменту diff/merge + Інструмент + ЗАГАЛЬНІ + Перевіряти оновлення при запуску + Формат дати + Мова + Кількість комітів в історії + Показувати час автора замість часу коміту в графі + Показувати дочірні коміти в деталях + Показувати теги в графі комітів + Довжина лінії-орієнтира для теми + GIT + Увімкнути авто-CRLF + Тека клонування за замовчуванням + Email користувача + Глобальний email користувача git + Увімкнути --prune при fetch + Git (>= 2.25.1) є обов'язковим для цієї програми + Шлях встановлення + Увімкнути перевірку HTTP SSL + Ім'я користувача + Глобальне ім'я користувача git + Версія Git + ПІДПИС GPG + Підпис GPG для комітів + Формат GPG + Шлях встановлення програми + Введіть шлях до встановленої програми GPG + Підпис GPG для тегів + Ключ підпису користувача + Ключ підпису GPG користувача + ІНТЕГРАЦІЯ + КОНСОЛЬ/ТЕРМІНАЛ + Шлях + Консоль/Термінал + Prune для віддаленого сховища + Ціль: + Prune для робочих дерев + Видалити застарілу інформацію про робочі дерева в `$GIT_COMMON_DIR/worktrees` + Pull (Витягти) + Віддалена гілка: + В: + Локальні зміни: + Скасувати + Сховати та Застосувати + Віддалене сховище: + Pull (Fetch & Merge) + Використовувати rebase замість merge + Push (Надіслати) + Переконатися, що підмодулі надіслано + Примусовий push + Локальна гілка: + Віддалене сховище: + Надіслати зміни на віддалене сховище + Віддалена гілка: + Встановити як відстежувану гілку + Надіслати всі теги + Надіслати тег на віддалене сховище + Надіслати на всі віддалені сховища + Віддалене сховище: + Тег: + Вийти + Перебазувати поточну гілку + Сховати та застосувати локальні зміни + На: + Перебазувати: + Додати віддалене сховище + Редагувати віддалене сховище + Назва: + Назва віддаленого сховища + URL сховища: + URL віддаленого git сховища + Копіювати URL + Видалити... + Редагувати... + Fetch (Отримати) + Відкрити у браузері + Prune (Очистити) + Підтвердити видалення робочого дерева + Увімкнути опцію `--force` + Ціль: + Перейменувати гілку + Нова назва: + Унікальна назва для цієї гілки + Гілка: + ПЕРЕРВАТИ + Автоматичне отримання змін з віддалених сховищ... + Очистка (GC & Prune) + Виконати команду `git gc` для цього сховища. + Очистити все + Налаштувати це сховище + ПРОДОВЖИТИ + Спеціальні дії + Немає спеціальних дій + Скасувати всі зміни + Увімкнути опцію '--reflog' + Відкрити у файловому менеджері + Пошук гілок/тегів/підмодулів + Видимість у графі + Не встановлено + Приховати в графі комітів + Фільтрувати в графі комітів + Увімкнути опцію '--first-parent' + РОЗТАШУВАННЯ + Горизонтальне + Вертикальне + ПОРЯДОК КОМІТІВ + За датою коміту + Топологічний + ЛОКАЛЬНІ ГІЛКИ + Перейти до HEAD + Створити гілку + ОЧИСТИТИ СПОВІЩЕННЯ + Виділяти лише поточну гілку в графі + Відкрити в {0} + Відкрити в зовнішніх інструментах + Оновити + ВІДДАЛЕНІ СХОВИЩА + ДОДАТИ ВІДДАЛЕНЕ СХОВИЩЕ + Пошук коміту + Автор + Комітер + Файл + Повідомлення + SHA + Поточна гілка + Показати теги як дерево + ПРОПУСТИТИ + Статистика + ПІДМОДУЛІ + ДОДАТИ ПІДМОДУЛЬ + ОНОВИТИ ПІДМОДУЛЬ + ТЕГИ + НОВИЙ ТЕГ + За датою створення + За назвою + Сортувати + Відкрити в терміналі + Використовувати відносний час в історії + РОБОЧІ ДЕРЕВА + ДОДАТИ РОБОЧЕ ДЕРЕВО + PRUNE (ОЧИСТИТИ) + URL Git сховища + Скинути поточну гілку до ревізії + Режим скидання: + Перемістити до: + Поточна гілка: + Показати у файловому менеджері + Revert (Скасувати коміт) + Коміт: + Закомітити зміни скасування + Змінити повідомлення коміту + Використовуйте 'Shift+Enter' для введення нового рядка. 'Enter' - гаряча клавіша кнопки OK + Виконується. Будь ласка, зачекайте... + ЗБЕРЕГТИ + Зберегти як... + Патч успішно збережено! + Сканувати сховища + Коренева тека: + Перевірити оновлення... + Доступна нова версія програми: + Не вдалося перевірити оновлення! + Завантажити + Пропустити цю версію + Оновлення програми + У вас встановлена остання версія. + Встановити відстежувану гілку + Гілка: + Скасувати upstream + Upstream: + Копіювати SHA + Перейти до + Squash (Склеїти коміти) + В: + Приватний ключ SSH: + Шлях до сховища приватного ключа SSH + ПОЧАТИ + Stash (Сховати) + Автоматично відновити після схову + Ваші робочі файли залишаться без змін, але буде збережено схованку. + Включити невідстежувані файли + Зберегти проіндексовані файли + Повідомлення: + Необов'язково. Назва цієї схованки + Лише проіндексовані зміни + Будуть сховані як проіндексовані, так і не проіндексовані зміни вибраних файлів!!! + Сховати локальні зміни + Застосувати + Видалити + Зберегти як патч... + Видалити схованку + Видалити: + СХОВАНКИ + ЗМІНИ + СХОВАНКИ + Статистика + КОМІТИ + КОМІТЕР + ОГЛЯД + МІСЯЦЬ + ТИЖДЕНЬ + АВТОРІВ: + КОМІТІВ: + ПІДМОДУЛІ + Додати підмодуль + Копіювати відносний шлях + Отримати вкладені підмодулі + Відкрити сховище підмодуля + Відносний шлях: + Відносна тека для зберігання цього модуля. + Видалити підмодуль + OK + Копіювати назву тегу + Копіювати повідомлення тегу + Видалити ${0}$... + Злиття ${0}$ в ${1}$... + Надіслати ${0}$... + Оновити підмодулі + Усі підмодулі + Ініціалізувати за потреби + Рекурсивно + Підмодуль: + Використовувати опцію --remote + URL: + Попередження + Вітальна сторінка + Створити групу + Створити підгрупу + Клонувати сховище + Видалити + ПІДТРИМУЄТЬСЯ ПЕРЕТЯГУВАННЯ ТЕК. МОЖЛИВЕ ГРУПУВАННЯ. + Редагувати + Перемістити до іншої групи + Відкрити всі сховища + Відкрити сховище + Відкрити термінал + Пересканувати сховища у теці клонування за замовчуванням + Пошук сховищ... + Сортувати + ЛОКАЛЬНІ ЗМІНИ + Git Ignore + Ігнорувати всі файли *{0} + Ігнорувати файли *{0} у цій же теці + Ігнорувати файли у цій же теці + Ігнорувати лише цей файл + Amend (Доповнити) + Тепер ви можете проіндексувати цей файл. + КОМІТ + КОМІТ ТА PUSH + Шаблон/Історії + Викликати подію кліку + Коміт (Редагувати) + Індексувати всі зміни та закомітити + Ви проіндексували {0} файл(ів), але відображено лише {1} ({2} файлів відфільтровано). Продовжити? + ВИЯВЛЕНО КОНФЛІКТИ + ВІДКРИТИ ЗОВНІШНІЙ ІНСТРУМЕНТ ЗЛИТТЯ + ВІДКРИТИ ВСІ КОНФЛІКТИ В ЗОВНІШНЬОМУ ІНСТРУМЕНТІ ЗЛИТТЯ + КОНФЛІКТИ ФАЙЛІВ ВИРІШЕНО + ВИКОРИСТАТИ МОЮ ВЕРСІЮ + ВИКОРИСТАТИ ЇХНЮ ВЕРСІЮ + ВКЛЮЧИТИ НЕВІДСТЕЖУВАНІ ФАЙЛИ + НЕМАЄ ОСТАННІХ ПОВІДОМЛЕНЬ + НЕМАЄ ШАБЛОНІВ КОМІТІВ + Клацніть правою кнопкою миші на вибраних файлах та оберіть спосіб вирішення конфліктів. + Підпис + ПРОІНДЕКСОВАНІ + ВИДАЛИТИ З ІНДЕКСУ + ВИДАЛИТИ ВСЕ З ІНДЕКСУ + НЕПРОІНДЕКСОВАНІ + ІНДЕКСУВАТИ + ІНДЕКСУВАТИ ВСЕ + ПЕРЕГЛЯНУТИ ФАЙЛИ, ЩО ВВАЖАЮТЬСЯ НЕЗМІНЕНИМИ + Шаблон: ${0}$ + РОБОЧИЙ ПРОСТІР: + Налаштувати робочі простори... + РОБОЧЕ ДЕРЕВО + Копіювати шлях + Заблокувати + Видалити + Розблокувати + diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 919e4093..2a117fb8 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -2,41 +2,34 @@ + 关于软件 关于本软件 - • 项目依赖于 - • 图表绘制组件来自 - © 2024 sourcegit-scm - • 文本编辑器使用 - • 等宽字体来自于 - • 项目源代码地址 开源免费的Git客户端 新增工作树 - 检出分支方式 : - 已有分支 - 创建新分支 工作树路径 : 填写该工作树的路径。支持相对路径。 分支名 : 选填。默认使用目标文件夹名称。 跟踪分支 设置上游跟踪分支 - OpenAI助手 - 使用OpenAI助手生成提交信息 + 检出分支方式 : + 创建新分支 + 已有分支 + AI助手 + 重新生成 + 使用AI助手生成提交信息 + 应用本次生成 应用补丁(apply) - 错误 - 输出错误,并终止应用补丁 - 更多错误 - 与【错误】级别相似,但输出内容更多 补丁文件 : 选择补丁文件 忽略空白符号 - 忽略 - 关闭所有警告 应用补丁 - 警告 - 应用补丁,输出关于空白符的警告 空白符号处理 : + 应用贮藏 + 在成功应用后丢弃该贮藏 + 恢复索引中已暂存的变化 + 已选贮藏 : 存档(archive) ... 存档文件路径: 选择存档文件的存放路径 @@ -46,45 +39,59 @@ 不跟踪更改的文件 没有不跟踪更改的文件 移除 + 加载本地图片 + 重新加载 二进制文件不支持该操作!!! + 二分定位(bisect) + 终止 + 标记错误 + 二分定位进行中。当前提交是 '正确' 还是 '错误' ? + 标记正确 + 无法判定 + 二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。 逐行追溯(blame) 选中文件不支持该操作!!! 检出(checkout) ${0}$... - 与其他分支对比 - 与当前HEAD比较 + 与当前 ${0}$ 比较 与本地工作树比较 复制分支名 + 自定义操作 删除 ${0}$... 删除选中的 {0} 个分支 - 放弃所有更改 快进(fast-forward)到 ${0}$ + 拉取(fetch) ${0}$ 至 ${1}$... GIT工作流 - 完成 ${0}$ 合并 ${0}$ 到 ${1}$... + 合并 {0} 个分支到当前分支 拉回(pull) ${0}$ 拉回(pull) ${0}$ 内容至 ${1}$... 推送(push)${0}$ - 变基(rebase) ${0}$ 分支至 ${1}$... + 变基(rebase) ${0}$ 至 ${1}$... 重命名 ${0}$... - 切换上游分支 - 取消追踪 + 重置 ${0}$ 到 ${1}$... + 切换上游分支 ... 分支比较 + 跟踪的上游分支不存在或已删除! 字节 取 消 - 重置文件到该版本 重置文件到上一版本 + 重置文件到该版本 + 生成提交信息 切换变更显示模式 文件名+路径列表模式 全路径列表模式 文件目录树形结构模式 检出(checkout)分支 检出(checkout)提交 - 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 提交 : - 目标分支 : + 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 + 同时更新所有子模块 + 目标分支 : + 检出分支并快进 + 上游分支 : 挑选提交 提交信息中追加来源信息 提交列表 : @@ -99,17 +106,24 @@ 本地仓库名 : 本地仓库目录的名字,选填。 父级目录 : + 初始化并更新子模块 远程仓库 : 关闭 提交信息编辑器 + 检出此提交 挑选(cherry-pick)此提交 挑选(cherry-pick)... - 检出此提交 与当前HEAD比较 与本地工作树比较 - 复制简要信息 - 复制提交指纹 + 作者 + 提交者 + 简要信息 + 提交指纹 + 主题 + 自定义操作 交互式变基(rebase -i) ${0}$ 到此处 + 合并(merge)此提交至 ${0}$ + 合并(merge)... 变基(rebase) ${0}$ 到此处 重置(reset) ${0}$ 到此处 回滚此提交 @@ -118,13 +132,16 @@ 合并此提交到上一个提交 合并之后的提交到此处 变更对比 + 个文件发生变更 查找变更... 文件列表 LFS文件 + 查找文件... 子模块 基本信息 修改者 变更列表 + 子提交 提交者 查看包含此提交的分支/标签 本提交已被以下分支/标签包含 @@ -134,33 +151,58 @@ 相关引用 提交指纹 浏览器中查看 - 填写提交信息主题 详细描述 + 主题 + 填写提交信息主题 仓库配置 提交信息模板 - 模板名 : 模板内容 : + 模板名 : + 自定义操作 + 命令行参数 : + 请使用${REPO}代替仓库路径,${BRANCH}代替选中的分支,${SHA}代替提交哈希 + 可执行文件路径 : + 名称 : + 作用目标 : + 选中的分支 + 选中的提交 + 仓库 + 等待操作执行完成 电子邮箱 邮箱地址 GIT配置 启用定时自动拉取远程更新 分钟 默认远程 + 默认合并方式 ISSUE追踪 + 新增匹配Azure DevOps规则 + 新增匹配Gitee议题规则 + 新增匹配Gitee合并请求规则 新增匹配Github Issue规则 + 新增匹配GitLab议题规则 + 新增匹配GitLab合并请求规则 新增匹配Jira规则 新增自定义规则 匹配ISSUE的正则表达式 : 规则名 : 为ISSUE生成的URL链接 : 可在URL中使用$1,$2等变量填入正则表达式匹配的内容 + AI + 启用特定服务 : + 当【启用特定服务】被设置时,SourceGit将在本仓库中仅使用该服务。否则将弹出可用的AI服务列表供用户选择。 HTTP代理 HTTP网络代理 用户名 应用于本仓库的用户名 工作区 颜色 + 名称 启动时恢复打开的仓库 + 确认继续 + 提交未包含变更文件!是否继续(--allow-empty)? + 自动暂存并提交 + 提交未包含变更文件!是否继续(--allow-empty)或是自动暂存所有变更并提交? 规范化提交信息生成 破坏性更新: 关闭的ISSUE: @@ -170,19 +212,19 @@ 类型: 复制 复制全部文本 - 复制内容 + 复制完整路径 复制路径 - 复制文件名 新建分支 ... 新分支基于 : 完成后切换到新分支 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 新分支名 : 填写分支名称。 + 空格将被替换为'-'符号 创建本地分支 + 允许重置已存在的分支 新建标签 ... 标签位于 : 使用GPG签名 @@ -197,6 +239,9 @@ 轻量标签 按住Ctrl键点击将以默认参数运行 剪切 + 取消初始化子模块 + 强制取消,即使包含本地变更 + 子模块 : 删除分支确认 分支名 : 您正在删除远程上的分支,请务必小心!!! @@ -205,7 +250,10 @@ 您正在尝试一次性删除多个分支,请务必仔细检查后再执行操作! 删除远程确认 远程名 : + 路径 : 目标 : + 所有子节点将被同时从列表中移除。 + 仅从列表中移除,不会删除硬盘中的文件! 删除分组确认 删除仓库确认 删除子模块确认 @@ -218,19 +266,25 @@ 原始大小 复制 文件权限已变化 + 首个差异 忽略空白符号变化 + 最后一个差异 LFS对象变更 下一个差异 没有变更或仅有换行符差异 上一个差异 + 保存为补丁文件 显示隐藏符号 分列对比 子模块 + 删除 新增 交换比对双方 语法高亮 自动换行 + 启用基于变更块的跳转 使用外部合并工具查看 + 显示完整文件 减少可见的行数 增加可见的行数 请选择需要对比的文件 @@ -246,11 +300,12 @@ 目标 : 编辑分组 编辑仓库 - 快进(fast-forward,无需checkout) + 执行自定义操作 + 自定义操作 : 拉取(fetch) 拉取所有的远程仓库 + 强制覆盖本地REFs 不拉取远程标签 - 自动清理远程已删除分支 远程仓库 : 拉取远程仓库内容 不跟踪此文件的更改 @@ -258,6 +313,7 @@ 放弃 {0} 个文件的更改... 放弃选中的更改 使用外部合并工具打开 + 应用 ${0}$ 另存为补丁... 暂存(add) 暂存(add){0} 个文件 @@ -267,12 +323,11 @@ 从暂存中移除 从暂存中移除 {0} 个文件 从暂存中移除选中的更改 - 使用 THEIRS (checkout --theirs) 使用 MINE (checkout --ours) + 使用 THEIRS (checkout --theirs) 文件历史 - 文件内容 文件变更 - 过滤 + 文件内容 GIT工作流 开发分支 : 特性分支 : @@ -281,6 +336,8 @@ 结束修复分支 结束版本分支 目标分支 : + 完成后自动推送 + 压缩变更为单一提交后合并分支 修复分支 : 修复分支名前缀 : 初始化GIT工作流 @@ -302,8 +359,8 @@ 规则 : 添加LFS追踪文件规则 拉取LFS对象 (fetch) - 拉取LFS对象 执行`git lfs prune`命令,下载远程LFS对象,但不会更新工作副本。 + 拉取LFS对象 启用Git LFS支持 显示LFS对象锁 没有锁定的LFS文件 @@ -315,16 +372,15 @@ 精简本地LFS对象存储 运行`git lfs prune`命令,从本地存储中精简当前版本不需要的LFS对象 拉回LFS对象 (pull) - 拉回LFS对象 运行`git lfs pull`命令,下载远程LFS对象并更新工作副本。 + 拉回LFS对象 推送 - 推送LFS对象 将排队的大文件推送到Git LFS远程服务 + 推送LFS对象 远程 : 跟踪名为'{0}'的文件 跟踪所有 *{0} 文件 历史记录 - 切换横向/纵向显示 作者 修改时间 路线图与主题 @@ -337,20 +393,27 @@ 快捷键参考 全局快捷键 取消弹出面板 + 克隆远程仓库 关闭当前页面 - 切换到上一个页面 切换到下一个页面 + 切换到上一个页面 新建页面 - 打开偏好设置面板 + 打开偏好设置面板 + 切换工作区 + 切换显示页面 仓库页面快捷键 提交暂存区更改 提交暂存区更改并推送 自动暂存全部变更并提交 + 基于选中提交创建新分支 丢弃选中的更改 + 拉取 (fetch) 远程变更 切换左边栏为分支/标签等显示模式(默认) + 切换左边栏为提交搜索模式 + 拉回 (pull) 远程变更 + 推送本地变更到远程 重新加载仓库状态 将选中的变更暂存或从暂存列表中移除 - 切换左边栏为提交搜索模式 显示本地更改 显示历史记录 显示贮藏列表 @@ -358,25 +421,38 @@ 关闭搜索 定位到下一个匹配搜索的位置 定位到上一个匹配搜索的位置 + 使用外部比对工具查看 打开搜索 + 丢弃 暂存 移出暂存区 - 丢弃 初始化新仓库 路径 : - 挑选(Cherry-Pick)操作进行中。点击【终止】回滚到操作前的状态。 - 合并操作进行中。点击【终止】回滚到操作前的状态。 - 变基(Rebase)操作进行中。点击【终止】回滚到操作前的状态。 - 回滚提交操作进行中。点击【终止】回滚到操作前的状态。 + 挑选(Cherry-Pick)操作进行中。 + 正在处理提交 + 合并操作进行中。 + 正在处理 + 变基(Rebase)操作进行中。 + 当前停止于 + 回滚提交操作进行中。 + 正在回滚提交 交互式变基 - 目标分支 : 起始提交 : + 目标分支 : + 复制链接地址 + 在浏览器中访问 出错了 系统提示 + 工作区列表 + 页面列表 合并分支 目标分支 : 合并方式 : - 合并分支 : + 合并目标 : + 合并(多目标) + 提交变化 + 合并策略 : + 目标列表 : 调整仓库分组 请选择目标分组: 名称 : @@ -392,70 +468,86 @@ 复制仓库路径 新标签页 粘贴 - 刚刚 - {0}分钟前 - {0}小时前 - 昨天 {0}天前 + 1小时前 + {0}小时前 + 刚刚 上个月 - {0}个月前 一年前 + {0}分钟前 + {0}个月前 {0}年前 - 偏好设置 - 外观配置 - 缺省字体 - 默认字体大小 - 等宽字体 - 仅在文本编辑器中使用等宽字体 - 主题 - 主题自定义 - 主标签使用固定宽度 - 使用系统默认窗体样式 - 对比/合并工具 - 安装路径 - 填写工具可执行文件所在位置 - 工具 - 通用配置 - 启动时检测软件更新 - 显示语言 - 最大历史提交数 - 在提交路线图中显示修改时间而非提交时间 - SUBJECT字数检测 - GIT配置 - 自动换行转换 - 默认克隆路径 - 邮箱 - 默认GIT用户邮箱 - 安装路径 - 用户名 - 默认GIT用户名 - Git 版本 - 本软件要求GIT最低版本为2.23.0 - GPG签名 - 启用提交签名 - 启用标签签名 - 签名格式 - 签名程序位置 - 签名程序所在路径 - 用户签名KEY - 输入签名提交所使用的KEY - 第三方工具集成 - 终端/SHELL - 终端/SHELL - 安装路径 + 昨天 + 偏好设置 + AI + Analyze Diff Prompt + API密钥 + Generate Subject Prompt + 模型 + 配置名称 + 服务地址 + 启用流式输出 + 外观配置 + 缺省字体 + 编辑器制表符宽度 + 字体大小 + 默认 + 代码编辑器 + 等宽字体 + 仅在文本编辑器中使用等宽字体 + 主题 + 主题自定义 + 主标签使用固定宽度 + 使用系统默认窗体样式 + 对比/合并工具 + 安装路径 + 填写工具可执行文件所在位置 + 工具 + 通用配置 + 启动时检测软件更新 + 日期时间格式 + 显示语言 + 最大历史提交数 + 在提交路线图中显示修改时间而非提交时间 + 在提交详情页中显示子提交列表 + 在提交路线图中显示标签 + SUBJECT字数检测 + GIT配置 + 自动换行转换 + 默认克隆路径 + 邮箱 + 默认GIT用户邮箱 + 拉取更新时启用修剪(--prune) + 对比文件时,默认忽略换行符变更 (--ignore-cr-at-eol) + 本软件要求GIT最低版本为2.25.1 + 安装路径 + 启用HTTP SSL验证 + 用户名 + 默认GIT用户名 + Git 版本 + GPG签名 + 启用提交签名 + 签名格式 + 签名程序位置 + 签名程序所在路径 + 启用标签签名 + 用户签名KEY + 输入签名提交所使用的KEY + 第三方工具集成 + 终端/SHELL + 安装路径 + 终端/SHELL 清理远程已删除分支 目标 : 清理工作树 - 清理在`$GIT_DIR/worktrees`中的无效工作树信息 + 清理在`$GIT_COMMON_DIR/worktrees`中的无效工作树信息 拉回(pull) 拉取分支 : - 拉取远程中的所有分支变更 本地分支 : 未提交更改 : 丢弃更改 - 不做处理 贮藏并自动恢复 - 不拉取远程标签 + 同时更新所有子模块 远程 : 拉回(拉取并合并) 使用变基方式合并分支 @@ -477,7 +569,6 @@ 自动贮藏并恢复本地变更 目标提交 : 分支 : - 重新加载 添加远程仓库 编辑远程仓库 远程名 : @@ -499,39 +590,66 @@ 分支 : 终止合并 自动拉取远端变更中... + 排序方式 + 按提交时间 + 按名称 清理本仓库(GC) 本操作将执行`git gc`命令。 清空过滤规则 + 清空 配置本仓库 下一步 + 自定义操作 + 自定义操作未设置 + 放弃所有更改 启用 --reflog 选项 在文件浏览器中打开 快速查找分支/标签/子模块 - 过滤规则 : + 设置在列表中的可见性 + 不指定 + 在提交列表中隐藏 + 使用其对提交列表过滤 + 启用 --first-parent 过滤选项 + 布局方式 + 水平排布 + 竖直排布 + 提交列表排序规则 + 按提交时间 + 按拓扑排序 本地分支 定位HEAD - 启用 --first-parent 过滤选项 新建分支 + 清空通知列表 + 提交路线图中仅高亮显示当前分支 在 {0} 中打开 使用外部工具打开 重新加载 远程列表 添加远程 - 解决冲突 查找提交 + 作者 + 提交者 + 变更内容 文件 提交信息 提交指纹 - 作者及提交者 仅在当前分支中查找 + 以树型结构展示 以树型结构展示 + 跳过此提交 提交统计 子模块列表 添加子模块 更新子模块 标签列表 新建标签 + 按创建时间 + 按名称 + 排序 在终端中打开 + 在提交列表中使用相对时间 + 查看命令日志 + 访问远程仓库 '{0}' 工作树列表 新增工作树 清理 @@ -540,6 +658,9 @@ 重置模式 : 提交 : 当前分支 : + 重置所选分支(非当前分支) + 重置点 : + 操作分支 : 在文件浏览器中查看 回滚操作确认 目标提交 : @@ -559,13 +680,22 @@ 忽略此版本 软件更新 当前已是最新版本。 + 切换上游分支 + 本地分支 : + 取消追踪 + 上游分支 : + 复制提交指纹 + 跳转到提交 压缩为单个提交 合并入: SSH密钥 : SSH密钥文件 开 始 贮藏(stash) + 贮藏后自动恢复工作区 + 工作区文件保持未修改状态,但贮藏内容已保存。 包含未跟踪的文件 + 保留暂存区文件 信息 : 选填,用于命名此贮藏 仅贮藏暂存区的变更 @@ -573,7 +703,7 @@ 贮藏本地变更 应用(apply) 删除(drop) - 应用并删除(pop) + 另存为补丁... 丢弃贮藏确认 丢弃贮藏 : 贮藏列表 @@ -582,31 +712,43 @@ 提交统计 提交次数 提交者 + 总览 本月 本周 - 提交次数: 贡献者人数: - 总览 + 提交次数: 子模块 添加子模块 复制路径 + 取消初始化 拉取子孙模块 打开仓库 相对仓库路径 : 本地存放的相对路径。 删除子模块 + 状态 + 未提交修改 + 未初始化 + SHA变更 + 未解决冲突 + 仓库 确 定 复制标签名 复制标签信息 删除 ${0}$... + 合并 ${0}$ 到 ${1}$... 推送 ${0}$... - 仓库地址 : 更新子模块 更新所有子模块 启用 '--init' 启用 '--recursive' 子模块 : 启用 '--remote' + 仓库地址 : + 日志列表 + 清空日志 + 复制 + 删除 警告 起始页 新建分组 @@ -628,28 +770,35 @@ 忽略同目录下所有 *{0} 文件 忽略同目录下所有文件 忽略本文件 - 修补(--amend) - 自动暂存 + 修补 现在您已可将其加入暂存区中 提交 提交并推送 历史输入/模板 触发点击事件 + 提交(修改原始提交) 自动暂存所有变更并提交 + 当前有 {0} 个文件在暂存区中,但仅显示了 {1} 个文件({2} 个文件被过滤掉了),是否继续提交? 检测到冲突 + 打开合并工具 + 打开合并工具解决冲突 文件冲突已解决 + 使用 MINE + 使用 THEIRS 显示未跟踪文件 没有提交信息记录 没有可应用的提交信息模板 + 重置提交者 + 请选中冲突文件,打开右键菜单,选择合适的解决方式 + 署名 已暂存 从暂存区移除选中 从暂存区移除所有 未暂存 暂存选中 暂存所有 - 查看忽略变更文件 + 查看忽略变更文件 模板:${0}$ - 请选中冲突文件,打开右键菜单,选择合适的解决方式 工作区: 配置工作区... 本地工作树 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index bccf4aab..29a5346d 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -2,41 +2,34 @@ + 關於 關於 SourceGit - • 專案依賴於 - • 圖表繪製元件來自 - © 2024 sourcegit-scm - • 文字編輯器使用 - • 等寬字型來自於 - • 專案原始碼網址 開源免費的 Git 客戶端 新增工作區 - 簽出分支方式: - 已有分支 - 建立新分支 工作區路徑: 填寫該工作區的路徑。支援相對路徑。 分支名稱: 選填。預設使用目標資料夾名稱。 追蹤分支 設定遠端追蹤分支 - OpenAI 助理 - 使用 OpenAI 產生提交訊息 + 簽出分支方式: + 建立新分支 + 已有分支 + AI 助理 + 重新產生 + 使用 AI 產生提交訊息 + 套用為提交訊息 套用修補檔 (apply patch) - 錯誤 - 輸出錯誤,並中止套用修補檔 - 更多錯誤 - 與 [錯誤] 級別相似,但輸出更多內容 修補檔: 選擇修補檔 忽略空白符號 - 忽略 - 關閉所有警告 套用修補檔 - 警告 - 套用修補檔,輸出關於空白字元的警告 空白字元處理: + 套用擱置變更 + 套用擱置變更後刪除 + 還原索引中已暫存的變更 (--index) + 已選擇擱置變更: 封存 (archive)... 封存檔案路徑: 選擇封存檔案的儲存路徑 @@ -46,45 +39,59 @@ 不追蹤變更的檔案 沒有不追蹤變更的檔案 移除 + 載入本機圖片... + 重新載入 二進位檔案不支援該操作! + 二分搜尋 (bisect) + 中止 + 標記為錯誤 + 二分搜尋進行中。目前的提交是「良好」是「錯誤」? + 標記為良好 + 無法確認 + 二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。 逐行溯源 (blame) 所選擇的檔案不支援該操作! 簽出 (checkout) ${0}$... - 與其他分支比較 - 與目前 HEAD 比較 + 與目前 ${0}$ 比較 與本機工作區比較 複製分支名稱 + 自訂動作 刪除 ${0}$... 刪除所選的 {0} 個分支 - 捨棄所有變更 快轉 (fast-forward) 到 ${0}$ + 提取 (fetch) ${0}$ 到 ${1}$... Git 工作流 - 完成 ${0}$ 合併 ${0}$ 到 ${1}$... + 合併 {0} 個分支到目前分支 拉取 (pull) ${0}$ 拉取 (pull) ${0}$ 內容至 ${1}$... 推送 (push) ${0}$ 重定基底 (rebase) ${0}$ 分支至 ${1}$... 重新命名 ${0}$... - 切換上游分支 - 取消設定上游分支 + 重設 ${0}$ 至 ${1}$... + 切換上游分支... 分支比較 + 追蹤上游分支不存在或已刪除! 位元組 取 消 - 重設檔案為此版本 重設檔案到上一版本 + 重設檔案為此版本 + 產生提交訊息 切換變更顯示模式 檔案名稱 + 路徑列表模式 全路徑列表模式 檔案目錄樹狀結構模式 簽出 (checkout) 分支 簽出 (checkout) 提交 - 注意: 執行該操作後,目前 HEAD 會變為分離 (detached) 狀態! 提交: - 目標分支: + 注意: 執行該操作後,目前 HEAD 會變為分離 (detached) 狀態! 未提交變更: 捨棄變更 - 不做處理 擱置變更並自動復原 + 同時更新所有子模組 + 目標分支: + 簽出分支並快轉 + 上游分支 : 揀選提交 提交資訊中追加來源資訊 提交列表: @@ -99,17 +106,24 @@ 本機存放庫名稱: 本機存放庫目錄的名稱,選填。 父級目錄: + 初始化並更新子模組 遠端存放庫: 關閉 提交訊息編輯器 + 簽出 (checkout) 此提交 揀選 (cherry-pick) 此提交 揀選 (cherry-pick)... - 簽出 (checkout) 此提交 與目前 HEAD 比較 與本機工作區比較 - 複製摘要資訊 - 複製提交編號 + 作者 + 提交者 + 摘要資訊 + 提交編號 + 標題 + 自訂動作 互動式重定基底 (rebase -i) ${0}$ 到此處 + 合併 (merge) 此提交到 ${0}$ + 合併 (merge)... 重定基底 (rebase) ${0}$ 到此處 重設 (reset) ${0}$ 到此處 復原此提交 @@ -118,13 +132,16 @@ 合併此提交到上一個提交 合併之後的提交到此處 變更對比 + 個檔案已變更 搜尋變更... 檔案列表 LFS 檔案 + 搜尋檔案... 子模組 基本資訊 作者 變更列表 + 後續提交 提交者 檢視包含此提交的分支或標籤 本提交包含於以下分支或標籤 @@ -134,33 +151,58 @@ 相關參照 提交編號 在瀏覽器中檢視 - 填寫提交訊息標題 詳細描述 + 標題 + 填寫提交訊息標題 存放庫設定 提交訊息範本 - 範本名稱: 範本內容: + 範本名稱: + 自訂動作 + 指令參數: + 使用 ${REPO} 表示存放庫路徑、${BRANCH} 表示所選的分支、${SHA} 表示所選的提交編號 + 可執行檔案路徑: + 名稱: + 執行範圍: + 選取的分支 + 選取的提交 + 存放庫 + 等待自訂動作執行結束 電子郵件 電子郵件地址 Git 設定 啟用定時自動提取 (fetch) 遠端更新 分鐘 預設遠端存放庫 + 預設合併模式 Issue 追蹤 + 新增符合 Azure DevOps 規則 + 新增符合 Gitee 議題規則 + 新增符合 Gitee 合併請求規則 新增符合 GitHub Issue 規則 + 新增符合 GitLab 議題規則 + 新增符合 GitLab 合併請求規則 新增符合 Jira 規則 新增自訂規則 - 符合 Issue 的正則表達式: + 符合 Issue 的正規表達式: 規則名稱: 為 Issue 產生的網址連結: - 可在網址中使用 $1、$2 等變數填入正則表示式相符的內容 + 可在網址中使用 $1、$2 等變數填入正規表達式相符的內容 + AI + 偏好服務: + 設定 [偏好服務] 後,SourceGit 將於此存放庫中使用該服務,否則會顯示 AI 服務列表供使用者選擇。 HTTP 代理 HTTP 網路代理 使用者名稱 用於本存放庫的使用者名稱 工作區 顏色 + 名稱 啟動時還原上次開啟的存放庫 + 確認繼續 + 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)? + 自動暫存並提交 + 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存全部變更並提交? 產生約定式提交訊息 破壞性變更: 關閉的 Issue: @@ -170,19 +212,19 @@ 類型: 複製 複製全部內容 - 複製內容 + 複製完整路徑 複製路徑 - 複製檔案名稱 新增分支... 新分支基於: 完成後切換到新分支 未提交變更: 捨棄變更 - 不做處理 擱置變更並自動復原 新分支名稱: 輸入分支名稱。 + 空格將以英文破折號取代 建立本機分支 + 允許覆寫現有分支 新增標籤... 標籤位於: 使用 GPG 簽章 @@ -197,6 +239,9 @@ 輕量標籤 按住 Ctrl 鍵將直接以預設參數執行 剪下 + 取消初始化子模組 + 強制取消,即使它包含本地變更 + 子模組 : 刪除分支確認 分支名稱: 您正在刪除遠端上的分支,請務必小心! @@ -205,7 +250,10 @@ 您正在嘗試一次性刪除多個分支,請務必仔細檢查後再刪除! 刪除遠端確認 遠端名稱: + 路徑: 目標: + 所有子節點都會從清單中移除。 + 只會從清單中移除,而不會刪除磁碟中的檔案! 刪除群組確認 刪除存放庫確認 刪除子模組確認 @@ -218,19 +266,25 @@ 原始大小 複製 檔案權限已變更 + 第一個差異 忽略空白符號變化 + 最後一個差異 LFS 物件變更 下一個差異 沒有變更或僅有換行字元差異 上一個差異 + 另存為修補檔 (patch) 顯示隱藏符號 並排對比 子模組 + 已刪除 新增 交換比對雙方 語法上色 自動換行 + 區塊切換上/下一個差異 使用外部合併工具檢視 + 顯示檔案的全部內容 減少可見的行數 增加可見的行數 請選擇需要對比的檔案 @@ -246,11 +300,12 @@ 目標: 編輯群組 編輯存放庫 - 快進 (fast-forward,無需 checkout) + 執行自訂動作 + 自訂動作: 提取 (fetch) 提取所有的遠端存放庫 + 強制覆寫本機 REFs 不提取遠端標籤 - 自動清理遠端已刪除分支 遠端存放庫: 提取遠端存放庫內容 不追蹤此檔案的變更 @@ -258,6 +313,7 @@ 捨棄已選的 {0} 個檔案變更... 捨棄選取的變更 使用外部合併工具開啟 + 使用 ${0}$ 另存為修補檔 (patch)... 暫存 (add) 暫存 (add) 已選的 {0} 個檔案 @@ -267,12 +323,11 @@ 取消暫存 從暫存中移除 {0} 個檔案 取消暫存選取的變更 - 使用對方版本 (checkout --theirs) - 使用我方版本 (checkout --ours) + 使用我方版本 (ours) + 使用對方版本 (theirs) 檔案歷史 - 檔案内容 檔案變更 - 篩選 + 檔案内容 Git 工作流 開發分支: 功能分支: @@ -281,6 +336,8 @@ 完成修復分支 完成發行分支 目標分支: + 完成後自動推送 + 壓縮為單一提交後合併 修復分支: 修復分支前置詞: 初始化 Git 工作流 @@ -302,8 +359,8 @@ 規則: 加入 LFS 追蹤檔案規則 提取 (fetch) - 提取 LFS 物件 執行 `git lfs fetch` 以下載遠端 LFS 物件,但不會更新工作副本。 + 提取 LFS 物件 啟用 Git LFS 支援 顯示 LFS 物件鎖 沒有鎖定的 LFS 物件 @@ -315,16 +372,15 @@ 清理 (prune) 執行 `git lfs prune` 以從本機中清理目前版本不需要的 LFS 物件 拉取 (pull) - 拉取 LFS 物件 執行 `git lfs pull` 以下載遠端 LFS 物件並更新工作副本。 + 拉取 LFS 物件 推送 (push) - 推送 LFS 物件 將大型檔案推送到 Git LFS 遠端服務 + 推送 LFS 物件 遠端存放庫: 追蹤名稱為「{0}」的檔案 追蹤所有 *{0} 檔案 歷史記錄 - 切換橫向/縱向顯示 作者 修改時間 路線圖與訊息標題 @@ -337,20 +393,27 @@ 快速鍵參考 全域快速鍵 取消彈出面板 + 複製 (clone) 遠端存放庫 關閉目前頁面 - 切換到上一個頁面 切換到下一個頁面 + 切換到上一個頁面 新增頁面 - 開啟偏好設定面板 + 開啟偏好設定面板 + 切換工作區 + 切換目前頁面 存放庫頁面快速鍵 提交暫存區變更 提交暫存區變更並推送 自動暫存全部變更並提交 + 基於選取的提交建立新分支 捨棄選取的變更 + 提取 (fetch) 遠端的變更 切換左邊欄為分支/標籤等顯示模式 (預設) + 切換左邊欄為歷史搜尋模式 + 拉取 (pull) 遠端的變更 + 推送 (push) 本機變更到遠端存放庫 強制重新載入存放庫 暫存或取消暫存選取的變更 - 切換左邊欄為歷史搜尋模式 顯示本機變更 顯示歷史記錄 顯示擱置變更列表 @@ -358,25 +421,38 @@ 關閉搜尋面板 前往下一個搜尋相符的位置 前往上一個搜尋相符的位置 + 使用外部比對工具檢視 開啟搜尋面板 + 捨棄 暫存 取消暫存 - 捨棄 初始化存放庫 路徑: - 揀選 (cherry-pick) 操作進行中。點選 [中止] 復原到操作前的狀態。 - 合併操作進行中。點選 [中止] 復原到操作前的狀態。 - 重定基底 (rebase) 操作進行中。點選 [中止] 復原到操作前的狀態。 - 復原提交操作進行中。點選 [中止] 復原到操作前的狀態。 + 揀選 (cherry-pick) 操作進行中。 + 正在處理提交 + 合併操作進行中。 + 正在處理 + 重定基底 (rebase) 操作進行中。 + 目前停止於 + 復原提交操作進行中。 + 正在復原提交 互動式重定基底 - 目標分支: 起始提交: + 目標分支: + 複製連結 + 在瀏覽器中開啟連結 發生錯誤 系統提示 + 工作區列表 + 頁面列表 合併分支 目標分支: 合併方式: - 合併分支: + 合併來源: + 合併 (多個來源) + 提交變更 + 合併策略: + 目標列表: 調整存放庫分組 請選擇目標分組: 名稱: @@ -392,74 +468,86 @@ 複製存放庫路徑 新分頁 貼上 - 剛剛 - {0} 分鐘前 - {0} 小時前 - 昨天 {0} 天前 + 1 小時前 + {0} 小時前 + 剛剛 上個月 - {0} 個月前 一年前 + {0} 分鐘前 + {0} 個月前 {0} 年前 - 偏好設定 - OpenAI - 伺服器 - API 金鑰 - 模型 - 外觀設定 - 預設字型 - 預設字型大小 - 等寬字型 - 僅在文字編輯器中使用等寬字型 - 佈景主題 - 自訂主題 - 使用固定寬度的分頁標籤 - 使用系統原生預設視窗樣式 - 對比/合併工具 - 安裝路徑 - 填寫可執行檔案所在路徑 - 工具 - 一般設定 - 啟動時檢查軟體更新 - 顯示語言 - 最大歷史提交數 - 在提交路線圖中顯示修改時間而非提交時間 - 提交標題字數偵測 - Git 設定 - 自動換行轉換 - 預設複製 (clone) 路徑 - 電子郵件 - 預設 Git 使用者電子郵件 - 安裝路徑 - 使用者名稱 - 預設 Git 使用者名稱 - Git 版本 - 本軟體要求 Git 最低版本為 2.23.0 - GPG 簽章 - 啟用提交簽章 - 啟用標籤簽章 - GPG 簽章格式 - 可執行檔案路徑 - 填寫 gpg.exe 所在路徑 - 使用者簽章金鑰 - 填寫簽章提交所使用的金鑰 - 第三方工具整合 - 終端機/Shell - 終端機/Shell - 安裝路徑 + 昨天 + 偏好設定 + AI + 分析變更差異提示詞 + API 金鑰 + 產生提交訊息提示詞 + 模型 + 名稱 + 伺服器 + 啟用串流輸出 + 外觀設定 + 預設字型 + 編輯器 Tab 寬度 + 字型大小 + 預設 + 程式碼 + 等寬字型 + 僅在文字編輯器中使用等寬字型 + 佈景主題 + 自訂主題 + 使用固定寬度的分頁標籤 + 使用系統原生預設視窗樣式 + 對比/合併工具 + 安裝路徑 + 填寫可執行檔案所在路徑 + 工具 + 一般設定 + 啟動時檢查軟體更新 + 日期時間格式 + 顯示語言 + 最大歷史提交數 + 在提交路線圖中顯示修改時間而非提交時間 + 在提交詳細資訊中顯示後續提交 + 在路線圖中顯示標籤 + 提交標題字數偵測 + Git 設定 + 自動換行轉換 + 預設複製 (clone) 路徑 + 電子郵件 + 預設 Git 使用者電子郵件 + 拉取變更時進行清理 (--prune) + 對比檔案時,預設忽略行尾的 CR 變更 (--ignore-cr-at-eol) + 本軟體要求 Git 最低版本為 2.25.1 + 安裝路徑 + 啟用 HTTP SSL 驗證 + 使用者名稱 + 預設 Git 使用者名稱 + Git 版本 + GPG 簽章 + 啟用提交簽章 + GPG 簽章格式 + 可執行檔案路徑 + 填寫 gpg.exe 所在路徑 + 啟用標籤簽章 + 使用者簽章金鑰 + 填寫簽章提交所使用的金鑰 + 第三方工具整合 + 終端機/Shell + 安裝路徑 + 終端機/Shell 清理遠端已刪除分支 目標: 清理工作區 - 清理在 `$GIT_DIR/worktrees` 中的無效工作區資訊 + 清理在 `$GIT_COMMON_DIR/worktrees` 中的無效工作區資訊 拉取 (pull) 拉取分支: - 拉取遠端中的所有分支 本機分支: 未提交變更: 捨棄變更 - 不做處理 擱置變更並自動復原 - 不拉取遠端標籤 + 同時更新所有子模組 遠端: 拉取 (提取並合併) 使用重定基底 (rebase) 合併分支 @@ -481,7 +569,6 @@ 自動擱置變更並復原本機變更 目標提交: 分支: - 重新載入 新增遠端存放庫 編輯遠端存放庫 遠端名稱: @@ -494,7 +581,6 @@ 提取 (fetch) 更新 在瀏覽器中存取網址 清理遠端已刪除分支 - 目標: 刪除工作區操作確認 啟用 [--force] 選項 目標工作區: @@ -504,39 +590,66 @@ 分支: 中止 自動提取遠端變更中... + 排序 + 依建立時間 + 依名稱升序 清理本存放庫 (GC) 本操作將執行 `git gc` 命令。 清空篩選規則 + 清空 設定本存放庫 下一步 + 自訂動作 + 沒有自訂的動作 + 捨棄所有變更 啟用 [--reflog] 選項 在檔案瀏覽器中開啟 快速搜尋分支/標籤/子模組 - 篩選規則: + 篩選以顯示或隱藏 + 取消指定 + 在提交列表中隱藏 + 以其篩選提交列表 + 啟用 [--first-parent] 選項 + 版面配置 + 橫向顯示 + 縱向顯示 + 提交顯示順序 + 依時間排序 + 依拓撲排序 本機分支 回到 HEAD - 啟用 [--first-parent] 選項 新增分支 + 清除所有通知 + 在提交路線圖中僅對目前分支上色 在 {0} 中開啟 使用外部工具開啟 重新載入 遠端列表 新增遠端 - 解決衝突 搜尋提交 + 作者 + 提交者 + 變更內容 檔案 提交訊息 提交編號 - 作者及提交者 僅搜尋目前分支 + 以樹型結構展示 以樹型結構展示 + 跳過此提交 提交統計 子模組列表 新增子模組 更新子模組 標籤列表 新增標籤 + 依建立時間 + 依名稱 + 排序 在終端機中開啟 + 在提交列表中使用相對時間 + 檢視 Git 指令記錄 + 檢視遠端存放庫 '{0}' 工作區列表 新增工作區 清理 @@ -545,6 +658,9 @@ 重設模式: 移至提交: 目前分支: + 重設選取的分支(非目前分支) + 重設位置 : + 選取分支 : 在檔案瀏覽器中檢視 復原操作確認 目標提交: @@ -564,13 +680,22 @@ 忽略此版本 軟體更新 目前已是最新版本。 + 切換上游分支 + 本機分支: + 取消設定上游分支 + 上游分支: + 複製提交編號 + 前往此提交 壓縮為單個提交 合併入: SSH 金鑰: SSH 金鑰檔案 開 始 擱置變更 (stash) + 擱置變更後自動復原工作區 + 工作區檔案保持未修改,但擱置內容已儲存。 包含未追蹤的檔案 + 保留已暫存的變更 擱置變更訊息: 選填,用於命名此擱置變更 僅擱置已暫存的變更 @@ -578,7 +703,7 @@ 擱置本機變更 套用 (apply) 刪除 (drop) - 套用並刪除 (pop) + 另存為修補檔 (patch)... 捨棄擱置變更確認 捨棄擱置變更: 擱置變更 @@ -587,31 +712,43 @@ 提交統計 提交次數 提交者 + 總覽 本月 本週 - 提交次數: 貢獻者人數: - 總覽 + 提交次數: 子模組 新增子模組 複製路徑 + 取消初始化 提取子模組 開啟存放庫 相對存放庫路徑: 本機存放的相對路徑。 刪除子模組 + 狀態 + 未提交變更 + 未初始化 + SHA 變更 + 未解決的衝突 + 存放庫 確 定 複製標籤名稱 複製標籤訊息 刪除 ${0}$... + 合併 ${0}$ 到 ${1}$... 推送 ${0}$... - 存放庫網址: 更新子模組 更新所有子模組 啟用 [--init] 選項 啟用 [--recursive] 選項 子模組: 啟用 [--remote] 選項 + 存放庫網址: + 記錄 + 清除所有記錄 + 複製 + 刪除 警告 起始頁 新增群組 @@ -624,8 +761,8 @@ 開啟所有包含存放庫 開啟本機存放庫 開啟終端機 - 快速搜尋存放庫... 重新掃描預設複製 (clone) 目錄下的存放庫 + 快速搜尋存放庫... 排序 本機變更 加入至 .gitignore 忽略清單 @@ -633,28 +770,35 @@ 忽略同路徑下所有 *{0} 檔案 忽略同路徑下所有檔案 忽略本檔案 - 修補 (--amend) - 自動暫存 + 修補 現在您已可將其加入暫存區中 提 交 提交並推送 歷史輸入/範本 觸發點擊事件 + 提交 (修改原始提交) 自動暫存全部變更並提交 - 檢測到衝突 + 您已暫存 {0} 個檔案,但只顯示 {1} 個檔案 ({2} 個檔案被篩選器隱藏)。您確定要繼續提交嗎? + 偵測到衝突 + 使用外部合併工具開啟 + 使用外部合併工具開啟 檔案衝突已解決 + 使用我方版本 (ours) + 使用對方版本 (theirs) 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 + 重設作者 + 請選擇發生衝突的檔案,開啟右鍵選單,選擇合適的解決方式 + 署名 已暫存 取消暫存選取的檔案 取消暫存所有檔案 未暫存 暫存選取的檔案 暫存所有檔案 - 檢視不追蹤變更的檔案 + 檢視不追蹤變更的檔案 範本: ${0}$ - 請選擇發生衝突的檔案,開啟右鍵選單,選擇合適的解決方式 工作區: 設定工作區... 本機工作區 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 50f4d830..15704775 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -2,8 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="using:SourceGit" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" xmlns:ae="using:AvaloniaEdit" + xmlns:aee="using:AvaloniaEdit.Editing" xmlns:aes="using:AvaloniaEdit.Search"> @@ -14,7 +16,7 @@ 12 - + @@ -36,9 +38,9 @@ - + - + + + + @@ -512,6 +525,79 @@ + + + + + + + + + - + @@ -850,6 +953,20 @@ + + + + + - - - - - - - - - - - - - - - - + + + - + + - - + + - - + + - - + + - - + + + + + - - + + + + - - + + + + + + - + - diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index 081160d0..045e9d1b 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -17,15 +17,6 @@ namespace SourceGit.Views { public class BranchTreeNodeIcon : UserControl { - public static readonly StyledProperty NodeProperty = - AvaloniaProperty.Register(nameof(Node)); - - public ViewModels.BranchTreeNode Node - { - get => GetValue(NodeProperty); - set => SetValue(NodeProperty, value); - } - public static readonly StyledProperty IsExpandedProperty = AvaloniaProperty.Register(nameof(IsExpanded)); @@ -35,16 +26,23 @@ namespace SourceGit.Views set => SetValue(IsExpandedProperty, value); } - static BranchTreeNodeIcon() + protected override void OnDataContextChanged(EventArgs e) { - NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); - IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + base.OnDataContextChanged(e); + UpdateContent(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsExpandedProperty) + UpdateContent(); } private void UpdateContent() { - var node = Node; - if (node == null) + if (DataContext is not ViewModels.BranchTreeNode node) { Content = null; return; @@ -52,31 +50,31 @@ namespace SourceGit.Views if (node.Backend is Models.Remote) { - CreateContent(new Thickness(0, 0, 0, 0), "Icons.Remote"); + CreateContent(new Thickness(0, 0, 0, 0), "Icons.Remote", false); } else if (node.Backend is Models.Branch branch) { if (branch.IsCurrent) - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Check"); + CreateContent(new Thickness(0, 0, 0, 0), "Icons.CheckCircled", true); else - CreateContent(new Thickness(2, 0, 0, 0), "Icons.Branch"); + CreateContent(new Thickness(2, 0, 0, 0), "Icons.Branch", false); } else { if (node.IsExpanded) - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open"); + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open", false); else - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder"); + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder", false); } } - private void CreateContent(Thickness margin, string iconKey) + private void CreateContent(Thickness margin, string iconKey, bool highlight) { var geo = this.FindResource(iconKey) as StreamGeometry; if (geo == null) return; - Content = new Path() + var path = new Path() { Width = 12, Height = 12, @@ -85,6 +83,11 @@ namespace SourceGit.Views Margin = margin, Data = geo, }; + + if (highlight) + path.Fill = Brushes.Green; + + Content = path; } } @@ -275,6 +278,9 @@ namespace SourceGit.Views rows.RemoveRange(idx + 1, removeCount); } + var repo = DataContext as ViewModels.Repository; + repo?.UpdateBranchNodeIsExpanded(node); + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); _disableSelectionChangingEvent = false; } @@ -310,6 +316,31 @@ namespace SourceGit.Views } } + private void OnNodePointerPressed(object sender, PointerPressedEventArgs e) + { + var p = e.GetCurrentPoint(this); + if (!p.Properties.IsLeftButtonPressed) + return; + + if (DataContext is not ViewModels.Repository repo) + return; + + if (sender is not Border { DataContext: ViewModels.BranchTreeNode node }) + return; + + if (node.Backend is not Models.Branch branch) + return; + + if (BranchesPresenter.SelectedItems is { Count: > 0 }) + { + var ctrl = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + if (e.KeyModifiers.HasFlag(ctrl) || e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + return; + } + + repo.NavigateToCommit(branch.Head); + } + private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e) { if (_disableSelectionChangingEvent) @@ -335,9 +366,6 @@ namespace SourceGit.Views if (selected == null || selected.Count == 0) return; - if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) - repo.NavigateToCommit(branch.Head); - var prev = null as ViewModels.BranchTreeNode; foreach (var row in Rows) { @@ -374,7 +402,7 @@ namespace SourceGit.Views if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) { var menu = repo.CreateContextMenuForRemote(remote); - this.OpenContextMenu(menu); + menu?.Open(this); return; } @@ -391,11 +419,23 @@ namespace SourceGit.Views var menu = branch.IsLocal ? repo.CreateContextMenuForLocalBranch(branch) : repo.CreateContextMenuForRemoteBranch(branch); - this.OpenContextMenu(menu); + menu?.Open(this); } else if (branches.Find(x => x.IsCurrent) == null) { var menu = new ContextMenu(); + + var mergeMulti = new MenuItem(); + mergeMulti.Header = App.Text("BranchCM.MergeMultiBranches", branches.Count); + mergeMulti.Icon = App.CreateMenuIcon("Icons.Merge"); + mergeMulti.Click += (_, ev) => + { + repo.MergeMultipleBranches(branches); + ev.Handled = true; + }; + menu.Items.Add(mergeMulti); + menu.Items.Add(new MenuItem() { Header = "-" }); + var deleteMulti = new MenuItem(); deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); @@ -405,10 +445,49 @@ namespace SourceGit.Views ev.Handled = true; }; menu.Items.Add(deleteMulti); - this.OpenContextMenu(menu); + + menu?.Open(this); } } + private void OnTreeKeyDown(object _, KeyEventArgs e) + { + if (e.Key is not (Key.Delete or Key.Back)) + return; + + var repo = DataContext as ViewModels.Repository; + if (repo?.Settings == null) + return; + + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) + { + repo.DeleteRemote(remote); + e.Handled = true; + return; + } + + var branches = new List(); + foreach (var item in selected) + { + if (item is ViewModels.BranchTreeNode node) + CollectBranchesInNode(branches, node); + } + + if (branches.Find(x => x.IsCurrent) != null) + return; + + if (branches.Count == 1) + repo.DeleteBranch(branches[0]); + else + repo.DeleteMultipleBranches(branches, branches[0].IsLocal); + + e.Handled = true; + } + private void OnDoubleTappedBranchNode(object sender, TappedEventArgs _) { if (sender is Grid { DataContext: ViewModels.BranchTreeNode node }) @@ -428,28 +507,6 @@ namespace SourceGit.Views } } - private void OnToggleFilterClicked(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo && - sender is ToggleButton toggle && - toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch } node) - { - bool filtered = toggle.IsChecked == true; - List filters = [branch.FullName]; - if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream)) - { - filters.Add(branch.Upstream); - - node.IsFiltered = filtered; - UpdateUpstreamFilterState(repo.RemoteBranchTrees, branch.Upstream, filtered); - } - - repo.UpdateFilters(filters, filtered); - } - - e.Handled = true; - } - private void MakeRows(List rows, List nodes, int depth) { foreach (var node in nodes) @@ -477,24 +534,6 @@ namespace SourceGit.Views CollectBranchesInNode(outs, sub); } - private bool UpdateUpstreamFilterState(List collection, string upstream, bool isFiltered) - { - foreach (var node in collection) - { - if (node.Backend is Models.Branch b && b.FullName == upstream) - { - node.IsFiltered = isFiltered; - return true; - } - - if (node.Backend is Models.Remote r && upstream.StartsWith($"refs/remotes/{r.Name}/", StringComparison.Ordinal)) - return UpdateUpstreamFilterState(node.Children, upstream, isFiltered); - } - - return false; - } - private bool _disableSelectionChangingEvent = false; } } - diff --git a/src/Views/CaptionButtonsMacOS.axaml b/src/Views/CaptionButtonsMacOS.axaml deleted file mode 100644 index 59c12e8d..00000000 --- a/src/Views/CaptionButtonsMacOS.axaml +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - #FFED6A5E - #FF69110A - #FFB24F46 - #FF2E0402 - #FFF4BF4F - #FF8F591D - #FFB78F3A - #FF522A0A - #FF61C554 - #FF296017 - #FF48943F - #FF102F07 - - - #FFED6A5E - #FF8C1A10 - #FFF09389 - #FF69120A - #FFF4BF4F - #FF8F591D - #FFFBEB74 - #FF705F1B - #FF61C554 - #FF296017 - #FF86F37F - #FF2C681A - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/CaptionButtonsMacOS.axaml.cs b/src/Views/CaptionButtonsMacOS.axaml.cs deleted file mode 100644 index 98bbb88f..00000000 --- a/src/Views/CaptionButtonsMacOS.axaml.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace SourceGit.Views -{ - public partial class CaptionButtonsMacOS : UserControl - { - public static readonly StyledProperty IsCloseButtonOnlyProperty = - AvaloniaProperty.Register(nameof(IsCloseButtonOnly)); - - public bool IsCloseButtonOnly - { - get => GetValue(IsCloseButtonOnlyProperty); - set => SetValue(IsCloseButtonOnlyProperty, value); - } - - public CaptionButtonsMacOS() - { - InitializeComponent(); - } - - private void MinimizeWindow(object _, RoutedEventArgs e) - { - var window = this.FindAncestorOfType(); - if (window != null) - window.WindowState = WindowState.Minimized; - - e.Handled = true; - } - - private void MaximizeOrRestoreWindow(object _, RoutedEventArgs e) - { - var window = this.FindAncestorOfType(); - if (window != null) - window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - - e.Handled = true; - } - - private void CloseWindow(object _, RoutedEventArgs e) - { - var window = this.FindAncestorOfType(); - window?.Close(); - e.Handled = true; - } - } -} diff --git a/src/Views/ChangeCollectionView.axaml b/src/Views/ChangeCollectionView.axaml index 6ce3d033..43af3a9a 100644 --- a/src/Views/ChangeCollectionView.axaml +++ b/src/Views/ChangeCollectionView.axaml @@ -20,26 +20,28 @@ - + - + - - + DoubleTapped="OnRowDoubleTapped" + DataContextChanged="OnRowDataContextChanged"> - - + + + + + + @@ -65,23 +75,27 @@ - - + - + + + + - - + - + + + + diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index a47988f0..6623a60b 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.VisualTree; namespace SourceGit.Views @@ -42,7 +44,7 @@ namespace SourceGit.Views } } - if (!e.Handled && e.Key != Key.Space) + if (!e.Handled && e.Key != Key.Space && e.Key != Key.Enter) base.OnKeyDown(e); } } @@ -85,6 +87,15 @@ namespace SourceGit.Views set => SetValue(ChangesProperty, value); } + public static readonly StyledProperty AutoSelectFirstChangeProperty = + AvaloniaProperty.Register(nameof(AutoSelectFirstChange)); + + public bool AutoSelectFirstChange + { + get => GetValue(AutoSelectFirstChangeProperty); + set => SetValue(AutoSelectFirstChangeProperty, value); + } + public static readonly StyledProperty> SelectedChangesProperty = AvaloniaProperty.Register>(nameof(SelectedChanges)); @@ -136,6 +147,7 @@ namespace SourceGit.Views removeCount++; } + tree.Rows.RemoveRange(idx + 1, removeCount); } } @@ -200,18 +212,47 @@ namespace SourceGit.Views return null; } + public void TakeFocus() + { + var container = this.FindDescendantOfType(); + if (container is { IsFocused: false }) + container.Focus(); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ViewModeProperty) - UpdateDataSource(false); - else if (change.Property == ChangesProperty) UpdateDataSource(true); + else if (change.Property == ChangesProperty) + UpdateDataSource(false); else if (change.Property == SelectedChangesProperty) UpdateSelection(); } + private void OnRowDataContextChanged(object sender, EventArgs e) + { + if (sender is not Control control) + return; + + if (control.DataContext is ViewModels.ChangeTreeNode node) + { + if (node.Change is { } c) + UpdateRowTips(control, c); + else + ToolTip.SetTip(control, node.FullPath); + } + else if (control.DataContext is Models.Change change) + { + UpdateRowTips(control, change); + } + else + { + ToolTip.SetTip(control, null); + } + } + private void OnRowDoubleTapped(object sender, TappedEventArgs e) { var grid = sender as Grid; @@ -292,9 +333,9 @@ namespace SourceGit.Views } } - private void UpdateDataSource(bool disableEvents) + private void UpdateDataSource(bool onlyViewModeChange) { - _disableSelectionChangingEvent = disableEvents; + _disableSelectionChangingEvent = !onlyViewModeChange; var changes = Changes; if (changes == null || changes.Count == 0) @@ -324,7 +365,19 @@ namespace SourceGit.Views MakeTreeRows(rows, tree.Tree); tree.Rows.AddRange(rows); - if (selected.Count > 0) + if (!onlyViewModeChange && AutoSelectFirstChange) + { + foreach (var row in tree.Rows) + { + if (row.Change != null) + { + tree.SelectedRows.Add(row); + SetCurrentValue(SelectedChangesProperty, [row.Change]); + break; + } + } + } + else if (selected.Count > 0) { var sets = new HashSet(); foreach (var c in selected) @@ -346,16 +399,34 @@ namespace SourceGit.Views { var grid = new ViewModels.ChangeCollectionAsGrid(); grid.Changes.AddRange(changes); - if (selected.Count > 0) + + if (!onlyViewModeChange && AutoSelectFirstChange) + { + grid.SelectedChanges.Add(changes[0]); + SetCurrentValue(SelectedChangesProperty, [changes[0]]); + } + else if (selected.Count > 0) + { grid.SelectedChanges.AddRange(selected); + } + Content = grid; } else { var list = new ViewModels.ChangeCollectionAsList(); list.Changes.AddRange(changes); - if (selected.Count > 0) + + if (!onlyViewModeChange && AutoSelectFirstChange) + { + list.SelectedChanges.Add(changes[0]); + SetCurrentValue(SelectedChangesProperty, [changes[0]]); + } + else if (selected.Count > 0) + { list.SelectedChanges.AddRange(selected); + } + Content = list; } @@ -419,6 +490,21 @@ namespace SourceGit.Views } } + private void UpdateRowTips(Control control, Models.Change change) + { + var tip = new TextBlock() { TextWrapping = TextWrapping.Wrap }; + tip.Inlines!.Add(new Run(change.Path)); + tip.Inlines!.Add(new Run(" • ") { Foreground = Brushes.Gray }); + tip.Inlines!.Add(new Run(IsUnstagedChange ? change.WorkTreeDesc : change.IndexDesc) { Foreground = Brushes.Gray }); + if (change.IsConflicted) + { + tip.Inlines!.Add(new Run(" • ") { Foreground = Brushes.Gray }); + tip.Inlines!.Add(new Run(change.ConflictDesc) { Foreground = Brushes.Gray }); + } + + ToolTip.SetTip(control, tip); + } + private bool _disableSelectionChangingEvent = false; } } diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs index 5d34be09..d66ac11d 100644 --- a/src/Views/ChangeStatusIcon.cs +++ b/src/Views/ChangeStatusIcon.cs @@ -9,6 +9,7 @@ namespace SourceGit.Views { public class ChangeStatusIcon : Control { + private static readonly string[] INDICATOR = ["?", "±", "T", "+", "−", "➜", "❏", "★", "!"]; private static readonly IBrush[] BACKGROUNDS = [ Brushes.Transparent, new LinearGradientBrush @@ -48,22 +49,14 @@ namespace SourceGit.Views EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), }, new LinearGradientBrush - { - GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) }, - StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), - EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), - }, - new LinearGradientBrush { GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) }, StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), }, + Brushes.OrangeRed, ]; - private static readonly string[] INDICATOR = ["?", "±", "T", "+", "−", "➜", "❏", "U", "★"]; - private static readonly string[] TIPS = ["Unknown", "Modified", "Type Changed", "Added", "Deleted", "Renamed", "Copied", "Unmerged", "Untracked"]; - public static readonly StyledProperty IsUnstagedChangeProperty = AvaloniaProperty.Register(nameof(IsUnstagedChange)); @@ -93,16 +86,8 @@ namespace SourceGit.Views string indicator; if (IsUnstagedChange) { - if (Change.IsConflit) - { - background = Brushes.OrangeRed; - indicator = "!"; - } - else - { - background = BACKGROUNDS[(int)Change.WorkTree]; - indicator = INDICATOR[(int)Change.WorkTree]; - } + background = BACKGROUNDS[(int)Change.WorkTree]; + indicator = INDICATOR[(int)Change.WorkTree]; } else { @@ -129,22 +114,7 @@ namespace SourceGit.Views base.OnPropertyChanged(change); if (change.Property == IsUnstagedChangeProperty || change.Property == ChangeProperty) - { - var isUnstaged = IsUnstagedChange; - var c = Change; - if (c == null) - { - ToolTip.SetTip(this, null); - return; - } - - if (isUnstaged) - ToolTip.SetTip(this, c.IsConflit ? "Conflict" : TIPS[(int)c.WorkTree]); - else - ToolTip.SetTip(this, TIPS[(int)c.Index]); - InvalidateVisual(); - } } } } diff --git a/src/Views/ChangeViewModeSwitcher.axaml b/src/Views/ChangeViewModeSwitcher.axaml index 4ded60c7..911fb41d 100644 --- a/src/Views/ChangeViewModeSwitcher.axaml +++ b/src/Views/ChangeViewModeSwitcher.axaml @@ -6,38 +6,38 @@ xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.ChangeViewModeSwitcher" - x:DataType="v:ChangeViewModeSwitcher"> + x:Name="ThisControl"> diff --git a/src/Views/ChangeViewModeSwitcher.axaml.cs b/src/Views/ChangeViewModeSwitcher.axaml.cs index 0cb2c4a9..ed306619 100644 --- a/src/Views/ChangeViewModeSwitcher.axaml.cs +++ b/src/Views/ChangeViewModeSwitcher.axaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Interactivity; namespace SourceGit.Views { @@ -16,13 +17,25 @@ namespace SourceGit.Views public ChangeViewModeSwitcher() { - DataContext = this; InitializeComponent(); } - public void SwitchMode(object param) + private void SwitchToList(object sender, RoutedEventArgs e) { - ViewMode = (Models.ChangeViewMode)param; + ViewMode = Models.ChangeViewMode.List; + e.Handled = true; + } + + private void SwitchToGrid(object sender, RoutedEventArgs e) + { + ViewMode = Models.ChangeViewMode.Grid; + e.Handled = true; + } + + private void SwitchToTree(object sender, RoutedEventArgs e) + { + ViewMode = Models.ChangeViewMode.Tree; + e.Handled = true; } } } diff --git a/src/Views/Checkout.axaml b/src/Views/Checkout.axaml index eaf2e79e..42b9cec5 100644 --- a/src/Views/Checkout.axaml +++ b/src/Views/Checkout.axaml @@ -17,8 +17,9 @@ + - + - - - - - - - + Content="{DynamicResource Text.Checkout.LocalChanges.StashAndReply}" + IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/> + + + diff --git a/src/Views/CheckoutAndFastForward.axaml b/src/Views/CheckoutAndFastForward.axaml new file mode 100644 index 00000000..40ca3e14 --- /dev/null +++ b/src/Views/CheckoutAndFastForward.axaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CheckoutAndFastForward.axaml.cs b/src/Views/CheckoutAndFastForward.axaml.cs new file mode 100644 index 00000000..c54f5a1f --- /dev/null +++ b/src/Views/CheckoutAndFastForward.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class CheckoutAndFastForward : UserControl + { + public CheckoutAndFastForward() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml index 37021565..11b4b5d0 100644 --- a/src/Views/CheckoutCommit.axaml +++ b/src/Views/CheckoutCommit.axaml @@ -12,13 +12,13 @@ Classes="bold" Text="{DynamicResource Text.Checkout.Commit}" /> - + - + @@ -30,16 +30,23 @@ + Margin="0,0,8,0" + IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/> + GroupName="LocalChanges"/> - - + + + - + - + @@ -73,15 +73,14 @@ - - - + diff --git a/src/Views/ChromelessWindow.cs b/src/Views/ChromelessWindow.cs index a9b9f259..1662bcd7 100644 --- a/src/Views/ChromelessWindow.cs +++ b/src/Views/ChromelessWindow.cs @@ -3,7 +3,6 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Platform; namespace SourceGit.Views { @@ -11,35 +10,33 @@ namespace SourceGit.Views { public bool UseSystemWindowFrame { - get => OperatingSystem.IsLinux() && ViewModels.Preference.Instance.UseSystemWindowFrame; + get => Native.OS.UseSystemWindowFrame; } protected override Type StyleKeyOverride => typeof(Window); public ChromelessWindow() { - if (OperatingSystem.IsLinux()) - { - if (UseSystemWindowFrame) - { - ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; - ExtendClientAreaToDecorationsHint = false; - } - else - { - ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; - ExtendClientAreaToDecorationsHint = true; - Classes.Add("custom_window_frame"); - } - } - else - { - ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; - ExtendClientAreaToDecorationsHint = true; + Focusable = true; + Native.OS.SetupForWindow(this); + } - if (OperatingSystem.IsWindows()) - Classes.Add("fix_maximized_padding"); - } + public void BeginMoveWindow(object _, PointerPressedEventArgs e) + { + if (e.ClickCount == 1) + BeginMoveDrag(e); + + e.Handled = true; + } + + public void MaximizeOrRestoreWindow(object _, TappedEventArgs e) + { + if (WindowState == WindowState.Maximized) + WindowState = WindowState.Normal; + else + WindowState = WindowState.Maximized; + + e.Handled = true; } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) diff --git a/src/Views/Clone.axaml b/src/Views/Clone.axaml index 25c46a00..8c7c9faf 100644 --- a/src/Views/Clone.axaml +++ b/src/Views/Clone.axaml @@ -10,15 +10,15 @@ - - + - - - - + + diff --git a/src/Views/Clone.axaml.cs b/src/Views/Clone.axaml.cs index 1c299211..9316721a 100644 --- a/src/Views/Clone.axaml.cs +++ b/src/Views/Clone.axaml.cs @@ -23,7 +23,11 @@ namespace SourceGit.Views { var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options); if (selected.Count == 1) - TxtParentFolder.Text = selected[0].Path.LocalPath; + { + var folder = selected[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder?.Path.ToString(); + TxtParentFolder.Text = folderPath; + } } catch (Exception exception) { diff --git a/src/Views/ColorPicker.cs b/src/Views/ColorPicker.cs index 8a0ecb55..5090af86 100644 --- a/src/Views/ColorPicker.cs +++ b/src/Views/ColorPicker.cs @@ -19,7 +19,7 @@ namespace SourceGit.Views } // Values are copied from Avalonia: src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs - private static readonly Color[,] COLOR_TABLE = new Color[,] + private static readonly Color[,] COLOR_TABLE = new[,] { { Color.FromArgb(255, 255, 67, 67), /* #FF4343 */ @@ -110,7 +110,7 @@ namespace SourceGit.Views context.DrawLine(pen, new Point(j * 32, 0), new Point(j * 32, 192)); // Selected - if (_hightlightedTableRect is { } rect) + if (_highlightedTableRect is { } rect) context.DrawRectangle(new Pen(Brushes.White, 2), rect); } @@ -143,7 +143,7 @@ namespace SourceGit.Views protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); - _hightlightedTableRect = null; + _highlightedTableRect = null; } protected override Size MeasureOverride(Size availableSize) @@ -161,9 +161,9 @@ namespace SourceGit.Views var col = (int)Math.Floor(p.X / 32.0); var row = (int)Math.Floor(p.Y / 32.0); var rect = new Rect(col * 32 + 2, row * 32 + 2, 28, 28); - if (!rect.Equals(_hightlightedTableRect)) + if (!rect.Equals(_highlightedTableRect)) { - _hightlightedTableRect = rect; + _highlightedTableRect = rect; SetCurrentValue(ValueProperty, COLOR_TABLE[row, col].ToUInt32()); } @@ -172,32 +172,32 @@ namespace SourceGit.Views if (_darkestRect.Rect.Contains(p)) { - _hightlightedTableRect = null; + _highlightedTableRect = null; SetCurrentValue(ValueProperty, _darkestColor.ToUInt32()); } else if (_darkerRect.Contains(p)) { - _hightlightedTableRect = null; + _highlightedTableRect = null; SetCurrentValue(ValueProperty, _darkerColor.ToUInt32()); } else if (_darkRect.Contains(p)) { - _hightlightedTableRect = null; + _highlightedTableRect = null; SetCurrentValue(ValueProperty, _darkColor.ToUInt32()); } else if (_lightRect.Contains(p)) { - _hightlightedTableRect = null; + _highlightedTableRect = null; SetCurrentValue(ValueProperty, _lightColor.ToUInt32()); } else if (_lighterRect.Contains(p)) { - _hightlightedTableRect = null; + _highlightedTableRect = null; SetCurrentValue(ValueProperty, _lighterColor.ToUInt32()); } else if (_lightestRect.Rect.Contains(p)) { - _hightlightedTableRect = null; + _highlightedTableRect = null; SetCurrentValue(ValueProperty, _lightestColor.ToUInt32()); } } @@ -234,7 +234,7 @@ namespace SourceGit.Views private Rect _lightRect = new Rect(160, 200, 32, 32); private Rect _lighterRect = new Rect(192, 200, 32, 32); private RoundedRect _lightestRect = new RoundedRect(new Rect(224, 200, 32, 32), new CornerRadius(0, 4, 4, 0)); - private Rect? _hightlightedTableRect = null; + private Rect? _highlightedTableRect = null; private Color _darkestColor; private Color _darkerColor; diff --git a/src/Views/CommandLogContentPresenter.cs b/src/Views/CommandLogContentPresenter.cs new file mode 100644 index 00000000..a2499f8d --- /dev/null +++ b/src/Views/CommandLogContentPresenter.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Media; + +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using AvaloniaEdit.Rendering; +using AvaloniaEdit.TextMate; + +namespace SourceGit.Views +{ + public class CommandLogContentPresenter : TextEditor + { + public class LineStyleTransformer : DocumentColorizingTransformer + { + protected override void ColorizeLine(DocumentLine line) + { + var content = CurrentContext.Document.GetText(line); + if (content.StartsWith("$ git ", StringComparison.Ordinal)) + { + ChangeLinePart(line.Offset, line.Offset + 1, v => + { + v.TextRunProperties.SetForegroundBrush(Brushes.Orange); + }); + + ChangeLinePart(line.Offset + 2, line.EndOffset, v => + { + var old = v.TextRunProperties.Typeface; + v.TextRunProperties.SetTypeface(new Typeface(old.FontFamily, old.Style, FontWeight.Bold)); + }); + } + else if (content.StartsWith("remote: ", StringComparison.Ordinal)) + { + ChangeLinePart(line.Offset, line.Offset + 7, v => + { + v.TextRunProperties.SetForegroundBrush(Brushes.SeaGreen); + }); + } + else + { + foreach (var err in _errors) + { + var idx = content.IndexOf(err, StringComparison.Ordinal); + if (idx >= 0) + { + ChangeLinePart(line.Offset + idx, line.Offset + err.Length + 1, v => + { + v.TextRunProperties.SetForegroundBrush(Brushes.Red); + }); + } + } + } + } + + private readonly List _errors = ["! [rejected]", "! [remote rejected]"]; + } + + public static readonly StyledProperty LogProperty = + AvaloniaProperty.Register(nameof(Log)); + + public ViewModels.CommandLog Log + { + get => GetValue(LogProperty); + set => SetValue(LogProperty, value); + } + + public static readonly StyledProperty PureTextProperty = + AvaloniaProperty.Register(nameof(PureText)); + + public string PureText + { + get => GetValue(PureTextProperty); + set => SetValue(PureTextProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextEditor); + + public CommandLogContentPresenter() : base(new TextArea(), new TextDocument()) + { + IsReadOnly = true; + ShowLineNumbers = false; + WordWrap = false; + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + + TextArea.TextView.Margin = new Thickness(4, 0); + TextArea.TextView.Options.EnableHyperlinks = false; + TextArea.TextView.Options.EnableEmailHyperlinks = false; + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (_textMate == null) + { + _textMate = Models.TextMateHelper.CreateForEditor(this); + Models.TextMateHelper.SetGrammarByFileName(_textMate, "Log.log"); + TextArea.TextView.LineTransformers.Add(new LineStyleTransformer()); + } + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + + if (_textMate != null) + { + _textMate.Dispose(); + _textMate = null; + } + + GC.Collect(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == LogProperty) + { + if (change.NewValue is ViewModels.CommandLog log) + { + Text = log.Content; + log.Register(OnLogLineReceived); + } + else + { + Text = string.Empty; + } + } + else if (change.Property == PureTextProperty) + { + if (!string.IsNullOrEmpty(PureText)) + Text = PureText; + } + } + + private void OnLogLineReceived(string newline) + { + AppendText("\n"); + AppendText(newline); + } + + private TextMate.Installation _textMate = null; + } +} diff --git a/src/Views/CommandLogTime.cs b/src/Views/CommandLogTime.cs new file mode 100644 index 00000000..665e6e7b --- /dev/null +++ b/src/Views/CommandLogTime.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace SourceGit.Views +{ + public class CommandLogTime : TextBlock + { + public static readonly StyledProperty LogProperty = + AvaloniaProperty.Register(nameof(Log), null); + + public ViewModels.CommandLog Log + { + get => GetValue(LogProperty); + set => SetValue(LogProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextBlock); + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + StopTimer(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == LogProperty) + { + StopTimer(); + + if (change.NewValue is ViewModels.CommandLog log) + SetupCommandLog(log); + else + Text = string.Empty; + } + } + + private void SetupCommandLog(ViewModels.CommandLog log) + { + Text = GetDisplayText(log); + if (log.IsComplete) + return; + + _refreshTimer = new Timer(_ => + { + Dispatcher.UIThread.Invoke(() => + { + Text = GetDisplayText(log); + if (log.IsComplete) + StopTimer(); + }); + }, null, 0, 100); + } + + private void StopTimer() + { + if (_refreshTimer is not null) + { + _refreshTimer.Dispose(); + _refreshTimer = null; + } + } + + private string GetDisplayText(ViewModels.CommandLog log) + { + var endTime = log.IsComplete ? log.EndTime : DateTime.Now; + var duration = (endTime - log.StartTime).ToString(@"hh\:mm\:ss\.fff"); + return $"{log.StartTime:T} ({duration})"; + } + + private Timer _refreshTimer = null; + } +} diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index 623332c4..2d3b6ff4 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -19,14 +19,14 @@ - - - - - + + + + + @@ -35,14 +35,14 @@ - - - - - + + + + + @@ -51,13 +51,13 @@ - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -90,31 +111,119 @@ TextDecorations="Underline" Cursor="Hand" Margin="0,0,16,0" - PointerPressed="OnParentSHAPressed"/> + PointerEntered="OnSHAPointerEntered" + PointerPressed="OnSHAPressed" + ToolTip.ShowDelay="0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + - + - - + + TextWrapping="Wrap"> + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitBaseInfo.axaml.cs b/src/Views/CommitBaseInfo.axaml.cs index ad137147..ac9b53cc 100644 --- a/src/Views/CommitBaseInfo.axaml.cs +++ b/src/Views/CommitBaseInfo.axaml.cs @@ -1,20 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + using Avalonia; -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Threading; namespace SourceGit.Views { public partial class CommitBaseInfo : UserControl { - public static readonly StyledProperty MessageProperty = - AvaloniaProperty.Register(nameof(Message), string.Empty); + public static readonly StyledProperty FullMessageProperty = + AvaloniaProperty.Register(nameof(FullMessage)); - public string Message + public Models.CommitFullMessage FullMessage { - get => GetValue(MessageProperty); - set => SetValue(MessageProperty, value); + get => GetValue(FullMessageProperty); + set => SetValue(FullMessageProperty, value); + } + + public static readonly StyledProperty SignInfoProperty = + AvaloniaProperty.Register(nameof(SignInfo)); + + public Models.CommitSignInfo SignInfo + { + get => GetValue(SignInfoProperty); + set => SetValue(SignInfoProperty, value); } public static readonly StyledProperty SupportsContainsInProperty = @@ -26,22 +38,22 @@ namespace SourceGit.Views set => SetValue(SupportsContainsInProperty, value); } - public static readonly StyledProperty> WebLinksProperty = - AvaloniaProperty.Register>(nameof(WebLinks)); + public static readonly StyledProperty> WebLinksProperty = + AvaloniaProperty.Register>(nameof(WebLinks)); - public AvaloniaList WebLinks + public List WebLinks { get => GetValue(WebLinksProperty); set => SetValue(WebLinksProperty, value); } - public static readonly StyledProperty> IssueTrackerRulesProperty = - AvaloniaProperty.Register>(nameof(IssueTrackerRules)); + public static readonly StyledProperty> ChildrenProperty = + AvaloniaProperty.Register>(nameof(Children)); - public AvaloniaList IssueTrackerRules + public List Children { - get => GetValue(IssueTrackerRulesProperty); - set => SetValue(IssueTrackerRulesProperty, value); + get => GetValue(ChildrenProperty); + set => SetValue(ChildrenProperty, value); } public CommitBaseInfo() @@ -59,7 +71,7 @@ namespace SourceGit.Views private void OnOpenWebLink(object sender, RoutedEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail) + if (DataContext is ViewModels.CommitDetail detail && sender is Control control) { var links = WebLinks; if (links.Count > 1) @@ -79,7 +91,7 @@ namespace SourceGit.Views menu.Items.Add(item); } - (sender as Control)?.OpenContextMenu(menu); + menu.Open(control); } else if (links.Count == 1) { @@ -104,9 +116,36 @@ namespace SourceGit.Views e.Handled = true; } - private void OnParentSHAPressed(object sender, PointerPressedEventArgs e) + private void OnSHAPointerEntered(object sender, PointerEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail && sender is Control { DataContext: string sha }) + if (DataContext is ViewModels.CommitDetail detail && sender is Control { DataContext: string sha } ctl) + { + var tooltip = ToolTip.GetTip(ctl); + if (tooltip is Models.Commit commit && commit.SHA == sha) + return; + + Task.Run(() => + { + var c = detail.GetParent(sha); + if (c == null) + return; + + Dispatcher.UIThread.Invoke(() => + { + if (ctl.IsEffectivelyVisible && ctl.DataContext is string newSHA && newSHA == sha) + ToolTip.SetTip(ctl, c); + }); + }); + } + + e.Handled = true; + } + + private void OnSHAPressed(object sender, PointerPressedEventArgs e) + { + var point = e.GetCurrentPoint(this); + + if (point.Properties.IsLeftButtonPressed && DataContext is ViewModels.CommitDetail detail && sender is Control { DataContext: string sha }) { detail.NavigateTo(sha); } diff --git a/src/Views/CommitChanges.axaml b/src/Views/CommitChanges.axaml index 1c7b34bd..4dafee37 100644 --- a/src/Views/CommitChanges.axaml +++ b/src/Views/CommitChanges.axaml @@ -4,22 +4,23 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CommitChanges" x:DataType="vm:CommitDetail"> - + - + + ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=CommitChangeViewMode, Mode=TwoWay}"/> + + + + + + + + - + Children="{Binding Children}"/> - @@ -40,30 +41,38 @@ - + - + - + + + + + + + + - + - diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index 999d1c07..f0599c66 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -26,7 +26,7 @@ namespace SourceGit.Views if (DataContext is ViewModels.CommitDetail detail && sender is Grid grid && grid.DataContext is Models.Change change) { var menu = detail.CreateChangeContextMenu(change); - grid.OpenContextMenu(menu); + menu?.Open(grid); } e.Handled = true; diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs new file mode 100644 index 00000000..5db39300 --- /dev/null +++ b/src/Views/CommitGraph.cs @@ -0,0 +1,245 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class CommitGraph : Control + { + public static readonly StyledProperty GraphProperty = + AvaloniaProperty.Register(nameof(Graph)); + + public Models.CommitGraph Graph + { + get => GetValue(GraphProperty); + set => SetValue(GraphProperty, value); + } + + public static readonly StyledProperty DotBrushProperty = + AvaloniaProperty.Register(nameof(DotBrush), Brushes.Transparent); + + public IBrush DotBrush + { + get => GetValue(DotBrushProperty); + set => SetValue(DotBrushProperty, value); + } + + public static readonly StyledProperty OnlyHighlightCurrentBranchProperty = + AvaloniaProperty.Register(nameof(OnlyHighlightCurrentBranch), true); + + public bool OnlyHighlightCurrentBranch + { + get => GetValue(OnlyHighlightCurrentBranchProperty); + set => SetValue(OnlyHighlightCurrentBranchProperty, value); + } + + static CommitGraph() + { + AffectsRender(GraphProperty, DotBrushProperty, OnlyHighlightCurrentBranchProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var graph = Graph; + if (graph == null) + return; + + var histories = this.FindAncestorOfType(); + if (histories == null) + return; + + var list = histories.CommitListContainer; + if (list == null) + return; + + var container = list.ItemsPanelRoot as VirtualizingStackPanel; + if (container == null) + return; + + var item = list.ContainerFromIndex(container.FirstRealizedIndex); + if (item == null) + return; + + var width = histories.CommitListHeader.ColumnDefinitions[0].ActualWidth; + var height = Bounds.Height; + var rowHeight = item.Bounds.Height; + var startY = container.FirstRealizedIndex * rowHeight - item.TranslatePoint(new Point(0, 0), list).Value!.Y; + var endY = startY + height + 28; + + using (context.PushClip(new Rect(0, 0, width, height))) + using (context.PushTransform(Matrix.CreateTranslation(0, -startY))) + { + DrawCurves(context, graph, startY, endY, rowHeight); + DrawAnchors(context, graph, startY, endY, rowHeight); + } + } + + private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom, double rowHeight) + { + var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness); + var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; + + if (onlyHighlightCurrentBranch) + { + foreach (var link in graph.Links) + { + if (link.IsMerged) + continue; + + var startY = link.Start.Y * rowHeight; + var endY = link.End.Y * rowHeight; + + if (endY < top) + continue; + if (startY > bottom) + break; + + var geo = new StreamGeometry(); + using (var ctx = geo.Open()) + { + ctx.BeginFigure(new Point(link.Start.X, startY), false); + ctx.QuadraticBezierTo(new Point(link.Control.X, link.Control.Y * rowHeight), new Point(link.End.X, endY)); + } + + context.DrawGeometry(null, grayedPen, geo); + } + } + + foreach (var line in graph.Paths) + { + var last = new Point(line.Points[0].X, line.Points[0].Y * rowHeight); + var size = line.Points.Count; + var endY = line.Points[size - 1].Y * rowHeight; + + if (endY < top) + continue; + if (last.Y > bottom) + break; + + var geo = new StreamGeometry(); + var pen = Models.CommitGraph.Pens[line.Color]; + + using (var ctx = geo.Open()) + { + var started = false; + var ended = false; + for (int i = 1; i < size; i++) + { + var cur = new Point(line.Points[i].X, line.Points[i].Y * rowHeight); + if (cur.Y < top) + { + last = cur; + continue; + } + + if (!started) + { + ctx.BeginFigure(last, false); + started = true; + } + + if (cur.Y > bottom) + { + cur = new Point(cur.X, bottom); + ended = true; + } + + if (cur.X > last.X) + { + ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur); + } + else if (cur.X < last.X) + { + if (i < size - 1) + { + var midY = (last.Y + cur.Y) / 2; + ctx.CubicBezierTo(new Point(last.X, midY + 4), new Point(cur.X, midY - 4), cur); + } + else + { + ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur); + } + } + else + { + ctx.LineTo(cur); + } + + if (ended) + break; + last = cur; + } + } + + if (!line.IsMerged && onlyHighlightCurrentBranch) + context.DrawGeometry(null, grayedPen, geo); + else + context.DrawGeometry(null, pen, geo); + } + + foreach (var link in graph.Links) + { + if (onlyHighlightCurrentBranch && !link.IsMerged) + continue; + + var startY = link.Start.Y * rowHeight; + var endY = link.End.Y * rowHeight; + + if (endY < top) + continue; + if (startY > bottom) + break; + + var geo = new StreamGeometry(); + using (var ctx = geo.Open()) + { + ctx.BeginFigure(new Point(link.Start.X, startY), false); + ctx.QuadraticBezierTo(new Point(link.Control.X, link.Control.Y * rowHeight), new Point(link.End.X, endY)); + } + + context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo); + } + } + + private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, double top, double bottom, double rowHeight) + { + var dotFill = DotBrush; + var dotFillPen = new Pen(dotFill, 2); + var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness); + var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; + + foreach (var dot in graph.Dots) + { + var center = new Point(dot.Center.X, dot.Center.Y * rowHeight); + + if (center.Y < top) + continue; + if (center.Y > bottom) + break; + + var pen = Models.CommitGraph.Pens[dot.Color]; + if (!dot.IsMerged && onlyHighlightCurrentBranch) + pen = grayedPen; + + switch (dot.Type) + { + case Models.CommitGraph.DotType.Head: + context.DrawEllipse(dotFill, pen, center, 6, 6); + context.DrawEllipse(pen.Brush, null, center, 3, 3); + break; + case Models.CommitGraph.DotType.Merge: + context.DrawEllipse(pen.Brush, null, center, 6, 6); + context.DrawLine(dotFillPen, new Point(center.X, center.Y - 3), new Point(center.X, center.Y + 3)); + context.DrawLine(dotFillPen, new Point(center.X - 3, center.Y), new Point(center.X + 3, center.Y)); + break; + default: + context.DrawEllipse(dotFill, pen, center, 3, 3); + break; + } + } + } + } +} diff --git a/src/Views/CommitMessagePresenter.cs b/src/Views/CommitMessagePresenter.cs index 55e1dfb1..61119991 100644 --- a/src/Views/CommitMessagePresenter.cs +++ b/src/Views/CommitMessagePresenter.cs @@ -1,37 +1,25 @@ using System; using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Threading.Tasks; using Avalonia; -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Input; +using Avalonia.Threading; using Avalonia.VisualTree; namespace SourceGit.Views { - public partial class CommitMessagePresenter : SelectableTextBlock + public class CommitMessagePresenter : SelectableTextBlock { - [GeneratedRegex(@"\b([0-9a-fA-F]{8,40})\b")] - private static partial Regex REG_SHA_FORMAT(); + public static readonly StyledProperty FullMessageProperty = + AvaloniaProperty.Register(nameof(FullMessage)); - public static readonly StyledProperty MessageProperty = - AvaloniaProperty.Register(nameof(Message)); - - public string Message + public Models.CommitFullMessage FullMessage { - get => GetValue(MessageProperty); - set => SetValue(MessageProperty, value); - } - - public static readonly StyledProperty> IssueTrackerRulesProperty = - AvaloniaProperty.Register>(nameof(IssueTrackerRules)); - - public AvaloniaList IssueTrackerRules - { - get => GetValue(IssueTrackerRulesProperty); - set => SetValue(IssueTrackerRulesProperty, value); + get => GetValue(FullMessageProperty); + set => SetValue(FullMessageProperty, value); } protected override Type StyleKeyOverride => typeof(SelectableTextBlock); @@ -40,67 +28,37 @@ namespace SourceGit.Views { base.OnPropertyChanged(change); - if (change.Property == MessageProperty || change.Property == IssueTrackerRulesProperty) + if (change.Property == FullMessageProperty) { Inlines!.Clear(); - _matches = null; + _inlineCommits.Clear(); + _lastHover = null; ClearHoveredIssueLink(); - var message = Message; + var message = FullMessage?.Message; if (string.IsNullOrEmpty(message)) return; - var matches = new List(); - if (IssueTrackerRules is { Count: > 0 } rules) - { - foreach (var rule in rules) - rule.Matches(matches, message); - } - - var shas = REG_SHA_FORMAT().Matches(message); - for (int i = 0; i < shas.Count; i++) - { - var sha = shas[i]; - if (!sha.Success) - continue; - - var start = sha.Index; - var len = sha.Length; - var intersect = false; - foreach (var match in matches) - { - if (match.Intersect(start, len)) - { - intersect = true; - break; - } - } - - if (!intersect) - matches.Add(new Models.Hyperlink(start, len, sha.Groups[1].Value, true)); - } - - if (matches.Count == 0) + var links = FullMessage?.Inlines; + if (links == null || links.Count == 0) { Inlines.Add(new Run(message)); return; } - matches.Sort((l, r) => l.Start - r.Start); - _matches = matches; - var inlines = new List(); var pos = 0; - foreach (var match in matches) + for (var i = 0; i < links.Count; i++) { - if (match.Start > pos) - inlines.Add(new Run(message.Substring(pos, match.Start - pos))); + var link = links[i]; + if (link.Start > pos) + inlines.Add(new Run(message.Substring(pos, link.Start - pos))); - var link = new Run(message.Substring(match.Start, match.Length)); - link.Classes.Add(match.IsCommitSHA ? "commit_link" : "issue_link"); - inlines.Add(link); + var run = new Run(message.Substring(link.Start, link.Length)); + run.Classes.Add(link.Type == Models.InlineElementType.CommitSHA ? "commit_link" : "issue_link"); + inlines.Add(run); - pos = match.Start + match.Length; + pos = link.Start + link.Length; } if (pos < message.Length) @@ -114,7 +72,7 @@ namespace SourceGit.Views { base.OnPointerMoved(e); - if (e.Pointer.Captured == this) + if (Equals(e.Pointer.Captured, this)) { var relativeSelfY = e.GetPosition(this).Y; if (relativeSelfY <= 0 || relativeSelfY > Bounds.Height) @@ -130,7 +88,7 @@ namespace SourceGit.Views scrollViewer.LineDown(); } } - else if (_matches != null) + else if (FullMessage is { Inlines: { Count: > 0 } links }) { var point = e.GetPosition(this) - new Point(Padding.Left, Padding.Top); var x = Math.Min(Math.Max(point.X, 0), Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)); @@ -138,51 +96,138 @@ namespace SourceGit.Views point = new Point(x, y); var pos = TextLayout.HitTestPoint(point).TextPosition; - foreach (var match in _matches) - { - if (!match.Intersect(pos, 1)) - continue; - - if (match == _lastHover) - return; - - SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); - - _lastHover = match; - if (!_lastHover.IsCommitSHA) - { - ToolTip.SetTip(this, match.Link); - ToolTip.SetIsOpen(this, true); - } - - return; - } - - ClearHoveredIssueLink(); + if (links.Intersect(pos, 1) is { } link) + SetHoveredIssueLink(link); + else + ClearHoveredIssueLink(); } } protected override void OnPointerPressed(PointerPressedEventArgs e) { + var point = e.GetCurrentPoint(this); + if (_lastHover != null) { + var link = _lastHover.Link; e.Pointer.Capture(null); - if (_lastHover.IsCommitSHA) + if (_lastHover.Type == Models.InlineElementType.CommitSHA) { var parentView = this.FindAncestorOfType(); if (parentView is { DataContext: ViewModels.CommitDetail detail }) - detail.NavigateTo(_lastHover.Link); + { + if (point.Properties.IsLeftButtonPressed) + { + detail.NavigateTo(_lastHover.Link); + } + else if (point.Properties.IsRightButtonPressed) + { + var open = new MenuItem(); + open.Header = App.Text("SHALinkCM.NavigateTo"); + open.Icon = App.CreateMenuIcon("Icons.Commit"); + open.Click += (_, ev) => + { + detail.NavigateTo(link); + ev.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = App.Text("SHALinkCM.CopySHA"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += (_, ev) => + { + App.CopyText(link); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(open); + menu.Items.Add(copy); + menu.Open(this); + } + } } else { - Native.OS.OpenBrowser(_lastHover.Link); + if (point.Properties.IsLeftButtonPressed) + { + Native.OS.OpenBrowser(link); + } + else if (point.Properties.IsRightButtonPressed) + { + var open = new MenuItem(); + open.Header = App.Text("IssueLinkCM.OpenInBrowser"); + open.Icon = App.CreateMenuIcon("Icons.OpenWith"); + open.Click += (_, ev) => + { + Native.OS.OpenBrowser(link); + ev.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = App.Text("IssueLinkCM.CopyLink"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += (_, ev) => + { + App.CopyText(link); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(open); + menu.Items.Add(copy); + menu.Open(this); + } } e.Handled = true; return; } + if (point.Properties.IsLeftButtonPressed && e.ClickCount == 3) + { + var text = Inlines?.Text; + if (string.IsNullOrEmpty(text)) + { + e.Handled = true; + return; + } + + var position = e.GetPosition(this) - new Point(Padding.Left, Padding.Top); + var x = Math.Min(Math.Max(position.X, 0), Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)); + var y = Math.Min(Math.Max(position.Y, 0), Math.Max(TextLayout.Height, 0)); + position = new Point(x, y); + + var textPos = TextLayout.HitTestPoint(position).TextPosition; + var lineStart = 0; + var lineEnd = text.IndexOf('\n', lineStart); + if (lineEnd <= 0) + { + lineEnd = text.Length; + } + else + { + while (lineEnd < textPos) + { + lineStart = lineEnd + 1; + lineEnd = text.IndexOf('\n', lineStart); + if (lineEnd == -1) + { + lineEnd = text.Length; + break; + } + } + } + + SetCurrentValue(SelectionStartProperty, lineStart); + SetCurrentValue(SelectionEndProperty, lineEnd); + + e.Pointer.Capture(this); + e.Handled = true; + return; + } + base.OnPointerPressed(e); } @@ -192,6 +237,58 @@ namespace SourceGit.Views ClearHoveredIssueLink(); } + private void ProcessHoverCommitLink(Models.InlineElement link) + { + var sha = link.Link; + + // If we have already queried this SHA, just use it. + if (_inlineCommits.TryGetValue(sha, out var exist)) + { + ToolTip.SetTip(this, exist); + return; + } + + var parentView = this.FindAncestorOfType(); + if (parentView is { DataContext: ViewModels.CommitDetail detail }) + { + // Record the SHA of current viewing commit in the CommitDetail panel to determine if it is changed after + // asynchronous queries. + var lastDetailCommit = detail.Commit.SHA; + Task.Run(() => + { + var c = detail.GetParent(sha); + Dispatcher.UIThread.Invoke(() => + { + // Make sure the DataContext of CommitBaseInfo is not changed. + var currentParent = this.FindAncestorOfType(); + if (currentParent is { DataContext: ViewModels.CommitDetail currentDetail } && + currentDetail.Commit.SHA == lastDetailCommit) + { + _inlineCommits.TryAdd(sha, c); + + // Make sure user still hovers the target SHA. + if (_lastHover == link && c != null) + ToolTip.SetTip(this, c); + } + }); + }); + } + } + + private void SetHoveredIssueLink(Models.InlineElement link) + { + if (link == _lastHover) + return; + + SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); + + _lastHover = link; + if (link.Type == Models.InlineElementType.Link) + ToolTip.SetTip(this, link.Link); + else + ProcessHoverCommitLink(link); + } + private void ClearHoveredIssueLink() { if (_lastHover != null) @@ -202,7 +299,7 @@ namespace SourceGit.Views } } - private List _matches = null; - private Models.Hyperlink _lastHover = null; + private Models.InlineElement _lastHover = null; + private Dictionary _inlineCommits = new(); } } diff --git a/src/Views/CommitMessageTextBox.axaml b/src/Views/CommitMessageTextBox.axaml index 88862a8a..73c3a193 100644 --- a/src/Views/CommitMessageTextBox.axaml +++ b/src/Views/CommitMessageTextBox.axaml @@ -12,43 +12,23 @@ BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" CornerRadius="4"> - - - + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/src/Views/CommitMessageTextBox.axaml.cs b/src/Views/CommitMessageTextBox.axaml.cs index 6eec0379..83d6f900 100644 --- a/src/Views/CommitMessageTextBox.axaml.cs +++ b/src/Views/CommitMessageTextBox.axaml.cs @@ -56,6 +56,9 @@ namespace SourceGit.Views FromEditor, } + public static readonly StyledProperty ShowAdvancedOptionsProperty = + AvaloniaProperty.Register(nameof(ShowAdvancedOptions), false); + public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text), string.Empty); @@ -65,6 +68,12 @@ namespace SourceGit.Views public static readonly StyledProperty DescriptionProperty = AvaloniaProperty.Register(nameof(Description), string.Empty); + public bool ShowAdvancedOptions + { + get => GetValue(ShowAdvancedOptionsProperty); + set => SetValue(ShowAdvancedOptionsProperty, value); + } + public string Text { get => GetValue(TextProperty); @@ -95,7 +104,7 @@ namespace SourceGit.Views if (change.Property == TextProperty && _changingWay == TextChangeWay.None) { _changingWay = TextChangeWay.FromSource; - var normalized = Text.ReplaceLineEndings("\n").Trim(); + var normalized = Text.ReplaceLineEndings("\n"); var subjectEnd = normalized.IndexOf("\n\n", StringComparison.Ordinal); if (subjectEnd == -1) { @@ -125,7 +134,7 @@ namespace SourceGit.Views DescriptionEditor.CaretIndex = 0; e.Handled = true; } - else if (e.Key == Key.V && ((OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Meta) || (!OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Control))) + else if (e.Key == Key.V && e.KeyModifiers == (OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) { e.Handled = true; @@ -167,6 +176,41 @@ namespace SourceGit.Views } } + private void OnOpenCommitMessagePicker(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.WorkingCopy vm) + { + var menu = vm.CreateContextMenuForCommitMessages(); + menu.Placement = PlacementMode.TopEdgeAlignedLeft; + menu?.Open(button); + } + + e.Handled = true; + } + + private void OnOpenOpenAIHelper(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.WorkingCopy vm && sender is Control control) + { + var menu = vm.CreateContextForOpenAI(); + menu?.Open(control); + } + + e.Handled = true; + } + + private void OnOpenConventionalCommitHelper(object _, RoutedEventArgs e) + { + App.ShowWindow(new ViewModels.ConventionalCommitMessageBuilder(text => Text = text), true); + e.Handled = true; + } + + private void CopyAllText(object sender, RoutedEventArgs e) + { + App.CopyText(Text); + e.Handled = true; + } + private TextChangeWay _changingWay = TextChangeWay.None; } } diff --git a/src/Views/CommitRefsPresenter.cs b/src/Views/CommitRefsPresenter.cs index fc3233a5..507da1c2 100644 --- a/src/Views/CommitRefsPresenter.cs +++ b/src/Views/CommitRefsPresenter.cs @@ -38,7 +38,7 @@ namespace SourceGit.Views } public static readonly StyledProperty BackgroundProperty = - AvaloniaProperty.Register(nameof(Background), null); + AvaloniaProperty.Register(nameof(Background), Brushes.Transparent); public IBrush Background { @@ -56,7 +56,7 @@ namespace SourceGit.Views } public static readonly StyledProperty UseGraphColorProperty = - AvaloniaProperty.Register(nameof(UseGraphColor), false); + AvaloniaProperty.Register(nameof(UseGraphColor)); public bool UseGraphColor { @@ -64,13 +64,22 @@ namespace SourceGit.Views set => SetValue(UseGraphColorProperty, value); } - public static readonly StyledProperty TagBackgroundProperty = - AvaloniaProperty.Register(nameof(TagBackground), Brushes.White); + public static readonly StyledProperty AllowWrapProperty = + AvaloniaProperty.Register(nameof(AllowWrap)); - public IBrush TagBackground + public bool AllowWrap { - get => GetValue(TagBackgroundProperty); - set => SetValue(TagBackgroundProperty, value); + get => GetValue(AllowWrapProperty); + set => SetValue(AllowWrapProperty, value); + } + + public static readonly StyledProperty ShowTagsProperty = + AvaloniaProperty.Register(nameof(ShowTags), true); + + public bool ShowTags + { + get => GetValue(ShowTagsProperty); + set => SetValue(ShowTagsProperty, value); } static CommitRefsPresenter() @@ -79,10 +88,9 @@ namespace SourceGit.Views FontFamilyProperty, FontSizeProperty, ForegroundProperty, - TagBackgroundProperty); - - AffectsRender( - BackgroundProperty); + UseGraphColorProperty, + BackgroundProperty, + ShowTagsProperty); } public override void Render(DrawingContext context) @@ -93,11 +101,19 @@ namespace SourceGit.Views var useGraphColor = UseGraphColor; var fg = Foreground; var bg = Background; + var allowWrap = AllowWrap; var x = 1.0; + var y = 0.0; + foreach (var item in _items) { - var iconRect = new RoundedRect(new Rect(x, 0, 16, 16), new CornerRadius(2, 0, 0, 2)); - var entireRect = new RoundedRect(new Rect(x, 0, item.Width, 16), new CornerRadius(2)); + if (allowWrap && x > 1.0 && x + item.Width > Bounds.Width) + { + x = 1.0; + y += 20.0; + } + + var entireRect = new RoundedRect(new Rect(x, y, item.Width, 16), new CornerRadius(2)); if (item.IsHead) { @@ -110,24 +126,24 @@ namespace SourceGit.Views context.DrawRectangle(item.Brush, null, entireRect); } - context.DrawText(item.Label, new Point(x + 16, 8.0 - item.Label.Height * 0.5)); + context.DrawText(item.Label, new Point(x + 16, y + 8.0 - item.Label.Height * 0.5)); } else { if (bg != null) context.DrawRectangle(bg, null, entireRect); - var labelRect = new RoundedRect(new Rect(x + 16, 0, item.Label.Width + 8, 16), new CornerRadius(0, 2, 2, 0)); + var labelRect = new RoundedRect(new Rect(x + 16, y, item.Label.Width + 8, 16), new CornerRadius(0, 2, 2, 0)); using (context.PushOpacity(.2)) context.DrawRectangle(item.Brush, null, labelRect); - context.DrawLine(new Pen(item.Brush), new Point(x + 16, 0), new Point(x + 16, 16)); - context.DrawText(item.Label, new Point(x + 20, 8.0 - item.Label.Height * 0.5)); + context.DrawLine(new Pen(item.Brush), new Point(x + 16, y), new Point(x + 16, y + 16)); + context.DrawText(item.Label, new Point(x + 20, y + 8.0 - item.Label.Height * 0.5)); } context.DrawRectangle(null, new Pen(item.Brush), entireRect); - using (context.PushTransform(Matrix.CreateTranslation(x + 3, 3))) + using (context.PushTransform(Matrix.CreateTranslation(x + 3, y + 3))) context.DrawGeometry(fg, null, item.Icon); x += item.Width + 4; @@ -155,12 +171,18 @@ namespace SourceGit.Views var typefaceBold = new Typeface(FontFamily, FontStyle.Normal, FontWeight.Bold); var fg = Foreground; var normalBG = UseGraphColor ? commit.Brush : Brushes.Gray; - var tagBG = TagBackground; var labelSize = FontSize; var requiredWidth = 0.0; + var requiredHeight = 16.0; + var x = 0.0; + var allowWrap = AllowWrap; + var showTags = ShowTags; foreach (var decorator in refs) { + if (!showTags && decorator.Type == Models.DecoratorType.Tag) + continue; + var isHead = decorator.Type == Models.DecoratorType.CurrentBranchHead || decorator.Type == Models.DecoratorType.CurrentCommitHead; @@ -190,7 +212,7 @@ namespace SourceGit.Views geo = this.FindResource("Icons.Remote") as StreamGeometry; break; case Models.DecoratorType.Tag: - item.Brush = tagBG; + item.Brush = Brushes.Gray; geo = this.FindResource("Icons.Tag") as StreamGeometry; break; default: @@ -212,11 +234,24 @@ namespace SourceGit.Views item.Width = 16 + (isHead ? 0 : 4) + label.Width + 4; _items.Add(item); - requiredWidth += item.Width + 4; + x += item.Width + 4; + if (allowWrap) + { + if (x > availableSize.Width) + { + requiredHeight += 20.0; + x = item.Width; + } + } } + if (allowWrap && requiredHeight > 16.0) + requiredWidth = availableSize.Width; + else + requiredWidth = x + 2; + InvalidateVisual(); - return new Size(requiredWidth + 2, 16); + return new Size(requiredWidth, requiredHeight); } InvalidateVisual(); diff --git a/src/Views/CommitRelationTracking.axaml b/src/Views/CommitRelationTracking.axaml index 5e3574d8..9d036e10 100644 --- a/src/Views/CommitRelationTracking.axaml +++ b/src/Views/CommitRelationTracking.axaml @@ -30,7 +30,7 @@ - + diff --git a/src/Views/CommitStatusIndicator.cs b/src/Views/CommitStatusIndicator.cs new file mode 100644 index 00000000..7073011a --- /dev/null +++ b/src/Views/CommitStatusIndicator.cs @@ -0,0 +1,90 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace SourceGit.Views +{ + public class CommitStatusIndicator : Control + { + public static readonly StyledProperty CurrentBranchProperty = + AvaloniaProperty.Register(nameof(CurrentBranch)); + + public Models.Branch CurrentBranch + { + get => GetValue(CurrentBranchProperty); + set => SetValue(CurrentBranchProperty, value); + } + + public static readonly StyledProperty AheadBrushProperty = + AvaloniaProperty.Register(nameof(AheadBrush)); + + public IBrush AheadBrush + { + get => GetValue(AheadBrushProperty); + set => SetValue(AheadBrushProperty, value); + } + + public static readonly StyledProperty BehindBrushProperty = + AvaloniaProperty.Register(nameof(BehindBrush)); + + public IBrush BehindBrush + { + get => GetValue(BehindBrushProperty); + set => SetValue(BehindBrushProperty, value); + } + + private enum Status + { + Normal, + Ahead, + Behind, + } + + public override void Render(DrawingContext context) + { + if (_status == Status.Normal) + return; + + context.DrawEllipse(_status == Status.Ahead ? AheadBrush : BehindBrush, null, new Rect(0, 0, 5, 5)); + } + + protected override Size MeasureOverride(Size availableSize) + { + if (DataContext is Models.Commit commit && CurrentBranch is not null) + { + var sha = commit.SHA; + var track = CurrentBranch.TrackStatus; + + if (track.Ahead.Contains(sha)) + _status = Status.Ahead; + else if (track.Behind.Contains(sha)) + _status = Status.Behind; + else + _status = Status.Normal; + } + else + { + _status = Status.Normal; + } + + return _status == Status.Normal ? new Size(0, 0) : new Size(9, 5); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + InvalidateMeasure(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == CurrentBranchProperty) + InvalidateMeasure(); + } + + private Status _status = Status.Normal; + } +} diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs new file mode 100644 index 00000000..bfeab34f --- /dev/null +++ b/src/Views/CommitSubjectPresenter.cs @@ -0,0 +1,363 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; + +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; + +namespace SourceGit.Views +{ + public partial class CommitSubjectPresenter : Control + { + public static readonly StyledProperty FontFamilyProperty = + AvaloniaProperty.Register(nameof(FontFamily)); + + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + public static readonly StyledProperty CodeFontFamilyProperty = + AvaloniaProperty.Register(nameof(CodeFontFamily)); + + public FontFamily CodeFontFamily + { + get => GetValue(CodeFontFamilyProperty); + set => SetValue(CodeFontFamilyProperty, value); + } + + public static readonly StyledProperty FontSizeProperty = + TextBlock.FontSizeProperty.AddOwner(); + + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public static readonly StyledProperty FontWeightProperty = + TextBlock.FontWeightProperty.AddOwner(); + + public FontWeight FontWeight + { + get => GetValue(FontWeightProperty); + set => SetValue(FontWeightProperty, value); + } + + public static readonly StyledProperty InlineCodeBackgroundProperty = + AvaloniaProperty.Register(nameof(InlineCodeBackground), Brushes.Transparent); + + public IBrush InlineCodeBackground + { + get => GetValue(InlineCodeBackgroundProperty); + set => SetValue(InlineCodeBackgroundProperty, 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 LinkForegroundProperty = + AvaloniaProperty.Register(nameof(LinkForeground), Brushes.White); + + public IBrush LinkForeground + { + get => GetValue(LinkForegroundProperty); + set => SetValue(LinkForegroundProperty, value); + } + + public static readonly StyledProperty SubjectProperty = + AvaloniaProperty.Register(nameof(Subject)); + + public string Subject + { + get => GetValue(SubjectProperty); + set => SetValue(SubjectProperty, value); + } + + public static readonly StyledProperty> IssueTrackerRulesProperty = + AvaloniaProperty.Register>(nameof(IssueTrackerRules)); + + public AvaloniaList IssueTrackerRules + { + get => GetValue(IssueTrackerRulesProperty); + set => SetValue(IssueTrackerRulesProperty, value); + } + + public override void Render(DrawingContext context) + { + if (_needRebuildInlines) + { + _needRebuildInlines = false; + GenerateFormattedTextElements(); + } + + if (_inlines.Count == 0) + return; + + var ro = new RenderOptions() + { + TextRenderingMode = TextRenderingMode.SubpixelAntialias, + EdgeMode = EdgeMode.Antialias + }; + + using (context.PushRenderOptions(ro)) + { + var height = Bounds.Height; + var width = Bounds.Width; + foreach (var inline in _inlines) + { + if (inline.X > width) + return; + + if (inline.Element is { Type: Models.InlineElementType.Code }) + { + var rect = new Rect(inline.X, (height - inline.Text.Height - 2) * 0.5, inline.Text.WidthIncludingTrailingWhitespace + 8, inline.Text.Height + 2); + var roundedRect = new RoundedRect(rect, new CornerRadius(4)); + context.DrawRectangle(InlineCodeBackground, null, roundedRect); + context.DrawText(inline.Text, new Point(inline.X + 4, (height - inline.Text.Height) * 0.5)); + } + else + { + context.DrawText(inline.Text, new Point(inline.X, (height - inline.Text.Height) * 0.5)); + } + } + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty) + { + _elements.Clear(); + ClearHoveredIssueLink(); + + var subject = Subject; + if (string.IsNullOrEmpty(subject)) + { + _needRebuildInlines = true; + InvalidateVisual(); + return; + } + + var rules = IssueTrackerRules ?? []; + foreach (var rule in rules) + rule.Matches(_elements, subject); + + var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject); + if (!keywordMatch.Success) + keywordMatch = REG_KEYWORD_FORMAT2().Match(subject); + + if (keywordMatch.Success && _elements.Intersect(0, keywordMatch.Length) == null) + _elements.Add(new Models.InlineElement(Models.InlineElementType.Keyword, 0, keywordMatch.Length, string.Empty)); + + var codeMatches = REG_INLINECODE_FORMAT().Matches(subject); + for (var i = 0; i < codeMatches.Count; i++) + { + var match = codeMatches[i]; + if (!match.Success) + continue; + + var start = match.Index; + var len = match.Length; + if (_elements.Intersect(start, len) != null) + continue; + + _elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty)); + } + + _elements.Sort(); + _needRebuildInlines = true; + InvalidateVisual(); + } + else if (change.Property == FontFamilyProperty || + change.Property == CodeFontFamilyProperty || + change.Property == FontSizeProperty || + change.Property == FontWeightProperty || + change.Property == ForegroundProperty || + change.Property == LinkForegroundProperty) + { + _needRebuildInlines = true; + InvalidateVisual(); + } + else if (change.Property == InlineCodeBackgroundProperty) + { + InvalidateVisual(); + } + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + var point = e.GetPosition(this); + foreach (var inline in _inlines) + { + if (inline.Element is not { Type: Models.InlineElementType.Link } link) + continue; + + if (inline.X > point.X || inline.X + inline.Text.WidthIncludingTrailingWhitespace < point.X) + continue; + + _lastHover = link; + SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); + ToolTip.SetTip(this, link.Link); + e.Handled = true; + return; + } + + ClearHoveredIssueLink(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + if (_lastHover != null) + Native.OS.OpenBrowser(_lastHover.Link); + } + + protected override void OnPointerExited(PointerEventArgs e) + { + base.OnPointerExited(e); + ClearHoveredIssueLink(); + } + + private void GenerateFormattedTextElements() + { + _inlines.Clear(); + + var subject = Subject; + if (string.IsNullOrEmpty(subject)) + return; + + var fontFamily = FontFamily; + var codeFontFamily = CodeFontFamily; + var fontSize = FontSize; + var foreground = Foreground; + var linkForeground = LinkForeground; + var typeface = new Typeface(fontFamily, FontStyle.Normal, FontWeight); + var codeTypeface = new Typeface(codeFontFamily, FontStyle.Normal, FontWeight); + var pos = 0; + var x = 0.0; + for (var i = 0; i < _elements.Count; i++) + { + var elem = _elements[i]; + if (elem.Start > pos) + { + var normal = new FormattedText( + subject.Substring(pos, elem.Start - pos), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + fontSize, + foreground); + + _inlines.Add(new Inline(x, normal, null)); + x += normal.WidthIncludingTrailingWhitespace; + } + + if (elem.Type == Models.InlineElementType.Keyword) + { + var keyword = new FormattedText( + subject.Substring(elem.Start, elem.Length), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(fontFamily, FontStyle.Normal, FontWeight.Bold), + fontSize, + foreground); + _inlines.Add(new Inline(x, keyword, elem)); + x += keyword.WidthIncludingTrailingWhitespace; + } + else if (elem.Type == Models.InlineElementType.Link) + { + var link = new FormattedText( + subject.Substring(elem.Start, elem.Length), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + fontSize, + linkForeground); + _inlines.Add(new Inline(x, link, elem)); + x += link.WidthIncludingTrailingWhitespace; + } + else if (elem.Type == Models.InlineElementType.Code) + { + var link = new FormattedText( + subject.Substring(elem.Start + 1, elem.Length - 2), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + codeTypeface, + fontSize - 0.5, + foreground); + _inlines.Add(new Inline(x, link, elem)); + x += link.WidthIncludingTrailingWhitespace + 8; + } + + pos = elem.Start + elem.Length; + } + + if (pos < subject.Length) + { + var normal = new FormattedText( + subject.Substring(pos), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + fontSize, + foreground); + + _inlines.Add(new Inline(x, normal, null)); + } + } + + private void ClearHoveredIssueLink() + { + if (_lastHover != null) + { + ToolTip.SetTip(this, null); + SetCurrentValue(CursorProperty, Cursor.Parse("Arrow")); + _lastHover = null; + } + } + + [GeneratedRegex(@"`.*?`")] + private static partial Regex REG_INLINECODE_FORMAT(); + + [GeneratedRegex(@"^\[[\w\s]+\]")] + private static partial Regex REG_KEYWORD_FORMAT1(); + + [GeneratedRegex(@"^\S+([\<\(][\w\s_\-\*,]+[\>\)])?\!?\s?:\s")] + private static partial Regex REG_KEYWORD_FORMAT2(); + + private class Inline + { + public double X { get; set; } = 0; + public FormattedText Text { get; set; } = null; + public Models.InlineElement Element { get; set; } = null; + + public Inline(double x, FormattedText text, Models.InlineElement elem) + { + X = x; + Text = text; + Element = elem; + } + } + + private Models.InlineElementCollector _elements = new(); + private List _inlines = []; + private Models.InlineElement _lastHover = null; + private bool _needRebuildInlines = false; + } +} diff --git a/src/Views/CommitTimeTextBlock.cs b/src/Views/CommitTimeTextBlock.cs new file mode 100644 index 00000000..db63e8a6 --- /dev/null +++ b/src/Views/CommitTimeTextBlock.cs @@ -0,0 +1,166 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace SourceGit.Views +{ + public class CommitTimeTextBlock : TextBlock + { + public static readonly StyledProperty ShowAsDateTimeProperty = + AvaloniaProperty.Register(nameof(ShowAsDateTime), true); + + public bool ShowAsDateTime + { + get => GetValue(ShowAsDateTimeProperty); + set => SetValue(ShowAsDateTimeProperty, value); + } + + public static readonly StyledProperty DateTimeFormatProperty = + AvaloniaProperty.Register(nameof(DateTimeFormat), 0); + + public int DateTimeFormat + { + get => GetValue(DateTimeFormatProperty); + set => SetValue(DateTimeFormatProperty, value); + } + + public static readonly StyledProperty UseAuthorTimeProperty = + AvaloniaProperty.Register(nameof(UseAuthorTime), true); + + public bool UseAuthorTime + { + get => GetValue(UseAuthorTimeProperty); + set => SetValue(UseAuthorTimeProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextBlock); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == UseAuthorTimeProperty) + { + SetCurrentValue(TextProperty, GetDisplayText()); + } + else if (change.Property == ShowAsDateTimeProperty) + { + SetCurrentValue(TextProperty, GetDisplayText()); + + if (ShowAsDateTime) + StopTimer(); + else + StartTimer(); + } + else if (change.Property == DateTimeFormatProperty) + { + if (ShowAsDateTime) + SetCurrentValue(TextProperty, GetDisplayText()); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (!ShowAsDateTime) + StartTimer(); + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + StopTimer(); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + SetCurrentValue(TextProperty, GetDisplayText()); + } + + private void StartTimer() + { + if (_refreshTimer != null) + return; + + _refreshTimer = DispatcherTimer.Run(() => + { + Dispatcher.UIThread.Invoke(() => + { + var text = GetDisplayText(); + if (!text.Equals(Text, StringComparison.Ordinal)) + Text = text; + }); + + return true; + }, TimeSpan.FromSeconds(10)); + } + + private void StopTimer() + { + if (_refreshTimer != null) + { + _refreshTimer.Dispose(); + _refreshTimer = null; + } + } + + private string GetDisplayText() + { + var commit = DataContext as Models.Commit; + if (commit == null) + return string.Empty; + + if (ShowAsDateTime) + return UseAuthorTime ? commit.AuthorTimeStr : commit.CommitterTimeStr; + + var timestamp = UseAuthorTime ? commit.AuthorTime : commit.CommitterTime; + var now = DateTime.Now; + var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); + var span = now - localTime; + if (span.TotalMinutes < 1) + return App.Text("Period.JustNow"); + + if (span.TotalHours < 1) + return App.Text("Period.MinutesAgo", (int)span.TotalMinutes); + + if (span.TotalDays < 1) + { + var hours = (int)span.TotalHours; + return hours == 1 ? App.Text("Period.HourAgo") : App.Text("Period.HoursAgo", hours); + } + + var lastDay = now.AddDays(-1).Date; + if (localTime >= lastDay) + return App.Text("Period.Yesterday"); + + if ((localTime.Year == now.Year && localTime.Month == now.Month) || span.TotalDays < 28) + { + var diffDay = now.Date - localTime.Date; + return App.Text("Period.DaysAgo", (int)diffDay.TotalDays); + } + + var lastMonth = now.AddMonths(-1).Date; + if (localTime.Year == lastMonth.Year && localTime.Month == lastMonth.Month) + return App.Text("Period.LastMonth"); + + if (localTime.Year == now.Year || localTime > now.AddMonths(-11)) + { + var diffMonth = (12 + now.Month - localTime.Month) % 12; + return App.Text("Period.MonthsAgo", diffMonth); + } + + var diffYear = now.Year - localTime.Year; + if (diffYear == 1) + return App.Text("Period.LastYear"); + + return App.Text("Period.YearsAgo", diffYear); + } + + private IDisposable _refreshTimer = null; + } +} diff --git a/src/Views/ConfigureWorkspace.axaml b/src/Views/ConfigureWorkspace.axaml index c7ed900b..b239399a 100644 --- a/src/Views/ConfigureWorkspace.axaml +++ b/src/Views/ConfigureWorkspace.axaml @@ -2,10 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.ConfigureWorkspace" x:DataType="vm:ConfigureWorkspace" @@ -17,30 +15,23 @@ WindowStartupLocation="CenterOwner"> - - + - - - - - @@ -75,11 +66,11 @@ - @@ -88,32 +79,64 @@ - - - - - - - - - + + + - - - + + + + + + + + + + + + + + - - diff --git a/src/Views/ConfigureWorkspace.axaml.cs b/src/Views/ConfigureWorkspace.axaml.cs index 82d8cd30..06294caf 100644 --- a/src/Views/ConfigureWorkspace.axaml.cs +++ b/src/Views/ConfigureWorkspace.axaml.cs @@ -1,5 +1,7 @@ +using System; using Avalonia.Controls; -using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; namespace SourceGit.Views { @@ -12,13 +14,35 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { - ViewModels.Preference.Instance.Save(); base.OnClosing(e); + + if (!Design.IsDesignMode) + ViewModels.Preferences.Instance.Save(); } - private void BeginMoveWindow(object _, PointerPressedEventArgs e) + private async void SelectDefaultCloneDir(object _, RoutedEventArgs e) { - BeginMoveDrag(e); + var workspace = DataContext as ViewModels.ConfigureWorkspace; + if (workspace?.Selected == null) + return; + + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + try + { + var selected = await StorageProvider.OpenFolderPickerAsync(options); + if (selected.Count == 1) + { + var folder = selected[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder?.Path.ToString(); + workspace.Selected.DefaultCloneDir = folderPath; + } + } + catch (Exception ex) + { + App.RaiseException(string.Empty, $"Failed to select default clone directory: {ex.Message}"); + } + + e.Handled = true; } } } diff --git a/src/Views/ConfirmCommit.axaml b/src/Views/ConfirmCommit.axaml new file mode 100644 index 00000000..a835f0b6 --- /dev/null +++ b/src/Views/ConfirmCommit.axaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Conflict.axaml.cs b/src/Views/Conflict.axaml.cs new file mode 100644 index 00000000..6121b5c8 --- /dev/null +++ b/src/Views/Conflict.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public partial class Conflict : UserControl + { + public Conflict() + { + InitializeComponent(); + } + + private void OnPressedSHA(object sender, PointerPressedEventArgs e) + { + var repoView = this.FindAncestorOfType(); + if (repoView is { DataContext: ViewModels.Repository repo } && sender is TextBlock text) + repo.NavigateToCommit(text.Text); + + e.Handled = true; + } + } +} diff --git a/src/Views/ContextMenuExtension.cs b/src/Views/ContextMenuExtension.cs deleted file mode 100644 index 2abcf2b9..00000000 --- a/src/Views/ContextMenuExtension.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel; -using Avalonia.Controls; - -namespace SourceGit.Views -{ - public static class ContextMenuExtension - { - public static void OpenContextMenu(this Control control, ContextMenu menu) - { - if (menu == null) - return; - - menu.PlacementTarget = control; - menu.Closing += OnContextMenuClosing; // Clear context menu because it is dynamic. - - control.ContextMenu = menu; - control.ContextMenu?.Open(); - } - - private static void OnContextMenuClosing(object sender, CancelEventArgs e) - { - if (sender is ContextMenu menu && menu.PlacementTarget != null) - menu.PlacementTarget.ContextMenu = null; - } - } -} diff --git a/src/Views/ConventionalCommitMessageBuilder.axaml b/src/Views/ConventionalCommitMessageBuilder.axaml index 19174a6d..a64037f8 100644 --- a/src/Views/ConventionalCommitMessageBuilder.axaml +++ b/src/Views/ConventionalCommitMessageBuilder.axaml @@ -5,7 +5,6 @@ xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.ConventionalCommitMessageBuilder" x:DataType="vm:ConventionalCommitMessageBuilder" @@ -16,32 +15,25 @@ SizeToContent="Height" CanResize="False" WindowStartupLocation="CenterOwner"> - + - - + - - - - - @@ -56,12 +48,33 @@ Height="28" Padding="8,0" VerticalAlignment="Center" HorizontalAlignment="Stretch" ItemsSource="{Binding Source={x:Static m:ConventionalCommitType.Supported}}" - SelectedItem="{Binding Type, Mode=TwoWay}"> + SelectedItem="{Binding Type, Mode=TwoWay}" + Grid.IsSharedSizeScope="True"> + + + + + + + + + + + - - - - + + + + + + + + + + + + + @@ -138,9 +151,13 @@ + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - + - + - @@ -130,7 +213,7 @@ - + @@ -157,7 +240,7 @@ - + @@ -178,26 +261,43 @@ - + + + + + - + + + + + + + + + + - + - - - - + + + + + + + + @@ -209,9 +309,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/Views/DiffView.axaml.cs b/src/Views/DiffView.axaml.cs index 860627d3..54f9617a 100644 --- a/src/Views/DiffView.axaml.cs +++ b/src/Views/DiffView.axaml.cs @@ -1,4 +1,7 @@ +using Avalonia; using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace SourceGit.Views { @@ -8,5 +11,54 @@ namespace SourceGit.Views { InitializeComponent(); } + + private void OnGotoFirstChange(object _, RoutedEventArgs e) + { + this.FindDescendantOfType()?.GotoFirstChange(); + e.Handled = true; + } + + private void OnGotoPrevChange(object _, RoutedEventArgs e) + { + this.FindDescendantOfType()?.GotoPrevChange(); + e.Handled = true; + } + + private void OnGotoNextChange(object _, RoutedEventArgs e) + { + this.FindDescendantOfType()?.GotoNextChange(); + e.Handled = true; + } + + private void OnGotoLastChange(object _, RoutedEventArgs e) + { + this.FindDescendantOfType()?.GotoLastChange(); + e.Handled = true; + } + + private void OnBlockNavigationChanged(object sender, RoutedEventArgs e) + { + if (sender is TextDiffView textDiff) + BlockNavigationIndicator.Text = textDiff.BlockNavigation?.Indicator ?? string.Empty; + } + + private void OnUseFullTextDiffClicked(object sender, RoutedEventArgs e) + { + var textDiffView = this.FindDescendantOfType(); + if (textDiffView == null) + return; + + var presenter = textDiffView.FindDescendantOfType(); + if (presenter == null) + return; + + if (presenter.DataContext is Models.TextDiff combined) + combined.ScrollOffset = Vector.Zero; + else if (presenter.DataContext is ViewModels.TwoSideTextDiff twoSides) + twoSides.File = string.Empty; // Just to reset `SyncScrollOffset` without affect UI refresh. + + (DataContext as ViewModels.DiffContext)?.ToggleFullTextDiff(); + e.Handled = true; + } } } diff --git a/src/Views/Discard.axaml b/src/Views/Discard.axaml index 23162060..1699b051 100644 --- a/src/Views/Discard.axaml +++ b/src/Views/Discard.axaml @@ -11,16 +11,16 @@ - + - - + @@ -31,13 +31,13 @@ Text="{DynamicResource Text.Discard.Changes}"/> - + - + - + - diff --git a/src/Views/EditRepositoryNode.axaml b/src/Views/EditRepositoryNode.axaml index ac8f50f3..615e3f11 100644 --- a/src/Views/EditRepositoryNode.axaml +++ b/src/Views/EditRepositoryNode.axaml @@ -43,7 +43,7 @@ Fill="{Binding Converter={x:Static c:IntConverters.ToBookmarkBrush}}" HorizontalAlignment="Center" VerticalAlignment="Center" Data="{StaticResource Icons.Bookmark}"/> - + diff --git a/src/Views/EnhancedSelectableTextBlock.cs b/src/Views/EnhancedSelectableTextBlock.cs new file mode 100644 index 00000000..183b7021 --- /dev/null +++ b/src/Views/EnhancedSelectableTextBlock.cs @@ -0,0 +1,20 @@ +using System; + +using Avalonia; +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public class EnhancedSelectableTextBlock : SelectableTextBlock + { + protected override Type StyleKeyOverride => typeof(SelectableTextBlock); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == TextProperty) + UpdateLayout(); + } + } +} diff --git a/src/Views/ExecuteCustomAction.axaml b/src/Views/ExecuteCustomAction.axaml new file mode 100644 index 00000000..9ee2b55d --- /dev/null +++ b/src/Views/ExecuteCustomAction.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/src/Views/ExecuteCustomAction.axaml.cs b/src/Views/ExecuteCustomAction.axaml.cs new file mode 100644 index 00000000..e4f9cecf --- /dev/null +++ b/src/Views/ExecuteCustomAction.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class ExecuteCustomAction : UserControl + { + public ExecuteCustomAction() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/Fetch.axaml b/src/Views/Fetch.axaml index e6821c31..67669380 100644 --- a/src/Views/Fetch.axaml +++ b/src/Views/Fetch.axaml @@ -33,16 +33,19 @@ + Content="{DynamicResource Text.Fetch.Force}" + IsChecked="{Binding Force, Mode=TwoWay}" + ToolTip.Tip="--force"/> + Content="{DynamicResource Text.Fetch.AllRemotes}" + IsChecked="{Binding FetchAllRemotes, Mode=TwoWay}" + ToolTip.Tip="--all"/> + IsChecked="{Binding NoTags, Mode=TwoWay}" + ToolTip.Tip="--no-tags"/> diff --git a/src/Views/FastForwardWithoutCheckout.axaml b/src/Views/FetchInto.axaml similarity index 77% rename from src/Views/FastForwardWithoutCheckout.axaml rename to src/Views/FetchInto.axaml index 16b40256..4a0c0966 100644 --- a/src/Views/FastForwardWithoutCheckout.axaml +++ b/src/Views/FetchInto.axaml @@ -4,18 +4,18 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" - x:Class="SourceGit.Views.FastForwardWithoutCheckout" - x:DataType="vm:FastForwardWithoutCheckout"> + x:Class="SourceGit.Views.FetchInto" + x:DataType="vm:FetchInto"> + Text="{DynamicResource Text.Fetch.Title}"/> - + - + diff --git a/src/Views/FetchInto.axaml.cs b/src/Views/FetchInto.axaml.cs new file mode 100644 index 00000000..c61c052e --- /dev/null +++ b/src/Views/FetchInto.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class FetchInto : UserControl + { + public FetchInto() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index bc048706..be0c91a0 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -20,29 +20,26 @@ - + - - - - - + - - - - - + - - - + @@ -59,8 +56,8 @@ Margin="8,4,4,8" BorderBrush="{DynamicResource Brush.Border2}" ItemsSource="{Binding Commits}" - SelectedItem="{Binding SelectedCommit, Mode=TwoWay}" - SelectionMode="Single" + SelectedItems="{Binding SelectedCommits, Mode=TwoWay}" + SelectionMode="Multiple" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto"> @@ -81,9 +78,9 @@ - + - + - + - + + + @@ -110,65 +109,148 @@ BorderThickness="1,0,0,0" BorderBrush="{DynamicResource Brush.Border0}"/> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Views/FilterModeSwitchButton.axaml.cs b/src/Views/FilterModeSwitchButton.axaml.cs new file mode 100644 index 00000000..b3b2c3da --- /dev/null +++ b/src/Views/FilterModeSwitchButton.axaml.cs @@ -0,0 +1,165 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public partial class FilterModeSwitchButton : UserControl + { + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode)); + + public Models.FilterMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + public static readonly StyledProperty IsNoneVisibleProperty = + AvaloniaProperty.Register(nameof(IsNoneVisible)); + + public bool IsNoneVisible + { + get => GetValue(IsNoneVisibleProperty); + set => SetValue(IsNoneVisibleProperty, value); + } + + public static readonly StyledProperty IsContextMenuOpeningProperty = + AvaloniaProperty.Register(nameof(IsContextMenuOpening)); + + public bool IsContextMenuOpening + { + get => GetValue(IsContextMenuOpeningProperty); + set => SetValue(IsContextMenuOpeningProperty, value); + } + + public FilterModeSwitchButton() + { + IsVisible = false; + InitializeComponent(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ModeProperty || + change.Property == IsNoneVisibleProperty || + change.Property == IsContextMenuOpeningProperty) + { + var visible = (Mode != Models.FilterMode.None || IsNoneVisible || IsContextMenuOpening); + SetCurrentValue(IsVisibleProperty, visible); + } + } + + private void OnChangeFilterModeButtonClicked(object sender, RoutedEventArgs e) + { + var repoView = this.FindAncestorOfType(); + if (repoView == null) + return; + + var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + + var button = sender as Button; + if (button == null) + return; + + var menu = new ContextMenu(); + var mode = Models.FilterMode.None; + if (DataContext is Models.Tag tag) + { + mode = tag.FilterMode; + + if (mode != Models.FilterMode.None) + { + var unset = new MenuItem(); + unset.Header = App.Text("Repository.FilterCommits.Default"); + unset.Click += (_, ev) => + { + repo.SetTagFilterMode(tag, Models.FilterMode.None); + ev.Handled = true; + }; + + menu.Items.Add(unset); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var include = new MenuItem(); + include.Icon = App.CreateMenuIcon("Icons.Filter"); + include.Header = App.Text("Repository.FilterCommits.Include"); + include.IsEnabled = mode != Models.FilterMode.Included; + include.Click += (_, ev) => + { + repo.SetTagFilterMode(tag, Models.FilterMode.Included); + ev.Handled = true; + }; + + var exclude = new MenuItem(); + exclude.Icon = App.CreateMenuIcon("Icons.EyeClose"); + exclude.Header = App.Text("Repository.FilterCommits.Exclude"); + exclude.IsEnabled = mode != Models.FilterMode.Excluded; + exclude.Click += (_, ev) => + { + repo.SetTagFilterMode(tag, Models.FilterMode.Excluded); + ev.Handled = true; + }; + + menu.Items.Add(include); + menu.Items.Add(exclude); + } + else if (DataContext is ViewModels.BranchTreeNode node) + { + mode = node.FilterMode; + + if (mode != Models.FilterMode.None) + { + var unset = new MenuItem(); + unset.Header = App.Text("Repository.FilterCommits.Default"); + unset.Click += (_, ev) => + { + repo.SetBranchFilterMode(node, Models.FilterMode.None, false, true); + ev.Handled = true; + }; + + menu.Items.Add(unset); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var include = new MenuItem(); + include.Icon = App.CreateMenuIcon("Icons.Filter"); + include.Header = App.Text("Repository.FilterCommits.Include"); + include.IsEnabled = mode != Models.FilterMode.Included; + include.Click += (_, ev) => + { + repo.SetBranchFilterMode(node, Models.FilterMode.Included, false, true); + ev.Handled = true; + }; + + var exclude = new MenuItem(); + exclude.Icon = App.CreateMenuIcon("Icons.EyeClose"); + exclude.Header = App.Text("Repository.FilterCommits.Exclude"); + exclude.IsEnabled = mode != Models.FilterMode.Excluded; + exclude.Click += (_, ev) => + { + repo.SetBranchFilterMode(node, Models.FilterMode.Excluded, false, true); + ev.Handled = true; + }; + + menu.Items.Add(include); + menu.Items.Add(exclude); + } + + if (mode == Models.FilterMode.None) + { + IsContextMenuOpening = true; + menu.Closed += (_, _) => IsContextMenuOpening = false; + } + + menu.Open(button); + e.Handled = true; + } + } +} diff --git a/src/Views/GitFlowFinish.axaml b/src/Views/GitFlowFinish.axaml index 7af46fd9..fa847bba 100644 --- a/src/Views/GitFlowFinish.axaml +++ b/src/Views/GitFlowFinish.axaml @@ -2,7 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.GitFlowFinish" x:DataType="vm:GitFlowFinish"> @@ -10,16 +12,16 @@ + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Feature}}"/> + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Release}}"/> - + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Hotfix}}"/> + + + + + + IsChecked="{Binding KeepBranch, Mode=TwoWay}" + ToolTip.Tip="-k"/> diff --git a/src/Views/GitFlowStart.axaml b/src/Views/GitFlowStart.axaml index 7d2b78b2..aed970de 100644 --- a/src/Views/GitFlowStart.axaml +++ b/src/Views/GitFlowStart.axaml @@ -2,8 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.GitFlowStart" x:DataType="vm:GitFlowStart"> @@ -11,15 +13,15 @@ + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Feature}}"/> + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Release}}"/> + IsVisible="{Binding Type, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:GitFlowBranchType.Hotfix}}"/> - - - + + + - - + + - - + + - - - + + + - + - + - + - - + + @@ -48,17 +48,14 @@ - + IsVisible="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowAuthorTimeInGraph, Converter={x:Static BoolConverters.Not}, Mode=OneWay}"/> + IsVisible="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowAuthorTimeInGraph, Mode=OneWay}"/> @@ -79,7 +76,7 @@ - + - + - + - + - + + + + + + + + + + + + + - + - - - - + + - + - + - + - + - + + + - + + + - + - + @@ -241,7 +258,7 @@ HorizontalAlignment="Center" Fill="{DynamicResource Brush.FG2}" IsVisible="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNull}}"/> - + @@ -255,15 +272,15 @@ - - @@ -111,7 +104,7 @@ - + diff --git a/src/Views/LFSLocks.axaml.cs b/src/Views/LFSLocks.axaml.cs index ee4b6ff1..695341f4 100644 --- a/src/Views/LFSLocks.axaml.cs +++ b/src/Views/LFSLocks.axaml.cs @@ -1,5 +1,4 @@ using Avalonia.Controls; -using Avalonia.Input; using Avalonia.Interactivity; namespace SourceGit.Views @@ -11,11 +10,6 @@ namespace SourceGit.Views InitializeComponent(); } - private void BeginMoveWindow(object _, PointerPressedEventArgs e) - { - BeginMoveDrag(e); - } - private void OnUnlockButtonClicked(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.LFSLocks vm && sender is Button button) diff --git a/src/Views/LFSTrackCustomPattern.axaml b/src/Views/LFSTrackCustomPattern.axaml index 36eaef65..f60304d5 100644 --- a/src/Views/LFSTrackCustomPattern.axaml +++ b/src/Views/LFSTrackCustomPattern.axaml @@ -11,7 +11,7 @@ - + + WindowStartupLocation="CenterScreen"> @@ -25,19 +25,17 @@ - - - + - + - - + + - - + + - + @@ -99,5 +102,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index 359a80fa..08620f83 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Platform; using Avalonia.VisualTree; namespace SourceGit.Views @@ -19,44 +20,77 @@ namespace SourceGit.Views set => SetValue(CaptionHeightProperty, value); } - public bool IsRightCaptionButtonsVisible + public static readonly StyledProperty HasLeftCaptionButtonProperty = + AvaloniaProperty.Register(nameof(HasLeftCaptionButton)); + + public bool HasLeftCaptionButton + { + get => GetValue(HasLeftCaptionButtonProperty); + set => SetValue(HasLeftCaptionButtonProperty, value); + } + + public bool HasRightCaptionButton { get { if (OperatingSystem.IsLinux()) - return !ViewModels.Preference.Instance.UseSystemWindowFrame; + return !Native.OS.UseSystemWindowFrame; + return OperatingSystem.IsWindows(); } } public Launcher() { - var layout = ViewModels.Preference.Instance.Layout; + var layout = ViewModels.Preferences.Instance.Layout; if (layout.LauncherWindowState != WindowState.Maximized) { Width = layout.LauncherWidth; Height = layout.LauncherHeight; } - if (UseSystemWindowFrame) + if (OperatingSystem.IsMacOS()) + { + HasLeftCaptionButton = true; + CaptionHeight = new GridLength(34); + ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; + } + else if (UseSystemWindowFrame) + { CaptionHeight = new GridLength(30); + } else + { CaptionHeight = new GridLength(38); + } InitializeComponent(); } + public void BringToTop() + { + if (WindowState == WindowState.Minimized) + WindowState = _lastWindowState; + else + Activate(); + } + public bool HasKeyModifier(KeyModifiers modifier) { return _unhandledModifiers.HasFlag(modifier); } + public void ClearKeyModifier() + { + _unhandledModifiers = KeyModifiers.None; + } + protected override void OnOpened(EventArgs e) { base.OnOpened(e); - var layout = ViewModels.Preference.Instance.Layout; - if (layout.LauncherWindowState == WindowState.Maximized) + var state = ViewModels.Preferences.Instance.Layout.LauncherWindowState; + if (state == WindowState.Maximized || state == WindowState.FullScreen) WindowState = WindowState.Maximized; } @@ -66,15 +100,16 @@ namespace SourceGit.Views if (change.Property == WindowStateProperty) { - var state = (WindowState)change.NewValue!; - if (OperatingSystem.IsLinux() && UseSystemWindowFrame) - CaptionHeight = new GridLength(30); - else if (state == WindowState.Maximized) - CaptionHeight = new GridLength(OperatingSystem.IsMacOS() ? 34 : 30); - else - CaptionHeight = new GridLength(38); + _lastWindowState = (WindowState)change.OldValue!; - ViewModels.Preference.Instance.Layout.LauncherWindowState = state; + var state = (WindowState)change.NewValue!; + if (!OperatingSystem.IsMacOS() && !UseSystemWindowFrame) + CaptionHeight = new GridLength(state == WindowState.Maximized ? 30 : 38); + + if (OperatingSystem.IsMacOS()) + HasLeftCaptionButton = state != WindowState.FullScreen; + + ViewModels.Preferences.Instance.Layout.LauncherWindowState = state; } } @@ -87,17 +122,55 @@ namespace SourceGit.Views // We should clear all unhandled key modifiers. _unhandledModifiers = KeyModifiers.None; - // Ctrl+Shift+P opens preference dialog (macOS use hotkeys in system menu bar) - if (!OperatingSystem.IsMacOS() && e.KeyModifiers == (KeyModifiers.Control | KeyModifiers.Shift) && e.Key == Key.P) + // Check for AltGr (which is detected as Ctrl+Alt) + bool isAltGr = e.KeyModifiers.HasFlag(KeyModifiers.Control) && + e.KeyModifiers.HasFlag(KeyModifiers.Alt); + + // Skip hotkey processing if AltGr is pressed + if (isAltGr) { - App.OpenDialog(new Preference()); + base.OnKeyDown(e); + return; + } + + // Ctrl+, opens preference dialog (macOS use hotkeys in system menu bar) + if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: KeyModifiers.Control, Key: Key.OemComma }) + { + App.ShowWindow(new Preferences(), true); e.Handled = true; return; } - if ((OperatingSystem.IsMacOS() && e.KeyModifiers.HasFlag(KeyModifiers.Meta)) || - (!OperatingSystem.IsMacOS() && e.KeyModifiers.HasFlag(KeyModifiers.Control))) + // F1 opens preference dialog (macOS use hotkeys in system menu bar) + if (!OperatingSystem.IsMacOS() && e.Key == Key.F1) { + App.ShowWindow(new Hotkeys(), true); + return; + } + + // Ctrl+Q quits the application (macOS use hotkeys in system menu bar) + if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: KeyModifiers.Control, Key: Key.Q }) + { + App.Quit(0); + return; + } + + if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) + { + if (e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.P) + { + vm.OpenWorkspaceSwitcher(); + e.Handled = true; + return; + } + + if (e.Key == Key.P) + { + vm.OpenTabSwitcher(); + e.Handled = true; + return; + } + if (e.Key == Key.W) { vm.CloseTab(null); @@ -112,6 +185,16 @@ namespace SourceGit.Views return; } + if (e.Key == Key.N) + { + if (vm.ActivePage.Data is not ViewModels.Welcome) + vm.AddNewTab(); + + ViewModels.Welcome.Instance.Clone(); + e.Handled = true; + return; + } + if ((OperatingSystem.IsMacOS() && e.KeyModifiers.HasFlag(KeyModifiers.Alt) && e.Key == Key.Right) || (!OperatingSystem.IsMacOS() && !e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.Tab)) { @@ -181,7 +264,11 @@ namespace SourceGit.Views } else if (e.Key == Key.Escape) { - vm.ActivePage.CancelPopup(); + if (vm.Switcher != null) + vm.CancelSwitcher(); + else + vm.ActivePage.CancelPopup(); + e.Handled = true; return; } @@ -197,18 +284,18 @@ namespace SourceGit.Views base.OnKeyDown(e); - // Record unhandled key modifers. + // Record unhandled key modifiers. if (!e.Handled) { _unhandledModifiers = e.KeyModifiers; - if (!_unhandledModifiers.HasFlag(KeyModifiers.Alt) && (e.Key == Key.LeftAlt || e.Key == Key.RightAlt)) + if (!_unhandledModifiers.HasFlag(KeyModifiers.Alt) && e.Key is Key.LeftAlt or Key.RightAlt) _unhandledModifiers |= KeyModifiers.Alt; - if (!_unhandledModifiers.HasFlag(KeyModifiers.Control) && (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl)) + if (!_unhandledModifiers.HasFlag(KeyModifiers.Control) && e.Key is Key.LeftCtrl or Key.RightCtrl) _unhandledModifiers |= KeyModifiers.Control; - if (!_unhandledModifiers.HasFlag(KeyModifiers.Shift) && (e.Key == Key.LeftShift || e.Key == Key.RightShift)) + if (!_unhandledModifiers.HasFlag(KeyModifiers.Shift) && e.Key is Key.LeftShift or Key.RightShift) _unhandledModifiers |= KeyModifiers.Shift; } } @@ -221,26 +308,10 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { - (DataContext as ViewModels.Launcher)?.Quit(Width, Height); base.OnClosing(e); - } - private void OnTitleBarDoubleTapped(object _, TappedEventArgs e) - { - if (WindowState == WindowState.Maximized) - WindowState = WindowState.Normal; - else - WindowState = WindowState.Maximized; - - e.Handled = true; - } - - private void BeginMoveWindow(object _, PointerPressedEventArgs e) - { - if (e.ClickCount == 1) - BeginMoveDrag(e); - - e.Handled = true; + if (!Design.IsDesignMode && DataContext is ViewModels.Launcher launcher) + launcher.Quit(Width, Height); } private void OnOpenWorkspaceMenu(object sender, RoutedEventArgs e) @@ -248,12 +319,20 @@ namespace SourceGit.Views if (sender is Button btn && DataContext is ViewModels.Launcher launcher) { var menu = launcher.CreateContextForWorkspace(); - btn.OpenContextMenu(menu); + menu?.Open(btn); } e.Handled = true; } + private void OnCancelSwitcher(object sender, PointerPressedEventArgs e) + { + if (e.Source == sender) + (DataContext as ViewModels.Launcher)?.CancelSwitcher(); + e.Handled = true; + } + private KeyModifiers _unhandledModifiers = KeyModifiers.None; + private WindowState _lastWindowState = WindowState.Normal; } } diff --git a/src/Views/LauncherPage.axaml b/src/Views/LauncherPage.axaml index 3ef4286a..36ca39f0 100644 --- a/src/Views/LauncherPage.axaml +++ b/src/Views/LauncherPage.axaml @@ -5,6 +5,7 @@ xmlns:m="using:SourceGit.Models" xmlns:v="using:SourceGit.Views" xmlns:vm="using:SourceGit.ViewModels" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.LauncherPage" x:DataType="vm:LauncherPage"> @@ -16,14 +17,14 @@ - + - + @@ -31,104 +32,128 @@ - + - - - - - - - - - - - + + + + + + + + - - - - + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/LauncherPageSwitcher.axaml.cs b/src/Views/LauncherPageSwitcher.axaml.cs new file mode 100644 index 00000000..9bc0bf2d --- /dev/null +++ b/src/Views/LauncherPageSwitcher.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class LauncherPageSwitcher : UserControl + { + public LauncherPageSwitcher() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Enter && DataContext is ViewModels.LauncherPageSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnItemDoubleTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.LauncherPageSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && PagesListBox.ItemCount > 0) + { + PagesListBox.Focus(NavigationMethod.Directional); + + if (PagesListBox.SelectedIndex < 0) + PagesListBox.SelectedIndex = 0; + else if (PagesListBox.SelectedIndex < PagesListBox.ItemCount) + PagesListBox.SelectedIndex++; + + e.Handled = true; + } + } + } +} diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index 40a2efe6..01711afd 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -4,11 +4,17 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" xmlns:c="using:SourceGit.Converters" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.LauncherTabBar" - x:DataType="vm:Launcher"> + x:DataType="vm:Launcher" + x:Name="ThisControl"> - + @@ -34,7 +40,7 @@ - + - - + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/LauncherTabBar.axaml.cs b/src/Views/LauncherTabBar.axaml.cs index 3258a09c..5bab6d80 100644 --- a/src/Views/LauncherTabBar.axaml.cs +++ b/src/Views/LauncherTabBar.axaml.cs @@ -1,6 +1,7 @@ using System; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -10,6 +11,29 @@ namespace SourceGit.Views { public partial class LauncherTabBar : UserControl { + public static readonly StyledProperty IsScrollerVisibleProperty = + AvaloniaProperty.Register(nameof(IsScrollerVisible)); + + public bool IsScrollerVisible + { + get => GetValue(IsScrollerVisibleProperty); + set => SetValue(IsScrollerVisibleProperty, value); + } + + public static readonly StyledProperty SearchFilterProperty = + AvaloniaProperty.Register(nameof(SearchFilter)); + + public string SearchFilter + { + get => GetValue(SearchFilterProperty); + set => SetValue(SearchFilterProperty, value); + } + + public AvaloniaList SelectablePages + { + get; + } = []; + public LauncherTabBar() { InitializeComponent(); @@ -43,6 +67,9 @@ namespace SourceGit.Views if (containerEndX < startX || containerEndX > endX) continue; + if (IsScrollerVisible && i == count - 1) + break; + var separatorX = containerEndX - startX + LauncherTabsScroller.Bounds.X; context.DrawLine(separatorPen, new Point(separatorX, separatorY), new Point(separatorX, separatorY + 20)); } @@ -71,7 +98,6 @@ namespace SourceGit.Views ctx.BeginFigure(new Point(x, y), true); y = 1; ctx.LineTo(new Point(x, y)); - x = drawRightX - 6; } else { @@ -85,10 +111,11 @@ namespace SourceGit.Views x += 6; y = 1; ctx.ArcTo(new Point(x, y), new Size(6, 6), angle, false, SweepDirection.Clockwise); - x = drawRightX - 6; } - if (drawRightX < LauncherTabsScroller.Bounds.Right) + x = drawRightX - 6; + + if (drawRightX <= LauncherTabsScroller.Bounds.Right) { ctx.LineTo(new Point(x, y)); x = drawRightX; @@ -114,6 +141,14 @@ namespace SourceGit.Views context.DrawGeometry(fill, stroke, geo); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SearchFilterProperty) + UpdateSelectablePages(); + } + private void ScrollTabs(object _, PointerWheelEventArgs e) { if (!e.KeyModifiers.HasFlag(KeyModifiers.Shift)) @@ -140,19 +175,7 @@ namespace SourceGit.Views private void OnTabsLayoutUpdated(object _1, EventArgs _2) { - if (LauncherTabsScroller.Extent.Width > LauncherTabsScroller.Viewport.Width) - { - LeftScrollIndicator.IsVisible = true; - LeftScrollIndicator.IsEnabled = LauncherTabsScroller.Offset.X > 0; - RightScrollIndicator.IsVisible = true; - RightScrollIndicator.IsEnabled = LauncherTabsScroller.Offset.X < LauncherTabsScroller.Extent.Width - LauncherTabsScroller.Viewport.Width; - } - else - { - LeftScrollIndicator.IsVisible = false; - RightScrollIndicator.IsVisible = false; - } - + SetCurrentValue(IsScrollerVisibleProperty, LauncherTabsScroller.Extent.Width > LauncherTabsScroller.Viewport.Width); InvalidateVisual(); } @@ -234,7 +257,7 @@ namespace SourceGit.Views if (sender is Border border && DataContext is ViewModels.Launcher vm) { var menu = vm.CreateContextForPageTab(border.DataContext as ViewModels.LauncherPage); - border.OpenContextMenu(menu); + menu?.Open(border); } e.Handled = true; @@ -248,6 +271,97 @@ namespace SourceGit.Views e.Handled = true; } + private void OnTabsDropdownOpened(object sender, EventArgs e) + { + UpdateSelectablePages(); + } + + private void OnTabsDropdownClosed(object sender, EventArgs e) + { + SelectablePages.Clear(); + SearchFilter = string.Empty; + } + + private void OnTabsDropdownKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + else if (e.Key == Key.Enter) + { + if (TabsDropdownList.SelectedItem is ViewModels.LauncherPage page && + DataContext is ViewModels.Launcher vm) + { + vm.ActivePage = page; + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + } + } + + private void OnTabsDropdownSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && TabsDropdownList.ItemCount > 0) + { + TabsDropdownList.Focus(NavigationMethod.Directional); + + if (TabsDropdownList.SelectedIndex < 0) + TabsDropdownList.SelectedIndex = 0; + else if (TabsDropdownList.SelectedIndex < TabsDropdownList.ItemCount) + TabsDropdownList.SelectedIndex++; + + e.Handled = true; + } + } + + private void OnTabsDropdownLostFocus(object sender, RoutedEventArgs e) + { + if (sender is Control { IsFocused: false, IsKeyboardFocusWithin: false }) + PageSelector.Flyout?.Hide(); + } + + private void OnClearSearchFilter(object sender, RoutedEventArgs e) + { + SearchFilter = string.Empty; + } + + private void OnTabsDropdownItemTapped(object sender, TappedEventArgs e) + { + if (sender is Control { DataContext: ViewModels.LauncherPage page } && + DataContext is ViewModels.Launcher vm) + { + vm.ActivePage = page; + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + } + + private void UpdateSelectablePages() + { + if (DataContext is not ViewModels.Launcher vm) + return; + + SelectablePages.Clear(); + + var pages = vm.Pages; + var filter = SearchFilter?.Trim() ?? ""; + if (string.IsNullOrEmpty(filter)) + { + SelectablePages.AddRange(pages); + return; + } + + foreach (var page in pages) + { + var node = page.Node; + if (node.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || + (node.IsRepository && node.Id.Contains(filter, StringComparison.OrdinalIgnoreCase))) + SelectablePages.Add(page); + } + } + private bool _pressedTab = false; private Point _pressedTabPosition = new Point(); private bool _startDragTab = false; diff --git a/src/Views/MenuItemExtension.cs b/src/Views/MenuItemExtension.cs new file mode 100644 index 00000000..1c23b2ea --- /dev/null +++ b/src/Views/MenuItemExtension.cs @@ -0,0 +1,12 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; + +namespace SourceGit.Views +{ + public class MenuItemExtension : AvaloniaObject + { + public static readonly AttachedProperty CommandProperty = + AvaloniaProperty.RegisterAttached("Command", string.Empty, false, BindingMode.OneWay); + } +} diff --git a/src/Views/Merge.axaml b/src/Views/Merge.axaml index b63bfc04..33d07f02 100644 --- a/src/Views/Merge.axaml +++ b/src/Views/Merge.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.Merge" x:DataType="vm:Merge"> @@ -11,15 +12,36 @@ - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + SelectedItem="{Binding Mode, Mode=TwoWay}" + Grid.IsSharedSizeScope="True"> - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/MergeMultiple.axaml b/src/Views/MergeMultiple.axaml new file mode 100644 index 00000000..332d9fef --- /dev/null +++ b/src/Views/MergeMultiple.axaml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/MergeMultiple.axaml.cs b/src/Views/MergeMultiple.axaml.cs new file mode 100644 index 00000000..c0997067 --- /dev/null +++ b/src/Views/MergeMultiple.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class MergeMultiple : UserControl + { + public MergeMultiple() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/MoveRepositoryNode.axaml.cs b/src/Views/MoveRepositoryNode.axaml.cs index 450c6924..494f4f30 100644 --- a/src/Views/MoveRepositoryNode.axaml.cs +++ b/src/Views/MoveRepositoryNode.axaml.cs @@ -10,5 +10,3 @@ namespace SourceGit.Views } } } - - diff --git a/src/Views/NameHighlightedTextBlock.cs b/src/Views/NameHighlightedTextBlock.cs index 82eb0827..49f245dd 100644 --- a/src/Views/NameHighlightedTextBlock.cs +++ b/src/Views/NameHighlightedTextBlock.cs @@ -49,21 +49,16 @@ namespace SourceGit.Views AffectsMeasure(TextProperty); } - public NameHighlightedTextBlock(string nameKey, params object[] args) - { - SetCurrentValue(TextProperty, App.Text(nameKey, args)); - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center; - } - protected override Size MeasureOverride(Size availableSize) { var text = Text; if (string.IsNullOrEmpty(text)) return base.MeasureOverride(availableSize); + var trimmed = text.Replace("$", ""); var typeface = new Typeface(FontFamily); var formatted = new FormattedText( - Text, + trimmed, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, @@ -100,20 +95,15 @@ namespace SourceGit.Views normalTypeface, FontSize, Foreground); + context.DrawText(formatted, new Point(offsetX, 0)); if (isName) { var lineY = formatted.Baseline + 2; - context.DrawText(formatted, new Point(offsetX, 0)); context.DrawLine(underlinePen, new Point(offsetX, lineY), new Point(offsetX + formatted.Width, lineY)); - offsetX += formatted.WidthIncludingTrailingWhitespace; - } - else - { - context.DrawText(formatted, new Point(offsetX, 0)); - offsetX += formatted.WidthIncludingTrailingWhitespace; } + offsetX += formatted.WidthIncludingTrailingWhitespace; isName = !isName; } } diff --git a/src/Views/PopupRunningStatus.axaml b/src/Views/PopupRunningStatus.axaml index 0522e46b..6a0dbdb4 100644 --- a/src/Views/PopupRunningStatus.axaml +++ b/src/Views/PopupRunningStatus.axaml @@ -9,19 +9,18 @@ x:Name="ThisControl"> - + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default - Dark - Light - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml new file mode 100644 index 00000000..beb228b6 --- /dev/null +++ b/src/Views/Preferences.axaml @@ -0,0 +1,784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + Dark + Light + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Preference.axaml.cs b/src/Views/Preferences.axaml.cs similarity index 58% rename from src/Views/Preference.axaml.cs rename to src/Views/Preferences.axaml.cs index 2f08e0db..6856fbce 100644 --- a/src/Views/Preference.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -3,13 +3,12 @@ using System.Collections.Generic; using Avalonia; using Avalonia.Controls; -using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Platform.Storage; namespace SourceGit.Views { - public partial class Preference : ChromelessWindow + public partial class Preferences : ChromelessWindow { public string DefaultUser { @@ -29,8 +28,14 @@ namespace SourceGit.Views set; } = null; + public bool EnablePruneOnFetch + { + get; + set; + } + public static readonly StyledProperty GitVersionProperty = - AvaloniaProperty.Register(nameof(GitVersion)); + AvaloniaProperty.Register(nameof(GitVersion)); public string GitVersion { @@ -38,6 +43,15 @@ namespace SourceGit.Views set => SetValue(GitVersionProperty, value); } + public static readonly StyledProperty ShowGitVersionWarningProperty = + AvaloniaProperty.Register(nameof(ShowGitVersionWarning)); + + public bool ShowGitVersionWarning + { + get => GetValue(ShowGitVersionWarningProperty); + set => SetValue(ShowGitVersionWarningProperty, value); + } + public bool EnableGPGCommitSigning { get; @@ -51,7 +65,7 @@ namespace SourceGit.Views } public static readonly StyledProperty GPGFormatProperty = - AvaloniaProperty.Register(nameof(GPGFormat), Models.GPGFormat.Supported[0]); + AvaloniaProperty.Register(nameof(GPGFormat), Models.GPGFormat.Supported[0]); public Models.GPGFormat GPGFormat { @@ -60,7 +74,7 @@ namespace SourceGit.Views } public static readonly StyledProperty GPGExecutableFileProperty = - AvaloniaProperty.Register(nameof(GPGExecutableFile)); + AvaloniaProperty.Register(nameof(GPGExecutableFile)); public string GPGExecutableFile { @@ -74,12 +88,35 @@ namespace SourceGit.Views set; } - public Preference() + public bool EnableHTTPSSLVerify { - var pref = ViewModels.Preference.Instance; + get; + set; + } = false; + + public static readonly StyledProperty SelectedOpenAIServiceProperty = + AvaloniaProperty.Register(nameof(SelectedOpenAIService)); + + public Models.OpenAIService SelectedOpenAIService + { + get => GetValue(SelectedOpenAIServiceProperty); + set => SetValue(SelectedOpenAIServiceProperty, value); + } + + public static readonly StyledProperty SelectedCustomActionProperty = + AvaloniaProperty.Register(nameof(SelectedCustomAction)); + + public Models.CustomAction SelectedCustomAction + { + get => GetValue(SelectedCustomActionProperty); + set => SetValue(SelectedCustomActionProperty, value); + } + + public Preferences() + { + var pref = ViewModels.Preferences.Instance; DataContext = pref; - var ver = string.Empty; if (pref.IsGitConfigured()) { var config = new Commands.Config(null).ListAll(); @@ -92,6 +129,8 @@ namespace SourceGit.Views GPGUserKey = signingKey; if (config.TryGetValue("core.autocrlf", out var crlf)) CRLFMode = Models.CRLFMode.Supported.Find(x => x.Value == crlf); + if (config.TryGetValue("fetch.prune", out var pruneOnFetch)) + EnablePruneOnFetch = (pruneOnFetch == "true"); if (config.TryGetValue("commit.gpgsign", out var gpgCommitSign)) EnableGPGCommitSigning = (gpgCommitSign == "true"); if (config.TryGetValue("tag.gpgsign", out var gpgTagSign)) @@ -104,11 +143,14 @@ namespace SourceGit.Views else if (config.TryGetValue($"gpg.{GPGFormat.Value}.program", out var gpgProgram)) GPGExecutableFile = gpgProgram; - ver = new Commands.Version().Query(); + if (config.TryGetValue("http.sslverify", out var sslVerify)) + EnableHTTPSSLVerify = sslVerify == "true"; + else + EnableHTTPSSLVerify = true; } + UpdateGitVersion(); InitializeComponent(); - GitVersion = ver; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -127,13 +169,20 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { + base.OnClosing(e); + + if (Design.IsDesignMode) + return; + var config = new Commands.Config(null).ListAll(); SetIfChanged(config, "user.name", DefaultUser, ""); SetIfChanged(config, "user.email", DefaultEmail, ""); SetIfChanged(config, "user.signingkey", GPGUserKey, ""); - SetIfChanged(config, "core.autocrlf", CRLFMode != null ? CRLFMode.Value : null, null); + SetIfChanged(config, "core.autocrlf", CRLFMode?.Value, null); + SetIfChanged(config, "fetch.prune", EnablePruneOnFetch ? "true" : "false", "false"); SetIfChanged(config, "commit.gpgsign", EnableGPGCommitSigning ? "true" : "false", "false"); SetIfChanged(config, "tag.gpgsign", EnableGPGTagSigning ? "true" : "false", "false"); + SetIfChanged(config, "http.sslverify", EnableHTTPSSLVerify ? "" : "false", ""); SetIfChanged(config, "gpg.format", GPGFormat.Value, "openpgp"); if (!GPGFormat.Value.Equals("ssh", StringComparison.Ordinal)) @@ -154,12 +203,7 @@ namespace SourceGit.Views new Commands.Config(null).Set($"gpg.{GPGFormat.Value}.program", GPGExecutableFile); } - base.OnClosing(e); - } - - private void BeginMoveWindow(object _, PointerPressedEventArgs e) - { - BeginMoveDrag(e); + ViewModels.Preferences.Instance.Save(); } private async void SelectThemeOverrideFile(object _, RoutedEventArgs e) @@ -173,7 +217,7 @@ namespace SourceGit.Views var selected = await StorageProvider.OpenFilePickerAsync(options); if (selected.Count == 1) { - ViewModels.Preference.Instance.ThemeOverrides = selected[0].Path.LocalPath; + ViewModels.Preferences.Instance.ThemeOverrides = selected[0].Path.LocalPath; } e.Handled = true; @@ -191,8 +235,8 @@ namespace SourceGit.Views var selected = await StorageProvider.OpenFilePickerAsync(options); if (selected.Count == 1) { - ViewModels.Preference.Instance.GitInstallPath = selected[0].Path.LocalPath; - GitVersion = new Commands.Version().Query(); + ViewModels.Preferences.Instance.GitInstallPath = selected[0].Path.LocalPath; + UpdateGitVersion(); } e.Handled = true; @@ -206,7 +250,9 @@ namespace SourceGit.Views var selected = await StorageProvider.OpenFolderPickerAsync(options); if (selected.Count == 1) { - ViewModels.Preference.Instance.GitDefaultCloneDir = selected[0].Path.LocalPath; + var folder = selected[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder?.Path.ToString(); + ViewModels.Preferences.Instance.GitDefaultCloneDir = folderPath; } } catch (Exception ex) @@ -242,7 +288,7 @@ namespace SourceGit.Views private async void SelectShellOrTerminal(object _, RoutedEventArgs e) { - var type = ViewModels.Preference.Instance.ShellOrTerminal; + var type = ViewModels.Preferences.Instance.ShellOrTerminal; if (type == -1) return; @@ -256,7 +302,7 @@ namespace SourceGit.Views var selected = await StorageProvider.OpenFilePickerAsync(options); if (selected.Count == 1) { - ViewModels.Preference.Instance.ShellOrTerminalPath = selected[0].Path.LocalPath; + ViewModels.Preferences.Instance.ShellOrTerminalPath = selected[0].Path.LocalPath; } e.Handled = true; @@ -264,10 +310,10 @@ namespace SourceGit.Views private async void SelectExternalMergeTool(object _, RoutedEventArgs e) { - var type = ViewModels.Preference.Instance.ExternalMergeToolType; + var type = ViewModels.Preferences.Instance.ExternalMergeToolType; if (type < 0 || type >= Models.ExternalMerger.Supported.Count) { - ViewModels.Preference.Instance.ExternalMergeToolType = 0; + ViewModels.Preferences.Instance.ExternalMergeToolType = 0; e.Handled = true; return; } @@ -282,7 +328,7 @@ namespace SourceGit.Views var selected = await StorageProvider.OpenFilePickerAsync(options); if (selected.Count == 1) { - ViewModels.Preference.Instance.ExternalMergeToolPath = selected[0].Path.LocalPath; + ViewModels.Preferences.Instance.ExternalMergeToolPath = selected[0].Path.LocalPath; } e.Handled = true; @@ -304,13 +350,99 @@ namespace SourceGit.Views { if (sender is CheckBox box) { - ViewModels.Preference.Instance.UseSystemWindowFrame = box.IsChecked == true; - - var dialog = new ConfirmRestart(); - App.OpenDialog(dialog); + ViewModels.Preferences.Instance.UseSystemWindowFrame = box.IsChecked == true; + App.ShowWindow(new ConfirmRestart(), true); } e.Handled = true; } + + private void OnGitInstallPathChanged(object sender, TextChangedEventArgs e) + { + UpdateGitVersion(); + } + + private void OnAddOpenAIService(object sender, RoutedEventArgs e) + { + var service = new Models.OpenAIService() { Name = "Unnamed Service" }; + ViewModels.Preferences.Instance.OpenAIServices.Add(service); + SelectedOpenAIService = service; + + e.Handled = true; + } + + private void OnRemoveSelectedOpenAIService(object sender, RoutedEventArgs e) + { + if (SelectedOpenAIService == null) + return; + + ViewModels.Preferences.Instance.OpenAIServices.Remove(SelectedOpenAIService); + SelectedOpenAIService = null; + e.Handled = true; + } + + private void OnAddCustomAction(object sender, RoutedEventArgs e) + { + var action = new Models.CustomAction() { Name = "Unnamed Action (Global)" }; + ViewModels.Preferences.Instance.CustomActions.Add(action); + SelectedCustomAction = action; + + e.Handled = true; + } + + private async void SelectExecutableForCustomAction(object sender, RoutedEventArgs e) + { + var options = new FilePickerOpenOptions() + { + FileTypeFilter = [new FilePickerFileType("Executable file(script)") { Patterns = ["*.*"] }], + AllowMultiple = false, + }; + + var selected = await StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1 && sender is Button { DataContext: Models.CustomAction action }) + action.Executable = selected[0].Path.LocalPath; + + e.Handled = true; + } + + private void OnRemoveSelectedCustomAction(object sender, RoutedEventArgs e) + { + if (SelectedCustomAction == null) + return; + + ViewModels.Preferences.Instance.CustomActions.Remove(SelectedCustomAction); + SelectedCustomAction = null; + e.Handled = true; + } + + private void OnMoveSelectedCustomActionUp(object sender, RoutedEventArgs e) + { + if (SelectedCustomAction == null) + return; + + var idx = ViewModels.Preferences.Instance.CustomActions.IndexOf(SelectedCustomAction); + if (idx > 0) + ViewModels.Preferences.Instance.CustomActions.Move(idx - 1, idx); + + e.Handled = true; + } + + private void OnMoveSelectedCustomActionDown(object sender, RoutedEventArgs e) + { + if (SelectedCustomAction == null) + return; + + var idx = ViewModels.Preferences.Instance.CustomActions.IndexOf(SelectedCustomAction); + if (idx < ViewModels.Preferences.Instance.CustomActions.Count - 1) + ViewModels.Preferences.Instance.CustomActions.Move(idx + 1, idx); + + e.Handled = true; + } + + private void UpdateGitVersion() + { + GitVersion = Native.OS.GitVersionString; + ShowGitVersionWarning = !string.IsNullOrEmpty(GitVersion) && Native.OS.GitVersion < Models.GitVersions.MINIMAL; + } } } diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index a23bb5d5..96d308b4 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -19,10 +19,9 @@ - - + - + - + + + + + + + - - - - - - - + Content="{DynamicResource Text.Pull.LocalChanges.StashAndReply}" + IsChecked="{Binding !DiscardLocalChanges, Mode=TwoWay}"/> + - + + Content="{DynamicResource Text.Pull.UseRebase}" + IsChecked="{Binding UseRebase, Mode=TwoWay}" + ToolTip.Tip="--rebase"/> - - + Height="32" + Content="{DynamicResource Text.Pull.RecurseSubmodules}" + IsChecked="{Binding RecurseSubmodules, Mode=TwoWay}" + IsVisible="{Binding IsRecurseSubmoduleVisible}" + ToolTip.Tip="--recurse-submodules"/> diff --git a/src/Views/Push.axaml b/src/Views/Push.axaml index 87921c88..743606ee 100644 --- a/src/Views/Push.axaml +++ b/src/Views/Push.axaml @@ -22,6 +22,7 @@ VerticalAlignment="Center" HorizontalAlignment="Stretch" ItemsSource="{Binding LocalBranches}" SelectedItem="{Binding SelectedLocalBranch, Mode=TwoWay}" + IsTextSearchEnabled="True" IsEnabled="{Binding !HasSpecifiedLocalBranch}"> @@ -31,6 +32,12 @@ + + + + + + @@ -68,8 +76,8 @@ @@ -77,27 +85,37 @@ + + + + + + + IsVisible="{Binding IsSetTrackOptionVisible}" + ToolTip.Tip="-u"/> + IsVisible="{Binding IsCheckSubmodulesVisible}" + ToolTip.Tip="--recurse-submodules=check"/> + IsChecked="{Binding PushAllTags, Mode=TwoWay}" + ToolTip.Tip="--tags"/> + IsChecked="{Binding ForcePush, Mode=TwoWay}" + ToolTip.Tip="--force-with-lease"/> diff --git a/src/Views/Rebase.axaml b/src/Views/Rebase.axaml index 166e9e32..91c1fab2 100644 --- a/src/Views/Rebase.axaml +++ b/src/Views/Rebase.axaml @@ -37,7 +37,7 @@ - + diff --git a/src/Views/RemoveWorktree.axaml b/src/Views/RemoveWorktree.axaml index 6d7ea914..736e6e40 100644 --- a/src/Views/RemoveWorktree.axaml +++ b/src/Views/RemoveWorktree.axaml @@ -18,8 +18,8 @@ - - + + diff --git a/src/Views/RenameBranch.axaml b/src/Views/RenameBranch.axaml index 59a849fe..efbbf323 100644 --- a/src/Views/RenameBranch.axaml +++ b/src/Views/RenameBranch.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.RenameBranch" x:DataType="vm:RenameBranch"> @@ -11,7 +12,7 @@ - + + + + + + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index bb34a821..f1f3fccc 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -11,7 +11,7 @@ x:DataType="vm:Repository"> - + @@ -40,8 +40,18 @@ - + + + + + @@ -67,37 +83,55 @@ - - - + + + + - - + + + + + + + - - - - + + + + + + - - - - + + + + + - + + VerticalContentAlignment="Center"> - + - + + + + + + - + - - + @@ -197,20 +283,33 @@ - + - - - + + + + - + @@ -221,8 +320,7 @@ Height="0" Margin="8,0,4,0" Background="Transparent" - ShowTagsAsTree="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowTagsAsTree, Mode=OneWay}" - Tags="{Binding VisibleTags}" + Content="{Binding VisibleTags}" Focusable="False" IsVisible="{Binding IsTagGroupExpanded, Mode=OneWay}" SelectionChanged="OnTagsSelectionChanged" @@ -230,10 +328,18 @@ - + - - + + + + + - - + - - - + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 499f5e62..8196b72f 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -134,7 +134,7 @@ namespace SourceGit.Views } else if (e.Key == Key.Down) { - if (repo.IsSearchCommitSuggestionOpen) + if (repo.MatchedFilesForSearching is { Count: > 0 }) { SearchSuggestionBox.Focus(NavigationMethod.Tab); SearchSuggestionBox.SelectedIndex = 0; @@ -144,12 +144,7 @@ namespace SourceGit.Views } else if (e.Key == Key.Escape) { - if (repo.IsSearchCommitSuggestionOpen) - { - repo.SearchCommitFilterSuggestion.Clear(); - repo.IsSearchCommitSuggestionOpen = false; - } - + repo.ClearMatchedFilesForSearching(); e.Handled = true; } } @@ -184,24 +179,9 @@ namespace SourceGit.Views RemoteBranchTree.UnselectAll(); } - private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e) + private void OnSubmodulesRowsChanged(object _, RoutedEventArgs e) { - if (sender is ListBox { SelectedItem: Models.Submodule submodule } grid && DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForSubmodule(submodule.Path); - grid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e) - { - if (sender is ListBox { SelectedItem: Models.Submodule submodule } && DataContext is ViewModels.Repository repo) - { - repo.OpenSubmodule(submodule.Path); - } - + UpdateLeftSidebarLayout(); e.Handled = true; } @@ -210,7 +190,7 @@ namespace SourceGit.Views if (sender is ListBox { SelectedItem: Models.Worktree worktree } grid && DataContext is ViewModels.Repository repo) { var menu = repo.CreateContextMenuForWorktree(worktree); - grid.OpenContextMenu(menu); + menu?.Open(grid); } e.Handled = true; @@ -226,9 +206,9 @@ namespace SourceGit.Views e.Handled = true; } - private void OnLeftSidebarListBoxPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e) + private void OnWorktreeListPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e) { - if (e.Property == ListBox.ItemsSourceProperty || e.Property == ListBox.IsVisibleProperty) + if (e.Property == ItemsControl.ItemsSourceProperty || e.Property == IsVisibleProperty) UpdateLeftSidebarLayout(); } @@ -241,37 +221,40 @@ namespace SourceGit.Views private void UpdateLeftSidebarLayout() { var vm = DataContext as ViewModels.Repository; - if (vm == null || vm.Settings == null) + if (vm?.Settings == null) return; if (!IsLoaded) return; var leftHeight = LeftSidebarGroups.Bounds.Height - 28.0 * 5 - 4; + if (leftHeight <= 0) + return; + var localBranchRows = vm.IsLocalBranchGroupExpanded ? LocalBranchTree.Rows.Count : 0; var remoteBranchRows = vm.IsRemoteGroupExpanded ? RemoteBranchTree.Rows.Count : 0; var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; var desiredTag = vm.IsTagGroupExpanded ? 24.0 * TagsList.Rows : 0; - var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? 24.0 * vm.VisibleSubmodules.Count : 0; + var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? 24.0 * SubmoduleList.Rows : 0; var desiredWorktree = vm.IsWorktreeGroupExpanded ? 24.0 * vm.Worktrees.Count : 0; var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree; var hasOverflow = (desiredBranches + desiredOthers > leftHeight); - if (vm.IsTagGroupExpanded) + if (vm.IsWorktreeGroupExpanded) { - var height = desiredTag; + var height = desiredWorktree; if (hasOverflow) { - var test = leftHeight - desiredBranches - desiredSubmodule - desiredWorktree; + var test = leftHeight - desiredBranches - desiredTag - desiredSubmodule; if (test < 0) - height = Math.Min(200, height); + height = Math.Min(120, height); else - height = Math.Max(200, test); + height = Math.Max(120, test); } leftHeight -= height; - TagsList.Height = height; - hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight; + WorktreeList.Height = height; + hasOverflow = (desiredBranches + desiredTag + desiredSubmodule) > leftHeight; } if (vm.IsSubmoduleGroupExpanded) @@ -279,35 +262,35 @@ namespace SourceGit.Views var height = desiredSubmodule; if (hasOverflow) { - var test = leftHeight - desiredBranches - desiredWorktree; + var test = leftHeight - desiredBranches - desiredTag; if (test < 0) - height = Math.Min(200, height); + height = Math.Min(120, height); else - height = Math.Max(200, test); + height = Math.Max(120, test); } leftHeight -= height; SubmoduleList.Height = height; - hasOverflow = (desiredBranches + desiredWorktree) > leftHeight; + hasOverflow = (desiredBranches + desiredTag) > leftHeight; } - if (vm.IsWorktreeGroupExpanded) + if (vm.IsTagGroupExpanded) { - var height = desiredWorktree; + var height = desiredTag; if (hasOverflow) { var test = leftHeight - desiredBranches; if (test < 0) - height = Math.Min(200, height); + height = Math.Min(120, height); else - height = Math.Max(200, test); + height = Math.Max(120, test); } leftHeight -= height; - WorktreeList.Height = height; + TagsList.Height = height; } - if (desiredBranches > leftHeight) + if (leftHeight > 0 && desiredBranches > leftHeight) { var local = localBranchRows * 24.0; var remote = remoteBranchRows * 24.0; @@ -366,9 +349,7 @@ namespace SourceGit.Views if (e.Key == Key.Escape) { - repo.IsSearchCommitSuggestionOpen = false; - repo.SearchCommitFilterSuggestion.Clear(); - + repo.ClearMatchedFilesForSearching(); e.Handled = true; } else if (e.Key == Key.Enter && SearchSuggestionBox.SelectedItem is string content) @@ -395,5 +376,75 @@ namespace SourceGit.Views } e.Handled = true; } + + private void OnOpenAdvancedHistoriesOption(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForHistoriesPage(); + menu?.Open(button); + } + + e.Handled = true; + } + + private void OnOpenSortLocalBranchMenu(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForBranchSortMode(true); + menu?.Open(button); + } + + e.Handled = true; + } + + private void OnOpenSortRemoteBranchMenu(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForBranchSortMode(false); + menu?.Open(button); + } + + e.Handled = true; + } + + private void OnOpenSortTagMenu(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForTagSortMode(); + menu?.Open(button); + } + + e.Handled = true; + } + + private void OnSkipInProgress(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + repo.SkipMerge(); + + e.Handled = true; + } + + private void OnRemoveSelectedHistoriesFilter(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo && sender is Button { DataContext: Models.Filter filter }) + repo.RemoveHistoriesFilter(filter); + + e.Handled = true; + } + + private void OnBisectCommand(object sender, RoutedEventArgs e) + { + if (sender is Button button && + DataContext is ViewModels.Repository { IsBisectCommandRunning: false } repo && + repo.CanCreatePopup()) + repo.Bisect(button.Tag as string); + + e.Handled = true; + } } } diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index f167b8f0..5ded6f5c 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -16,30 +16,23 @@ WindowStartupLocation="CenterOwner"> - - + - - - - - @@ -51,7 +44,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - - + + + @@ -153,7 +186,7 @@ - + @@ -225,9 +258,9 @@ - + @@ -247,9 +280,9 @@ BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" Background="{DynamicResource Brush.Contents}"> - @@ -259,7 +292,7 @@ - + @@ -268,7 +301,10 @@ - + + + + @@ -283,6 +319,11 @@ + + + + + @@ -307,11 +348,11 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Fill="{DynamicResource Brush.FG2}" - Data="{StaticResource Icons.Issue}"/> + Data="{StaticResource Icons.Empty}"/> - + @@ -334,6 +375,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/RepositoryConfigure.axaml.cs b/src/Views/RepositoryConfigure.axaml.cs index 7e559cc2..2c80dd45 100644 --- a/src/Views/RepositoryConfigure.axaml.cs +++ b/src/Views/RepositoryConfigure.axaml.cs @@ -1,5 +1,7 @@ using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; namespace SourceGit.Views { @@ -12,13 +14,33 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { - (DataContext as ViewModels.RepositoryConfigure)?.Save(); base.OnClosing(e); + + if (!Design.IsDesignMode && DataContext is ViewModels.RepositoryConfigure configure) + configure.Save(); } - private void BeginMoveWindow(object _, PointerPressedEventArgs e) + private async void SelectExecutableForCustomAction(object sender, RoutedEventArgs e) { - BeginMoveDrag(e); + var options = new FilePickerOpenOptions() + { + FileTypeFilter = [new FilePickerFileType("Executable file(script)") { Patterns = ["*.*"] }], + AllowMultiple = false, + }; + + var selected = await StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1 && sender is Button { DataContext: Models.CustomAction action }) + action.Executable = selected[0].Path.LocalPath; + + e.Handled = true; + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (!e.Handled && e.Key == Key.Escape) + Close(); } } } diff --git a/src/Views/RepositoryToolbar.axaml b/src/Views/RepositoryToolbar.axaml index b76cfd63..08cfd308 100644 --- a/src/Views/RepositoryToolbar.axaml +++ b/src/Views/RepositoryToolbar.axaml @@ -8,124 +8,127 @@ x:Class="SourceGit.Views.RepositoryToolbar" x:DataType="vm:Repository"> - - + + - - - + - + - - + + - - + + + + - + + + - + - + + + - + - - + - - + - - - - - - - - + - - - - - - - - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/src/Views/RepositoryToolbar.axaml.cs b/src/Views/RepositoryToolbar.axaml.cs index 27ac43cd..cbd0041a 100644 --- a/src/Views/RepositoryToolbar.axaml.cs +++ b/src/Views/RepositoryToolbar.axaml.cs @@ -17,27 +17,25 @@ namespace SourceGit.Views if (sender is Button button && DataContext is ViewModels.Repository repo) { var menu = repo.CreateContextMenuForExternalTools(); - button.OpenContextMenu(menu); + menu?.Open(button); e.Handled = true; } } - private async void OpenStatistics(object _, RoutedEventArgs e) + private void OpenStatistics(object _, RoutedEventArgs e) { - if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner) + if (DataContext is ViewModels.Repository repo) { - var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; - await dialog.ShowDialog(owner); + App.ShowWindow(new ViewModels.Statistics(repo.FullPath), true); e.Handled = true; } } - private async void OpenConfigure(object sender, RoutedEventArgs e) + private void OpenConfigure(object sender, RoutedEventArgs e) { - if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner) + if (DataContext is ViewModels.Repository repo) { - var dialog = new RepositoryConfigure() { DataContext = new ViewModels.RepositoryConfigure(repo) }; - await dialog.ShowDialog(owner); + App.ShowWindow(new ViewModels.RepositoryConfigure(repo), true); e.Handled = true; } } @@ -45,37 +43,63 @@ 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; + if (launcher is not null && DataContext is ViewModels.Repository repo) + { + var startDirectly = launcher.HasKeyModifier(KeyModifiers.Control); + launcher.ClearKeyModifier(); + repo.Fetch(startDirectly); + 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; + if (launcher is not null && DataContext is ViewModels.Repository repo) + { + if (repo.IsBare) + { + App.RaiseException(repo.FullPath, "Can't run `git pull` in bare repository!"); + return; + } + + var startDirectly = launcher.HasKeyModifier(KeyModifiers.Control); + launcher.ClearKeyModifier(); + repo.Pull(startDirectly); + 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; + if (launcher is not null && DataContext is ViewModels.Repository repo) + { + var startDirectly = launcher.HasKeyModifier(KeyModifiers.Control); + launcher.ClearKeyModifier(); + repo.Push(startDirectly); + 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; + if (launcher is not null && DataContext is ViewModels.Repository repo) + { + var startDirectly = launcher.HasKeyModifier(KeyModifiers.Control); + launcher.ClearKeyModifier(); + repo.StashAll(startDirectly); + e.Handled = true; + } } private void OpenGitFlowMenu(object sender, RoutedEventArgs e) { - if (DataContext is ViewModels.Repository repo) + if (DataContext is ViewModels.Repository repo && sender is Control control) { var menu = repo.CreateContextMenuForGitFlow(); - (sender as Control)?.OpenContextMenu(menu); + menu?.Open(control); } e.Handled = true; @@ -83,14 +107,59 @@ namespace SourceGit.Views private void OpenGitLFSMenu(object sender, RoutedEventArgs e) { - if (DataContext is ViewModels.Repository repo) + if (DataContext is ViewModels.Repository repo && sender is Control control) { var menu = repo.CreateContextMenuForGitLFS(); - (sender as Control)?.OpenContextMenu(menu); + menu?.Open(control); } e.Handled = true; } + + private void StartBisect(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository { IsBisectCommandRunning: false } repo && + repo.InProgressContext == null && + repo.CanCreatePopup()) + { + if (repo.LocalChangesCount > 0) + App.RaiseException(repo.FullPath, "You have un-committed local changes. Please discard or stash them first."); + else if (repo.IsBisectCommandRunning || repo.BisectState != Models.BisectState.None) + App.RaiseException(repo.FullPath, "Bisect is running! Please abort it before starting a new one."); + else + repo.Bisect("start"); + } + + e.Handled = true; + } + + private void OpenCustomActionMenu(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo && sender is Control control) + { + var menu = repo.CreateContextMenuForCustomAction(); + menu?.Open(control); + } + + e.Handled = true; + } + + private void OpenGitLogs(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + App.ShowWindow(new ViewModels.ViewLogs(repo), true); + e.Handled = true; + } + } + + private void NavigateToHead(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository { CurrentBranch: { } } repo) + { + repo.NavigateToCommit(repo.CurrentBranch.Head); + e.Handled = true; + } + } } } - diff --git a/src/Views/Reset.axaml b/src/Views/Reset.axaml index c703ac1d..bce9d747 100644 --- a/src/Views/Reset.axaml +++ b/src/Views/Reset.axaml @@ -27,7 +27,7 @@ Margin="0,0,8,0" Text="{DynamicResource Text.Reset.MoveTo}"/> - + @@ -37,19 +37,32 @@ Margin="0,0,8,0" Text="{DynamicResource Text.Reset.Mode}"/> + SelectedItem="{Binding SelectedMode, Mode=TwoWay}" + KeyDown="OnResetModeKeyDown"> - + - - + + + + + + + + + + + + + diff --git a/src/Views/Reset.axaml.cs b/src/Views/Reset.axaml.cs index cc4b9b58..8c380538 100644 --- a/src/Views/Reset.axaml.cs +++ b/src/Views/Reset.axaml.cs @@ -1,4 +1,6 @@ using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; namespace SourceGit.Views { @@ -8,5 +10,29 @@ namespace SourceGit.Views { InitializeComponent(); } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + ResetMode.Focus(); + } + + private void OnResetModeKeyDown(object sender, KeyEventArgs e) + { + if (sender is ComboBox comboBox) + { + var key = e.Key.ToString(); + for (int i = 0; i < Models.ResetMode.Supported.Length; i++) + { + if (key.Equals(Models.ResetMode.Supported[i].Key, System.StringComparison.OrdinalIgnoreCase)) + { + comboBox.SelectedIndex = i; + e.Handled = true; + return; + } + } + } + } } } diff --git a/src/Views/ResetWithoutCheckout.axaml b/src/Views/ResetWithoutCheckout.axaml new file mode 100644 index 00000000..3808a8dd --- /dev/null +++ b/src/Views/ResetWithoutCheckout.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/ResetWithoutCheckout.axaml.cs b/src/Views/ResetWithoutCheckout.axaml.cs new file mode 100644 index 00000000..9280c070 --- /dev/null +++ b/src/Views/ResetWithoutCheckout.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class ResetWithoutCheckout : UserControl + { + public ResetWithoutCheckout() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/Revert.axaml b/src/Views/Revert.axaml index 294112b2..cafe1725 100644 --- a/src/Views/Revert.axaml +++ b/src/Views/Revert.axaml @@ -18,7 +18,7 @@ Text="{DynamicResource Text.Revert.Commit}"/> - + diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index f6303b45..6367c866 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -15,7 +15,7 @@ - + @@ -28,28 +28,33 @@ - + - - + + - + - + - + - + + + + @@ -86,14 +91,15 @@ + ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=CommitChangeViewMode, Mode=TwoWay}"/> - diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index e3ecb2b7..2c548240 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -1,5 +1,7 @@ using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; namespace SourceGit.Views { @@ -15,7 +17,7 @@ namespace SourceGit.Views if (DataContext is ViewModels.RevisionCompare vm && sender is ChangeCollectionView view) { var menu = vm.CreateChangeContextMenu(); - view.OpenContextMenu(menu); + menu?.Open(view); } e.Handled = true; @@ -28,5 +30,27 @@ namespace SourceGit.Views e.Handled = true; } + + private async void OnSaveAsPatch(object sender, RoutedEventArgs e) + { + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel == null) + return; + + var vm = DataContext as ViewModels.RevisionCompare; + if (vm == null) + return; + + var options = new FilePickerSaveOptions(); + options.Title = App.Text("FileCM.SaveAsPatch"); + options.DefaultExtension = ".patch"; + options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; + + var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); + if (storageFile != null) + vm.SaveAsPatch(storageFile.Path.LocalPath); + + e.Handled = true; + } } } diff --git a/src/Views/RevisionFileContentViewer.axaml b/src/Views/RevisionFileContentViewer.axaml index 2084a8b2..3e8362c9 100644 --- a/src/Views/RevisionFileContentViewer.axaml +++ b/src/Views/RevisionFileContentViewer.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:v="using:SourceGit.Views" + xmlns:vm="using:SourceGit.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.RevisionFileContentViewer"> @@ -19,24 +20,28 @@ - + - + - + - + - + @@ -60,10 +65,35 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/src/Views/RevisionFileContentViewer.axaml.cs b/src/Views/RevisionFileContentViewer.axaml.cs index bca6a082..5e9d5437 100644 --- a/src/Views/RevisionFileContentViewer.axaml.cs +++ b/src/Views/RevisionFileContentViewer.axaml.cs @@ -1,7 +1,160 @@ +using System; + +using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Media; + +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using AvaloniaEdit.TextMate; namespace SourceGit.Views { + public class RevisionTextFileView : TextEditor + { + public static readonly StyledProperty TabWidthProperty = + AvaloniaProperty.Register(nameof(TabWidth), 4); + + public int TabWidth + { + get => GetValue(TabWidthProperty); + set => SetValue(TabWidthProperty, value); + } + + public static readonly StyledProperty UseSyntaxHighlightingProperty = + AvaloniaProperty.Register(nameof(UseSyntaxHighlighting)); + + public bool UseSyntaxHighlighting + { + get => GetValue(UseSyntaxHighlightingProperty); + set => SetValue(UseSyntaxHighlightingProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextEditor); + + public RevisionTextFileView() : base(new TextArea(), new TextDocument()) + { + IsReadOnly = true; + ShowLineNumbers = true; + WordWrap = false; + + Options.IndentationSize = TabWidth; + Options.EnableHyperlinks = false; + Options.EnableEmailHyperlinks = false; + + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + + TextArea.LeftMargins[0].Margin = new Thickness(8, 0); + TextArea.TextView.Margin = new Thickness(4, 0); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + TextArea.TextView.ContextRequested += OnTextViewContextRequested; + UpdateTextMate(); + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + + TextArea.TextView.ContextRequested -= OnTextViewContextRequested; + + if (_textMate != null) + { + _textMate.Dispose(); + _textMate = null; + } + + GC.Collect(); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + if (DataContext is Models.RevisionTextFile source) + { + Text = source.Content; + Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName); + ScrollToHome(); + } + else + { + Text = string.Empty; + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == TabWidthProperty) + Options.IndentationSize = TabWidth; + else if (change.Property == UseSyntaxHighlightingProperty) + UpdateTextMate(); + } + + private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) + { + var selected = SelectedText; + if (string.IsNullOrEmpty(selected)) + return; + + var copy = new MenuItem() { Header = App.Text("Copy") }; + copy.Click += (_, ev) => + { + App.CopyText(selected); + ev.Handled = true; + }; + + if (this.FindResource("Icons.Copy") is Geometry geo) + { + copy.Icon = new Avalonia.Controls.Shapes.Path() + { + Width = 10, + Height = 10, + Stretch = Stretch.Uniform, + Data = geo, + }; + } + + var menu = new ContextMenu(); + menu.Items.Add(copy); + menu.Open(TextArea.TextView); + + e.Handled = true; + } + + private void UpdateTextMate() + { + if (UseSyntaxHighlighting) + { + if (_textMate == null) + _textMate = Models.TextMateHelper.CreateForEditor(this); + + if (DataContext is Models.RevisionTextFile file) + Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName); + } + else if (_textMate != null) + { + _textMate.Dispose(); + _textMate = null; + GC.Collect(); + + TextArea.TextView.Redraw(); + } + } + + private TextMate.Installation _textMate = null; + } + public partial class RevisionFileContentViewer : UserControl { public RevisionFileContentViewer() @@ -10,4 +163,3 @@ namespace SourceGit.Views } } } - diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs index a6e8df41..569e121f 100644 --- a/src/Views/RevisionFileTreeView.axaml.cs +++ b/src/Views/RevisionFileTreeView.axaml.cs @@ -144,6 +144,68 @@ namespace SourceGit.Views InitializeComponent(); } + public void SetSearchResult(string file) + { + _rows.Clear(); + _searchResult.Clear(); + + var rows = new List(); + if (string.IsNullOrEmpty(file)) + { + MakeRows(rows, _tree, 0); + } + else + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm?.Commit == null) + return; + + var objects = vm.GetRevisionFilesUnderFolder(file); + if (objects == null || objects.Count != 1) + return; + + var routes = file.Split('/', StringSplitOptions.None); + if (routes.Length == 1) + { + _searchResult.Add(new ViewModels.RevisionFileTreeNode + { + Backend = objects[0] + }); + } + else + { + var last = _searchResult; + var prefix = string.Empty; + for (var i = 0; i < routes.Length - 1; i++) + { + var folder = new ViewModels.RevisionFileTreeNode + { + Backend = new Models.Object + { + Type = Models.ObjectType.Tree, + Path = prefix + routes[i], + }, + IsExpanded = true, + }; + + last.Add(folder); + last = folder.Children; + prefix = folder.Backend + "/"; + } + + last.Add(new ViewModels.RevisionFileTreeNode + { + Backend = objects[0] + }); + } + + MakeRows(rows, _searchResult, 0); + } + + _rows.AddRange(rows); + GC.Collect(); + } + public void ToggleNodeIsExpanded(ViewModels.RevisionFileTreeNode node) { _disableSelectionChangingEvent = true; @@ -189,9 +251,10 @@ namespace SourceGit.Views { _tree.Clear(); _rows.Clear(); + _searchResult.Clear(); var vm = DataContext as ViewModels.CommitDetail; - if (vm == null || vm.Commit == null) + if (vm?.Commit == null) { GC.Collect(); return; @@ -207,12 +270,7 @@ namespace SourceGit.Views foreach (var obj in objects) _tree.Add(new ViewModels.RevisionFileTreeNode { Backend = obj }); - _tree.Sort((l, r) => - { - if (l.IsFolder == r.IsFolder) - return string.Compare(l.Name, r.Name, StringComparison.Ordinal); - return l.IsFolder ? -1 : 1; - }); + SortNodes(_tree); var topTree = new List(); MakeRows(topTree, _tree, 0); @@ -229,7 +287,7 @@ namespace SourceGit.Views if (obj.Type != Models.ObjectType.Tree) { var menu = vm.CreateRevisionFileContextMenu(obj); - grid.OpenContextMenu(menu); + menu?.Open(grid); } } @@ -250,16 +308,13 @@ namespace SourceGit.Views private void OnRowsSelectionChanged(object sender, SelectionChangedEventArgs _) { - if (_disableSelectionChangingEvent) + if (_disableSelectionChangingEvent || DataContext is not ViewModels.CommitDetail vm) return; - if (sender is ListBox { SelectedItem: ViewModels.RevisionFileTreeNode node } && DataContext is ViewModels.CommitDetail vm) - { - if (!node.IsFolder) - vm.ViewRevisionFile(node.Backend); - else - vm.ViewRevisionFile(null); - } + if (sender is ListBox { SelectedItem: ViewModels.RevisionFileTreeNode { IsFolder: false } node }) + vm.ViewRevisionFile(node.Backend); + else + vm.ViewRevisionFile(null); } private List GetChildrenOfTreeNode(ViewModels.RevisionFileTreeNode node) @@ -281,13 +336,7 @@ namespace SourceGit.Views foreach (var obj in objects) node.Children.Add(new ViewModels.RevisionFileTreeNode() { Backend = obj }); - node.Children.Sort((l, r) => - { - if (l.IsFolder == r.IsFolder) - return string.Compare(l.Name, r.Name, StringComparison.Ordinal); - return l.IsFolder ? -1 : 1; - }); - + SortNodes(node.Children); return node.Children; } @@ -305,8 +354,19 @@ namespace SourceGit.Views } } + private void SortNodes(List nodes) + { + nodes.Sort((l, r) => + { + if (l.IsFolder == r.IsFolder) + return Models.NumericSort.Compare(l.Name, r.Name); + return l.IsFolder ? -1 : 1; + }); + } + private List _tree = []; private AvaloniaList _rows = []; private bool _disableSelectionChangingEvent = false; + private List _searchResult = []; } } diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index d0b20963..5b512060 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -2,32 +2,151 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.RevisionFiles" x:DataType="vm:CommitDetail"> - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index b76e1360..3208fbb8 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -1,122 +1,84 @@ -using System; - -using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using Avalonia.Media; - -using AvaloniaEdit; -using AvaloniaEdit.Document; -using AvaloniaEdit.Editing; -using AvaloniaEdit.TextMate; +using Avalonia.Input; namespace SourceGit.Views { - public class RevisionTextFileView : TextEditor - { - protected override Type StyleKeyOverride => typeof(TextEditor); - - public RevisionTextFileView() : base(new TextArea(), new TextDocument()) - { - IsReadOnly = true; - ShowLineNumbers = true; - WordWrap = false; - HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; - VerticalScrollBarVisibility = ScrollBarVisibility.Auto; - - TextArea.LeftMargins[0].Margin = new Thickness(8, 0); - TextArea.TextView.Margin = new Thickness(4, 0); - TextArea.TextView.Options.EnableHyperlinks = false; - TextArea.TextView.Options.EnableEmailHyperlinks = false; - } - - protected override void OnLoaded(RoutedEventArgs e) - { - base.OnLoaded(e); - - TextArea.TextView.ContextRequested += OnTextViewContextRequested; - UpdateTextMate(); - } - - protected override void OnUnloaded(RoutedEventArgs e) - { - base.OnUnloaded(e); - - TextArea.TextView.ContextRequested -= OnTextViewContextRequested; - - if (_textMate != null) - { - _textMate.Dispose(); - _textMate = null; - } - - GC.Collect(); - } - - protected override void OnDataContextChanged(EventArgs e) - { - base.OnDataContextChanged(e); - - if (DataContext is Models.RevisionTextFile source) - { - UpdateTextMate(); - Text = source.Content; - } - else - { - Text = string.Empty; - } - } - - private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) - { - var selected = SelectedText; - if (string.IsNullOrEmpty(selected)) - return; - - var copy = new MenuItem() { Header = App.Text("Copy") }; - copy.Click += (_, ev) => - { - App.CopyText(selected); - ev.Handled = true; - }; - - if (this.FindResource("Icons.Copy") is Geometry geo) - { - copy.Icon = new Avalonia.Controls.Shapes.Path() - { - Width = 10, - Height = 10, - Stretch = Stretch.Uniform, - Data = geo, - }; - } - - var menu = new ContextMenu(); - menu.Items.Add(copy); - - TextArea.TextView.OpenContextMenu(menu); - e.Handled = true; - } - - private void UpdateTextMate() - { - if (_textMate == null) - _textMate = Models.TextMateHelper.CreateForEditor(this); - - if (DataContext is Models.RevisionTextFile file) - Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName); - } - - private TextMate.Installation _textMate = null; - } - public partial class RevisionFiles : UserControl { public RevisionFiles() { InitializeComponent(); } + + private void OnSearchBoxKeyDown(object _, KeyEventArgs e) + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return; + + if (e.Key == Key.Enter) + { + FileTree.SetSearchResult(vm.RevisionFileSearchFilter); + e.Handled = true; + } + else if (e.Key == Key.Down || e.Key == Key.Up) + { + if (vm.RevisionFileSearchSuggestion.Count > 0) + { + SearchSuggestionBox.Focus(NavigationMethod.Tab); + SearchSuggestionBox.SelectedIndex = 0; + } + + e.Handled = true; + } + else if (e.Key == Key.Escape) + { + vm.CancelRevisionFileSuggestions(); + e.Handled = true; + } + } + + private void OnSearchBoxTextChanged(object _, TextChangedEventArgs e) + { + if (string.IsNullOrEmpty(TxtSearchRevisionFiles.Text)) + FileTree.SetSearchResult(null); + } + + private void OnSearchSuggestionBoxKeyDown(object _, KeyEventArgs e) + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return; + + if (e.Key == Key.Escape) + { + vm.CancelRevisionFileSuggestions(); + e.Handled = true; + } + else if (e.Key == Key.Enter && SearchSuggestionBox.SelectedItem is string content) + { + vm.RevisionFileSearchFilter = content; + TxtSearchRevisionFiles.CaretIndex = content.Length; + FileTree.SetSearchResult(vm.RevisionFileSearchFilter); + e.Handled = true; + } + } + + private void OnSearchSuggestionDoubleTapped(object sender, TappedEventArgs e) + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return; + + var content = (sender as StackPanel)?.DataContext as string; + if (!string.IsNullOrEmpty(content)) + { + vm.RevisionFileSearchFilter = content; + TxtSearchRevisionFiles.CaretIndex = content.Length; + FileTree.SetSearchResult(vm.RevisionFileSearchFilter); + } + + e.Handled = true; + } } } diff --git a/src/Views/Reword.axaml b/src/Views/Reword.axaml index 7e84033c..3ea1ad98 100644 --- a/src/Views/Reword.axaml +++ b/src/Views/Reword.axaml @@ -14,12 +14,12 @@ Text="{DynamicResource Text.Reword}"/> - + - + - + - - + - - - - - @@ -59,13 +51,13 @@ - + + + + + + + @@ -71,6 +65,7 @@ ItemsSource="{Binding VisibleStashes}" SelectedItem="{Binding SelectedStash, Mode=TwoWay}" SelectionMode="Single" + KeyDown="OnStashListKeyDown" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto"> @@ -92,7 +87,7 @@ - + @@ -105,10 +100,12 @@ - + - - + + + + diff --git a/src/Views/StashesPage.axaml.cs b/src/Views/StashesPage.axaml.cs index c499f76a..d152a12f 100644 --- a/src/Views/StashesPage.axaml.cs +++ b/src/Views/StashesPage.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Input; namespace SourceGit.Views { @@ -9,22 +10,48 @@ namespace SourceGit.Views InitializeComponent(); } + private void OnMainLayoutSizeChanged(object sender, SizeChangedEventArgs e) + { + var grid = sender as Grid; + if (grid == null) + return; + + var layout = ViewModels.Preferences.Instance.Layout; + var width = grid.Bounds.Width; + var maxLeft = width - 304; + + if (layout.StashesLeftWidth.Value - maxLeft > 1.0) + layout.StashesLeftWidth = new GridLength(maxLeft, GridUnitType.Pixel); + } + + private void OnStashListKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is not (Key.Delete or Key.Back)) + return; + + if (DataContext is not ViewModels.StashesPage vm) + return; + + vm.Drop(vm.SelectedStash); + e.Handled = true; + } + private void OnStashContextRequested(object sender, ContextRequestedEventArgs e) { if (DataContext is ViewModels.StashesPage vm && sender is Border border) { var menu = vm.MakeContextMenu(border.DataContext as Models.Stash); - border.OpenContextMenu(menu); + menu?.Open(border); } e.Handled = true; } - private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { if (DataContext is ViewModels.StashesPage vm && sender is Grid grid) { var menu = vm.MakeContextMenuForChange(grid.DataContext as Models.Change); - grid.OpenContextMenu(menu); + menu?.Open(grid); } e.Handled = true; } diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index 6e2a00dd..163ce031 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -16,30 +16,23 @@ CanResize="False"> - - + - - - - - @@ -143,9 +136,10 @@ - + - - - - - + + + + + + - + @@ -184,7 +184,7 @@ - + diff --git a/src/Views/Statistics.axaml.cs b/src/Views/Statistics.axaml.cs index 3c5e70b6..4ebf9016 100644 --- a/src/Views/Statistics.axaml.cs +++ b/src/Views/Statistics.axaml.cs @@ -1,5 +1,3 @@ -using Avalonia.Input; - namespace SourceGit.Views { public partial class Statistics : ChromelessWindow @@ -8,10 +6,5 @@ namespace SourceGit.Views { InitializeComponent(); } - - private void BeginMoveWindow(object _, PointerPressedEventArgs e) - { - BeginMoveDrag(e); - } } } diff --git a/src/Views/SubmodulesView.axaml b/src/Views/SubmodulesView.axaml new file mode 100644 index 00000000..b8147384 --- /dev/null +++ b/src/Views/SubmodulesView.axaml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/SubmodulesView.axaml.cs b/src/Views/SubmodulesView.axaml.cs new file mode 100644 index 00000000..81ccdc5d --- /dev/null +++ b/src/Views/SubmodulesView.axaml.cs @@ -0,0 +1,182 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class SubmoduleTreeNodeToggleButton : ToggleButton + { + protected override Type StyleKeyOverride => typeof(ToggleButton); + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + DataContext is ViewModels.SubmoduleTreeNode { IsFolder: true } node) + { + var view = this.FindAncestorOfType(); + view?.ToggleNodeIsExpanded(node); + } + + e.Handled = true; + } + } + + public class SubmoduleTreeNodeIcon : UserControl + { + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register(nameof(IsExpanded)); + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsExpandedProperty) + UpdateContent(); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + UpdateContent(); + } + + private void UpdateContent() + { + if (DataContext is not ViewModels.SubmoduleTreeNode node) + { + Content = null; + return; + } + + if (node.Module != null) + CreateContent(new Thickness(0, 0, 0, 0), "Icons.Submodule"); + else if (node.IsExpanded) + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open"); + else + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder"); + } + + private void CreateContent(Thickness margin, string iconKey) + { + var geo = this.FindResource(iconKey) as StreamGeometry; + if (geo == null) + return; + + Content = new Avalonia.Controls.Shapes.Path() + { + Width = 12, + Height = 12, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = margin, + Data = geo, + }; + } + } + + public partial class SubmodulesView : UserControl + { + public static readonly RoutedEvent RowsChangedEvent = + RoutedEvent.Register(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler RowsChanged + { + add { AddHandler(RowsChangedEvent, value); } + remove { RemoveHandler(RowsChangedEvent, value); } + } + + public int Rows + { + get; + private set; + } + + public SubmodulesView() + { + InitializeComponent(); + } + + public void ToggleNodeIsExpanded(ViewModels.SubmoduleTreeNode node) + { + if (Content is ViewModels.SubmoduleCollectionAsTree tree) + { + tree.ToggleExpand(node); + Rows = tree.Rows.Count; + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ContentProperty) + { + if (Content is ViewModels.SubmoduleCollectionAsTree tree) + Rows = tree.Rows.Count; + else if (Content is ViewModels.SubmoduleCollectionAsList list) + Rows = list.Submodules.Count; + else + Rows = 0; + + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + else if (change.Property == IsVisibleProperty) + { + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); + } + } + + private void OnItemDoubleTapped(object sender, TappedEventArgs e) + { + if (sender is Control control && DataContext is ViewModels.Repository repo) + { + if (control.DataContext is ViewModels.SubmoduleTreeNode node) + { + if (node.IsFolder) + ToggleNodeIsExpanded(node); + else if (node.Module.Status != Models.SubmoduleStatus.NotInited) + repo.OpenSubmodule(node.Module.Path); + } + else if (control.DataContext is Models.Submodule m && m.Status != Models.SubmoduleStatus.NotInited) + { + repo.OpenSubmodule(m.Path); + } + } + + e.Handled = true; + } + + private void OnItemContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is Control control && DataContext is ViewModels.Repository repo) + { + if (control.DataContext is ViewModels.SubmoduleTreeNode node && node.Module != null) + { + var menu = repo.CreateContextMenuForSubmodule(node.Module); + menu?.Open(control); + } + else if (control.DataContext is Models.Submodule m) + { + var menu = repo.CreateContextMenuForSubmodule(m); + menu?.Open(control); + } + } + + e.Handled = true; + } + } +} diff --git a/src/Views/TagsView.axaml b/src/Views/TagsView.axaml index a30f63de..655d046a 100644 --- a/src/Views/TagsView.axaml +++ b/src/Views/TagsView.axaml @@ -12,47 +12,73 @@ + + - + + KeyDown="OnKeyDown" + SelectionChanged="OnSelectionChanged"> + + + + + + + + + + + + + + + + + - - - - + + + - - - - + + + + + + + + + + + + + + + + @@ -60,37 +86,50 @@ + SelectionMode="Single" + KeyDown="OnKeyDown" + SelectionChanged="OnSelectionChanged"> - - + + + + + + + + + + - - - - + + + + + + + + + + + + - diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs index 29b591fb..1b384262 100644 --- a/src/Views/TagsView.axaml.cs +++ b/src/Views/TagsView.axaml.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia; using Avalonia.Controls; @@ -31,15 +30,6 @@ namespace SourceGit.Views public class TagTreeNodeIcon : UserControl { - public static readonly StyledProperty NodeProperty = - AvaloniaProperty.Register(nameof(Node)); - - public ViewModels.TagTreeNode Node - { - get => GetValue(NodeProperty); - set => SetValue(NodeProperty, value); - } - public static readonly StyledProperty IsExpandedProperty = AvaloniaProperty.Register(nameof(IsExpanded)); @@ -49,23 +39,30 @@ namespace SourceGit.Views set => SetValue(IsExpandedProperty, value); } - static TagTreeNodeIcon() + protected override void OnDataContextChanged(EventArgs e) { - NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); - IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + base.OnDataContextChanged(e); + UpdateContent(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsExpandedProperty) + UpdateContent(); } private void UpdateContent() { - var node = Node; - if (node == null) + if (DataContext is not ViewModels.TagTreeNode node) { Content = null; return; } if (node.Tag != null) - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Tag"); + CreateContent(new Thickness(0, 0, 0, 0), "Icons.Tag"); else if (node.IsExpanded) CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open"); else @@ -92,24 +89,6 @@ namespace SourceGit.Views public partial class TagsView : UserControl { - public static readonly StyledProperty ShowTagsAsTreeProperty = - AvaloniaProperty.Register(nameof(ShowTagsAsTree)); - - public bool ShowTagsAsTree - { - get => GetValue(ShowTagsAsTreeProperty); - set => SetValue(ShowTagsAsTreeProperty, value); - } - - public static readonly StyledProperty> TagsProperty = - AvaloniaProperty.Register>(nameof(Tags)); - - public List Tags - { - get => GetValue(TagsProperty); - set => SetValue(TagsProperty, value); - } - public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); @@ -150,33 +129,7 @@ namespace SourceGit.Views { if (Content is ViewModels.TagCollectionAsTree tree) { - node.IsExpanded = !node.IsExpanded; - - var depth = node.Depth; - var idx = tree.Rows.IndexOf(node); - if (idx == -1) - return; - - if (node.IsExpanded) - { - var subrows = new List(); - MakeTreeRows(subrows, node.Children); - tree.Rows.InsertRange(idx + 1, subrows); - } - else - { - var removeCount = 0; - for (int i = idx + 1; i < tree.Rows.Count; i++) - { - var row = tree.Rows[i]; - if (row.Depth <= depth) - break; - - removeCount++; - } - tree.Rows.RemoveRange(idx + 1, removeCount); - } - + tree.ToggleExpand(node); Rows = tree.Rows.Count; RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } @@ -186,9 +139,15 @@ namespace SourceGit.Views { base.OnPropertyChanged(change); - if (change.Property == ShowTagsAsTreeProperty || change.Property == TagsProperty) + if (change.Property == ContentProperty) { - UpdateDataSource(); + if (Content is ViewModels.TagCollectionAsTree tree) + Rows = tree.Rows.Count; + else if (Content is ViewModels.TagCollectionAsList list) + Rows = list.Tags.Count; + else + Rows = 0; + RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } else if (change.Property == IsVisibleProperty) @@ -197,18 +156,30 @@ namespace SourceGit.Views } } - private void OnDoubleTappedNode(object sender, TappedEventArgs e) + private void OnItemDoubleTapped(object sender, TappedEventArgs e) { - if (sender is Grid { DataContext: ViewModels.TagTreeNode node }) - { - if (node.IsFolder) - ToggleNodeIsExpanded(node); - } + if (sender is Control { DataContext: ViewModels.TagTreeNode { IsFolder: true } node }) + ToggleNodeIsExpanded(node); e.Handled = true; } - private void OnRowContextRequested(object sender, ContextRequestedEventArgs e) + private void OnItemPointerPressed(object sender, PointerPressedEventArgs e) + { + var p = e.GetCurrentPoint(this); + if (!p.Properties.IsLeftButtonPressed) + return; + + if (DataContext is not ViewModels.Repository repo) + return; + + if (sender is Control { DataContext: Models.Tag tag }) + repo.NavigateToCommit(tag.SHA); + else if (sender is Control { DataContext: ViewModels.TagTreeNode { Tag: { } nodeTag } }) + repo.NavigateToCommit(nodeTag.SHA); + } + + private void OnItemContextRequested(object sender, ContextRequestedEventArgs e) { var control = sender as Control; if (control == null) @@ -225,13 +196,13 @@ namespace SourceGit.Views if (selected != null && DataContext is ViewModels.Repository repo) { var menu = repo.CreateContextMenuForTag(selected); - control.OpenContextMenu(menu); + menu?.Open(control); } e.Handled = true; } - private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _) + private void OnSelectionChanged(object sender, SelectionChangedEventArgs _) { var selected = (sender as ListBox)?.SelectedItem; var selectedTag = null as Models.Tag; @@ -240,86 +211,22 @@ namespace SourceGit.Views else if (selected is Models.Tag tag) selectedTag = tag; - if (selectedTag != null && DataContext is ViewModels.Repository repo) - { + if (selectedTag != null) RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); - repo.NavigateToCommit(selectedTag.SHA); - } } - private void OnToggleFilterClicked(object sender, RoutedEventArgs e) + private void OnKeyDown(object sender, KeyEventArgs e) { - if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo) - { - var target = null as Models.Tag; - if (toggle.DataContext is ViewModels.TagTreeNode node) - target = node.Tag; - else if (toggle.DataContext is Models.Tag tag) - target = tag; + if (DataContext is not ViewModels.Repository repo) + return; - if (target != null) - repo.UpdateFilters([target.Name], toggle.IsChecked == true); - } + var selected = (sender as ListBox)?.SelectedItem; + if (selected is ViewModels.TagTreeNode { Tag: { } tagInNode }) + repo.DeleteTag(tagInNode); + else if (selected is Models.Tag tag) + repo.DeleteTag(tag); e.Handled = true; } - - private void MakeTreeRows(List rows, List nodes) - { - foreach (var node in nodes) - { - rows.Add(node); - - if (!node.IsExpanded || !node.IsFolder) - continue; - - MakeTreeRows(rows, node.Children); - } - } - - private void UpdateDataSource() - { - var tags = Tags; - if (tags == null || tags.Count == 0) - { - Rows = 0; - Content = null; - return; - } - - if (ShowTagsAsTree) - { - var oldExpanded = new HashSet(); - if (Content is ViewModels.TagCollectionAsTree oldTree) - { - foreach (var row in oldTree.Rows) - { - if (row.IsFolder && row.IsExpanded) - oldExpanded.Add(row.FullPath); - } - } - - var tree = new ViewModels.TagCollectionAsTree(); - tree.Tree = ViewModels.TagTreeNode.Build(tags, oldExpanded); - - var rows = new List(); - MakeTreeRows(rows, tree.Tree); - tree.Rows.AddRange(rows); - - Content = tree; - Rows = rows.Count; - } - else - { - var list = new ViewModels.TagCollectionAsList(); - list.Tags.AddRange(tags); - - Content = list; - Rows = tags.Count; - } - - RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); - } } } - diff --git a/src/Views/TextDiffView.axaml b/src/Views/TextDiffView.axaml index 4ba8628e..ec3475fa 100644 --- a/src/Views/TextDiffView.axaml +++ b/src/Views/TextDiffView.axaml @@ -13,26 +13,41 @@ - + + + + + + + - + + SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}" + BlockNavigation="{Binding #ThisControl.BlockNavigation, Mode=TwoWay}"/> @@ -64,20 +82,50 @@ DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}" IndicatorForeground="{DynamicResource Brush.FG2}" FontFamily="{DynamicResource Fonts.Monospace}" - UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}" + FontSize="{Binding Source={x:Static vm:Preferences.Instance}, Path=EditorFontSize}" + TabWidth="{Binding Source={x:Static vm:Preferences.Instance}, Path=EditorTabWidth}" + UseSyntaxHighlighting="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSyntaxHighlighting}" WordWrap="False" - ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}" + ShowHiddenSymbols="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowHiddenSymbolsInDiffView}" EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}" - SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/> + SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}" + BlockNavigation="{Binding #ThisControl.BlockNavigation, Mode=TwoWay}"/> + + + + - + + + + diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index dd3471bf..b6235ac8 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Text; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -46,9 +46,21 @@ namespace SourceGit.Views } } + public record TextDiffViewRange + { + public int StartIdx { get; set; } = 0; + public int EndIdx { get; set; } = 0; + + public TextDiffViewRange(int startIdx, int endIdx) + { + StartIdx = startIdx; + EndIdx = endIdx; + } + } + public class ThemedTextDiffPresenter : TextEditor { - public class VerticalSeperatorMargin : AbstractMargin + public class VerticalSeparatorMargin : AbstractMargin { public override void Render(DrawingContext context) { @@ -72,6 +84,8 @@ namespace SourceGit.Views { _usePresenter = usePresenter; _isOld = isOld; + + Margin = new Thickness(8, 0); ClipToBounds = true; } @@ -112,7 +126,7 @@ namespace SourceGit.Views typeface, presenter.FontSize, presenter.Foreground); - context.DrawText(txt, new Point(Bounds.Width - txt.Width, y - txt.Height * 0.5)); + context.DrawText(txt, new Point(Bounds.Width - txt.Width, y - (txt.Height * 0.5))); } } } @@ -145,6 +159,88 @@ namespace SourceGit.Views private bool _isOld = false; } + public class LineModifyTypeMargin : AbstractMargin + { + public LineModifyTypeMargin() + { + Margin = new Thickness(1, 0); + ClipToBounds = true; + } + + public override void Render(DrawingContext context) + { + var presenter = this.FindAncestorOfType(); + if (presenter == null) + return; + + var lines = presenter.GetLines(); + var view = TextView; + if (view != null && view.VisualLinesValid) + { + 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; + + var info = lines[index - 1]; + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineMiddle) - view.VerticalOffset; + var indicator = null as FormattedText; + if (info.Type == Models.TextDiffLineType.Added) + { + indicator = new FormattedText( + "+", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + presenter.FontSize, + Brushes.Green); + } + else if (info.Type == Models.TextDiffLineType.Deleted) + { + indicator = new FormattedText( + "-", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + presenter.FontSize, + Brushes.Red); + } + + if (indicator != null) + context.DrawText(indicator, new Point(0, y - (indicator.Height * 0.5))); + } + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var presenter = this.FindAncestorOfType(); + if (presenter == null) + return new Size(0, 0); + + var typeface = TextView.CreateTypeface(); + var test = new FormattedText( + $"-", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + presenter.FontSize, + Brushes.White); + return new Size(test.Width, 0); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + InvalidateMeasure(); + } + } + public class LineBackgroundRenderer : IBackgroundRenderer { public KnownLayer Layer => KnownLayer.Background; @@ -159,6 +255,10 @@ namespace SourceGit.Views if (_presenter.Document == null || !textView.VisualLinesValid) return; + var changeBlock = _presenter.BlockNavigation?.GetCurrentBlock(); + Brush changeBlockBG = new SolidColorBrush(Colors.Gray, 0.25); + Pen changeBlockFG = new Pen(Brushes.Gray); + var lines = _presenter.GetLines(); var width = textView.Bounds.Width; foreach (var line in textView.VisualLines) @@ -171,51 +271,62 @@ namespace SourceGit.Views break; var info = lines[index - 1]; - var bg = GetBrushByLineType(info.Type); - if (bg == null) - continue; 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 bg = GetBrushByLineType(info.Type); + if (bg != null) { - var highlightBG = info.Type == Models.TextDiffLineType.Added ? _presenter.AddedHighlightBrush : _presenter.DeletedHighlightBrush; - var processingIdxStart = 0; - var processingIdxEnd = 0; - var nextHightlight = 0; + drawingContext.DrawRectangle(bg, null, new Rect(0, startY, width, endY - startY)); - foreach (var tl in line.TextLines) + if (info.Highlights.Count > 0) { - processingIdxEnd += tl.Length; + var highlightBG = info.Type == Models.TextDiffLineType.Added ? _presenter.AddedHighlightBrush : _presenter.DeletedHighlightBrush; + var processingIdxStart = 0; + var processingIdxEnd = 0; + var nextHighlight = 0; - var y = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineTop) - textView.VerticalOffset; - var h = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineBottom) - textView.VerticalOffset - y; - - while (nextHightlight < info.Highlights.Count) + foreach (var tl in line.TextLines) { - var highlight = info.Highlights[nextHightlight]; - if (highlight.Start >= processingIdxEnd) - break; + processingIdxEnd += tl.Length; - var start = line.GetVisualColumn(highlight.Start < processingIdxStart ? processingIdxStart : highlight.Start); - var end = line.GetVisualColumn(highlight.End >= processingIdxEnd ? processingIdxEnd : highlight.End + 1); + var y = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineTop) - textView.VerticalOffset; + var h = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineBottom) - textView.VerticalOffset - y; - 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); + while (nextHighlight < info.Highlights.Count) + { + var highlight = info.Highlights[nextHighlight]; + if (highlight.Start >= processingIdxEnd) + break; - if (highlight.End >= processingIdxEnd) - break; + var start = line.GetVisualColumn(highlight.Start < processingIdxStart ? processingIdxStart : highlight.Start); + var end = line.GetVisualColumn(highlight.End >= processingIdxEnd ? processingIdxEnd : highlight.End + 1); - nextHightlight++; + 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; + + nextHighlight++; + } + + processingIdxStart = processingIdxEnd; } - - processingIdxStart = processingIdxEnd; } } + + if (changeBlock != null && changeBlock.IsInRange(index)) + { + drawingContext.DrawRectangle(changeBlockBG, null, new Rect(0, startY, width, endY - startY)); + if (index == changeBlock.Start) + drawingContext.DrawLine(changeBlockFG, new Point(0, startY), new Point(width, startY)); + if (index == changeBlock.End) + drawingContext.DrawLine(changeBlockFG, new Point(0, endY), new Point(width, endY)); + } } } @@ -364,6 +475,15 @@ namespace SourceGit.Views set => SetValue(ShowHiddenSymbolsProperty, value); } + public static readonly StyledProperty TabWidthProperty = + AvaloniaProperty.Register(nameof(TabWidth), 4); + + public int TabWidth + { + get => GetValue(TabWidthProperty); + set => SetValue(TabWidthProperty, value); + } + public static readonly StyledProperty EnableChunkSelectionProperty = AvaloniaProperty.Register(nameof(EnableChunkSelection)); @@ -382,6 +502,24 @@ namespace SourceGit.Views set => SetValue(SelectedChunkProperty, value); } + public static readonly StyledProperty DisplayRangeProperty = + AvaloniaProperty.Register(nameof(DisplayRange), new TextDiffViewRange(0, 0)); + + public TextDiffViewRange DisplayRange + { + get => GetValue(DisplayRangeProperty); + set => SetValue(DisplayRangeProperty, value); + } + + public static readonly StyledProperty BlockNavigationProperty = + AvaloniaProperty.Register(nameof(BlockNavigation)); + + public ViewModels.BlockNavigation BlockNavigation + { + get => GetValue(BlockNavigationProperty); + set => SetValue(BlockNavigationProperty, value); + } + protected override Type StyleKeyOverride => typeof(TextEditor); public ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc) @@ -390,12 +528,13 @@ namespace SourceGit.Views ShowLineNumbers = false; BorderThickness = new Thickness(0); + Options.IndentationSize = TabWidth; + Options.EnableHyperlinks = false; + Options.EnableEmailHyperlinks = false; + _lineStyleTransformer = new LineStyleTransformer(this); TextArea.TextView.Margin = new Thickness(2, 0); - TextArea.TextView.Options.EnableHyperlinks = false; - TextArea.TextView.Options.EnableEmailHyperlinks = false; - TextArea.TextView.BackgroundRenderers.Add(new LineBackgroundRenderer(this)); TextArea.TextView.LineTransformers.Add(_lineStyleTransformer); } @@ -414,6 +553,132 @@ namespace SourceGit.Views { } + public virtual void GotoFirstChange() + { + var blockNavigation = BlockNavigation; + if (blockNavigation != null) + { + var prev = blockNavigation.GotoFirst(); + if (prev != null) + { + TextArea.Caret.Line = prev.Start; + ScrollToLine(prev.Start); + } + } + } + + public virtual void GotoPrevChange() + { + var blockNavigation = BlockNavigation; + if (blockNavigation != null) + { + var prev = blockNavigation.GotoPrev(); + if (prev != null) + { + TextArea.Caret.Line = prev.Start; + ScrollToLine(prev.Start); + } + + return; + } + + var firstLineIdx = DisplayRange.StartIdx; + if (firstLineIdx <= 1) + return; + + var lines = GetLines(); + var firstLineType = lines[firstLineIdx].Type; + var prevLineType = lines[firstLineIdx - 1].Type; + var isChangeFirstLine = firstLineType != Models.TextDiffLineType.Normal && firstLineType != Models.TextDiffLineType.Indicator; + var isChangePrevLine = prevLineType != Models.TextDiffLineType.Normal && prevLineType != Models.TextDiffLineType.Indicator; + if (isChangeFirstLine && isChangePrevLine) + { + for (var i = firstLineIdx - 2; i >= 0; i--) + { + var prevType = lines[i].Type; + if (prevType == Models.TextDiffLineType.Normal || prevType == Models.TextDiffLineType.Indicator) + { + ScrollToLine(i + 2); + return; + } + } + } + + var findChange = false; + for (var i = firstLineIdx - 1; i >= 0; i--) + { + var prevType = lines[i].Type; + if (prevType == Models.TextDiffLineType.Normal || prevType == Models.TextDiffLineType.Indicator) + { + if (findChange) + { + ScrollToLine(i + 2); + return; + } + } + else if (!findChange) + { + findChange = true; + } + } + } + + public virtual void GotoNextChange() + { + var blockNavigation = BlockNavigation; + if (blockNavigation != null) + { + var next = blockNavigation.GotoNext(); + if (next != null) + { + TextArea.Caret.Line = next.Start; + ScrollToLine(next.Start); + } + + return; + } + + var lines = GetLines(); + var lastLineIdx = DisplayRange.EndIdx; + if (lastLineIdx >= lines.Count - 1) + return; + + var lastLineType = lines[lastLineIdx].Type; + var findNormalLine = lastLineType == Models.TextDiffLineType.Normal || lastLineType == Models.TextDiffLineType.Indicator; + for (var idx = lastLineIdx + 1; idx < lines.Count; idx++) + { + var nextType = lines[idx].Type; + if (nextType == Models.TextDiffLineType.None || + nextType == Models.TextDiffLineType.Added || + nextType == Models.TextDiffLineType.Deleted) + { + if (findNormalLine) + { + ScrollToLine(idx + 1); + return; + } + } + else if (!findNormalLine) + { + findNormalLine = true; + } + } + } + + public virtual void GotoLastChange() + { + var blockNavigation = BlockNavigation; + if (blockNavigation != null) + { + var next = blockNavigation.GotoLast(); + if (next != null) + { + TextArea.Caret.Line = next.Start; + ScrollToLine(next.Start); + } + } + } + public override void Render(DrawingContext context) { base.Render(context); @@ -440,18 +705,25 @@ namespace SourceGit.Views TextArea.TextView.PointerEntered += OnTextViewPointerChanged; TextArea.TextView.PointerMoved += OnTextViewPointerChanged; TextArea.TextView.PointerWheelChanged += OnTextViewPointerWheelChanged; + TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged; + + TextArea.AddHandler(KeyDownEvent, OnTextAreaKeyDown, RoutingStrategies.Tunnel); UpdateTextMate(); + OnTextViewVisualLinesChanged(null, null); } protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); + TextArea.RemoveHandler(KeyDownEvent, OnTextAreaKeyDown); + TextArea.TextView.ContextRequested -= OnTextViewContextRequested; TextArea.TextView.PointerEntered -= OnTextViewPointerChanged; TextArea.TextView.PointerMoved -= OnTextViewPointerChanged; TextArea.TextView.PointerWheelChanged -= OnTextViewPointerWheelChanged; + TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged; if (_textMate != null) { @@ -470,9 +742,14 @@ namespace SourceGit.Views } else if (change.Property == ShowHiddenSymbolsProperty) { - var val = change.NewValue is true; + var val = ShowHiddenSymbols; Options.ShowTabs = val; Options.ShowSpaces = val; + Options.ShowEndOfLine = val; + } + else if (change.Property == TabWidthProperty) + { + Options.IndentationSize = TabWidth; } else if (change.Property == FileNameProperty) { @@ -486,6 +763,37 @@ namespace SourceGit.Views { InvalidateVisual(); } + else if (change.Property == BlockNavigationProperty) + { + if (change.OldValue is ViewModels.BlockNavigation oldValue) + oldValue.PropertyChanged -= OnBlockNavigationPropertyChanged; + + if (change.NewValue is ViewModels.BlockNavigation newValue) + newValue.PropertyChanged += OnBlockNavigationPropertyChanged; + + TextArea?.TextView?.Redraw(); + } + } + + private void OnTextAreaKeyDown(object sender, KeyEventArgs e) + { + if (e.KeyModifiers.Equals(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) + { + if (e.Key == Key.C) + { + CopyWithoutIndicators(); + e.Handled = true; + } + } + + if (!e.Handled) + base.OnKeyDown(e); + } + + private void OnBlockNavigationPropertyChanged(object _1, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Current") + TextArea?.TextView?.Redraw(); } private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) @@ -499,14 +807,14 @@ namespace SourceGit.Views copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Click += (_, ev) => { - App.CopyText(SelectedText); + CopyWithoutIndicators(); ev.Handled = true; }; var menu = new ContextMenu(); menu.Items.Add(copy); + menu.Open(TextArea.TextView); - TextArea.TextView.OpenContextMenu(menu); e.Handled = true; } @@ -559,6 +867,34 @@ namespace SourceGit.Views } } + private void OnTextViewVisualLinesChanged(object sender, EventArgs e) + { + if (!TextArea.TextView.VisualLinesValid) + { + SetCurrentValue(DisplayRangeProperty, new TextDiffViewRange(0, 0)); + return; + } + + var lines = GetLines(); + var start = int.MaxValue; + var count = 0; + foreach (var line in TextArea.TextView.VisualLines) + { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + + var index = line.FirstDocumentLine.LineNumber - 1; + if (index >= lines.Count) + continue; + + count++; + if (start > index) + start = index; + } + + SetCurrentValue(DisplayRangeProperty, new TextDiffViewRange(start, start + count)); + } + protected void TrySetChunk(TextDiffViewChunk chunk) { var old = SelectedChunk; @@ -664,6 +1000,84 @@ namespace SourceGit.Views } } + private void CopyWithoutIndicators() + { + var selection = TextArea.Selection; + if (selection.IsEmpty) + { + App.CopyText(string.Empty); + return; + } + + var lines = GetLines(); + + var startPosition = selection.StartPosition; + var endPosition = selection.EndPosition; + + if (startPosition.Location > endPosition.Location) + (startPosition, endPosition) = (endPosition, startPosition); + + var startIdx = startPosition.Line - 1; + var endIdx = endPosition.Line - 1; + + if (startIdx == endIdx) + { + var line = lines[startIdx]; + if (line.Type == Models.TextDiffLineType.Indicator || + line.Type == Models.TextDiffLineType.None) + { + App.CopyText(string.Empty); + return; + } + + App.CopyText(SelectedText); + return; + } + + var builder = new StringBuilder(); + for (var i = startIdx; i <= endIdx && i <= lines.Count - 1; i++) + { + var line = lines[i]; + if (line.Type == Models.TextDiffLineType.Indicator || + line.Type == Models.TextDiffLineType.None) + continue; + + // The first selected line (partial selection) + if (i == startIdx && startPosition.Column > 1) + { + builder.Append(line.Content.AsSpan(startPosition.Column - 1)); + builder.Append(Environment.NewLine); + continue; + } + + // The selection range is larger than original source. + if (i == lines.Count - 1 && i < endIdx) + { + builder.Append(line.Content); + break; + } + + // For the last line (selection range is within original source) + if (i == endIdx) + { + if (endPosition.Column - 1 < line.Content.Length) + { + builder.Append(line.Content.AsSpan(0, endPosition.Column - 1)); + } + else + { + builder.Append(line.Content); + } + break; + } + + // Other lines. + builder.AppendLine(line.Content); + } + + App.CopyText(builder.ToString()); + } + private TextMate.Installation _textMate = null; private TextLocation _lastSelectStart = TextLocation.Empty; private TextLocation _lastSelectEnd = TextLocation.Empty; @@ -674,10 +1088,11 @@ namespace SourceGit.Views { public CombinedTextDiffPresenter() : base(new TextArea(), new TextDocument()) { - TextArea.LeftMargins.Add(new LineNumberMargin(false, true) { Margin = new Thickness(8, 0) }); - TextArea.LeftMargins.Add(new VerticalSeperatorMargin()); - TextArea.LeftMargins.Add(new LineNumberMargin(false, false) { Margin = new Thickness(8, 0) }); - TextArea.LeftMargins.Add(new VerticalSeperatorMargin()); + TextArea.LeftMargins.Add(new LineNumberMargin(false, true)); + TextArea.LeftMargins.Add(new VerticalSeparatorMargin()); + TextArea.LeftMargins.Add(new LineNumberMargin(false, false)); + TextArea.LeftMargins.Add(new VerticalSeparatorMargin()); + TextArea.LeftMargins.Add(new LineModifyTypeMargin()); } public override List GetLines() @@ -814,19 +1229,18 @@ namespace SourceGit.Views { base.OnLoaded(e); - var scroller = this.FindDescendantOfType(); - if (scroller != null) + _scrollViewer = this.FindDescendantOfType(); + if (_scrollViewer != null) { - scroller.Bind(ScrollViewer.OffsetProperty, new Binding("SyncScrollOffset", BindingMode.TwoWay)); - scroller.GotFocus += OnTextViewScrollGotFocus; + _scrollViewer.Bind(ScrollViewer.OffsetProperty, new Binding("ScrollOffset", BindingMode.TwoWay)); + _scrollViewer.ScrollChanged += OnTextViewScrollChanged; } } protected override void OnUnloaded(RoutedEventArgs e) { - var scroller = this.FindDescendantOfType(); - if (scroller != null) - scroller.GotFocus -= OnTextViewScrollGotFocus; + if (_scrollViewer != null) + _scrollViewer.ScrollChanged -= OnTextViewScrollChanged; base.OnUnloaded(e); } @@ -835,22 +1249,25 @@ namespace SourceGit.Views { base.OnDataContextChanged(e); - var textDiff = DataContext as Models.TextDiff; - if (textDiff != null) + if (DataContext is Models.TextDiff textDiff) { var builder = new StringBuilder(); foreach (var line in textDiff.Lines) { if (line.Content.Length > 10000) { - builder.Append(line.Content.Substring(0, 1000)); + builder.Append(line.Content.AsSpan(0, 1000)); builder.Append($"...({line.Content.Length - 1000} character trimmed)"); - builder.AppendLine(); } else { - builder.AppendLine(line.Content); + builder.Append(line.Content); } + + if (line.NoNewLineEndOfFile) + builder.Append("\u26D4"); + + builder.Append('\n'); } Text = builder.ToString(); @@ -863,23 +1280,22 @@ namespace SourceGit.Views GC.Collect(); } - private void OnTextViewScrollGotFocus(object sender, GotFocusEventArgs e) + private void OnTextViewScrollChanged(object sender, ScrollChangedEventArgs e) { - if (EnableChunkSelection && sender is ScrollViewer viewer) - { - var area = viewer.FindDescendantOfType