diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 369a122a5..48859b540 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: os: windows-2022 runtime: win-arm64 - name: macOS (Intel) - os: macos-13 + os: macos-15-intel runtime: osx-x64 - name: macOS (Apple Silicon) os: macos-latest @@ -43,7 +43,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Configure arm64 packages if: matrix.runtime == 'linux-arm64' run: | diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 5e75fb2a3..59720f15a 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -18,7 +18,7 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Run formatting check run: dotnet format --verify-no-changes diff --git a/.github/workflows/homebrew-notify.yml b/.github/workflows/homebrew-notify.yml new file mode 100644 index 000000000..d0538b37a --- /dev/null +++ b/.github/workflows/homebrew-notify.yml @@ -0,0 +1,22 @@ +name: Notify Homebrew Tap + +on: + release: + types: [published] + +jobs: + notify-homebrew: + runs-on: ubuntu-latest + steps: + - name: Notify Homebrew tap + env: + TAG: ${{ github.event.release.tag_name }} + HOMEBREW_TAP_REPO_TOKEN: ${{ secrets.HOMEBREW_TAP_REPO_TOKEN }} + run: | + echo "📢 Notifying Homebrew tap of new release $TAG..." + curl -X POST \ + -H "Authorization: token $HOMEBREW_TAP_REPO_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/ybeapps/homebrew-sourcegit/dispatches \ + -d "{\"event_type\":\"new-sourcegit-release\",\"client_payload\":{\"version\":\"$TAG\"}}" + echo "✅ Homebrew tap notified successfully" diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 37d8afbab..d203dd2e2 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -22,11 +22,11 @@ jobs: name: sourcegit.${{ matrix.runtime }} path: build/SourceGit - name: Package - shell: bash + shell: pwsh env: VERSION: ${{ inputs.version }} RUNTIME: ${{ matrix.runtime }} - run: ./build/scripts/package.windows.sh + run: ./build/scripts/package.win.ps1 - name: Upload package artifact uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index e84f438fa..c4ac40081 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,26 @@ [![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) +## Screenshots + +* Dark Theme + + ![Theme Dark](./screenshots/theme_dark.png) + +* Light Theme + + ![Theme Light](./screenshots/theme_light.png) + +* Custom + + You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git). And welcome to share your own themes. + ## Highlights * Supports Windows/macOS/Linux * Opensource/Free * Fast -* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil) +* Deutsch/English/Español/Bahasa Indonesia/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil)/한국어 * Built-in light/dark themes * Customize theme * Visual commit graph @@ -43,7 +57,9 @@ * Issue Link * Workspace * Custom Action +* Create PR on GitHub/Gitlab/Gitea/Gitee/Bitbucket... * Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama)) +* Built-in conventional commit message helper. > [!WARNING] > **Linux** only tested on **Debian 12** on both **X11** & **Wayland**. @@ -73,12 +89,6 @@ This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationD For **Windows** users: * **MSYS Git is NOT supported**. Please use official [Git for Windows](https://git-scm.com/download/win) instead. -* You can install the latest stable from `winget` with follow commands: - ```shell - winget install SourceGit - ``` -> [!NOTE] -> `winget` will install this software as a commandline tool. You need run `SourceGit` from console or `Win+R` at the first time. Then you can add it to the taskbar. * You can install the latest stable by `scoop` with follow commands: ```shell scoop bucket add extras @@ -86,17 +96,25 @@ For **Windows** users: ``` * Pre-built binaries can be found in [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) +> [!NOTE] +> `git-flow` is no longer shipped with **Git for Windows** since `2.51.1`. You can use it by following these steps: +> * Download [git-flow-next](https://github.com/gittower/git-flow-next/releases) +> * Unzip & Rename the `git-flow-next` to `git-flow` +> * Copy to `$GIT_INSTALL_DIR/cmd` or just add its path to you `PATH` directly + For **macOS** users: -* Thanks [@ybeapps](https://github.com/ybeapps) for making `SourceGit` available on `Homebrew`. You can simply install it with following command: +* Thanks [@ybeapps](https://github.com/ybeapps) for making `SourceGit` available on `Homebrew`: ```shell - brew tap ybeapps/homebrew-sourcegit - brew install --cask --no-quarantine sourcegit + brew install --cask 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 ``` +> [!NOTE] +> macOS packages in the `Release` page of this project are all unsigned. If you are worried about potential security issues with the above command, you can download the signed package from the [distribution repository](https://github.com/ybeapps/homebrew-sourcegit/releases) provided by [@ybeapps](https://github.com/ybeapps) (there is no need to execute the above command while installing `SourceGit`). + * Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your mac. * You can run `echo $PATH > ~/Library/Application\ Support/SourceGit/PATH` to generate a custom PATH env file to introduce `PATH` env to SourceGit. @@ -155,7 +173,7 @@ This app supports open repository in external tools listed in the table below. | Cursor | YES | YES | YES | | Fleet | YES | YES | YES | | Sublime Text | YES | YES | YES | -| Zed | NO | YES | YES | +| Zed | YES | YES | YES | | Visual Studio | YES | NO | NO | > [!NOTE] @@ -172,19 +190,26 @@ This app supports open repository in external tools listed in the table below. > [!NOTE] > This app also supports a lot of `JetBrains` IDEs, installing `JetBrains Toolbox` will help this app to find them. -## Screenshots +## Conventional Commit Helper -* Dark Theme - - ![Theme Dark](./screenshots/theme_dark.png) +You can define your own conventional commit types (per-repository) by following steps: -* Light Theme - - ![Theme Light](./screenshots/theme_light.png) - -* Custom - - You can find custom themes from [sourcegit-theme](https://github.com/sourcegit-scm/sourcegit-theme.git). And welcome to share your own themes. +1. Create a json file with your own conventional commit type definitions. For example: +```json +[ + { + "Name": "New Feature", + "Type": "Feature", + "Description": "Adding a new feature" + }, + { + "Name": "Bug Fixes", + "Type": "Fix", + "Description": "Fixing a bug" + } +] +``` +2. Configure the `Conventional Commit Types` in repository configuration window. ## Contributing diff --git a/SourceGit.sln b/SourceGit.sln deleted file mode 100644 index dad5a4757..000000000 --- a/SourceGit.sln +++ /dev/null @@ -1,123 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34714.143 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGit", "src\SourceGit.csproj", "{2091C34D-4A17-4375-BEF3-4D60BE8113E4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{773082AC-D9C8-4186-8521-4B6A7BEE6158}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{FD384607-ED99-47B7-AF31-FB245841BC92}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F45A9D95-AF25-42D8-BBAC-8259C9EEE820}" -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 - .github\workflows\format-check.yml = .github\workflows\format-check.yml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "files", "files", "{3AB707DB-A02C-4AFC-BF12-D7DF2B333BAC}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitattributes = .gitattributes - .gitignore = .gitignore - global.json = global.json - LICENSE = LICENSE - README.md = README.md - VERSION = VERSION - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "app", "app", "{ABC98884-F023-4EF4-A9C9-5DE9452BE955}" - ProjectSection(SolutionItems) = preProject - build\resources\app\App.icns = build\resources\app\App.icns - build\resources\app\App.plist = build\resources\app\App.plist - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_common", "_common", "{04FD74B1-FBDB-496E-A48F-3D59D71FF952}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "usr", "usr", "{76639799-54BC-45E8-BD90-F45F63ACD11D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "share", "share", "{A3ABAA7C-EE14-4448-B466-6E69C1347E7D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "applications", "applications", "{2AF28D3B-14A8-46A8-B828-157FAAB1B06F}" - ProjectSection(SolutionItems) = preProject - build\resources\_common\usr\share\applications\sourcegit.desktop = build\resources\_common\usr\share\applications\sourcegit.desktop - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "icons", "icons", "{7166EC6C-17F5-4B5E-B38E-1E53C81EACF6}" - ProjectSection(SolutionItems) = preProject - build\resources\_common\usr\share\icons\sourcegit.png = build\resources\_common\usr\share\icons\sourcegit.png - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deb", "deb", "{9C2F0CDA-B56E-44A5-94B6-F3EA7AC20CDC}" -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}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SPECS", "SPECS", "{7802CD7A-591B-4EDD-96F8-9BF3F61692E4}" - ProjectSection(SolutionItems) = preProject - build\resources\rpm\SPECS\build.spec = build\resources\rpm\SPECS\build.spec - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "appimage", "appimage", "{5D125DD9-B48A-491F-B2FB-D7830D74C4DC}" - ProjectSection(SolutionItems) = preProject - build\resources\appimage\sourcegit.appdata.xml = build\resources\appimage\sourcegit.appdata.xml - build\resources\appimage\sourcegit.png = build\resources\appimage\sourcegit.png - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C54D4001-9940-477C-A0B6-E795ED0A3209}" - ProjectSection(SolutionItems) = preProject - build\scripts\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.sh = build\scripts\package.windows.sh - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2091C34D-4A17-4375-BEF3-4D60BE8113E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2091C34D-4A17-4375-BEF3-4D60BE8113E4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2091C34D-4A17-4375-BEF3-4D60BE8113E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2091C34D-4A17-4375-BEF3-4D60BE8113E4}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {2091C34D-4A17-4375-BEF3-4D60BE8113E4} = {49A7C2D6-558C-4FAA-8F5D-EEE81497AED7} - {FD384607-ED99-47B7-AF31-FB245841BC92} = {773082AC-D9C8-4186-8521-4B6A7BEE6158} - {67B6D05F-A000-40BA-ADB4-C9065F880D7B} = {F45A9D95-AF25-42D8-BBAC-8259C9EEE820} - {ABC98884-F023-4EF4-A9C9-5DE9452BE955} = {FD384607-ED99-47B7-AF31-FB245841BC92} - {04FD74B1-FBDB-496E-A48F-3D59D71FF952} = {FD384607-ED99-47B7-AF31-FB245841BC92} - {76639799-54BC-45E8-BD90-F45F63ACD11D} = {04FD74B1-FBDB-496E-A48F-3D59D71FF952} - {A3ABAA7C-EE14-4448-B466-6E69C1347E7D} = {76639799-54BC-45E8-BD90-F45F63ACD11D} - {2AF28D3B-14A8-46A8-B828-157FAAB1B06F} = {A3ABAA7C-EE14-4448-B466-6E69C1347E7D} - {7166EC6C-17F5-4B5E-B38E-1E53C81EACF6} = {A3ABAA7C-EE14-4448-B466-6E69C1347E7D} - {9C2F0CDA-B56E-44A5-94B6-F3EA7AC20CDC} = {FD384607-ED99-47B7-AF31-FB245841BC92} - {F101849D-BDB7-40D4-A516-751150C3CCFC} = {9C2F0CDA-B56E-44A5-94B6-F3EA7AC20CDC} - {9BA0B044-0CC9-46F8-B551-204F149BF45D} = {FD384607-ED99-47B7-AF31-FB245841BC92} - {7802CD7A-591B-4EDD-96F8-9BF3F61692E4} = {9BA0B044-0CC9-46F8-B551-204F149BF45D} - {5D125DD9-B48A-491F-B2FB-D7830D74C4DC} = {FD384607-ED99-47B7-AF31-FB245841BC92} - {C54D4001-9940-477C-A0B6-E795ED0A3209} = {773082AC-D9C8-4186-8521-4B6A7BEE6158} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7FF1B9C6-B5BF-4A50-949F-4B407A0E31C9} - EndGlobalSection -EndGlobal diff --git a/SourceGit.slnx b/SourceGit.slnx new file mode 100644 index 000000000..080f2e63e --- /dev/null +++ b/SourceGit.slnx @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md index efc5676f9..5c696b1ba 100644 --- a/THIRD-PARTY-LICENSES.md +++ b/THIRD-PARTY-LICENSES.md @@ -7,42 +7,42 @@ The project uses the following third-party libraries or assets ### AvaloniaUI - **Source**: https://github.com/AvaloniaUI/Avalonia -- **Version**: 11.2.8 +- **Version**: 11.3.9 - **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 +- **Version**: 11.3.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 +- **Version**: 2.0.0-rc6.1 - **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 +- **Version**: 1.0.70 - **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-beta.4 +- **Version**: 2.8.0 - **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-beta.4 +- **Version**: 2.8.0-beta.1 - **License**: MIT License - **License Link**: https://github.com/Azure/azure-sdk-for-net/blob/main/LICENSE.txt @@ -56,7 +56,7 @@ The project uses the following third-party libraries or assets ### Pfim - **Source**: https://github.com/nickbabcock/Pfim -- **Version**: 0.11.3 +- **Version**: 0.11.4 - **License**: MIT License - **License Link**: https://github.com/nickbabcock/Pfim/blob/master/LICENSE.txt @@ -98,3 +98,10 @@ The project uses the following third-party libraries or assets - **Commit**: 0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355 - **License**: MIT License - **License Link**: https://github.com/samuel-weinhardt/vscode-jsp-lang/blob/0e89ecdb13650dbbe5a1e85b47b2e1530bf2f355/LICENSE + +### vuejs-language-tools + +- **Source**: https://github.com/vuejs/language-tools +- **Commit**: 68d98dc57f8486c2946ae28dc86bf8e91d45da4d +- **License**: MIT License +- **License Link**: https://github.com/vuejs/language-tools/blob/68d98dc57f8486c2946ae28dc86bf8e91d45da4d/LICENSE diff --git a/TRANSLATION.md b/TRANSLATION.md index 65f1b5142..4014aadf9 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,293 +6,180 @@ This document shows the translation status of each locale file in the repository ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) -### ![de__DE](https://img.shields.io/badge/de__DE-%E2%88%9A-brightgreen) +### ![de__DE](https://img.shields.io/badge/de__DE-98.80%25-yellow) + +
+Missing keys in de_DE.axaml + +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into + +
### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen) -### ![fr__FR](https://img.shields.io/badge/fr__FR-82.16%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-98.04%25-yellow)
Missing keys in fr_FR.axaml -- Text.AddToIgnore -- Text.AddToIgnore.Pattern -- Text.AddToIgnore.Storage -- Text.Askpass.Passphrase -- 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.BranchTree.Local -- Text.BranchTree.Remote -- Text.BranchTree.Tracking -- Text.BranchTree.URL -- Text.ChangeSubmoduleUrl -- Text.ChangeSubmoduleUrl.Submodule -- Text.ChangeSubmoduleUrl.URL -- Text.Checkout.RecurseSubmodules -- Text.Checkout.WarnLostCommits -- Text.Checkout.WithFastForward -- Text.Checkout.WithFastForward.Upstream -- Text.CommitCM.CopyAuthor -- Text.CommitCM.CopyCommitMessage -- Text.CommitCM.CopyCommitter -- Text.CommitCM.CopySubject -- Text.CommitCM.InteractiveRebase -- Text.CommitCM.InteractiveRebase.Drop -- Text.CommitCM.InteractiveRebase.Edit -- Text.CommitCM.InteractiveRebase.Fixup -- Text.CommitCM.InteractiveRebase.Manually -- Text.CommitCM.InteractiveRebase.Reword -- Text.CommitCM.InteractiveRebase.Squash -- Text.CommitCM.PushRevision -- Text.CommitCM.Rebase -- Text.CommitCM.Reset -- Text.CommitDetail.Changes.Count -- Text.CommitDetail.Info.Key -- Text.CommitDetail.Info.Signer -- Text.CommitMessageTextBox.SubjectCount +- Text.BranchCM.EditDescription +- Text.CommitMessageTextBox.Placeholder +- Text.EditBranchDescription +- Text.EditBranchDescription.Target +- Text.FileCM.CustomAction +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu +- Text.OpenFile +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.PageTabBar.Tab.Refresh +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into + +
+ +### ![id__ID](https://img.shields.io/badge/id__ID-95.86%25-yellow) + +
+Missing keys in id_ID.axaml + +- Text.About.ReleaseNotes +- Text.Blame.BlameOnPreviousRevision +- Text.BranchCM.CreatePR +- Text.BranchCM.CreatePRForUpstream +- Text.BranchCM.EditDescription +- Text.CommitCM.Drop +- Text.CommitMessageTextBox.Placeholder - Text.Configure.CommitMessageTemplate.BuiltinVars -- Text.Configure.CustomAction.Arguments.Tip -- Text.Configure.CustomAction.InputControls -- Text.Configure.CustomAction.InputControls.Edit -- Text.Configure.CustomAction.InputControls.Tip -- Text.Configure.CustomAction.Scope.Tag -- Text.Configure.Git.PreferredMergeMode -- Text.Configure.IssueTracker.Share -- Text.ConfigureCustomActionControls -- Text.ConfigureCustomActionControls.CheckedValue -- Text.ConfigureCustomActionControls.CheckedValue.Tip -- Text.ConfigureCustomActionControls.Description -- Text.ConfigureCustomActionControls.DefaultValue -- Text.ConfigureCustomActionControls.IsFolder -- Text.ConfigureCustomActionControls.Label -- Text.ConfigureCustomActionControls.Options -- Text.ConfigureCustomActionControls.Options.Tip -- Text.ConfigureCustomActionControls.Type -- Text.ConfirmEmptyCommit.Continue -- Text.ConfirmEmptyCommit.NoLocalChanges -- Text.ConfirmEmptyCommit.StageAllThenCommit -- Text.ConfirmEmptyCommit.WithLocalChanges -- Text.ConfirmRestart.Title -- Text.ConfirmRestart.Message -- Text.CreateBranch.OverwriteExisting -- Text.DeinitSubmodule -- Text.DeinitSubmodule.Force -- Text.DeinitSubmodule.Path -- Text.Diff.Image.Blend -- Text.Diff.Image.SideBySide -- Text.Diff.Image.Swipe -- Text.Diff.New -- Text.Diff.Old -- Text.Diff.Submodule.Deleted -- Text.DirHistories -- Text.Discard.IncludeUntracked -- Text.ExecuteCustomAction.Target -- Text.ExecuteCustomAction.Repository -- Text.GitFlow.FinishWithPush -- Text.GitFlow.FinishWithSquash -- Text.Hotkeys.Global.SwitchWorkspace -- Text.Hotkeys.Global.SwitchTab -- Text.Hotkeys.TextEditor.OpenExternalMergeTool -- Text.InteractiveRebase.ReorderTip -- Text.Launcher.Workspaces -- Text.Launcher.Pages -- Text.Merge.Edit -- Text.MoveSubmodule -- Text.MoveSubmodule.MoveTo -- Text.MoveSubmodule.Submodule -- Text.Preferences.Git.IgnoreCRAtEOLInDiff -- Text.Preferences.Git.UseLibsecret -- Text.Pull.RecurseSubmodules -- Text.Push.New -- Text.Push.Revision -- Text.Push.Revision.Title -- Text.Repository.BranchSort -- Text.Repository.BranchSort.ByCommitterDate -- Text.Repository.BranchSort.ByName -- Text.Repository.ClearStashes -- Text.Repository.OnlyHighlightCurrentBranchInGraph -- Text.Repository.Search.ByContent -- Text.Repository.Search.ByPath -- Text.Repository.ShowDecoratedCommitsOnly -- Text.Repository.ShowFirstParentOnly -- Text.Repository.ShowFlags -- Text.Repository.ShowLostCommits -- Text.Repository.ShowSubmodulesAsTree -- Text.Repository.UseRelativeTimeInGraph -- Text.Repository.ViewLogs -- Text.Repository.Visit -- Text.ResetWithoutCheckout -- Text.ResetWithoutCheckout.MoveTo -- Text.ResetWithoutCheckout.Target -- Text.SetSubmoduleBranch -- Text.SetSubmoduleBranch.Submodule -- Text.SetSubmoduleBranch.Current -- Text.SetSubmoduleBranch.New -- Text.SetSubmoduleBranch.New.Tip -- Text.Stash.Mode -- Text.StashCM.CopyMessage -- Text.Submodule.Branch -- Text.Submodule.Deinit -- Text.Submodule.Histories -- Text.Submodule.Move -- Text.Submodule.RelativePath -- Text.Submodule.RelativePath.Placeholder -- Text.Submodule.SetBranch -- Text.Submodule.SetURL -- Text.Submodule.Status -- Text.Submodule.Status.Modified -- Text.Submodule.Status.NotInited -- Text.Submodule.Status.RevisionChanged -- Text.Submodule.Status.Unmerged -- Text.Submodule.Update -- Text.Submodule.URL -- Text.TagCM.CustomAction -- Text.UpdateSubmodules.UpdateToRemoteTrackingBranch -- Text.ViewLogs -- Text.ViewLogs.Clear -- Text.ViewLogs.CopyLog -- Text.ViewLogs.Delete -- Text.WorkingCopy.AddToGitIgnore.InFolder -- Text.WorkingCopy.ConfirmCommitWithDetachedHead -- Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool -- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts -- Text.WorkingCopy.Conflicts.UseMine -- Text.WorkingCopy.Conflicts.UseTheirs -- Text.WorkingCopy.ResetAuthor +- Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip +- Text.DropHead +- Text.DropHead.Commit +- Text.DropHead.NewHead +- Text.EditBranchDescription +- Text.EditBranchDescription.Target +- Text.FileCM.CustomAction +- Text.GitLFS.Locks.UnlockAllMyLocks +- Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu +- Text.Hotkeys.Repo.OpenCommandPalette +- Text.Launcher.Commands +- Text.Launcher.OpenRepository +- Text.Open +- Text.Open.SystemDefaultEditor +- Text.OpenFile +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.PageTabBar.Tab.Refresh +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip +- Text.PushToNewBranch +- Text.PushToNewBranch.Title +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into
-### ![it__IT](https://img.shields.io/badge/it__IT-87.44%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-93.24%25-yellow)
Missing keys in it_IT.axaml -- Text.AddToIgnore -- Text.AddToIgnore.Pattern -- Text.AddToIgnore.Storage -- Text.Askpass.Passphrase -- Text.Avatar.Load -- Text.BranchCM.ResetToSelectedCommit -- Text.BranchTree.Local -- Text.BranchTree.Remote -- Text.BranchTree.Tracking -- Text.BranchTree.URL -- Text.ChangeSubmoduleUrl -- Text.ChangeSubmoduleUrl.Submodule -- Text.ChangeSubmoduleUrl.URL -- Text.Checkout.WarnLostCommits -- Text.Checkout.WithFastForward -- Text.Checkout.WithFastForward.Upstream -- Text.CommitCM.CopyCommitMessage -- Text.CommitCM.InteractiveRebase -- Text.CommitCM.InteractiveRebase.Drop -- Text.CommitCM.InteractiveRebase.Edit -- Text.CommitCM.InteractiveRebase.Fixup -- Text.CommitCM.InteractiveRebase.Manually -- Text.CommitCM.InteractiveRebase.Reword -- Text.CommitCM.InteractiveRebase.Squash -- Text.CommitCM.PushRevision -- Text.CommitCM.Rebase -- Text.CommitCM.Reset -- Text.CommitDetail.Changes.Count -- Text.CommitDetail.Info.Key -- Text.CommitDetail.Info.Signer +- Text.About.ReleaseNotes +- Text.Blame.BlameOnPreviousRevision +- Text.BranchCM.CreatePR +- Text.BranchCM.CreatePRForUpstream +- Text.BranchCM.EditDescription +- Text.BranchCM.SwitchToWorktree +- Text.BranchTree.Ahead +- Text.BranchTree.AheadBehind +- Text.BranchTree.Behind +- Text.BranchTree.Status +- Text.BranchTree.Worktree +- Text.CommitCM.Drop +- Text.CommitDetail.Info.CopyEmail +- Text.CommitDetail.Info.CopyName +- Text.CommitDetail.Info.CopyNameAndEmail +- Text.CommitMessageTextBox.Placeholder - Text.Configure.CommitMessageTemplate.BuiltinVars -- Text.Configure.CustomAction.Arguments.Tip -- Text.Configure.CustomAction.InputControls -- Text.Configure.CustomAction.InputControls.Edit -- Text.Configure.CustomAction.InputControls.Tip -- Text.Configure.CustomAction.Scope.Tag -- Text.Configure.IssueTracker.Share -- Text.ConfigureCustomActionControls -- Text.ConfigureCustomActionControls.CheckedValue -- Text.ConfigureCustomActionControls.CheckedValue.Tip -- Text.ConfigureCustomActionControls.Description -- Text.ConfigureCustomActionControls.DefaultValue -- Text.ConfigureCustomActionControls.IsFolder -- Text.ConfigureCustomActionControls.Label -- Text.ConfigureCustomActionControls.Options -- Text.ConfigureCustomActionControls.Options.Tip -- Text.ConfigureCustomActionControls.Type -- Text.ConfirmRestart.Title -- Text.ConfirmRestart.Message -- Text.CreateBranch.OverwriteExisting -- Text.DeinitSubmodule -- Text.DeinitSubmodule.Force -- Text.DeinitSubmodule.Path -- Text.Diff.Image.Blend -- Text.Diff.Image.SideBySide -- Text.Diff.Image.Swipe -- Text.Diff.New -- Text.Diff.Old -- Text.Diff.Submodule.Deleted -- Text.DirHistories -- Text.Discard.IncludeUntracked -- Text.ExecuteCustomAction.Target -- Text.ExecuteCustomAction.Repository -- Text.Hotkeys.Global.SwitchWorkspace -- Text.Hotkeys.Global.SwitchTab -- Text.InteractiveRebase.ReorderTip -- Text.Launcher.Workspaces -- Text.Launcher.Pages -- Text.Merge.Edit -- Text.MoveSubmodule -- Text.MoveSubmodule.MoveTo -- Text.MoveSubmodule.Submodule -- Text.Preferences.Git.UseLibsecret -- Text.Pull.RecurseSubmodules -- Text.Push.New -- Text.Push.Revision -- Text.Push.Revision.Title -- Text.Repository.ClearStashes -- Text.Repository.OnlyHighlightCurrentBranchInGraph -- Text.Repository.Search.ByPath -- Text.Repository.ShowDecoratedCommitsOnly -- Text.Repository.ShowFirstParentOnly -- Text.Repository.ShowFlags -- Text.Repository.ShowLostCommits -- Text.Repository.UseRelativeTimeInGraph -- Text.ResetWithoutCheckout -- Text.ResetWithoutCheckout.MoveTo -- Text.ResetWithoutCheckout.Target -- Text.SetSubmoduleBranch -- Text.SetSubmoduleBranch.Submodule -- Text.SetSubmoduleBranch.Current -- Text.SetSubmoduleBranch.New -- Text.SetSubmoduleBranch.New.Tip -- Text.Stash.Mode -- Text.StashCM.CopyMessage -- Text.Submodule.Branch -- Text.Submodule.Deinit -- Text.Submodule.Histories -- Text.Submodule.Move -- Text.Submodule.SetBranch -- Text.Submodule.SetURL -- Text.Submodule.Update -- Text.TagCM.CustomAction -- Text.UpdateSubmodules.UpdateToRemoteTrackingBranch -- Text.WorkingCopy.AddToGitIgnore.InFolder -- Text.WorkingCopy.ConfirmCommitWithDetachedHead -- Text.WorkingCopy.ResetAuthor +- Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip +- Text.Diff.Image.Difference +- Text.DirtyState.HasLocalChanges +- Text.DirtyState.HasPendingPullOrPush +- Text.DirtyState.UpToDate +- Text.DropHead +- Text.DropHead.Commit +- Text.DropHead.NewHead +- Text.EditBranchDescription +- Text.EditBranchDescription.Target +- Text.FileCM.CustomAction +- Text.GitLFS.Locks.UnlockAllMyLocks +- Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu +- Text.Hotkeys.Repo.OpenCommandPalette +- Text.Launcher.Commands +- Text.Launcher.OpenRepository +- Text.Open +- Text.Open.SystemDefaultEditor +- Text.OpenFile +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.PageTabBar.Tab.Refresh +- Text.Preferences.AI.ReadApiKeyFromEnv +- Text.Preferences.Appearance.UseAutoHideScrollBars +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip +- Text.Preferences.General.EnableCompactFolders +- Text.Preferences.General.ShowChangesPageByDefault +- Text.Preferences.General.ShowChangesTabInCommitDetailByDefault +- Text.Preferences.General.UseGitHubStyleAvatar +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip +- Text.PushToNewBranch +- Text.PushToNewBranch.Title +- Text.ScanRepositories.UseCustomDir +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into +- Text.WorkingCopy.ClearCommitHistories +- Text.WorkingCopy.ClearCommitHistories.Confirm +- Text.WorkingCopy.NoVerify +- Text.Worktree.Open
-### ![ja__JP](https://img.shields.io/badge/ja__JP-82.16%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-75.03%25-yellow)
Missing keys in ja_JP.axaml +- Text.About.ReleaseNotes - Text.AddToIgnore - Text.AddToIgnore.Pattern - Text.AddToIgnore.Storage +- Text.App.Hide +- Text.App.ShowAll - Text.Askpass.Passphrase - Text.Avatar.Load - Text.Bisect @@ -302,16 +189,25 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CompareWithCurrent +- Text.BranchCM.CreatePR +- Text.BranchCM.CreatePRForUpstream +- Text.BranchCM.EditDescription - Text.BranchCM.ResetToSelectedCommit -- Text.BranchTree.Local +- Text.BranchCM.SwitchToWorktree +- Text.BranchTree.Ahead +- Text.BranchTree.AheadBehind +- Text.BranchTree.Behind +- Text.BranchTree.InvalidUpstream - Text.BranchTree.Remote +- Text.BranchTree.Status - Text.BranchTree.Tracking - Text.BranchTree.URL +- Text.BranchTree.Worktree - Text.ChangeSubmoduleUrl - Text.ChangeSubmoduleUrl.Submodule - Text.ChangeSubmoduleUrl.URL -- Text.Checkout.RecurseSubmodules - Text.Checkout.WarnLostCommits - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream @@ -319,6 +215,7 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.CopyCommitMessage - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject +- Text.CommitCM.Drop - Text.CommitCM.InteractiveRebase - Text.CommitCM.InteractiveRebase.Drop - Text.CommitCM.InteractiveRebase.Edit @@ -329,17 +226,25 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.PushRevision - Text.CommitCM.Rebase - Text.CommitCM.Reset +- Text.CommitCM.Fixup - Text.CommitDetail.Changes.Count +- Text.CommitDetail.Info.CopyEmail +- Text.CommitDetail.Info.CopyName +- Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer +- Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.CustomAction.Arguments.Tip - Text.Configure.CustomAction.InputControls - Text.Configure.CustomAction.InputControls.Edit -- Text.Configure.CustomAction.InputControls.Tip +- Text.Configure.CustomAction.Scope.File +- Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.ConventionalTypesOverride - Text.Configure.Git.PreferredMergeMode +- Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share - Text.ConfigureCustomActionControls - Text.ConfigureCustomActionControls.CheckedValue @@ -350,6 +255,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges @@ -361,39 +267,78 @@ This document shows the translation status of each locale file in the repository - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteMultiTags +- Text.DeleteMultiTags.DeleteFromRemotes +- Text.DeleteMultiTags.Tip - Text.Diff.Image.Blend +- Text.Diff.Image.Difference - Text.Diff.Image.SideBySide - Text.Diff.Image.Swipe - Text.Diff.New - Text.Diff.Old - Text.Diff.Submodule.Deleted - Text.DirHistories +- Text.DirtyState.HasLocalChanges +- Text.DirtyState.HasPendingPullOrPush +- Text.DirtyState.UpToDate - Text.Discard.IncludeUntracked +- Text.DropHead +- Text.DropHead.Commit +- Text.DropHead.NewHead +- Text.EditBranchDescription +- Text.EditBranchDescription.Target - Text.ExecuteCustomAction.Target - Text.ExecuteCustomAction.Repository +- Text.FileCM.CustomAction - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash -- Text.Hotkeys.Global.SwitchWorkspace +- Text.GitLFS.Locks.UnlockAllMyLocks +- Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.Repo.OpenCommandPalette - Text.Hotkeys.TextEditor.OpenExternalMergeTool - Text.InteractiveRebase.ReorderTip -- Text.Launcher.Workspaces +- Text.Launcher.Commands +- Text.Launcher.OpenRepository - Text.Launcher.Pages +- Text.Launcher.Workspaces - Text.Merge.Edit - Text.MoveSubmodule - Text.MoveSubmodule.MoveTo - Text.MoveSubmodule.Submodule +- Text.Open +- Text.Open.SystemDefaultEditor +- Text.OpenFile +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.PageTabBar.Tab.Refresh +- Text.Preferences.AI.ReadApiKeyFromEnv +- Text.Preferences.Appearance.UseAutoHideScrollBars +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip +- Text.Preferences.General.EnableCompactFolders +- Text.Preferences.General.ShowChangesPageByDefault +- Text.Preferences.General.ShowChangesTabInCommitDetailByDefault +- Text.Preferences.General.UseGitHubStyleAvatar - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.UseLibsecret -- Text.Pull.RecurseSubmodules +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip - Text.Push.New - Text.Push.Revision - Text.Push.Revision.Title +- Text.PushToNewBranch +- Text.PushToNewBranch.Title +- Text.RemoteCM.CustomAction - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes +- Text.Repository.Dashboard - Text.Repository.FilterCommits +- Text.Repository.MoreOptions - Text.Repository.OnlyHighlightCurrentBranchInGraph - Text.Repository.Search.ByContent - Text.Repository.Search.ByPath @@ -408,14 +353,19 @@ This document shows the translation status of each locale file in the repository - Text.ResetWithoutCheckout - Text.ResetWithoutCheckout.MoveTo - Text.ResetWithoutCheckout.Target +- Text.ScanRepositories.UseCustomDir - Text.SetSubmoduleBranch - Text.SetSubmoduleBranch.Submodule - Text.SetSubmoduleBranch.Current - Text.SetSubmoduleBranch.New - Text.SetSubmoduleBranch.New.Tip +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into - Text.Stash.Mode - Text.StashCM.CopyMessage - Text.Submodule.Branch +- Text.Submodule.CopyBranch - Text.Submodule.Deinit - Text.Submodule.Histories - Text.Submodule.Move @@ -428,33 +378,90 @@ This document shows the translation status of each locale file in the repository - Text.Submodule.Status.Unmerged - Text.Submodule.Update - Text.Submodule.URL +- Text.Tag.Tagger +- Text.Tag.Time +- Text.TagCM.Copy.Message +- Text.TagCM.Copy.Name +- Text.TagCM.Copy.Tagger +- Text.TagCM.CopyName - Text.TagCM.CustomAction +- Text.TagCM.DeleteMultiple - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog - Text.ViewLogs.Delete - Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.ClearCommitHistories +- Text.WorkingCopy.ClearCommitHistories.Confirm - Text.WorkingCopy.ConfirmCommitWithDetachedHead - Text.WorkingCopy.ConfirmCommitWithFilter - Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor +- Text.Worktree.Open
-### ![pt__BR](https://img.shields.io/badge/pt__BR-75.12%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-96.18%25-yellow) + +
+Missing keys in ko_KR.axaml + +- Text.Blame.BlameOnPreviousRevision +- Text.Blame.TypeNotSupported +- Text.BranchCM.CreatePR +- Text.BranchCM.CreatePRForUpstream +- Text.BranchCM.EditDescription +- Text.CommitMessageTextBox.Placeholder +- Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip +- Text.EditBranchDescription +- Text.EditBranchDescription.Target +- Text.FileCM.CustomAction +- Text.GitLFS.Locks.UnlockAllMyLocks +- Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu +- Text.Hotkeys.Repo.OpenCommandPalette +- Text.Launcher.Commands +- Text.Launcher.OpenRepository +- Text.Open +- Text.Open.SystemDefaultEditor +- Text.OpenFile +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.PageTabBar.Tab.Refresh +- Text.Preferences.Appearance.UseFixedTabWidth +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip +- Text.PushToNewBranch +- Text.PushToNewBranch.Title +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into +- Text.Submodule.Status.Unmerged + +
+ +### ![pt__BR](https://img.shields.io/badge/pt__BR-68.81%25-red)
Missing keys in pt_BR.axaml +- Text.About.ReleaseNotes - Text.AddToIgnore - Text.AddToIgnore.Pattern - Text.AddToIgnore.Storage - Text.AIAssistant.Regen - Text.AIAssistant.Use +- Text.App.Hide +- Text.App.ShowAll - Text.ApplyStash - Text.ApplyStash.DropAfterApply - Text.ApplyStash.RestoreIndex @@ -468,18 +475,26 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision +- Text.BranchCM.CreatePR +- Text.BranchCM.CreatePRForUpstream - Text.BranchCM.CustomAction +- Text.BranchCM.EditDescription - Text.BranchCM.MergeMultiBranches - Text.BranchCM.ResetToSelectedCommit -- Text.BranchTree.Local +- Text.BranchCM.SwitchToWorktree +- Text.BranchTree.Ahead +- Text.BranchTree.AheadBehind +- Text.BranchTree.Behind +- Text.BranchTree.InvalidUpstream - Text.BranchTree.Remote +- Text.BranchTree.Status - Text.BranchTree.Tracking - Text.BranchTree.URL -- Text.BranchUpstreamInvalid +- Text.BranchTree.Worktree - Text.ChangeSubmoduleUrl - Text.ChangeSubmoduleUrl.Submodule - Text.ChangeSubmoduleUrl.URL -- Text.Checkout.RecurseSubmodules - Text.Checkout.WarnLostCommits - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream @@ -488,6 +503,7 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.CopyCommitMessage - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject +- Text.CommitCM.Drop - Text.CommitCM.InteractiveRebase - Text.CommitCM.InteractiveRebase.Drop - Text.CommitCM.InteractiveRebase.Edit @@ -500,21 +516,29 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.PushRevision - Text.CommitCM.Rebase - Text.CommitCM.Reset +- Text.CommitCM.Fixup - Text.CommitDetail.Changes.Count - Text.CommitDetail.Files.Search - Text.CommitDetail.Info.Children +- Text.CommitDetail.Info.CopyEmail +- Text.CommitDetail.Info.CopyName +- Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer +- Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.CustomAction.Arguments.Tip - Text.Configure.CustomAction.InputControls - Text.Configure.CustomAction.InputControls.Edit -- Text.Configure.CustomAction.InputControls.Tip - Text.Configure.CustomAction.Scope.Branch +- Text.Configure.CustomAction.Scope.File +- Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag - Text.Configure.CustomAction.WaitForExit +- Text.Configure.Git.ConventionalTypesOverride - Text.Configure.Git.PreferredMergeMode +- Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.AddSampleGiteeIssue - Text.Configure.IssueTracker.AddSampleGiteePullRequest - Text.Configure.IssueTracker.Share @@ -527,6 +551,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges @@ -535,42 +560,58 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message - Text.CopyFullPath -- Text.CreateBranch.Name.WarnSpace - Text.CreateBranch.OverwriteExisting - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteMultiTags +- Text.DeleteMultiTags.DeleteFromRemotes +- Text.DeleteMultiTags.Tip - Text.DeleteRepositoryNode.Path - Text.DeleteRepositoryNode.TipForGroup - Text.DeleteRepositoryNode.TipForRepository - Text.Diff.First - Text.Diff.Image.Blend +- Text.Diff.Image.Difference - Text.Diff.Image.SideBySide - Text.Diff.Image.Swipe - Text.Diff.Last - Text.Diff.New - Text.Diff.Old - Text.Diff.Submodule.Deleted -- Text.Diff.UseBlockNavigation - Text.DirHistories +- Text.DirtyState.HasLocalChanges +- Text.DirtyState.HasPendingPullOrPush +- Text.DirtyState.UpToDate - Text.Discard.IncludeUntracked +- Text.DropHead +- Text.DropHead.Commit +- Text.DropHead.NewHead +- Text.EditBranchDescription +- Text.EditBranchDescription.Target - Text.ExecuteCustomAction.Target - Text.ExecuteCustomAction.Repository - Text.Fetch.Force +- Text.FileCM.CustomAction - Text.FileCM.ResolveUsing - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash +- Text.GitLFS.Locks.UnlockAllMyLocks +- Text.GitLFS.Locks.UnlockAllMyLocks.Confirm - Text.Hotkeys.Global.Clone -- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.Repo.OpenCommandPalette - Text.Hotkeys.TextEditor.OpenExternalMergeTool - Text.InProgress.CherryPick.Head - Text.InProgress.Merge.Operating - Text.InProgress.Rebase.StoppedAt - Text.InProgress.Revert.Head - Text.InteractiveRebase.ReorderTip -- Text.Launcher.Workspaces +- Text.Launcher.Commands +- Text.Launcher.OpenRepository - Text.Launcher.Pages +- Text.Launcher.Workspaces - Text.Merge.Edit - Text.Merge.Source - Text.MergeMultiple @@ -580,27 +621,48 @@ This document shows the translation status of each locale file in the repository - Text.MoveSubmodule - Text.MoveSubmodule.MoveTo - Text.MoveSubmodule.Submodule +- Text.Open +- Text.Open.SystemDefaultEditor +- Text.OpenFile +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.PageTabBar.Tab.Refresh +- Text.Preferences.AI.ReadApiKeyFromEnv - Text.Preferences.AI.Streaming - Text.Preferences.Appearance.EditorTabWidth +- Text.Preferences.Appearance.UseAutoHideScrollBars +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip - Text.Preferences.General.DateFormat +- Text.Preferences.General.EnableCompactFolders +- Text.Preferences.General.ShowChangesPageByDefault +- Text.Preferences.General.ShowChangesTabInCommitDetailByDefault - Text.Preferences.General.ShowChildren - Text.Preferences.General.ShowTagsInGraph +- Text.Preferences.General.UseGitHubStyleAvatar - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.SSLVerify - Text.Preferences.Git.UseLibsecret -- Text.Pull.RecurseSubmodules +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip - Text.Push.New - Text.Push.Revision - Text.Push.Revision.Title +- Text.PushToNewBranch +- Text.PushToNewBranch.Title +- Text.RemoteCM.CustomAction - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes +- Text.Repository.Dashboard - Text.Repository.FilterCommits - Text.Repository.HistoriesLayout - Text.Repository.HistoriesLayout.Horizontal - Text.Repository.HistoriesLayout.Vertical - Text.Repository.HistoriesOrder +- Text.Repository.MoreOptions - Text.Repository.Notifications.Clear - Text.Repository.OnlyHighlightCurrentBranchInGraph - Text.Repository.Search.ByContent @@ -620,6 +682,7 @@ This document shows the translation status of each locale file in the repository - Text.ResetWithoutCheckout - Text.ResetWithoutCheckout.MoveTo - Text.ResetWithoutCheckout.Target +- Text.ScanRepositories.UseCustomDir - Text.SetSubmoduleBranch - Text.SetSubmoduleBranch.Submodule - Text.SetSubmoduleBranch.Current @@ -630,10 +693,14 @@ This document shows the translation status of each locale file in the repository - Text.SetUpstream.Unset - Text.SetUpstream.Upstream - Text.SHALinkCM.NavigateTo +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into - Text.Stash.Mode - Text.StashCM.CopyMessage - Text.StashCM.SaveAsPatch - Text.Submodule.Branch +- Text.Submodule.CopyBranch - Text.Submodule.Deinit - Text.Submodule.Histories - Text.Submodule.Move @@ -646,13 +713,22 @@ This document shows the translation status of each locale file in the repository - Text.Submodule.Status.Unmerged - Text.Submodule.Update - Text.Submodule.URL +- Text.Tag.Tagger +- Text.Tag.Time +- Text.TagCM.Copy.Message +- Text.TagCM.Copy.Name +- Text.TagCM.Copy.Tagger +- Text.TagCM.CopyName - Text.TagCM.CustomAction +- Text.TagCM.DeleteMultiple - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog - Text.ViewLogs.Delete - Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.ClearCommitHistories +- Text.WorkingCopy.ClearCommitHistories.Confirm - Text.WorkingCopy.CommitToEdit - Text.WorkingCopy.ConfirmCommitWithDetachedHead - Text.WorkingCopy.ConfirmCommitWithFilter @@ -660,21 +736,26 @@ This document shows the translation status of each locale file in the repository - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor - Text.WorkingCopy.SignOff +- Text.Worktree.Open
### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) -### ![ta__IN](https://img.shields.io/badge/ta__IN-82.28%25-yellow) +### ![ta__IN](https://img.shields.io/badge/ta__IN-75.14%25-yellow)
Missing keys in ta_IN.axaml +- Text.About.ReleaseNotes - Text.AddToIgnore - Text.AddToIgnore.Pattern - Text.AddToIgnore.Storage +- Text.App.Hide +- Text.App.ShowAll - Text.Askpass.Passphrase - Text.Avatar.Load - Text.Bisect @@ -684,16 +765,25 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CompareWithCurrent +- Text.BranchCM.CreatePR +- Text.BranchCM.CreatePRForUpstream +- Text.BranchCM.EditDescription - Text.BranchCM.ResetToSelectedCommit -- Text.BranchTree.Local +- Text.BranchCM.SwitchToWorktree +- Text.BranchTree.Ahead +- Text.BranchTree.AheadBehind +- Text.BranchTree.Behind +- Text.BranchTree.InvalidUpstream - Text.BranchTree.Remote +- Text.BranchTree.Status - Text.BranchTree.Tracking - Text.BranchTree.URL +- Text.BranchTree.Worktree - Text.ChangeSubmoduleUrl - Text.ChangeSubmoduleUrl.Submodule - Text.ChangeSubmoduleUrl.URL -- Text.Checkout.RecurseSubmodules - Text.Checkout.WarnLostCommits - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream @@ -701,6 +791,7 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.CopyCommitMessage - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject +- Text.CommitCM.Drop - Text.CommitCM.InteractiveRebase - Text.CommitCM.InteractiveRebase.Drop - Text.CommitCM.InteractiveRebase.Edit @@ -711,17 +802,25 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.PushRevision - Text.CommitCM.Rebase - Text.CommitCM.Reset +- Text.CommitCM.Fixup - Text.CommitDetail.Changes.Count +- Text.CommitDetail.Info.CopyEmail +- Text.CommitDetail.Info.CopyName +- Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer +- Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.CustomAction.Arguments.Tip - Text.Configure.CustomAction.InputControls - Text.Configure.CustomAction.InputControls.Edit -- Text.Configure.CustomAction.InputControls.Tip +- Text.Configure.CustomAction.Scope.File +- Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.ConventionalTypesOverride - Text.Configure.Git.PreferredMergeMode +- Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share - Text.ConfigureCustomActionControls - Text.ConfigureCustomActionControls.CheckedValue @@ -732,6 +831,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges @@ -743,38 +843,77 @@ This document shows the translation status of each locale file in the repository - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteMultiTags +- Text.DeleteMultiTags.DeleteFromRemotes +- Text.DeleteMultiTags.Tip - Text.Diff.Image.Blend +- Text.Diff.Image.Difference - Text.Diff.Image.SideBySide - Text.Diff.Image.Swipe - Text.Diff.New - Text.Diff.Old - Text.Diff.Submodule.Deleted - Text.DirHistories +- Text.DirtyState.HasLocalChanges +- Text.DirtyState.HasPendingPullOrPush +- Text.DirtyState.UpToDate - Text.Discard.IncludeUntracked +- Text.DropHead +- Text.DropHead.Commit +- Text.DropHead.NewHead +- Text.EditBranchDescription +- Text.EditBranchDescription.Target - Text.ExecuteCustomAction.Target - Text.ExecuteCustomAction.Repository +- Text.FileCM.CustomAction - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash -- Text.Hotkeys.Global.SwitchWorkspace +- Text.GitLFS.Locks.UnlockAllMyLocks +- Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.Repo.OpenCommandPalette - Text.Hotkeys.TextEditor.OpenExternalMergeTool - Text.InteractiveRebase.ReorderTip -- Text.Launcher.Workspaces +- Text.Launcher.Commands +- Text.Launcher.OpenRepository - Text.Launcher.Pages +- Text.Launcher.Workspaces - Text.Merge.Edit - Text.MoveSubmodule - Text.MoveSubmodule.MoveTo - Text.MoveSubmodule.Submodule +- Text.Open +- Text.Open.SystemDefaultEditor +- Text.OpenFile +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.PageTabBar.Tab.Refresh +- Text.Preferences.AI.ReadApiKeyFromEnv +- Text.Preferences.Appearance.UseAutoHideScrollBars +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip +- Text.Preferences.General.EnableCompactFolders +- Text.Preferences.General.ShowChangesPageByDefault +- Text.Preferences.General.ShowChangesTabInCommitDetailByDefault +- Text.Preferences.General.UseGitHubStyleAvatar - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.UseLibsecret -- Text.Pull.RecurseSubmodules +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip - Text.Push.New - Text.Push.Revision - Text.Push.Revision.Title +- Text.PushToNewBranch +- Text.PushToNewBranch.Title +- Text.RemoteCM.CustomAction - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes +- Text.Repository.Dashboard +- Text.Repository.MoreOptions - Text.Repository.OnlyHighlightCurrentBranchInGraph - Text.Repository.Search.ByContent - Text.Repository.Search.ByPath @@ -789,14 +928,19 @@ This document shows the translation status of each locale file in the repository - Text.ResetWithoutCheckout - Text.ResetWithoutCheckout.MoveTo - Text.ResetWithoutCheckout.Target +- Text.ScanRepositories.UseCustomDir - Text.SetSubmoduleBranch - Text.SetSubmoduleBranch.Submodule - Text.SetSubmoduleBranch.Current - Text.SetSubmoduleBranch.New - Text.SetSubmoduleBranch.New.Tip +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into - Text.Stash.Mode - Text.StashCM.CopyMessage - Text.Submodule.Branch +- Text.Submodule.CopyBranch - Text.Submodule.Deinit - Text.Submodule.Histories - Text.Submodule.Move @@ -809,7 +953,14 @@ This document shows the translation status of each locale file in the repository - Text.Submodule.Status.Unmerged - Text.Submodule.Update - Text.Submodule.URL +- Text.Tag.Tagger +- Text.Tag.Time +- Text.TagCM.Copy.Message +- Text.TagCM.Copy.Name +- Text.TagCM.Copy.Tagger +- Text.TagCM.CopyName - Text.TagCM.CustomAction +- Text.TagCM.DeleteMultiple - Text.UpdateSubmodules.Target - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs @@ -817,23 +968,30 @@ This document shows the translation status of each locale file in the repository - Text.ViewLogs.CopyLog - Text.ViewLogs.Delete - Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.ClearCommitHistories +- Text.WorkingCopy.ClearCommitHistories.Confirm - Text.WorkingCopy.ConfirmCommitWithDetachedHead - Text.WorkingCopy.Conflicts.OpenExternalMergeTool - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor +- Text.Worktree.Open
-### ![uk__UA](https://img.shields.io/badge/uk__UA-83.45%25-yellow) +### ![uk__UA](https://img.shields.io/badge/uk__UA-76.23%25-yellow)
Missing keys in uk_UA.axaml +- Text.About.ReleaseNotes - Text.AddToIgnore - Text.AddToIgnore.Pattern - Text.AddToIgnore.Storage +- Text.App.Hide +- Text.App.ShowAll - Text.Askpass.Passphrase - Text.Avatar.Load - Text.Bisect @@ -843,15 +1001,24 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision +- Text.BranchCM.CreatePR +- Text.BranchCM.CreatePRForUpstream +- Text.BranchCM.EditDescription - Text.BranchCM.ResetToSelectedCommit -- Text.BranchTree.Local +- Text.BranchCM.SwitchToWorktree +- Text.BranchTree.Ahead +- Text.BranchTree.AheadBehind +- Text.BranchTree.Behind +- Text.BranchTree.InvalidUpstream - Text.BranchTree.Remote +- Text.BranchTree.Status - Text.BranchTree.Tracking - Text.BranchTree.URL +- Text.BranchTree.Worktree - Text.ChangeSubmoduleUrl - Text.ChangeSubmoduleUrl.Submodule - Text.ChangeSubmoduleUrl.URL -- Text.Checkout.RecurseSubmodules - Text.Checkout.WarnLostCommits - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream @@ -859,6 +1026,7 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.CopyCommitMessage - Text.CommitCM.CopyCommitter - Text.CommitCM.CopySubject +- Text.CommitCM.Drop - Text.CommitCM.InteractiveRebase - Text.CommitCM.InteractiveRebase.Drop - Text.CommitCM.InteractiveRebase.Edit @@ -869,16 +1037,24 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.PushRevision - Text.CommitCM.Rebase - Text.CommitCM.Reset +- Text.CommitCM.Fixup - Text.CommitDetail.Changes.Count +- Text.CommitDetail.Info.CopyEmail +- Text.CommitDetail.Info.CopyName +- Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer +- Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.CustomAction.Arguments.Tip - Text.Configure.CustomAction.InputControls - Text.Configure.CustomAction.InputControls.Edit -- Text.Configure.CustomAction.InputControls.Tip +- Text.Configure.CustomAction.Scope.File +- Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.ConventionalTypesOverride +- Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share - Text.ConfigureCustomActionControls - Text.ConfigureCustomActionControls.CheckedValue @@ -889,6 +1065,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfigureWorkspace.Name - Text.ConfirmRestart.Title @@ -897,38 +1074,77 @@ This document shows the translation status of each locale file in the repository - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path +- Text.DeleteMultiTags +- Text.DeleteMultiTags.DeleteFromRemotes +- Text.DeleteMultiTags.Tip - Text.Diff.Image.Blend +- Text.Diff.Image.Difference - Text.Diff.Image.SideBySide - Text.Diff.Image.Swipe - Text.Diff.New - Text.Diff.Old - Text.Diff.Submodule.Deleted - Text.DirHistories +- Text.DirtyState.HasLocalChanges +- Text.DirtyState.HasPendingPullOrPush +- Text.DirtyState.UpToDate - Text.Discard.IncludeUntracked +- Text.DropHead +- Text.DropHead.Commit +- Text.DropHead.NewHead +- Text.EditBranchDescription +- Text.EditBranchDescription.Target - Text.ExecuteCustomAction.Target - Text.ExecuteCustomAction.Repository +- Text.FileCM.CustomAction - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash -- Text.Hotkeys.Global.SwitchWorkspace +- Text.GitLFS.Locks.UnlockAllMyLocks +- Text.GitLFS.Locks.UnlockAllMyLocks.Confirm +- Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.Repo.OpenCommandPalette - Text.Hotkeys.TextEditor.OpenExternalMergeTool - Text.InteractiveRebase.ReorderTip -- Text.Launcher.Workspaces +- Text.Launcher.Commands +- Text.Launcher.OpenRepository - Text.Launcher.Pages +- Text.Launcher.Workspaces - Text.Merge.Edit - Text.MoveSubmodule - Text.MoveSubmodule.MoveTo - Text.MoveSubmodule.Submodule +- Text.Open +- Text.Open.SystemDefaultEditor +- Text.OpenFile +- Text.PageTabBar.Tab.MoveToWorkspace +- Text.PageTabBar.Tab.Refresh +- Text.Preferences.AI.ReadApiKeyFromEnv +- Text.Preferences.Appearance.UseAutoHideScrollBars +- Text.Preferences.DiffMerge.DiffArgs +- Text.Preferences.DiffMerge.DiffArgs.Tip +- Text.Preferences.DiffMerge.MergeArgs +- Text.Preferences.DiffMerge.MergeArgs.Tip +- Text.Preferences.General.EnableCompactFolders +- Text.Preferences.General.ShowChangesPageByDefault +- Text.Preferences.General.ShowChangesTabInCommitDetailByDefault +- Text.Preferences.General.UseGitHubStyleAvatar - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Preferences.Git.UseLibsecret -- Text.Pull.RecurseSubmodules +- Text.Preferences.Shell.Args +- Text.Preferences.Shell.Args.Tip - Text.Push.New - Text.Push.Revision - Text.Push.Revision.Title +- Text.PushToNewBranch +- Text.PushToNewBranch.Title +- Text.RemoteCM.CustomAction - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes +- Text.Repository.Dashboard +- Text.Repository.MoreOptions - Text.Repository.OnlyHighlightCurrentBranchInGraph - Text.Repository.Search.ByContent - Text.Repository.Search.ByPath @@ -943,14 +1159,19 @@ This document shows the translation status of each locale file in the repository - Text.ResetWithoutCheckout - Text.ResetWithoutCheckout.MoveTo - Text.ResetWithoutCheckout.Target +- Text.ScanRepositories.UseCustomDir - Text.SetSubmoduleBranch - Text.SetSubmoduleBranch.Submodule - Text.SetSubmoduleBranch.Current - Text.SetSubmoduleBranch.New - Text.SetSubmoduleBranch.New.Tip +- Text.SquashOrFixup.Squash +- Text.SquashOrFixup.Fixup +- Text.SquashOrFixup.Into - Text.Stash.Mode - Text.StashCM.CopyMessage - Text.Submodule.Branch +- Text.Submodule.CopyBranch - Text.Submodule.Deinit - Text.Submodule.Histories - Text.Submodule.Move @@ -963,15 +1184,26 @@ This document shows the translation status of each locale file in the repository - Text.Submodule.Status.Unmerged - Text.Submodule.Update - Text.Submodule.URL +- Text.Tag.Tagger +- Text.Tag.Time +- Text.TagCM.Copy.Message +- Text.TagCM.Copy.Name +- Text.TagCM.Copy.Tagger +- Text.TagCM.CopyName - Text.TagCM.CustomAction +- Text.TagCM.DeleteMultiple - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog - Text.ViewLogs.Delete - Text.WorkingCopy.AddToGitIgnore.InFolder +- Text.WorkingCopy.ClearCommitHistories +- Text.WorkingCopy.ClearCommitHistories.Confirm - Text.WorkingCopy.ConfirmCommitWithDetachedHead +- Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor +- Text.Worktree.Open
diff --git a/VERSION b/VERSION index 67b05cfc5..dff1fb957 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.27 \ No newline at end of file +2025.41 \ No newline at end of file diff --git a/build/README.md b/build/README.md index 17305edf6..0698a8fbc 100644 --- a/build/README.md +++ b/build/README.md @@ -5,7 +5,7 @@ ## How to build this project manually -1. Make sure [.NET SDK 9](https://dotnet.microsoft.com/en-us/download) is installed on your machine. +1. Make sure [.NET SDK 10](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 diff --git a/build/resources/appimage/sourcegit.png b/build/resources/appimage/sourcegit.png deleted file mode 100644 index 8cdcd3a87..000000000 Binary files a/build/resources/appimage/sourcegit.png and /dev/null differ diff --git a/build/resources/rpm/SPECS/build.spec b/build/resources/rpm/SPECS/build.spec index 2a684837a..669fdf846 100644 --- a/build/resources/rpm/SPECS/build.spec +++ b/build/resources/rpm/SPECS/build.spec @@ -20,10 +20,10 @@ mkdir -p %{buildroot}/opt/sourcegit mkdir -p %{buildroot}/%{_bindir} mkdir -p %{buildroot}/usr/share/applications mkdir -p %{buildroot}/usr/share/icons -cp -f ../../../SourceGit/* %{buildroot}/opt/sourcegit/ +cp -f %{_topdir}/../../SourceGit/* %{buildroot}/opt/sourcegit/ ln -rsf %{buildroot}/opt/sourcegit/sourcegit %{buildroot}/%{_bindir} -cp -r ../../_common/applications %{buildroot}/%{_datadir} -cp -r ../../_common/icons %{buildroot}/%{_datadir} +cp -r %{_topdir}/../_common/applications %{buildroot}/%{_datadir} +cp -r %{_topdir}/../_common/icons %{buildroot}/%{_datadir} chmod 755 -R %{buildroot}/opt/sourcegit chmod 755 %{buildroot}/%{_datadir}/applications/sourcegit.desktop diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh index 1b4adbdcb..d9900cc48 100755 --- a/build/scripts/package.linux.sh +++ b/build/scripts/package.linux.sh @@ -41,7 +41,7 @@ cp -r SourceGit SourceGit.AppDir/opt/sourcegit desktop-file-install resources/_common/applications/sourcegit.desktop --dir SourceGit.AppDir/usr/share/applications \ --set-icon com.sourcegit_scm.SourceGit --set-key=Exec --set-value=AppRun mv SourceGit.AppDir/usr/share/applications/{sourcegit,com.sourcegit_scm.SourceGit}.desktop -cp resources/appimage/sourcegit.png SourceGit.AppDir/com.sourcegit_scm.SourceGit.png +cp resources/_common/icons/sourcegit.png SourceGit.AppDir/com.sourcegit_scm.SourceGit.png ln -rsf SourceGit.AppDir/opt/sourcegit/sourcegit SourceGit.AppDir/AppRun ln -rsf SourceGit.AppDir/usr/share/applications/com.sourcegit_scm.SourceGit.desktop SourceGit.AppDir cp resources/appimage/sourcegit.appdata.xml SourceGit.AppDir/usr/share/metainfo/com.sourcegit_scm.SourceGit.appdata.xml diff --git a/build/scripts/package.win.ps1 b/build/scripts/package.win.ps1 new file mode 100644 index 000000000..04cd07134 --- /dev/null +++ b/build/scripts/package.win.ps1 @@ -0,0 +1,2 @@ +Remove-Item -Path build\SourceGit\*.pdb -Force +Compress-Archive -Path build\SourceGit -DestinationPath "build\sourcegit_${env:VERSION}.${env:RUNTIME}.zip" -Force \ No newline at end of file diff --git a/build/scripts/package.windows.sh b/build/scripts/package.windows.sh deleted file mode 100755 index c22a9d354..000000000 --- a/build/scripts/package.windows.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/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 a27a2b823..32035c656 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.0", "rollForward": "latestMajor", "allowPrerelease": false } -} \ No newline at end of file +} diff --git a/src/App.Commands.cs b/src/App.Commands.cs index f26919c73..194adc07c 100644 --- a/src/App.Commands.cs +++ b/src/App.Commands.cs @@ -1,6 +1,8 @@ using System; using System.Windows.Input; + using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; namespace SourceGit { @@ -53,5 +55,17 @@ public static bool IsCheckForUpdateCommandVisible else if (!string.IsNullOrEmpty(textBlock.Text)) await CopyTextAsync(textBlock.Text); }); + + public static readonly Command HideAppCommand = new Command(_ => + { + if (Current is App app && app.TryGetFeature(typeof(IActivatableLifetime)) is IActivatableLifetime lifetime) + lifetime.TryEnterBackground(); + }); + + public static readonly Command ShowAppCommand = new Command(_ => + { + if (Current is App app && app.TryGetFeature(typeof(IActivatableLifetime)) is IActivatableLifetime lifetime) + lifetime.TryLeaveBackground(); + }); } } diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index d60b76515..f19480a39 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -60,11 +60,14 @@ public override void Write(Utf8JsonWriter writer, DataGridLength value, JsonSeri ] )] [JsonSerializable(typeof(Models.ExternalToolPaths))] + [JsonSerializable(typeof(Models.HistoryFilterCollection))] [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] [JsonSerializable(typeof(Models.JetBrainsState))] [JsonSerializable(typeof(Models.ThemeOverrides))] [JsonSerializable(typeof(Models.Version))] [JsonSerializable(typeof(Models.RepositorySettings))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(ViewModels.Preferences))] internal partial class JsonCodeGen : JsonSerializerContext { } diff --git a/src/App.axaml b/src/App.axaml index f4dc3d893..a7a0c17f2 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -14,6 +14,7 @@ + @@ -23,6 +24,7 @@ + @@ -42,6 +44,9 @@ + + + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index f5c0559a1..a247062da 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -14,10 +14,10 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; +using Avalonia.Input.Platform; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Fonts; -using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; @@ -106,7 +106,7 @@ public static void LogException(Exception ex) #endregion #region Utility Functions - public static object CreateViewForViewModel(object data) + public static Control CreateViewForViewModel(object data) { var dataTypeName = data.GetType().FullName; if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal)) @@ -115,7 +115,7 @@ public static object CreateViewForViewModel(object data) var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views."); var viewType = Type.GetType(viewTypeName); if (viewType != null) - return Activator.CreateInstance(viewType); + return Activator.CreateInstance(viewType) as Control; return null; } @@ -159,19 +159,31 @@ public static void ShowWindow(object data) } } - public static async Task AskConfirmAsync(string message, Action onSure) + public static async Task AskConfirmAsync(string message) { if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) { var confirm = new Views.Confirm(); confirm.Message.Text = message; - confirm.OnSure = onSure; return await confirm.ShowDialog(owner); } return false; } + public static async Task AskConfirmEmptyCommitAsync(bool hasLocalChanges) + { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + var confirm = new Views.ConfirmEmptyCommit(); + confirm.TxtMessage.Text = Text(hasLocalChanges ? "ConfirmEmptyCommit.WithLocalChanges" : "ConfirmEmptyCommit.NoLocalChanges"); + confirm.BtnStageAllAndCommit.IsVisible = hasLocalChanges; + return await confirm.ShowDialog(owner); + } + + return Models.ConfirmEmptyCommitResult.Cancel; + } + public static void RaiseException(string context, string message) { if (Current is App { _launcher: not null } app) @@ -236,8 +248,6 @@ public static void SetTheme(string theme, string themeOverridesFile) else Models.CommitGraph.SetDefaultPens(overrides.GraphPenThickness); - Models.Commit.OpacityForNotMerged = overrides.OpacityForNotMergedCommits; - app.Resources.MergedDictionaries.Add(resDic); app._themeOverrides = resDic; } @@ -252,7 +262,7 @@ public static void SetTheme(string theme, string themeOverridesFile) } } - public static void SetFonts(string defaultFont, string monospaceFont, bool onlyUseMonospaceFontInEditor) + public static void SetFonts(string defaultFont, string monospaceFont) { if (Current is not App app) return; @@ -275,7 +285,7 @@ public static void SetFonts(string defaultFont, string monospaceFont, bool onlyU if (!string.IsNullOrEmpty(defaultFont)) { monospaceFont = $"fonts:SourceGit#JetBrains Mono,{defaultFont}"; - resDic.Add("Fonts.Monospace", new FontFamily(monospaceFont)); + resDic.Add("Fonts.Monospace", FontFamily.Parse(monospaceFont)); } } else @@ -283,20 +293,7 @@ public static void SetFonts(string defaultFont, string monospaceFont, bool onlyU if (!string.IsNullOrEmpty(defaultFont) && !monospaceFont.Contains(defaultFont, StringComparison.Ordinal)) monospaceFont = $"{monospaceFont},{defaultFont}"; - resDic.Add("Fonts.Monospace", new FontFamily(monospaceFont)); - } - - if (onlyUseMonospaceFontInEditor) - { - if (string.IsNullOrEmpty(defaultFont)) - resDic.Add("Fonts.Primary", new FontFamily("fonts:Inter#Inter")); - else - resDic.Add("Fonts.Primary", new FontFamily(defaultFont)); - } - else - { - if (!string.IsNullOrEmpty(monospaceFont)) - resDic.Add("Fonts.Primary", new FontFamily(monospaceFont)); + resDic.Add("Fonts.Monospace", FontFamily.Parse(monospaceFont)); } if (resDic.Count > 0) @@ -315,7 +312,7 @@ public static async Task CopyTextAsync(string data) public static async Task GetClipboardTextAsync() { if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow.Clipboard: { } clipboard }) - return await clipboard.GetTextAsync(); + return await clipboard.TryGetTextAsync(); return null; } @@ -344,14 +341,6 @@ public static Avalonia.Controls.Shapes.Path CreateMenuIcon(string key) return icon; } - public static IStorageProvider GetStorageProvider() - { - if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - return desktop.MainWindow?.StorageProvider; - - return null; - } - public static ViewModels.Launcher GetLauncher() { return Current is App app ? app._launcher : null; @@ -381,7 +370,7 @@ public override void Initialize() SetLocale(pref.Locale); SetTheme(pref.Theme, pref.ThemeOverrides); - SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily, pref.OnlyUseMonoFontInEditor); + SetFonts(pref.DefaultFontFamily, pref.MonospaceFontFamily); } public override void OnFrameworkInitializationCompleted() @@ -398,6 +387,9 @@ public override void OnFrameworkInitializationCompleted() e.Cancel = true; }); + if (TryLaunchAsFileHistoryViewer(desktop)) + return; + if (TryLaunchAsCoreEditor(desktop)) return; @@ -446,7 +438,7 @@ private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) if (!dirInfo.Exists || !dirInfo.Name.Equals("rebase-merge", StringComparison.Ordinal)) return true; - var jobsFile = Path.Combine(dirInfo.Parent!.FullName, "sourcegit_rebase_jobs.json"); + var jobsFile = Path.Combine(dirInfo.Parent!.FullName, "sourcegit.interactive_rebase"); if (!File.Exists(jobsFile)) return true; @@ -491,7 +483,7 @@ private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCo 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"); + var jobsFile = Path.Combine(gitDir, "sourcegit.interactive_rebase"); if (!File.Exists(ontoFile) || !File.Exists(origHeadFile) || !File.Exists(doneFile) || !File.Exists(jobsFile)) return true; @@ -524,6 +516,33 @@ private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCo return true; } + private bool TryLaunchAsFileHistoryViewer(IClassicDesktopStyleApplicationLifetime desktop) + { + var args = desktop.Args; + if (args is not { Length: > 1 } || !args[0].Equals("--file-history", StringComparison.Ordinal)) + return false; + + var file = Path.GetFullPath(args[1]); + var dir = Path.GetDirectoryName(file); + + var test = new Commands.QueryRepositoryRootPath(dir).GetResult(); + if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut)) + { + Console.Out.WriteLine($"'{args[1]}' is not in a valid git repository"); + desktop.Shutdown(-1); + return true; + } + + var repo = test.StdOut.Trim(); + var relFile = Path.GetRelativePath(repo, file); + var viewer = new Views.FileHistories() + { + DataContext = new ViewModels.FileHistories(repo, relFile) + }; + desktop.MainWindow = viewer; + return true; + } + private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) { var args = desktop.Args; @@ -587,7 +606,7 @@ private void TryOpenRepository(string repo) { if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo)) { - var test = new Commands.QueryRepositoryRootPath(repo).GetResultAsync().Result; + var test = new Commands.QueryRepositoryRootPath(repo).GetResult(); if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) { Dispatcher.UIThread.Invoke(() => @@ -618,10 +637,10 @@ private void Check4Update(bool manually = false) try { // Fetch latest release information. - using var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(5) }; - var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json"); + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(5); - // Parse JSON into Models.Version. + var data = await client.GetStringAsync("https://sourcegit-scm.github.io/data/version.json"); var ver = JsonSerializer.Deserialize(data, JsonCodeGen.Default.Version); if (ver == null) return; @@ -674,9 +693,8 @@ private string FixFontFamilyName(string input) if (string.IsNullOrEmpty(t)) continue; - // Collapse multiple spaces into single space - var prevChar = '\0'; var sb = new StringBuilder(); + var prevChar = '\0'; foreach (var c in t) { @@ -687,14 +705,16 @@ private string FixFontFamilyName(string input) } var name = sb.ToString(); - if (name.Contains('#')) + try { - if (!name.Equals("fonts:Inter#Inter", StringComparison.Ordinal) && - !name.Equals("fonts:SourceGit#JetBrains Mono", StringComparison.Ordinal)) - continue; + var fontFamily = FontFamily.Parse(name); + if (fontFamily.FamilyTypefaces.Count > 0) + trimmed.Add(name); + } + catch + { + // Ignore exceptions. } - - trimmed.Add(name); } return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; diff --git a/src/Commands/Add.cs b/src/Commands/Add.cs index 9bd576359..916063d82 100644 --- a/src/Commands/Add.cs +++ b/src/Commands/Add.cs @@ -2,25 +2,11 @@ { public class Add : Command { - public Add(string repo, bool includeUntracked) - { - WorkingDirectory = repo; - Context = repo; - Args = includeUntracked ? "add ." : "add -u ."; - } - - public Add(string repo, Models.Change change) - { - WorkingDirectory = repo; - Context = repo; - Args = $"add -- {change.Path.Quoted()}"; - } - public Add(string repo, string pathspecFromFile) { WorkingDirectory = repo; Context = repo; - Args = $"add --pathspec-from-file={pathspecFromFile.Quoted()}"; + Args = $"add --force --verbose --pathspec-from-file={pathspecFromFile.Quoted()}"; } } } diff --git a/src/Commands/Apply.cs b/src/Commands/Apply.cs index 189d43359..ca6ffe8d9 100644 --- a/src/Commands/Apply.cs +++ b/src/Commands/Apply.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Text; + +namespace SourceGit.Commands { public class Apply : Command { @@ -6,14 +8,19 @@ public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceM { WorkingDirectory = repo; Context = repo; - Args = "apply "; + + var builder = new StringBuilder(1024); + builder.Append("apply "); + if (ignoreWhitespace) - Args += "--ignore-whitespace "; + builder.Append("--ignore-whitespace "); else - Args += $"--whitespace={whitespaceMode} "; + builder.Append("--whitespace=").Append(whitespaceMode).Append(' '); + if (!string.IsNullOrEmpty(extra)) - Args += $"{extra} "; - Args += $"{file.Quoted()}"; + builder.Append(extra).Append(' '); + + Args = builder.Append(file.Quoted()).ToString(); } } } diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 1928154e9..efb325d06 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -32,12 +32,12 @@ public async Task RenameAsync(string to) return await ExecAsync().ConfigureAwait(false); } - public async Task SetUpstreamAsync(string upstream) + public async Task SetUpstreamAsync(Models.Branch tracking) { - if (string.IsNullOrEmpty(upstream)) + if (tracking == null) Args = $"branch {_name} --unset-upstream"; else - Args = $"branch {_name} -u {upstream}"; + Args = $"branch {_name} -u {tracking.FriendlyName}"; return await ExecAsync().ConfigureAwait(false); } diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index 23cbd413b..024636bf9 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -72,5 +72,20 @@ public async Task FileWithRevisionAsync(string file, string revision) Args = $"checkout --no-overlay {revision} -- {file.Quoted()}"; return await ExecAsync().ConfigureAwait(false); } + + public async Task MultipleFilesWithRevisionAsync(List files, string revision) + { + var builder = new StringBuilder(); + builder + .Append("checkout --no-overlay ") + .Append(revision) + .Append(" --"); + + foreach (var f in files) + builder.Append(' ').Append(f.Quoted()); + + Args = builder.ToString(); + return await ExecAsync().ConfigureAwait(false); + } } } diff --git a/src/Commands/CherryPick.cs b/src/Commands/CherryPick.cs index 0c82b9fda..d9d4faea2 100644 --- a/src/Commands/CherryPick.cs +++ b/src/Commands/CherryPick.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Text; + +namespace SourceGit.Commands { public class CherryPick : Command { @@ -7,14 +9,16 @@ public CherryPick(string repo, string commits, bool noCommit, bool appendSourceT WorkingDirectory = repo; Context = repo; - Args = "cherry-pick "; + var builder = new StringBuilder(1024); + builder.Append("cherry-pick "); if (noCommit) - Args += "-n "; + builder.Append("-n "); if (appendSourceToMessage) - Args += "-x "; + builder.Append("-x "); if (!string.IsNullOrEmpty(extraParams)) - Args += $"{extraParams} "; - Args += commits; + builder.Append(extraParams).Append(' '); + + Args = builder.Append(commits).ToString(); } } } diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index efec264b8..ec3c4594c 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Text; + +namespace SourceGit.Commands { public class Clone : Command { @@ -7,15 +9,16 @@ public Clone(string ctx, string path, string url, string localName, string sshKe Context = ctx; WorkingDirectory = path; SSHKey = sshKey; - Args = "clone --progress --verbose "; + var builder = new StringBuilder(1024); + builder.Append("clone --progress --verbose "); if (!string.IsNullOrEmpty(extraArgs)) - Args += $"{extraArgs} "; - - Args += $"{url} "; - + builder.Append(extraArgs).Append(' '); + builder.Append(url).Append(' '); if (!string.IsNullOrEmpty(localName)) - Args += localName; + builder.Append(localName); + + Args = builder.ToString(); } } } diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 11a25b369..e6e64f054 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -37,19 +37,6 @@ public enum EditorType public bool RaiseError { get; set; } = true; public Models.ICommandLog Log { get; set; } = null; - public void Exec() - { - try - { - var start = CreateGitStartInfo(false); - Process.Start(start); - } - catch (Exception ex) - { - App.RaiseException(Context, ex.Message); - } - } - public async Task ExecAsync() { Log?.AppendLine($"$ git {Args}\n"); @@ -61,8 +48,8 @@ public async Task ExecAsync() proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs); proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs); - Process dummy = null; - var dummyProcLock = new object(); + var captured = new CapturedProcess() { Process = proc }; + var capturedLock = new object(); try { proc.Start(); @@ -70,13 +57,12 @@ public async Task ExecAsync() // Not safe, please only use `CancellationToken` in readonly commands. if (CancellationToken.CanBeCanceled) { - dummy = proc; CancellationToken.Register(() => { - lock (dummyProcLock) + lock (capturedLock) { - if (dummy is { HasExited: false }) - dummy.Kill(); + if (captured is { Process: { HasExited: false } }) + captured.Process.Kill(); } }); } @@ -102,12 +88,9 @@ public async Task ExecAsync() HandleOutput(e.Message, errs); } - if (dummy != null) + lock (capturedLock) { - lock (dummyProcLock) - { - dummy = null; - } + captured.Process = null; } Log?.AppendLine(string.Empty); @@ -127,9 +110,33 @@ public async Task ExecAsync() return true; } + protected Result ReadToEnd() + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + + try + { + proc.Start(); + } + catch (Exception e) + { + return Result.Failed(e.Message); + } + + var rs = new Result() { IsSuccess = true }; + rs.StdOut = proc.StandardOutput.ReadToEnd(); + rs.StdErr = proc.StandardError.ReadToEnd(); + proc.WaitForExit(); + + rs.IsSuccess = proc.ExitCode == 0; + return rs; + } + protected async Task ReadToEndAsync() { - using var proc = new Process() { StartInfo = CreateGitStartInfo(true) }; + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); try { @@ -149,7 +156,7 @@ protected async Task ReadToEndAsync() return rs; } - private ProcessStartInfo CreateGitStartInfo(bool redirect) + protected ProcessStartInfo CreateGitStartInfo(bool redirect) { var start = new ProcessStartInfo(); start.FileName = Native.OS.GitExecutable; @@ -167,12 +174,14 @@ private ProcessStartInfo CreateGitStartInfo(bool redirect) // Force using this app as SSH askpass program var selfExecFile = Process.GetCurrentProcess().MainModule!.FileName; start.Environment.Add("SSH_ASKPASS", selfExecFile); // Can not use parameter here, because it invoked by SSH with `exec` - start.Environment.Add("SSH_ASKPASS_REQUIRE", "force"); + start.Environment.Add("SSH_ASKPASS_REQUIRE", "prefer"); start.Environment.Add("SOURCEGIT_LAUNCH_AS_ASKPASS", "TRUE"); + if (!OperatingSystem.IsLinux()) + start.Environment.Add("DISPLAY", "required"); // If an SSH private key was provided, sets the environment. if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey)) - start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'"); + start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}' -F '/dev/null'"); // Force using en_US.UTF-8 locale if (OperatingSystem.IsLinux()) @@ -181,7 +190,7 @@ private ProcessStartInfo CreateGitStartInfo(bool redirect) start.Environment.Add("LC_ALL", "C"); } - var builder = new StringBuilder(); + var builder = new StringBuilder(2048); builder .Append("--no-pager -c core.quotepath=off -c credential.helper=") .Append(Native.OS.CredentialHelper) @@ -234,6 +243,11 @@ private void HandleOutput(string line, List errs) errs.Add(line); } + private class CapturedProcess + { + public Process Process { get; set; } = null; + } + [GeneratedRegex(@"\d+%")] private static partial Regex REG_PROGRESS(); } diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs index 41d650f76..b756c4ab4 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -1,22 +1,39 @@ using System.IO; +using System.Text; using System.Threading.Tasks; namespace SourceGit.Commands { public class Commit : Command { - public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor) + public Commit(string repo, string message, bool signOff, bool noVerify, bool amend, bool resetAuthor) { _tmpFile = Path.GetTempFileName(); _message = message; WorkingDirectory = repo; Context = repo; - Args = $"commit --allow-empty --file={_tmpFile.Quoted()}"; + + var builder = new StringBuilder(); + builder.Append("commit --allow-empty --file="); + builder.Append(_tmpFile.Quoted()); + builder.Append(' '); + if (signOff) - Args += " --signoff"; + builder.Append("--signoff "); + + if (noVerify) + builder.Append("--no-verify "); + if (amend) - Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit"; + { + builder.Append("--amend "); + if (resetAuthor) + builder.Append("--reset-author "); + builder.Append("--no-edit"); + } + + Args = builder.ToString(); } public async Task RunAsync() @@ -34,7 +51,7 @@ public async Task RunAsync() } } - private readonly string _tmpFile = string.Empty; - private readonly string _message = string.Empty; + private readonly string _tmpFile; + private readonly string _message; } } diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index 7951e6adf..a4ed972d6 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Diagnostics; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -33,56 +33,62 @@ public CompareRevisions(string repo, string start, string end, string path) public async Task> ReadAsync() { var changes = new List(); - var rs = await ReadToEndAsync().ConfigureAwait(false); - if (!rs.IsSuccess) - return changes; + try + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - ParseLine(changes, line); + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } 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); + } - changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); - return changes; - } + continue; + } - private void ParseLine(List outs, 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); - outs.Add(renamed); - } + var change = new Models.Change() { Path = match.Groups[2].Value }; + var status = match.Groups[1].Value; - return; - } + 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 'C': + change.Set(Models.ChangeState.Copied); + changes.Add(change); + break; + } + } - var change = new Models.Change() { Path = match.Groups[2].Value }; - var status = match.Groups[1].Value; + await proc.WaitForExitAsync().ConfigureAwait(false); - switch (status[0]) + changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); + } + catch { - case 'M': - change.Set(Models.ChangeState.Modified); - outs.Add(change); - break; - case 'A': - change.Set(Models.ChangeState.Added); - outs.Add(change); - break; - case 'D': - change.Set(Models.ChangeState.Deleted); - outs.Add(change); - break; - case 'C': - change.Set(Models.ChangeState.Copied); - outs.Add(change); - break; + //ignore changes; } + + return changes; } } } diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs index 0348c5109..52fc021cb 100644 --- a/src/Commands/Config.cs +++ b/src/Commands/Config.cs @@ -20,6 +20,26 @@ public Config(string repository) } } + public Dictionary ReadAll() + { + Args = "config -l"; + + var output = ReadToEnd(); + var rs = new Dictionary(); + if (output.IsSuccess) + { + var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var parts = line.Split('=', 2); + if (parts.Length == 2) + rs[parts[0]] = parts[1]; + } + } + + return rs; + } + public async Task> ReadAllAsync() { Args = "config -l"; @@ -40,6 +60,12 @@ public async Task> ReadAllAsync() return rs; } + public string Get(string key) + { + Args = $"config {key}"; + return ReadToEnd().StdOut.Trim(); + } + public async Task GetAsync(string key) { Args = $"config {key}"; diff --git a/src/Commands/CountLocalChangesWithoutUntracked.cs b/src/Commands/CountLocalChanges.cs similarity index 66% rename from src/Commands/CountLocalChangesWithoutUntracked.cs rename to src/Commands/CountLocalChanges.cs index 769d732e9..17916926d 100644 --- a/src/Commands/CountLocalChangesWithoutUntracked.cs +++ b/src/Commands/CountLocalChanges.cs @@ -3,13 +3,14 @@ namespace SourceGit.Commands { - public class CountLocalChangesWithoutUntracked : Command + public class CountLocalChanges : Command { - public CountLocalChangesWithoutUntracked(string repo) + public CountLocalChanges(string repo, bool includeUntracked) { + var option = includeUntracked ? "-uall" : "-uno"; WorkingDirectory = repo; Context = repo; - Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain"; + Args = $"--no-optional-locks status {option} --ignore-submodules=all --porcelain"; } public async Task GetResultAsync() diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 3eae1b54b..680aff63d 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Diagnostics; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -20,29 +21,53 @@ public partial class Diff : Command public Diff(string repo, Models.DiffOption opt, int unified, bool ignoreWhitespace) { - _result.TextDiff = new Models.TextDiff() - { - Repo = repo, - Option = opt, - }; + _result.TextDiff = new Models.TextDiff(); WorkingDirectory = repo; Context = repo; + var builder = new StringBuilder(256); + builder.Append("diff --no-color --no-ext-diff --patch "); + if (Models.DiffOption.IgnoreCRAtEOL) + builder.Append("--ignore-cr-at-eol "); if (ignoreWhitespace) - 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 --no-ext-diff --patch --unified={unified} {opt}"; + builder.Append("--ignore-space-change "); + builder.Append("--unified=").Append(unified).Append(' '); + builder.Append(opt.ToString()); + + Args = builder.ToString(); } public async Task ReadAsync() { - var rs = await ReadToEndAsync().ConfigureAwait(false); - var sr = new StringReader(rs.StdOut); - while (sr.ReadLine() is { } line) - ParseLine(line); + try + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); + + var text = await proc.StandardOutput.ReadToEndAsync().ConfigureAwait(false); + + var start = 0; + var end = text.IndexOf('\n', start); + while (end > 0) + { + var line = text[start..end]; + ParseLine(line); + + start = end + 1; + end = text.IndexOf('\n', start); + } + + if (start < text.Length) + ParseLine(text[start..]); + + await proc.WaitForExitAsync().ConfigureAwait(false); + } + catch + { + // Ignore exceptions. + } if (_result.IsBinary || _result.IsLFS || _result.TextDiff.Lines.Count == 0) { @@ -240,10 +265,10 @@ private void ProcessInlineHighlights() foreach (var chunk in chunks) { if (chunk.DeletedCount > 0) - left.Highlights.Add(new Models.TextInlineRange(chunk.DeletedStart, chunk.DeletedCount)); + left.Highlights.Add(new Models.TextRange(chunk.DeletedStart, chunk.DeletedCount)); if (chunk.AddedCount > 0) - right.Highlights.Add(new Models.TextInlineRange(chunk.AddedStart, chunk.AddedCount)); + right.Highlights.Add(new Models.TextRange(chunk.AddedStart, chunk.AddedCount)); } } } diff --git a/src/Commands/DiffTool.cs b/src/Commands/DiffTool.cs index b80f55959..5bc23db36 100644 --- a/src/Commands/DiffTool.cs +++ b/src/Commands/DiffTool.cs @@ -1,47 +1,77 @@ -using System.IO; +using System; +using System.Diagnostics; namespace SourceGit.Commands { public class DiffTool : Command { - public DiffTool(string repo, int type, string exec, Models.DiffOption option) + public DiffTool(string repo, Models.DiffOption option) { WorkingDirectory = repo; Context = repo; - - _merger = Models.ExternalMerger.Supported.Find(x => x.Type == type); - _exec = exec; _option = option; } public void Open() { - if (_merger == null) + var tool = Native.OS.GetDiffMergeTool(true); + if (tool == null) { - App.RaiseException(Context, "Invalid merge tool in preference setting!"); + App.RaiseException(Context, "Invalid diff/merge tool in preference setting!"); return; } - if (_merger.Type == 0) + if (string.IsNullOrEmpty(tool.Cmd)) { + if (!CheckGitConfiguration()) + return; + Args = $"difftool -g --no-prompt {_option}"; } - else if (File.Exists(_exec)) + else { - var cmd = $"{_exec.Quoted()} {_merger.DiffCmd}"; + var cmd = $"{tool.Exec.Quoted()} {tool.Cmd}"; Args = $"-c difftool.sourcegit.cmd={cmd.Quoted()} difftool --tool=sourcegit --no-prompt {_option}"; } - else + + try { - App.RaiseException(Context, $"Can NOT find external diff tool in '{_exec}'!"); - return; + Process.Start(CreateGitStartInfo(false)); + } + catch (Exception ex) + { + App.RaiseException(Context, ex.Message); + } + } + + private bool CheckGitConfiguration() + { + var config = new Config(WorkingDirectory).ReadAll(); + if (config.TryGetValue("diff.guitool", out var guiTool)) + return CheckCLIBasedTool(guiTool); + if (config.TryGetValue("merge.guitool", out var mergeGuiTool)) + return CheckCLIBasedTool(mergeGuiTool); + if (config.TryGetValue("diff.tool", out var diffTool)) + return CheckCLIBasedTool(diffTool); + if (config.TryGetValue("merge.tool", out var mergeTool)) + return CheckCLIBasedTool(mergeTool); + + App.RaiseException(Context, "Missing git configuration: diff.guitool"); + return false; + } + + private bool CheckCLIBasedTool(string tool) + { + if (tool.StartsWith("vimdiff", StringComparison.Ordinal) || + tool.StartsWith("nvimdiff", StringComparison.Ordinal)) + { + App.RaiseException(Context, $"CLI based diff tool \"{tool}\" is not supported by this app!"); + return false; } - Exec(); + return true; } - private Models.ExternalMerger _merger; - private string _exec; private Models.DiffOption _option; } } diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index cbf38bd49..aad589112 100644 --- a/src/Commands/Discard.cs +++ b/src/Commands/Discard.cs @@ -86,7 +86,7 @@ public static async Task ChangesAsync(string repo, List changes, { var pathSpecFile = Path.GetTempFileName(); await File.WriteAllLinesAsync(pathSpecFile, restores).ConfigureAwait(false); - await new Restore(repo, pathSpecFile, false).Use(log).ExecAsync().ConfigureAwait(false); + await new Restore(repo, pathSpecFile).Use(log).ExecAsync().ConfigureAwait(false); File.Delete(pathSpecFile); } } diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index d25cc80c8..914361257 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Text; +using System.Threading.Tasks; namespace SourceGit.Commands { @@ -6,27 +7,35 @@ public class Fetch : Command { public Fetch(string repo, string remote, bool noTags, bool force) { - _remoteKey = $"remote.{remote}.sshkey"; + _remote = remote; WorkingDirectory = repo; Context = repo; - Args = "fetch --progress --verbose "; - - if (noTags) - Args += "--no-tags "; - else - Args += "--tags "; + var builder = new StringBuilder(512); + builder.Append("fetch --progress --verbose "); + builder.Append(noTags ? "--no-tags " : "--tags "); if (force) - Args += "--force "; + builder.Append("--force "); + builder.Append(remote); + + Args = builder.ToString(); + } - Args += remote; + public Fetch(string repo, string remote) + { + _remote = remote; + + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + Args = $"fetch --progress --verbose {remote}"; } public Fetch(string repo, Models.Branch local, Models.Branch remote) { - _remoteKey = $"remote.{remote.Remote}.sshkey"; + _remote = remote.Remote; WorkingDirectory = repo; Context = repo; @@ -35,10 +44,10 @@ public Fetch(string repo, Models.Branch local, Models.Branch remote) public async Task RunAsync() { - SSHKey = await new Config(WorkingDirectory).GetAsync(_remoteKey).ConfigureAwait(false); + SSHKey = await new Config(WorkingDirectory).GetAsync($"remote.{_remote}.sshkey").ConfigureAwait(false); return await ExecAsync().ConfigureAwait(false); } - private readonly string _remoteKey; + private readonly string _remote; } } diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs index 4889881bf..bbefa34e5 100644 --- a/src/Commands/GenerateCommitMessage.cs +++ b/src/Commands/GenerateCommitMessage.cs @@ -17,7 +17,7 @@ public GetDiffContent(string repo, Models.DiffOption opt) { WorkingDirectory = repo; Context = repo; - Args = $"diff --diff-algorithm=minimal {opt}"; + Args = $"diff --no-color --no-ext-diff --diff-algorithm=minimal {opt}"; } public async Task ReadAsync() diff --git a/src/Commands/InteractiveRebase.cs b/src/Commands/InteractiveRebase.cs index ebcadec13..7e4ca86b2 100644 --- a/src/Commands/InteractiveRebase.cs +++ b/src/Commands/InteractiveRebase.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Text; + +namespace SourceGit.Commands { public class InteractiveRebase : Command { @@ -7,10 +9,13 @@ public InteractiveRebase(string repo, string basedOn, bool autoStash) WorkingDirectory = repo; Context = repo; Editor = EditorType.RebaseEditor; - Args = "rebase -i --autosquash "; + + var builder = new StringBuilder(512); + builder.Append("rebase -i --autosquash "); if (autoStash) - Args += "--autostash "; - Args += basedOn; + builder.Append("--autostash "); + + Args = builder.Append(basedOn).ToString(); } } } diff --git a/src/Commands/IsAncestor.cs b/src/Commands/IsAncestor.cs new file mode 100644 index 000000000..e40793e2d --- /dev/null +++ b/src/Commands/IsAncestor.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class IsAncestor : Command + { + public IsAncestor(string repo, string checkPoint, string endPoint) + { + WorkingDirectory = repo; + Context = repo; + Args = $"merge-base --is-ancestor {checkPoint} {endPoint}"; + } + + public async Task GetResultAsync() + { + var rs = await ReadToEndAsync().ConfigureAwait(false); + return rs.IsSuccess; + } + } +} diff --git a/src/Commands/IsBareRepository.cs b/src/Commands/IsBareRepository.cs index 98b127ce6..03520131c 100644 --- a/src/Commands/IsBareRepository.cs +++ b/src/Commands/IsBareRepository.cs @@ -11,6 +11,17 @@ public IsBareRepository(string path) Args = "rev-parse --is-bare-repository"; } + public bool GetResult() + { + 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"; + } + public async Task GetResultAsync() { if (!Directory.Exists(Path.Combine(WorkingDirectory, "refs")) || diff --git a/src/Commands/IsBinary.cs b/src/Commands/IsBinary.cs index 0e60f38c6..087e71c7b 100644 --- a/src/Commands/IsBinary.cs +++ b/src/Commands/IsBinary.cs @@ -12,7 +12,7 @@ public IsBinary(string repo, string commit, string path) { WorkingDirectory = repo; Context = repo; - Args = $"diff {Models.Commit.EmptyTreeSHA1} {commit} --numstat -- {path.Quoted()}"; + Args = $"diff --no-color --no-ext-diff --numstat {Models.Commit.EmptyTreeSHA1} {commit} -- {path.Quoted()}"; RaiseError = false; } diff --git a/src/Commands/IsConflictResolved.cs b/src/Commands/IsConflictResolved.cs index 2d53766a2..e5b752d35 100644 --- a/src/Commands/IsConflictResolved.cs +++ b/src/Commands/IsConflictResolved.cs @@ -10,7 +10,12 @@ public IsConflictResolved(string repo, Models.Change change) WorkingDirectory = repo; Context = repo; - Args = $"diff -a --ignore-cr-at-eol --check {opt}"; + Args = $"diff --no-color --no-ext-diff -a --ignore-cr-at-eol --check {opt}"; + } + + public bool GetResult() + { + return ReadToEnd().IsSuccess; } public async Task GetResultAsync() diff --git a/src/Commands/IsLFSFiltered.cs b/src/Commands/IsLFSFiltered.cs index e8e5513ca..5c45f8bc6 100644 --- a/src/Commands/IsLFSFiltered.cs +++ b/src/Commands/IsLFSFiltered.cs @@ -20,9 +20,19 @@ public IsLFSFiltered(string repo, string sha, string path) RaiseError = false; } + public bool GetResult() + { + return Parse(ReadToEnd()); + } + public async Task GetResultAsync() { var rs = await ReadToEndAsync().ConfigureAwait(false); + return Parse(rs); + } + + private bool Parse(Result rs) + { return rs.IsSuccess && rs.StdOut.Contains("filter\0lfs"); } } diff --git a/src/Commands/IssueTracker.cs b/src/Commands/IssueTracker.cs new file mode 100644 index 000000000..e4c156417 --- /dev/null +++ b/src/Commands/IssueTracker.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class IssueTracker : Command + { + public IssueTracker(string repo, bool isShared) + { + WorkingDirectory = repo; + Context = repo; + + if (isShared) + { + var storage = $"{repo}/.issuetracker"; + _isStorageFileExists = File.Exists(storage); + _baseArg = $"config -f {storage.Quoted()}"; + } + else + { + _isStorageFileExists = true; + _baseArg = "config --local"; + } + } + + public async Task ReadAllAsync(List outs, bool isShared) + { + if (!_isStorageFileExists) + return; + + Args = $"{_baseArg} -l"; + + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (rs.IsSuccess) + { + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var parts = line.Split('=', 2); + if (parts.Length < 2) + continue; + + var key = parts[0]; + var value = parts[1]; + + if (!key.StartsWith("issuetracker.", StringComparison.Ordinal)) + continue; + + if (key.EndsWith(".regex", StringComparison.Ordinal)) + { + var prefixLen = "issuetracker.".Length; + var suffixLen = ".regex".Length; + var ruleName = key.Substring(prefixLen, key.Length - prefixLen - suffixLen); + FindOrAdd(outs, ruleName, isShared).RegexString = value; + } + else if (key.EndsWith(".url", StringComparison.Ordinal)) + { + var prefixLen = "issuetracker.".Length; + var suffixLen = ".url".Length; + var ruleName = key.Substring(prefixLen, key.Length - prefixLen - suffixLen); + FindOrAdd(outs, ruleName, isShared).URLTemplate = value; + } + } + } + } + + public async Task AddAsync(Models.IssueTracker rule) + { + Args = $"{_baseArg} issuetracker.{rule.Name.Quoted()}.regex {rule.RegexString.Quoted()}"; + + var succ = await ExecAsync().ConfigureAwait(false); + if (succ) + { + Args = $"{_baseArg} issuetracker.{rule.Name.Quoted()}.url {rule.URLTemplate.Quoted()}"; + return await ExecAsync().ConfigureAwait(false); + } + + return false; + } + + public async Task UpdateRegexAsync(Models.IssueTracker rule) + { + Args = $"{_baseArg} issuetracker.{rule.Name.Quoted()}.regex {rule.RegexString.Quoted()}"; + return await ExecAsync().ConfigureAwait(false); + } + + public async Task UpdateURLTemplateAsync(Models.IssueTracker rule) + { + Args = $"{_baseArg} issuetracker.{rule.Name.Quoted()}.url {rule.URLTemplate.Quoted()}"; + return await ExecAsync().ConfigureAwait(false); + } + + public async Task RemoveAsync(string name) + { + if (!_isStorageFileExists) + return true; + + Args = $"{_baseArg} --remove-section issuetracker.{name.Quoted()}"; + return await ExecAsync().ConfigureAwait(false); + } + + private Models.IssueTracker FindOrAdd(List rules, string ruleName, bool isShared) + { + var rule = rules.Find(x => x.Name.Equals(ruleName, StringComparison.Ordinal)); + if (rule != null) + return rule; + + rule = new Models.IssueTracker() { IsShared = isShared, Name = ruleName }; + rules.Add(rule); + return rule; + } + + private readonly bool _isStorageFileExists; + private readonly string _baseArg; + } +} diff --git a/src/Commands/LFS.cs b/src/Commands/LFS.cs index 001002d1d..6e4283359 100644 --- a/src/Commands/LFS.cs +++ b/src/Commands/LFS.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; -using System.Text.RegularExpressions; +using System.Text.Json; using System.Threading.Tasks; namespace SourceGit.Commands { - public partial class LFS : Command + public class LFS : Command { - [GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")] - private static partial Regex REG_LOCK(); - public LFS(string repo) { WorkingDirectory = repo; @@ -60,30 +56,23 @@ public async Task PruneAsync() public async Task> GetLocksAsync(string remote) { - Args = $"lfs locks --remote={remote}"; + Args = $"lfs locks --json --remote={remote}"; var rs = await ReadToEndAsync().ConfigureAwait(false); - var locks = new List(); - if (rs.IsSuccess) { - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) + try + { + var locks = JsonSerializer.Deserialize(rs.StdOut, JsonCodeGen.Default.ListLFSLock); + return locks; + } + catch { - var match = REG_LOCK().Match(line); - if (match.Success) - { - locks.Add(new Models.LFSLock() - { - File = match.Groups[1].Value, - User = match.Groups[2].Value, - ID = long.Parse(match.Groups[3].Value), - }); - } + // Ignore exceptions. } } - return locks; + return []; } public async Task LockAsync(string remote, string file) @@ -104,5 +93,20 @@ public async Task UnlockAsync(string remote, string file, bool force) Args = builder.ToString(); return await ExecAsync().ConfigureAwait(false); } + + public async Task UnlockMultipleAsync(string remote, List files, bool force) + { + var builder = new StringBuilder(); + builder + .Append("lfs unlock --remote=") + .Append(remote) + .Append(force ? " -f" : " "); + + foreach (string file in files) + builder.Append(' ').Append(file.Quoted()); + + Args = builder.ToString(); + return await ExecAsync().ConfigureAwait(false); + } } } diff --git a/src/Commands/MergeTool.cs b/src/Commands/MergeTool.cs index c2262c69f..0f15fb617 100644 --- a/src/Commands/MergeTool.cs +++ b/src/Commands/MergeTool.cs @@ -1,48 +1,65 @@ -using System.IO; +using System; using System.Threading.Tasks; namespace SourceGit.Commands { public class MergeTool : Command { - public MergeTool(string repo, int type, string exec, string file) + public MergeTool(string repo, string file) { WorkingDirectory = repo; - Context = exec; - - _merger = Models.ExternalMerger.Supported.Find(x => x.Type == type); - _exec = exec; + Context = repo; _file = string.IsNullOrEmpty(file) ? string.Empty : file.Quoted(); } public async Task OpenAsync() { - if (_merger == null) + var tool = Native.OS.GetDiffMergeTool(false); + if (tool == null) { - App.RaiseException(Context, "Invalid merge tool in preference setting!"); + App.RaiseException(Context, "Invalid diff/merge tool in preference setting!"); return false; } - if (_merger.Type == 0) + if (string.IsNullOrEmpty(tool.Cmd)) { - Args = $"mergetool {_file}"; + var ok = await CheckGitConfigurationAsync(); + if (!ok) + return false; + + Args = $"mergetool -g --no-prompt {_file}"; } - else if (File.Exists(_exec)) + else { - var cmd = $"{_exec.Quoted()} {_merger.Cmd}"; + var cmd = $"{tool.Exec.Quoted()} {tool.Cmd}"; Args = $"-c mergetool.sourcegit.cmd={cmd.Quoted()} -c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit {_file}"; } - else + + return await ExecAsync().ConfigureAwait(false); + } + + private async Task CheckGitConfigurationAsync() + { + var tool = await new Config(WorkingDirectory).GetAsync("merge.guitool"); + if (string.IsNullOrEmpty(tool)) + tool = await new Config(WorkingDirectory).GetAsync("merge.tool"); + + if (string.IsNullOrEmpty(tool)) { - App.RaiseException(Context, $"Can NOT find external merge tool in '{_exec}'!"); + App.RaiseException(Context, "Missing git configuration: merge.guitool"); return false; } - return await ExecAsync().ConfigureAwait(false); + if (tool.StartsWith("vimdiff", StringComparison.Ordinal) || + tool.StartsWith("nvimdiff", StringComparison.Ordinal)) + { + App.RaiseException(Context, $"CLI based merge tool \"{tool}\" is not supported by this app!"); + return false; + } + + return true; } - private Models.ExternalMerger _merger; - private string _exec; private string _file; } } diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index 93896c754..e121445ba 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Text; +using System.Threading.Tasks; namespace SourceGit.Commands { @@ -10,12 +11,14 @@ public Pull(string repo, string remote, string branch, bool useRebase) WorkingDirectory = repo; Context = repo; - Args = "pull --verbose --progress "; + var builder = new StringBuilder(512); + builder.Append("pull --verbose --progress "); if (useRebase) - Args += "--rebase=true "; + builder.Append("--rebase=true "); + builder.Append(remote).Append(' ').Append(branch); - Args += $"{remote} {branch}"; + Args = builder.ToString(); } public async Task RunAsync() diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index b822af46d..44394dc41 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Text; +using System.Threading.Tasks; namespace SourceGit.Commands { @@ -10,18 +11,20 @@ public Push(string repo, string local, string remote, string remoteBranch, bool WorkingDirectory = repo; Context = repo; - Args = "push --progress --verbose "; + var builder = new StringBuilder(1024); + builder.Append("push --progress --verbose "); if (withTags) - Args += "--tags "; + builder.Append("--tags "); if (checkSubmodules) - Args += "--recurse-submodules=check "; + builder.Append("--recurse-submodules=check "); if (track) - Args += "-u "; + builder.Append("-u "); if (force) - Args += "--force-with-lease "; + builder.Append("--force-with-lease "); - Args += $"{remote} {local}:{remoteBranch}"; + builder.Append(remote).Append(' ').Append(local).Append(':').Append(remoteBranch); + Args = builder.ToString(); } public Push(string repo, string remote, string refname, bool isDelete) @@ -30,12 +33,14 @@ public Push(string repo, string remote, string refname, bool isDelete) WorkingDirectory = repo; Context = repo; - Args = "push "; + var builder = new StringBuilder(512); + builder.Append("push "); if (isDelete) - Args += "--delete "; + builder.Append("--delete "); + builder.Append(remote).Append(' ').Append(refname); - Args += $"{remote} {refname}"; + Args = builder.ToString(); } public async Task RunAsync() diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 0eaf2837f..b741fb5c0 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -15,7 +15,7 @@ public QueryBranches(string repo) { WorkingDirectory = repo; Context = repo; - Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%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)%00%(worktreepath)\""; } public async Task> GetResultAsync() @@ -26,15 +26,16 @@ public QueryBranches(string repo) return branches; var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - var remoteHeads = new Dictionary(); + var mismatched = new HashSet(); + var remotes = new Dictionary(); foreach (var line in lines) { - var b = ParseLine(line); + var b = ParseLine(line, mismatched); if (b != null) { branches.Add(b); if (!b.IsLocal) - remoteHeads.Add(b.FullName, b.Head); + remotes.Add(b.FullName, b); } } @@ -42,15 +43,16 @@ public QueryBranches(string repo) { if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream)) { - if (remoteHeads.TryGetValue(b.Upstream, out var upstreamHead)) + if (remotes.TryGetValue(b.Upstream, out var upstream)) { b.IsUpstreamGone = false; - b.TrackStatus ??= await new QueryTrackStatus(WorkingDirectory, b.Head, upstreamHead).GetResultAsync().ConfigureAwait(false); + + if (mismatched.Contains(b.FullName)) + await new QueryTrackStatus(WorkingDirectory).GetResultAsync(b, upstream).ConfigureAwait(false); } else { b.IsUpstreamGone = true; - b.TrackStatus ??= new Models.BranchTrackStatus(); } } } @@ -58,10 +60,10 @@ public QueryBranches(string repo) return branches; } - private Models.Branch ParseLine(string line) + private Models.Branch ParseLine(string line, HashSet mismatched) { var parts = line.Split('\0'); - if (parts.Length != 6) + if (parts.Length != 7) return null; var branch = new Models.Branch(); @@ -94,19 +96,22 @@ private Models.Branch ParseLine(string line) branch.IsLocal = true; } + ulong.TryParse(parts[1], out var committerDate); + branch.FullName = refName; - branch.CommitterDate = ulong.Parse(parts[1]); + branch.CommitterDate = committerDate; branch.Head = parts[2]; branch.IsCurrent = parts[3] == "*"; branch.Upstream = parts[4]; branch.IsUpstreamGone = false; - if (!branch.IsLocal || - string.IsNullOrEmpty(branch.Upstream) || - string.IsNullOrEmpty(parts[5]) || - parts[5].Equals("=", StringComparison.Ordinal)) - branch.TrackStatus = new Models.BranchTrackStatus(); + if (branch.IsLocal && + !string.IsNullOrEmpty(branch.Upstream) && + !string.IsNullOrEmpty(parts[5]) && + !parts[5].Equals("=", StringComparison.Ordinal)) + mismatched.Add(branch.FullName); + branch.WorktreePath = parts[6]; return branch; } } diff --git a/src/Commands/QueryCommitFullMessage.cs b/src/Commands/QueryCommitFullMessage.cs index 07b77f2e3..2e9db9298 100644 --- a/src/Commands/QueryCommitFullMessage.cs +++ b/src/Commands/QueryCommitFullMessage.cs @@ -11,6 +11,12 @@ public QueryCommitFullMessage(string repo, string sha) Args = $"show --no-show-signature --format=%B -s {sha}"; } + public string GetResult() + { + var rs = ReadToEnd(); + return rs.IsSuccess ? rs.StdOut.TrimEnd() : string.Empty; + } + public async Task GetResultAsync() { var rs = await ReadToEndAsync().ConfigureAwait(false); diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 311406bdc..0e46ead81 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Text; using System.Threading.Tasks; @@ -7,144 +8,107 @@ namespace SourceGit.Commands { public class QueryCommits : Command { - public QueryCommits(string repo, string limits, bool needFindHead = true) + public QueryCommits(string repo, string limits, bool markMerged = true) { WorkingDirectory = repo; Context = repo; - 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; + Args = $"log --no-show-signature --decorate=full --format=%H%x00%P%x00%D%x00%aN±%aE%x00%at%x00%cN±%cE%x00%ct%x00%s {limits}"; + _markMerged = markMerged; } public QueryCommits(string repo, string filter, Models.CommitSearchMethod method, bool onlyCurrentBranch) { - string search = onlyCurrentBranch ? string.Empty : "--branches --remotes "; + var builder = new StringBuilder(); + builder.Append("log -1000 --date-order --no-show-signature --decorate=full --format=%H%x00%P%x00%D%x00%aN±%aE%x00%at%x00%cN±%cE%x00%ct%x00%s "); + + if (!onlyCurrentBranch) + builder.Append("--branches --remotes "); if (method == Models.CommitSearchMethod.ByAuthor) { - search += $"-i --author={filter.Quoted()}"; + builder.Append("-i --author=").Append(filter.Quoted()); } else if (method == Models.CommitSearchMethod.ByCommitter) { - search += $"-i --committer={filter.Quoted()}"; + builder.Append("-i --committer=").Append(filter.Quoted()); } else if (method == Models.CommitSearchMethod.ByMessage) { - var argsBuilder = new StringBuilder(); - argsBuilder.Append(search); - var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries); foreach (var word in words) - argsBuilder.Append("--grep=").Append(word.Trim().Quoted()).Append(' '); - argsBuilder.Append("--all-match -i"); - - search = argsBuilder.ToString(); + builder.Append("--grep=").Append(word.Trim().Quoted()).Append(' '); + builder.Append("--all-match -i"); } else if (method == Models.CommitSearchMethod.ByPath) { - search += $"-- {filter.Quoted()}"; + builder.Append("-- ").Append(filter.Quoted()); } else { - search = $"-G{filter.Quoted()}"; + builder.Append("-G").Append(filter.Quoted()); } WorkingDirectory = repo; Context = repo; - 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; + Args = builder.ToString(); + _markMerged = false; } public async Task> GetResultAsync() { - var rs = await ReadToEndAsync().ConfigureAwait(false); - if (!rs.IsSuccess) - return _commits; - - var nextPartIdx = 0; - var start = 0; - var end = rs.StdOut.IndexOf('\n', start); - while (end > 0) + var commits = new List(); + try { - var line = rs.StdOut.Substring(start, end - start); - switch (nextPartIdx) + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); + + var findHead = false; + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line) { - case 0: - _current = new Models.Commit() { SHA = line }; - _commits.Add(_current); - break; - case 1: - ParseParent(line); - break; - case 2: - _current.ParseDecorators(line); - if (_current.IsMerged && !_isHeadFound) - _isHeadFound = true; - break; - case 3: - _current.Author = Models.User.FindOrAdd(line); - break; - case 4: - _current.AuthorTime = ulong.Parse(line); - break; - case 5: - _current.Committer = Models.User.FindOrAdd(line); - break; - case 6: - _current.CommitterTime = ulong.Parse(line); - break; - case 7: - _current.Subject = line; - nextPartIdx = -1; - break; + var parts = line.Split('\0'); + if (parts.Length != 8) + continue; + + var commit = new Models.Commit() { SHA = parts[0] }; + commit.ParseParents(parts[1]); + commit.ParseDecorators(parts[2]); + commit.Author = Models.User.FindOrAdd(parts[3]); + commit.AuthorTime = ulong.Parse(parts[4]); + commit.Committer = Models.User.FindOrAdd(parts[5]); + commit.CommitterTime = ulong.Parse(parts[6]); + commit.Subject = parts[7]; + commits.Add(commit); + + findHead |= commit.IsMerged; } - nextPartIdx++; - - start = end + 1; - end = rs.StdOut.IndexOf('\n', start); - } - - if (start < rs.StdOut.Length) - _current.Subject = rs.StdOut.Substring(start); - - if (_findFirstMerged && !_isHeadFound && _commits.Count > 0) - await MarkFirstMergedAsync().ConfigureAwait(false); + await proc.WaitForExitAsync().ConfigureAwait(false); - return _commits; - } - - private void ParseParent(string data) - { - if (data.Length < 8) - return; - - _current.Parents.AddRange(data.Split(' ', StringSplitOptions.RemoveEmptyEntries)); - } - - private async Task MarkFirstMergedAsync() - { - Args = $"log --since={_commits[^1].CommitterTimeStr.Quoted()} --format=\"%H\""; - - var rs = await ReadToEndAsync().ConfigureAwait(false); - var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - if (shas.Length == 0) - return; - - var set = new HashSet(shas); - - foreach (var c in _commits) - { - if (set.Contains(c.SHA)) + if (_markMerged && !findHead && commits.Count > 0) { - c.IsMerged = true; - break; + var set = await new QueryCurrentBranchCommitHashes(WorkingDirectory, commits[^1].CommitterTime) + .GetResultAsync() + .ConfigureAwait(false); + + foreach (var c in commits) + { + if (set.Contains(c.SHA)) + { + c.IsMerged = true; + break; + } + } } } + catch (Exception e) + { + App.RaiseException(Context, $"Failed to query commits. Reason: {e.Message}"); + } + + return commits; } - private List _commits = new List(); - private Models.Commit _current = null; - private bool _findFirstMerged = false; - private bool _isHeadFound = false; + private bool _markMerged = false; } } diff --git a/src/Commands/QueryCommitsForInteractiveRebase.cs b/src/Commands/QueryCommitsForInteractiveRebase.cs index 81e28d4fc..e1f964881 100644 --- a/src/Commands/QueryCommitsForInteractiveRebase.cs +++ b/src/Commands/QueryCommitsForInteractiveRebase.cs @@ -12,14 +12,20 @@ public QueryCommitsForInteractiveRebase(string repo, string on) WorkingDirectory = repo; Context = repo; - 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"; + Args = $"log --topo-order --cherry-pick --right-only --no-merges --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 async Task> GetResultAsync() { + var commits = new List(); var rs = await ReadToEndAsync().ConfigureAwait(false); if (!rs.IsSuccess) - return _commits; + { + App.RaiseException(Context, $"Failed to query commits for interactive-rebase. Reason: {rs.StdErr}"); + return commits; + } + + Models.InteractiveCommit current = null; var nextPartIdx = 0; var start = 0; @@ -30,38 +36,38 @@ public QueryCommitsForInteractiveRebase(string repo, string on) switch (nextPartIdx) { case 0: - _current = new Models.InteractiveCommit(); - _current.Commit.SHA = line; - _commits.Add(_current); + current = new Models.InteractiveCommit(); + current.Commit.SHA = line; + commits.Add(current); break; case 1: - ParseParent(line); + current.Commit.ParseParents(line); break; case 2: - _current.Commit.ParseDecorators(line); + current.Commit.ParseDecorators(line); break; case 3: - _current.Commit.Author = Models.User.FindOrAdd(line); + current.Commit.Author = Models.User.FindOrAdd(line); break; case 4: - _current.Commit.AuthorTime = ulong.Parse(line); + current.Commit.AuthorTime = ulong.Parse(line); break; case 5: - _current.Commit.Committer = Models.User.FindOrAdd(line); + current.Commit.Committer = Models.User.FindOrAdd(line); break; case 6: - _current.Commit.CommitterTime = ulong.Parse(line); + current.Commit.CommitterTime = ulong.Parse(line); break; default: var boundary = rs.StdOut.IndexOf(_boundary, end + 1, StringComparison.Ordinal); if (boundary > end) { - _current.Message = rs.StdOut.Substring(start, boundary - start - 1); + current.Message = rs.StdOut.Substring(start, boundary - start - 1); end = boundary + _boundary.Length; } else { - _current.Message = rs.StdOut.Substring(start); + current.Message = rs.StdOut.Substring(start); end = rs.StdOut.Length - 2; } @@ -78,19 +84,9 @@ public QueryCommitsForInteractiveRebase(string repo, string on) end = rs.StdOut.IndexOf('\n', start); } - return _commits; - } - - private void ParseParent(string data) - { - if (data.Length < 8) - return; - - _current.Commit.Parents.AddRange(data.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + return commits; } - private List _commits = []; - private Models.InteractiveCommit _current = null; private readonly string _boundary; } } diff --git a/src/Commands/QueryCurrentBranch.cs b/src/Commands/QueryCurrentBranch.cs index c721d13b0..8b698d1e5 100644 --- a/src/Commands/QueryCurrentBranch.cs +++ b/src/Commands/QueryCurrentBranch.cs @@ -11,6 +11,11 @@ public QueryCurrentBranch(string repo) Args = "branch --show-current"; } + public string GetResult() + { + return ReadToEnd().StdOut.Trim(); + } + public async Task GetResultAsync() { var rs = await ReadToEndAsync().ConfigureAwait(false); diff --git a/src/Commands/QueryCurrentBranchCommitHashes.cs b/src/Commands/QueryCurrentBranchCommitHashes.cs new file mode 100644 index 000000000..a795e1f54 --- /dev/null +++ b/src/Commands/QueryCurrentBranchCommitHashes.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class QueryCurrentBranchCommitHashes : Command + { + public QueryCurrentBranchCommitHashes(string repo, ulong sinceTimestamp) + { + var since = DateTime.UnixEpoch.AddSeconds(sinceTimestamp).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); + WorkingDirectory = repo; + Context = repo; + Args = $"log --since={since.Quoted()} --format=%H"; + } + + public async Task> GetResultAsync() + { + var outs = new HashSet(); + + try + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); + + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { Length: > 8 } line) + outs.Add(line); + + await proc.WaitForExitAsync().ConfigureAwait(false); + } + catch + { + // Ignore exceptions; + } + + return outs; + } + } +} diff --git a/src/Commands/QueryFileContent.cs b/src/Commands/QueryFileContent.cs index 41cf63836..66f964600 100644 --- a/src/Commands/QueryFileContent.cs +++ b/src/Commands/QueryFileContent.cs @@ -21,7 +21,7 @@ public static async Task RunAsync(string repo, string revision, string f var stream = new MemoryStream(); try { - using var proc = Process.Start(starter); + using var proc = Process.Start(starter)!; await proc.StandardOutput.BaseStream.CopyToAsync(stream).ConfigureAwait(false); await proc.WaitForExitAsync().ConfigureAwait(false); } @@ -49,7 +49,7 @@ public static async Task FromLFSAsync(string repo, string oid, long size var stream = new MemoryStream(); try { - using var proc = Process.Start(starter); + using var proc = Process.Start(starter)!; await proc.StandardInput.WriteLineAsync("version https://git-lfs.github.com/spec/v1").ConfigureAwait(false); await proc.StandardInput.WriteLineAsync($"oid sha256:{oid}").ConfigureAwait(false); await proc.StandardInput.WriteLineAsync($"size {size}").ConfigureAwait(false); diff --git a/src/Commands/QueryGitCommonDir.cs b/src/Commands/QueryGitCommonDir.cs index c5b9339d5..e71cf2b07 100644 --- a/src/Commands/QueryGitCommonDir.cs +++ b/src/Commands/QueryGitCommonDir.cs @@ -9,19 +9,19 @@ public QueryGitCommonDir(string workDir) { WorkingDirectory = workDir; Args = "rev-parse --git-common-dir"; + RaiseError = false; } public async Task GetResultAsync() { var rs = await ReadToEndAsync().ConfigureAwait(false); - if (!rs.IsSuccess) - return null; + if (!rs.IsSuccess || string.IsNullOrEmpty(rs.StdOut)) + return string.Empty; - var stdout = rs.StdOut.Trim(); - if (string.IsNullOrEmpty(stdout)) - return null; - - return Path.IsPathRooted(stdout) ? stdout : Path.GetFullPath(Path.Combine(WorkingDirectory, stdout)); + var dir = rs.StdOut.Trim(); + if (Path.IsPathRooted(dir)) + return dir; + return Path.GetFullPath(Path.Combine(WorkingDirectory, dir)); } } } diff --git a/src/Commands/QueryGitDir.cs b/src/Commands/QueryGitDir.cs index ce8bfee60..5a91b2173 100644 --- a/src/Commands/QueryGitDir.cs +++ b/src/Commands/QueryGitDir.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Threading.Tasks; namespace SourceGit.Commands { @@ -11,9 +10,13 @@ public QueryGitDir(string workDir) Args = "rev-parse --git-dir"; } - public async Task GetResultAsync() + public string GetResult() + { + return Parse(ReadToEnd()); + } + + private string Parse(Result rs) { - var rs = await ReadToEndAsync().ConfigureAwait(false); if (!rs.IsSuccess) return null; diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 9605014da..385a95f27 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Diagnostics; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -21,141 +21,145 @@ public QueryLocalChanges(string repo, bool includeUntracked = true) public async Task> GetResultAsync() { var outs = new List(); - var rs = await ReadToEndAsync().ConfigureAwait(false); - if (!rs.IsSuccess) - { - App.RaiseException(Context, rs.StdErr); - return outs; - } - var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) + try { - var match = REG_FORMAT().Match(line); - if (!match.Success) - continue; - - var change = new Models.Change() { Path = match.Groups[2].Value }; - var status = match.Groups[1].Value; + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); - switch (status) + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line) { - 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; - } + var match = REG_FORMAT().Match(line); + if (!match.Success) + continue; + + var change = new Models.Change() { Path = match.Groups[2].Value }; + var status = match.Groups[1].Value; - if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None) - outs.Add(change); + 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); + } + } + catch + { + // Ignore exceptions. } return outs; diff --git a/src/Commands/QueryRemotes.cs b/src/Commands/QueryRemotes.cs index bd42aabf1..48d7040a0 100644 --- a/src/Commands/QueryRemotes.cs +++ b/src/Commands/QueryRemotes.cs @@ -40,6 +40,16 @@ public QueryRemotes(string repo) if (outs.Find(x => x.Name == remote.Name) != null) continue; + if (remote.URL.StartsWith("git@", StringComparison.Ordinal)) + { + var hostEnd = remote.URL.IndexOf(':', 4); + if (hostEnd > 4) + { + var host = remote.URL.Substring(4, hostEnd - 4); + Models.HTTPSValidator.Add(host); + } + } + outs.Add(remote); } diff --git a/src/Commands/QueryRepositoryRootPath.cs b/src/Commands/QueryRepositoryRootPath.cs index f7e1eb637..89d259296 100644 --- a/src/Commands/QueryRepositoryRootPath.cs +++ b/src/Commands/QueryRepositoryRootPath.cs @@ -10,6 +10,11 @@ public QueryRepositoryRootPath(string path) Args = "rev-parse --show-toplevel"; } + public Result GetResult() + { + return ReadToEnd(); + } + public async Task GetResultAsync() { return await ReadToEndAsync().ConfigureAwait(false); diff --git a/src/Commands/QueryRepositoryStatus.cs b/src/Commands/QueryRepositoryStatus.cs new file mode 100644 index 000000000..32c2489a6 --- /dev/null +++ b/src/Commands/QueryRepositoryStatus.cs @@ -0,0 +1,59 @@ +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public partial class QueryRepositoryStatus : Command + { + [GeneratedRegex(@"ahead\s(\d+)")] + private static partial Regex REG_AHEAD(); + + [GeneratedRegex(@"behind\s(\d+)")] + private static partial Regex REG_BEHIND(); + + public QueryRepositoryStatus(string repo) + { + WorkingDirectory = repo; + RaiseError = false; + } + + public async Task GetResultAsync() + { + Args = "branch -l -v --format=\"%(refname:short)%00%(HEAD)%00%(upstream:track,nobracket)\""; + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (!rs.IsSuccess) + return null; + + var status = new Models.RepositoryStatus(); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var parts = line.Split('\0'); + if (parts.Length != 3 || !parts[1].Equals("*", StringComparison.Ordinal)) + continue; + + status.CurrentBranch = parts[0]; + if (!string.IsNullOrEmpty(parts[2])) + ParseTrackStatus(status, parts[2]); + } + + status.LocalChanges = await new CountLocalChanges(WorkingDirectory, true) { RaiseError = false } + .GetResultAsync() + .ConfigureAwait(false); + + return status; + } + + private void ParseTrackStatus(Models.RepositoryStatus status, string input) + { + var aheadMatch = REG_AHEAD().Match(input); + if (aheadMatch.Success) + status.Ahead = int.Parse(aheadMatch.Groups[1].Value); + + var behindMatch = REG_BEHIND().Match(input); + if (behindMatch.Success) + status.Behind = int.Parse(behindMatch.Groups[1].Value); + } + } +} diff --git a/src/Commands/QueryRevisionByRefName.cs b/src/Commands/QueryRevisionByRefName.cs index 64a03e9df..78104523a 100644 --- a/src/Commands/QueryRevisionByRefName.cs +++ b/src/Commands/QueryRevisionByRefName.cs @@ -11,9 +11,19 @@ public QueryRevisionByRefName(string repo, string refname) Args = $"rev-parse {refname}"; } + public string GetResult() + { + return Parse(ReadToEnd()); + } + public async Task GetResultAsync() { var rs = await ReadToEndAsync().ConfigureAwait(false); + return Parse(rs); + } + + private string Parse(Result rs) + { if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut)) return rs.StdOut.Trim(); diff --git a/src/Commands/QueryRevisionFileNames.cs b/src/Commands/QueryRevisionFileNames.cs index 747534124..e4dcc25c5 100644 --- a/src/Commands/QueryRevisionFileNames.cs +++ b/src/Commands/QueryRevisionFileNames.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Threading.Tasks; namespace SourceGit.Commands @@ -9,17 +10,30 @@ public QueryRevisionFileNames(string repo, string revision) { WorkingDirectory = repo; Context = repo; - Args = $"ls-tree -r -z --name-only {revision}"; + Args = $"ls-tree -r --name-only {revision}"; } public async Task> GetResultAsync() { - var rs = await ReadToEndAsync().ConfigureAwait(false); - if (!rs.IsSuccess) - return []; + var outs = new List(); - var lines = rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries); - return [.. lines]; + try + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); + + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } line) + outs.Add(line); + + await proc.WaitForExitAsync().ConfigureAwait(false); + } + catch + { + // Ignore exceptions. + } + + return outs; } } } diff --git a/src/Commands/QueryRevisionObjects.cs b/src/Commands/QueryRevisionObjects.cs index 9657c7c7e..a7eaaa9e7 100644 --- a/src/Commands/QueryRevisionObjects.cs +++ b/src/Commands/QueryRevisionObjects.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; -using System.IO; +using System.Diagnostics; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -14,47 +15,56 @@ public QueryRevisionObjects(string repo, string sha, string parentFolder) { WorkingDirectory = repo; Context = repo; - Args = $"ls-tree {sha}"; + var builder = new StringBuilder(1024); + builder.Append("ls-tree ").Append(sha); if (!string.IsNullOrEmpty(parentFolder)) - Args += $" -- {parentFolder.Quoted()}"; + builder.Append(" -- ").Append(parentFolder.Quoted()); + + Args = builder.ToString(); } public async Task> GetResultAsync() { var outs = new List(); - var rs = await ReadToEndAsync().ConfigureAwait(false); - if (rs.IsSuccess) + + try { - var sr = new StringReader(rs.StdOut); - while (sr.ReadLine() is { } line) - Parse(outs, line); - } + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); - return outs; - } + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line) + { + var match = REG_FORMAT().Match(line); + if (!match.Success) + continue; - private void Parse(List outs, string line) - { - var match = REG_FORMAT().Match(line); - if (!match.Success) - return; + var obj = new Models.Object(); + obj.SHA = match.Groups[2].Value; + obj.Type = Models.ObjectType.Blob; + obj.Path = match.Groups[3].Value; - var obj = new Models.Object(); - obj.SHA = match.Groups[2].Value; - obj.Type = Models.ObjectType.Blob; - obj.Path = match.Groups[3].Value; + obj.Type = match.Groups[1].Value switch + { + "blob" => Models.ObjectType.Blob, + "tree" => Models.ObjectType.Tree, + "tag" => Models.ObjectType.Tag, + "commit" => Models.ObjectType.Commit, + _ => obj.Type, + }; - obj.Type = match.Groups[1].Value switch + outs.Add(obj); + } + + await proc.WaitForExitAsync().ConfigureAwait(false); + } + catch { - "blob" => Models.ObjectType.Blob, - "tree" => Models.ObjectType.Tree, - "tag" => Models.ObjectType.Tag, - "commit" => Models.ObjectType.Commit, - _ => obj.Type, - }; - - outs.Add(obj); + // Ignore exceptions. + } + + return outs; } } } diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs index 897459f0d..822f5c4a5 100644 --- a/src/Commands/QuerySingleCommit.cs +++ b/src/Commands/QuerySingleCommit.cs @@ -12,31 +12,40 @@ public QuerySingleCommit(string repo, string 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 GetResult() + { + var rs = ReadToEnd(); + return Parse(rs); + } + public async Task GetResultAsync() { var rs = await ReadToEndAsync().ConfigureAwait(false); - if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut)) - { - var commit = new Models.Commit(); - var lines = rs.StdOut.Split('\n'); - if (lines.Length < 8) - return null; + return Parse(rs); + } + + private Models.Commit Parse(Result rs) + { + if (!rs.IsSuccess || string.IsNullOrEmpty(rs.StdOut)) + return null; - commit.SHA = lines[0]; - if (!string.IsNullOrEmpty(lines[1])) - commit.Parents.AddRange(lines[1].Split(' ', StringSplitOptions.RemoveEmptyEntries)); - if (!string.IsNullOrEmpty(lines[2])) - commit.ParseDecorators(lines[2]); - commit.Author = Models.User.FindOrAdd(lines[3]); - commit.AuthorTime = ulong.Parse(lines[4]); - commit.Committer = Models.User.FindOrAdd(lines[5]); - commit.CommitterTime = ulong.Parse(lines[6]); - commit.Subject = lines[7]; + var commit = new Models.Commit(); + var lines = rs.StdOut.Split('\n'); + if (lines.Length < 8) + return null; - return commit; - } + commit.SHA = lines[0]; + if (!string.IsNullOrEmpty(lines[1])) + commit.Parents.AddRange(lines[1].Split(' ', StringSplitOptions.RemoveEmptyEntries)); + if (!string.IsNullOrEmpty(lines[2])) + commit.ParseDecorators(lines[2]); + commit.Author = Models.User.FindOrAdd(lines[3]); + commit.AuthorTime = ulong.Parse(lines[4]); + commit.Committer = Models.User.FindOrAdd(lines[5]); + commit.CommitterTime = ulong.Parse(lines[6]); + commit.Subject = lines[7]; - return null; + return commit; } } } diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs index bec033ff7..229d9e65e 100644 --- a/src/Commands/QueryStagedChangesWithAmend.cs +++ b/src/Commands/QueryStagedChangesWithAmend.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace SourceGit.Commands { @@ -20,9 +19,9 @@ public QueryStagedChangesWithAmend(string repo, string parent) _parent = parent; } - public async Task> GetResultAsync() + public List GetResult() { - var rs = await ReadToEndAsync().ConfigureAwait(false); + var rs = ReadToEnd(); if (!rs.IsSuccess) return []; diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index ba83cb182..8718542eb 100644 --- a/src/Commands/QueryTags.cs +++ b/src/Commands/QueryTags.cs @@ -12,7 +12,7 @@ public QueryTags(string repo) Context = repo; WorkingDirectory = repo; - Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; + Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(taggername)±%(taggeremail)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; } public async Task> GetResultAsync() @@ -26,20 +26,23 @@ public QueryTags(string repo) foreach (var record in records) { var subs = record.Split('\0'); - if (subs.Length != 6) + if (subs.Length != 7) continue; var name = subs[0].Substring(10); - var message = subs[5].Trim(); + var message = subs[6].Trim(); if (!string.IsNullOrEmpty(message) && message.Equals(name, StringComparison.Ordinal)) message = null; + ulong.TryParse(subs[5], out var creatorDate); + tags.Add(new Models.Tag() { Name = name, IsAnnotated = subs[1].Equals("tag", StringComparison.Ordinal), SHA = string.IsNullOrEmpty(subs[3]) ? subs[2] : subs[3], - CreatorDate = ulong.Parse(subs[4]), + Creator = Models.User.FindOrAdd(subs[4]), + CreatorDate = creatorDate, Message = message, }); } @@ -47,6 +50,6 @@ public QueryTags(string repo) return tags; } - private string _boundary = string.Empty; + private readonly string _boundary; } } diff --git a/src/Commands/QueryTrackStatus.cs b/src/Commands/QueryTrackStatus.cs index d687d2745..f00074f82 100644 --- a/src/Commands/QueryTrackStatus.cs +++ b/src/Commands/QueryTrackStatus.cs @@ -5,31 +5,28 @@ namespace SourceGit.Commands { public class QueryTrackStatus : Command { - public QueryTrackStatus(string repo, string local, string upstream) + public QueryTrackStatus(string repo) { WorkingDirectory = repo; Context = repo; - Args = $"rev-list --left-right {local}...{upstream}"; } - public async Task GetResultAsync() + public async Task GetResultAsync(Models.Branch local, Models.Branch remote) { - var status = new Models.BranchTrackStatus(); + Args = $"rev-list --left-right {local.Head}...{remote.Head}"; var rs = await ReadToEndAsync().ConfigureAwait(false); if (!rs.IsSuccess) - return status; + return; var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (line[0] == '>') - status.Behind.Add(line.Substring(1)); + local.Behind.Add(line.Substring(1)); else - status.Ahead.Add(line.Substring(1)); + local.Ahead.Add(line.Substring(1)); } - - return status; } } } diff --git a/src/Commands/QueryUpdatableSubmodules.cs b/src/Commands/QueryUpdatableSubmodules.cs index 05fcc0538..55f429905 100644 --- a/src/Commands/QueryUpdatableSubmodules.cs +++ b/src/Commands/QueryUpdatableSubmodules.cs @@ -7,14 +7,16 @@ namespace SourceGit.Commands { public partial class QueryUpdatableSubmodules : Command { - [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] + [GeneratedRegex(@"^([\-\+])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] private static partial Regex REG_FORMAT_STATUS(); - public QueryUpdatableSubmodules(string repo) + public QueryUpdatableSubmodules(string repo, bool includeUninited) { WorkingDirectory = repo; Context = repo; Args = "submodule status"; + + _includeUninited = includeUninited; } public async Task> GetResultAsync() @@ -30,12 +32,16 @@ public async Task> GetResultAsync() { var stat = match.Groups[1].Value; var path = match.Groups[3].Value; - if (!stat.StartsWith(' ')) - submodules.Add(path); + if (!_includeUninited && stat.StartsWith('-')) + continue; + + submodules.Add(path); } } return submodules; } + + private bool _includeUninited = false; } } diff --git a/src/Commands/Rebase.cs b/src/Commands/Rebase.cs index d08d55ad8..f7f33ac5c 100644 --- a/src/Commands/Rebase.cs +++ b/src/Commands/Rebase.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Text; + +namespace SourceGit.Commands { public class Rebase : Command { @@ -6,10 +8,13 @@ public Rebase(string repo, string basedOn, bool autoStash) { WorkingDirectory = repo; Context = repo; - Args = "rebase "; + + var builder = new StringBuilder(512); + builder.Append("rebase "); if (autoStash) - Args += "--autostash "; - Args += basedOn; + builder.Append("--autostash "); + + Args = builder.Append(basedOn).ToString(); } } } diff --git a/src/Commands/Reset.cs b/src/Commands/Reset.cs index 6a54533b1..cfcd337af 100644 --- a/src/Commands/Reset.cs +++ b/src/Commands/Reset.cs @@ -8,5 +8,12 @@ public Reset(string repo, string revision, string mode) Context = repo; Args = $"reset {mode} {revision}"; } + + public Reset(string repo, string pathspec) + { + WorkingDirectory = repo; + Context = repo; + Args = $"reset HEAD --pathspec-from-file={pathspec.Quoted()}"; + } } } diff --git a/src/Commands/Restore.cs b/src/Commands/Restore.cs index e2f9aa09a..bf3bd0a55 100644 --- a/src/Commands/Restore.cs +++ b/src/Commands/Restore.cs @@ -1,45 +1,12 @@ -using System.Text; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Restore : Command { - /// - /// Only used for single staged change. - /// - /// - /// - public Restore(string repo, Models.Change stagedChange) + public Restore(string repo, string pathspecFile) { WorkingDirectory = repo; Context = repo; - - var builder = new StringBuilder(); - builder.Append("restore --staged -- ").Append(stagedChange.Path.Quoted()); - - if (stagedChange.Index == Models.ChangeState.Renamed) - builder.Append(' ').Append(stagedChange.OriginalPath.Quoted()); - - Args = builder.ToString(); - } - - /// - /// Restore changes given in a path-spec file. - /// - /// - /// - /// - public Restore(string repo, string pathspecFile, bool isStaged) - { - WorkingDirectory = repo; - Context = repo; - - var builder = new StringBuilder(); - builder.Append("restore "); - builder.Append(isStaged ? "--staged " : "--worktree --recurse-submodules "); - builder.Append("--pathspec-from-file=").Append(pathspecFile.Quoted()); - - Args = builder.ToString(); + Args = $"restore --progress --worktree --recurse-submodules --pathspec-from-file={pathspecFile.Quoted()}"; } } } diff --git a/src/Commands/Revert.cs b/src/Commands/Revert.cs index 2e7afd11d..f42a62d05 100644 --- a/src/Commands/Revert.cs +++ b/src/Commands/Revert.cs @@ -1,4 +1,6 @@ -namespace SourceGit.Commands +using System.Text; + +namespace SourceGit.Commands { public class Revert : Command { @@ -6,9 +8,16 @@ public Revert(string repo, string commit, bool autoCommit) { WorkingDirectory = repo; Context = repo; - Args = $"revert -m 1 {commit} --no-edit"; + + var builder = new StringBuilder(512); + builder + .Append("revert -m 1 ") + .Append(commit) + .Append(" --no-edit"); if (!autoCommit) - Args += " --no-commit"; + builder.Append(" --no-commit"); + + Args = builder.ToString(); } } } diff --git a/src/Commands/SaveChangesAsPatch.cs b/src/Commands/SaveChangesAsPatch.cs index c86fc0e06..51bc1319b 100644 --- a/src/Commands/SaveChangesAsPatch.cs +++ b/src/Commands/SaveChangesAsPatch.cs @@ -54,7 +54,7 @@ private static async Task ProcessSingleChangeAsync(string repo, Models.Dif var starter = new ProcessStartInfo(); starter.WorkingDirectory = repo; starter.FileName = Native.OS.GitExecutable; - starter.Arguments = $"diff --ignore-cr-at-eol --unified=4 {opt}"; + starter.Arguments = $"diff --no-color --no-ext-diff --ignore-cr-at-eol --unified=4 {opt}"; starter.UseShellExecute = false; starter.CreateNoWindow = true; starter.WindowStyle = ProcessWindowStyle.Hidden; @@ -62,7 +62,7 @@ private static async Task ProcessSingleChangeAsync(string repo, Models.Dif try { - using var proc = Process.Start(starter); + using var proc = Process.Start(starter)!; await proc.StandardOutput.BaseStream.CopyToAsync(writer).ConfigureAwait(false); await proc.WaitForExitAsync().ConfigureAwait(false); return proc.ExitCode == 0; diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index a3ca373f8..e05d6d358 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -9,7 +9,7 @@ public static class SaveRevisionFile { public static async Task RunAsync(string repo, string revision, string file, string saveTo) { - var dir = Path.GetDirectoryName(saveTo); + var dir = Path.GetDirectoryName(saveTo) ?? string.Empty; if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); @@ -42,7 +42,7 @@ private static async Task ExecCmdAsync(string repo, string args, string outputFi { try { - using var proc = Process.Start(starter); + using var proc = Process.Start(starter)!; if (input != null) { diff --git a/src/Commands/SharedIssueTracker.cs b/src/Commands/SharedIssueTracker.cs deleted file mode 100644 index 3e96fbeb7..000000000 --- a/src/Commands/SharedIssueTracker.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace SourceGit.Commands -{ - public class SharedIssueTracker : Command - { - public SharedIssueTracker(string repo) - { - WorkingDirectory = repo; - Context = repo; - _file = $"{repo}/.issuetracker"; - } - - public async Task> ReadAllAsync() - { - if (!File.Exists(_file)) - return []; - - Args = $"config -f {_file.Quoted()} -l"; - - var output = await ReadToEndAsync().ConfigureAwait(false); - var rs = new List(); - if (output.IsSuccess) - { - var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - var parts = line.Split('=', 2); - if (parts.Length < 2) - continue; - - var key = parts[0]; - var value = parts[1]; - - if (!key.StartsWith("issuetracker.", StringComparison.Ordinal)) - continue; - - if (key.EndsWith(".regex", StringComparison.Ordinal)) - { - var prefixLen = "issuetracker.".Length; - var suffixLen = ".regex".Length; - var ruleName = key.Substring(prefixLen, key.Length - prefixLen - suffixLen); - FindOrAdd(rs, ruleName).RegexString = value; - } - else if (key.EndsWith(".url", StringComparison.Ordinal)) - { - var prefixLen = "issuetracker.".Length; - var suffixLen = ".url".Length; - var ruleName = key.Substring(prefixLen, key.Length - prefixLen - suffixLen); - FindOrAdd(rs, ruleName).URLTemplate = value; - } - } - } - - return rs; - } - - public async Task AddAsync(Models.IssueTrackerRule rule) - { - Args = $"config -f {_file.Quoted()} issuetracker.{rule.Name.Quoted()}.regex {rule.RegexString.Quoted()}"; - - var succ = await ExecAsync().ConfigureAwait(false); - if (succ) - { - Args = $"config -f {_file.Quoted()} issuetracker.{rule.Name.Quoted()}.url {rule.URLTemplate.Quoted()}"; - return await ExecAsync().ConfigureAwait(false); - } - - return false; - } - - public async Task RemoveAsync(Models.IssueTrackerRule rule) - { - Args = $"config -f {_file.Quoted()} --remove-section issuetracker.{rule.Name.Quoted()}"; - return await ExecAsync().ConfigureAwait(false); - } - - private Models.IssueTrackerRule FindOrAdd(List rules, string ruleName) - { - var rule = rules.Find(x => x.Name.Equals(ruleName, StringComparison.Ordinal)); - if (rule != null) - return rule; - - rule = new Models.IssueTrackerRule() { IsShared = true, Name = ruleName }; - rules.Add(rule); - return rule; - } - - private readonly string _file; - } -} diff --git a/src/Commands/UnstageChangesForAmend.cs b/src/Commands/UpdateIndexInfo.cs similarity index 84% rename from src/Commands/UnstageChangesForAmend.cs rename to src/Commands/UpdateIndexInfo.cs index 0b2b7e478..3633a2823 100644 --- a/src/Commands/UnstageChangesForAmend.cs +++ b/src/Commands/UpdateIndexInfo.cs @@ -6,9 +6,9 @@ namespace SourceGit.Commands { - public class UnstageChangesForAmend + public class UpdateIndexInfo { - public UnstageChangesForAmend(string repo, List changes) + public UpdateIndexInfo(string repo, List changes) { _repo = repo; @@ -18,7 +18,7 @@ public UnstageChangesForAmend(string repo, List changes) { _patchBuilder.Append("0 0000000000000000000000000000000000000000\t"); _patchBuilder.Append(c.Path); - _patchBuilder.Append("\0100644 "); + _patchBuilder.Append("\n100644 "); _patchBuilder.Append(c.DataForAmend.ObjectHash); _patchBuilder.Append("\t"); _patchBuilder.Append(c.OriginalPath); @@ -60,10 +60,12 @@ public async Task ExecAsync() starter.RedirectStandardInput = true; starter.RedirectStandardOutput = false; starter.RedirectStandardError = true; + starter.StandardInputEncoding = new UTF8Encoding(false); + starter.StandardErrorEncoding = Encoding.UTF8; try { - using var proc = Process.Start(starter); + using var proc = Process.Start(starter)!; await proc.StandardInput.WriteAsync(_patchBuilder.ToString()); proc.StandardInput.Close(); @@ -78,12 +80,12 @@ public async Task ExecAsync() } catch (Exception e) { - App.RaiseException(_repo, "Failed to unstage changes: " + e.Message); + App.RaiseException(_repo, "Failed to update index: " + e.Message); return false; } } - private string _repo = ""; - private StringBuilder _patchBuilder = new StringBuilder(); + private readonly string _repo; + private readonly StringBuilder _patchBuilder = new(); } } diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs index af03029f6..50ee97242 100644 --- a/src/Commands/Worktree.cs +++ b/src/Commands/Worktree.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading.Tasks; namespace SourceGit.Commands @@ -30,26 +31,32 @@ public Worktree(string repo) last = new Models.Worktree() { FullPath = line.Substring(9).Trim() }; last.RelativePath = Path.GetRelativePath(WorkingDirectory, last.FullPath); worktrees.Add(last); + continue; } - else if (line.StartsWith("bare", StringComparison.Ordinal)) + + if (last == null) + continue; + + if (line.StartsWith("bare", StringComparison.Ordinal)) { - last!.IsBare = true; + worktrees.Remove(last); + last = null; } else if (line.StartsWith("HEAD ", StringComparison.Ordinal)) { - last!.Head = line.Substring(5).Trim(); + last.Head = line.Substring(5).Trim(); } else if (line.StartsWith("branch ", StringComparison.Ordinal)) { - last!.Branch = line.Substring(7).Trim(); + last.Branch = line.Substring(7).Trim(); } else if (line.StartsWith("detached", StringComparison.Ordinal)) { - last!.IsDetached = true; + last.IsDetached = true; } else if (line.StartsWith("locked", StringComparison.Ordinal)) { - last!.IsLocked = true; + last.IsLocked = true; } } } @@ -59,26 +66,20 @@ public Worktree(string repo) public async Task AddAsync(string fullpath, string name, bool createNew, string tracking) { - Args = "worktree add "; - + var builder = new StringBuilder(1024); + builder.Append("worktree add "); if (!string.IsNullOrEmpty(tracking)) - Args += "--track "; - + builder.Append("--track "); if (!string.IsNullOrEmpty(name)) - { - if (createNew) - Args += $"-b {name} "; - else - Args += $"-B {name} "; - } - - Args += $"{fullpath.Quoted()} "; + builder.Append(createNew ? "-b " : "-B ").Append(name).Append(' '); + builder.Append(fullpath.Quoted()).Append(' '); if (!string.IsNullOrEmpty(tracking)) - Args += tracking; + builder.Append(tracking); else if (!string.IsNullOrEmpty(name) && !createNew) - Args += name; + builder.Append(name); + Args = builder.ToString(); return await ExecAsync().ConfigureAwait(false); } diff --git a/src/Converters/BoolConverters.cs b/src/Converters/BoolConverters.cs index 3563fb37c..8a2f31416 100644 --- a/src/Converters/BoolConverters.cs +++ b/src/Converters/BoolConverters.cs @@ -5,10 +5,10 @@ namespace SourceGit.Converters { public static class BoolConverters { - 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); + new FuncValueConverter(x => x ? FontWeight.Bold : FontWeight.Regular); + + public static readonly FuncValueConverter IsMergedToOpacity = + new FuncValueConverter(x => x ? 1 : 0.65); } } diff --git a/src/Converters/DirtyStateConverters.cs b/src/Converters/DirtyStateConverters.cs new file mode 100644 index 000000000..f140f7d3c --- /dev/null +++ b/src/Converters/DirtyStateConverters.cs @@ -0,0 +1,28 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace SourceGit.Converters +{ + public static class DirtyStateConverters + { + public static readonly FuncValueConverter ToBrush = + new FuncValueConverter(v => + { + if (v.HasFlag(Models.DirtyState.HasLocalChanges)) + return Brushes.Gray; + if (v.HasFlag(Models.DirtyState.HasPendingPullOrPush)) + return Brushes.RoyalBlue; + return Brushes.Transparent; + }); + + public static readonly FuncValueConverter ToDesc = + new FuncValueConverter(v => + { + if (v.HasFlag(Models.DirtyState.HasLocalChanges)) + return " • " + App.Text("DirtyState.HasLocalChanges"); + if (v.HasFlag(Models.DirtyState.HasPendingPullOrPush)) + return " • " + App.Text("DirtyState.HasPendingPullOrPush"); + return " • " + App.Text("DirtyState.UpToDate"); + }); + } +} diff --git a/src/Converters/DoubleConverters.cs b/src/Converters/DoubleConverters.cs index 5b7c0a03d..871a80b3e 100644 --- a/src/Converters/DoubleConverters.cs +++ b/src/Converters/DoubleConverters.cs @@ -1,4 +1,5 @@ -using Avalonia.Data.Converters; +using Avalonia; +using Avalonia.Data.Converters; namespace SourceGit.Converters { @@ -11,9 +12,12 @@ public static class DoubleConverters new FuncValueConverter(v => v - 1.0); public static readonly FuncValueConverter ToPercentage = - new FuncValueConverter(v => (v * 100).ToString("F3") + "%"); + new FuncValueConverter(v => (v * 100).ToString("F0") + "%"); public static readonly FuncValueConverter OneMinusToPercentage = - new FuncValueConverter(v => ((1.0 - v) * 100).ToString("F3") + "%"); + new FuncValueConverter(v => ((1.0 - v) * 100).ToString("F0") + "%"); + + public static readonly FuncValueConverter ToLeftMargin = + new FuncValueConverter(v => new Thickness(v, 0, 0, 0)); } } diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index f21c5d240..7d2dabe25 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -32,12 +32,6 @@ public static class IntConverters new FuncValueConverter(v => new Thickness(v * 16, 0, 0, 0)); public static readonly FuncValueConverter ToBookmarkBrush = - new FuncValueConverter(bookmark => - { - if (bookmark == 0) - return Application.Current?.FindResource("Brush.FG1") as IBrush; - else - return Models.Bookmarks.Brushes[bookmark]; - }); + new FuncValueConverter(v => Models.Bookmarks.Get(v) ?? App.Current?.FindResource("Brush.FG1") as IBrush); } } diff --git a/src/Converters/InteractiveRebaseActionConverters.cs b/src/Converters/InteractiveRebaseActionConverters.cs index 3534c809f..76967ce3e 100644 --- a/src/Converters/InteractiveRebaseActionConverters.cs +++ b/src/Converters/InteractiveRebaseActionConverters.cs @@ -21,8 +21,5 @@ public static class InteractiveRebaseActionConverters public static readonly FuncValueConverter ToName = new FuncValueConverter(v => v.ToString()); - - public static readonly FuncValueConverter CanEditMessage = - new FuncValueConverter(v => v == Models.InteractiveRebaseAction.Reword || v == Models.InteractiveRebaseAction.Squash); } } diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs index 6f3ae98b0..e0c5967e8 100644 --- a/src/Converters/ListConverters.cs +++ b/src/Converters/ListConverters.cs @@ -7,9 +7,6 @@ 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})"); diff --git a/src/Converters/PathConverters.cs b/src/Converters/PathConverters.cs index ac1e61e52..23dae2ab3 100644 --- a/src/Converters/PathConverters.cs +++ b/src/Converters/PathConverters.cs @@ -1,6 +1,4 @@ -using System; using System.IO; - using Avalonia.Data.Converters; namespace SourceGit.Converters @@ -14,17 +12,6 @@ public static class PathConverters new(v => Path.GetDirectoryName(v) ?? ""); public static readonly FuncValueConverter RelativeToHome = - new(v => - { - if (OperatingSystem.IsWindows()) - return v; - - var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; - if (v.StartsWith(home, StringComparison.Ordinal)) - return $"~{v.AsSpan(prefixLen)}"; - - return v; - }); + new(Native.OS.GetRelativePathToHome); } } diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index 56c00fb4d..7d2821914 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -86,6 +86,6 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu new FuncValueConverter(v => v != null && v.Trim().Length > 0); public static readonly FuncValueConverter ToFriendlyUpstream = - new FuncValueConverter(v => v != null ? v.Substring(13) : string.Empty); + new FuncValueConverter(v => v is { Length: > 13 } ? v.Substring(13) : string.Empty); } } diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index 69e12819b..cf4eb3e38 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -51,7 +51,7 @@ public void Start() LoadDefaultAvatar("noreply@github.com", "github.png"); LoadDefaultAvatar("unrealbot@epicgames.com", "unreal.png"); - Task.Run(() => + Task.Run(async () => { while (true) { @@ -84,7 +84,7 @@ public void Start() { using var client = new HttpClient(); client.Timeout = TimeSpan.FromSeconds(2); - var rsp = client.GetAsync(url).Result; + var rsp = await client.GetAsync(url); if (rsp.IsSuccessStatusCode) { using (var stream = rsp.Content.ReadAsStream()) @@ -190,9 +190,6 @@ public void SetFromLocal(string email, string file) image = Bitmap.DecodeToWidth(stream, 128); } - if (image == null) - return; - _resources[email] = image; lock (_synclock) diff --git a/src/Models/Bookmarks.cs b/src/Models/Bookmarks.cs index 37cf689b7..ae3b2abd3 100644 --- a/src/Models/Bookmarks.cs +++ b/src/Models/Bookmarks.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; - -namespace SourceGit.Models +namespace SourceGit.Models { public static class Bookmarks { public static readonly Avalonia.Media.IBrush[] Brushes = [ - Avalonia.Media.Brushes.Transparent, + null, Avalonia.Media.Brushes.Red, Avalonia.Media.Brushes.Orange, Avalonia.Media.Brushes.Gold, @@ -15,12 +13,9 @@ public static class Bookmarks Avalonia.Media.Brushes.Purple, ]; - public static readonly List Supported = new List(); - - static Bookmarks() + public static Avalonia.Media.IBrush Get(int i) { - for (int i = 0; i < Brushes.Length; i++) - Supported.Add(i); + return (i >= 0 && i < Brushes.Length) ? Brushes[i] : null; } } } diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index 350bc5b5a..47aa2153a 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -1,36 +1,14 @@ using System.Collections.Generic; -using System.Text.RegularExpressions; namespace SourceGit.Models { - public class BranchTrackStatus - { - 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) - return string.Empty; - - var track = ""; - if (Ahead.Count > 0) - track += $"{Ahead.Count}↑"; - if (Behind.Count > 0) - track += $" {Behind.Count}↓"; - return track.Trim(); - } - } - public enum BranchSortMode { Name = 0, CommitterDate, } - public partial class Branch + public class Branch { public string Name { get; set; } public string FullName { get; set; } @@ -40,18 +18,27 @@ public partial class Branch public bool IsCurrent { get; set; } public bool IsDetachedHead { get; set; } public string Upstream { get; set; } - public BranchTrackStatus TrackStatus { get; set; } + public List Ahead { get; set; } = []; + public List Behind { get; set; } = []; public string Remote { get; set; } public bool IsUpstreamGone { get; set; } + public string WorktreePath { get; set; } + public bool HasWorktree => !IsCurrent && !string.IsNullOrEmpty(WorktreePath); public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}"; + public bool IsTrackStatusVisible => Ahead.Count > 0 || Behind.Count > 0; - [GeneratedRegex(@"\s+")] - private static partial Regex REG_FIX_NAME(); - - public static string FixName(string name) + public string TrackStatusDescription { - return REG_FIX_NAME().Replace(name, "-"); + get + { + var ahead = Ahead.Count; + var behind = Behind.Count; + if (ahead > 0) + return behind > 0 ? $"{ahead}↑ {behind}↓" : $"{ahead}↑"; + + return behind > 0 ? $"{behind}↓" : string.Empty; + } } } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 61438424a..4a98b985a 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using Avalonia; -using Avalonia.Media; - namespace SourceGit.Models { public enum CommitSearchMethod @@ -18,15 +15,8 @@ public enum CommitSearchMethod public class Commit { - // As retrieved by: git mktree Parents { get; set; } = new(); public List Decorators { get; set; } = new(); - public bool HasDecorators => Decorators.Count > 0; + + public bool IsMerged { get; set; } = false; + public int Color { get; set; } = 0; + public double LeftMargin { get; set; } = 0; 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; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; - - 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(0); - public IBrush Brush => CommitGraph.Pens[Color].Brush; + public bool HasDecorators => Decorators.Count > 0; public string GetFriendlyName() { @@ -65,6 +52,14 @@ public string GetFriendlyName() return SHA[..10]; } + public void ParseParents(string data) + { + if (data.Length < 8) + return; + + Parents.AddRange(data.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + public void ParseDecorators(string data) { if (data.Length < 3) diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index cb5696101..82505c351 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -6,12 +6,7 @@ namespace SourceGit.Models { - public record CommitGraphLayout(double startY, double clipWidth, double rowHeight) - { - public double StartY { get; set; } = startY; - public double ClipWidth { get; set; } = clipWidth; - public double RowHeight { get; set; } = rowHeight; - } + public record CommitGraphLayout(double StartY, double ClipWidth, double RowHeight); public class CommitGraph { @@ -204,8 +199,8 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable // Margins & merge state (used by Views.Histories). commit.IsMerged = isMerged; - commit.Margin = new Thickness(Math.Max(offsetX, maxOffsetOld) + halfWidth + 2, 0, 0, 0); commit.Color = dotColor; + commit.LeftMargin = Math.Max(offsetX, maxOffsetOld) + halfWidth + 2; } // Deal with curves haven't ended yet. diff --git a/src/Models/CommitLink.cs b/src/Models/CommitLink.cs index 08caad8d1..fa78f206c 100644 --- a/src/Models/CommitLink.cs +++ b/src/Models/CommitLink.cs @@ -5,8 +5,8 @@ namespace SourceGit.Models { public class CommitLink { - public string Name { get; set; } = null; - public string URLPrefix { get; set; } = null; + public string Name { get; } = null; + public string URLPrefix { get; } = null; public CommitLink(string name, string prefix) { @@ -20,26 +20,28 @@ public static List Get(List remotes) foreach (var remote in remotes) { - if (remote.TryGetVisitURL(out var url)) + if (remote.TryGetVisitURL(out var link)) { - var trimmedUrl = url.AsSpan(); - if (url.EndsWith(".git")) - trimmedUrl = url.AsSpan(0, url.Length - 4); + var uri = new Uri(link, UriKind.Absolute); + var host = uri.Host; + var route = uri.AbsolutePath.TrimStart('/'); - if (url.StartsWith("https://github.com/", StringComparison.Ordinal)) - outs.Add(new($"GitHub ({trimmedUrl[19..]})", $"{url}/commit/")); - else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal)) - outs.Add(new($"GitLab ({trimmedUrl[(trimmedUrl[15..].IndexOf('/') + 16)..]})", $"{url}/-/commit/")); - else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal)) - outs.Add(new($"Gitee ({trimmedUrl[18..]})", $"{url}/commit/")); - else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal)) - outs.Add(new($"BitBucket ({trimmedUrl[22..]})", $"{url}/commits/")); - else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal)) - outs.Add(new($"Codeberg ({trimmedUrl[21..]})", $"{url}/commit/")); - else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal)) - outs.Add(new($"Gitea ({trimmedUrl[18..]})", $"{url}/commit/")); - else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal)) - outs.Add(new($"sourcehut ({trimmedUrl[18..]})", $"{url}/commit/")); + if (host.Equals("github.com", StringComparison.Ordinal)) + outs.Add(new($"GitHub ({route})", $"{link}/commit/")); + else if (host.Contains("gitlab", StringComparison.Ordinal)) + outs.Add(new($"GitLab ({route})", $"{link}/-/commit/")); + else if (host.Equals("gitee.com", StringComparison.Ordinal)) + outs.Add(new($"Gitee ({route})", $"{link}/commit/")); + else if (host.Equals("bitbucket.org", StringComparison.Ordinal)) + outs.Add(new($"BitBucket ({route})", $"{link}/commits/")); + else if (host.Equals("codeberg.org", StringComparison.Ordinal)) + outs.Add(new($"Codeberg ({route})", $"{link}/commit/")); + else if (host.Equals("gitea.org", StringComparison.Ordinal)) + outs.Add(new($"Gitea ({route})", $"{link}/commit/")); + else if (host.Equals("git.sr.ht", StringComparison.Ordinal)) + outs.Add(new($"sourcehut ({route})", $"{link}/commit/")); + else if (host.Equals("gitcode.com", StringComparison.Ordinal)) + outs.Add(new($"GitCode ({route})", $"{link}/commit/")); } } diff --git a/src/Models/ConfirmEmptyCommitResult.cs b/src/Models/ConfirmEmptyCommitResult.cs new file mode 100644 index 000000000..176845b9a --- /dev/null +++ b/src/Models/ConfirmEmptyCommitResult.cs @@ -0,0 +1,9 @@ +namespace SourceGit.Models +{ + public enum ConfirmEmptyCommitResult + { + Cancel = 0, + StageAllAndCommit, + CreateEmptyCommit, + } +} diff --git a/src/Models/ConventionalCommitType.cs b/src/Models/ConventionalCommitType.cs index 531a16c07..9275ff116 100644 --- a/src/Models/ConventionalCommitType.cs +++ b/src/Models/ConventionalCommitType.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.IO; +using System.Text.Json; namespace SourceGit.Models { @@ -8,26 +10,39 @@ public class ConventionalCommitType public string Type { get; set; } public string Description { get; set; } - 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 name, string type, string description) { Name = name; Type = type; Description = description; } + + public static List Load(string storageFile) + { + try + { + if (!string.IsNullOrEmpty(storageFile) && File.Exists(storageFile)) + return JsonSerializer.Deserialize(File.ReadAllText(storageFile), JsonCodeGen.Default.ListConventionalCommitType) ?? []; + } + catch + { + // Ignore errors. + } + + return new List { + 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"), + }; + } } } diff --git a/src/Models/CustomAction.cs b/src/Models/CustomAction.cs index 1ed65b8b3..ec500d3c0 100644 --- a/src/Models/CustomAction.cs +++ b/src/Models/CustomAction.cs @@ -9,6 +9,8 @@ public enum CustomActionScope Commit, Branch, Tag, + Remote, + File, } public enum CustomActionControlType @@ -19,6 +21,8 @@ public enum CustomActionControlType ComboBox, } + public record CustomActionTargetFile(string File, Commit Revision); + public class CustomActionControl : ObservableObject { public CustomActionControlType Type diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index 95ffa99f1..6c381df35 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -1,8 +1,6 @@ using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; - -using Avalonia; using Avalonia.Media.Imaging; namespace SourceGit.Models @@ -16,7 +14,7 @@ public enum TextDiffLineType Deleted, } - public class TextInlineRange(int p, int n) + public class TextRange(int p, int n) { public int Start { get; set; } = p; public int End { get; set; } = p + n - 1; @@ -28,7 +26,7 @@ public class TextDiffLine public string Content { get; set; } = ""; public int OldLineNumber { get; set; } = 0; public int NewLineNumber { get; set; } = 0; - public List Highlights { get; set; } = new List(); + public List Highlights { get; set; } = new List(); public bool NoNewLineEndOfFile { get; set; } = false; public string OldLine => OldLineNumber == 0 ? string.Empty : OldLineNumber.ToString(); @@ -49,26 +47,15 @@ public class TextDiffSelection public int StartLine { get; set; } = 0; public int EndLine { get; set; } = 0; public bool HasChanges { get; set; } = false; - public bool HasLeftChanges { get; set; } = false; public int IgnoredAdds { get; set; } = 0; public int IgnoredDeletes { get; set; } = 0; - - public bool IsInRange(int idx) - { - return idx >= StartLine - 1 && idx < EndLine; - } } public partial class TextDiff { - public string File { get; set; } = string.Empty; public List Lines { get; set; } = new List(); - public Vector ScrollOffset { get; set; } = Vector.Zero; public int MaxLineNumber = 0; - public string Repo { get; set; } = null; - public DiffOption Option { get; set; } = null; - public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombined, bool isOldSide) { var rs = new TextDiffSelection(); @@ -79,15 +66,9 @@ public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombin { var line = Lines[i]; if (line.Type == TextDiffLineType.Added) - { - rs.HasLeftChanges = true; rs.IgnoredAdds++; - } else if (line.Type == TextDiffLineType.Deleted) - { - rs.HasLeftChanges = true; rs.IgnoredDeletes++; - } } for (int i = startLine - 1; i < endLine; i++) @@ -95,48 +76,19 @@ public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombin var line = Lines[i]; if (line.Type == TextDiffLineType.Added) { - if (isCombined) + if (isCombined || !isOldSide) { rs.HasChanges = true; break; } - if (isOldSide) - { - rs.HasLeftChanges = true; - } - else - { - rs.HasChanges = true; - } } else if (line.Type == TextDiffLineType.Deleted) { - if (isCombined) + if (isCombined || isOldSide) { rs.HasChanges = true; break; } - if (isOldSide) - { - rs.HasChanges = true; - } - else - { - rs.HasLeftChanges = true; - } - } - } - - if (!rs.HasLeftChanges) - { - for (int i = endLine; i < Lines.Count; i++) - { - var line = Lines[i]; - if (line.Type == TextDiffLineType.Added || line.Type == TextDiffLineType.Deleted) - { - rs.HasLeftChanges = true; - break; - } } } @@ -149,6 +101,7 @@ public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, Te var fileGuid = isTracked ? fileBlobGuid : "00000000"; using var writer = new StreamWriter(output); + writer.NewLine = "\n"; writer.WriteLine($"diff --git a/{change.Path} b/{change.Path}"); if (!revert && !isTracked) writer.WriteLine("new file mode 100644"); @@ -169,7 +122,11 @@ public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, Te var line = Lines[i]; if (line.Type != TextDiffLineType.Added) continue; - writer.WriteLine($"{(selection.IsInRange(i) ? "+" : " ")}{line.Content}"); + + if (i >= selection.StartLine - 1 && i < selection.EndLine) + writer.WriteLine($"+{line.Content}"); + else + writer.WriteLine($" {line.Content}"); } } else @@ -193,6 +150,7 @@ public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextD var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path; using var writer = new StreamWriter(output); + writer.NewLine = "\n"; writer.WriteLine($"diff --git a/{change.Path} b/{change.Path}"); writer.WriteLine($"index 00000000...{fileTreeGuid} 100644"); writer.WriteLine($"--- a/{orgFile}"); @@ -260,16 +218,16 @@ public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextD else if (line.Type == TextDiffLineType.Added) { if (revert) - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } else if (line.Type == TextDiffLineType.Deleted) { if (!revert) - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } else if (line.Type == TextDiffLineType.Normal) { - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } } } @@ -285,15 +243,15 @@ public void GeneratePatchFromSelection(Change change, string fileTreeGuid, TextD } else if (line.Type == TextDiffLineType.Normal) { - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } else if (line.Type == TextDiffLineType.Added) { - writer.WriteLine($"+{line.Content}"); + WriteLine(writer, '+', line); } else if (line.Type == TextDiffLineType.Deleted) { - writer.WriteLine($"-{line.Content}"); + WriteLine(writer, '-', line); } } @@ -306,6 +264,7 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path; using var writer = new StreamWriter(output); + writer.NewLine = "\n"; writer.WriteLine($"diff --git a/{change.Path} b/{change.Path}"); writer.WriteLine($"index 00000000...{fileTreeGuid} 100644"); writer.WriteLine($"--- a/{orgFile}"); @@ -382,16 +341,16 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG else if (line.Type == TextDiffLineType.Added) { if (revert) - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } else if (line.Type == TextDiffLineType.Deleted) { if (!revert) - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } else if (line.Type == TextDiffLineType.Normal) { - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } } } @@ -407,7 +366,7 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG } else if (line.Type == TextDiffLineType.Normal) { - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } else if (line.Type == TextDiffLineType.Added) { @@ -415,7 +374,7 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG { if (revert) { - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } else { @@ -424,20 +383,20 @@ public void GeneratePatchFromSelectionSingleSide(Change change, string fileTreeG } else { - writer.WriteLine($"+{line.Content}"); + WriteLine(writer, '+', line); } } else if (line.Type == TextDiffLineType.Deleted) { if (isOldSide) { - writer.WriteLine($"-{line.Content}"); + WriteLine(writer, '-', line); } else { if (!revert) { - writer.WriteLine($" {line.Content}"); + WriteLine(writer, ' ', line); } else { @@ -600,6 +559,14 @@ private bool ProcessIndicatorForPatchSingleSide(StreamWriter writer, TextDiffLin return true; } + private static void WriteLine(StreamWriter writer, char prefix, TextDiffLine line) + { + writer.WriteLine($"{prefix}{line.Content}"); + + if (line.NoNewLineEndOfFile) + writer.WriteLine("\\ No newline at end of file"); + } + [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] private static partial Regex REG_INDICATOR(); } diff --git a/src/Models/ExternalMerger.cs b/src/Models/ExternalMerger.cs index ed9b9b08a..655a1d58a 100644 --- a/src/Models/ExternalMerger.cs +++ b/src/Models/ExternalMerger.cs @@ -7,14 +7,13 @@ namespace SourceGit.Models { - public class ExternalMerger + public class ExternalMerger(string icon, string name, string finder, string mergeCmd, string diffCmd) { - public int Type { get; set; } - public string Icon { get; set; } - public string Name { get; set; } - public string Exec { get; set; } - public string Cmd { get; set; } - public string DiffCmd { get; set; } + public string Icon { get; } = icon; + public string Name { get; } = name; + public string Finder { get; } = finder; + public string MergeCmd { get; } = mergeCmd; + public string DiffCmd { get; } = diffCmd; public Bitmap IconImage { @@ -32,74 +31,69 @@ static ExternalMerger() if (OperatingSystem.IsWindows()) { Supported = new List() { - new ExternalMerger(0, "git", "Use Git Settings", "", "", ""), - new ExternalMerger(1, "vscode", "Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(2, "vscode_insiders", "Visual Studio Code - Insiders", "Code - Insiders.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(3, "vs", "Visual Studio", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" /m", "\"$LOCAL\" \"$REMOTE\""), - 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", "\"$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\""), - new ExternalMerger(12, "cursor", "Cursor", "Cursor.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("git", "Use Git Settings", "", "", ""), + new ExternalMerger("vscode", "Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("vscode_insiders", "Visual Studio Code - Insiders", "Code - Insiders.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("vs", "Visual Studio", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" /m", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("tortoise_merge", "Tortoise Merge", "TortoiseMerge.exe;TortoiseGitMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", "-base:\"$LOCAL\" -theirs:\"$REMOTE\""), + new ExternalMerger("kdiff3", "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("beyond_compare", "Beyond Compare", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e -sw \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("plastic_merge", "Plastic SCM", "mergetool.exe", "-s=\"$REMOTE\" -b=\"$BASE\" -d=\"$LOCAL\" -r=\"$MERGED\" --automatic", "-s=\"$LOCAL\" -d=\"$REMOTE\""), + new ExternalMerger("meld", "Meld", "Meld.exe", "\"$LOCAL\" \"$BASE\" \"$REMOTE\" --output \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("cursor", "Cursor", "Cursor.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), }; } else if (OperatingSystem.IsMacOS()) { Supported = new List() { - new ExternalMerger(0, "git", "Use Git Settings", "", "", ""), - new ExternalMerger(1, "xcode", "FileMerge", "/usr/bin/opendiff", "\"$BASE\" \"$LOCAL\" \"$REMOTE\" -ancestor \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(2, "vscode", "Visual Studio Code", "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(3, "vscode_insiders", "Visual Studio Code - Insiders", "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(4, "kdiff3", "KDiff3", "/Applications/kdiff3.app/Contents/MacOS/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(5, "beyond_compare", "Beyond Compare", "/Applications/Beyond Compare.app/Contents/MacOS/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(6, "codium", "VSCodium", "/Applications/VSCodium.app/Contents/Resources/app/bin/codium", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(7, "p4merge", "P4Merge", "/Applications/p4merge.app/Contents/Resources/launchp4merge", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""), - new ExternalMerger(8, "cursor", "Cursor", "/Applications/Cursor.app/Contents/Resources/app/bin/cursor", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("git", "Use Git Settings", "", "", ""), + new ExternalMerger("xcode", "FileMerge", "/usr/bin/opendiff", "\"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("vscode", "Visual Studio Code", "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("vscode_insiders", "Visual Studio Code - Insiders", "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("kdiff3", "KDiff3", "/Applications/kdiff3.app/Contents/MacOS/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("beyond_compare", "Beyond Compare", "/Applications/Beyond Compare.app/Contents/MacOS/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("codium", "VSCodium", "/Applications/VSCodium.app/Contents/Resources/app/bin/codium", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("p4merge", "P4Merge", "/Applications/p4merge.app/Contents/Resources/launchp4merge", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("cursor", "Cursor", "/Applications/Cursor.app/Contents/Resources/app/bin/cursor", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), }; } else if (OperatingSystem.IsLinux()) { Supported = new List() { - new ExternalMerger(0, "git", "Use Git Settings", "", "", ""), - new ExternalMerger(1, "vscode", "Visual Studio Code", "/usr/share/code/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), - 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(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\""), - new ExternalMerger(8, "cursor", "Cursor", "cursor", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("git", "Use Git Settings", "", "", ""), + new ExternalMerger("vscode", "Visual Studio Code", "/usr/share/code/code", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("vscode_insiders", "Visual Studio Code - Insiders", "/usr/share/code-insiders/code-insiders", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("kdiff3", "KDiff3", "/usr/bin/kdiff3", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("beyond_compare", "Beyond Compare", "/usr/bin/bcomp", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("meld", "Meld", "/usr/bin/meld", "\"$LOCAL\" \"$BASE\" \"$REMOTE\" --output \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("codium", "VSCodium", "/usr/share/codium/bin/codium", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("p4merge", "P4Merge", "/usr/local/bin/p4merge", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""), + new ExternalMerger("cursor", "Cursor", "cursor", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""), }; } else { Supported = new List() { - new ExternalMerger(0, "git", "Use Git Settings", "", "", ""), + new ExternalMerger("git", "Use Git Settings", "", "", ""), }; } } - public ExternalMerger(int type, string icon, string name, string exec, string cmd, string diffCmd) - { - Type = type; - Icon = icon; - Name = name; - Exec = exec; - Cmd = cmd; - DiffCmd = diffCmd; - } - - public string[] GetPatterns() + public string[] GetPatternsToFindExecFile() { if (OperatingSystem.IsWindows()) - return Exec.Split(';'); + return Finder.Split(';', StringSplitOptions.RemoveEmptyEntries); - var choices = Exec.Split(';', StringSplitOptions.RemoveEmptyEntries); - return Array.ConvertAll(choices, Path.GetFileName); + return [Path.GetFileName(Finder)]; } } + + public class DiffMergeTool(string exec, string cmd) + { + public string Exec { get; } = exec; + public string Cmd { get; } = cmd; + } } diff --git a/src/Models/ExternalTool.cs b/src/Models/ExternalTool.cs index 377eba2f2..0f272ab35 100644 --- a/src/Models/ExternalTool.cs +++ b/src/Models/ExternalTool.cs @@ -12,14 +12,15 @@ namespace SourceGit.Models { public class ExternalTool { - public string Name { get; private set; } - public Bitmap IconImage { get; private set; } = null; + public string Name { get; } + public string ExecFile { get; } + public Bitmap IconImage { get; } public ExternalTool(string name, string icon, string execFile, Func execArgsGenerator = null) { Name = name; - _execFile = execFile; - _execArgsGenerator = execArgsGenerator ?? (repo => repo.Quoted()); + ExecFile = execFile; + _execArgsGenerator = execArgsGenerator ?? (path => path.Quoted()); try { @@ -33,18 +34,16 @@ public ExternalTool(string name, string icon, string execFile, Func _execArgsGenerator = null; } @@ -181,17 +180,24 @@ public void FindJetBrainsFromToolbox(Func platformFinder) var state = Path.Combine(platformFinder(), "state.json"); if (File.Exists(state)) { - using var stream = File.OpenRead(state); - var stateData = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.JetBrainsState); - foreach (var tool in stateData.Tools) + try { - if (exclude.Contains(tool.ToolId.ToLowerInvariant())) - continue; - - Tools.Add(new ExternalTool( - $"{tool.DisplayName} {tool.DisplayVersion}", - supportedIcons.Contains(tool.ProductCode) ? $"JetBrains/{tool.ProductCode}" : "JetBrains/JB", - Path.Combine(tool.InstallLocation, tool.LaunchCommand))); + using var stream = File.OpenRead(state); + var stateData = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.JetBrainsState); + foreach (var tool in stateData.Tools) + { + if (exclude.Contains(tool.ToolId.ToLowerInvariant())) + continue; + + Tools.Add(new ExternalTool( + $"{tool.DisplayName} {tool.DisplayVersion}", + supportedIcons.Contains(tool.ProductCode) ? $"JetBrains/{tool.ProductCode}" : "JetBrains/JB", + Path.Combine(tool.InstallLocation, tool.LaunchCommand))); + } + } + catch + { + // Ignore exceptions. } } } diff --git a/src/Models/Filter.cs b/src/Models/Filter.cs deleted file mode 100644 index af4569fad..000000000 --- a/src/Models/Filter.cs +++ /dev/null @@ -1,60 +0,0 @@ -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/HTTPSValidator.cs b/src/Models/HTTPSValidator.cs new file mode 100644 index 000000000..014207c21 --- /dev/null +++ b/src/Models/HTTPSValidator.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Net.Security; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace SourceGit.Models +{ + public static class HTTPSValidator + { + public static void Add(string host) + { + lock (_syncLock) + { + // Already checked + if (_hosts.ContainsKey(host)) + return; + + // Temporarily mark as supported to avoid duplicate checks + _hosts.Add(host, true); + + // Well-known hosts always support HTTPS + if (host.Contains("github.com", StringComparison.Ordinal) || + host.Contains("gitlab", StringComparison.Ordinal) || + host.Contains("azure.com", StringComparison.Ordinal) || + host.Equals("gitee.com", StringComparison.Ordinal) || + host.Equals("bitbucket.org", StringComparison.Ordinal) || + host.Equals("gitea.org", StringComparison.Ordinal) || + host.Equals("gitcode.com", StringComparison.Ordinal)) + return; + } + + Task.Run(() => + { + var supported = false; + + try + { + using (var client = new TcpClient()) + { + client.ConnectAsync(host, 443).Wait(3000); + if (!client.Connected) + { + client.ConnectAsync(host, 80).Wait(3000); + supported = !client.Connected; // If the network is not available, assume HTTPS is supported + } + else + { + using (var ssl = new SslStream(client.GetStream(), false, (s, cert, chain, errs) => true)) + { + ssl.AuthenticateAsClient(host); + supported = ssl.IsAuthenticated; // Hand-shake succeeded + } + } + } + } + catch + { + // Ignore exceptions + } + + lock (_syncLock) + { + _hosts[host] = supported; + } + }); + } + + public static bool IsSupported(string host) + { + lock (_syncLock) + { + if (_hosts.TryGetValue(host, out var supported)) + return supported; + + return false; + } + } + + private static Lock _syncLock = new(); + private static Dictionary _hosts = new(); + } +} diff --git a/src/Models/HistoryFilterCollection.cs b/src/Models/HistoryFilterCollection.cs new file mode 100644 index 000000000..e2aeb56b1 --- /dev/null +++ b/src/Models/HistoryFilterCollection.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Collections; +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 HistoryFilter : 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 HistoryFilter() + { + } + + public HistoryFilter(string pattern, FilterType type, FilterMode mode) + { + _pattern = pattern; + _mode = mode; + Type = type; + } + + private string _pattern = string.Empty; + private FilterMode _mode = FilterMode.None; + } + + public class HistoryFilterCollection + { + public AvaloniaList Filters + { + get; + set; + } = []; + + public FilterMode Mode => Filters.Count > 0 ? Filters[0].Mode : FilterMode.None; + + public Dictionary ToMap() + { + var map = new Dictionary(); + foreach (var filter in Filters) + map.Add(filter.Pattern, filter.Mode); + return map; + } + + public bool Update(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 Filters) + { + if (filter.Mode != mode) + { + clear = true; + break; + } + } + + if (clear) + { + Filters.Clear(); + Filters.Add(new HistoryFilter(pattern, type, mode)); + return true; + } + } + else + { + for (int i = 0; i < Filters.Count; i++) + { + var filter = Filters[i]; + if (filter.Type == type && filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + { + Filters.RemoveAt(i); + return true; + } + } + + return false; + } + + foreach (var filter in Filters) + { + if (filter.Type != type) + continue; + + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + return false; + } + + Filters.Add(new HistoryFilter(pattern, type, mode)); + return true; + } + + public FilterMode GetFilterMode(string pattern) + { + foreach (var filter in Filters) + { + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + return filter.Mode; + } + + return FilterMode.None; + } + + public void RemoveFilter(string pattern, FilterType type) + { + foreach (var filter in Filters) + { + if (filter.Type == type && filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + { + Filters.Remove(filter); + break; + } + } + } + + public void RemoveBranchFiltersByPrefix(string pattern) + { + var dirty = new List(); + var prefix = $"{pattern}/"; + + foreach (var filter in Filters) + { + if (filter.Type != FilterType.Tag) + continue; + + if (filter.Pattern.StartsWith(prefix, StringComparison.Ordinal)) + dirty.Add(filter); + } + + foreach (var filter in dirty) + Filters.Remove(filter); + } + + public string Build() + { + var includedRefs = new List(); + var excludedBranches = new List(); + var excludedRemotes = new List(); + var excludedTags = new List(); + foreach (var filter in Filters) + { + 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(); + } + } +} diff --git a/src/Models/ICommandLog.cs b/src/Models/ICommandLog.cs index 34ec70316..28e1fcb39 100644 --- a/src/Models/ICommandLog.cs +++ b/src/Models/ICommandLog.cs @@ -1,5 +1,10 @@ namespace SourceGit.Models { + public interface ICommandLogReceiver + { + void OnReceiveCommandLog(string line); + } + public interface ICommandLog { void AppendLine(string line); diff --git a/src/Models/InteractiveRebase.cs b/src/Models/InteractiveRebase.cs index d1710d4a9..bae99ac52 100644 --- a/src/Models/InteractiveRebase.cs +++ b/src/Models/InteractiveRebase.cs @@ -12,6 +12,15 @@ public enum InteractiveRebaseAction Drop, } + public enum InteractiveRebasePendingType + { + None = 0, + Target, + Pending, + Ignore, + Last, + } + public class InteractiveCommit { public Commit Commit { get; set; } = new Commit(); diff --git a/src/Models/IpcChannel.cs b/src/Models/IpcChannel.cs index 001c65a65..702f0630b 100644 --- a/src/Models/IpcChannel.cs +++ b/src/Models/IpcChannel.cs @@ -80,7 +80,7 @@ private async void StartServer() if (!_cancellationTokenSource.IsCancellationRequested) { var line = await reader.ReadToEndAsync(_cancellationTokenSource.Token); - MessageReceived?.Invoke(line?.Trim()); + MessageReceived?.Invoke(line.Trim()); } _server.Disconnect(); diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTracker.cs similarity index 95% rename from src/Models/IssueTrackerRule.cs rename to src/Models/IssueTracker.cs index 7adaa1763..b424707f6 100644 --- a/src/Models/IssueTrackerRule.cs +++ b/src/Models/IssueTracker.cs @@ -1,10 +1,9 @@ using System.Text.RegularExpressions; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.Models { - public class IssueTrackerRule : ObservableObject + public class IssueTracker : ObservableObject { public bool IsShared { @@ -27,12 +26,11 @@ public string RegexString { try { - _regex = null; _regex = new Regex(_regexString, RegexOptions.Multiline); } catch { - // Ignore errors. + _regex = null; } } diff --git a/src/Models/LFSLock.cs b/src/Models/LFSLock.cs index 0a328cfb2..8d9a4acff 100644 --- a/src/Models/LFSLock.cs +++ b/src/Models/LFSLock.cs @@ -1,9 +1,22 @@ -namespace SourceGit.Models +using System.Text.Json.Serialization; + +namespace SourceGit.Models { + public class LFSLockOwner + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + } + public class LFSLock { - public string File { get; set; } = string.Empty; - public string User { get; set; } = string.Empty; - public long ID { get; set; } = 0; + [JsonPropertyName("id")] + public string ID { get; set; } = string.Empty; + + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + [JsonPropertyName("owner")] + public LFSLockOwner Owner { get; set; } = null; } } diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs index 1788a9b22..027433336 100644 --- a/src/Models/Locales.cs +++ b/src/Models/Locales.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace SourceGit.Models { @@ -12,6 +12,7 @@ public class Locale new Locale("English", "en_US"), new Locale("Español", "es_ES"), new Locale("Français", "fr_FR"), + new Locale("Bahasa Indonesia", "id_ID"), new Locale("Italiano", "it_IT"), new Locale("Português (Brasil)", "pt_BR"), new Locale("Українська", "uk_UA"), @@ -20,6 +21,7 @@ public class Locale new Locale("繁體中文", "zh_TW"), new Locale("日本語", "ja_JP"), new Locale("தமிழ் (Tamil)", "ta_IN"), + new Locale("한국어", "ko_KR"), }; public Locale(string name, string key) diff --git a/src/Models/NumericSort.cs b/src/Models/NumericSort.cs index baaf3da4f..433a921bd 100644 --- a/src/Models/NumericSort.cs +++ b/src/Models/NumericSort.cs @@ -6,6 +6,8 @@ public static class NumericSort { public static int Compare(string s1, string s2) { + var comparer = StringComparer.InvariantCultureIgnoreCase; + int len1 = s1.Length; int len2 = s2.Length; @@ -20,7 +22,7 @@ public static int Compare(string s1, string s2) bool isDigit1 = char.IsDigit(c1); bool isDigit2 = char.IsDigit(c2); if (isDigit1 != isDigit2) - return c1.CompareTo(c2); + return comparer.Compare(c1.ToString(), c2.ToString()); int subLen1 = 1; while (marker1 + subLen1 < len1 && char.IsDigit(s1[marker1 + subLen1]) == isDigit1) @@ -40,7 +42,7 @@ public static int Compare(string s1, string s2) if (isDigit1) result = (subLen1 == subLen2) ? string.CompareOrdinal(sub1, sub2) : (subLen1 - subLen2); else - result = string.Compare(sub1, sub2, StringComparison.OrdinalIgnoreCase); + result = comparer.Compare(sub1, sub2); if (result != 0) return result; diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs index f8d76328d..c38eb674e 100644 --- a/src/Models/OpenAI.cs +++ b/src/Models/OpenAI.cs @@ -117,6 +117,12 @@ public string ApiKey set => SetProperty(ref _apiKey, value); } + public bool ReadApiKeyFromEnv + { + get => _readApiKeyFromEnv; + set => SetProperty(ref _readApiKeyFromEnv, value); + } + public string Model { get => _model; @@ -176,15 +182,19 @@ Your only goal is to retrieve a single commit message. public async Task ChatAsync(string prompt, string question, CancellationToken cancellation, Action onUpdate) { - var server = new Uri(_server); - var key = new ApiKeyCredential(_apiKey); - var oaiClient = _server.Contains("openai.azure.com/", StringComparison.Ordinal) - ? new AzureOpenAIClient(server, key) - : new OpenAIClient(key, new() { Endpoint = server }); - var client = oaiClient.GetChatClient(_model); - var messages = new List(); - messages.Add(_model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt)); - messages.Add(new UserChatMessage(question)); + var key = _readApiKeyFromEnv ? Environment.GetEnvironmentVariable(_apiKey) : _apiKey; + var endPoint = new Uri(_server); + var credential = new ApiKeyCredential(key); + var client = _server.Contains("openai.azure.com/", StringComparison.Ordinal) + ? new AzureOpenAIClient(endPoint, credential) + : new OpenAIClient(credential, new() { Endpoint = endPoint }); + + var chatClient = client.GetChatClient(_model); + var messages = new List() + { + _model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt), + new UserChatMessage(question), + }; try { @@ -192,7 +202,7 @@ public async Task ChatAsync(string prompt, string question, CancellationToken ca if (_streaming) { - var updates = client.CompleteChatStreamingAsync(messages, null, cancellation); + var updates = chatClient.CompleteChatStreamingAsync(messages, null, cancellation); await foreach (var update in updates) { @@ -202,7 +212,7 @@ public async Task ChatAsync(string prompt, string question, CancellationToken ca } else { - var completion = await client.CompleteChatAsync(messages, null, cancellation); + var completion = await chatClient.CompleteChatAsync(messages, null, cancellation); if (completion.Value.Content.Count > 0) rsp.Append(completion.Value.Content[0].Text); @@ -220,6 +230,7 @@ public async Task ChatAsync(string prompt, string question, CancellationToken ca private string _name; private string _server; private string _apiKey; + private bool _readApiKeyFromEnv = false; private string _model; private bool _streaming = true; private string _analyzeDiffPrompt; diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index 6e36cfb9e..d1fa2cb4d 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text.RegularExpressions; +using System.Web; namespace SourceGit.Models { @@ -8,14 +9,17 @@ public partial class Remote { [GeneratedRegex(@"^https?://[^/]+/.+[^/\.]$")] private static partial Regex REG_HTTPS(); + [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]+)?/([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 = [ @@ -62,7 +66,6 @@ public bool TryGetVisitURL(out string url) if (URL.StartsWith("http", StringComparison.Ordinal)) { - // Try to remove the user before host and `.git` extension. var uri = new Uri(URL.EndsWith(".git", StringComparison.Ordinal) ? URL.Substring(0, URL.Length - 4) : URL); if (uri.Port != 80 && uri.Port != 443) url = $"{uri.Scheme}://{uri.Host}:{uri.Port}{uri.LocalPath}"; @@ -75,7 +78,61 @@ public bool TryGetVisitURL(out string url) var match = REG_TO_VISIT_URL_CAPTURE().Match(URL); if (match.Success) { - url = $"https://{match.Groups[1].Value}/{match.Groups[2].Value}"; + var host = match.Groups[1].Value; + var supportHTTPS = HTTPSValidator.IsSupported(host); + var scheme = supportHTTPS ? "https" : "http"; + url = $"{scheme}://{host}/{match.Groups[2].Value}"; + return true; + } + + return false; + } + + public bool TryGetCreatePullRequestURL(out string url, string mergeBranch) + { + url = null; + + if (!TryGetVisitURL(out var baseURL)) + return false; + + var uri = new Uri(baseURL); + var host = uri.Host; + var route = uri.AbsolutePath.TrimStart('/'); + var encodedBranch = HttpUtility.UrlEncode(mergeBranch); + + if (host.Contains("github.com", StringComparison.Ordinal)) + { + url = $"{baseURL}/compare/{encodedBranch}?expand=1"; + return true; + } + + if (host.Contains("gitlab", StringComparison.Ordinal)) + { + url = $"{baseURL}/-/merge_requests/new?merge_request%5Bsource_branch%5D={encodedBranch}"; + return true; + } + + if (host.Equals("gitee.com", StringComparison.Ordinal)) + { + url = $"{baseURL}/pulls/new?source={encodedBranch}"; + return true; + } + + if (host.Equals("bitbucket.org", StringComparison.Ordinal)) + { + url = $"{baseURL}/pull-requests/new?source={encodedBranch}"; + return true; + } + + if (host.Equals("gitea.org", StringComparison.Ordinal)) + { + url = $"{baseURL}/compare/{encodedBranch}"; + return true; + } + + if (host.Contains("azure.com", StringComparison.Ordinal)) + { + url = $"{baseURL}/pullrequestcreate?sourceRef={encodedBranch}"; return true; } diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index f287826c5..ddee7fe03 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Generic; -using System.Text; - using Avalonia.Collections; namespace SourceGit.Models @@ -62,6 +59,12 @@ public bool EnableForceOnFetch set; } = false; + public bool FetchAllRemotes + { + get; + set; + } = false; + public bool FetchWithoutTags { get; @@ -104,18 +107,6 @@ public bool CheckoutBranchOnCreateBranch set; } = true; - public bool UpdateSubmodulesOnCheckoutBranch - { - get; - set; - } = true; - - public AvaloniaList HistoriesFilters - { - get; - set; - } = []; - public AvaloniaList CommitTemplates { get; @@ -128,12 +119,6 @@ public AvaloniaList CommitMessages set; } = []; - public AvaloniaList IssueTrackerRules - { - get; - set; - } = []; - public AvaloniaList CustomActions { get; @@ -158,6 +143,12 @@ public bool EnableSignOffForCommit set; } = false; + public bool NoVerifyOnCommit + { + get; + set; + } = false; + public bool IncludeUntrackedWhenStash { get; @@ -230,165 +221,11 @@ public string LastCommitMessage 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) + public string ConventionalTypesOverride { - 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(); - } + get; + set; + } = string.Empty; public void PushCommitMessage(string message) { @@ -409,25 +246,6 @@ public void PushCommitMessage(string message) CommitMessages.Insert(0, message); } - public IssueTrackerRule AddIssueTracker(string name, string regex, string url) - { - var rule = new IssueTrackerRule() - { - Name = name, - RegexString = regex, - URLTemplate = url, - }; - - IssueTrackerRules.Add(rule); - return rule; - } - - public void RemoveIssueTracker(IssueTrackerRule rule) - { - if (rule != null) - IssueTrackerRules.Remove(rule); - } - public CustomAction AddNewCustomAction() { var act = new CustomAction() { Name = "Unnamed Action" }; diff --git a/src/Models/RepositoryStatus.cs b/src/Models/RepositoryStatus.cs new file mode 100644 index 000000000..c7c498ae5 --- /dev/null +++ b/src/Models/RepositoryStatus.cs @@ -0,0 +1,29 @@ +namespace SourceGit.Models +{ + public class RepositoryStatus + { + public string CurrentBranch { get; set; } = string.Empty; + public int Ahead { get; set; } = 0; + public int Behind { get; set; } = 0; + public int LocalChanges { get; set; } = 0; + + public bool IsTrackingStatusVisible + { + get + { + return Ahead > 0 || Behind > 0; + } + } + + public string TrackingDescription + { + get + { + if (Ahead > 0) + return Behind > 0 ? $"{Ahead}↑ {Behind}↓" : $"{Ahead}↑"; + + return Behind > 0 ? $"{Behind}↓" : string.Empty; + } + } + } +} diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs index 7dfb22373..84c9ae066 100644 --- a/src/Models/ShellOrTerminal.cs +++ b/src/Models/ShellOrTerminal.cs @@ -11,6 +11,7 @@ public class ShellOrTerminal public string Type { get; set; } public string Name { get; set; } public string Exec { get; set; } + public string Args { get; set; } public Bitmap Icon { @@ -32,18 +33,18 @@ static ShellOrTerminal() new ShellOrTerminal("git-bash", "Git Bash", "bash.exe"), new ShellOrTerminal("pwsh", "PowerShell", "pwsh.exe|powershell.exe"), new ShellOrTerminal("cmd", "Command Prompt", "cmd.exe"), - new ShellOrTerminal("wt", "Windows Terminal", "wt.exe") + new ShellOrTerminal("wt", "Windows Terminal", "wt.exe", "-d .") }; } else if (OperatingSystem.IsMacOS()) { Supported = new List() { - new ShellOrTerminal("mac-terminal", "Terminal", ""), - new ShellOrTerminal("iterm2", "iTerm", ""), - new ShellOrTerminal("warp", "Warp", ""), - new ShellOrTerminal("ghostty", "Ghostty", ""), - new ShellOrTerminal("kitty", "kitty", "") + new ShellOrTerminal("mac-terminal", "Terminal", "Terminal"), + new ShellOrTerminal("iterm2", "iTerm", "iTerm"), + new ShellOrTerminal("warp", "Warp", "Warp"), + new ShellOrTerminal("ghostty", "Ghostty", "Ghostty"), + new ShellOrTerminal("kitty", "kitty", "kitty") }; } else @@ -57,19 +58,20 @@ static ShellOrTerminal() new ShellOrTerminal("deepin-terminal", "Deepin Terminal", "deepin-terminal"), 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("wezterm", "WezTerm", "wezterm", "start --cwd ."), + new ShellOrTerminal("ptyxis", "Ptyxis", "ptyxis", "--new-window --working-directory=."), new ShellOrTerminal("kitty", "kitty", "kitty"), new ShellOrTerminal("custom", "Custom", ""), }; } } - public ShellOrTerminal(string type, string name, string exec) + public ShellOrTerminal(string type, string name, string exec, string args = null) { Type = type; Name = name; Exec = exec; + Args = args; } } } diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 87944637a..9e1654575 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System; namespace SourceGit.Models { @@ -8,20 +8,18 @@ public enum TagSortMode Name, } - public class Tag : ObservableObject + public class Tag { public string Name { get; set; } = string.Empty; public bool IsAnnotated { get; set; } = false; public string SHA { get; set; } = string.Empty; + public User Creator { get; set; } = null; public ulong CreatorDate { get; set; } = 0; public string Message { get; set; } = string.Empty; - public FilterMode FilterMode + public string CreatorDateStr { - get => _filterMode; - set => SetProperty(ref _filterMode, value); + get => DateTime.UnixEpoch.AddSeconds(CreatorDate).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); } - - private FilterMode _filterMode = FilterMode.None; } } diff --git a/src/Models/TemplateEngine.cs b/src/Models/TemplateEngine.cs index 87822fb11..12280006e 100644 --- a/src/Models/TemplateEngine.cs +++ b/src/Models/TemplateEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Text.RegularExpressions; @@ -285,7 +286,7 @@ private string ParseReplacement() switch (c) { case ESCAPE: - // allow to escape only } + // allow to escape only right-brace if (Peek() == VARIABLE_END) { esc = true; @@ -349,14 +350,10 @@ private static string EvalVariable(Context context, RegexVariable variable) 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}, + {"pure_files", GetPureFiles}, }; private static string GetBranchName(Context context) @@ -377,13 +374,19 @@ private static string GetFiles(Context context) return string.Join(", ", paths); } + private static string GetPureFiles(Context context) + { + var names = new List(); + foreach (var c in context.changes) + names.Add(Path.GetFileName(c.Path)); + return string.Join(", ", names); + } + private delegate string VariableSliceGetter(Context context, int count); private static readonly IReadOnlyDictionary s_slicedVariables = new Dictionary() { - // legacy variables {"files", GetFilesSliced}, - // - {"FILES", GetFilesSliced}, + {"pure_files", GetPureFilesSliced} }; private static string GetFilesSliced(Context context, int count) @@ -400,5 +403,20 @@ private static string GetFilesSliced(Context context, int count) return sb.ToString(); } + + private static string GetPureFilesSliced(Context context, int count) + { + var sb = new StringBuilder(); + var names = new List(); + var max = Math.Min(count, context.changes.Count); + for (int i = 0; i < max; i++) + names.Add(Path.GetFileName(context.changes[i].Path)); + + sb.AppendJoin(", ", names); + if (max < context.changes.Count) + sb.Append($" and {context.changes.Count - max} other files"); + + return sb.ToString(); + } } } diff --git a/src/Models/TextMateHelper.cs b/src/Models/TextMateHelper.cs index e9903890b..38c0aaa72 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -26,6 +26,7 @@ public static class GrammarUtility new ExtraGrammar("source.hx", [".hx"], "haxe.json"), new ExtraGrammar("source.hxml", [".hxml"], "hxml.json"), new ExtraGrammar("text.html.jsp", [".jsp", ".jspf", ".tag"], "jsp.json"), + new ExtraGrammar("source.vue", [".vue"], "vue.json"), ]; public static string GetScope(string file, RegistryOptions reg) diff --git a/src/Models/User.cs b/src/Models/User.cs index d3faedf0d..15a45762b 100644 --- a/src/Models/User.cs +++ b/src/Models/User.cs @@ -21,7 +21,7 @@ public User(string data) parts = [string.Empty, data]; Name = parts[0]; - Email = parts[1]; + Email = parts[1].TrimStart('<').TrimEnd('>'); _hash = data.GetHashCode(); } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 9ba7ee9cb..cdeddd65f 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -8,9 +8,27 @@ namespace SourceGit.Models { public class Watcher : IDisposable { + public class LockContext : IDisposable + { + public LockContext(Watcher target) + { + _target = target; + Interlocked.Increment(ref _target._lockCount); + } + + public void Dispose() + { + Interlocked.Decrement(ref _target._lockCount); + } + + private Watcher _target; + } + public Watcher(IRepository repo, string fullpath, string gitDir) { _repo = repo; + _root = new DirectoryInfo(fullpath).FullName; + _watchers = new List(); var testGitDir = new DirectoryInfo(Path.Combine(fullpath, ".git")).FullName; var desiredDir = new DirectoryInfo(gitDir).FullName; @@ -25,7 +43,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) combined.Renamed += OnRepositoryChanged; combined.Changed += OnRepositoryChanged; combined.Deleted += OnRepositoryChanged; - combined.EnableRaisingEvents = true; + combined.EnableRaisingEvents = false; _watchers.Add(combined); } @@ -40,7 +58,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) wc.Renamed += OnWorkingCopyChanged; wc.Changed += OnWorkingCopyChanged; wc.Deleted += OnWorkingCopyChanged; - wc.EnableRaisingEvents = true; + wc.EnableRaisingEvents = false; var git = new FileSystemWatcher(); git.Path = gitDir; @@ -51,51 +69,58 @@ public Watcher(IRepository repo, string fullpath, string gitDir) git.Renamed += OnGitDirChanged; git.Changed += OnGitDirChanged; git.Deleted += OnGitDirChanged; - git.EnableRaisingEvents = true; + git.EnableRaisingEvents = false; _watchers.Add(wc); _watchers.Add(git); } _timer = new Timer(Tick, null, 100, 100); + + // Starts filesystem watchers in another thread to avoid UI blocking + Task.Run(() => + { + try + { + foreach (var watcher in _watchers) + watcher.EnableRaisingEvents = true; + } + catch + { + // Ignore exceptions. This may occur while `Dispose` is called. + } + }); } - public void SetEnabled(bool enabled) + public IDisposable Lock() { - if (enabled) - { - if (_lockCount > 0) - _lockCount--; - } - else - { - _lockCount++; - } + return new LockContext(this); } - public void SetSubmodules(List submodules) + public void MarkBranchUpdated() { - lock (_lockSubmodule) - { - _submodules.Clear(); - foreach (var submodule in submodules) - _submodules.Add(submodule.Path); - } + Interlocked.Exchange(ref _updateBranch, 0); + Interlocked.Exchange(ref _updateWC, 0); + } + + public void MarkTagUpdated() + { + Interlocked.Exchange(ref _updateTags, 0); } - public void MarkBranchDirtyManually() + public void MarkWorkingCopyUpdated() { - _updateBranch = DateTime.Now.ToFileTime() - 1; + Interlocked.Exchange(ref _updateWC, 0); } - public void MarkTagDirtyManually() + public void MarkStashUpdated() { - _updateTags = DateTime.Now.ToFileTime() - 1; + Interlocked.Exchange(ref _updateStashes, 0); } - public void MarkWorkingCopyDirtyManually() + public void MarkSubmodulesUpdated() { - _updateWC = DateTime.Now.ToFileTime() - 1; + Interlocked.Exchange(ref _updateSubmodules, 0); } public void Dispose() @@ -113,57 +138,91 @@ public void Dispose() private void Tick(object sender) { - if (_lockCount > 0) + if (Interlocked.Read(ref _lockCount) > 0) return; var now = DateTime.Now.ToFileTime(); - if (_updateBranch > 0 && now > _updateBranch) + var refreshCommits = false; + var refreshSubmodules = false; + var refreshWC = false; + + var oldUpdateBranch = Interlocked.Exchange(ref _updateBranch, -1); + if (oldUpdateBranch > 0) { - _updateBranch = 0; - _updateWC = 0; + if (now > oldUpdateBranch) + { + refreshCommits = true; + refreshSubmodules = _repo.MayHaveSubmodules(); + refreshWC = true; - if (_updateTags > 0) + _repo.RefreshBranches(); + _repo.RefreshWorktrees(); + } + else { - _updateTags = 0; - Task.Run(_repo.RefreshTags); + Interlocked.CompareExchange(ref _updateBranch, oldUpdateBranch, -1); } + } - if (_updateSubmodules > 0 || _repo.MayHaveSubmodules()) + if (refreshWC) + { + Interlocked.Exchange(ref _updateWC, -1); + _repo.RefreshWorkingCopyChanges(); + } + else + { + var oldUpdateWC = Interlocked.Exchange(ref _updateWC, -1); + if (oldUpdateWC > 0) { - _updateSubmodules = 0; - Task.Run(_repo.RefreshSubmodules); + if (now > oldUpdateWC) + _repo.RefreshWorkingCopyChanges(); + else + Interlocked.CompareExchange(ref _updateWC, oldUpdateWC, -1); } - - Task.Run(_repo.RefreshBranches); - Task.Run(_repo.RefreshCommits); - Task.Run(_repo.RefreshWorkingCopyChanges); - Task.Run(_repo.RefreshWorktrees); } - if (_updateWC > 0 && now > _updateWC) + if (refreshSubmodules) { - _updateWC = 0; - Task.Run(_repo.RefreshWorkingCopyChanges); + Interlocked.Exchange(ref _updateSubmodules, -1); + _repo.RefreshSubmodules(); } - - if (_updateSubmodules > 0 && now > _updateSubmodules) + else { - _updateSubmodules = 0; - Task.Run(_repo.RefreshSubmodules); + var oldUpdateSubmodule = Interlocked.Exchange(ref _updateSubmodules, -1); + if (oldUpdateSubmodule > 0) + { + if (now > oldUpdateSubmodule) + _repo.RefreshSubmodules(); + else + Interlocked.CompareExchange(ref _updateSubmodules, oldUpdateSubmodule, -1); + } } - if (_updateStashes > 0 && now > _updateStashes) + var oldUpdateStashes = Interlocked.Exchange(ref _updateStashes, -1); + if (oldUpdateStashes > 0) { - _updateStashes = 0; - Task.Run(_repo.RefreshStashes); + if (now > oldUpdateStashes) + _repo.RefreshStashes(); + else + Interlocked.CompareExchange(ref _updateStashes, oldUpdateStashes, -1); } - if (_updateTags > 0 && now > _updateTags) + var oldUpdateTags = Interlocked.Exchange(ref _updateTags, -1); + if (oldUpdateTags > 0) { - _updateTags = 0; - Task.Run(_repo.RefreshTags); - Task.Run(_repo.RefreshCommits); + if (now > oldUpdateTags) + { + refreshCommits = true; + _repo.RefreshTags(); + } + else + { + Interlocked.CompareExchange(ref _updateTags, oldUpdateTags, -1); + } } + + if (refreshCommits) + _repo.RefreshCommits(); } private void OnRepositoryChanged(object o, FileSystemEventArgs e) @@ -178,7 +237,7 @@ private void OnRepositoryChanged(object o, FileSystemEventArgs e) if (name.StartsWith(".git/", StringComparison.Ordinal)) HandleGitDirFileChanged(name.Substring(5)); else - HandleWorkingCopyFileChanged(name); + HandleWorkingCopyFileChanged(name, e.FullPath); } private void OnGitDirChanged(object o, FileSystemEventArgs e) @@ -201,7 +260,7 @@ private void OnWorkingCopyChanged(object o, FileSystemEventArgs e) name.EndsWith("/.git", StringComparison.Ordinal)) return; - HandleWorkingCopyFileChanged(name); + HandleWorkingCopyFileChanged(name, e.FullPath); } private void HandleGitDirFileChanged(string name) @@ -216,23 +275,24 @@ private void HandleGitDirFileChanged(string name) if (name.EndsWith("/HEAD", StringComparison.Ordinal) || name.EndsWith("/ORIG_HEAD", StringComparison.Ordinal)) { - _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); - _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + var desired = DateTime.Now.AddSeconds(1).ToFileTime(); + Interlocked.Exchange(ref _updateSubmodules, desired); + Interlocked.Exchange(ref _updateWC, desired); } } else if (name.Equals("MERGE_HEAD", StringComparison.Ordinal) || name.Equals("AUTO_MERGE", StringComparison.Ordinal)) { if (_repo.MayHaveSubmodules()) - _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); + Interlocked.Exchange(ref _updateSubmodules, DateTime.Now.AddSeconds(1).ToFileTime()); } else if (name.StartsWith("refs/tags", StringComparison.Ordinal)) { - _updateTags = DateTime.Now.AddSeconds(.5).ToFileTime(); + Interlocked.Exchange(ref _updateTags, DateTime.Now.AddSeconds(.5).ToFileTime()); } else if (name.StartsWith("refs/stash", StringComparison.Ordinal)) { - _updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); + Interlocked.Exchange(ref _updateStashes, DateTime.Now.AddSeconds(.5).ToFileTime()); } else if (name.Equals("HEAD", StringComparison.Ordinal) || name.Equals("BISECT_START", StringComparison.Ordinal) || @@ -240,52 +300,58 @@ private void HandleGitDirFileChanged(string name) name.StartsWith("refs/remotes/", StringComparison.Ordinal) || (name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal))) { - _updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime(); + Interlocked.Exchange(ref _updateBranch, DateTime.Now.AddSeconds(.5).ToFileTime()); } else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal)) { - _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + Interlocked.Exchange(ref _updateWC, DateTime.Now.AddSeconds(1).ToFileTime()); } } - private void HandleWorkingCopyFileChanged(string name) + private void HandleWorkingCopyFileChanged(string name, string fullpath) { if (name.StartsWith(".vs/", StringComparison.Ordinal)) return; if (name.Equals(".gitmodules", StringComparison.Ordinal)) { - _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); - _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + var desired = DateTime.Now.AddSeconds(1).ToFileTime(); + Interlocked.Exchange(ref _updateSubmodules, desired); + Interlocked.Exchange(ref _updateWC, desired); return; } - lock (_lockSubmodule) + var dir = Directory.Exists(fullpath) ? fullpath : Path.GetDirectoryName(fullpath); + if (IsInSubmodule(dir)) { - foreach (var submodule in _submodules) - { - if (name.StartsWith(submodule, StringComparison.Ordinal)) - { - _updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); - return; - } - } + Interlocked.Exchange(ref _updateSubmodules, DateTime.Now.AddSeconds(1).ToFileTime()); + return; } - _updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + Interlocked.Exchange(ref _updateWC, DateTime.Now.AddSeconds(1).ToFileTime()); + } + + private bool IsInSubmodule(string folder) + { + if (string.IsNullOrEmpty(folder) || folder.Equals(_root, StringComparison.Ordinal)) + return false; + + if (File.Exists($"{folder}/.git")) + return true; + + return IsInSubmodule(Path.GetDirectoryName(folder)); } - private readonly IRepository _repo = null; - private List _watchers = []; - private Timer _timer = null; - private int _lockCount = 0; - private long _updateWC = 0; - private long _updateBranch = 0; - private long _updateSubmodules = 0; - private long _updateStashes = 0; - private long _updateTags = 0; - - private readonly Lock _lockSubmodule = new(); - private List _submodules = new List(); + private readonly IRepository _repo; + private readonly string _root; + private List _watchers; + private Timer _timer; + + private long _lockCount; + private long _updateWC; + private long _updateBranch; + private long _updateSubmodules; + private long _updateStashes; + private long _updateTags; } } diff --git a/src/Models/Worktree.cs b/src/Models/Worktree.cs index 26f88a8a8..00eb33a05 100644 --- a/src/Models/Worktree.cs +++ b/src/Models/Worktree.cs @@ -9,7 +9,6 @@ public class Worktree : ObservableObject 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; public bool IsLocked diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index f6eb4ebf3..c0b067e60 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -57,13 +57,20 @@ public string FindTerminal(Models.ShellOrTerminal shell) finder.Fleet(() => FindJetBrainsFleet(localAppDataDir)); finder.FindJetBrainsFromToolbox(() => Path.Combine(localAppDataDir, "JetBrains/Toolbox")); finder.SublimeText(() => FindExecutable("subl")); - finder.Zed(() => FindExecutable("zeditor")); + finder.Zed(() => + { + var exec = FindExecutable("zeditor"); + return string.IsNullOrEmpty(exec) ? FindExecutable("zed") : exec; + }); return finder.Tools; } public void OpenBrowser(string url) { - Process.Start("xdg-open", url.Quoted()); + var browser = Environment.GetEnvironmentVariable("BROWSER"); + if (string.IsNullOrEmpty(browser)) + browser = "xdg-open"; + Process.Start(browser, url.Quoted()); } public void OpenInFileManager(string path, bool select) @@ -80,20 +87,15 @@ public void OpenInFileManager(string path, bool select) } } - public void OpenTerminal(string workdir) + public void OpenTerminal(string workdir, string args) { 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 = terminal; - - if (terminal.EndsWith("wezterm", StringComparison.OrdinalIgnoreCase)) - startInfo.Arguments = $"start --cwd {cwd.Quoted()}"; - else if (terminal.EndsWith("ptyxis", StringComparison.OrdinalIgnoreCase)) - startInfo.Arguments = $"--new-window --working-directory={cwd.Quoted()}"; + startInfo.FileName = OS.ShellOrTerminal; + startInfo.Arguments = args; try { @@ -130,7 +132,8 @@ private string FindExecutable(string filename) return test; } - return string.Empty; + var local = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", filename); + return File.Exists(local) ? local : string.Empty; } private string FindJetBrainsFleet(string localAppDataDir) diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index a021a16d8..48d276496 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -62,15 +62,7 @@ public string FindGitExecutable() public string FindTerminal(Models.ShellOrTerminal shell) { - return shell.Type switch - { - "mac-terminal" => "Terminal", - "iterm2" => "iTerm", - "warp" => "Warp", - "ghostty" => "Ghostty", - "kitty" => "kitty", - _ => string.Empty, - }; + return shell.Exec; } public List FindExternalTools() @@ -101,7 +93,7 @@ public void OpenInFileManager(string path, bool select) Process.Start("open", $"{path.Quoted()} -R"); } - public void OpenTerminal(string workdir) + public void OpenTerminal(string workdir, string _) { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var dir = string.IsNullOrEmpty(workdir) ? home : workdir; diff --git a/src/Native/OS.cs b/src/Native/OS.cs index df72aff88..1fd4b34d9 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -21,7 +21,7 @@ public interface IBackend string FindTerminal(Models.ShellOrTerminal shell); List FindExternalTools(); - void OpenTerminal(string workdir); + void OpenTerminal(string workdir, string args); void OpenInFileManager(string path, bool select); void OpenBrowser(string url); void OpenWithDefaultEditor(string file); @@ -70,12 +70,42 @@ public static string ShellOrTerminal set; } = string.Empty; + public static string ShellOrTerminalArgs + { + get; + set; + } = string.Empty; + public static List ExternalTools { get; set; } = []; + public static int ExternalMergerType + { + get; + set; + } = 0; + + public static string ExternalMergerExecFile + { + get; + set; + } = string.Empty; + + public static string ExternalMergeArgs + { + get; + set; + } = string.Empty; + + public static string ExternalDiffArgs + { + get; + set; + } = string.Empty; + public static bool UseSystemWindowFrame { get => OperatingSystem.IsLinux() && _enableSystemWindowFrame; @@ -98,7 +128,7 @@ static OS() } else { - throw new Exception("Platform unsupported!!!"); + throw new PlatformNotSupportedException(); } } @@ -112,7 +142,7 @@ public static void SetupDataDir() if (OperatingSystem.IsWindows()) { var execFile = Process.GetCurrentProcess().MainModule!.FileName; - var portableDir = Path.Combine(Path.GetDirectoryName(execFile), "data"); + var portableDir = Path.Combine(Path.GetDirectoryName(execFile)!, "data"); if (Directory.Exists(portableDir)) { DataDir = portableDir; @@ -156,6 +186,43 @@ public static void SetShellOrTerminal(Models.ShellOrTerminal shell) ShellOrTerminal = string.Empty; else ShellOrTerminal = _backend.FindTerminal(shell); + + ShellOrTerminalArgs = shell.Args; + } + + public static Models.DiffMergeTool GetDiffMergeTool(bool onlyDiff) + { + if (ExternalMergerType < 0 || ExternalMergerType >= Models.ExternalMerger.Supported.Count) + return null; + + if (ExternalMergerType != 0 && (string.IsNullOrEmpty(ExternalMergerExecFile) || !File.Exists(ExternalMergerExecFile))) + return null; + + return new Models.DiffMergeTool(ExternalMergerExecFile, onlyDiff ? ExternalDiffArgs : ExternalMergeArgs); + } + + public static void AutoSelectExternalMergeToolExecFile() + { + if (ExternalMergerType >= 0 && ExternalMergerType < Models.ExternalMerger.Supported.Count) + { + var merger = Models.ExternalMerger.Supported[ExternalMergerType]; + var externalTool = ExternalTools.Find(x => x.Name.Equals(merger.Name, StringComparison.Ordinal)); + if (externalTool != null) + ExternalMergerExecFile = externalTool.ExecFile; + else if (!OperatingSystem.IsWindows() && File.Exists(merger.Finder)) + ExternalMergerExecFile = merger.Finder; + else + ExternalMergerExecFile = string.Empty; + + ExternalDiffArgs = merger.DiffCmd; + ExternalMergeArgs = merger.MergeCmd; + } + else + { + ExternalMergerExecFile = string.Empty; + ExternalDiffArgs = string.Empty; + ExternalMergeArgs = string.Empty; + } } public static void OpenInFileManager(string path, bool select = false) @@ -173,7 +240,7 @@ public static void OpenTerminal(string workdir) if (string.IsNullOrEmpty(ShellOrTerminal)) App.RaiseException(workdir, "Terminal is not specified! Please confirm that the correct shell/terminal has been configured."); else - _backend.OpenTerminal(workdir); + _backend.OpenTerminal(workdir, ShellOrTerminalArgs); } public static void OpenWithDefaultEditor(string file) @@ -190,6 +257,19 @@ public static string GetAbsPath(string root, string sub) return fullpath; } + public static string GetRelativePathToHome(string path) + { + if (OperatingSystem.IsWindows()) + return path; + + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; + if (path.StartsWith(home, StringComparison.Ordinal)) + return $"~{path.AsSpan(prefixLen)}"; + + return path; + } + private static void UpdateGitVersion() { if (string.IsNullOrEmpty(_gitExecutable) || !File.Exists(_gitExecutable)) @@ -211,7 +291,7 @@ private static void UpdateGitVersion() try { - using var proc = Process.Start(start); + using var proc = Process.Start(start)!; var rs = proc.StandardOutput.ReadToEnd(); proc.WaitForExit(); if (proc.ExitCode == 0 && !string.IsNullOrWhiteSpace(rs)) diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index 69e8d842c..756eeecac 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -17,6 +17,7 @@ namespace SourceGit.Native [SupportedOSPlatform("windows")] internal class Windows : OS.IBackend { + [StructLayout(LayoutKind.Sequential)] internal struct RECT { public int left; @@ -55,7 +56,7 @@ internal struct MARGINS public void SetupApp(AppBuilder builder) { // Fix drop shadow issue on Windows 10 - if (!OperatingSystem.IsWindowsVersionAtLeast(10, 22000)) + if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) { Window.WindowStateProperty.Changed.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); Control.LoadedEvent.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); @@ -66,18 +67,13 @@ public void SetupWindow(Window window) { window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; window.ExtendClientAreaToDecorationsHint = true; - window.Classes.Add("fix_maximized_padding"); + window.BorderThickness = new Thickness(1); Win32Properties.AddWndProcHookCallback(window, (IntPtr hWnd, uint msg, IntPtr _, IntPtr lParam, ref bool handled) => { - // Custom WM_NCHITTEST - if (msg == 0x0084) + // Custom WM_NCHITTEST only used to limit the resize border to 4 * window.RenderScaling pixels. + if (msg == 0x0084 && window.WindowState == WindowState.Normal) { - handled = true; - - if (window.WindowState == WindowState.FullScreen || window.WindowState == WindowState.Maximized) - return 1; // HTCLIENT - var p = IntPtrToPixelPoint(lParam); GetWindowRect(hWnd, out var rcWindow); @@ -94,18 +90,23 @@ public void SetupWindow(Window window) else if (p.Y < rcWindow.bottom && p.Y >= rcWindow.bottom - borderThickness) y = 2; + // If it's in the client area, do not handle it here. var zone = y * 3 + x; + if (zone == 4) + return IntPtr.Zero; + + // If it's in the resize border area, return the proper HT code. + handled = true; return zone switch { 0 => 13, // HTTOPLEFT 1 => 12, // HTTOP 2 => 14, // HTTOPRIGHT 3 => 10, // HTLEFT - 4 => 1, // HTCLIENT 5 => 11, // HTRIGHT 6 => 16, // HTBOTTOMLEFT 7 => 15, // HTBOTTOM - _ => 17, + _ => 17, // HTBOTTOMRIGHT }; } @@ -143,7 +144,7 @@ public string FindTerminal(Models.ShellOrTerminal shell) break; var binDir = Path.GetDirectoryName(OS.GitExecutable)!; - var bash = Path.Combine(binDir, "bash.exe"); + var bash = Path.GetFullPath(Path.Combine(binDir, "..", "git-bash.exe")); if (!File.Exists(bash)) break; @@ -186,10 +187,11 @@ public string FindTerminal(Models.ShellOrTerminal shell) finder.VSCode(FindVSCode); finder.VSCodeInsiders(FindVSCodeInsiders); finder.VSCodium(FindVSCodium); - finder.Cursor(FindCursor); + finder.Cursor(() => Path.Combine(localAppDataDir, @"Programs\Cursor\Cursor.exe")); finder.Fleet(() => Path.Combine(localAppDataDir, @"Programs\Fleet\Fleet.exe")); finder.FindJetBrainsFromToolbox(() => Path.Combine(localAppDataDir, @"JetBrains\Toolbox")); finder.SublimeText(FindSublimeText); + finder.Zed(FindZed); FindVisualStudio(finder); return finder.Tools; } @@ -201,22 +203,22 @@ public void OpenBrowser(string url) Process.Start(info); } - public void OpenTerminal(string workdir) + public void OpenTerminal(string workdir, string args) { - if (!File.Exists(OS.ShellOrTerminal)) + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var cwd = string.IsNullOrEmpty(workdir) ? home : workdir; + var terminal = OS.ShellOrTerminal; + + if (!File.Exists(terminal)) { App.RaiseException(workdir, "Terminal is not specified! Please confirm that the correct shell/terminal has been configured."); return; } var startInfo = new ProcessStartInfo(); - startInfo.WorkingDirectory = workdir; - startInfo.FileName = OS.ShellOrTerminal; - - // Directly launching `Windows Terminal` need to specify the `-d` parameter - if (OS.ShellOrTerminal.EndsWith("wt.exe", StringComparison.OrdinalIgnoreCase)) - startInfo.Arguments = $"-d {workdir.Quoted()}"; - + startInfo.WorkingDirectory = cwd; + startInfo.FileName = terminal; + startInfo.Arguments = args; Process.Start(startInfo); } @@ -389,7 +391,7 @@ private void FindVisualStudio(Models.ExternalToolsFinder finder) try { - using var proc = Process.Start(startInfo); + using var proc = Process.Start(startInfo)!; var output = proc.StandardOutput.ReadToEnd(); proc.WaitForExit(); @@ -410,16 +412,20 @@ private void FindVisualStudio(Models.ExternalToolsFinder finder) } } - private string FindCursor() + private string FindZed() { - var cursorPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "Programs", - "Cursor", - "Cursor.exe"); + var currentUser = Microsoft.Win32.RegistryKey.OpenBaseKey( + Microsoft.Win32.RegistryHive.CurrentUser, + Microsoft.Win32.RegistryView.Registry64); - if (File.Exists(cursorPath)) - return cursorPath; + // NOTE: this is the official Zed Preview reg data. + var preview = currentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F70E4811-D0E2-4D88-AC99-D63752799F95}_is1"); + if (preview != null) + return preview.GetValue("DisplayIcon") as string; + + var findInPath = new StringBuilder("zed.exe", 512); + if (PathFindOnPath(findInPath, null)) + return findInPath.ToString(); return string.Empty; } @@ -439,10 +445,15 @@ private void OpenFolderAndSelectFile(string folderPath) } } - private string GenerateCommandlineArgsForVisualStudio(string repo) + private string GenerateCommandlineArgsForVisualStudio(string path) { - var sln = FindVSSolutionFile(new DirectoryInfo(repo), 4); - return string.IsNullOrEmpty(sln) ? repo.Quoted() : sln.Quoted(); + if (Directory.Exists(path)) + { + var sln = FindVSSolutionFile(new DirectoryInfo(path), 4); + return string.IsNullOrEmpty(sln) ? path.Quoted() : sln.Quoted(); + } + + return path.Quoted(); } private string FindVSSolutionFile(DirectoryInfo dir, int leftDepth) @@ -450,7 +461,8 @@ private string FindVSSolutionFile(DirectoryInfo dir, int leftDepth) var files = dir.GetFiles(); foreach (var f in files) { - if (f.Name.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) + if (f.Name.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase) || + f.Name.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) return f.FullName; } diff --git a/src/Resources/Grammars/vue.json b/src/Resources/Grammars/vue.json new file mode 100644 index 000000000..fd7e47c54 --- /dev/null +++ b/src/Resources/Grammars/vue.json @@ -0,0 +1,1326 @@ +{ + "information_for_contributors": [ + "This file has been copied from https://github.com/vuejs/language-tools/blob/68d98dc57f8486c2946ae28dc86bf8e91d45da4d/extensions/vscode/syntaxes/vue.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": "Vue", + "scopeName": "source.vue", + "patterns": [ + { + "include": "#vue-comments" + }, + { + "include": "#self-closing-tag" + }, + { + "begin": "(<)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + } + }, + "end": "(>)", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.end.html.vue" + } + }, + "patterns": [ + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)md\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "text.html.markdown", + "patterns": [ + { + "include": "text.html.markdown" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)html\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "text.html.derivative", + "patterns": [ + { + "include": "#html-stuff" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)pug\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "text.pug", + "patterns": [ + { + "include": "text.pug" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)stylus\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.stylus", + "patterns": [ + { + "include": "source.stylus" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)postcss\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.postcss", + "patterns": [ + { + "include": "source.postcss" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)sass\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.sass", + "patterns": [ + { + "include": "source.sass" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)css\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.css", + "patterns": [ + { + "include": "source.css" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)scss\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.css.scss", + "patterns": [ + { + "include": "source.css.scss" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)less\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.css.less", + "patterns": [ + { + "include": "source.css.less" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)js\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.js", + "patterns": [ + { + "include": "source.js" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)ts\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.ts", + "patterns": [ + { + "include": "source.ts" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)jsx\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.js.jsx", + "patterns": [ + { + "include": "source.js.jsx" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)tsx\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.tsx", + "patterns": [ + { + "include": "source.tsx" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)coffee\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.coffee", + "patterns": [ + { + "include": "source.coffee" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)json\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.json", + "patterns": [ + { + "include": "source.json" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)jsonc\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.json.comments", + "patterns": [ + { + "include": "source.json.comments" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)json5\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.json5", + "patterns": [ + { + "include": "source.json5" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)yaml\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.yaml", + "patterns": [ + { + "include": "source.yaml" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)toml\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.toml", + "patterns": [ + { + "include": "source.toml" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)(gql|graphql)\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.graphql", + "patterns": [ + { + "include": "source.graphql" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)\\b(?=[^>]*\\blang\\s*=\\s*(['\"]?)vue\\b\\2)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "source.vue", + "patterns": [ + { + "include": "source.vue" + } + ] + } + ] + }, + { + "begin": "(template)\\b", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/template\\b)", + "name": "text.html.derivative", + "patterns": [ + { + "include": "#html-stuff" + } + ] + } + ] + }, + { + "begin": "(script)\\b", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/script\\b)", + "name": "source.js", + "patterns": [ + { + "include": "source.js" + } + ] + } + ] + }, + { + "begin": "(style)\\b", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/style\\b)", + "name": "source.css", + "patterns": [ + { + "include": "source.css" + } + ] + } + ] + }, + { + "begin": "([a-zA-Z0-9:-]+)", + "beginCaptures": { + "1": { + "name": "entity.name.tag.$1.html.vue" + } + }, + "end": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "patterns": [ + { + "include": "#tag-stuff" + }, + { + "begin": "(?<=>)", + "end": "(?=<\\/)", + "name": "text" + } + ] + } + ] + } + ], + "repository": { + "self-closing-tag": { + "begin": "(<)([a-zA-Z0-9:-]+)(?=([^>]+/>))", + "beginCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + } + }, + "end": "(/>)", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.end.html.vue" + } + }, + "name": "self-closing-tag", + "patterns": [ + { + "include": "#tag-stuff" + } + ] + }, + "template-tag": { + "patterns": [ + { + "include": "#template-tag-1" + }, + { + "include": "#template-tag-2" + } + ] + }, + "template-tag-1": { + "begin": "(<)(template)\\b(>)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html.vue" + }, + "2": { + "name": "entity.name.tag.$2.html.vue" + }, + "3": { + "name": "punctuation.definition.tag.end.html.vue" + } + }, + "end": "(/?>)", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.end.html.vue" + } + }, + "name": "meta.template-tag.start", + "patterns": [ + { + "begin": "\\G", + "end": "(?=/>)|(()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.end.html.vue" + } + }, + "name": "meta.template-tag.start", + "patterns": [ + { + "begin": "\\G", + "end": "(?=/>)|(()|(>)", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.end.html.vue" + } + }, + "name": "meta.tag-stuff", + "patterns": [ + { + "include": "#vue-directives" + }, + { + "include": "text.html.basic#attribute" + } + ] + }, + "vue-directives": { + "patterns": [ + { + "include": "#vue-directives-control" + }, + { + "include": "#vue-directives-generic-attr" + }, + { + "include": "#vue-directives-style-attr" + }, + { + "include": "#vue-directives-original" + } + ] + }, + "vue-directives-original": { + "begin": "(?:(?:(v-[\\w-]+)(:)?)|([:\\.])|(@)|(#))(?:(?:(\\[)([^\\]]*)(\\]))|([\\w-]+))?", + "beginCaptures": { + "1": { + "name": "entity.other.attribute-name.html.vue" + }, + "2": { + "name": "punctuation.separator.key-value.html.vue" + }, + "3": { + "name": "punctuation.attribute-shorthand.bind.html.vue" + }, + "4": { + "name": "punctuation.attribute-shorthand.event.html.vue" + }, + "5": { + "name": "punctuation.attribute-shorthand.slot.html.vue" + }, + "6": { + "name": "punctuation.separator.key-value.html.vue" + }, + "7": { + "name": "source.ts.embedded.html.vue", + "patterns": [ + { + "include": "source.ts#expression" + } + ] + }, + "8": { + "name": "punctuation.separator.key-value.html.vue" + }, + "9": { + "name": "entity.other.attribute-name.html.vue" + } + }, + "end": "(?=\\s*[^=\\s])", + "name": "meta.attribute.directive.vue", + "patterns": [ + { + "match": "(\\.)([\\w-]*)", + "1": { + "name": "punctuation.separator.key-value.html.vue" + }, + "2": { + "name": "entity.other.attribute-name.html.vue" + } + }, + { + "include": "#vue-directives-expression" + } + ] + }, + "vue-directives-control": { + "begin": "(?:(v-for)|(v-if|v-else-if|v-else))(?=[=/>)\\s])", + "beginCaptures": { + "1": { + "name": "keyword.control.loop.vue" + }, + "2": { + "name": "keyword.control.conditional.vue" + } + }, + "end": "(?=\\s*[^=\\s])", + "name": "meta.attribute.directive.control.vue", + "patterns": [ + { + "include": "#vue-directives-expression" + } + ] + }, + "vue-directives-expression": { + "patterns": [ + { + "begin": "(=)\\s*('|\"|`)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.key-value.html.vue" + }, + "2": { + "name": "punctuation.definition.string.begin.html.vue" + } + }, + "end": "(\\2)", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.html.vue" + } + }, + "patterns": [ + { + "begin": "(?<=('|\"|`))", + "end": "(?=\\1)", + "name": "source.ts.embedded.html.vue", + "patterns": [ + { + "include": "source.ts#expression" + } + ] + } + ] + }, + { + "begin": "(=)\\s*(?=[^'\"`])", + "beginCaptures": { + "1": { + "name": "punctuation.separator.key-value.html.vue" + } + }, + "end": "(?=(\\s|>|\\/>))", + "patterns": [ + { + "begin": "(?=[^'\"`])", + "end": "(?=(\\s|>|\\/>))", + "name": "source.ts.embedded.html.vue", + "patterns": [ + { + "include": "source.ts#expression" + } + ] + } + ] + } + ] + }, + "vue-directives-style-attr": { + "begin": "\\b(style)\\s*(=)", + "beginCaptures": { + "1": { + "name": "entity.other.attribute-name.html.vue" + }, + "2": { + "name": "punctuation.separator.key-value.html.vue" + } + }, + "end": "(?<='|\")", + "name": "meta.attribute.style.vue", + "patterns": [ + { + "comment": "Copy from source.css#rule-list-innards", + "begin": "('|\")", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.html.vue" + } + }, + "end": "(\\1)", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.html.vue" + } + }, + "name": "source.css.embedded.html.vue", + "patterns": [ + { + "include": "source.css#comment-block" + }, + { + "include": "source.css#escapes" + }, + { + "include": "source.css#font-features" + }, + { + "match": "(?x) (?)" + } + ] + } + ] + }, + "vue-interpolations": { + "patterns": [ + { + "begin": "(\\{\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.interpolation.begin.html.vue" + } + }, + "end": "(\\}\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.interpolation.end.html.vue" + } + }, + "name": "expression.embedded.vue", + "patterns": [ + { + "begin": "\\G", + "end": "(?=\\}\\})", + "name": "source.ts.embedded.html.vue", + "patterns": [ + { + "include": "source.ts#expression" + } + ] + } + ] + } + ] + }, + "vue-comments": { + "patterns": [ + { + "include": "#vue-comments-key-value" + }, + { + "begin": "", + "name": "comment.block.vue" + } + ] + }, + "vue-comments-key-value": { + "begin": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.comment.vue" + } + }, + "name": "comment.block.vue", + "patterns": [ + { + "include": "source.json#value" + } + ] + } + } +} diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index c7011bce1..4fd176dfa 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -14,20 +14,20 @@ 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 M523 398 918 3l113 113-396 396 397 397-113 113-397-397-397 397-113-113 397-397L14 116l113-113 396 396z 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 + M325 312l-60 60L404 513 265 652l60 60 200-200L325 312zm194 345h236v97h-236v-97zM29 77v870h968V77H29zm870 774H125V173h774v678z 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 M896 811l-128 0c-23 0-43-19-43-43 0-23 19-43 43-43l107 0c13 0 21-9 21-21L896 107c0-13-9-21-21-21L448 85c-13 0-21 9-21 21l0 21c0 23-19 43-43 43-23 0-43-19-43-43L341 85c0-47 38-85 85-85l469 0c47 0 85 38 85 85l0 640C981 772 943 811 896 811zM683 299l0 640c0 47-38 85-85 85L128 1024c-47 0-85-38-85-85L43 299c0-47 38-85 85-85l469 0C644 213 683 252 683 299zM576 299 149 299c-13 0-21 9-21 21l0 597c0 13 9 21 21 21l427 0c13 0 21-9 21-21L597 320C597 307 589 299 576 299z + M339 297c17-23 25-51 25-83 2-42-12-79-43-108S255 62 215 65c-32 0-60 8-84 25s-42 39-54 67-15 56-10 86 19 55 40 76c21 21 47 35 76 41v303c-30 6-55 20-76 41-21 21-35 47-40 76-5 30-2 58 10 86s30 50 54 67 52 25 83 25 59-8 84-25c25-17 45-39 57-67 6-19 10-39 10-61 0-30-8-57-25-83-21-34-52-55-92-64v-299l25-6c28-13 50-32 67-57zm-45 471c8 15 12 30 11 46-1 16-6 31-16 46-10 15-23 25-40 32s-34 8-51 5-32-11-46-24-22-28-24-45c-6-28-1-53 18-75s41-33 68-33c17 0 32 4 46 13 14 8 25 20 33 35zM167 288c-15-11-26-24-33-41-7-17-10-34-6-51 3-17 11-32 24-45s28-21 46-25 36-3 53 5c17 7 30 18 40 32 10 14 15 29 16 46 1 17-3 33-11 48-8 15-20 27-33 35-14 8-29 13-46 13s-33-5-48-16zm615 45c2-19-1-38-10-57-11-28-29-50-54-67s-53-25-83-25h-111l76-76-45-41-127 127v41l127 127 45-41-76-76h111c23 0 44 8 62 25 18 17 27 38 27 64v89h57V332zm0 449H960v-61H782V542h-61v178H543v61h178v178h61V781z M280 145l243 341 0-0 45 63-0 0 79 110a143 143 0 11-36 75l-88-123-92 126c1 4 1 9 1 13l0 5a143 143 0 11-36-95l82-113L221 188l60-43zm473 541a70 70 0 100 140 70 70 0 000-140zm-463 0a70 70 0 100 140 70 70 0 000-140zM772 145l59 43-232 319-45-63L772 145z M128 183C128 154 154 128 183 128h521c30 0 55 26 55 55v38c0 17-17 34-34 34s-34-17-34-34v-26H196v495h26c17 0 34 17 34 34s-17 34-34 34h-38c-30 0-55-26-55-55V183zM380 896h-34c-26 0-47-21-47-47v-90h68V828h64V896H380c4 0 0 0 0 0zM759 828V896h90c26 0 47-21 47-47v-90h-68V828h-68zM828 435H896V346c0-26-21-47-47-47h-90v68H828v68zM435 299v68H367V439H299V346C299 320 320 299 346 299h90zM367 649H299v-107h68v107zM546 367V299h107v68h-107zM828 546H896v107h-68v-107zM649 828V896h-107v-68h107zM730 508v188c0 17-17 34-34 34h-188c-17 0-34-17-34-34s17-34 34-34h102l-124-124c-13-13-13-34 0-47 13-13 34-13 47 0l124 124V512c0-17 17-34 34-34 21-4 38 9 38 30z M889 0H135c-32 0-59 26-59 59v906c0 32 26 59 59 59h753c32 0 59-26 59-59v-906c1-33-26-59-58-59zm-165 177c31 0 56 25 56 56s-25 56-56 56-56-25-56-56 25-56 56-56zm-212 0c31 0 56 25 56 56S543 288 512 288s-56-25-56-56S481 177 512 177zm-212 0c31 0 56 25 56 56s-25 56-56 56-56-25-56-56 25-56 56-56zm209 606H285c-25 0-44-20-44-44 0-25 20-44 44-44h224c25 0 44 20 44 44 0 24-20 44-44 44zm230-212H285c-25 0-44-20-44-44 0-25 20-44 44-44h453c25 0 44 20 44 44 1 24-20 44-44 44z @@ -36,6 +36,7 @@ M768 800V685L512 480 256 685V800l256-205L768 800zM512 339 768 544V429L512 224 256 429V544l256-205z M509 546l271-271 91 91-348 349-1-1-13 13-363-361 91-91z M652 157a113 113 0 11156 161L731 395 572 236l80-80 1 1zM334 792v0H175v-159l358-358 159 159-358 358zM62 850h900v113H62v-113z + M926 356V780a73 73 0 01-73 73H171a73 73 0 01-73-73V356l304 258a171 171 0 00221 0L926 356zM853 171a74 74 0 0126 5 73 73 0 0131 22 74 74 0 0111 18c3 8 5 16 6 24L926 244v24L559 581a73 73 0 01-91 3l-4-3L98 268v-24a73 73 0 0140-65A73 73 0 01171 171h683z 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 @@ -49,7 +50,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 + M320 239 213 299l60-107L213 85l107 60L427 85 367 192 427 299 320 239m512 418L939 597l-60 107L939 811l-107-60L725 811l60-107L725 597l107 60M939 85l-60 107L939 299l-107-60L725 299l60-107L725 85l107 60L939 85m-369 460 104-104-90-90-104 104 90 90m44-234 100 100c17 16 17 44 0 60L215 969c-17 17-44 17-60 0l-100-100c-17-16-17-44 0-60L553 311c17-17 44-17 60 0z 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 M922 450c-6-9-15-13-26-13h-11V341c0-41-34-75-75-75H514c-4 0-6-2-9-4l-38-66c-13-21-38-36-64-36H171c-41 0-75 34-75 75v597c0 6 2 13 6 19 6 9 15 13 26 13h640c13 0 26-9 30-21l128-363c4-11 2-21-4-30zM171 224h233c4 0 6 2 9 4l38 66c13 21 38 36 64 36H811c6 0 11 4 11 11v96H256c-13 0-26 9-30 21l-66 186V235c0-6 4-11 11-11zm574 576H173l105-299h572l-105 299z @@ -59,7 +60,9 @@ 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 + M727 641c-78 0-142 55-157 128H256V320h251c16 108 108 192 221 192 124 0 224-100 224-224S851 64 727 64c-113 0-205 84-221 192H96c-18 0-32 14-32 32s14 32 32 32h96v482c0 18 14 32 32 32h347c15 73 79 128 157 128 88 0 160-72 160-160s-72-160-160-160zm0 256c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96z 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 + M566 585l37-146-145 0-37 146 145 0zM1005 297l-32 128q-4 14-18 14l-187 0-37 146 178 0q9 0 14 7 6 8 3 16l-32 128q-3 14-18 14l-187 0-46 187q-4 14-18 14l-128 0q-9 0-15-7-5-7-3-16l45-178-145 0-46 187q-4 14-18 14l-129 0q-9 0-14-7-5-7-3-16l45-178-178 0q-9 0-14-7-5-7-3-16l32-128q4-14 18-14l187 0 37-146-178 0q-9 0-14-7-6-8-3-16l32-128q3-14 18-14l187 0 46-187q4-14 18-14l128 0q9 0 14 7 5 7 3 16l-45 178 145 0 46-187q4-14 18-14l128 0q9 0 14 7 5 7 3 16l-45 178 178 0q9 0 14 7 5 7 3 16z 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 M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z M24 512A488 488 0 01512 24A488 488 0 011000 512A488 488 0 01512 1000A488 488 0 0124 512zm447-325v327L243 619l51 111 300-138V187H471z @@ -87,7 +90,6 @@ M0 512M1024 512M512 0M512 1024M64 576h896V448H64z 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 @@ -111,8 +113,10 @@ M883 567l-128-128c-17-17-43-17-60 0l-128 128c-17 17-17 43 0 60 17 17 43 17 60 0l55-55V683c0 21-21 43-43 43H418c-13-38-43-64-77-77V375c51-17 85-64 85-119 0-73-60-128-128-128-73 0-128 55-128 128 0 55 34 102 85 119v269c-51 17-85 64-85 119 0 73 55 128 128 128 55 0 102-34 119-85H640c73 0 128-55 128-128v-111l55 55c9 9 17 13 30 13 13 0 21-4 30-13 17-13 17-43 0-55zM299 213c26 0 43 17 43 43 0 21-21 43-43 43-26 0-43-21-43-43 0-26 17-43 43-43zm0 597c-26 0-43-21-43-43 0-26 17-43 43-43s43 17 43 43c0 21-17 43-43 43zM725 384c-73 0-128-60-128-128 0-73 55-128 128-128s128 55 128 128c0 68-55 128-128 128zm0-171c-26 0-43 17-43 43s17 43 43 43 43-17 43-43-17-43-43-43z M293 122v244h439V146l171 175V829a73 73 0 01-73 73h-98V536H293v366H195a73 73 0 01-73-73V195a73 73 0 0173-73h98zm366 512v268H366V634h293zm-49 49h-195v73h195v-73zm49-561v171H366V122h293z M0 551V472c0-11 9-19 19-19h984c11 0 19 9 19 19v79c0 11-9 19-19 19H19c-11 0-19-9-19-19zM114 154v240c0 11-9 19-19 19H19C9 413 0 404 0 394V79C0 35 35 0 79 0h315c11 0 19 9 19 19v75c0 11-9 19-19 19H154c-21 0-39 18-39 39zm795 0c0-22-17-39-39-39h-240c-11 0-19-9-19-19V19c0-11 9-19 19-19h315c43 0 79 35 79 79v315c0 11-9 19-19 19h-75c-11 0-19-9-19-19l-1-240zm0 716v-240c0-11 9-19 19-19h75c11 0 19 9 19 19v315c0 43-35 79-79 79h-315c-11 0-19-9-19-19v-75c0-11 9-19 19-19H870c21-1 39-18 39-40zm-795 0c0 21 18 39 39 39h240c11 0 19 9 19 19v75c0 11-9 19-19 19H79C35 1023 0 988 0 945v-315c0-11 9-19 19-19h75c11 0 19 9 19 19V870z + M155 143h715v81H155V143zm358 94 358 369H662l1 278H363V605H155l358-369z M702 677 590 565a148 148 0 10-25 27L676 703zm-346-200a115 115 0 11115 115A115 115 0 01355 478z M928 500a21 21 0 00-19-20L858 472a11 11 0 01-9-9c-1-6-2-13-3-19a11 11 0 015-12l46-25a21 21 0 0010-26l-8-22a21 21 0 00-24-13l-51 10a11 11 0 01-12-6c-3-6-6-11-10-17a11 11 0 011-13l34-39a21 21 0 001-28l-15-18a20 20 0 00-27-4l-45 27a11 11 0 01-13-1c-5-4-10-9-15-12a11 11 0 01-3-12l19-49a21 21 0 00-9-26l-20-12a21 21 0 00-27 6L650 193a9 9 0 01-11 3c-1-1-12-5-20-7a11 11 0 01-7-10l1-52a21 21 0 00-17-22l-23-4a21 21 0 00-24 14L532 164a11 11 0 01-11 7h-20a11 11 0 01-11-7l-17-49a21 21 0 00-24-15l-23 4a21 21 0 00-17 22l1 52a11 11 0 01-8 11c-5 2-15 6-19 7c-4 1-8 0-12-4l-33-40A21 21 0 00313 146l-20 12A21 21 0 00285 184l19 49a11 11 0 01-3 12c-5 4-10 8-15 12a11 11 0 01-13 1L228 231a21 21 0 00-27 4L186 253a21 21 0 001 28L221 320a11 11 0 011 13c-3 5-7 11-10 17a11 11 0 01-12 6l-51-10a21 21 0 00-24 13l-8 22a21 21 0 0010 26l46 25a11 11 0 015 12l0 3c-1 6-2 11-3 16a11 11 0 01-9 9l-51 8A21 21 0 0096 500v23A21 21 0 00114 544l51 8a11 11 0 019 9c1 6 2 13 3 19a11 11 0 01-5 12l-46 25a21 21 0 00-10 26l8 22a21 21 0 0024 13l51-10a11 11 0 0112 6c3 6 6 11 10 17a11 11 0 01-1 13l-34 39a21 21 0 00-1 28l15 18a20 20 0 0027 4l45-27a11 11 0 0113 1c5 4 10 9 15 12a11 11 0 013 12l-19 49a21 21 0 009 26l20 12a21 21 0 0027-6L374 832c3-3 7-5 10-4c7 3 12 5 20 7a11 11 0 018 10l-1 52a21 21 0 0017 22l23 4a21 21 0 0024-14l17-50a11 11 0 0111-7h20a11 11 0 0111 7l17 49a21 21 0 0020 15a19 19 0 004 0l23-4a21 21 0 0017-22l-1-52a11 11 0 018-10c8-3 13-5 18-7l1 0c6-2 9 0 11 3l34 41A21 21 0 00710 878l20-12a21 21 0 009-26l-18-49a11 11 0 013-12c5-4 10-8 15-12a11 11 0 0113-1l45 27a21 21 0 0027-4l15-18a21 21 0 00-1-28l-34-39a11 11 0 01-1-13c3-5 7-11 10-17a11 11 0 0112-6l51 10a21 21 0 0024-13l8-22a21 21 0 00-10-26l-46-25a11 11 0 01-5-12l0-3c1-6 2-11 3-16a11 11 0 019-9l51-8a21 21 0 0018-21v-23zm-565 188a32 32 0 01-51 5a270 270 0 011-363a32 32 0 0151 6l91 161a32 32 0 010 31zM512 782a270 270 0 01-57-6a32 32 0 01-20-47l92-160a32 32 0 0127-16h184a32 32 0 0130 41c-35 109-137 188-257 188zm15-328L436 294a32 32 0 0121-47a268 268 0 0155-6c120 0 222 79 257 188a32 32 0 01-30 41h-184a32 32 0 01-28-16z + 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 M900 287c40 69 60 144 60 225s-20 156-60 225c-40 69-94 123-163 163-69 40-144 60-225 60s-156-20-225-60c-69-40-123-94-163-163C84 668 64 593 64 512s20-156 60-225 94-123 163-163c69-40 144-60 225-60s156 20 225 60 123 94 163 163zM762 512c0-9-3-16-9-22L578 315l-44-44c-6-6-13-9-22-9s-16 3-22 9l-44 44-176 176c-6 6-9 13-9 22s3 16 9 22l44 44c6 6 13 9 22 9s16-3 22-9l92-92v269c0 9 3 16 9 22 6 6 13 9 22 9h62c8 0 16-3 22-9 6-6 9-13 9-22V486l92 92c6 6 13 9 22 9 8 0 16-3 22-9l44-44c6-6 9-13 9-22z M512 939C465 939 427 900 427 853 427 806 465 768 512 768 559 768 597 806 597 853 597 900 559 939 512 939M555 85 555 555 747 363 807 423 512 719 217 423 277 363 469 555 469 85 555 85Z M961 320 512 577 63 320 512 62l449 258zM512 628 185 442 63 512 512 770 961 512l-123-70L512 628zM512 821 185 634 63 704 512 962l449-258L839 634 512 821z @@ -122,8 +126,9 @@ 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 + M512 0C230 0 0 230 0 512c0 145 60 282 166 375L90 1024H512c282 0 512-230 512-512S794 0 512 0z 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 + M1097 372h-460l-146-299H146a73 73 0 00-73 73v731a73 73 0 0073 73h878a73 73 0 0073-73V372zM146 0h390l146 299h488V878a146 146 0 01-146 146H146a146 146 0 01-146-146V146a146 146 0 01146-146zm439 0h195l146 246h-195l-146-246zm244 0h195a146 146 0 01146 146v100h-195l-146-246z 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 @@ -146,7 +151,7 @@ M153 154h768v768h-768v-768zm64 64v640h640v-640h-640z M796 231v727H64V231h732zm-82 78H146V880h567V309zM229 66H960v732H796v-82h82V148h-567v82h-82V66z M248 221a77 77 0 00-30-21c-18-7-40-10-68-5a224 224 0 00-45 13c-5 2-10 5-15 8l-3 2v68l11-9c10-8 21-14 34-19 13-5 26-7 39-7 12 0 21 3 28 10 6 6 9 16 9 29l-62 9c-14 2-26 6-36 11a80 80 0 00-25 20c-7 8-12 17-15 27-6 21-6 44 1 65a70 70 0 0041 43c10 4 21 6 34 6a80 80 0 0063-28v22h64V298c0-16-2-31-6-44a91 91 0 00-18-33zm-41 121v15c0 8-1 15-4 22a48 48 0 01-24 29 44 44 0 01-33 2 29 29 0 01-10-6 25 25 0 01-6-9 30 30 0 01-2-12c0-5 1-9 2-14a21 21 0 015-9 28 28 0 0110-7 83 83 0 0120-5l42-6zm323-68a144 144 0 00-16-42 87 87 0 00-28-29 75 75 0 00-41-11 73 73 0 00-44 14c-6 5-12 11-17 17V64H326v398h59v-18c8 10 18 17 30 21 6 2 13 3 21 3 16 0 31-4 43-11 12-7 23-18 31-31a147 147 0 0019-46 248 248 0 006-57c0-17-2-33-5-49zm-55 49c0 15-1 28-4 39-2 11-6 20-10 27a41 41 0 01-15 15 37 37 0 01-36 1 44 44 0 01-13-12 59 59 0 01-9-18A76 76 0 01384 352v-33c0-10 1-20 4-29 2-8 6-15 10-22a43 43 0 0115-13 37 37 0 0119-5 35 35 0 0132 18c4 6 7 14 9 23 2 9 3 20 3 31zM154 634a58 58 0 0120-15c14-6 35-7 49-1 7 3 13 6 20 12l21 17V572l-6-4a124 124 0 00-58-14c-20 0-38 4-54 11-16 7-30 17-41 30-12 13-20 29-26 46-6 17-9 36-9 57 0 18 3 36 8 52 6 16 14 30 24 42 10 12 23 21 38 28 15 7 32 10 50 10 15 0 28-2 39-5 11-3 21-8 30-14l5-4v-57l-13 6a26 26 0 01-5 2c-3 1-6 2-8 3-2 1-15 6-15 6-4 2-9 3-14 4a63 63 0 01-38-4 53 53 0 01-20-14 70 70 0 01-13-24 111 111 0 01-5-34c0-13 2-26 5-36 3-10 8-19 14-26zM896 384h-256V320h288c21 1 32 12 32 32v384c0 18-12 32-32 32H504l132 133-45 45-185-185c-16-21-16-25 0-45l185-185L637 576l-128 128H896V384z - M128 691H6V38h838v160h-64V102H70v525H128zM973 806H154V250h819v557zm-755-64h691V314H218v429zM365 877h448v64h-448z + M0 512M1024 512M512 0M512 1024M128 691H6V38h838v160h-64V102H70v525H128zM973 806H154V250h819v557zm-755-64h691V314H218v429zM365 877h448v64h-448z 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 M896 96 614 96c-58 0-128-19-179-51C422 38 390 19 358 19L262 19 128 19c-70 0-128 58-128 128l0 736c0 70 58 128 128 128l768 0c70 0 128-58 128-128L1024 224C1024 154 966 96 896 96zM704 685 544 685l0 160c0 19-13 32-32 32s-32-13-32-32l0-160L320 685c-19 0-32-13-32-32 0-19 13-32 32-32l160 0L480 461c0-19 13-32 32-32s32 13 32 32l0 160L704 621c19 0 32 13 32 32C736 666 723 685 704 685zM890 326 102 326 102 250c0-32 32-64 64-64l659 0c38 0 64 32 64 64L890 326z M1182 527a91 91 0 00-88-117H92a91 91 0 00-88 117l137 441A80 80 0 00217 1024h752a80 80 0 0076-56zM133 295a31 31 0 0031 31h858a31 31 0 0031-31A93 93 0 00959 203H226a93 93 0 00-94 92zM359 123h467a31 31 0 0031-31A92 92 0 00765 0H421a92 92 0 00-92 92 31 31 0 0031 31z diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 7bbeca503..7d2e27c08 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -5,6 +5,7 @@ Info Über SourceGit + Hinweise zur Veröffentlichung Open Source & freier Git GUI Client Zu ignorierende Datei(en) hinzufügen Muster: @@ -23,6 +24,8 @@ Neu generieren Verwende OpenAI, um Commit-Nachrichten zu generieren Als Commit-Nachricht verwenden + SourceGit minimieren + Alles anzeigen Patch Patch-Datei: Wähle die anzuwendende .patch-Datei @@ -53,14 +56,18 @@ Überspringen Bisecting. Aktuellen Commit als gut oder schlecht markieren und einen anderen auschecken. Blame - BLAME WIRD BEI DIESER DATEI NICHT UNTERSTÜTZT!!! + Blame auf vorheriger Revision + BLAME WIRD BEI DIESER DATEI NICHT UNTERSTÜTZT!!! Auschecken von ${0}$... Mit ${0}$ vergleichen Mit Worktree vergleichen Branch-Namen kopieren + PR erstellen... + PR für Upstream ${0}$ erstellen... Benutzerdefinierte Aktion Lösche ${0}$... Lösche alle ausgewählten {0} Branches + Beschreibung für ${0}$ bearbeiten... Fast-Forward zu ${0}$ Fetche ${0}$ in ${1}$ hinein... Git Flow - Abschließen ${0}$ @@ -72,13 +79,18 @@ Rebase ${0}$ auf ${1}$... Benenne ${0}$ um... Setze ${0}$ zurück auf ${1}$... + Zu ${0}$ wechseln (Worktree) Setze verfolgten Branch... Branch Vergleich - LOKAL + {0} Commit(s) voraus + {0} Commit(s) voraus, {1} Commit(s) zurück + {0} Commit(s) zurück + Ungültig REMOTE + STATUS VERFOLGT URL - Ungültiger Upstream! + WORKTREE ABBRECHEN Auf Vorgänger-Revision zurücksetzen Auf diese Revision zurücksetzen @@ -96,8 +108,7 @@ Warnung: Beim Auschecken eines Commits wird dein HEAD losgelöst (detached) sein! Lokale Änderungen: Verwerfen - Stashen & wieder anwenden - Alle Submodule updaten + Stashen & wieder anwenden Branch: Dein aktueller HEAD enthält Commit(s) ohne Verbindung zu einem Branch/Tag. Möchtest du trotzdem fortfahren? Auschecken & Fast-Forward @@ -106,7 +117,7 @@ Quelle an Commit-Nachricht anhängen Commit(s): Alle Änderungen committen - Hautplinie: + Hauptlinie: Normalerweise ist es nicht möglich einen Merge zu cherry-picken, da unklar ist welche Seite des Merges die Hauptlinie ist. Diese Option ermöglicht es die Änderungen relativ zum ausgewählten Vorgänger zu wiederholen. Stashes löschen Du versuchst alle Stashes zu löschen. Möchtest du wirklich fortfahren? @@ -131,7 +142,8 @@ SHA Betreff Benutzerdefinierte Aktion - Interakives Rebase + Commit entfernen + Interaktives Rebase Entfernen... Bearbeiten... Fixup in den Vorgänger... @@ -147,6 +159,7 @@ Umformulieren Als Patch speichern... Squash in den Vorgänger + Fixup in den Vorgänger ÄNDERUNGEN geänderte Datei(en) Änderungen durchsuchen... @@ -160,6 +173,9 @@ COMMITTER Prüfe Refs, die diesen Commit enthalten COMMIT ENTHALTEN IN + Email kopieren + Name kopieren + Name & Email kopieren Zeigt nur die ersten 100 Änderungen. Alle Änderungen im ÄNDERUNGEN Tab. Schlüssel: COMMIT-NACHRICHT @@ -168,25 +184,42 @@ SHA Unterzeichner: Im Browser öffnen - Details + Commit-Nachricht eingeben. Leerzeile zum Trennen von Betreff und Beschreibung verwenden! Betreff - Commit-Nachricht Repository Einstellungen COMMIT TEMPLATE - Du kannst ${files_num}, ${branch_name}, ${files} und ${files:N} verwenden, wobei N die maximale Anzahl an auszugebenden Dateipfaden ist. + Vordefinierte Parameter: + + ${branch_name} Name des aktuellen lokalen Branches. + ${files_num} Anzahl der geänderten Dateien + ${files} Pfade der geänderten Dateien + ${files:N} Maximale Anzahl N an Pfaden geänderter Dateien + ${pure_files} Wie ${files}, aber nur die reinen Dateinamen + ${pure_files:N} Wie ${files:N}, aber ohne Ordner Template Inhalt: Template Name: BENUTZERDEFINIERTE AKTION Argumente: - Vordefinierte Parameter: ${REPO} - Repository Pfad; ${BRANCH} - selektierter Branch; ${SHA} - Hash des selektierten Commits; ${TAG} - selektiertes Tag + Vordefinierte Parameter: + + ${REPO} Repository-Pfad + ${REMOTE} selektierter Remote oder selektierter Branch-Remote + ${BRANCH} selektierter Branch, ohne ${REMOTE}-Teil für Remote-Branches + ${BRANCH_FRIENDLY_NAME} Freundlicher Name des selektierten Branches, enthält ${REMOTE}-Teil für Remote-Branches + ${SHA} Hash des selektierten Commits + ${TAG} selektiertes Tag + ${FILE} Ausgewählte Datei, relativ zum Stammverzeichnis des Repositorys + $1, $2 ... Werte der Eingabe-Steuerelemente + Ausführbare Datei: Eingabe-Steuerelemente: Bearbeiten - $1, $2 ... können als Argumente in Eingabe-Steuerelementen benutzt werden Name: Geltungsbereich: Branch Commit + Datei + Remote Repository Tag Auf Beenden der Aktion warten @@ -195,10 +228,12 @@ GIT Remotes automatisch fetchen Minute(n) + Typen für konventionellen Commit Standard Remote Bevorzugter Merge Modus TICKETSYSTEM Beispiel für Azure DevOps Regel hinzufügen + Beispiel für Gerrit Change-Id hinzufügen Beispiel für Gitee Issue Regel hinzufügen Beispiel für Gitee Pull Request Regel hinzufügen Beispiel für GitHub Regel hinzufügen @@ -213,7 +248,7 @@ 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. + Der ausgewählte 'Bevorzugte Service' wird nur in diesem Repository gesetzt und verwendet. Wenn keiner gesetzt ist und mehrere Services verfügbar sind wird ein Kontextmenü zur Auswahl angezeigt. HTTP Proxy HTTP Proxy für dieses Repository Benutzername @@ -227,6 +262,7 @@ Bezeichnung: Einträge: Nutze '|', um Einträge zu trennen + Die vordefinierten Parameter ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE} und ${TAG} bleiben hier verwendbar Typ: Arbeitsplätze Farbe @@ -254,10 +290,9 @@ Erstellten Branch auschecken Lokale Änderungen: Verwerfen - Stashen & wieder anwenden + Stashen & wieder anwenden Neuer Branch-Name: Branch-Name - Leerzeichen werden durch Bindestriche ersetzt. Lokalen Branch erstellen Überschreibe existierenden Branch Tag erstellen... @@ -283,6 +318,9 @@ Auch Remote-Branch ${0}$ löschen Mehrere Branches löschen Du versuchst mehrere Branches auf einmal zu löschen. Kontrolliere noch einmal vor dem Fortfahren! + Mehrere Tags löschen + von Remotes löschen + Du versuchst, mehrere Tags gleichzeitig zu löschen. Sei dir sicher, das doppelt zu prüfen, bevor du loslegst! Remote löschen Remote: Pfad: @@ -301,6 +339,7 @@ Erste Differenz Ignoriere Leerzeichenänderungen Überblenden + Differenz Nebeneinander Wischen Letzte Differenz @@ -319,13 +358,15 @@ 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 Verzeichnisverlauf + Hat lokale Änderungen + Abweichungen vom Upstream + Schon aktuell Änderungen verwerfen Alle Änderungen in der Arbeitskopie. Änderungen: @@ -333,6 +374,11 @@ Nicht-verfolgte Dateien einbeziehen Insgesamt {0} Änderungen werden verworfen Du kannst das nicht rückgängig machen!!! + Commit entfernen + Commit: + Neuer HEAD: + Beschreibung eines Branches bearbeiten + Ziel-Branch: Lesezeichen: Neuer Name: Ziel: @@ -347,6 +393,7 @@ Remote: Remote-Änderungen fetchen Als unverändert betrachten + Benutzerdefinierte Aktion Verwerfen... Verwerfe {0} Dateien... Löse mit ${0}$ @@ -402,11 +449,13 @@ Zeige nur meine Sperren LFS Sperren Entsperren + Alle meine Dateien entsperren + Sollen alle meine Dateien entsperrt werden? Erzwinge entsperren Prune Führt `git lfs prune` aus um alte LFS Dateien vom lokalen Speicher zu löschen Pull - Führt `git lfs pull` aus um alle Git LFS Dasteien für aktuellen Ref & Checkout herunterzuladen + Führt `git lfs pull` aus um alle Git LFS-Dateien für aktuellen Ref & Checkout herunterzuladen LFS Objekte pullen Push Pushe große Dateien in der Warteschlange zum Git LFS Endpunkt @@ -432,7 +481,6 @@ Zum vorherigen Tab wechseln Neuen Tab erstellen Einstellungen öffnen - Aktiven Arbeitsplatz wechseln Aktiven Tab wechseln REPOSITORY Gestagte Änderungen committen @@ -440,6 +488,7 @@ Alle Änderungen stagen und committen Fetch, wird direkt ausgeführt Dashboard Modus (Standard) + Befehlspalette öffnen Commit-Suchmodus Pull, wird direkt ausgeführt Push, wird direkt ausgeführt @@ -473,10 +522,12 @@ Ziel Branch: Link kopieren In Browser öffnen + Befehle FEHLER INFO - Arbeitsplätze + Repositorys öffnen Tabs + Arbeitsplätze Branch mergen Merge-Nachricht anpassen Ziel-Branch: @@ -493,9 +544,11 @@ Wähle Vorgänger-Knoten für: Name: Git wurde NICHT konfiguriert. Gehe bitte zuerst in die [Einstellungen] und konfiguriere Git. + Öffnen + Standard-Texteditor (System) App-Daten Ordner öffnen + Datei öffnen Öffne in Merge Tool - Öffne mit... Optional. Neue Seite erstellen Lesezeichen @@ -503,6 +556,7 @@ Andere Tabs schließen Rechte Tabs schließen Kopiere Repository-Pfad + Aktualisieren Repositories Einfügen Vor {0} Tagen @@ -510,12 +564,11 @@ Vor {0} Stunden Gerade eben Letzter Monat - Leztes Jahr + Letztes Jahr Vor {0} Minuten Vor {0} Monaten Vor {0} Jahren Gestern - Verwende 'Shift+Enter' um eine neue Zeile einzufügen. 'Enter' ist das Kürzel für den OK Button Einstellungen OPEN AI System-Prompt für Diff Analyse @@ -523,6 +576,7 @@ System-Prompt für Erstellung von Commit-Nachricht Modell Name + Der eingegebene Wert ist der Name der Environment-Variable, von der der Schlüssel gelesen wird Server Streaming aktivieren DARSTELLUNG @@ -532,9 +586,9 @@ Standard Texteditor Monospace-Schriftart - Verwende die Monospace-Schriftart nur im Texteditor Design Design-Anpassungen + Scrollbars automatisch ausblenden Fixe Tab-Breite in Titelleiste Verwende nativen Fensterrahmen DIFF/MERGE TOOL @@ -544,12 +598,16 @@ ALLGEMEIN Beim Starten nach Updates suchen Datumsformat + Aktiviere kompakte Ordner im Änderungsbaum Sprache Commit-Historie Zeige Autor Zeitpunkt anstatt Commit Zeitpunkt + Standardmäßig Seite `Änderungen` anzeigen + Standardmäßig Tab `ÄNDERUNGEN` in Commit-Details anzeigen Zeige Nachfolger in den Commit Details Zeige Tags im Commit Verlauf Längenvorgabe für Commit-Nachrichten + Standard-Avatar im GitHub-Stil generieren GIT Aktiviere Auto-CRLF Standard Klon-Ordner @@ -585,8 +643,7 @@ Lokaler Branch: Lokale Änderungen: Verwerfen - Stashen & wieder anwenden - Alle Submodule aktualisieren + Stashen & wieder anwenden Remote: Pull (Fetch & Merge) Rebase anstatt Merge verwenden @@ -606,6 +663,8 @@ Zu allen Remotes pushen Remote: Tag: + Push zu NEUEM Branch + Eingabe des Namens des neuen Remote-Branch: Schließen Aktuellen Branch rebasen Lokale Änderungen stashen & wieder anwenden @@ -617,6 +676,7 @@ Repository URL: Remote Git Repository URL URL kopieren + Benutzerdefinierte Aktion Löschen... Bearbeiten... Fetch @@ -642,6 +702,7 @@ WEITER Benutzerdefinierte Aktionen Keine benutzerdefinierten Aktionen + Dashboard Alle Änderungen verwerfen Öffne im Datei-Browser Suche Branches/Tags/Submodule @@ -656,6 +717,7 @@ Commit Zeitpunkt Topologie LOKALE BRANCHES + Mehr Optionen... Zum HEAD wechseln Erstelle Branch BENACHRICHTIGUNGEN LÖSCHEN @@ -714,6 +776,7 @@ Patch wurde erfolgreich gespeichert! Durchsuche Repositories Hauptverzeichnis: + Anderes benutzerdefiniertes Verzeichnis durchsuchen Suche nach Updates... Neue Version ist verfügbar: Suche nach Updates fehlgeschlagen! @@ -732,8 +795,6 @@ Upstream: SHA kopieren Zum Commit wechseln - Squash Commits - In: SSH privater Schlüssel: Pfad zum privaten SSH Schlüssel START @@ -743,7 +804,7 @@ Optional. Informationen zu diesem Stash Modus: Nur gestagte Änderungen - Gestagte und unstagte Änderungen der ausgewähleten Datei(en) werden gestasht!!! + Gestagte und ungestagte Änderungen der ausgewählten Datei(en) werden gestasht!!! Lokale Änderungen stashen Anwenden Kopiere Nachricht @@ -763,6 +824,7 @@ SUBMODULE Submodul hinzufügen BRANCH + Branch Relativen Pfad De-initialisiere Submodul Untergeordnete Submodule fetchen @@ -782,10 +844,15 @@ Update URL OK - Tag-Namen kopieren - Tag-Nachricht kopieren + TAGGER + TIME + Nachricht + Name + Tagger + Name des Tags kopieren Benutzerdefinierte Aktion Lösche ${0}$... + Selektierte {0} Tags löschen... Merge ${0}$ in ${1}$ hinein... Pushe ${0}$... Submodule aktualisieren @@ -816,11 +883,13 @@ Änderungen Git Ignore Ignoriere alle *{0} Dateien - Ignoriere *{0} Datein im selben Ordner + Ignoriere *{0} Dateien im selben Ordner Ignoriere nicht-verfolgte Dateien in diesem Ordner Ignoriere nur diese Datei Amend Du kannst diese Datei jetzt stagen. + Verlauf löschen + Soll wirklich der Verlauf aller Commit-Nachrichten gelöscht werden? Das kann nicht rückgängig gemacht werden. COMMIT COMMIT & PUSH Template/Historie @@ -838,6 +907,7 @@ NICHT-VERFOLGTE DATEIEN EINBEZIEHEN KEINE BISHERIGEN COMMIT-NACHRICHTEN KEINE COMMIT TEMPLATES + Ohne Überprüfung Autor zurücksetzen SignOff GESTAGED @@ -853,6 +923,7 @@ WORKTREE Pfad kopieren Sperren + Öffnen Entfernen Entsperren diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 2166f05f4..d60e868e3 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1,6 +1,7 @@ About About SourceGit + Release Notes Opensource & Free Git GUI Client Add File(s) To Ignore Pattern: @@ -19,6 +20,8 @@ RE-GENERATE Use AI to generate commit message APPLY AS COMMIT MESSAGE + Hide SourceGit + Show All Patch Patch File: Select .patch file to apply @@ -49,14 +52,18 @@ Skip Bisecting. Mark current commit as good or bad and checkout another one. Blame - BLAME ON THIS FILE IS NOT SUPPORTED!!! + Blame on Previous Revision + BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout ${0}$... Compare with ${0}$ Compare with Worktree Copy Branch Name + Create PR... + Create PR for upstream ${0}$... Custom Action Delete ${0}$... Delete selected {0} branches + Edit description for ${0}$... Fast-Forward to ${0}$ Fetch ${0}$ into ${1}$... Git Flow - Finish ${0}$ @@ -68,13 +75,18 @@ Rebase ${0}$ on ${1}$... Rename ${0}$... Reset ${0}$ to ${1}$... + Switch to ${0}$ (worktree) Set Tracking Branch... Branch Compare - LOCAL + {0} commit(s) ahead + {0} commit(s) ahead, {1} commit(s) behind + {0} commit(s) behind + Invalid REMOTE + STATUS TRACKING URL - Invalid upstream! + WORKTREE CANCEL Reset to Parent Revision Reset to This Revision @@ -92,8 +104,7 @@ Warning: By doing a commit checkout, your Head will be detached Local Changes: Discard - Stash & Reapply - Update all submodules + Stash & Reapply Branch: Your current HEAD contains commit(s) not connected to any branches/tags! Do you want to continue? Checkout & Fast-Forward @@ -127,6 +138,7 @@ SHA Subject Custom Action + Drop Commit Interactive Rebase Drop... Edit... @@ -143,6 +155,7 @@ Reword Save as Patch... Squash into Parent + Fixup into Parent CHANGES changed file(s) Search Changes... @@ -156,6 +169,9 @@ COMMITTER Check refs that contains this commit COMMIT IS CONTAINED BY + Copy Email + Copy Name + Copy Name & Email Shows only the first 100 changes. See all changes on the CHANGES tab. Key: MESSAGE @@ -164,25 +180,41 @@ SHA Signer: Open in Browser - Description + Enter commit message. Please use an empty-line to separate subject and description! SUBJECT - Enter commit subject Repository Configure COMMIT TEMPLATE - You can use ${files_num}, ${branch_name}, ${files} and ${files:N} where N is the max number of file paths to output. + Built-in parameters: + + ${branch_name} Current local branch name. + ${files_num} Number of changed files + ${files} Paths of changed files + ${files:N} Max N number of paths of changed files + ${pure_files} Likes ${files}, but only pure file names + ${pure_files:N} Likes ${files:N}, but without folders Template Content: Template Name: CUSTOM ACTION Arguments: - Built-in parameters: ${REPO} - repository's path; ${BRANCH} - selected branch; ${SHA} - selected commit's hash; ${TAG} - selected tag + Built-in parameters: + + ${REPO} Repository's path + ${REMOTE} Selected remote or selected branch's remote + ${BRANCH} Selected branch, without ${REMOTE} part for remote branches + ${BRANCH_FRIENDLY_NAME} Friendly name of selected branch, contains ${REMOTE} part for remote branches + ${SHA} Selected commit's hash + ${TAG} Selected tag + ${FILE} Selected file, relative to repository root + $1, $2 ... Input control values Executable File: Input Controls: Edit - You can use $1, $2 ... in arguments for input control values Name: Scope: Branch Commit + File + Remote Repository Tag Wait for action exit @@ -191,16 +223,18 @@ GIT Fetch remotes automatically Minute(s) + Conventional Commit Types 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 + Add Azure DevOps Rule + Add Gerrit Change-Id Commit Rule + Add Gitee Issue Rule + Add Gitee Pull Request Rule + Add GitHub Rule + Add GitLab Issue Rule + Add GitLab Merge Request Rule + Add Jira Rule New Rule Issue Regex Expression: Rule Name: @@ -222,7 +256,8 @@ Is Folder: Label: Options: - Use '|' as delimitter for options + Use '|' as delimiter for options + The built-in variables ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, and ${TAG} remain available here Type: Workspaces Color @@ -250,10 +285,9 @@ Check out the created branch Local Changes: Discard - Stash & Reapply + Stash & Reapply New Branch Name: Enter branch name. - Spaces will be replaced with dashes. Create Local Branch Overwrite existing branch Create Tag... @@ -279,6 +313,9 @@ Also delete remote branch ${0}$ Delete Multiple Branches You are trying to delete multiple branches at one time. Be sure to double-check before taking action! + Delete Multiple Tags + Delete them from remotes + You are trying to delete multiple tags at one time. Be sure to double-check before taking action! Delete Remote Remote: Path: @@ -295,8 +332,9 @@ BINARY DIFF File Mode Changed First Difference - Ignore All Whitespace Changes + Ignore Whitespace Changes BLEND + DIFFERENCE SIDE-BY-SIDE SWIPE Last Difference @@ -315,13 +353,15 @@ 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 Dir History + Has Local Changes + Mismatched with Upstream + Already Up-To-Date Discard Changes All local changes in working copy. Changes: @@ -329,6 +369,11 @@ Include untracked files {0} changes will be discarded You can't undo this action!!! + Drop Commit + Commit: + New HEAD: + Edit Branch's Description + Target: Bookmark: New Name: Target: @@ -343,6 +388,7 @@ Remote: Fetch Remote Changes Assume unchanged + Custom Action Discard... Discard {0} files... Resolve Using ${0}$ @@ -398,6 +444,8 @@ Show only my locks LFS Locks Unlock + Unlock all of my locks + Are you sure you want to unlock all your locked files? Force Unlock Prune Run `git lfs prune` to delete old LFS files from local storage @@ -428,7 +476,7 @@ Go to previous tab Create new tab Open Preferences dialog - Switch active workspace + Show workspace dropdown menu Switch active tab REPOSITORY Commit staged changes @@ -436,6 +484,7 @@ Stage all changes and commit Fetch, starts directly Dashboard mode (Default) + Open command palette Commit search mode Pull, starts directly Push, starts directly @@ -469,10 +518,12 @@ Target Branch: Copy Link Open in Browser + Commands ERROR NOTICE - Workspaces + Open Repositories Tabs + Workspaces Merge Branch Customize merge message Into: @@ -489,9 +540,11 @@ Select parent node for: Name: Git has NOT been configured. Please to go [Preferences] and configure it first. + Open + Default Editor (System) Open Data Storage Directory + Open File Open in Merge Tool - Open with... Optional. Create New Tab Bookmark @@ -499,6 +552,8 @@ Close Other Tabs Close Tabs to the Right Copy Repository Path + Move to Workspace + Refresh Repositories Paste {0} days ago @@ -511,7 +566,6 @@ {0} months ago {0} years ago Yesterday - Use 'Shift+Enter' to input a new line. 'Enter' is the hotkey of OK button Preferences AI Analyze Diff Prompt @@ -519,6 +573,7 @@ Generate Subject Prompt Model Name + Entered value is the name to load API key from ENV Server Enable Streaming APPEARANCE @@ -528,24 +583,32 @@ Default Editor Monospace Font - Use monospace font only in text editor Theme Theme Overrides + Use auto-hide scrollbars Use fixed tab width in titlebar Use native window frame DIFF/MERGE TOOL + Diff Arguments + Available variables: $LOCAL, $REMOTE + Merge Arguments + Available variables: $BASE, $LOCAL, $REMOTE, $MERGED Install Path Input path for diff/merge tool Tool GENERAL Check for updates on startup Date Format + Enable compact folders in changes tree Language History Commits Show author time instead of commit time in graph + Show `LOCAL CHANGES` page by default + Show `CHANGES` tab in commit detail by default Show children in the commit details Show tags in commit graph Subject Guide Length + Generate Github style default avatar GIT Enable Auto CRLF Default Clone Dir @@ -570,6 +633,8 @@ User's gpg signing key INTEGRATION SHELL/TERMINAL + Arguments + Please use '.' to indicate working directory Path Shell/Terminal Prune Remote @@ -581,8 +646,7 @@ Into: Local Changes: Discard - Stash & Reapply - Update all submodules + Stash & Reapply Remote: Pull (Fetch & Merge) Use rebase instead of merge @@ -602,6 +666,8 @@ Push to all remotes Remote: Tag: + Push to a NEW branch + Input name of the new remote branch: Quit Rebase Current Branch Stash & reapply local changes @@ -613,6 +679,7 @@ Repository URL: Remote git repository URL Copy URL + Custom Action Delete... Edit... Fetch @@ -638,6 +705,7 @@ CONTINUE Custom Actions No Custom Actions + Dashboard Discard all changes Open in File Browser Search Branches/Tags/Submodules @@ -652,6 +720,7 @@ Commit Date Topologically LOCAL BRANCHES + More options... Navigate to HEAD Create Branch CLEAR NOTIFICATIONS @@ -710,6 +779,7 @@ Patch has been saved successfully! Scan Repositories Root Dir: + Scan another custom directory Check for Updates... New version of this software is available: Check for updates failed! @@ -728,8 +798,9 @@ Upstream: Copy SHA Go to - Squash Commits - Into: + Squash HEAD into Parent + Fixup HEAD into Parent + Into: SSH Private Key: Private SSH key store path START @@ -759,6 +830,7 @@ SUBMODULES Add Submodule BRANCH + Branch Relative Path De-initialize Fetch nested submodules @@ -778,10 +850,15 @@ Update URL OK - Copy Tag Name - Copy Tag Message + TAGGER + TIME + Message + Name + Tagger + Copy Tag Name Custom Action Delete ${0}$... + Delete selected {0} tags... Merge ${0}$ into ${1}$... Push ${0}$... Update Submodules @@ -817,6 +894,8 @@ Ignore this file only Amend You can stage this file now. + Clear History + Are you sure you want to clear all commit message history? This action cannot be undone. COMMIT COMMIT & PUSH Template/History @@ -834,6 +913,7 @@ INCLUDE UNTRACKED FILES NO RECENT INPUT MESSAGES NO COMMIT TEMPLATES + No-Verify Reset Author SignOff STAGED @@ -849,6 +929,7 @@ WORKTREE Copy Path Lock + Open Remove Unlock diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 9e0c08de0..0aa2d2483 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -5,6 +5,7 @@ Acerca de Acerca de SourceGit + Notas de la versión (Release) Cliente Git GUI de código abierto y gratuito Agregar Archivo(s) Para Ignorar Patrón: @@ -22,7 +23,9 @@ Asistente OpenAI RE-GENERAR Usar OpenAI para generar mensaje de commit - APLICAR CÓMO MENSAJE DE COMMIT + APLICAR COMO MENSAJE DE COMMIT + Ocultar SourceGit + Mostrar Todo Aplicar Parche Archivo del Parche: Seleccionar archivo .patch para aplicar @@ -51,16 +54,20 @@ Bisecting. ¿Es el HEAD actual bueno o malo? Bueno Saltar - Bisecting. Marcar el commit actual cómo bueno o malo y revisar otro. + Bisecting. Marcar el commit actual como bueno o malo y revisar otro. Blame - ¡BLAME EN ESTE ARCHIVO NO SOPORTADO! + Blame sobre la Revisión Previa + ¡BLAME EN ESTE ARCHIVO NO SOPORTADO! Checkout ${0}$... Comparar con ${0}$ Comparar con Worktree - Copiar Nombre de Rama + Copiar Nombre de la Rama + Crear PR... + Crear PR para upstream ${0}$... Acción personalizada Eliminar ${0}$... Eliminar {0} ramas seleccionadas + Editar la descripción para ${0}$... Fast-Forward a ${0}$ Fetch ${0}$ en ${1}$... Git Flow - Finalizar ${0}$ @@ -72,13 +79,18 @@ Rebase ${0}$ en ${1}$... Renombrar ${0}$... Resetear ${0}$ a ${1}$... + Cambiar a ${0}$ (worktree) Establecer Rama de Seguimiento... Comparar Ramas - LOCAL + {0} commit(s) adelante + {0} commit(s) adelante, {1} commit(s) detrás + {0} commit(s) detrás + Inválido REMOTO + ESTADO SEGUIMIENTO URL - ¡Upstream inválido! + WORKTREE CANCELAR Resetear a Revisión Padre Resetear a Esta Revisión @@ -96,8 +108,7 @@ Advertencia: Al hacer un checkout de commit, tu Head se separará Cambios Locales: Descartar - Stash & Reaplicar - Actualizar todos los submódulos + Stash & Reaplicar Rama: ¡Tu HEAD actual contiene commit(s) que no están conectados a ningunas ramas/etiquetas! ¿Quieres continuar? Checkout & Fast-Forward @@ -131,6 +142,7 @@ SHA Asunto Acción personalizada + Eliminar Commit Rebase interactivo Eliminar... Editar... @@ -146,7 +158,8 @@ Revertir Commit Reescribir Guardar como Parche... - Squash en Parent + Squash en el Padre + Arreglar en el Padre CAMBIOS archivo(s) modificado(s) Buscar Cambios... @@ -160,6 +173,9 @@ COMMITTER Ver refs que contienen este commit COMMIT ESTÁ CONTENIDO EN + Copiar Email + Copiar Nombre + Copiar Nombre & Email Muestra solo los primeros 100 cambios. Ver todos los cambios en la pestaña CAMBIOS. Clave: MENSAJE @@ -168,25 +184,41 @@ SHA Firmante: Abrir en Navegador - Descripción + Ingresa el mensaje de commit. ¡Por favor usa una línea en blanco para separar el título y la descripción! ASUNTO - Introducir asunto del commit Configurar Repositorio PLANTILLA DE COMMIT - Puedes usar ${files_num}, ${branch_name}, ${files} y ${files:N} donde N es el número máximo de rutas de archivo a la salida. + Parámetros incorporados: + + ${branch_name} Nombre de la rama local actual. + ${files_num} Número de archivos modificados + ${files} Rutas de archivos modificados + ${files:N} Número N máximo de rutas de archivos modificados + ${pure_files} Cómo ${files}, pero solo nombres de archivos puros + ${pure_files:N} Cómo ${files:N}, pero sin carpetas Contenido de la Plantilla: Nombre de la Plantilla: ACCIÓN PERSONALIZADA Argumentos: - Parámetros incorporados: ${REPO} - ruta del repositorio; ${BRANCH} - rama seleccionada; ${SHA} - hash del commit seleccionado; ${TAG} - etiqueta seleccionada + Parámetros incorporados: + + ${REPO} Ruta del repositorio + ${REMOTE} Remoto seleccionado o Remoto de la rama seleccionada + ${BRANCH} Rama seleccionada, sin la parte ${REMOTE} para ramas remotas + ${BRANCH_FRIENDLY_NAME} Nombre amigable de la rama seleccionada, contiene la parte ${REMOTE} para ramas remotas + ${SHA} Hash del commit seleccionado + ${TAG} Etiqueta seleccionada + ${FILE} Archivo seleccionado, relativo a la raíz del repositorio + $1, $2 ... Valores de control de entrada Archivo Ejecutable: Controles de entrada: Editar - Puedes usar $1, $2 ... en argumentos, para valores de los controles de entrada Nombre: Alcance: Rama Commit + Archivo + Remoto Repositorio Etiqueta Esperar la acción de salida @@ -195,10 +227,12 @@ GIT Fetch remotos automáticamente Minuto(s) + Tipos de Commit Convencionales Remoto por Defecto Modo preferido de Merge SEGUIMIENTO DE INCIDENCIAS Añadir Regla de Ejemplo para Azure DevOps + Añadir Regla de "Gerrit Change-Id Commit" 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 @@ -216,8 +250,8 @@ 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 + Nombre del Usuario + Nombre del usuario para este repositorio Editar Controles de Acción Personalizados Valor Comprobado: Cuando sea comprobado, este valor será usado en argumentos de la línea de comandos @@ -227,6 +261,7 @@ Etiqueta: Opciones: Usar '|' como delimitador para las opciones + La variables incorporadas ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, y ${TAG} permanecen disponibles aquí Tipo: Espacios de Trabajo Color @@ -254,10 +289,9 @@ Checkout de la rama creada Cambios Locales: Descartar - Stash & Reaplicar + 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... @@ -282,7 +316,10 @@ ¡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! + Estás intentando eliminar múltiples ramas a la vez. ¡Asegúrate de comprobar dos veces antes de realizar esta acción! + Eliminar Múltiples Etiquetas + Eliminarlas de los remotos + Estás intentando eliminar múltiples etiquetas a la vez. ¡Asegúrate de comprobar dos veces antes de realizar esta acción! Eliminar Remoto Remoto: Ruta: @@ -301,6 +338,7 @@ Primera Diferencia Ignorar Cambio de Espacios en Blanco MEZCLAR + DIFERENCIA LADO-A-LADO DESLIZAR Última Diferencia @@ -319,13 +357,15 @@ 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 Historial del directorio + Tiene Cambios Locales + No coincide con Upstream + Ya se encuentra actualizado Descartar Cambios Todos los cambios locales en la copia de trabajo. Cambios: @@ -333,6 +373,11 @@ Incluir archivos no rastreados Total {0} cambios serán descartados ¡No puedes deshacer esta acción! + Eliminar Commit + Commit: + Nuevo HEAD: + Editar la descripción de la rama + Destino: Marcador: Nuevo Nombre: Destino: @@ -346,7 +391,8 @@ Fetch sin etiquetas Remoto: Fetch Cambios Remotos - Asumir sin cambios + Asumir como sin cambios + Acción Personalizada Descartar... Descartar {0} archivos... Resolver usando ${0}$ @@ -402,6 +448,8 @@ Mostrar solo mis bloqueos Bloqueos LFS Desbloquear + Desbloquear todos mis candados + ¿Estás seguro de querer desbloquear todos tus archivos bloqueados? Forzar Desbloqueo Prune Ejecuta `git lfs prune` para eliminar archivos LFS antiguos del almacenamiento local @@ -432,7 +480,7 @@ Ir a la página anterior Crear nueva página Abrir diálogo de preferencias - Cambiar espacio de trabajo activo + Mostrar menú desplegable del espacio de trabajo Cambiar página activa REPOSITORIO Commit cambios staged @@ -440,6 +488,7 @@ Stage todos los cambios y commit Fetch, empieza directamente Modo Dashboard (Por Defecto) + Abrir paleta de comandos Modo de búsqueda de commits Pull, empieza directamente Push, empieza directamente @@ -473,10 +522,12 @@ Rama Objetivo: Copiar Enlace Abrir en el Navegador + Comandos ERROR AVISO - Espacios de trabajo + Abrir Repositorios Páginas + Espacios de trabajo Merge Rama Personalizar mensaje de merge En: @@ -493,9 +544,11 @@ Seleccionar nodo padre para: Nombre: Git NO ha sido configurado. Por favor, ve a [Preferencias] y configúralo primero. + Abrir + Editor por defecto (Sistema) Abrir Directorio de Datos de la App + Abrir Archivo Abrir en Herramienta de Merge - Abrir Con... Opcional. Crear Nueva Página Marcador @@ -503,6 +556,8 @@ Cerrar Otras Pestañas Cerrar Pestañas a la Derecha Copiar Ruta del Repositorio + Mover al Espacio de trabajo + Actualizar Repositorios Pegar Hace {0} días @@ -515,7 +570,6 @@ Hace {0} meses Hace {0} años Ayer - Usa 'Shift+Enter' para introducir una nueva línea. 'Enter' es el atajo del botón OK Preferencias OPEN AI Analizar Diff Prompt @@ -523,8 +577,9 @@ Generar Subject Prompt Modelo Nombre + El valor ingresado es el nombre de la clave API a cargar desde ENV Servidor - Activar Transmisión + Habilitar Transmisión APARIENCIA Fuente por defecto Ancho de la Pestaña del Editor @@ -532,28 +587,36 @@ Por defecto Editor Fuente Monospace - Usar solo fuente monospace en el editor de texto Tema Sobreescritura de temas + Usar barras de desplazamiento que se oculten automáticamente Usar ancho de pestaña fijo en la barra de título Usar marco de ventana nativo HERRAMIENTA DIFF/MERGE + Argumentos para Diff + Variables disponibles: $LOCAL, $REMOTE + Argumentos para Merge + Variables disponibles: $BASE, $LOCAL, $REMOTE, $MERGED Ruta de instalación Introducir ruta para la herramienta diff/merge Herramienta GENERAL Buscar actualizaciones al iniciar Formato de Fecha + Habilitar carpetas compactas en el árbol de cambios Idioma Commits en el historial Mostrar hora del autor en lugar de la hora del commit en el gráfico + Mostrar la página `CAMBIOS LOCALES` por defecto + Mostrar pestaña de `CAMBIOS` en los detalles del commit por defecto Mostrar hijos en los detalles de commit Mostrar etiquetas en el gráfico de commit Longitud de la guía del asunto + Generar avatar con estilo por defecto de Github GIT Habilitar Auto CRLF Directorio de clonado por defecto - Email de usuario + Email del usuario Email global del usuario git Habilitar --prune para fetch Habilitar --ignore-cr-at-eol en diff @@ -561,7 +624,7 @@ Ruta de instalación Habilitar verificación HTTP SSL Usar git-credential-libsecret en lugar de git-credential-manager - Nombre de usuario + Nombre del usuario Nombre global del usuario git Versión de Git FIRMA GPG @@ -574,6 +637,8 @@ Clave de firma gpg del usuario INTEGRACIÓN SHELL/TERMINAL + Argumentos + Por favor utiliza '.' para indicar el directorio de trabajo Ruta Shell/Terminal Podar Remoto @@ -585,8 +650,7 @@ En: Cambios Locales: Descartar - Stash & Reaplicar - Actualizar todos los submódulos + Stash & Reaplicar Remoto: Pull (Fetch & Merge) Usar rebase en lugar de merge @@ -606,6 +670,8 @@ Push a todos los remotos Remoto: Etiqueta: + Push a una NUEVA rama + Nombre de entrada de la nueva rama remota: Salir Rebase Rama Actual Stash & reaplicar cambios locales @@ -617,6 +683,7 @@ URL del Repositorio: URL del repositorio git remoto Copiar URL + Acción Personalizada Borrar... Editar... Fetch @@ -642,6 +709,7 @@ CONTINUAR Acciones Personalizadas No hay ninguna Acción Personalizada + Dashboard Descartar todos los cambios Abrir en el Explorador Buscar Ramas/Etiquetas/Submódulos @@ -656,6 +724,7 @@ Fecha de Commit Topológicamente RAMAS LOCALES + Más opciones... Navegar a HEAD Crear Rama LIMPIAR NOTIFICACIONES @@ -714,6 +783,7 @@ ¡El parche se ha guardado exitosamente! Escanear Repositorios Directorio Raíz: + Escanear otro directorio personalizado Buscar Actualizaciones... Nueva versión de este software disponible: ¡Error al buscar actualizaciones! @@ -732,8 +802,9 @@ Upstream: Copiar SHA Ir a - Squash Commits - En: + Squash HEAD en el Padre + Fixup HEAD en el Padre + En: Clave Privada SSH: Ruta de almacenamiento de la clave privada SSH INICIAR @@ -763,6 +834,7 @@ SUBMÓDULOS Añadir Submódulo RAMA + Rama Ruta Relativa Desinicializar Submódulo Fetch submódulos anidados @@ -782,10 +854,15 @@ Actualizar URL OK - Copiar Nombre de la Etiqueta - Copiar Mensaje de la Etiqueta + ETIQUETADOR + HORA + Mensaje + Nombre + Etiquetador + Copiar Nombre de la Etiqueta Acción Personalizada Eliminar ${0}$... + Eliminar {0} etiquetas seleccionadas... Merge ${0}$ en ${1}$... Push ${0}$... Actualizar Submódulos @@ -821,6 +898,8 @@ Ignorar solo este archivo Enmendar Puedes hacer stage a este archivo ahora. + Limpiar Historial + ¿Estás seguro de querer limpiar todo el historial de los mensajes de commit? Esta acción no se puede deshacer. COMMIT COMMIT & PUSH Plantilla/Historias @@ -838,6 +917,7 @@ INCLUIR ARCHIVOS NO RASTREADOS NO HAY MENSAJES DE ENTRADA RECIENTES NO HAY PLANTILLAS DE COMMIT + Sin verificar Restablecer Autor Firmar STAGED @@ -846,13 +926,14 @@ UNSTAGED STAGE STAGE TODO - VER ASSUME UNCHANGED + VER ASUMIR COMO SIN CAMBIOS Plantilla: ${0}$ ESPACIO DE TRABAJO: Configura Espacios de Trabajo... WORKTREE Copiar Ruta Bloquear + Abrir Eliminar Desbloquear diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index e5e443ea1..5f0c6f109 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -5,7 +5,11 @@ À propos À propos de SourceGit + Notes de Version Client Git Open Source et Gratuit + Ajouter le(s) Fichier(s) à Ignorer + Modèle : + Fichier de Stockage : Ajouter un Worktree Emplacement : Chemin vers ce worktree. Relatif supporté. @@ -20,6 +24,8 @@ RE-GÉNÉRER Utiliser l'IA pour générer un message de commit APPLIQUER COMME MESSAGE DE COMMIT + Masquer SourceGit + Tout Afficher Appliquer Fichier de patch : Selectionner le fichier .patch à appliquer @@ -36,16 +42,28 @@ Révision : Archiver SourceGit Askpass + Saisir la phrase secrète : FICHIERS PRÉSUMÉS INCHANGÉS PAS DE FICHIERS PRÉSUMÉS INCHANGÉS + Charger l'Image... Rafraîchir FICHIER BINAIRE NON SUPPORTÉ !!! + Bisect + Annuler + Mauvais + Bisect en cours. Le HEAD actuel est-il bon ou mauvais ? + Bon + Passer + Bisect en cours. Marquer le commit actuel comme bon ou mauvais et en récupérer un autre. Blâme - LE BLÂME SUR CE FICHIER N'EST PAS SUPPORTÉ!!! + Blâmer sur la révision précédente + LE BLÂME SUR CE FICHIER N'EST PAS SUPPORTÉ!!! Récupérer ${0}$... Comparer avec ${0}$ Comparer avec le worktree Copier le nom de la branche + Créer une PR... + Créer une PR pour l'upstream ${0}$... Action personnalisée Supprimer ${0}$... Supprimer {0} branches sélectionnées @@ -59,9 +77,19 @@ Pousser ${0}$ Rebaser ${0}$ sur ${1}$... Renommer ${0}$... + Réinitialiser ${0}$ sur ${1}$... + Basculer vers ${0}$ (worktree) Définir la branche de suivi... Comparer les branches - Branche en amont invalide! + {0} commit(s) en avance + {0} commit(s) en avance, {1} commit(s) en retard + {0} commit(s) en retard + Invalide + DISTANT + STATUT + SUIVI + URL + WORKTREE ANNULER Réinitialiser à la révision parente Réinitialiser à cette révision @@ -70,14 +98,20 @@ Afficher comme liste de dossiers/fichiers Afficher comme liste de chemins Afficher comme arborescence + Changer l'URL du sous-module + Sous-module : + URL : Récupérer la branche Récupérer ce commit Commit : Avertissement: une récupération vers un commit aboutiera vers un HEAD détaché Changements locaux : Annuler - Mettre en stash et réappliquer + Mettre en stash et réappliquer Branche : + Votre HEAD actuel contient un ou plusieurs commits non connectés à une branche/tag ! Voulez-vous continuer ? + Récupérer & Fast-Forward + Fast-Forward vers : Cherry-Pick de ce commit Ajouter la source au message de commit Commit : @@ -101,15 +135,32 @@ Cherry-Pick ... Comparer avec HEAD Comparer avec le worktree + Auteur + Message + Committer SHA + Sujet Action personnalisée + Supprimer le Commit + Rebase Interactif + Supprimer... + Modifier... + Fixup dans le Parent... + Rebaser Interactivement ${0}$ sur ${1}$ + Reformuler... + Squash dans le Parent... Fusionner dans ${0}$ Fusionner ... + Pousser ${0}$ vers ${1}$ + Rebaser ${0}$ sur ${1}$ + Réinitialiser ${0}$ sur ${1}$ Annuler le commit Reformuler Enregistrer en tant que patch... - Squash dans le parent + Squash dans le Parent + Fixup dans le Parent CHANGEMENTS + fichier(s) modifié(s) Rechercher les changements... FICHIERS Fichier LFS @@ -121,35 +172,65 @@ COMMITTER Vérifier les références contenant ce commit LE COMMIT EST CONTENU PAR + Copier l'E-mail + Copier le Nom + Copier le Nom & l'E-mail Afficher seulement les 100 premiers changements. Voir tous les changements dans l'onglet CHANGEMENTS. + Clé : MESSAGE PARENTS REFS SHA + Signataire : Ouvrir dans le navigateur - Description - Entrez le message du commit + SUJET Configurer le dépôt MODÈLE DE COMMIT + Paramètres intégrés : + + ${branch_name} Nom de la branche locale actuelle. + ${files_num} Nombre de fichiers modifiés + ${files} Chemins des fichiers modifiés + ${files:N} Nombre maximum N de chemins de fichiers modifiés + ${pure_files} Comme ${files}, mais uniquement les noms de fichiers purs + ${pure_files:N} Comme ${files:N}, mais sans les dossiers Contenu de modèle: Nom de modèle: ACTION PERSONNALISÉE Arguments : + Paramètres intégrés : + + ${REPO} Chemin du dépôt + ${REMOTE} Dépôt distant sélectionné ou dépôt distant de la branche sélectionnée + ${BRANCH} Branche sélectionnée, sans la partie ${REMOTE} pour les branches distantes + ${BRANCH_FRIENDLY_NAME} Nom d'affichage de la branche sélectionnée, contient la partie ${REMOTE} pour les branches distantes + ${SHA} Hash du commit sélectionné + ${TAG} Tag sélectionné + ${FILE} Fichier sélectionné, relatif à la racine du dépôt + $1, $2 ... Paramètres d'entrée utilisateur Fichier exécutable : + Paramètres d'entrée utilisateur : + Modifier Nom : Portée : Branche Commit - Repository + Fichier + Remote + Dépôt + Tag Attendre la fin de l'action Adresse e-mail Adresse e-mail GIT Fetch les dépôts distants automatiquement minute(s) + Types de commit conventionnels Dépôt par défaut + Mode de fusion préféré SUIVI DES PROBLÈMES Ajouter une règle d'exemple Azure DevOps + Ajouter une règle de commit Gerrit Change-Id Ajouter une règle d'exemple Gitee Ajouter une règle d'exemple pour Pull Request Gitee Ajouter une règle d'exemple GitHub @@ -159,6 +240,7 @@ Nouvelle règle Issue Regex Expression: Nom de règle : + Partager cette règle dans le fichier .issuetracker URL résultant: Veuillez utiliser $1, $2 pour accéder aux valeurs des groupes regex. IA @@ -168,10 +250,27 @@ Proxy HTTP utilisé par ce dépôt Nom d'utilisateur Nom d'utilisateur pour ce dépôt + Modifier les Contrôles d'Action Personnalisée + Valeur Cochée : + Lorsque cochée, cette valeur sera utilisée dans les arguments de la ligne de commande + Description : + Défaut : + Est un dossier : + Libellé : + Options : + Utiliser '|' comme délimiteur pour les options + Les variables intégrées ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, et ${TAG} restent disponibles ici + Type : Espaces de travail Couleur Nom Restaurer les onglets au démarrage + CONTINUER + Commit vide détecté ! Voulez-vous continuer (--allow-empty) ? + TOUT INDEXER & COMMIT + Commit vide détecté ! Voulez-vous continuer (--allow-empty) ou tout indexer puis commit ? + Redémarrage Requis + Vous devez redémarrer cette application pour appliquer les changements. Assistant Commits Conventionnels Changement Radical : Incident Clos : @@ -188,11 +287,11 @@ Récupérer la branche créée Changements locaux : Rejeter - Stash & Réappliquer + 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 + Écraser la branche existante Créer un tag... Nouveau tag à : Signature GPG @@ -207,12 +306,18 @@ léger Maintenir Ctrl pour commencer directement Couper + Désinitialiser le sous-module + Forcer la désinitialisation même s'il contient des modifications locales. + Sous-module : Supprimer la branche Branche : Vous êtes sur le point de supprimer une branche distante !!! Supprimer également la branche distante ${0}$ Supprimer plusieurs branches Vous essayez de supprimer plusieurs branches à la fois. Assurez-vous de revérifier avant de procéder ! + Supprimer plusieurs tags + Les supprimer des dépôts distants + Vous essayez de supprimer plusieurs tags à la fois. Assurez-vous de bien vérifier avant d'agir ! Supprimer Remote Remote : Chemin: @@ -230,36 +335,52 @@ Mode de fichier changé Première différence Ignorer les changements d'espaces + FUSIONNER + DIFFÉRENCE + CÔTE À CÔTE + BALAYER Dernière différence CHANGEMENT D'OBJET LFS + NOUVEAU Différence suivante PAS DE CHANGEMENT OU SEULEMENT EN FIN DE LIGNE + ANCIEN Différence précédente Enregistrer en tant que patch Afficher les caractères invisibles Diff côte-à-côte SOUS-MODULE + SUPPRIMÉ NOUVEAU 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 + Historique du Répertoire + A des Modifications Locales + Divergence avec le Dépôt Distant + Déjà à jour Rejeter les changements Tous les changements dans la copie de travail. Changements : Inclure les fichiers ignorés + Inclure les fichiers non suivis {0} changements seront rejetés Vous ne pouvez pas annuler cette action !!! + Supprimer le Commit + Commit : + Nouveau HEAD : Signet : Nouveau nom : Cible : Éditer le groupe sélectionné Éditer le dépôt sélectionné + Cible : + Ce dépôt Fetch Fetch toutes les branches distantes Outrepasser les vérifications de refs @@ -290,6 +411,8 @@ FLOW - Terminer Hotfix FLOW - Terminer Release Cible: + Pousser vers le(s) dépôt(s) distant(s) après avoir terminé + Squash lors de la fusion Hotfix: Hotfix Prefix: Initialiser Git-Flow @@ -320,6 +443,8 @@ Afficher seulement mes verrous Verrous LFS Déverouiller + Déverrouiller tous mes verrous + Êtes-vous sûr de vouloir déverrouiller tous vos fichiers verrouillés ? Forcer le déverouillage Elaguer Lancer `git lfs prune` pour supprimer les anciens fichier LFS du stockage local @@ -350,12 +475,14 @@ Aller à la page précédente Créer une nouvelle page Ouvrir le dialogue des préférences + Changer d'onglet actif DÉPÔT Commit les changements de l'index Commit et pousser les changements de l'index Ajouter tous les changements et commit Fetch, démarre directement Mode tableau de bord (Défaut) + Ouvrir la palette de commandes Recherche de commit Pull, démarre directement Push, démarre directement @@ -367,6 +494,7 @@ Fermer le panneau de recherche Trouver la prochaine correspondance Trouver la correspondance précédente + Ouvrir avec l'outil externe de diff/fusion Ouvrir le panneau de recherche Rejeter Indexer @@ -384,12 +512,18 @@ Rebase interactif Stash & réappliquer changements locaux Sur : + Glisser-déposer pour réorganiser les commits Branche cible : Copier le lien Ouvrir dans le navigateur + Commandes ERREUR NOTICE + Ouvrir des dépôts + Onglets + Espaces de travail Merger la branche + Personnaliser le message de fusion Dans : Option de merge: Source: @@ -397,13 +531,17 @@ Commit tous les changement Stratégie: Cibles: + Déplacer le sous-module + Déplacer vers : + Sous-module : 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 + Éditeur par défaut (Système) Ouvrir le dossier AppData Ouvrir dans l'outil de fusion - Ouvrir avec... Optionnel. Créer un nouvel onglet Bookmark @@ -423,7 +561,6 @@ il y a {0} mois il y a {0} ans Hier - Utiliser 'Maj+Entrée' pour insérer une nouvelle ligne. 'Entrée' est la touche pour valider Préférences IA Analyser Diff Prompt @@ -431,6 +568,7 @@ Générer le sujet de Prompt Modèle Nom + La valeur saisie est le nom pour charger la clé API depuis l'ENV Serveur Activer le streaming APPARENCE @@ -440,9 +578,9 @@ Défaut Éditeur Police monospace - N'utiliser que des polices monospace pour l'éditeur de texte Thème Dérogations de thème + Utiliser les barres de défilement masquées automatiquement Utiliser des onglets de taille fixe dans la barre de titre Utiliser un cadre de fenêtre natif OUTIL DIFF/MERGE @@ -452,21 +590,27 @@ GÉNÉRAL Vérifier les mises à jour au démarrage Format de date + Activer les dossiers compacts dans l'arborescence des changements Language Historique de commits Afficher l'heure de l'auteur au lieu de l'heure de validation dans le graphique + Afficher la page 'CHANGEMENTS LOCAUX' par défaut + Afficher l'onglet 'CHANGEMENTS' dans les détails du commit par défaut Afficher les enfants dans les détails du commit Afficher les tags dans le graphique des commits Guide de longueur du sujet + Générer un avatar par défaut de style GitHub GIT Activer auto CRLF Répertoire de clônage par défaut E-mail utilsateur E-mail utilsateur global Activer --prune pour fetch + Activer --ignore-cr-at-eol dans la diff Cette application requière Git (>= 2.25.1) Chemin d'installation Activer la vérification HTTP SSL + Utiliser git-credential-libsecret au lieu de git-credential-manager Nom d'utilisateur Nom d'utilisateur global Version de Git @@ -491,7 +635,7 @@ Dans : Changements locaux : Rejeter - Stash & Réappliquer + Stash & Réappliquer Dépôt distant : Pull (Fetch & Merge) Utiliser rebase au lieu de merge @@ -499,7 +643,10 @@ Assurez-vous que les submodules ont été poussés Poussage forcé Branche locale : + NOUVEAU Dépôt distant : + Révision : + Pousser la Révision vers le Dépôt Distant Pousser les changements vers le dépôt distant Branche distante : Définir comme branche de suivi @@ -508,6 +655,8 @@ Pousser tous les dépôts distants Dépôt distant : Tag : + Pousser vers une NOUVELLE branche + Saisir le nom de la nouvelle branche distante : Quitter Rebase la branche actuelle Stash & réappliquer changements locaux @@ -519,6 +668,7 @@ URL du repository : URL du dépôt distant Copier l'URL + Action personnalisée Supprimer... Editer... Fetch @@ -527,19 +677,24 @@ Confirmer la suppression du Worktree Activer l'option `--force` Cible : - la branche + Renommer la branche Nouveau nom : Nom unique pour cette branche Branche : ABORT Fetch automatique des changements depuis les dépôts... + Trier + Par date du Committer + Par Nom Nettoyage(GC & Elaguage) Lancer `git gc` pour ce repository. Tout effacer + Nettoyer Configurer ce repository CONTINUER Actions personnalisées Pas d'actions personnalisées + Tableau de bord Rejeter tous les changements Ouvrir dans l'explorateur de fichiers Rechercher Branches/Tags/Submodules @@ -554,9 +709,11 @@ Date du commit Topologiquement BRANCHES LOCALES + Plus d'options... Naviguer vers le HEAD Créer une branche EFFACER LES NOTIFICATIONS + Surligner uniquement la branche actuelle Ouvrir dans {0} Ouvrir dans un outil externe DEPOTS DISTANTS @@ -564,9 +721,16 @@ Rechercher un commit Auteur Committer + Contenu Message + Chemin SHA Branche actuelle + Commits décorés uniquement + Premier parent uniquement + AFFICHER LES FLAGS + Afficher les commits perdus + Afficher les sous-modules en tant qu'arbre Voir les Tags en tant qu'arbre PASSER Statistiques @@ -579,6 +743,9 @@ Par nom Trier Ouvrir dans un terminal + Utiliser l'heure relative + Voir les Logs + Visiter '{0}' dans le navigateur WORKTREES AJOUTER WORKTREE ELAGUER @@ -587,6 +754,9 @@ Reset Mode: Déplacer vers : Branche actuelle : + Réinitialiser la Branche (Sans Récupération) + Déplacer Vers : + Branche : Ouvrir dans l'explorateur de fichier Annuler le Commit Commit : @@ -598,6 +768,7 @@ Le patch a été sauvegardé ! Analyser les repositories Dossier racine : + Scanner un autre répertoire personnalisé Rechercher des mises à jour... Une nouvelle version du logiciel est disponible : La vérification de mise à jour à échouée ! @@ -605,14 +776,17 @@ Passer cette version Mise à jour du logiciel Il n'y a pas de mise à jour pour le moment. + Définir la Branche du Sous-module + Sous-module : + Actuel : + Changer pour : + Optionnel. Défini par défaut si vide. 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 @@ -620,10 +794,12 @@ Inclure les fichiers non-suivis Message : Optionnel. Information de ce stash + Mode : 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 + Copier le Message Effacer Sauver comme Patch... Effacer le Stash @@ -639,14 +815,36 @@ COMMITS: SOUS-MODULES Ajouter un sous-module + BRANCHE + Branche Chemin relatif + Désinitialiser Fetch les sous-modules imbriqués + Historique + Déplacer Vers Ouvrir le dépôt de sous-module + Chemin relatif : + Dossier relatif pour stocker ce module. Supprimer le sous-module + Définir la Branche + Changer l'URL + STATUT + modifié + non initialisé + révision changée + non fusionné + Mettre à jour + URL OK - Copier le nom du Tag - Copier le message du tag + TAGUEUR + HEURE + Message + Nom + Tagueur + Copier le nom du tag + Action personnalisée Supprimer ${0}$... + Supprimer les {0} tags sélectionnés... Fusionner ${0}$ dans ${1}$... Pousser ${0}$... Actualiser les sous-modules @@ -654,7 +852,12 @@ Initialiser au besoin Récursivement Sous-module : + Mettre à jour vers la branche de suivi distante du sous-module URL : + Logs + TOUT EFFACER + Copier + Supprimer Avertissement Page d'accueil Créer un groupe @@ -673,21 +876,32 @@ Git Ignore Ignorer tous les *{0} fichiers Ignorer *{0} fichiers dans le même dossier + Ignorer les fichiers non suivis dans ce dossier N'ignorer que ce fichier Amender Vous pouvez indexer ce fichier. + Effacer l'historique + Êtes-vous sûr de vouloir effacer tout l'historique des messages de commit ? Cette action est irréversible. COMMIT COMMIT & POUSSER Modèles/Historiques Trigger click event Commit (Modifier) Indexer tous les changements et commit + Vous êtes en train de créer un commit sur un HEAD détaché. Voulez-vous continuer ? + Vous avez indexé {0} fichier(s) mais seulement {1} fichier(s) sont affichés ({2} fichiers sont filtrés). Voulez-vous continuer ? CONFLITS DÉTECTÉS + OUVRIR L'OUTIL DE FUSION EXTERNE + OUVRIR TOUS LES CONFLITS DANS L'OUTIL DE FUSION EXTERNE LES CONFLITS DE FICHIER SONT RÉSOLUS + UTILISER LES MIENS + UTILISER LES LEURS INCLURE LES FICHIERS NON-SUIVIS PAS DE MESSAGE D'ENTRÉE RÉCENT PAS DE MODÈLES DE COMMIT - SignOff + No-Verify + Réinitialiser l'Auteur + Signer INDEXÉ RETIRER DE L'INDEX RETIRER TOUT DE L'INDEX @@ -701,6 +915,7 @@ WORKTREE Copier le chemin Verrouiller + Ouvrir Supprimer Déverrouiller diff --git a/src/Resources/Locales/id_ID.axaml b/src/Resources/Locales/id_ID.axaml new file mode 100644 index 000000000..77eb5cc57 --- /dev/null +++ b/src/Resources/Locales/id_ID.axaml @@ -0,0 +1,894 @@ + + + + + + Tentang + Tentang SourceGit + Klien Git GUI Opensource & Gratis + Tambahkan Berkas ke Abaikan + Pola: + Berkas Penyimpanan: + Tambah Worktree + Lokasi: + Jalur untuk worktree ini. Jalur relatif didukung. + Nama Branch: + Opsional. Standar adalah nama folder tujuan. + Track Branch: + Track remote branch + Yang Akan Di-Checkout: + Buat Branch Baru + Branch Yang Ada + Asisten AI + BUAT ULANG + Gunakan AI untuk membuat pesan commit + TERAPKAN SEBAGAI PESAN COMMIT + Sembunyikan SourceGit + Tampilkan Semua + Patch + Berkas Patch: + Pilih berkas .patch untuk diterapkan + Abaikan perubahan whitespace + Terapkan Patch + Whitespace: + Terapkan Stash + Hapus setelah diterapkan + Pulihkan perubahan indeks + Stash: + Arsip... + Simpan Arsip Ke: + Pilih jalur berkas arsip + Revisi: + Arsip + SourceGit Askpass + Masukkan passphrase: + BERKAS DIASUMSIKAN TIDAK BERUBAH + TIDAK ADA BERKAS YANG DIASUMSIKAN TIDAK BERUBAH + Muat Gambar... + Segarkan + BERKAS BINARY TIDAK DIDUKUNG!!! + Bisect + Batalkan + Buruk + Bisect berjalan. Apakah HEAD saat ini baik atau buruk? + Baik + Lewati + Bisect berjalan. Tandai commit saat ini sebagai baik atau buruk dan checkout yang lain. + Blame + BLAME PADA BERKAS INI TIDAK DIDUKUNG!!! + Checkout ${0}$... + Bandingkan dengan ${0}$ + Bandingkan dengan Worktree + Salin Nama Branch + Aksi Kustom + Hapus ${0}$... + Hapus {0} branch yang dipilih + Fast-Forward ke ${0}$ + Fetch ${0}$ ke ${1}$... + Git Flow - Selesaikan ${0}$ + Merge ${0}$ ke ${1}$... + Merge {0} branch yang dipilih ke saat ini + Pull ${0}$ + Pull ${0}$ ke ${1}$... + Push ${0}$ + Rebase ${0}$ pada ${1}$... + Ganti Nama ${0}$... + Reset ${0}$ ke ${1}$... + Pindah ke ${0}$ (worktree) + Atur Tracking Branch... + Perbandingan Branch + {0} commit di depan + {0} commit di depan, {1} commit di belakang + {0} commit di belakang + Tidak Valid + REMOTE + STATUS + TRACKING + URL + WORKTREE + BATAL + Reset ke Revisi Parent + Reset ke Revisi Ini + Buat pesan commit + UBAH MODE TAMPILAN + Tampilkan sebagai Daftar Berkas dan Direktori + Tampilkan sebagai Daftar Jalur + Tampilkan sebagai Pohon Sistem Berkas + Ubah URL Submodule + Submodule: + URL: + Checkout Branch + Checkout Commit + Commit: + Peringatan: Dengan melakukan checkout commit, Head akan terlepas + Perubahan Lokal: + Buang + Stash & Terapkan Ulang + Branch: + HEAD saat ini mengandung commit yang tidak terhubung ke branch/tag manapun! Lanjutkan? + Checkout & Fast-Forward + Fast-Forward ke: + Cherry Pick + Tambahkan sumber ke pesan commit + Commit: + Commit semua perubahan + Mainline: + Biasanya Anda tidak dapat cherry-pick merge karena tidak tahu sisi mana dari merge yang dianggap mainline. Opsi ini memungkinkan cherry-pick untuk memutar ulang perubahan relatif terhadap parent yang ditentukan. + Hapus Stash + Anda akan menghapus semua stash. Lanjutkan? + Clone Repositori Remote + Parameter Tambahan: + Argumen tambahan untuk clone repositori. Opsional. + Nama Lokal: + Nama repositori. Opsional. + Folder Parent: + Inisialisasi & perbarui submodule + URL Repositori: + TUTUP + Editor + Checkout Commit + Cherry-Pick Commit + Cherry-Pick ... + Bandingkan dengan HEAD + Bandingkan dengan Worktree + Author + Pesan + Committer + SHA + Subjek + Aksi Kustom + Interactive Rebase + Drop... + Edit... + Fixup ke Parent... + Rebase ${0}$ pada ${1}$ secara Interaktif + Reword... + Squash ke Parent... + Merge ke ${0}$ + Merge ... + Push ${0}$ ke ${1}$ + Rebase ${0}$ pada ${1}$ + Reset ${0}$ ke ${1}$ + Revert Commit + Reword + Simpan sebagai Patch... + Squash ke Parent + Fixup ke Parent + PERUBAHAN + berkas berubah + Cari Perubahan... + BERKAS + Berkas LFS + Cari Berkas... + Submodule + INFORMASI + AUTHOR + CHILDREN + COMMITTER + Periksa ref yang mengandung commit ini + COMMIT TERKANDUNG DALAM + Salin Email + Salin Nama + Salin Nama & Email + Menampilkan hanya 100 perubahan pertama. Lihat semua perubahan di tab PERUBAHAN. + Kunci: + PESAN + PARENTS + REFS + SHA + Penanda Tangan: + Buka di Browser + SUBJEK + Konfigurasi Repositori + TEMPLATE COMMIT + Konten Template: + Nama Template: + AKSI KUSTOM + Argumen: + Parameter bawaan: + + ${REPO} Jalur repositori + ${REMOTE} Remote yang dipilih atau remote dari branch yang dipilih + ${BRANCH} Branch yang dipilih, tanpa bagian ${REMOTE} untuk remote branch + ${BRANCH_FRIENDLY_NAME} Nama ramah dari branch yang dipilih, mengandung bagian ${REMOTE} untuk remote branch + ${SHA} Hash commit yang dipilih + ${TAG} Tag yang dipilih + ${FILE} Berkas yang dipilih, relatif terhadap akar repositori + $1, $2 ... Nilai kontrol input + Berkas Eksekusi: + Kontrol Input: + Sunting + Nama: + Lingkup: + Branch + Commit + Berkas + Remote + Repositori + Tag + Tunggu aksi selesai + Alamat Email + Alamat email + GIT + Fetch remote secara otomatis + Menit + Remote Default + Mode Merge Pilihan + ISSUE TRACKER + Tambah Aturan Azure DevOps + Tambah Aturan Gerrit Change-Id Commit + Tambah Aturan Gitee Issue + Tambah Aturan Gitee Pull Request + Tambah Aturan GitHub + Tambah Aturan GitLab Issue + Tambah Aturan GitLab Merge Request + Tambah Aturan Jira + Aturan Baru + Ekspresi Regex Issue: + Nama Aturan: + Bagikan aturan ini di berkas .issuetracker + URL Hasil: + Gunakan $1, $2 untuk mengakses nilai grup regex. + AI + Layanan Pilihan: + Jika 'Layanan Pilihan' diatur, SourceGit hanya akan menggunakannya di repositori ini. Jika tidak, jika ada lebih dari satu layanan yang tersedia, menu konteks untuk memilih salah satunya akan ditampilkan. + Proksi HTTP + Proksi HTTP yang digunakan oleh repositori ini + Nama Pengguna + Nama pengguna untuk repositori ini + Sunting Kontrol Aksi Kustom + Nilai Tercentang: + Saat dicentang, nilai ini akan digunakan dalam argumen command-line + Deskripsi: + Default: + Adalah Folder: + Label: + Opsi: + Gunakan '|' sebagai pembatas untuk opsi + Jenis: + Workspace + Warna + Nama + Pulihkan tab saat startup + LANJUTKAN + Commit kosong terdeteksi! Lanjutkan (--allow-empty)? + STAGE SEMUA & COMMIT + Commit kosong terdeteksi! Lanjutkan (--allow-empty) atau stage semua lalu commit? + Perlu Mulai Ulang + Anda perlu memulai ulang aplikasi ini untuk menerapkan perubahan. + Pembantu Conventional Commit + Breaking Change: + Issue Ditutup: + Detail Perubahan: + Lingkup: + Deskripsi Singkat: + Jenis Perubahan: + Salin + Salin Semua Teks + Salin Jalur Lengkap + Salin Jalur + Buat Branch... + Berdasarkan: + Checkout branch yang dibuat + Perubahan Lokal: + Buang + Stash & Terapkan Ulang + Nama Branch Baru: + Masukkan nama branch. + Buat Branch Lokal + Timpa branch yang ada + Buat Tag... + Tag Baru Pada: + Tanda tangan GPG + Pesan Tag: + Opsional. + Nama Tag: + Format rekomendasi: v1.0.0-alpha + Push ke semua remote setelah dibuat + Buat Tag Baru + Jenis: + annotated + lightweight + Tahan Ctrl untuk memulai langsung + Potong + De-initialize Submodule + Paksa de-init meski mengandung perubahan lokal. + Submodule: + Hapus Branch + Branch: + Anda akan menghapus remote branch!!! + Juga hapus remote branch ${0}$ + Hapus Beberapa Branch + Anda akan menghapus beberapa branch sekaligus. Pastikan untuk memeriksa ulang sebelum bertindak! + Hapus Beberapa Tag + Hapus dari remote + Anda akan menghapus beberapa tag sekaligus. Pastikan untuk memeriksa ulang sebelum bertindak! + Hapus Remote + Remote: + Jalur: + Target: + Semua anak akan dihapus dari daftar. + Ini hanya akan menghapusnya dari daftar, bukan dari disk! + Konfirmasi Hapus Grup + Konfirmasi Hapus Repositori + Hapus Submodule + Jalur Submodule: + Hapus Tag + Tag: + Hapus dari repositori remote + DIFF BINARY + Mode Berkas Berubah + Perbedaan Pertama + Abaikan Perubahan Whitespace + BLEND + DIFFERENCE + SIDE-BY-SIDE + SWIPE + Perbedaan Terakhir + PERUBAHAN OBJEK LFS + BARU + Perbedaan Berikutnya + TIDAK ADA PERUBAHAN ATAU HANYA PERUBAHAN EOL + LAMA + Perbedaan Sebelumnya + Simpan sebagai Patch + Tampilkan Simbol Tersembunyi + Diff Side-By-Side + PERUBAHAN SUBMODULE + DIHAPUS + BARU + Tukar + Syntax Highlighting + Word Wrap Baris + Buka di Merge Tool + Tampilkan Semua Baris + Kurangi Jumlah Baris yang Tampak + Tambah Jumlah Baris yang Tampak + PILIH BERKAS UNTUK MELIHAT PERUBAHAN + Riwayat Direktori + Memiliki Perubahan Lokal + Tidak Cocok dengan Upstream + Sudah Up-To-Date + Buang Perubahan + Semua perubahan lokal dalam working copy. + Perubahan: + Termasuk berkas yang diabaikan + Termasuk berkas yang tidak dilacak + {0} perubahan akan dibuang + Anda tidak dapat membatalkan aksi ini!!! + Bookmark: + Nama Baru: + Target: + Sunting Grup yang Dipilih + Sunting Repositori yang Dipilih + Target: + Repositori ini + Fetch + Fetch semua remote + Paksa override ref lokal + Fetch tanpa tag + Remote: + Fetch Perubahan dari Remote + Asumsikan tidak berubah + Buang... + Buang {0} berkas... + Selesaikan Menggunakan ${0}$ + Simpan sebagai Patch... + Stage + Stage {0} berkas + Stash... + Stash {0} berkas... + Unstage + Unstage {0} berkas + Gunakan Milik Saya (checkout --ours) + Gunakan Milik Mereka (checkout --theirs) + Riwayat Berkas + PERUBAHAN + KONTEN + Git-Flow + Branch Development: + Feature: + Prefix Feature: + FLOW - Selesaikan Feature + FLOW - Selesaikan Hotfix + FLOW - Selesaikan Release + Target: + Push ke remote setelah selesai + Squash saat merge + Hotfix: + Prefix Hotfix: + Inisialisasi Git-Flow + Simpan branch + Branch Production: + Release: + Prefix Release: + Mulai Feature... + FLOW - Mulai Feature + Mulai Hotfix... + FLOW - Mulai Hotfix + Masukkan nama + Mulai Release... + FLOW - Mulai Release + Prefix Tag Versi: + Git LFS + Tambah Pola Track... + Pola adalah nama berkas + Pola Kustom: + Tambah Pola Track ke Git LFS + Fetch + Jalankan `git lfs fetch` untuk mengunduh objek Git LFS. Ini tidak memperbarui working copy. + Fetch Objek LFS + Instal hook Git LFS + Tampilkan Lock + Tidak Ada Berkas Terkunci + Lock + Hanya tampilkan lock saya + Lock LFS + Unlock + Paksa Unlock + Prune + Jalankan `git lfs prune` untuk menghapus berkas LFS lama dari penyimpanan lokal + Pull + Jalankan `git lfs pull` untuk mengunduh semua berkas Git LFS untuk ref & checkout saat ini + Pull Objek LFS + Push + Push berkas besar yang diantre ke endpoint Git LFS + Push Objek LFS + Remote: + Track berkas bernama '{0}' + Track semua berkas *{0} + RIWAYAT + AUTHOR + WAKTU AUTHOR + GRAFIK & SUBJEK + SHA + WAKTU COMMIT + DIPILIH {0} COMMIT + Tahan 'Ctrl' atau 'Shift' untuk memilih beberapa commit. + Tahan ⌘ atau ⇧ untuk memilih beberapa commit. + TIPS: + Referensi Shortcut Keyboard + GLOBAL + Clone repositori baru + Tutup tab saat ini + Ke tab berikutnya + Ke tab sebelumnya + Buat tab baru + Buka dialog Preferensi + Ganti tab aktif + REPOSITORI + Commit perubahan yang di-stage + Commit dan push perubahan yang di-stage + Stage semua perubahan dan commit + Fetch, langsung dimulai + Mode dashboard (Default) + Mode pencarian commit + Pull, langsung dimulai + Push, langsung dimulai + Paksa muat ulang repositori ini + Pindah ke 'Changes' + Pindah ke 'History' + Pindah ke 'Stashes' + TEXT EDITOR + Tutup panel pencarian + Cari kecocokan berikutnya + Cari kecocokan sebelumnya + Buka dengan diff/merge tool eksternal + Buka panel pencarian + Buang + Stage + Unstage + Inisialisasi Repositori + Jalur: + Cherry-Pick sedang berjalan. + Memproses commit + Merge sedang berjalan. + Melakukan merge + Rebase sedang berjalan. + Berhenti di + Revert sedang berjalan. + Melakukan revert commit + Interactive Rebase + Stash & terapkan ulang perubahan lokal + Pada: + Drag-drop untuk mengurutkan ulang commit + Branch Target: + Salin Link + Buka di Browser + ERROR + PEMBERITAHUAN + Tab + Workspace + Merge Branch + Sesuaikan pesan merge + Ke: + Opsi Merge: + Sumber: + Merge (Beberapa) + Commit semua perubahan + Strategi: + Target: + Pindahkan Submodule + Pindahkan Ke: + Submodule: + Pindahkan Node Repositori + Pilih node parent untuk: + Nama: + Git BELUM dikonfigurasi. Silakan ke [Preferences] dan konfigurasikan terlebih dahulu. + Buka Direktori Penyimpanan Data + Buka di Merge Tool + Opsional. + Buat Tab Baru + Bookmark + Tutup Tab + Tutup Tab Lain + Tutup Tab di Kanan + Salin Jalur Repositori + Repositori + Tempel + {0} hari lalu + 1 jam lalu + {0} jam lalu + Baru saja + Bulan lalu + Tahun lalu + {0} menit lalu + {0} bulan lalu + {0} tahun lalu + Kemarin + Preferensi + AI + Prompt Analisis Diff + API Key + Prompt Generate Subjek + Model + Nama + Nilai yang dimasukkan adalah nama untuk memuat API key dari ENV + Server + Aktifkan Streaming + TAMPILAN + Font Default + Lebar Tab Editor + Ukuran Font + Default + Editor + Font Monospace + Tema + Override Tema + Gunakan scrollbar auto-hide + Gunakan lebar tab tetap di titlebar + Gunakan frame window native + DIFF/MERGE TOOL + Jalur Instalasi + Masukkan jalur untuk diff/merge tool + Tool + UMUM + Periksa pembaruan saat startup + Format Tanggal + Aktifkan folder kompak di pohon perubahan + Bahasa + Commit Riwayat + Tampilkan waktu author alih-alih waktu commit di grafik + Tampilkan halaman `LOCAL CHANGES` secara default + Tampilkan tab `CHANGES` di detail commit secara default + Tampilkan children di detail commit + Tampilkan tag di grafik commit + Panjang Panduan Subjek + Generate avatar default bergaya GitHub + GIT + Aktifkan Auto CRLF + Direktori Clone Default + Email Pengguna + Email pengguna git global + Aktifkan --prune saat fetch + Aktifkan --ignore-cr-at-eol di diff + Git (>= 2.25.1) diperlukan oleh aplikasi ini + Jalur Instalasi + Aktifkan HTTP SSL Verify + Gunakan git-credential-libsecret alih-alih git-credential-manager + Nama Pengguna + Nama pengguna git global + Versi Git + PENANDATANGANAN GPG + Penandatanganan GPG commit + Format GPG + Jalur Instalasi Program + Masukkan jalur untuk program gpg yang terinstal + Penandatanganan GPG tag + Kunci Penandatanganan Pengguna + Kunci penandatanganan gpg pengguna + INTEGRASI + SHELL/TERMINAL + Jalur + Shell/Terminal + Prune Remote + Target: + Prune Worktree + Prune informasi worktree di `$GIT_COMMON_DIR/worktrees` + Pull + Remote Branch: + Ke: + Perubahan Lokal: + Buang + Stash & Terapkan Ulang + Remote: + Pull (Fetch & Merge) + Gunakan rebase alih-alih merge + Push + Pastikan submodule sudah di-push + Paksa push + Branch Lokal: + BARU + Remote: + Revisi: + Push Revisi ke Remote + Push Perubahan ke Remote + Remote Branch: + Atur sebagai tracking branch + Push semua tag + Push Tag ke Remote + Push ke semua remote + Remote: + Tag: + Keluar + Rebase Branch Saat Ini + Stash & terapkan ulang perubahan lokal + Pada: + Tambah Remote + Sunting Remote + Nama: + Nama remote + URL Repositori: + URL repositori git remote + Salin URL + Aksi Kustom + Hapus... + Sunting... + Fetch + Buka di Browser + Prune + Konfirmasi Hapus Worktree + Aktifkan Opsi `--force` + Target: + Ganti Nama Branch + Nama Baru: + Nama unik untuk branch ini + Branch: + BATALKAN + Auto fetch perubahan dari remote... + Urut + Berdasarkan Tanggal Committer + Berdasarkan Nama + Bersihkan (GC & Prune) + Jalankan perintah `git gc` untuk repositori ini. + Bersihkan semua + Bersihkan + Konfigurasikan repositori ini + LANJUTKAN + Aksi Kustom + Tidak Ada Aksi Kustom + Dashboard + Buang semua perubahan + Buka di File Browser + Cari Branch/Tag/Submodule + Visibilitas di Grafik + Tidak Diatur + Sembunyikan di grafik commit + Filter di grafik commit + TATA LETAK + Horizontal + Vertikal + URUTAN COMMIT + Tanggal Commit + Topologis + BRANCH LOKAL + Opsi lainnya... + Navigasi ke HEAD + Buat Branch + BERSIHKAN NOTIFIKASI + Hanya highlight branch saat ini + Buka di {0} + Buka di Tool Eksternal + REMOTE + Tambah Remote + Cari Commit + Author + Committer + Konten + Pesan + Jalur + SHA + Branch Saat Ini + Hanya commit yang didekorasi + Hanya first-parent + TAMPILKAN FLAG + Tampilkan commit yang hilang + Tampilkan Submodule sebagai Pohon + Tampilkan Tag sebagai Pohon + LEWATI + Statistik + SUBMODULE + Tambah Submodule + Perbarui Submodule + TAG + Tag Baru + Berdasarkan Tanggal Pembuat + Berdasarkan Nama + Urut + Buka di Terminal + Gunakan waktu relatif + Lihat Log + Kunjungi '{0}' di Browser + WORKTREE + Tambah Worktree + Prune + URL Repositori Git + Reset Branch Saat Ini ke Revisi + Mode Reset: + Pindah Ke: + Branch Saat Ini: + Reset Branch (Tanpa Checkout) + Pindah Ke: + Branch: + Tampilkan di File Explorer + Revert Commit + Commit: + Commit perubahan revert + Reword Pesan Commit + Sedang berjalan. Harap tunggu... + SIMPAN + Simpan Sebagai... + Patch berhasil disimpan! + Pindai Repositori + Direktori Root: + Pindai direktori kustom lain + Periksa Pembaruan... + Versi baru dari perangkat lunak ini tersedia: + Pemeriksaan pembaruan gagal! + Unduh + Lewati Versi Ini + Pembaruan Perangkat Lunak + Saat ini tidak ada pembaruan yang tersedia. + Atur Branch Submodule + Submodule: + Saat Ini: + Ubah Ke: + Opsional. Atur ke default jika kosong. + Atur Tracking Branch + Branch: + Hapus upstream + Upstream: + Salin SHA + Ke + Kunci SSH Privat: + Jalur penyimpanan kunci SSH privat + MULAI + Stash + Termasuk berkas yang tidak dilacak + Pesan: + Opsional. Pesan untuk stash ini + Mode: + Hanya perubahan yang di-stage + Perubahan staged dan unstaged dari berkas yang dipilih akan di-stash!!! + Stash Perubahan Lokal + Terapkan + Salin Pesan + Drop + Simpan sebagai Patch... + Drop Stash + Drop: + STASH + PERUBAHAN + STASH + Statistik + IKHTISAR + BULAN + MINGGU + AUTHOR: + COMMIT: + SUBMODULE + Tambah Submodule + BRANCH + Branch + Jalur Relatif + De-initialize + Fetch submodule bersarang + Riwayat + Pindahkan Ke + Buka Repositori + Jalur Relatif: + Folder relatif untuk menyimpan modul ini. + Hapus + Atur Branch + Ubah URL + STATUS + dimodifikasi + tidak diinisialisasi + revisi berubah + belum di-merge + Perbarui + URL + OK + TAGGER + WAKTU + Pesan + Nama + Tagger + Salin Nama Tag + Aksi Kustom + Hapus ${0}$... + Hapus {0} tag yang dipilih... + Merge ${0}$ ke ${1}$... + Push ${0}$... + Perbarui Submodule + Semua submodule + Inisialisasi sesuai kebutuhan + Telusuri submodule secara rekursif + Submodule: + Perbarui ke remote tracking branch submodule + URL: + Log + BERSIHKAN SEMUA + Salin + Hapus + Peringatan + Halaman Selamat Datang + Buat Grup + Buat Sub-Grup + Clone Repositori + Hapus + DRAG & DROP FOLDER DIDUKUNG. PENGELOMPOKAN KUSTOM DIDUKUNG. + Sunting + Pindah ke Grup Lain + Buka Semua Repositori + Buka Repositori + Buka Terminal + Pindai Ulang Repositori di Direktori Clone Default + Cari Repositori... + PERUBAHAN LOKAL + Git Ignore + Abaikan semua berkas *{0} + Abaikan berkas *{0} di folder yang sama + Abaikan berkas yang tidak dilacak di folder ini + Abaikan hanya berkas ini + Amend + Anda dapat stage berkas ini sekarang. + Bersihkan Riwayat + Yakin ingin membersihkan semua riwayat pesan commit? Aksi ini tidak dapat dibatalkan. + COMMIT + COMMIT & PUSH + Template/Riwayat + Picu event klik + Commit (Sunting) + Stage semua perubahan dan commit + Anda membuat commit pada HEAD yang terlepas. Lanjutkan? + Anda telah stage {0} berkas tetapi hanya {1} berkas yang ditampilkan ({2} berkas disaring). Lanjutkan? + KONFLIK TERDETEKSI + BUKA MERGETOOL EKSTERNAL + BUKA SEMUA KONFLIK DI MERGETOOL EKSTERNAL + KONFLIK BERKAS DISELESAIKAN + GUNAKAN MILIK SAYA + GUNAKAN MILIK MEREKA + TERMASUK BERKAS YANG TIDAK DILACAK + TIDAK ADA PESAN INPUT TERBARU + TIDAK ADA TEMPLATE COMMIT + No-Verify + Reset Author + SignOff + STAGED + UNSTAGE + UNSTAGE SEMUA + UNSTAGED + STAGE + STAGE SEMUA + LIHAT ASSUME UNCHANGED + Template: ${0}$ + WORKSPACE: + Konfigurasikan Workspace... + WORKTREE + Salin Jalur + Lock + Buka + Hapus + Unlock + diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index c83a8bdb9..c56954628 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -6,6 +6,9 @@ Informazioni Informazioni su SourceGit Client GUI Git open source e gratuito + Aggiungi file a Ignora + Pattern: + File di storage: Aggiungi Worktree Posizione: Percorso per questo worktree. Supportato il percorso relativo. @@ -20,6 +23,8 @@ RIGENERA Usa AI per generare il messaggio di commit APPLICA COME MESSAGGIO DI COMMIT + Nascondi SourceGit + Mostra Tutto Applica File Patch: Seleziona file .patch da applicare @@ -36,8 +41,10 @@ Revisione: Archivia Richiedi Password SourceGit + Inserisci passphrase: FILE ASSUNTI COME INVARIATI NESSUN FILE ASSUNTO COME INVARIATO + Carico l'Immagine... Aggiorna FILE BINARIO NON SUPPORTATO!!! Biseca @@ -48,7 +55,7 @@ Salta Bisecando. Marca il commit corrente come buono o cattivo e fai checkout di un altro. Attribuisci - L'ATTRIBUZIONE SU QUESTO FILE NON È SUPPORTATA!!! + L'ATTRIBUZIONE SU QUESTO FILE NON È SUPPORTATA!!! Checkout ${0}$... Confronta con ${0}$ Confronta con Worktree @@ -66,9 +73,13 @@ Invia ${0}$ Riallinea ${0}$ su ${1}$... Rinomina ${0}$... + Resetta ${0}$ a ${1}$... Imposta Branch di Tracciamento... Confronto Branch - Upstream non valido + Invalido + REMOTO + TRACCIAMENTO + URL ANNULLA Ripristina la Revisione Padre Ripristina Questa Revisione @@ -77,15 +88,20 @@ Mostra come elenco di file e cartelle Mostra come elenco di percorsi Mostra come albero del filesystem + Cambia l'URL del Sottomodulo + Sottomodulo: + URL: 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 + Stasha e Ripristina Branch: + Il tuo HEAD attuale contiene commit non connessi ad alcun branch/tag! Sicuro di voler continuare? + Checkout & Avanzamento Veloce + Avanzamento Veloce verso: Cherry Pick Aggiungi sorgente al messaggio di commit Commit(s): @@ -110,17 +126,30 @@ Confronta con HEAD Confronta con Worktree Autore + Messaggio Committer SHA Oggetto Azione Personalizzata + Rebase Interattivo + Scarta... + Modifica... + Correggi nel Genitore... + Ribasare interattivamente ${0}$ su ${1}$ + Riformula... + Compatta nel Genitore... Unisci a ${0}$ Unisci ... + Invia ${0}$ a ${1}$ + Ribasa ${0}$ su ${1}$ + Resetta ${0}$ su ${1}$ Annulla Commit Modifica Salva come Patch... Compatta nel Genitore + Correggi nel Genitore MODIFICHE + file modificati Cerca Modifiche... FILE File LFS @@ -133,26 +162,41 @@ Controlla i riferimenti che contengono questo commit IL COMMIT È CONTENUTO DA Mostra solo le prime 100 modifiche. Vedi tutte le modifiche nella scheda MODIFICHE. + Chiave: MESSAGGIO GENITORI RIFERIMENTI SHA + Firmatario: Apri nel Browser - Descrizione OGGETTO - Inserisci l'oggetto del commit Configura Repository TEMPLATE DI COMMIT Contenuto Template: Nome Template: AZIONE PERSONALIZZATA Argomenti: + Parametri integrati: + + ${REPO} Percorso del repository + ${REMOTE} Remoto selezionato o remoto del branch selezionato + ${BRANCH} Branch selezionato, senza la parte ${REMOTE} per i branch remoti + ${BRANCH_FRIENDLY_NAME} Nome amichevole del branch selezionato, contiene la parte ${REMOTE} per i branch remoti + ${SHA} Hash del commit selezionato + ${TAG} Tag selezionato + ${FILE} File selezionato, relativo alla radice del repository + $1, $2 ... Valori dei controlli di input File Eseguibile: + Controlli di Input: + Modifica Nome: Ambito: Branch Commit + File + Remoto Repository + Tag Attendi la fine dell'azione Indirizzo Email Indirizzo email @@ -163,6 +207,7 @@ Modalità di Merge Preferita TRACCIAMENTO ISSUE Aggiungi una regola di esempio per Azure DevOps + Aggiungi regola per Gerrit Change-Id Commit 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 @@ -172,6 +217,7 @@ Nuova Regola Espressione Regex Issue: Nome Regola: + Condividi questa regola nel file .issuetracker URL Risultato: Utilizza $1, $2 per accedere ai valori dei gruppi regex. AI @@ -181,6 +227,16 @@ Proxy HTTP usato da questo repository Nome Utente Nome utente per questo repository + Modifica Controlli Azione Personalizzata + Valore Selezionato: + Quando selezionato, questo valore sarà usato negli argomenti della riga di comando + Descrizione: + Predefinito: + È una Cartella: + Etichetta: + Opzioni: + Usa '|' come delimitatore per le opzioni + Tipo: Spazi di Lavoro Colore Nome @@ -189,6 +245,8 @@ 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? + Riavvio Necessario + È necessario riavviare l'applicazione per applicare le modifiche. Guida Commit Convenzionali Modifica Sostanziale: Issue Chiusa: @@ -205,11 +263,11 @@ Checkout del Branch Creato Modifiche Locali: Scarta - Stasha e Ripristina + Stasha e Ripristina Nome Nuovo Branch: Inserisci il nome del branch. - Gli spazi verranno rimpiazzati con dei trattini. Crea Branch Locale + Sovrascrivi branch esistente Crea Tag... Nuovo Tag Su: Firma con GPG @@ -224,12 +282,18 @@ leggero Tieni premuto Ctrl per avviare direttamente Taglia + Deinizializza Sottomodulo + Forza deinizializzazione anche se contiene modifiche locali. + Sottomodulo: 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 Tag Multipli + Eliminali dai remoti + Stai cercando di eliminare più tag contemporaneamente. Assicurati di controllare attentamente prima di procedere! Elimina Remoto Remoto: Percorso: @@ -247,29 +311,36 @@ Modalità File Modificata Prima differenza Ignora Modifiche agli Spazi + FUSIONE + AFFIANCATI + SCORRIMENTO Ultima differenza MODIFICA OGGETTO LFS + NUOVO Differenza Successiva NESSUNA MODIFICA O SOLO CAMBIAMENTI DI FINE LINEA + VECCHIO Differenza Precedente Salva come Patch Mostra Simboli Nascosti Diff Affiancato SOTTOMODULO + ELIMINATO 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 + Cronologia Cartella Scarta Modifiche Tutte le modifiche locali nella copia di lavoro. Modifiche: Includi file ignorati + Includi file non tracciati Un totale di {0} modifiche saranno scartate Questa azione non può essere annullata!!! Segnalibro: @@ -277,6 +348,8 @@ Destinazione: Modifica Gruppo Selezionato Modifica Repository Selezionato + Destinazione: + Questo repository Recupera Recupera da tutti i remoti Forza la sovrascrittura dei riferimenti locali @@ -369,6 +442,7 @@ Vai alla pagina precedente Crea una nuova pagina Apri la finestra delle preferenze + Cambia scheda attiva REPOSITORY Committa le modifiche in tsage Committa e invia le modifiche in stage @@ -404,12 +478,16 @@ Riallinea Interattivamente Stasha e Riapplica modifiche locali Su: + Trascina per riordinare i commit Branch di destinazione: Copia il Link Apri nel Browser ERRORE AVVISO + Schede + Workspaces Unisci Branch + Personalizza messaggio di merge In: Opzione di Unione: Sorgente: @@ -417,13 +495,15 @@ Commit di tutte le modifiche Strategia: Obiettivi: + Sposta Sottomodulo + Sposta Verso: + Sottomodulo: Sposta Nodo Repository Seleziona nodo padre per: Nome: Git NON è configurato. Prima vai su [Preferenze] per configurarlo. Apri Cartella Dati App Apri nello Strumento di Merge - Apri con... Opzionale. Crea Nuova Pagina Segnalibro @@ -443,7 +523,6 @@ {0} mesi fa {0} anni fa Ieri - Usa 'Shift+Enter' per inserire una nuova riga. 'Enter' è il tasto rapido per il pulsante OK Preferenze AI Analizza il Prompt Differenza @@ -460,7 +539,6 @@ 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 @@ -488,6 +566,7 @@ Questa applicazione richiede Git (>= 2.25.1) Percorso Installazione Abilita la verifica HTTP SSL + Usa git-credential-libsecret invece di git-credential-manager Nome Utente Nome utente Git globale Versione di Git @@ -512,7 +591,7 @@ In: Modifiche Locali: Scarta - Stasha e Riapplica + Stasha e Riapplica Remoto: Scarica (Recupera e Unisci) Riallineare anziché unire @@ -520,10 +599,13 @@ Assicurati che i sottomoduli siano stati inviati Forza l'invio Branch Locale: + NUOVO Remoto: + Revisione: + Invia Revisione Al Remoto Invia modifiche al remoto Branch Remoto: - Imposta come branch di tracking + Imposta come branch di tracciamento Invia tutti i tag Invia Tag al Remoto Invia a tutti i remoti @@ -540,6 +622,7 @@ URL del Repository: URL del repository Git remoto Copia URL + Azione Personalizzata Elimina... Modifica... Recupera @@ -560,10 +643,12 @@ Pulizia (GC e Potatura) Esegui il comando `git gc` per questo repository. Cancella tutto + Cancella Configura questo repository CONTINUA Azioni Personalizzate Nessuna Azione Personalizzata + Dashboard Scarta tutte le modifiche Apri nell'Esplora File Cerca Branch/Tag/Sottomodulo @@ -578,9 +663,11 @@ Per data del commit Topologicamente BRANCH LOCALI + Altre opzioni... Vai a HEAD Crea Branch CANCELLA LE NOTIFICHE + Evidenzia solo il branch corrente Apri in {0} Apri in Strumenti Esterni REMOTI @@ -590,8 +677,13 @@ Committer Contenuto Messaggio + Percorso SHA Branch Corrente + Solo commit decorati + Solo primo genitore + MOSTRA FLAG + Mostra commit persi Mostra i Sottomoduli Come Albero Mostra Tag come Albero SALTA @@ -605,6 +697,7 @@ Per nome Ordina Apri nel Terminale + Usa tempo relativo Visualizza i Log Visita '{0}' nel Browser WORKTREE @@ -615,6 +708,9 @@ Modalità Reset: Sposta a: Branch Corrente: + Resetta Branch (Senza Checkout) + Sposta Verso: + Branch: Mostra nell'Esplora File Ripristina Commit Commit: @@ -633,14 +729,17 @@ Salta questa versione Aggiornamento Software Non ci sono aggiornamenti disponibili. + Imposta Branch del Sottomodulo + Sottomodulo: + Attuale: + Cambia In: + Opzionale. Imposta al valore predefinito quando è vuoto. Imposta il Branch Branch: Rimuovi upstream Upstream: Copia SHA Vai a - Compatta Commit - In: Chiave Privata SSH: Percorso per la chiave SSH privata AVVIA @@ -648,10 +747,12 @@ Includi file non tracciati Messaggio: Opzionale. Informazioni di questo stash + Modalità: Solo modifiche in stage Sia le modifiche in stage che quelle non in stage dei file selezionati saranno stashate!!! Stasha Modifiche Locali Applica + Copia Messaggio Elimina Salva come Patch... Elimina Stash @@ -667,22 +768,36 @@ COMMIT: SOTTOMODULI Aggiungi Sottomodulo + BRANCH + Branch Percorso Relativo + Deinizializza Recupera sottomoduli annidati + Cronologia + Sposta Apri Repository del Sottomodulo Percorso Relativo: Cartella relativa per memorizzare questo modulo. Elimina Sottomodulo + Imposta Branch + Cambia URL STATO modificato non inizializzato revisione cambiata non unito + Aggiorna URL OK - Copia Nome Tag - Copia Messaggio Tag + AUTORE TAG + DATA + Messaggio + Nome + Autore + Copia Nome Tag + Azione Personalizzata Elimina ${0}$... + Elimina i {0} tag selezionati... Unisci ${0}$ in ${1}$... Invia ${0}$... Aggiorna Sottomoduli @@ -690,6 +805,7 @@ Inizializza se necessario Ricorsivamente Sottomodulo: + Aggiorna al branch di tracciamento remoto del sottomodulo URL: Log CANCELLA TUTTO @@ -713,6 +829,7 @@ Git Ignore Ignora tutti i file *{0} Ignora i file *{0} nella stessa cartella + Ignora file non tracciati in questa cartella Ignora solo questo file Modifica Puoi aggiungere in stage questo file ora. @@ -722,6 +839,7 @@ Attiva evento click Commit (Modifica) Stage di tutte le modifiche e fai il commit + Stai creando un commit su un HEAD distaccato. Vuoi continuare? Hai stageato {0} file ma solo {1} file mostrati ({2} file sono stati filtrati). Vuoi procedere? CONFLITTI RILEVATI APRI STRUMENTO DI MERGE ESTERNO @@ -732,6 +850,7 @@ INCLUDI FILE NON TRACCIATI NESSUN MESSAGGIO RECENTE INSERITO NESSUN TEMPLATE DI COMMIT + Reimposta Autore SignOff IN STAGE RIMUOVI DA STAGE diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index 63e4736d1..77d8edcc6 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -41,7 +41,7 @@ 更新 バイナリファイルはサポートされていません!!! Blame - BLAMEではこのファイルはサポートされていません!!! + BLAMEではこのファイルはサポートされていません!!! ${0}$ をチェックアウトする... ワークツリーと比較 ブランチ名をコピー @@ -60,7 +60,6 @@ ${0}$ をリネームする... トラッキングブランチを設定... ブランチの比較 - 無効な上流ブランチ! キャンセル 親リビジョンにリセット このリビジョンにリセット @@ -75,7 +74,7 @@ 警告: コミットをチェックアウトするとHEADが切断されます ローカルの変更: 破棄 - スタッシュして再適用 + スタッシュして再適用 ブランチ: チェリーピック ソースをコミットメッセージに追加 @@ -126,8 +125,6 @@ 参照 SHA ブラウザで開く - 説明 - コミットのタイトルを入力 リポジトリの設定 コミットテンプレート テンプレート内容: @@ -187,10 +184,9 @@ 作成したブランチにチェックアウト ローカルの変更: 破棄 - スタッシュして再適用 + スタッシュして再適用 新しいブランチの名前: ブランチの名前を入力 - スペースはダッシュに置き換えられます。 ローカルブランチを作成 タグを作成... 付与されるコミット: @@ -242,7 +238,6 @@ スワップ シンタックスハイライト 行の折り返し - ブロックナビゲーションを有効化 マージツールで開く すべての行を表示 表示する行数を減らす @@ -402,7 +397,6 @@ Gitが設定されていません。まず[設定]に移動して設定を行ってください。 アプリケーションデータのディレクトリを開く マージツールで開く - 外部ツールで開く... 任意。 新しいページを開く ブックマーク @@ -422,7 +416,6 @@ {0} ヶ月前 {0} 年前 昨日 - 改行には'Shift+Enter'キーを使用します。 'Enter"はOKボタンのホットキーとして機能します。 設定 AI 差分分析プロンプト @@ -439,7 +432,6 @@ デフォルト エディタ 等幅フォント - テキストエディタでは等幅フォントのみを使用する テーマ テーマの上書き タイトルバーの固定タブ幅を使用 @@ -490,7 +482,7 @@ 宛先: ローカルの変更: 破棄 - スタッシュして再適用 + スタッシュして再適用 リモート: プル (フェッチ & マージ) マージの代わりにリベースを使用 @@ -609,8 +601,6 @@ 上流ブランチ: SHAをコピー Go to - スカッシュコミット - 宛先: SSH プライベートキー: プライベートSSHキーストアのパス スタート @@ -644,8 +634,6 @@ このモジュールを保存するフォルダの相対パス サブモジュールを削除 OK - タグ名をコピー - タグメッセージをコピー ${0}$ を削除... ${0}$ を ${1}$ にマージ... ${0}$ をプッシュ... diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml new file mode 100644 index 000000000..5cc2a06a1 --- /dev/null +++ b/src/Resources/Locales/ko_KR.axaml @@ -0,0 +1,894 @@ + + + 정보 + SourceGit 정보 + 릴리스 노트 + 오픈소스 & 무료 Git GUI 클라이언트 + 무시할 파일 추가 + 패턴: + 저장 파일: + 워크트리 추가 + 위치: + 이 워크트리의 경로입니다. 상대 경로를 지원합니다. + 브랜치 이름: + 선택 사항. 기본값은 대상 폴더 이름입니다. + 추적할 브랜치: + 원격 브랜치 추적 + 체크아웃할 대상: + 새 브랜치 생성 + 기존 브랜치 + AI 어시스턴트 + 재생성 + AI를 사용하여 커밋 메시지 생성 + 커밋 메시지로 적용 + SourceGit 숨기기 + 모두 보기 + 패치 + 패치 파일: + 적용할 .patch 파일을 선택하세요 + 공백 변경 사항 무시 + 패치 적용 + 공백: + 스태시 적용 + 적용 후 삭제 + 인덱스의 변경 사항 복원 + 스태시: + 아카이브... + 아카이브 저장 위치: + 아카이브 파일 경로 선택 + 리비전: + 아카이브 + SourceGit Askpass + 암호 입력: + 변경되지 않음으로 간주된 파일 + 변경되지 않음으로 간주된 파일 없음 + 이미지 불러오기... + 새로 고침 + 바이너리 파일은 지원되지 않습니다!!! + 이진 탐색 + 중단 + 나쁨 + 이진 탐색 중. 현재 HEAD가 '좋음' 상태입니까, '나쁨' 상태입니까? + 좋음 + 건너뛰기 + 이진 탐색 중. 현재 커밋을 '좋음' 또는 '나쁨'으로 표시하고 다른 커밋을 체크아웃하세요. + 블레임 + ${0}$ 체크아웃... + ${0}$와(과) 비교 + 워크트리와 비교 + 브랜치 이름 복사 + 사용자 지정 작업 + ${0}$ 삭제... + 선택한 {0}개의 브랜치 삭제 + ${0}$(으)로 Fast-Forward + ${0}$에서 ${1}$(으)로 Fetch... + Git Flow - ${0}$ 완료 + ${0}$을(를) ${1}$(으)로 병합... + 선택한 {0}개의 브랜치를 현재 브랜치로 병합 + ${0}$ Pull + ${0}$에서 ${1}$(으)로 Pull... + ${0}$ Push + ${1}$을(를) 기반으로 ${0}$ 리베이스... + ${0}$ 이름 바꾸기... + ${0}$을(를) ${1}$(으)로 리셋... + ${0}$(워크트리)로 전환 + 추적 브랜치 설정... + 브랜치 비교 + {0}개 커밋 앞섬 + {0}개 커밋 앞섬, {1}개 커밋 뒤처짐 + {0}개 커밋 뒤처짐 + 유효하지 않음 + 원격 + 상태 + 추적 중 + URL + 워크트리 + 취소 + 부모 리비전으로 리셋 + 이 리비전으로 리셋 + 커밋 메시지 생성 + 표시 모드 변경 + 파일 및 디렉터리 목록으로 보기 + 경로 목록으로 보기 + 파일 시스템 트리로 보기 + 서브모듈 URL 변경 + 서브모듈: + URL: + 브랜치 체크아웃 + 커밋 체크아웃 + 커밋: + 경고: 커밋 체크아웃을 하면, HEAD가 분리됩니다(detached) + 로컬 변경 사항: + 폐기 + 스태시 & 재적용 + 브랜치: + 현재 HEAD에 브랜치/태그에 연결되지 않은 커밋이 있습니다! 계속하시겠습니까? + 체크아웃 & Fast-Forward + Fast-Forward 대상: + 체리픽 + 커밋 메시지에 원본 추가 + 커밋: + 모든 변경 사항 커밋 + 메인라인: + 어느 쪽을 메인라인으로 간주해야 할지 알 수 없기 때문에 일반적으로 병합(merge)을 체리픽할 수 없습니다. 이 옵션을 사용하면 지정된 부모를 기준으로 변경 사항을 다시 적용할 수 있습니다. + 모든 스태시 지우기 + 모든 스태시를 지우려고 합니다. 계속하시겠습니까? + 원격 저장소 복제 + 추가 파라미터: + 저장소 복제 시 추가 인수. 선택 사항. + 로컬 이름: + 저장소 이름. 선택 사항. + 상위 폴더: + 서브모듈 초기화 & 업데이트 + 저장소 URL: + 닫기 + 에디터 + 커밋 체크아웃 + 커밋 체리픽 + 체리픽... + HEAD와 비교 + 워크트리와 비교 + 작성자 + 메시지 + 커밋터 + SHA + 제목 + 사용자 지정 작업 + 커밋 삭제 + 대화형 리베이스 + 삭제(Drop)... + 수정(Edit)... + 부모에 합치기(Fixup)... + ${1}$을(를) 기반으로 ${0}$ 대화형 리베이스 + 메시지 수정(Reword)... + 부모에 합치기(Squash)... + ${0}$(으)로 병합 + 병합... + ${0}$을(를) ${1}$(으)로 푸시 + ${1}$을(를) 기반으로 ${0}$ 리베이스 + ${0}$을(를) ${1}$(으)로 리셋 + 커밋 되돌리기 + 메시지 수정 + 패치로 저장... + 부모에 합치기 + 부모에 합치기(Fixup) + 변경 사항 + 변경된 파일 + 변경 사항 검색... + 파일 + LFS 파일 + 파일 검색... + 서브모듈 + 정보 + 작성자 + 자식 + 커밋터 + 이 커밋을 포함하는 ref 확인 + 커밋 포함 REF + 이메일 복사 + 이름 복사 + 이름 & 이메일 복사 + 처음 100개의 변경 사항만 표시합니다. 모든 변경 사항은 '변경 사항' 탭에서 확인하세요. + 키: + 메시지 + 부모 + REFS + SHA + 서명자: + 브라우저에서 열기 + 제목 + 저장소 설정 + 커밋 템플릿 + ${files_num}, ${branch_name}, ${files} 및 ${files:N} (N은 출력할 최대 파일 경로 수)을(를) 사용할 수 있습니다. + 템플릿 내용: + 템플릿 이름: + 사용자 지정 작업 + 인수: + 내장 파라미터: + + ${REPO} 저장소 경로 + ${REMOTE} 선택한 원격 또는 선택한 브랜치의 원격 + ${BRANCH} 선택한 브랜치 (원격 브랜치의 경우 ${REMOTE} 부분 제외) + ${BRANCH_FRIENDLY_NAME} 선택한 브랜치의 식별하기 쉬운 이름 (원격 브랜치의 경우 ${REMOTE} 부분 포함) + ${SHA} 선택한 커밋의 해시 + ${TAG} 선택한 태그 + ${FILE} 저장소 루트에 상대적인 선택된 파일 + $1, $2 ... 입력 컨트롤 값 + 실행 파일: + 입력 컨트롤: + 편집 + 이름: + 범위: + 브랜치 + 커밋 + 파일 + 원격 + 저장소 + 태그 + 작업이 끝날 때까지 대기 + 이메일 주소 + 이메일 주소 + GIT + 원격 자동 Fetch + + 기본 원격 + 선호하는 병합 모드 + 이슈 트래커 + Azure DevOps 규칙 추가 + Gerrit Change-Id 커밋 규칙 추가 + Gitee 이슈 규칙 추가 + Gitee Pull Request 규칙 추가 + GitHub 규칙 추가 + GitLab 이슈 규칙 추가 + GitLab Merge Request 규칙 추가 + Jira 규칙 추가 + 새 규칙 + 이슈 정규식: + 규칙 이름: + .issuetracker 파일에 이 규칙 공유 + 결과 URL: + 정규식 그룹 값에 접근하려면 $1, $2를 사용하세요. + AI + 선호하는 서비스: + '선호하는 서비스'가 설정되면, SourceGit은 이 저장소에서 해당 서비스만 사용합니다. 그렇지 않고 사용 가능한 서비스가 두 개 이상인 경우, 하나를 선택할 수 있는 컨텍스트 메뉴가 표시됩니다. + HTTP 프록시 + 이 저장소에서 사용하는 HTTP 프록시 + 사용자 이름 + 이 저장소의 사용자 이름 + 사용자 지정 작업 컨트롤 편집 + 선택 시 값: + 선택 시, 이 값이 명령줄 인수로 사용됩니다 + 설명: + 기본값: + 폴더 여부: + 레이블: + 옵션: + 옵션 구분자로 '|'를 사용하세요 + 유형: + 작업 공간 + 색상 + 이름 + 시작 시 탭 복원 + 계속 + 빈 커밋이 감지되었습니다! 계속하시겠습니까 (--allow-empty)? + 모두 스테이징 & 커밋 + 빈 커밋이 감지되었습니다! 계속하시겠습니까 (--allow-empty) 아니면 모두 스테이징 후 커밋하시겠습니까? + 재시작 필요 + 변경 사항을 적용하려면 앱을 다시 시작해야 합니다. + Conventional Commit 도우미 + 주요 변경 사항(Breaking Change): + 종료된 이슈: + 상세 변경 내역: + 범위: + 간단한 설명: + 변경 유형: + 복사 + 전체 텍스트 복사 + 전체 경로 복사 + 경로 복사 + 브랜치 생성... + 기준: + 생성된 브랜치로 체크아웃 + 로컬 변경 사항: + 폐기 + 스태시 & 재적용 + 새 브랜치 이름: + 브랜치 이름을 입력하세요. + 로컬 브랜치 생성 + 기존 브랜치 덮어쓰기 + 태그 생성... + 태그 생성 위치: + GPG 서명 + 태그 메시지: + 선택 사항. + 태그 이름: + 권장 형식: v1.0.0-alpha + 생성 후 모든 원격에 푸시 + 새 태그 생성 + 종류: + 주석 태그 + 경량 태그 + Ctrl을 누른 채 클릭하면 바로 시작합니다 + 잘라내기 + 서브모듈 초기화 해제 + 로컬 변경 사항이 있어도 강제로 초기화 해제합니다. + 서브모듈: + 브랜치 삭제 + 브랜치: + 원격 브랜치를 삭제하려고 합니다!!! + 원격 브랜치 ${0}$도 함께 삭제 + 여러 브랜치 삭제 + 한 번에 여러 브랜치를 삭제하려고 합니다. 실행하기 전에 다시 한번 확인하세요! + 여러 태그 삭제 + 원격 저장소에서도 삭제 + 한 번에 여러 태그를 삭제하려고 합니다. 실행하기 전에 다시 한번 확인하세요! + 원격 삭제 + 원격: + 경로: + 대상: + 모든 하위 항목이 목록에서 제거됩니다. + 목록에서만 제거되며, 디스크에서 삭제되지 않습니다! + 그룹 삭제 확인 + 저장소 삭제 확인 + 서브모듈 삭제 + 서브모듈 경로: + 태그 삭제 + 태그: + 원격 저장소에서도 삭제 + 바이너리 비교 + 파일 모드 변경됨 + 첫 번째 차이점 + 공백 변경 사항 무시 + 혼합 + 차이점 + 나란히 보기 + 스와이프 + 마지막 차이점 + LFS 객체 변경 + 신규 + 다음 차이점 + 변경 사항 없음 또는 줄바꿈(EOL) 변경만 있음 + 기존 + 이전 차이점 + 패치로 저장 + 숨겨진 기호 표시 + 나란히 비교 + 서브모듈 + 삭제됨 + 신규 + 전환 + 구문 강조 + 줄 바꿈 + 병합 도구에서 열기 + 모든 줄 표시 + 표시 줄 수 줄이기 + 표시 줄 수 늘리기 + 파일을 선택하여 변경 사항 보기 + 디렉터리 히스토리 + 로컬 변경 사항 있음 + 업스트림과 불일치 + 이미 최신 상태 + 변경 사항 폐기 + 작업 사본의 모든 로컬 변경 사항. + 변경 사항: + 무시된 파일 포함 + 추적하지 않는 파일 포함 + {0}개의 변경 사항이 폐기됩니다 + 이 작업은 되돌릴 수 없습니다!!! + 커밋 삭제 + 커밋: + 새 HEAD: + 북마크: + 새 이름: + 대상: + 선택한 그룹 편집 + 선택한 저장소 편집 + 대상: + 이 저장소 + Fetch + 모든 원격 Fetch + 로컬 ref 강제 덮어쓰기 + 태그 없이 Fetch + 원격: + 원격 변경 사항 Fetch + 변경되지 않음으로 간주 + 폐기... + {0}개 파일 폐기... + ${0}$을(를) 사용하여 해결 + 패치로 저장... + 스테이지 + {0}개 파일 스테이지 + 스태시... + {0}개 파일 스태시... + 언스테이지 + {0}개 파일 언스테이지 + 내 것 사용 (checkout --ours) + 상대방 것 사용 (checkout --theirs) + 파일 히스토리 + 변경 사항 + 내용 + Git-Flow + 개발 브랜치: + Feature: + Feature 접두사: + FLOW - Feature 완료 + FLOW - Hotfix 완료 + FLOW - Release 완료 + 대상: + 완료 후 원격(들)에 푸시 + 병합 시 스쿼시 + 핫픽스: + Hotfix 접두사: + Git-Flow 초기화 + 브랜치 유지 + 운영 브랜치: + 릴리스: + Release 접두사: + Feature 시작... + FLOW - Feature 시작 + Hotfix 시작... + FLOW - Hotfix 시작 + 이름 입력 + Release 시작... + FLOW - Release 시작 + 버전 태그 접두사: + Git LFS + 추적 패턴 추가... + 패턴이 파일 이름임 + 사용자 정의 패턴: + Git LFS에 추적 패턴 추가 + Fetch + Git LFS 객체를 다운로드하려면 `git lfs fetch`를 실행하세요. 이 작업은 작업 사본을 업데이트하지 않습니다. + LFS 객체 Fetch + Git LFS 훅(hook) 설치 + 잠금 보기 + 잠긴 파일 없음 + 잠금 + 내 잠금만 보기 + LFS 잠금 + 잠금 해제 + 강제 잠금 해제 + 정리 + 로컬 저장소에서 오래된 LFS 파일을 삭제하려면 `git lfs prune`을 실행하세요 + Pull + 현재 ref 및 체크아웃에 대한 모든 Git LFS 파일을 다운로드하려면 `git lfs pull`을 실행하세요 + LFS 객체 Pull + 푸시 + 대기 중인 대용량 파일을 Git LFS 엔드포인트로 푸시합니다 + LFS 객체 푸시 + 원격: + '{0}' 이름의 파일 추적 + 모든 *{0} 파일 추적 + 히스토리 + 작성자 + 작성 시간 + 그래프 & 제목 + SHA + 커밋 시간 + {0}개 커밋 선택됨 + 'Ctrl' 또는 'Shift' 키를 누른 채로 여러 커밋을 선택하세요. + ⌘ 또는 ⇧ 키를 누른 채로 여러 커밋을 선택하세요. + 팁: + 키보드 단축키 참조 + 전역 + 새 저장소 복제 + 현재 탭 닫기 + 다음 탭으로 이동 + 이전 탭으로 이동 + 새 탭 만들기 + 환경설정 대화상자 열기 + 활성 탭 전환 + 저장소 + 스테이징된 변경 사항 커밋 + 스테이징된 변경 사항 커밋 및 푸시 + 모든 변경 사항 스테이징 후 커밋 + Fetch (바로 시작) + 대시보드 모드 (기본) + 커밋 검색 모드 열기 + Pull (바로 시작) + 푸시 (바로 시작) + 이 저장소 강제 새로고침 + '변경 사항'으로 전환 + '히스토리'로 전환 + '스태시'로 전환 + 텍스트 에디터 + 검색 패널 닫기 + 다음 일치 항목 찾기 + 이전 일치 항목 찾기 + 외부 diff/merge 도구로 열기 + 검색 패널 열기 + 폐기 + 스테이지 + 언스테이지 + 저장소 초기화 + 경로: + 체리픽 진행 중. + 커밋 처리 중 + 병합 진행 중. + 병합 중 + 리베이스 진행 중. + 중단 지점 + 되돌리기 진행 중. + 커밋 되돌리는 중 + 대화형 리베이스 + 로컬 변경 사항 스태시 & 재적용 + 기준: + 드래그 앤 드롭으로 커밋 순서 변경 + 대상 브랜치: + 링크 복사 + 브라우저에서 열기 + 오류 + 알림 + + 작업 공간 + 브랜치 병합 + 병합 메시지 수정 + 대상: + 병합 옵션: + 소스: + 병합 (다중) + 모든 변경 사항 커밋 + 전략: + 대상: + 서브모듈 이동 + 이동 위치: + 서브모듈: + 저장소 노드 이동 + 상위 노드 선택: + 이름: + Git이 구성되지 않았습니다. [환경설정]으로 이동하여 먼저 구성하세요. + 데이터 저장 디렉터리 열기 + 병합 도구에서 열기 + 선택 사항. + 새 탭 만들기 + 북마크 + 탭 닫기 + 다른 탭 닫기 + 오른쪽 탭 닫기 + 저장소 경로 복사 + 저장소 + 붙여넣기 + {0}일 전 + 1시간 전 + {0}시간 전 + 방금 전 + 지난 달 + 작년 + {0}분 전 + {0}개월 전 + {0}년 전 + 어제 + 환경설정 + AI + Diff 분석 프롬프트 + API 키 + 제목 생성 프롬프트 + 모델 + 이름 + 입력된 값은 환경변수(ENV)에서 API 키를 불러올 이름입니다 + 서버 + 스트리밍 활성화 + 모양 + 기본 글꼴 + 에디터 탭 너비 + 글꼴 크기 + 기본 + 에디터 + 고정폭 글꼴 + 테마 + 테마 재정의 + 스크롤바 자동 숨기기 사용 + 네이티브 윈도우 프레임 사용 + DIFF/MERGE 도구 + 설치 경로 + diff/merge 도구 경로 입력 + 도구 + 일반 + 시작 시 업데이트 확인 + 날짜 형식 + 변경 사항 트리에서 폴더 압축 활성화 + 언어 + 히스토리 커밋 수 + 그래프에 커밋 시간 대신 작성자 시간 표시 + 기본으로 `로컬 변경 사항` 페이지 표시 + 커밋 세부 정보에서 기본으로 `변경 사항` 탭 표시 + 커밋 세부 정보에 자식 커밋 표시 + 커밋 그래프에 태그 표시 + 제목 가이드 길이 + GitHub 스타일 기본 아바타 생성 + GIT + 자동 CRLF 활성화 + 기본 복제 디렉터리 + 사용자 이메일 + 전역 git 사용자 이메일 + Fetch 시 --prune 활성화 + diff 시 --ignore-cr-at-eol 활성화 + 이 앱은 Git (>= 2.25.1)을(를) 필요로 합니다 + 설치 경로 + HTTP SSL 검증 활성화 + git-credential-manager 대신 git-credential-libsecret 사용 + 사용자 이름 + 전역 git 사용자 이름 + Git 버전 + GPG 서명 + 커밋 GPG 서명 + GPG 형식 + 프로그램 설치 경로 + 설치된 gpg 프로그램 경로 입력 + 태그 GPG 서명 + 사용자 서명 키 + 사용자의 gpg 서명 키 + 연동 + 셸/터미널 + 경로 + 셸/터미널 + 원격 정리 + 대상: + 워크트리 정리 + `$GIT_COMMON_DIR/worktrees`의 워크트리 정보 정리 + Pull + 원격 브랜치: + 대상: + 로컬 변경 사항: + 폐기 + 스태시 & 재적용 + 원격: + Pull (Fetch & 병합) + 병합 대신 리베이스 사용 + 푸시 + 서브모듈이 푸시되었는지 확인 + 강제 푸시 + 로컬 브랜치: + 신규 + 원격: + 리비전: + 리비전을 원격에 푸시 + 변경 사항을 원격에 푸시 + 원격 브랜치: + 추적 브랜치로 설정 + 모든 태그 푸시 + 태그를 원격에 푸시 + 모든 원격에 푸시 + 원격: + 태그: + 종료 + 현재 브랜치 리베이스 + 로컬 변경 사항 스태시 & 재적용 + 기준: + 원격 추가 + 원격 편집 + 이름: + 원격 이름 + 저장소 URL: + 원격 git 저장소 URL + URL 복사 + 사용자 지정 작업 + 삭제... + 편집... + Fetch + 브라우저에서 열기 + 정리 + 워크트리 제거 확인 + `--force` 옵션 활성화 + 대상: + 브랜치 이름 변경 + 새 이름: + 이 브랜치의 고유한 이름 + 브랜치: + 중단 + 원격에서 변경 사항 자동 Fetch 중... + 정렬 + 커밋 날짜 순 + 이름 순 + 정리 (GC & Prune) + 이 저장소에 대해 `git gc` 명령을 실행합니다. + 모두 지우기 + 지우기 + 이 저장소 설정 + 계속 + 사용자 지정 작업 + 사용자 지정 작업 없음 + 대시보드 + 모든 변경 사항 폐기 + 파일 탐색기에서 열기 + 브랜치/태그/서브모듈 검색 + 그래프에 표시 여부 + 설정 안 함 + 커밋 그래프에서 숨기기 + 커밋 그래프에서 필터링 + 레이아웃 + 수평 + 수직 + 커밋 순서 + 커밋 날짜 + 위상 정렬 + 로컬 브랜치 + 추가 옵션... + HEAD로 이동 + 브랜치 생성 + 알림 지우기 + 현재 브랜치만 강조 + {0}에서 열기 + 외부 도구에서 열기 + 원격 + 원격 추가 + 커밋 검색 + 작성자 + 커밋터 + 내용 + 메시지 + 경로 + SHA + 현재 브랜치 + 장식된(Decorated) 커밋만 + 첫 번째 부모만 + 플래그 표시 + 유실된(Lost) 커밋 표시 + 서브모듈을 트리로 표시 + 태그를 트리로 표시 + 건너뛰기 + 통계 + 서브모듈 + 서브모듈 추가 + 서브모듈 업데이트 + 태그 + 새 태그 + 생성 날짜 순 + 이름 순 + 정렬 + 터미널에서 열기 + 상대 시간 사용 + 로그 보기 + 브라우저에서 '{0}' 방문 + 워크트리 + 워크트리 추가 + 정리 + Git 저장소 URL + 현재 브랜치를 리비전으로 리셋 + 리셋 모드: + 이동 대상: + 현재 브랜치: + 브랜치 리셋 (체크아웃 없음) + 이동 대상: + 브랜치: + 파일 탐색기에서 보기 + 커밋 되돌리기 + 커밋: + 되돌린 변경 사항 커밋 + 커밋 메시지 수정 + 실행 중. 잠시만 기다려주세요... + 저장 + 다른 이름으로 저장... + 패치가 성공적으로 저장되었습니다! + 저장소 스캔 + 루트 디렉터리: + 다른 사용자 정의 디렉터리 스캔 + 업데이트 확인... + 이 소프트웨어의 새 버전을 사용할 수 있습니다: + 업데이트 확인 실패! + 다운로드 + 이 버전 건너뛰기 + 소프트웨어 업데이트 + 현재 사용 가능한 업데이트가 없습니다. + 서브모듈 브랜치 설정 + 서브모듈: + 현재: + 변경: + 선택 사항. 비어 있으면 기본값으로 설정됩니다. + 추적 브랜치 설정 + 브랜치: + 업스트림 설정 해제 + 업스트림: + SHA 복사 + 이동 + SSH 개인 키: + 개인 SSH 키 저장 경로 + 시작 + 스태시 + 추적하지 않는 파일 포함 + 메시지: + 선택 사항. 이 스태시의 메시지 + 모드: + 스테이징된 변경 사항만 + 선택한 파일의 스테이징된 변경 사항과 스테이징되지 않은 변경 사항이 모두 스태시됩니다!!! + 로컬 변경 사항 스태시 + 적용 + 메시지 복사 + 삭제 + 패치로 저장... + 스태시 삭제 + 삭제: + 스태시 + 변경 사항 + 스태시 + 통계 + 개요 + 이번 달 + 이번 주 + 작성자: + 커밋: + 서브모듈 + 서브모듈 추가 + 브랜치 + 브랜치 + 상대 경로 + 초기화 해제 + 중첩된 서브모듈 Fetch + 히스토리 + 이동 + 저장소 열기 + 상대 경로: + 이 모듈을 저장할 상대 폴더입니다. + 삭제 + 브랜치 설정 + URL 변경 + 상태 + 수정됨 + 초기화 안 됨 + 리비전 변경됨 + 업데이트 + URL + 확인 + 태그 생성자 + 시간 + 메시지 + 이름 + 태그 생성자 + 태그 이름 복사 + 사용자 지정 작업 + ${0}$ 삭제... + 선택한 {0}개의 태그 삭제... + ${0}$을(를) ${1}$(으)로 병합... + ${0}$ 푸시... + 서브모듈 업데이트 + 모든 서브모듈 + 필요시 초기화 + 서브모듈 재귀적으로 탐색 + 서브모듈: + 서브모듈의 원격 추적 브랜치로 업데이트 + URL: + 로그 + 모두 지우기 + 복사 + 삭제 + 경고 + 시작 페이지 + 그룹 생성 + 하위 그룹 생성 + 저장소 복제 + 삭제 + 폴더 끌어다 놓기 지원. 사용자 정의 그룹화 지원. + 편집 + 다른 그룹으로 이동 + 모든 저장소 열기 + 저장소 열기 + 터미널 열기 + 기본 복제 디렉터리의 저장소 다시 스캔 + 저장소 검색... + 로컬 변경 사항 + Git 무시 + 모든 *{0} 파일 무시 + 같은 폴더의 *{0} 파일 무시 + 이 폴더의 추적하지 않는 파일 무시 + 이 파일만 무시 + 수정 + 이제 이 파일을 스테이징할 수 있습니다. + 히스토리 지우기 + 모든 커밋 메시지 히스토리를 지우시겠습니까? 이 작업은 되돌릴 수 없습니다. + 커밋 + 커밋 & 푸시 + 템플릿/히스토리 + 클릭 이벤트 트리거 + 커밋 (수정) + 모든 변경 사항 스테이징 후 커밋 + 분리된(detached) HEAD에 커밋을 생성하고 있습니다. 계속하시겠습니까? + {0}개의 파일을 스테이징했지만 {1}개의 파일만 표시됩니다 ({2}개의 파일은 필터링됨). 계속하시겠습니까? + 충돌 감지됨 + 외부 병합 도구 열기 + 모든 충돌을 외부 병합 도구에서 열기 + 파일 충돌 해결됨 + 내 것 사용 + 상대방 것 사용 + 추적하지 않는 파일 포함 + 최근 입력한 메시지 없음 + 커밋 템플릿 없음 + 검증 안 함 + 작성자 리셋 + 서명(SignOff) + 스테이징됨 + 언스테이지 + 모두 언스테이지 + 스테이징 안 됨 + 스테이지 + 모두 스테이지 + 변경되지 않음으로 간주된 파일 보기 + 템플릿: ${0}$ + 작업 공간: + 작업 공간 설정... + 워크트리 + 경로 복사 + 잠금 + 열기 + 제거 + 잠금 해제 + diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 87a14a3af..b8681a11a 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -35,7 +35,7 @@ Atualizar ARQUIVO BINÁRIO NÃO SUPORTADO!!! Blame - BLAME NESTE ARQUIVO NÃO É SUPORTADO!!! + BLAME NESTE ARQUIVO NÃO É SUPORTADO!!! Checkout ${0}$... Comparar com ${0}$ Comparar com Worktree @@ -67,7 +67,7 @@ Aviso: Ao fazer o checkout de um commit, seu Head ficará desanexado Alterações Locais: Descartar - Stash & Reaplicar + Stash & Reaplicar Branch: Cherry-Pick Adicionar origem à mensagem de commit @@ -113,8 +113,6 @@ REFERÊNCIAS SHA Abrir no navegador - Descrição - Insira o assunto do commit Configurar Repositório TEMPLATE DE COMMIT Conteúdo do Template: @@ -169,7 +167,7 @@ Checar o branch criado Alterações Locais: Descartar - Guardar & Reaplicar + Guardar & Reaplicar Nome do Novo Branch: Insira o nome do branch. Criar Branch Local @@ -365,7 +363,6 @@ O Git NÃO foi configurado. Por favor, vá para [Preferências] e configure primeiro. Abrir Pasta de Dados do Aplicativo Abrir na Ferramenta de Mesclagem - Abrir Com... Opcional. Criar Nova Página Adicionar aos Favoritos @@ -385,7 +382,6 @@ {0} meses atrás {0} anos atrás Ontem - Use 'Shift+Enter' para inserir uma nova linha. 'Enter' é a tecla de atalho do botão OK Preferências INTELIGÊNCIA ARTIFICIAL Prompt para Analisar Diff @@ -400,7 +396,6 @@ 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 @@ -447,7 +442,7 @@ Para: Alterações Locais: Descartar - Guardar & Reaplicar + Guardar & Reaplicar Remoto: Puxar (Buscar & Mesclar) Usar rebase em vez de merge @@ -552,8 +547,6 @@ 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 @@ -586,8 +579,6 @@ Pasta relativa para armazenar este módulo. Excluir Submódulo OK - Copiar Nome da Tag - Copiar mensage da Tag Excluir ${0}$... Mesclar ${0}$ em ${1}$... Enviar ${0}$... diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index c5472492a..ea658ef9e 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -5,6 +5,7 @@ О программе О SourceGit + Примечания выпуска Бесплатный графический клиент Git с исходным кодом Добавить файл(ы) к игнорируемым Шаблон: @@ -13,7 +14,7 @@ Расположение: Путь к рабочему каталогу (поддерживается относительный путь) Имя ветки: - Имя целевого каталога по умолчанию. (необязательно) + Имя целевого каталога по умолчанию (необязательно) Отслеживание ветки: Отслеживание внешней ветки Переключиться на: @@ -23,6 +24,8 @@ ПЕРЕСОЗДАТЬ Использовать OpenAI для создания сообщения о ревизии ПРИМЕНИТЬ КАК СООБЩЕНИЕ РЕВИЗИИ + Скрыть SourceGit + Показать все Исправить Файл заплатки: Выберите файл .patch для применения @@ -53,14 +56,18 @@ Пропустить Раздвоение. Сделать текущую ревизию хорошей или плохой и переключиться на другой. Расследование - РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! + Расследование на предыдущей редакции + РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! Переключиться на ${0}$... Сравнить с ${0}$ Сравнить с рабочим каталогом Копировать имя ветки + Создать PR... + Создать PR для основной ветки ${0}$... Изменить действие Удалить ${0}$... Удалить выбранные {0} ветки + Править описание для ${0}$... Перемотать вперёд к ${0}$ Извлечь ${0}$ в ${1}$... Git-процесс - Завершение ${0}$ @@ -72,13 +79,18 @@ Переместить ${0}$ на ${1}$... Переименовать ${0}$... Сбросить ${0}$ к ${1}$... + Переключить на ${0}$ (рабочий каталог) Отслеживать ветку... Сравнение веток - ЛОКАЛЬНЫЙ + {0} ревизий вперёд + {0} ревизий вперёд, {1} ревизий назад + {0} ревизий назад + Неверно УДАЛЁННЫЙ + СОСТОЯНИЕ ОТСЛЕЖИВАНИЕ URL-АДРЕС - Недопустимая основная ветка! + РАБОЧИЙ КАТАЛОГ ОТМЕНА Сбросить родительскую ревизию Сбросить эту ревизию @@ -96,8 +108,7 @@ Предупреждение: После переключения ревизии ваша Голова (HEAD) будет отсоединена Локальные изменения: Отклонить - Отложить и примненить повторно - Обновить все подкаталоги + Отложить и применить повторно Ветка: Ваша текущая ГОЛОВА содержит ревизию(и), не связанные ни с к какими ветками или метками! Вы хотите продолжить? Переключиться и перемотать @@ -111,12 +122,12 @@ Очистить отложенные Вы пытаетесь очистить все отложенные. Вы уверены, что хотите продолжить? Клонировать внешний репозиторий - Расширенные параметры: - Дополнительные аргументы для клонирования репозитория. (необязательно). + Параметры: + Аргументы git clone (необязательно) Локальное имя: - Имя репозитория. (необязательно). + Имя репозитория (необязательно) Родительский каталог: - Создать и обновить подмодуль + Создать и обновить подмодули Адрес репозитория: ЗАКРЫТЬ Редактор @@ -131,13 +142,14 @@ SHA Субъект Пользовательское действие + Бросить ревизию Интерактивное перемещение Бросить... Редактировать... Исправить в родительском... Интерактивное перемещение ${0}$ в ${1}$ - Имзенить комментарий... - Втиуснуть в родительский... + Изменить комментарий... + Втиснуть в родительский... Влить в ${0}$ Влить ... Выложить ${0}$ в ${1}$ @@ -147,6 +159,7 @@ Изменить комментарий Сохранить как заплатки... Объединить с предыдущей ревизией + Исправить в родительском ИЗМЕНЕНИЯ изменённый(х) файл(ов) Найти изменения.... @@ -160,6 +173,9 @@ РЕВИЗОР (ИСПОЛНИТЕЛЬ) Найти все ветки с этой ревизией ВЕТКИ С ЭТОЙ РЕВИЗИЕЙ + Копировать адрес почты + Копировать имя + Копировать имя и адрес почты Отображаются только первые 100 изменений. Смотрите все изменения на вкладке ИЗМЕНЕНИЯ. Ключ: СООБЩЕНИЕ @@ -168,25 +184,41 @@ SHA Подписант: Открыть в браузере - Описание + Введите сообщение ревизии. Пожалуйста, используйте пустые строки для разделения субъекта и описания! СУБЪЕКТ - Введите тему ревизии Настройка репозитория ШАБЛОН РЕВИЗИИ - Вы можете использовать ${files_num}, ${branch_name}, ${files} и ${files:N}, где N — максимальное количество путей к файлам для вывода. + Встроенные параметры: + + ${branch_name} Имя текущей локальной ветки + ${files_num} Количество изменённых файлов + ${files} Пути изменённых файлов + ${files:N} Пути изменённых файлов, не более N + ${pure_files} То же, что и ${files}, но только имена файлов + ${pure_files:N} То же, что и ${files:N}, но только имена файлов Cодержание: Название: ПОЛЬЗОВАТЕЛЬСКОЕ ДЕЙСТВИЕ Аргументы: - Встроенные параметры: ${REPO} — путь к репозиторию; ${BRANCH} — выбранная ветка; ${SHA} — хеш выбранной ревизии; ${TAG} — выбранная метка + Встроенные параметры: + + ${REPO} Путь репозитория + ${REMOTE} Выбранная удаённая ветка + ${BRANCH} Выбранная ветка, без ${REMOTE} удалённых веток + ${BRANCH_FRIENDLY_NAME} Понятное имя выбранной ветки, содержащую ${REMOTE} удалённые ветки + ${SHA} Хеш выбранной ревизии + ${TAG} Выбранная метка + ${FILE} Выбранный файл, относительно корня репозитория + $1, $2 ... Ввод управляющих значений Исполняемый файл: Элементы управления вводом: Редактор - Вы можете использовать $1, $2 ... в аргументах для значений элемента управления вводом Имя: Диапазон: Ветка Ревизия + Файл + Удалённый Репозиторий Метка Ждать для выполения выхода @@ -195,10 +227,12 @@ GIT Автозагрузка изменений Минут(а/ы) + Общепринятые типы ревизии Внешний репозиторий по умолчанию Предпочтительный режим слияния ОТСЛЕЖИВАНИЕ ПРОБЛЕМ Добавить пример правила Azure DevOps + Добавить правило Gerrit ревизии идентификатора изменения Добавить пример правила для тем в Gitea Добавить пример правила запроса скачивания из Gitea Добавить пример правила для Git @@ -227,6 +261,7 @@ Метка: Опции: Используйте разделитель «|» для опций + Встроенные переменные ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE}, и ${TAG} останутся здесь доступными Тип: Рабочие пространства Цвет @@ -235,7 +270,7 @@ ПРОДОЛЖИТЬ Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty)? Сформировать всё и зафиксировать ревизию - Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty) или отложить всё, затем зафиксировать ревизию? + Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty) или отложить всё, а затем зафиксировать ревизию? Требуется перезапуск Вы должны перезапустить приложение после применения изменений. Общепринятый помощник по ревизии @@ -254,10 +289,9 @@ Переключиться на созданную ветку Локальные изменения: Отклонить - Отложить и применить повторно + Отложить и применить повторно Имя новой ветки: Введите имя ветки. - Пробелы будут заменены на тире. Создать локальную ветку Перезаписать существующую ветку Создать метку... @@ -283,6 +317,9 @@ Также удалите внешнюю ветку ${0}$ Удаление нескольких веток Вы пытаетесь удалить несколько веток одновременно. Обязательно перепроверьте, прежде чем предпринимать какие-либо действия! + Удалить несколько меток + Удалить их с удалённого + Вы пытаетесь удалить сразу несколько меток. Перепроверьте обязательно перед выполнением! Удалить внешний репозиторий Внешний репозиторий: Путь: @@ -301,6 +338,7 @@ Первое сравнение Игнорировать изменения пробелов СМЕСЬ + СРАВНЕНИЕ РЯДОМ СМАХИВАНИЕ Последнее сравнение @@ -319,13 +357,15 @@ Обмен Подсветка синтаксиса Перенос слов в строке - Разрешить навигацию по блокам Открыть в инструменте слияния Показывать все строки Меньше видимых строк Больше видимых строк ВЫБЕРИТЕ ФАЙЛ ДЛЯ ПРОСМОТРА ИЗМЕНЕНИЙ Каталог историй + Есть локальные изменения + Не соответствует с исходящим потоком + В актуальном состоянии Отклонить изменения Все локальные изменения в рабочей копии. Изменения: @@ -333,6 +373,11 @@ Включить неотслеживаемые файлы {0} изменений будут отменены Вы не можете отменить это действие!!! + Бросить ревизию + Ревизия: + Новая ГОЛОВА: + Править описание ветки + Цель: Закладка: Новое имя: Цель: @@ -347,6 +392,7 @@ Внешний репозиторий: Извлечь внешние изменения Не отслеживать + Пользовательское действие Отклонить... Отклонить {0} файлов... Взять версию ${0}$ @@ -402,6 +448,8 @@ Показывать только мои блокировки Блокировки LFS Разблокировать + Снять все мои блокировки + Вы уверены, что хотите снять все свои блокировки? Принудительно разблокировать Обрезать Запустить (git lfs prune), чтобы удалить старые файлы LFS из локального хранилища @@ -424,34 +472,35 @@ Удерживайте Ctrl или Shift, чтобы выбрать несколько ревизий. Удерживайте ⌘ или ⇧, чтобы выбрать несколько ревизий. ПОДСКАЗКИ: - Ссылка на сочетания клавиш - ОБЩЕЕ + Справка по сочетаниям клавиш + ГЛОБАЛЬНЫЕ Клонировать репозиторий - Закрыть вкладку + Закрыть текущую вкладку Перейти на следующую вкладку Перейти на предыдущую вкладку Создать новую вкладку Открыть диалоговое окно настроек - Переключить активное рабочее место - Переключить активную страницу + Показать рабочее пространство в выпадющем меню + Переключиться на вкладку РЕПОЗИТОРИЙ Зафиксировать сформированные изменения Зафиксировать и выложить сформированные изменения Сформировать все изменения и зафиксировать - Извлечение, запускается сразу + Извлечь (fetch), запускается сразу Режим доски (по умолчанию) + Открыть палитру команд Режим поиска ревизий - Загрузить, запускается сразу - Выложить, запускается сразу - Принудительно перезагрузить репозиторий + Загрузить (pull), запускается сразу + Выложить (push), запускается сразу + Принудительно перечитать репозиторий Переключить на «Изменения» - Переключить на «Истории» + Переключить на «Историю» Переключить на «Отложенные» ТЕКСТОВЫЙ РЕДАКТОР Закрыть панель поиска Найти следующее совпадение Найти предыдущее совпадение - Открыть с внешним инструментом сравнения/слияния + Открыть во внешнем инструменте сравнения/слияния Открыть панель поиска Отклонить Сформировать @@ -473,10 +522,12 @@ Целевая ветка: Копировать ссылку Открыть в браузере + Команды ОШИБКА УВЕДОМЛЕНИЕ + Открыть репозитории + Вкладки Рабочие места - Страницы Влить ветку Изменить сообщение слияния В: @@ -493,16 +544,20 @@ Выбрать группу для: Имя: Git НЕ был настроен. Пожалуйста, перейдите в [Настройки] и сначала настройте его. + Открыть + Редактор по умолчанию (Системный) Открыть каталог данных программы + Открыть файл Открыть в инструменте слияния - Окрыть с... Необязательно. - Создать новую страницу + Создать новую вкладку Закладка Закрыть вкладку Закрыть другие вкладки Закрыть вкладки справа Копировать путь репозитория + Переместить в рабочее пространство + Обновить Репозитории Вставить {0} дней назад @@ -515,7 +570,6 @@ {0} месяцев назад {0} лет назад Вчера - Используйте «Shift+Enter» для ввода новой строки. «Enter» - это горячая клавиша кнопки «OK» Параметры ОТКРЫТЬ ИИ Запрос на анализ сравнения @@ -523,6 +577,7 @@ Создать запрос на тему Модель Имя: + Введённое значение — это имя для загрузки API-ключа из ENV Сервер Разрешить потоковую передачу ВИД @@ -532,24 +587,32 @@ По умолчанию Редактор Моноширный шрифт - В текстовом редакторе используется только моноширный шрифт Тема Переопределение темы + Автоматически скрывать прокрутку Использовать фиксированную ширину табуляции в строке заголовка. Использовать системное окно ИНСТРУМЕНТ СРАВНЕНИЙ/СЛИЯНИЯ + Аргументы сравнения + Доступны переменные: $LOCAL, $REMOTE + Слить аргументы + Доступны переменные: $BASE, $LOCAL, $REMOTE, $MERGED Путь установки Введите путь для инструмента сравнения/слияния Инструмент ОСНОВНЫЕ Проверить обновления при старте Формат даты + Включить компактные каталоги в дереве изменений Язык Максимальная длина истории Показывать время автора вместо времени ревизии на графике + Показывать вкладку «ЛОКАЛЬНЫЕ ИЗМЕНЕНИЯ» по умолчанию + Показывать вкладку «Изменения» в сведении ревизии по умолчанию Показать наследника в деталях комментария Показывать метки на графике Длина темы ревизии + Создать Github-подобный аватар по умолчанию GIT Включить автозавершение CRLF Каталог клонирования по умолчанию @@ -574,6 +637,8 @@ Ключ GPG подписи пользователя ВНЕДРЕНИЕ ОБОЛОЧКА/ТЕРМИНАЛ + Аргументы + Используйте, пожалуйста, точку «.» для индикации рабочего каталога Путь Оболочка/Терминал Удалить внешний репозиторий @@ -585,8 +650,7 @@ В: Локальные изменения: Отклонить - Отложить и применить повторно - Обновить все подмодули + Отложить и применить повторно Внешний репозиторий: Загрузить (Получить и слить) Использовать перемещение вместо слияния @@ -606,6 +670,8 @@ Выложить на все внешние репозитории Внешний репозиторий: Метка: + Отправить к НОВОЙ ветке + Введитте имя для новой удалённой ветки: Выйти Перемещение текущей ветки Отложить и применить повторно локальные изменения @@ -617,6 +683,7 @@ Адрес: Адрес внешнего репозитория git Копировать адрес + Пользовательские действия Удалить... Редактировать... Извлечь @@ -642,6 +709,7 @@ ПРОДОЛЖИТЬ Изменить действия Не изменять действия + Панель Отклонить все изменения. Открыть в файловом менеджере Поиск веток, меток и подмодулей @@ -656,6 +724,7 @@ Дата ревизии Топологически ЛОКАЛЬНЫЕ ВЕТКИ + Больше опций... Навигация по ГОЛОВЕ (HEAD) Создать ветку ОЧИСТКА УВЕДОМЛЕНИЙ @@ -712,15 +781,16 @@ СОХРАНИТЬ Сохранить как... Заплатка успешно сохранена! - Сканирование репозиторий + Обнаружение репозиториев Корневой каталог: - Проверка для обновления... + Сканировать другой пользовательский каталог + Проверить обновления... Доступна новая версия программного обеспечения: Не удалось проверить наличие обновлений! Загрузка Пропустить эту версию Обновление ПО - В настоящее время обновления недоступны. + Сейчас нет обновлений. Установить ветку подмодуля Подмодуль: Текущий: @@ -732,8 +802,9 @@ Основная ветка: Копировать SHA Перейти - Втиснуть ревизии - В: + Втиснуть ГОЛОВУ (HEAD) в родительскую + Исправить ГОЛОВУ (HEAD) в родительском + В: Приватный ключ SSH: Путь хранения приватного ключа SSH ЗАПУСК @@ -762,7 +833,8 @@ РЕВИЗИИ: ПОДМОДУЛИ Добавить подмодули - Ветка + ВЕТКА + Ветка Каталог Удалить подмодуль Извлечение вложенных подмодулей @@ -782,10 +854,15 @@ Обновить URL-адрес ОК - Копировать имя метки - Копировать сообщение метки + РАЗМЕТЧИК + ВРЕМЯ + Сообщение + Имя + Разметчик + Копировать имя метки Пользовательское действие Удалить ${0}$... + Удалить выбранные метки ({0})... Влить ${0}$ в ${1}$... Выложить ${0}$... Обновление подмодулей @@ -802,17 +879,17 @@ Предупреждение Приветствие Создать группу - Создать подгруппу + Создать подгруппу... Клонировать репозиторий - Удалить + Удалить... ПОДДЕРЖИВАЕТСЯ: ПЕРЕТАСКИВАНИЕ КАТАЛОГОВ, ПОЛЬЗОВАТЕЛЬСКАЯ ГРУППИРОВКА. - Редактировать - Перейти в другую группу + Редактировать... + Переместить в другую группу... Открыть все репозитории Открыть репозиторий Открыть терминал - Повторное сканирование репозиториев в каталоге клонирования по умолчанию - Поиск репозиториев... + Обнаружить репозитории в каталоге клонирования по умолчанию + Найти репозиторий... Изменения Игнорировать Git Игнорировать все *{0} файлы @@ -821,6 +898,8 @@ Игнорировать только эти файлы Изменить Теперь вы можете сформировать этот файл. + Очистить историю + Вы действительно хотите очистить всю историю сообщений ревизии? Данное действие нельзя отменить. ЗАФИКСИРОВАТЬ ЗАФИКСИРОВАТЬ и ОТПРАВИТЬ Шаблон/Истории @@ -838,6 +917,7 @@ ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ НЕТ ПОСЛЕДНИХ ВХОДНЫХ СООБЩЕНИЙ НЕТ ШАБЛОНОВ РЕВИЗИИ + Не проверять Сбросить автора Завершение работы СФОРМИРОВАННЫЕ @@ -853,6 +933,7 @@ РАБОЧИЙ КАТАЛОГ Копировать путь Заблокировать + Открыть Удалить Разблокировать diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index 7604b3bd2..3dc6b1c30 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -41,7 +41,7 @@ புதுப்பி இருமம் கோப்பு ஆதரிக்கப்படவில்லை!!! குற்றச்சாட்டு - இந்த கோப்பில் குற்றம் சாட்ட ஆதரிக்கப்படவில்லை!!! + இந்த கோப்பில் குற்றம் சாட்ட ஆதரிக்கப்படவில்லை!!! ${0}$ சரிபார்... பணிமரத்துடன் ஒப்பிடுக கிளை பெயரை நகலெடு @@ -60,7 +60,6 @@ மறுபெயரிடு ${0}$... கண்காணிப்பு கிளையை அமை... கிளை ஒப்பிடு - தவறான மேல்ஓடை! விடு பெற்றோர் திருத்தத்திற்கு மீட்டமை இந்த திருத்தத்திற்கு மீட்டமை @@ -75,7 +74,7 @@ முன்னறிவிப்பு: ஒரு உறுதிமொழி சரிபார்பதன் மூலம், உங்கள் தலை பிரிக்கப்படும் உள்ளக மாற்றங்கள்: நிராகரி - பதுக்கிவை & மீண்டும் இடு + பதுக்கிவை & மீண்டும் இடு கிளை: கனி பறி உறுதிமொழி செய்திக்கு மூலத்தைச் சேர் @@ -126,8 +125,6 @@ குறிகள் பாகொவ உலாவியில் திற - விளக்கம் - உறுதிமொழி பொருளை உள்ளிடவும் களஞ்சியம் உள்ளமை உறுதிமொழி வளர்புரு வார்ப்புரு உள்ளடக்கம்: @@ -187,10 +184,9 @@ உருவாக்கப்பட்ட கிளையைப் சரிபார் உள்ளக மாற்றங்கள்: நிராகரி - பதுக்கிவை & மீண்டும் இடு + பதுக்கிவை & மீண்டும் இடு புதிய கிளை பெயர்: கிளை பெயரை உள்ளிடவும். - இடைவெளிகள் கோடுகளால் மாற்றப்படும். உள்ளக கிளையை உருவாக்கு குறிச்சொல்லை உருவாக்கு... இங்கு புதிய குறிச்சொல்: @@ -242,7 +238,6 @@ இடமாற்று தொடரியல் சிறப்பம்சமாக்கல் வரி சொல் மடக்கு - தடுப்பு-வழிசெலுத்தலை இயக்கு ஒன்றிணை கருவியில் திற அனைத்து வரிகளையும் காட்டு தெரியும் வரிகளின் எண்ணிக்கையைக் குறை @@ -402,7 +397,6 @@ அறிவிலி உள்ளமைக்கப்படவில்லை. [விருப்பத்தேர்வுகள்]க்குச் சென்று முதலில் அதை உள்ளமை. தரவு சேமிப்பக கோப்பகத்தைத் திற ஒன்றிணை கருவியில் திற - இதனுடன் திற... விருப்பத்தேர்வு. புதிய பக்கத்தை உருவாக்கு புத்தகக்குறி @@ -422,7 +416,6 @@ {0} திங்களுக்கு முன்பு {0} ஆண்டுகளுக்கு முன்பு நேற்று - புதிய வரியை உள்ளிட 'உயர்த்து+நுழை' ஐப் பயன்படுத்தவும். 'நுழை' என்பது சரி பொத்தானின் சூடானவிசை ஆகும் விருப்பத்தேர்வுகள் செநு வேறுபாடு உடனடியாக பகுப்பாய்வு செய் @@ -439,7 +432,6 @@ இயல்புநிலை திருத்தி ஒற்றைவெளி எழுத்துரு - ஒற்றைவெளி எழுத்துருவை உரை திருத்தியில் மட்டும் பயன்படுத்து கருப்பொருள் கருப்பொருள் மேலெழுதப்படுகிறது தலைப்புப்பட்டியில் நிலையான தாவல் அகலத்தைப் பயன்படுத்து @@ -490,7 +482,7 @@ இதனுள்: உள்ளக மாற்றங்கள்: நிராகரி - பதுக்கிவை & மீண்டும் இடு + பதுக்கிவை & மீண்டும் இடு தொலை: இழு (எடுத்து ஒன்றிணை) ஒன்றிணை என்பதற்குப் பதிலாக மறுதளத்தைப் பயன்படுத்து @@ -610,8 +602,6 @@ மேல்ஓடை: SHA ஐ நகலெடு இதற்கு செல் - நொறுக்கு உறுதிமொழிகள் - இதில்: பாஓடு தனியார் திறவுகோல்: தனியார் பாஓடு திறவுகோல் கடை பாதை தொடங்கு @@ -645,8 +635,6 @@ இந்த தொகுதியை சேமிப்பதற்கான தொடர்புடைய கோப்புறை. துணை தொகுதியை நீக்கு சரி - குறிச்சொல் பெயரை நகலெடு - குறிச்சொல் செய்தியை நகலெடு நீக்கு ${0}$... ${0}$ இதை ${1}$ இல் இணை... தள்ளு ${0}$... diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index b4b4ccbfd..fa15b7c4b 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -41,7 +41,7 @@ Оновити БІНАРНИЙ ФАЙЛ НЕ ПІДТРИМУЄТЬСЯ!!! Автор рядка - ПОШУК АВТОРА РЯДКА ДЛЯ ЦЬОГО ФАЙЛУ НЕ ПІДТРИМУЄТЬСЯ!!! + ПОШУК АВТОРА РЯДКА ДЛЯ ЦЬОГО ФАЙЛУ НЕ ПІДТРИМУЄТЬСЯ!!! Перейти на ${0}$... Порівняти з ${0}$ Порівняти з робочим деревом @@ -61,7 +61,6 @@ Перейменувати ${0}$... Встановити відстежувану гілку... Порівняти гілки - Недійсний upstream! СКАСУВАТИ Скинути до батьківської ревізії Скинути до цієї ревізії @@ -76,7 +75,7 @@ Попередження: Перехід на коміт призведе до стану "від'єднаний HEAD" Локальні зміни: Скасувати - Сховати та Застосувати + Сховати та Застосувати Гілка: Cherry-pick Додати джерело до повідомлення коміту @@ -127,8 +126,6 @@ ПОСИЛАННЯ (Refs) SHA Відкрити в браузері - Опис - Введіть тему коміту Налаштування сховища ШАБЛОН КОМІТУ Зміст шаблону: @@ -192,10 +189,9 @@ Перейти на створену гілку Локальні зміни: Скасувати - Сховати та Застосувати + Сховати та Застосувати Назва нової гілки: Введіть назву гілки. - Пробіли будуть замінені на тире. Створити локальну гілку Створити тег... Новий тег для: @@ -247,7 +243,6 @@ Поміняти місцями Підсвітка синтаксису Перенос слів - Увімкнути навігацію блоками Відкрити в інструменті злиття Показати всі рядки Зменшити кількість видимих рядків @@ -407,7 +402,6 @@ Git не налаштовано. Будь ласка, перейдіть до [Налаштування] та налаштуйте його. Відкрити теку зберігання даних Відкрити в інструменті злиття - Відкрити за допомогою... Необов'язково. Створити нову вкладку Закладка @@ -427,7 +421,6 @@ {0} місяців тому {0} років тому Вчора - Використовуйте 'Shift+Enter' для введення нового рядка. 'Enter' - гаряча клавіша кнопки OK Налаштування AI Промпт для аналізу різниці @@ -444,7 +437,6 @@ За замовчуванням Редактор Моноширинний шрифт - Використовувати моноширинний шрифт лише в текстовому редакторі Тема Перевизначення теми Використовувати фіксовану ширину вкладки в заголовку @@ -495,7 +487,7 @@ В: Локальні зміни: Скасувати - Сховати та Застосувати + Сховати та Застосувати Віддалене сховище: Pull (Fetch & Merge) Використовувати rebase замість merge @@ -615,8 +607,6 @@ Upstream: Копіювати SHA Перейти до - Squash (Склеїти коміти) - В: Приватний ключ SSH: Шлях до сховища приватного ключа SSH ПОЧАТИ @@ -650,8 +640,6 @@ Відносна тека для зберігання цього модуля. Видалити підмодуль OK - Копіювати назву тегу - Копіювати повідомлення тегу Видалити ${0}$... Злиття ${0}$ в ${1}$... Надіслати ${0}$... diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 1f024429f..bab627b7f 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -5,6 +5,7 @@ 关于软件 关于本软件 + 浏览版本更新说明 开源免费的Git客户端 新增忽略文件 匹配模式 : @@ -23,6 +24,8 @@ 重新生成 使用AI助手生成提交信息 应用本次生成 + 隐藏 SourceGit + 显示所有窗口 应用补丁(apply) 补丁文件 : 选择补丁文件 @@ -53,14 +56,18 @@ 无法判定 二分定位进行中。请标记当前的提交是 '正确' 还是 '错误',然后检出另一个提交。 逐行追溯(blame) - 选中文件不支持该操作!!! + 对当前版本的前一版本执行逐行追溯操作 + 选中文件不支持该操作!!! 检出(checkout) ${0}$... 与当前 ${0}$ 比较 与本地工作树比较 复制分支名 + 创建合并请求 ... + 为上游分支 ${0}$ 创建合并请求 ... 自定义操作 删除 ${0}$... 删除选中的 {0} 个分支 + 编辑 ${0}$ 的描述... 快进(fast-forward)到 ${0}$ 拉取(fetch) ${0}$ 至 ${1}$... GIT工作流 - 完成 ${0}$ @@ -72,13 +79,18 @@ 变基(rebase) ${0}$ 至 ${1}$... 重命名 ${0}$... 重置 ${0}$ 到 ${1}$... + 切换到 ${0}$ (工作树) 切换上游分支 ... 分支比较 - 本地 + 领先 {0} 个提交 + 领先 {0} 个提交,落后 {1} 个提交 + 落后 {0} 个提交 + 不存在 远程 + 状态 上游分支 远程地址 - 跟踪的上游分支不存在或已删除! + 工作树 取 消 重置文件到上一版本 重置文件到该版本 @@ -96,8 +108,7 @@ 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 未提交更改 : 丢弃更改 - 贮藏并自动恢复 - 同时更新所有子模块 + 贮藏并自动恢复 目标分支 : 您当前游离的HEAD包含未被任何分支及标签引用的提交!是否继续? 检出分支并快进 @@ -131,6 +142,7 @@ 提交指纹 主题 自定义操作 + 丢弃此提交 交互式变基(rebase -i) 丢弃... 编辑... @@ -147,6 +159,7 @@ 编辑提交信息 另存为补丁 ... 合并此提交到上一个提交 + 修复至父提交 变更对比 个文件发生变更 查找变更... @@ -160,6 +173,9 @@ 提交者 查看包含此提交的分支/标签 本提交已被以下分支/标签包含 + 复制电子邮箱 + 复制用户名 + 复制用户名及邮箱 仅显示前100项变更。请前往【变更对比】页面查看全部。 签名密钥 : 提交信息 @@ -168,25 +184,41 @@ 提交指纹 签名者 : 浏览器中查看 - 详细描述 + 请输入提交的信息。注意:主题与具体描述中间需要空白行分隔! 主题 - 填写提交信息主题 仓库配置 提交信息模板 - 您可使用 ${files_num}, ${branch_name}, ${files} 或 ${files:N}(N表示最大显示的文件数) + 内置变量: + + ${branch_name} 当前分支名 + ${files_num} 变更文件数量 + ${files} 变更文件路径列表 + ${files:N} 变更文件路径列表(仅输出指定 N 条) + ${pure_files} 与 ${files} 类似,但仅输出文件名 + ${pure_files:N} 与 ${files:N} 类似,但仅输出文件名 模板内容 : 模板名 : 自定义操作 命令行参数 : - 内置变量:${REPO} 仓库路径、${BRANCH} 选中的分支、${SHA} 选中的提交哈希,${TAG} 选中的标签 + 内置变量: + + ${REPO} 仓库路径 + ${REMOTE} 选中的远程仓库或选中分支所属的远程仓库 + ${BRANCH} 选中的分支,对于远程分支不包含远程名 + ${BRANCH_FRIENDLY_NAME} 选中的分支,对于远程分支包含远程名 + ${SHA} 选中的提交哈希 + ${TAG} 选中的标签 + ${FILE} 选中的文件 + $1, $2 ... 输入控件中填写的值 可执行文件路径 : 输入控件 : 编辑 - 请在命令行参数中使用 $1, $2 等占位符表示输入控件的值 名称 : 作用目标 : 选中的分支 选中的提交 + 选中的文件 + 远程仓库 仓库 选中的标签 等待操作执行完成 @@ -195,10 +227,12 @@ GIT配置 启用定时自动拉取远程更新 分钟 + 自定义规范化提交类型 默认远程 默认合并方式 ISSUE追踪 新增匹配Azure DevOps规则 + 新增匹配Gerrit Change-Id规则 新增匹配Gitee议题规则 新增匹配Gitee合并请求规则 新增匹配GitHub Issue规则 @@ -227,6 +261,7 @@ 名称 : 选项列表 : 选项之间请使用英文 '|' 作为分隔符 + 内置变量 ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, ${FILE} 与 ${TAG} 在这里仍然可用 类型 : 工作区 颜色 @@ -254,10 +289,9 @@ 完成后切换到新分支 未提交更改 : 丢弃更改 - 贮藏并自动恢复 + 贮藏并自动恢复 新分支名 : 填写分支名称。 - 空格将被替换为'-'符号 创建本地分支 允许重置已存在的分支 新建标签 ... @@ -283,6 +317,9 @@ 同时删除远程分支 ${0}$ 删除多个分支 您正在尝试一次性删除多个分支,请务必仔细检查后再执行操作! + 删除多个标签 + 同时在远程仓库中删除 + 您正在尝试一次性删除多个标签,请务必仔细检查后再执行操作! 删除远程确认 远程名 : 路径 : @@ -301,6 +338,7 @@ 首个差异 忽略空白符号变化 混合对比 + 差异比较 分列对比 填充对比 最后一个差异 @@ -319,13 +357,15 @@ 交换比对双方 语法高亮 自动换行 - 启用基于变更块的跳转 使用外部合并工具查看 显示完整文件 减少可见的行数 增加可见的行数 请选择需要对比的文件 目录内容变更历史 + 未提交的本地变更 + 当前分支HEAD与远端不一致 + 已是最新 放弃更改确认 所有本仓库未提交的修改。 变更 : @@ -333,6 +373,11 @@ 包括未跟踪的文件 总计{0}项选中更改 本操作不支持回退,请确认后继续!!! + 丢弃提交 + 提交 : + 丢弃后 HEAD : + 编辑分支描述 + 目标 : 书签 : 名称 : 目标 : @@ -347,6 +392,7 @@ 远程仓库 : 拉取远程仓库内容 不跟踪此文件的更改 + 自定义操作 放弃更改... 放弃 {0} 个文件的更改... 应用 ${0}$ @@ -402,6 +448,8 @@ 仅显示被我锁定的文件 LFS对象锁状态 解锁 + 解锁所有被我锁定的文件 + 确定要解锁所有被您锁定的文件吗? 强制解锁 精简本地LFS对象存储 运行`git lfs prune`命令,从本地存储中精简当前版本不需要的LFS对象 @@ -432,7 +480,7 @@ 切换到上一个页面 新建页面 打开偏好设置面板 - 切换工作区 + 显示工作区下拉菜单 切换显示页面 仓库页面快捷键 提交暂存区更改 @@ -440,6 +488,7 @@ 自动暂存全部变更并提交 拉取 (fetch) 远程变更 切换左边栏为分支/标签等显示模式(默认) + 打开快捷命令面板 切换左边栏为提交搜索模式 拉回 (pull) 远程变更 推送本地变更到远程 @@ -473,10 +522,12 @@ 目标分支 : 复制链接地址 在浏览器中访问 + 命令列表 出错了 系统提示 - 工作区列表 + 打开其他仓库 页面列表 + 工作区列表 合并分支 编辑合并信息 目标分支 : @@ -493,9 +544,11 @@ 请选择目标分组: 名称 : GIT尚未配置。请打开【偏好设置】配置GIT路径。 + 打开 + 系统默认编辑器 浏览应用数据目录 + 打开文件 使用外部对比工具查看 - 打开文件... 选填。 新建空白页 设置书签 @@ -503,6 +556,8 @@ 关闭其他标签页 关闭右侧标签页 复制仓库路径 + 移至工作区 + 刷新 新标签页 粘贴 {0}天前 @@ -515,7 +570,6 @@ {0}个月前 {0}年前 昨天 - 请使用Shift+Enter换行。Enter键已被【确 定】按钮占用。 偏好设置 AI Analyze Diff Prompt @@ -523,6 +577,7 @@ Generate Subject Prompt 模型 配置名称 + 从环境变量(填写环境变量名)中读取API密钥 服务地址 启用流式输出 外观配置 @@ -532,24 +587,32 @@ 默认 代码编辑器 等宽字体 - 仅在文本编辑器中使用等宽字体 主题 主题自定义 + 允许滚动条自动隐藏 主标签使用固定宽度 使用系统默认窗体样式 对比/合并工具 + 对比命令参数 + 可用参数:$LOCAL, $REMOTE + 合并命令参数 + 可用参数:$BASE, $LOCAL, $REMOTE, $MERGED 安装路径 填写工具可执行文件所在位置 工具 通用配置 启动时检测软件更新 日期时间格式 + 在变更列表树中启用紧凑文件夹模式 显示语言 最大历史提交数 在提交路线图中显示修改时间而非提交时间 + 默认显示【本地更改】页 + 在提交详情页默认打开【变更对比】标签页 在提交详情页中显示子提交列表 在提交路线图中显示标签 SUBJECT字数检测 + 生成GitHub风格的默认头像 GIT配置 自动换行转换 默认克隆路径 @@ -574,6 +637,8 @@ 输入签名提交所使用的KEY 第三方工具集成 终端/SHELL + 启动参数 + 请使用 '.' 来指定工作目录 安装路径 终端/SHELL 清理远程已删除分支 @@ -585,8 +650,7 @@ 本地分支 : 未提交更改 : 丢弃更改 - 贮藏并自动恢复 - 同时更新所有子模块 + 贮藏并自动恢复 远程 : 拉回(拉取并合并) 使用变基方式合并分支 @@ -606,6 +670,8 @@ 推送到所有远程仓库 远程仓库 : 标签 : + 推送到新的分支 + 输入新的远端分支名 退出 变基(rebase)操作 自动贮藏并恢复本地变更 @@ -617,6 +683,7 @@ 仓库地址 : 远程仓库的地址 复制远程地址 + 自定义操作 删除 ... 编辑 ... 拉取(fetch)更新 @@ -642,6 +709,7 @@ 下一步 自定义操作 自定义操作未设置 + 主页 放弃所有更改 在文件浏览器中打开 快速查找分支/标签/子模块 @@ -656,6 +724,7 @@ 按提交时间 按拓扑排序 本地分支 + 更多选项... 定位HEAD 新建分支 清空通知列表 @@ -714,6 +783,7 @@ 补丁已成功保存! 扫描仓库 根路径 : + 扫描其他自定义路径 检测更新... 检测到软件有版本更新: 获取最新版本信息失败! @@ -732,8 +802,9 @@ 上游分支 : 复制提交指纹 跳转到提交 - 压缩为单个提交 - 合并入: + 合并修改至父提交 + 修复至父提交 + 父提交: SSH密钥 : SSH密钥文件 开 始 @@ -763,6 +834,7 @@ 子模块 添加子模块 跟踪分支 + 跟踪分支 相对路径 取消初始化 拉取子孙模块 @@ -782,10 +854,15 @@ 更新 仓库 确 定 - 复制标签名 - 复制标签信息 + 创建者 + 创建时间 + 标签信息 + 标签名 + 创建者 + 复制标签名 自定义操作 删除 ${0}$... + 删除选中 {0} 个标签... 合并 ${0}$ 到 ${1}$... 推送 ${0}$... 更新子模块 @@ -821,6 +898,8 @@ 忽略本文件 修补 现在您已可将其加入暂存区中 + 清空历史提交信息 + 您确定要清空所有的历史提交信息记录吗(执行操作后无法撤回)? 提交 提交并推送 历史输入/模板 @@ -838,6 +917,7 @@ 显示未跟踪文件 没有提交信息记录 没有可应用的提交信息模板 + 跳过GIT钩子 重置提交者 署名 已暂存 @@ -853,6 +933,7 @@ 本地工作树 复制工作树路径 锁定工作树 + 打开工作树 移除工作树 解除工作树锁定 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index dee236eb3..a2a4443c8 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -5,10 +5,11 @@ 關於 關於 SourceGit + 版本說明 開源免費的 Git 客戶端 新增忽略檔案 - 匹配模式 : - 儲存路徑 : + 比對模式: + 儲存路徑: 新增工作區 工作區路徑: 填寫該工作區的路徑。支援相對路徑。 @@ -23,6 +24,8 @@ 重新產生 使用 AI 產生提交訊息 套用為提交訊息 + 隱藏 SourceGit + 顯示所有 套用修補檔 (apply patch) 修補檔: 選擇修補檔 @@ -53,14 +56,18 @@ 無法確認 二分搜尋進行中。請標記目前的提交為「良好」或「錯誤」,然後簽出另一個提交。 逐行溯源 (blame) - 所選擇的檔案不支援該操作! + 對上一個版本執行逐行溯源 + 所選擇的檔案不支援該操作! 簽出 (checkout) ${0}$... 與目前 ${0}$ 比較 與本機工作區比較 複製分支名稱 + 建立拉取請求... + 為上游分支 ${0}$ 建立拉取請求... 自訂動作 刪除 ${0}$... 刪除所選的 {0} 個分支 + 編輯 ${0}$ 的描述... 快轉 (fast-forward) 到 ${0}$ 提取 (fetch) ${0}$ 到 ${1}$... Git 工作流 - 完成 ${0}$ @@ -72,13 +79,18 @@ 重定基底 (rebase) ${0}$ 分支至 ${1}$... 重新命名 ${0}$... 重設 ${0}$ 至 ${1}$... + 切換到 ${0}$ (工作區) 切換上游分支... 分支比較 - 本機 + 領先 {0} 次提交 + 領先 {0} 次提交,落後 {0} 次提交 + 落後 {0} 次提交 + 無效 遠端 + 狀態 上游分支 遠端網址 - 追蹤上游分支不存在或已刪除! + 工作區 取 消 重設檔案到上一版本 重設檔案為此版本 @@ -96,12 +108,11 @@ 注意: 執行該操作後,目前 HEAD 會變為分離 (detached) 狀態! 未提交變更: 捨棄變更 - 擱置變更並自動復原 - 同時更新所有子模組 + 擱置變更並自動復原 目標分支: - 您目前的分離的 HEAD 包含與任何分支/標籤無關的提交!您要繼續嗎? + 您目前的分離的 HEAD 包含與任何分支/標籤無關的提交! 您要繼續嗎? 簽出分支並快轉 - 上游分支 : + 上游分支: 揀選提交 提交資訊中追加來源資訊 提交列表: @@ -115,7 +126,7 @@ 其他複製參數,選填。 本機存放庫名稱: 本機存放庫目錄的名稱,選填。 - 父級目錄: + 上層目錄: 初始化並更新子模組 遠端存放庫: 關閉 @@ -131,6 +142,7 @@ 提交編號 標題 自訂動作 + 捨棄此提交 互動式重定基底 (rebase -i) 捨棄... 編輯... @@ -147,6 +159,7 @@ 編輯提交訊息 另存為修補檔 (patch)... 合併此提交到上一個提交 + 修正至父提交 變更對比 個檔案已變更 搜尋變更... @@ -160,33 +173,52 @@ 提交者 檢視包含此提交的分支或標籤 本提交包含於以下分支或標籤 + 複製電子郵件 + 複製名稱 + 複製名稱及電子郵件 僅顯示前 100 項變更。請前往 [變更對比] 頁面以瀏覽所有變更。 - 簽名鑰匙: + 簽章金鑰: 提交訊息 前次提交 相關參照 提交編號 簽署人: 在瀏覽器中檢視 - 詳細描述 + 請輸入提交訊息,標題與詳細描述之間請使用單行空白區隔。 標題 - 填寫提交訊息標題 存放庫設定 提交訊息範本 - 您可以使用 ${files_num}、${branch_name}、${files} 或 ${files:N} 其中 N 是要輸出的檔案路徑的最大數目。 + 內建參數: + + ${branch_name} 目前分支名稱 + ${files_num} 已變更檔案數 + ${files} 已變更檔案路徑清單 + ${files:N} 已變更檔案路徑清單 (僅列出前 N 個) + ${pure_files} 類似 ${files},不含資料夾的純檔案名稱 + ${pure_files:N} 類似 ${files:N},不含資料夾的純檔案名稱 範本內容: 範本名稱: 自訂動作 指令參數: - 內建參數: ${REPO} 存放庫路徑、${BRANCH} 所選的分支、${SHA} 所選的提交編號、${TAG} 所選的標籤 + 內建參數: + + ${REPO} 存放庫路徑 + ${REMOTE} 所選的遠端存放庫或所選分支的遠端 + ${BRANCH} 所選的分支。對於遠端分支,不包含遠端名稱 + ${BRANCH_FRIENDLY_NAME} 所選的分支。對於遠端分支,不包含遠端名稱 + ${SHA} 所選的提交編號 + ${TAG} 所選的標籤 + ${FILE} 所選的檔案,相對於存放庫根目錄的路徑 + $1, $2 ... 輸入控制項中的值 可執行檔案路徑: - 輸入控件: + 輸入控制項: 編輯 - 請使用占位符如 $1, $2 來代表輸入控制項的值 名稱: 執行範圍: 選取的分支 選取的提交 + 選取的檔案 + 遠端存放庫 存放庫 選取的標籤 等待自訂動作執行結束 @@ -195,10 +227,12 @@ Git 設定 啟用定時自動提取 (fetch) 遠端更新 分鐘 + 自訂約定式提交類型 預設遠端存放庫 預設合併模式 Issue 追蹤 新增符合 Azure DevOps 規則 + 新增符合 Gerrit Change-Id 規則 新增符合 Gitee 議題規則 新增符合 Gitee 合併請求規則 新增符合 GitHub Issue 規則 @@ -208,7 +242,7 @@ 新增自訂規則 符合 Issue 的正規表達式: 規則名稱: - 寫入 .issuetracker 檔案以分享此規則 + 寫入 .issuetracker 檔案以共用此規則 為 Issue 產生的網址連結: 可在網址中使用 $1、$2 等變數填入正規表達式相符的內容 AI @@ -218,15 +252,16 @@ HTTP 網路代理 使用者名稱 用於本存放庫的使用者名稱 - 編輯自訂動作輸入控件 - 啟用時的指令行參數: - 勾選 CheckBox 後,此值將用於命令列參數中 + 編輯自訂動作輸入控制項 + 啟用時的指令參數: + 勾選 CheckBox 後,此值將用於指令參數中 描述: 預設值: 目標路徑是否為資料夾: 名稱: 選項列表: 請使用英文「|」符號分隔選項 + 內建變數 ${REPO}、${REMOTE}、${BRANCH}、${BRANCH_FRIENDLY_NAME}、${SHA}、${FILE} 及 ${TAG} 在此處仍可使用 類型: 工作區 顏色 @@ -237,7 +272,7 @@ 自動暫存並提交 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存全部變更並提交? 系統提示 - 您需要重新啟動此應用程式才能套用變更! + 您需要重新啟動此應用程式才能套用變更! 產生約定式提交訊息 破壞性變更: 關閉的 Issue: @@ -254,10 +289,9 @@ 完成後切換到新分支 未提交變更: 捨棄變更 - 擱置變更並自動復原 + 擱置變更並自動復原 新分支名稱: 輸入分支名稱。 - 空格將以英文破折號取代 建立本機分支 允許覆寫現有分支 新增標籤... @@ -275,14 +309,17 @@ 按住 Ctrl 鍵將直接以預設參數執行 剪下 取消初始化子模組 - 強制取消,即使它包含本地變更 - 子模組 : + 強制取消,即使包含本機變更 + 子模組: 刪除分支確認 分支名稱: 您正在刪除遠端上的分支,請務必小心! 同時刪除遠端分支 ${0}$ 刪除多個分支 您正在嘗試一次性刪除多個分支,請務必仔細檢查後再刪除! + 刪除多個標籤 + 同時刪除遠端存放庫中的這些標籤 + 您正在嘗試一次性刪除多個標籤,請務必仔細檢查後再刪除! 刪除遠端確認 遠端名稱: 路徑: @@ -301,8 +338,9 @@ 第一個差異 忽略空白符號變化 混合對比 + 差異對比 並排對比 - 填充對比 + 滑桿對比 最後一個差異 LFS 物件變更 變更後 @@ -319,13 +357,15 @@ 交換比對雙方 語法上色 自動換行 - 區塊切換上/下一個差異 使用外部合併工具檢視 顯示檔案的全部內容 減少可見的行數 增加可見的行數 請選擇需要對比的檔案 目錄内容變更歷史 + 未提交的本機變更 + 目前分支 HEAD 與上游不相符 + 已更新至最新 捨棄變更 所有本機未提交的變更。 變更: @@ -333,6 +373,11 @@ 包含未追蹤檔案 將捨棄總計 {0} 項已選取的變更 您無法復原此操作,請確認後再繼續! + 捨棄提交 + 提交: + 捨棄後新的 HEAD: + 編輯分支的描述 + 目標: 書籤: 名稱: 目標: @@ -347,6 +392,7 @@ 遠端存放庫: 提取遠端存放庫內容 不追蹤此檔案的變更 + 自訂動作 捨棄變更... 捨棄已選的 {0} 個檔案變更... 使用 ${0}$ @@ -402,6 +448,8 @@ 僅顯示被我鎖定的檔案 LFS 物件鎖 解鎖 + 解鎖所有由我鎖定的檔案 + 您確定要解鎖所有由您自己鎖定的檔案嗎? 強制解鎖 清理 (prune) 執行 `git lfs prune` 以從本機中清理目前版本不需要的 LFS 物件 @@ -432,7 +480,7 @@ 切換到上一個頁面 新增頁面 開啟偏好設定面板 - 切換工作區 + 顯示工作區的下拉式選單 切換目前頁面 存放庫頁面快速鍵 提交暫存區變更 @@ -440,6 +488,7 @@ 自動暫存全部變更並提交 提取 (fetch) 遠端的變更 切換左邊欄為分支/標籤等顯示模式 (預設) + 開啟命令面板 切換左邊欄為歷史搜尋模式 拉取 (pull) 遠端的變更 推送 (push) 本機變更到遠端存放庫 @@ -473,10 +522,12 @@ 目標分支: 複製連結 在瀏覽器中開啟連結 + 命令列表 發生錯誤 系統提示 - 工作區列表 + 開啟存放庫 頁面列表 + 工作區列表 合併分支 編輯合併訊息 目標分支: @@ -493,9 +544,11 @@ 請選擇目標分組: 名稱: 尚未設定 Git。請開啟 [偏好設定] 以設定 Git 路徑。 + 開啟 + 系統預設編輯器 瀏覽程式資料目錄 + 開啟檔案 使用外部比對工具檢視 - 開啟檔案... 選填。 新增分頁 設定書籤 @@ -503,6 +556,8 @@ 關閉其他分頁 關閉右側分頁 複製存放庫路徑 + 移至工作區 + 重新整理 新分頁 貼上 {0} 天前 @@ -515,7 +570,6 @@ {0} 個月前 {0} 年前 昨天 - 請使用 Shift + Enter 換行。Enter 鍵已被 [確定] 按鈕佔用。 偏好設定 AI 分析變更差異提示詞 @@ -523,6 +577,7 @@ 產生提交訊息提示詞 模型 名稱 + 從環境變數中 (輸入環境變數名稱) 讀取 API 金鑰 伺服器 啟用串流輸出 外觀設定 @@ -532,31 +587,39 @@ 預設 程式碼 等寬字型 - 僅在文字編輯器中使用等寬字型 佈景主題 自訂主題 + 允許自動隱藏捲軸 使用固定寬度的分頁標籤 使用系統原生預設視窗樣式 對比/合併工具 + 對比命令參數 + 可用參數:$LOCAL, $REMOTE + 合併命令參數 + 可用參數:$BASE, $LOCAL, $REMOTE, $MERGED 安裝路徑 填寫可執行檔案所在路徑 工具 一般設定 啟動時檢查軟體更新 日期時間格式 + 在樹狀變更目錄中啟用密集資料夾模式 顯示語言 最大歷史提交數 在提交路線圖中顯示修改時間而非提交時間 + 預設顯示 [本機變更] 頁面 + 在提交詳細資訊頁面預設顯示 [變更對比] 在提交詳細資訊中顯示後續提交 在路線圖中顯示標籤 提交標題字數偵測 + 產生 GitHub 風格的預設頭貼 Git 設定 自動換行轉換 預設複製 (clone) 路徑 電子郵件 預設 Git 使用者電子郵件 拉取變更時進行清理 (--prune) - 對比檔案時,預設忽略行尾的 CR 變更 (--ignore-cr-at-eol) + 對比檔案時,預設忽略行末的 CR 變更 (--ignore-cr-at-eol) 本軟體要求 Git 最低版本為 2.25.1 安裝路徑 啟用 HTTP SSL 驗證 @@ -574,6 +637,8 @@ 填寫簽章提交所使用的金鑰 第三方工具整合 終端機/Shell + 啟動參數 + 請使用「.」標示當前工作目錄 安裝路徑 終端機/Shell 清理遠端已刪除分支 @@ -585,8 +650,7 @@ 本機分支: 未提交變更: 捨棄變更 - 擱置變更並自動復原 - 同時更新所有子模組 + 擱置變更並自動復原 遠端: 拉取 (提取並合併) 使用重定基底 (rebase) 合併分支 @@ -606,6 +670,8 @@ 推送到所有遠端存放庫 遠端存放庫: 標籤: + 推送到新的分支 + 輸入新的遠端分支名稱: 結束 重定基底 (rebase) 操作 自動擱置變更並復原本機變更 @@ -617,6 +683,7 @@ 存放庫網址: 遠端存放庫的網址 複製遠端網址 + 自訂動作 刪除... 編輯... 提取 (fetch) 更新 @@ -635,13 +702,14 @@ 依建立時間 依名稱升序 清理本存放庫 (GC) - 本操作將執行 `git gc` 命令。 + 本操作將執行 `git gc` 指令。 清空篩選規則 清空 設定本存放庫 下一步 自訂動作 沒有自訂的動作 + 首頁 捨棄所有變更 在檔案瀏覽器中開啟 快速搜尋分支/標籤/子模組 @@ -656,6 +724,7 @@ 依時間排序 依拓撲排序 本機分支 + 更多選項... 回到 HEAD 新增分支 清除所有通知 @@ -675,7 +744,7 @@ 只顯示分支和標籤引用的提交 只顯示合併提交的第一父提交 顯示內容 - 顯示遺失參照的提交 + 顯示失去參照的提交 以樹型結構展示 以樹型結構展示 跳過此提交 @@ -700,9 +769,9 @@ 重設模式: 移至提交: 目前分支: - 重設選取的分支(非目前分支) - 重設位置 : - 選取分支 : + 重設選取的分支 (非目前分支) + 重設位置: + 選取分支: 在檔案瀏覽器中檢視 復原操作確認 目標提交: @@ -714,6 +783,7 @@ 修補檔已成功儲存! 掃描存放庫 頂層目錄: + 掃描其他自訂目錄 檢查更新... 軟體有版本更新: 取得最新版本資訊失敗! @@ -725,15 +795,16 @@ 子模組: 目前追蹤分支: 變更為: - 選購。為空時設定為預設值。 + 選填。留空時設定為預設值。 切換上游分支 本機分支: 取消設定上游分支 上游分支: 複製提交編號 前往此提交 - 壓縮為單個提交 - 合併入: + 合併變更入父提交 + 修正至父提交 + 父提交: SSH 金鑰: SSH 金鑰檔案 開 始 @@ -763,6 +834,7 @@ 子模組 新增子模組 追蹤分支 + 追蹤分支 相對路徑 取消初始化 提取子模組 @@ -782,15 +854,20 @@ 更新 存放庫 確 定 - 複製標籤名稱 - 複製標籤訊息 + 建立者 + 建立時間 + 標籤訊息 + 標籤名稱 + 建立者 + 複製標籤名稱 自訂動作 刪除 ${0}$... + 刪除所選的 {0} 個標籤... 合併 ${0}$ 到 ${1}$... 推送 ${0}$... 更新子模組 更新所有子模組 - 如果子模組尚未初始化,則初始化它 + 如果子模組尚未初始化,則將其初始化 遞迴更新子孫模組 子模組: 更新至子模組的遠端追蹤分支 @@ -821,6 +898,8 @@ 忽略本檔案 修補 現在您已可將其加入暫存區中 + 清除提交訊息歷史 + 您確定要清除所有提交訊息記錄嗎 (執行後無法復原)? 提 交 提交並推送 歷史輸入/範本 @@ -838,6 +917,7 @@ 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 + 繞過 Hooks 檢查 重設作者 署名 已暫存 @@ -848,11 +928,12 @@ 暫存所有檔案 檢視不追蹤變更的檔案 範本: ${0}$ - 工作區: + 工作區: 設定工作區... 本機工作區 複製工作區路徑 鎖定工作區 + 開啟工作區 移除工作區 解除鎖定工作區 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index e1ebf0061..f259fd32d 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -5,6 +5,7 @@ xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" xmlns:ae="using:AvaloniaEdit" + xmlns:acc="using:AvaloniaEdit.CodeCompletion" xmlns:aee="using:AvaloniaEdit.Editing" xmlns:aes="using:AvaloniaEdit.Search"> @@ -12,35 +13,39 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + - @@ -519,72 +706,19 @@ - - - - - - - - + + + + @@ -1221,6 +1363,9 @@ + @@ -1272,13 +1417,14 @@ Fill="{DynamicResource Brush.FG1}" HorizontalAlignment="Center" VerticalAlignment="Center" - Opacity="0.65"/> + Opacity="0.8"/> + + + + diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml index 3b4637331..84b89f43b 100644 --- a/src/Resources/Themes.axaml +++ b/src/Resources/Themes.axaml @@ -3,7 +3,7 @@ #FFF0F5F9 - #00000000 + #FF999999 #FFCFDEEA #FFF0F5F9 #FFF8F8F8 @@ -30,9 +30,9 @@ #FF252525 - #FF444444 + #FF606060 #FF1F1F1F - #FF2C2C2C + #FF2F2F2F #FF2B2B2B #FF1C1C1C #FF8F8F8F @@ -85,5 +85,4 @@ fonts:Inter#Inter fonts:SourceGit#JetBrains Mono - fonts:SourceGit#JetBrains Mono diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 8d76934c7..165fdc46c 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -1,7 +1,7 @@  WinExe - net9.0 + net10.0 App.manifest App.ico $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)\\..\\VERSION")) @@ -24,6 +24,10 @@ link + + 13.0 + + $(DefineConstants);DISABLE_UPDATE_DETECTION @@ -39,30 +43,26 @@ - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - - - diff --git a/src/ViewModels/AIAssistant.cs b/src/ViewModels/AIAssistant.cs index 920fafcf5..d538ce1b5 100644 --- a/src/ViewModels/AIAssistant.cs +++ b/src/ViewModels/AIAssistant.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -23,12 +22,11 @@ public string Text private set => SetProperty(ref _text, value); } - public AIAssistant(Repository repo, Models.OpenAIService service, List changes, Action onApply) + public AIAssistant(Repository repo, Models.OpenAIService service, List changes) { _repo = repo; _service = service; _changes = changes; - _onApply = onApply; _cancel = new CancellationTokenSource(); Gen(); @@ -44,7 +42,7 @@ public void Regen() public void Apply() { - _onApply?.Invoke(Text); + _repo.SetCommitMessage(Text); } public void Cancel() @@ -72,7 +70,6 @@ private void Gen() private readonly Repository _repo = null; private Models.OpenAIService _service = null; private List _changes = null; - private Action _onApply = null; private CancellationTokenSource _cancel = null; private bool _isGenerating = false; private string _text = string.Empty; diff --git a/src/ViewModels/AddRemote.cs b/src/ViewModels/AddRemote.cs index 5a6c019fc..fb5f0264b 100644 --- a/src/ViewModels/AddRemote.cs +++ b/src/ViewModels/AddRemote.cs @@ -89,7 +89,7 @@ public static ValidationResult ValidateSSHKey(string sshkey, ValidationContext c public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Adding remote ..."; var log = _repo.CreateLog("Add Remote"); @@ -114,7 +114,6 @@ public override async Task Sure() _repo.MarkFetched(); _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/AddSubmodule.cs b/src/ViewModels/AddSubmodule.cs index 313b2e8e2..e9554ea34 100644 --- a/src/ViewModels/AddSubmodule.cs +++ b/src/ViewModels/AddSubmodule.cs @@ -42,7 +42,7 @@ public static ValidationResult ValidateURL(string url, ValidationContext ctx) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Adding submodule..."; var log = _repo.CreateLog("Add Submodule"); @@ -64,7 +64,6 @@ public override async Task Sure() .AddAsync(_url, relativePath, Recursive); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/AddToIgnore.cs b/src/ViewModels/AddToIgnore.cs index d2a194f43..a502c1725 100644 --- a/src/ViewModels/AddToIgnore.cs +++ b/src/ViewModels/AddToIgnore.cs @@ -29,13 +29,13 @@ public AddToIgnore(Repository repo, string pattern) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Adding Ignored File(s) ..."; var file = StorageFile.GetFullPath(_repo.FullPath, _repo.GitDir); if (!File.Exists(file)) { - await File.WriteAllLinesAsync(file, [_pattern]); + await File.WriteAllLinesAsync(file!, [_pattern]); } else { @@ -47,7 +47,6 @@ public override async Task Sure() } _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/AddWorktree.cs b/src/ViewModels/AddWorktree.cs index 3c3d69ff5..5c7b21d25 100644 --- a/src/ViewModels/AddWorktree.cs +++ b/src/ViewModels/AddWorktree.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; @@ -51,7 +52,11 @@ public string SelectedBranch public bool SetTrackingBranch { get => _setTrackingBranch; - set => SetProperty(ref _setTrackingBranch, value); + set + { + if (SetProperty(ref _setTrackingBranch, value)) + AutoSelectTrackingBranch(); + } } public string SelectedTrackingBranch @@ -73,11 +78,6 @@ public AddWorktree(Repository repo) else RemoteBranches.Add(branch.FriendlyName); } - - if (RemoteBranches.Count > 0) - SelectedTrackingBranch = RemoteBranches[0]; - else - SelectedTrackingBranch = string.Empty; } public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx) @@ -106,7 +106,7 @@ public static ValidationResult ValidateWorktreePath(string path, ValidationConte public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Adding worktree ..."; var branchName = _selectedBranch; @@ -120,10 +120,26 @@ public override async Task Sure() .AddAsync(_path, branchName, _createNewBranch, tracking); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } + private void AutoSelectTrackingBranch() + { + if (!_setTrackingBranch || RemoteBranches.Count == 0) + return; + + var name = string.IsNullOrEmpty(_selectedBranch) ? System.IO.Path.GetFileName(_path.TrimEnd('/', '\\')) : _selectedBranch; + var remoteBranch = RemoteBranches.Find(b => b.EndsWith(name, StringComparison.Ordinal)); + if (string.IsNullOrEmpty(remoteBranch)) + remoteBranch = RemoteBranches[0]; + + if (!remoteBranch.Equals(SelectedTrackingBranch, StringComparison.Ordinal)) + { + SelectedTrackingBranch = remoteBranch; + OnPropertyChanged(nameof(SelectedTrackingBranch)); + } + } + private Repository _repo = null; private string _path = string.Empty; private bool _createNewBranch = true; diff --git a/src/ViewModels/Apply.cs b/src/ViewModels/Apply.cs index 709a66fd6..3eab5ef7a 100644 --- a/src/ViewModels/Apply.cs +++ b/src/ViewModels/Apply.cs @@ -43,7 +43,7 @@ public static ValidationResult ValidatePatchFile(string file, ValidationContext public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Apply patch..."; var log = _repo.CreateLog("Apply Patch"); @@ -54,7 +54,6 @@ public override async Task Sure() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/ApplyStash.cs b/src/ViewModels/ApplyStash.cs index b21d6fe80..8fb9d4b72 100644 --- a/src/ViewModels/ApplyStash.cs +++ b/src/ViewModels/ApplyStash.cs @@ -30,7 +30,7 @@ public ApplyStash(Repository repo, Models.Stash stash) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Applying stash: {Stash.Name}"; var log = _repo.CreateLog("Apply Stash"); @@ -40,13 +40,21 @@ public override async Task Sure() .Use(log) .ApplyAsync(Stash.Name, RestoreIndex); - if (succ && DropAfterApply) - await new Commands.Stash(_repo.FullPath) - .Use(log) - .DropAsync(Stash.Name); + if (succ) + { + _repo.MarkWorkingCopyDirtyManually(); + + if (DropAfterApply) + { + await new Commands.Stash(_repo.FullPath) + .Use(log) + .DropAsync(Stash.Name); + + _repo.MarkStashesDirtyManually(); + } + } log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/Archive.cs b/src/ViewModels/Archive.cs index 6f86357df..182ec562b 100644 --- a/src/ViewModels/Archive.cs +++ b/src/ViewModels/Archive.cs @@ -46,7 +46,7 @@ public Archive(Repository repo, Models.Tag tag) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Archiving ..."; var log = _repo.CreateLog("Archive"); @@ -57,7 +57,6 @@ public override async Task Sure() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); if (succ) App.SendNotification(_repo.FullPath, $"Save archive to : {_saveFile}"); diff --git a/src/ViewModels/Blame.cs b/src/ViewModels/Blame.cs index affe40d92..fda29964c 100644 --- a/src/ViewModels/Blame.cs +++ b/src/ViewModels/Blame.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; @@ -20,6 +21,12 @@ public Models.Commit Revision private set => SetProperty(ref _revision, value); } + public Models.Commit PrevRevision + { + get => _prevRevision; + private set => SetProperty(ref _prevRevision, value); + } + public Models.BlameData Data { get => _data; @@ -51,10 +58,10 @@ public Blame(string repo, string file, Models.Commit commit) FilePath = file; Revision = commit; + PrevRevision = null; _repo = repo; _navigationHistory.Add(sha); - _commits.Add(sha, commit); SetBlameData(sha); } @@ -63,10 +70,7 @@ public string GetCommitMessage(string sha) if (_commitMessages.TryGetValue(sha, out var msg)) return msg; - msg = new Commands.QueryCommitFullMessage(_repo, sha) - .GetResultAsync() - .Result; - + msg = new Commands.QueryCommitFullMessage(_repo, sha).GetResult(); _commitMessages[sha] = msg; return msg; } @@ -79,7 +83,7 @@ public void Back() _navigationActiveIndex--; OnPropertyChanged(nameof(CanBack)); OnPropertyChanged(nameof(CanForward)); - NavigateToCommit(_navigationHistory[_navigationActiveIndex]); + NavigateToCommit(_navigationHistory[_navigationActiveIndex], true); } public void Forward() @@ -90,21 +94,35 @@ public void Forward() _navigationActiveIndex++; OnPropertyChanged(nameof(CanBack)); OnPropertyChanged(nameof(CanForward)); - NavigateToCommit(_navigationHistory[_navigationActiveIndex]); + NavigateToCommit(_navigationHistory[_navigationActiveIndex], true); + } + + public void GotoPrevRevision() + { + if (_prevRevision == null) + return; + + NavigateToCommit(_prevRevision.SHA, false); } - public void NavigateToCommit(string commitSHA) + public void NavigateToCommit(string commitSHA, bool isBackOrForward) { - if (!_navigationHistory[_navigationActiveIndex].Equals(commitSHA, StringComparison.Ordinal)) + if (Revision.SHA.StartsWith(commitSHA, StringComparison.Ordinal)) + return; + + if (!isBackOrForward) { + var count = _navigationHistory.Count; + if (_navigationActiveIndex < count - 1) + _navigationHistory.RemoveRange(_navigationActiveIndex + 1, count - _navigationActiveIndex - 1); + _navigationHistory.Add(commitSHA); - _navigationActiveIndex = _navigationHistory.Count - 1; + _navigationActiveIndex++; OnPropertyChanged(nameof(CanBack)); OnPropertyChanged(nameof(CanForward)); } - if (!Revision.SHA.StartsWith(commitSHA, StringComparison.Ordinal)) - SetBlameData(commitSHA); + SetBlameData(commitSHA); if (App.GetLauncher() is { Pages: { } pages }) { @@ -127,28 +145,28 @@ private void SetBlameData(string commitSHA) _cancellationSource = new CancellationTokenSource(); var token = _cancellationSource.Token; - if (_commits.TryGetValue(commitSHA, out var c)) - { - Revision = c; - } - else + Task.Run(async () => { - Task.Run(async () => - { - var result = await new Commands.QuerySingleCommit(_repo, commitSHA) - .GetResultAsync() - .ConfigureAwait(false); + var argsBuilder = new StringBuilder(); + argsBuilder + .Append("--date-order -n 2 ") + .Append(commitSHA ?? string.Empty) + .Append(" -- ") + .Append(FilePath.Quoted()); + + var commits = await new Commands.QueryCommits(_repo, argsBuilder.ToString(), false) + .GetResultAsync() + .ConfigureAwait(false); - Dispatcher.UIThread.Post(() => + Dispatcher.UIThread.Post(() => + { + if (!token.IsCancellationRequested) { - if (!token.IsCancellationRequested) - { - _commits.Add(commitSHA, result); - Revision = result ?? new Models.Commit() { SHA = commitSHA }; - } - }); - }, token); - } + Revision = commits.Count > 0 ? commits[0] : null; + PrevRevision = commits.Count == 2 ? commits[1] : null; + } + }); + }); Task.Run(async () => { @@ -166,11 +184,11 @@ private void SetBlameData(string commitSHA) private string _repo; private Models.Commit _revision; + private Models.Commit _prevRevision; private CancellationTokenSource _cancellationSource = null; private int _navigationActiveIndex = 0; private List _navigationHistory = []; private Models.BlameData _data = null; - private Dictionary _commits = new(); private Dictionary _commitMessages = new(); } } diff --git a/src/ViewModels/BlameCommandPalette.cs b/src/ViewModels/BlameCommandPalette.cs new file mode 100644 index 000000000..fa24faf6a --- /dev/null +++ b/src/ViewModels/BlameCommandPalette.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Threading; + +namespace SourceGit.ViewModels +{ + public class BlameCommandPalette : ICommandPalette + { + public bool IsLoading + { + get => _isLoading; + private set => SetProperty(ref _isLoading, value); + } + + public List VisibleFiles + { + get => _visibleFiles; + private set => SetProperty(ref _visibleFiles, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateVisible(); + } + } + + public string SelectedFile + { + get => _selectedFile; + set => SetProperty(ref _selectedFile, value); + } + + public BlameCommandPalette(Launcher launcher, string repo) + { + _launcher = launcher; + _repo = repo; + _isLoading = true; + + Task.Run(async () => + { + var files = await new Commands.QueryRevisionFileNames(_repo, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); + + var head = await new Commands.QuerySingleCommit(_repo, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + IsLoading = false; + _repoFiles = files; + _head = head; + UpdateVisible(); + }); + }); + } + + public override void Cleanup() + { + _launcher = null; + _repo = null; + _head = null; + _repoFiles.Clear(); + _filter = null; + _visibleFiles.Clear(); + _selectedFile = null; + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public void Launch() + { + if (!string.IsNullOrEmpty(_selectedFile)) + App.ShowWindow(new Blame(_repo, _selectedFile, _head)); + _launcher.CancelCommandPalette(); + } + + private void UpdateVisible() + { + if (_repoFiles is { Count: > 0 }) + { + if (string.IsNullOrEmpty(_filter)) + { + VisibleFiles = _repoFiles; + + if (string.IsNullOrEmpty(_selectedFile)) + SelectedFile = _repoFiles[0]; + } + else + { + var visible = new List(); + + foreach (var f in _repoFiles) + { + if (f.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + visible.Add(f); + } + + var autoSelected = _selectedFile; + if (visible.Count == 0) + autoSelected = null; + else if (string.IsNullOrEmpty(_selectedFile) || !visible.Contains(_selectedFile)) + autoSelected = visible[0]; + + VisibleFiles = visible; + SelectedFile = autoSelected; + } + } + } + + private Launcher _launcher = null; + private string _repo = null; + private bool _isLoading = false; + private Models.Commit _head = null; + private List _repoFiles = null; + private string _filter = string.Empty; + private List _visibleFiles = []; + private string _selectedFile = null; + } +} diff --git a/src/ViewModels/BlockNavigation.cs b/src/ViewModels/BlockNavigation.cs index 4a51244cb..96f47146b 100644 --- a/src/ViewModels/BlockNavigation.cs +++ b/src/ViewModels/BlockNavigation.cs @@ -1,70 +1,54 @@ +using System; using System.Collections.Generic; -using Avalonia.Collections; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { + public enum BlockNavigationDirection + { + First = 0, + Prev, + Next, + Last + } + public class BlockNavigation : ObservableObject { - public class Block + public record Block(int Start, int End) { - public int Start { get; set; } = 0; - public int End { get; set; } = 0; - - public Block(int start, int end) - { - Start = start; - End = end; - } - - public bool IsInRange(int line) + public bool Contains(int line) { return line >= Start && line <= End; } } - public AvaloniaList Blocks - { - get; - } = []; - - public int Current - { - get => _current; - private set => SetProperty(ref _current, value); - } - public string Indicator { get { - if (Blocks.Count == 0) + if (_blocks.Count == 0) return "-/-"; - if (_current >= 0 && _current < Blocks.Count) - return $"{_current + 1}/{Blocks.Count}"; + if (_current >= 0 && _current < _blocks.Count) + return $"{_current + 1}/{_blocks.Count}"; - return $"-/{Blocks.Count}"; + return $"-/{_blocks.Count}"; } } - public BlockNavigation(object context) + public BlockNavigation(List lines, int cur) { - Blocks.Clear(); - Current = -1; - - var lines = new List(); - if (context is Models.TextDiff combined) - lines = combined.Lines; - else if (context is TwoSideTextDiff twoSide) - lines = twoSide.Old; + _blocks.Clear(); if (lines.Count == 0) + { + _current = -1; return; + } var lineIdx = 0; var blockStartIdx = 0; - var isNewBlock = true; + var isReadingBlock = false; var blocks = new List(); foreach (var line in lines) @@ -72,73 +56,86 @@ public BlockNavigation(object context) lineIdx++; if (line.Type is Models.TextDiffLineType.Added or Models.TextDiffLineType.Deleted or Models.TextDiffLineType.None) { - if (isNewBlock) + if (!isReadingBlock) { - isNewBlock = false; + isReadingBlock = true; blockStartIdx = lineIdx; } } else { - if (!isNewBlock) + if (isReadingBlock) { blocks.Add(new Block(blockStartIdx, lineIdx - 1)); - isNewBlock = true; + isReadingBlock = false; } } } - if (!isNewBlock) - blocks.Add(new Block(blockStartIdx, lines.Count - 1)); + if (isReadingBlock) + blocks.Add(new Block(blockStartIdx, lines.Count)); - Blocks.AddRange(blocks); + _blocks.AddRange(blocks); + _current = Math.Min(_blocks.Count - 1, cur); } - public Block GetCurrentBlock() + public int GetCurrentBlockIndex() { - return (_current >= 0 && _current < Blocks.Count) ? Blocks[_current] : null; + return _current; } - public Block GotoFirst() + public Block GetCurrentBlock() { - if (Blocks.Count == 0) - return null; + if (_current >= 0 && _current < _blocks.Count) + return _blocks[_current]; - Current = 0; - return Blocks[_current]; + return null; } - public Block GotoPrev() + public Block Goto(BlockNavigationDirection direction) { - if (Blocks.Count == 0) + if (_blocks.Count == 0) return null; - if (_current == -1) - Current = 0; - else if (_current > 0) - Current = _current - 1; - return Blocks[_current]; + _current = direction switch + { + BlockNavigationDirection.First => 0, + BlockNavigationDirection.Prev => _current <= 0 ? 0 : _current - 1, + BlockNavigationDirection.Next => _current >= _blocks.Count - 1 ? _blocks.Count - 1 : _current + 1, + BlockNavigationDirection.Last => _blocks.Count - 1, + _ => _current + }; + + OnPropertyChanged(nameof(Indicator)); + return _blocks[_current]; } - public Block GotoNext() + public void UpdateByCaretPosition(int caretLine) { - if (Blocks.Count == 0) - return null; + if (_current >= 0 && _current < _blocks.Count) + { + var block = _blocks[_current]; + if (block.Contains(caretLine)) + return; + } - if (_current < Blocks.Count - 1) - Current = _current + 1; - return Blocks[_current]; - } + _current = -1; - public Block GotoLast() - { - if (Blocks.Count == 0) - return null; + for (var i = 0; i < _blocks.Count; i++) + { + var block = _blocks[i]; + if (block.Start > caretLine) + break; + + _current = i; + if (block.End >= caretLine) + break; + } - Current = Blocks.Count - 1; - return Blocks[_current]; + OnPropertyChanged(nameof(Indicator)); } - private int _current = -1; + private int _current; + private readonly List _blocks = []; } } diff --git a/src/ViewModels/BranchCompare.cs b/src/ViewModels/BranchCompare.cs index f88bc32ab..7c6a484d4 100644 --- a/src/ViewModels/BranchCompare.cs +++ b/src/ViewModels/BranchCompare.cs @@ -1,17 +1,18 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; - -using Avalonia.Controls; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { public class BranchCompare : ObservableObject { + public string RepositoryPath + { + get => _repo; + } + public bool IsLoading { get => _isLoading; @@ -42,6 +43,12 @@ public Models.Commit ToHead private set => SetProperty(ref _toHead, value); } + public int TotalChanges + { + get => _totalChanges; + private set => SetProperty(ref _totalChanges, value); + } + public List VisibleChanges { get => _visibleChanges; @@ -127,68 +134,11 @@ public string GetAbsPath(string path) return Native.OS.GetAbsPath(_repo, path); } - public ContextMenu CreateChangeContextMenu() + public async Task SaveChangesAsPatchAsync(List changes, string saveTo) { - if (_selectedChanges is not { Count: 1 }) - return null; - - var change = _selectedChanges[0]; - var menu = new ContextMenu(); - - var openWithMerger = new MenuItem(); - openWithMerger.Header = App.Text("OpenInExternalMergeTool"); - openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openWithMerger.Click += (_, ev) => - { - var toolType = Preferences.Instance.ExternalMergeToolType; - var toolPath = Preferences.Instance.ExternalMergeToolPath; - var opt = new Models.DiffOption(_based.Head, _to.Head, change); - - new Commands.DiffTool(_repo, toolType, toolPath, opt).Open(); - ev.Handled = true; - }; - menu.Items.Add(openWithMerger); - - if (change.Index != Models.ChangeState.Deleted) - { - var full = Path.GetFullPath(Path.Combine(_repo, change.Path)); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(full); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(full, true); - ev.Handled = true; - }; - menu.Items.Add(explore); - } - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(change.Path); - ev.Handled = true; - }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copyPath); - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(Native.OS.GetAbsPath(_repo, change.Path)); - e.Handled = true; - }; - menu.Items.Add(copyFullPath); - - return menu; + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes, _based.Head, _to.Head, saveTo); + if (succ) + App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); } private void Refresh() @@ -231,6 +181,7 @@ private void Refresh() Dispatcher.UIThread.Post(() => { + TotalChanges = _changes.Count; VisibleChanges = visible; IsLoading = false; @@ -270,6 +221,7 @@ private void RefreshVisible() private Models.Branch _to = null; private Models.Commit _baseHead = null; private Models.Commit _toHead = null; + private int _totalChanges = 0; private List _changes = null; private List _visibleChanges = null; private List _selectedChanges = null; diff --git a/src/ViewModels/BranchCompareCommandPalette.cs b/src/ViewModels/BranchCompareCommandPalette.cs new file mode 100644 index 000000000..bf2f2fc15 --- /dev/null +++ b/src/ViewModels/BranchCompareCommandPalette.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.ViewModels +{ + public class BranchCompareCommandPalette : ICommandPalette + { + public List Branches + { + get => _branches; + private set => SetProperty(ref _branches, value); + } + + public Models.Branch SelectedBranch + { + get => _selectedBranch; + set => SetProperty(ref _selectedBranch, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateBranches(); + } + } + + public BranchCompareCommandPalette(Launcher launcher, Repository repo) + { + _launcher = launcher; + _repo = repo; + UpdateBranches(); + } + + public override void Cleanup() + { + _launcher = null; + _repo = null; + _branches.Clear(); + _selectedBranch = null; + _filter = null; + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public void Launch() + { + if (_selectedBranch != null) + App.ShowWindow(new BranchCompare(_repo.FullPath, _selectedBranch, _repo.CurrentBranch)); + _launcher?.CancelCommandPalette(); + } + + private void UpdateBranches() + { + var current = _repo.CurrentBranch; + if (current == null) + return; + + var branches = new List(); + foreach (var b in _repo.Branches) + { + if (b == current) + continue; + + if (string.IsNullOrEmpty(_filter) || b.FriendlyName.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + branches.Add(b); + } + + branches.Sort((l, r) => + { + if (l.IsLocal == r.IsLocal) + return l.Name.CompareTo(r.Name); + + return l.IsLocal ? -1 : 1; + }); + + var autoSelected = _selectedBranch; + if (branches.Count == 0) + autoSelected = null; + else if (_selectedBranch == null || !branches.Contains(_selectedBranch)) + autoSelected = branches[0]; + + Branches = branches; + SelectedBranch = autoSelected; + } + + private Launcher _launcher; + private Repository _repo; + private List _branches = []; + private Models.Branch _selectedBranch = null; + private string _filter; + } +} diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index 5a56e3361..53ac1df9b 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -252,8 +252,8 @@ private void SortNodesByTime(List nodes) SortNodesByTime(node.Children); } - private readonly Models.BranchSortMode _localSortMode = Models.BranchSortMode.Name; - private readonly Models.BranchSortMode _remoteSortMode = Models.BranchSortMode.Name; + private readonly Models.BranchSortMode _localSortMode; + private readonly Models.BranchSortMode _remoteSortMode; private readonly HashSet _expanded = new HashSet(); } } diff --git a/src/ViewModels/ChangeSubmoduleUrl.cs b/src/ViewModels/ChangeSubmoduleUrl.cs index f356234bf..74deaddda 100644 --- a/src/ViewModels/ChangeSubmoduleUrl.cs +++ b/src/ViewModels/ChangeSubmoduleUrl.cs @@ -39,7 +39,7 @@ public override async Task Sure() if (_url.Equals(Submodule.URL, StringComparison.Ordinal)) return true; - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Change submodule's url..."; var log = _repo.CreateLog("Change Submodule's URL"); @@ -50,7 +50,6 @@ public override async Task Sure() .SetURLAsync(Submodule.Path, _url); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/ChangeTreeNode.cs b/src/ViewModels/ChangeTreeNode.cs index 4d71a153b..c35f4fc1f 100644 --- a/src/ViewModels/ChangeTreeNode.cs +++ b/src/ViewModels/ChangeTreeNode.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; - +using System.IO; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -7,6 +7,7 @@ namespace SourceGit.ViewModels public class ChangeTreeNode : ObservableObject { public string FullPath { get; set; } + public string DisplayName { get; set; } public int Depth { get; private set; } = 0; public Models.Change Change { get; set; } = null; public List Children { get; set; } = new List(); @@ -32,22 +33,22 @@ public bool IsExpanded set => SetProperty(ref _isExpanded, value); } - public ChangeTreeNode(Models.Change c, int depth) + public ChangeTreeNode(Models.Change c) { FullPath = c.Path; - Depth = depth; + DisplayName = Path.GetFileName(c.Path); Change = c; IsExpanded = false; } - public ChangeTreeNode(string path, bool isExpanded, int depth) + public ChangeTreeNode(string path, bool isExpanded) { FullPath = path; - Depth = depth; + DisplayName = Path.GetFileName(path); IsExpanded = isExpanded; } - public static List Build(IList changes, HashSet folded) + public static List Build(IList changes, HashSet folded, bool compactFolders) { var nodes = new List(); var folders = new Dictionary(); @@ -57,12 +58,11 @@ public static List Build(IList changes, HashSet Build(IList changes, HashSet collection, ChangeTreeNode collection.Add(subFolder); } - private static void Sort(List nodes) + private static void Compact(ChangeTreeNode node) + { + var childrenCount = node.Children.Count; + if (childrenCount == 0) + return; + + if (childrenCount > 1) + { + foreach (var c in node.Children) + Compact(c); + return; + } + + var child = node.Children[0]; + if (child.Change != null) + return; + + node.FullPath = $"{node.FullPath}/{child.DisplayName}"; + node.DisplayName = $"{node.DisplayName} / {child.DisplayName}"; + node.IsExpanded = child.IsExpanded; + node.Children = child.Children; + Compact(node); + } + + private static void SortAndSetDepth(List nodes, int depth) { foreach (var node in nodes) { + node.Depth = depth; if (node.IsFolder) - Sort(node.Children); + SortAndSetDepth(node.Children, depth + 1); } nodes.Sort((l, r) => { if (l.IsFolder == r.IsFolder) - return Models.NumericSort.Compare(l.FullPath, r.FullPath); + return Models.NumericSort.Compare(l.DisplayName, r.DisplayName); return l.IsFolder ? -1 : 1; }); } diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index ce6195b8d..9980f84d2 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -15,17 +15,6 @@ public bool DiscardLocalChanges set; } - public bool IsRecurseSubmoduleVisible - { - get => _repo.Submodules.Count > 0; - } - - public bool RecurseSubmodules - { - get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; - set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; - } - public Checkout(Repository repo, string branch) { _repo = repo; @@ -35,7 +24,7 @@ public Checkout(Repository repo, string branch) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Checkout '{Branch}' ..."; var log = _repo.CreateLog($"Checkout '{Branch}'"); @@ -47,12 +36,9 @@ public override async Task Sure() if (refs.Count == 0) { var msg = App.Text("Checkout.WarnLostCommits"); - var shouldContinue = await App.AskConfirmAsync(msg, null); + var shouldContinue = await App.AskConfirmAsync(msg); if (!shouldContinue) - { - _repo.SetWatcherEnabled(true); return true; - } } } @@ -61,7 +47,7 @@ public override async Task Sure() if (!DiscardLocalChanges) { - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) @@ -70,7 +56,6 @@ public override async Task Sure() if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } @@ -84,14 +69,11 @@ public override async Task Sure() if (succ) { - if (IsRecurseSubmoduleVisible && RecurseSubmodules) - { - var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath).GetResultAsync(); - if (submodules.Count > 0) - await new Commands.Submodule(_repo.FullPath) - .Use(log) - .UpdateAsync(submodules, true, true); - } + var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath, false).GetResultAsync(); + if (submodules.Count > 0) + await new Commands.Submodule(_repo.FullPath) + .Use(log) + .UpdateAsync(submodules, false, true); if (needPopStash) await new Commands.Stash(_repo.FullPath) @@ -102,11 +84,10 @@ public override async Task Sure() log.Complete(); var b = _repo.Branches.Find(x => x.IsLocal && x.Name == Branch); - if (b != null && _repo.HistoriesFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(b, Models.FilterMode.Included, true, false); + if (b != null && _repo.HistoryFilterMode == Models.FilterMode.Included) + _repo.SetBranchFilterMode(b, Models.FilterMode.Included, false, false); _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); ProgressDescription = "Waiting for branch updated..."; await Task.Delay(400); diff --git a/src/ViewModels/CheckoutAndFastForward.cs b/src/ViewModels/CheckoutAndFastForward.cs index 205bbbd7b..44a477c8c 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -20,17 +20,6 @@ public bool DiscardLocalChanges set; } - public bool IsRecurseSubmoduleVisible - { - get => _repo.Submodules.Count > 0; - } - - public bool RecurseSubmodules - { - get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; - set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; - } - public CheckoutAndFastForward(Repository repo, Models.Branch localBranch, Models.Branch remoteBranch) { _repo = repo; @@ -40,7 +29,7 @@ public CheckoutAndFastForward(Repository repo, Models.Branch localBranch, Models public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Checkout and Fast-Forward '{LocalBranch.Name}' ..."; var log = _repo.CreateLog($"Checkout and Fast-Forward '{LocalBranch.Name}' ..."); @@ -52,12 +41,9 @@ public override async Task Sure() if (refs.Count == 0) { var msg = App.Text("Checkout.WarnLostCommits"); - var shouldContinue = await App.AskConfirmAsync(msg, null); + var shouldContinue = await App.AskConfirmAsync(msg); if (!shouldContinue) - { - _repo.SetWatcherEnabled(true); return true; - } } } @@ -66,7 +52,7 @@ public override async Task Sure() if (!DiscardLocalChanges) { - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) @@ -75,7 +61,6 @@ public override async Task Sure() if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } @@ -89,14 +74,11 @@ public override async Task Sure() if (succ) { - if (IsRecurseSubmoduleVisible && RecurseSubmodules) - { - var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath).GetResultAsync(); - if (submodules.Count > 0) - await new Commands.Submodule(_repo.FullPath) - .Use(log) - .UpdateAsync(submodules, true, true); - } + var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath, false).GetResultAsync(); + if (submodules.Count > 0) + await new Commands.Submodule(_repo.FullPath) + .Use(log) + .UpdateAsync(submodules, false, true); if (needPopStash) await new Commands.Stash(_repo.FullPath) @@ -106,11 +88,10 @@ public override async Task Sure() log.Complete(); - if (_repo.HistoriesFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(LocalBranch, Models.FilterMode.Included, true, false); + if (_repo.HistoryFilterMode == Models.FilterMode.Included) + _repo.SetBranchFilterMode(LocalBranch, Models.FilterMode.Included, false, false); _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); ProgressDescription = "Waiting for branch updated..."; await Task.Delay(400); diff --git a/src/ViewModels/CheckoutCommandPalette.cs b/src/ViewModels/CheckoutCommandPalette.cs new file mode 100644 index 000000000..3e25ebc9e --- /dev/null +++ b/src/ViewModels/CheckoutCommandPalette.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class CheckoutCommandPalette : ICommandPalette + { + public List Branches + { + get => _branches; + private set => SetProperty(ref _branches, value); + } + + public Models.Branch SelectedBranch + { + get => _selectedBranch; + set => SetProperty(ref _selectedBranch, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateBranches(); + } + } + + public CheckoutCommandPalette(Launcher launcher, Repository repo) + { + _launcher = launcher; + _repo = repo; + UpdateBranches(); + } + + public override void Cleanup() + { + _launcher = null; + _repo = null; + _branches.Clear(); + _selectedBranch = null; + _filter = null; + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public async Task ExecAsync() + { + _launcher.CommandPalette = null; + + if (_selectedBranch != null) + await _repo.CheckoutBranchAsync(_selectedBranch); + + Dispose(); + GC.Collect(); + } + + private void UpdateBranches() + { + var current = _repo.CurrentBranch; + if (current == null) + return; + + var branches = new List(); + foreach (var b in _repo.Branches) + { + if (b == current) + continue; + + if (string.IsNullOrEmpty(_filter) || b.FriendlyName.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + branches.Add(b); + } + + branches.Sort((l, r) => + { + if (l.IsLocal == r.IsLocal) + return l.Name.CompareTo(r.Name); + + return l.IsLocal ? -1 : 1; + }); + + var autoSelected = _selectedBranch; + if (branches.Count == 0) + autoSelected = null; + else if (_selectedBranch == null || !branches.Contains(_selectedBranch)) + autoSelected = branches[0]; + + Branches = branches; + SelectedBranch = autoSelected; + } + + private Launcher _launcher; + private Repository _repo; + private List _branches = []; + private Models.Branch _selectedBranch = null; + private string _filter; + } +} diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutCommit.cs index a77a72356..55da0862b 100644 --- a/src/ViewModels/CheckoutCommit.cs +++ b/src/ViewModels/CheckoutCommit.cs @@ -15,17 +15,6 @@ public bool DiscardLocalChanges set; } - public bool IsRecurseSubmoduleVisible - { - get => _repo.Submodules.Count > 0; - } - - public bool RecurseSubmodules - { - get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; - set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; - } - public CheckoutCommit(Repository repo, Models.Commit commit) { _repo = repo; @@ -35,7 +24,7 @@ public CheckoutCommit(Repository repo, Models.Commit commit) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Checkout Commit '{Commit.SHA}' ..."; var log = _repo.CreateLog("Checkout Commit"); @@ -47,12 +36,9 @@ public override async Task Sure() if (refs.Count == 0) { var msg = App.Text("Checkout.WarnLostCommits"); - var shouldContinue = await App.AskConfirmAsync(msg, null); + var shouldContinue = await App.AskConfirmAsync(msg); if (!shouldContinue) - { - _repo.SetWatcherEnabled(true); return true; - } } } @@ -61,7 +47,7 @@ public override async Task Sure() if (!DiscardLocalChanges) { - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) @@ -70,7 +56,6 @@ public override async Task Sure() if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } @@ -84,14 +69,11 @@ public override async Task Sure() if (succ) { - if (IsRecurseSubmoduleVisible && RecurseSubmodules) - { - var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath).GetResultAsync(); - if (submodules.Count > 0) - await new Commands.Submodule(_repo.FullPath) - .Use(log) - .UpdateAsync(submodules, true, true); - } + var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath, false).GetResultAsync(); + if (submodules.Count > 0) + await new Commands.Submodule(_repo.FullPath) + .Use(log) + .UpdateAsync(submodules, false, true); if (needPop) await new Commands.Stash(_repo.FullPath) @@ -100,7 +82,6 @@ public override async Task Sure() } log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/CherryPick.cs b/src/ViewModels/CherryPick.cs index ca525e0ac..fb8ae649f 100644 --- a/src/ViewModels/CherryPick.cs +++ b/src/ViewModels/CherryPick.cs @@ -65,7 +65,7 @@ public CherryPick(Repository repo, Models.Commit merge, List pare public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); _repo.ClearCommitMessage(); ProgressDescription = "Cherry-Pick commit(s) ..."; @@ -96,7 +96,6 @@ public override async Task Sure() } log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/Cleanup.cs b/src/ViewModels/Cleanup.cs index c8788d681..974f11cf3 100644 --- a/src/ViewModels/Cleanup.cs +++ b/src/ViewModels/Cleanup.cs @@ -11,7 +11,7 @@ public Cleanup(Repository repo) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Cleanup (GC & prune) ..."; var log = _repo.CreateLog("Cleanup (GC & prune)"); @@ -22,7 +22,6 @@ public override async Task Sure() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/ClearStashes.cs b/src/ViewModels/ClearStashes.cs index 6178d5efd..0c5092d12 100644 --- a/src/ViewModels/ClearStashes.cs +++ b/src/ViewModels/ClearStashes.cs @@ -11,7 +11,7 @@ public ClearStashes(Repository repo) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Clear all stashes..."; var log = _repo.CreateLog("Clear Stashes"); @@ -22,7 +22,7 @@ public override async Task Sure() .ClearAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); + _repo.MarkStashesDirtyManually(); return true; } diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index cfcf51bf2..014063bdb 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -140,7 +140,7 @@ public override async Task Sure() if (InitAndUpdateSubmodules) { - var submodules = await new Commands.QueryUpdatableSubmodules(path).GetResultAsync(); + var submodules = await new Commands.QueryUpdatableSubmodules(path, true).GetResultAsync(); if (submodules.Count > 0) await new Commands.Submodule(path) .Use(log) @@ -150,6 +150,8 @@ public override async Task Sure() log.Complete(); var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, null, true); + await node.UpdateStatusAsync(false, null); + var launcher = App.GetLauncher(); LauncherPage page = null; foreach (var one in launcher.Pages) diff --git a/src/ViewModels/CommandLog.cs b/src/ViewModels/CommandLog.cs index 96faaef7a..f12d27b45 100644 --- a/src/ViewModels/CommandLog.cs +++ b/src/ViewModels/CommandLog.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -43,10 +44,14 @@ public CommandLog(string name) Name = name; } - public void Register(Action handler) + public void Subscribe(Models.ICommandLogReceiver receiver) { - if (!IsComplete) - _onNewLineReceived += handler; + _receivers.Add(receiver); + } + + public void Unsubscribe(Models.ICommandLogReceiver receiver) + { + _receivers.Remove(receiver); } public void AppendLine(string line = null) @@ -59,7 +64,9 @@ public void AppendLine(string line = null) { var newline = line ?? string.Empty; _builder.AppendLine(newline); - _onNewLineReceived?.Invoke(newline); + + foreach (var receiver in _receivers) + receiver.OnReceiveCommandLog(newline); } } @@ -76,20 +83,14 @@ public void Complete() _content = _builder.ToString(); _builder.Clear(); + _receivers.Clear(); _builder = null; OnPropertyChanged(nameof(IsComplete)); - - if (_onNewLineReceived != null) - { - var dumpHandlers = _onNewLineReceived.GetInvocationList(); - foreach (var d in dumpHandlers) - _onNewLineReceived -= (Action)d; - } } private string _content = string.Empty; private StringBuilder _builder = new StringBuilder(); - private event Action _onNewLineReceived; + private List _receivers = new List(); } } diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 81bc6a214..f0862c0f8 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -5,27 +5,46 @@ using System.Threading; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Platform.Storage; +using Avalonia; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { + public class CommitDetailSharedData + { + public int ActiveTabIndex + { + get; + set; + } + + public CommitDetailSharedData() + { + ActiveTabIndex = Preferences.Instance.ShowChangesInCommitDetailByDefault ? 1 : 0; + } + } + public partial class CommitDetail : ObservableObject, IDisposable { - public int ActivePageIndex + public Repository Repository { - get => _rememberActivePageIndex ? _repo.CommitDetailActivePageIndex : _activePageIndex; + get => _repo; + } + + public int ActiveTabIndex + { + get => _sharedData.ActiveTabIndex; set { - if (_rememberActivePageIndex) - _repo.CommitDetailActivePageIndex = value; - else - _activePageIndex = value; + if (value != _sharedData.ActiveTabIndex) + { + _sharedData.ActiveTabIndex = value; - OnPropertyChanged(); + if (value == 1 && DiffContext == null && _selectedChanges is { Count: 1 }) + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, _selectedChanges[0])); + } } } @@ -34,6 +53,9 @@ public Models.Commit Commit get => _commit; set { + if (_commit != null && value != null && _commit.SHA.Equals(value.SHA, StringComparison.Ordinal)) + return; + if (SetProperty(ref _commit, value)) Refresh(); } @@ -82,7 +104,7 @@ public List SelectedChanges { if (SetProperty(ref _selectedChanges, value)) { - if (value is not { Count: 1 }) + if (ActiveTabIndex != 1 || value is not { Count: 1 }) DiffContext = null; else DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, value[0]), _diffContext); @@ -140,10 +162,16 @@ public bool CanOpenRevisionFileWithDefaultEditor private set => SetProperty(ref _canOpenRevisionFileWithDefaultEditor, value); } - public CommitDetail(Repository repo, bool rememberActivePageIndex) + public Vector ScrollOffset + { + get => _scrollOffset; + set => SetProperty(ref _scrollOffset, value); + } + + public CommitDetail(Repository repo, CommitDetailSharedData sharedData) { _repo = repo; - _rememberActivePageIndex = rememberActivePageIndex; + _sharedData = sharedData ?? new CommitDetailSharedData(); WebLinks = Models.CommitLink.Get(repo.Remotes); } @@ -198,6 +226,80 @@ public void CancelRevisionFileSuggestions() .ConfigureAwait(false); } + public string GetAbsPath(string path) + { + return Native.OS.GetAbsPath(_repo.FullPath, path); + } + + public void OpenChangeInMergeTool(Models.Change c) + { + new Commands.DiffTool(_repo.FullPath, new Models.DiffOption(_commit, c)).Open(); + } + + public async Task SaveChangesAsPatchAsync(List changes, string saveTo) + { + if (_commit == null) + return; + + var baseRevision = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : _commit.Parents[0]; + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo.FullPath, changes, baseRevision, _commit.SHA, saveTo); + if (succ) + App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); + } + + public async Task ResetToThisRevisionAsync(string path) + { + var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'"); + await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, _commit.SHA); + log.Complete(); + } + + public async Task ResetToParentRevisionAsync(Models.Change change) + { + var log = _repo.CreateLog($"Reset File to '{_commit.SHA}~1'"); + + if (change.Index == Models.ChangeState.Renamed) + await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.OriginalPath, $"{_commit.SHA}~1"); + + await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.Path, $"{_commit.SHA}~1"); + log.Complete(); + } + + public async Task ResetMultipleToThisRevisionAsync(List changes) + { + var files = new List(); + foreach (var c in changes) + files.Add(c.Path); + + var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}'"); + await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(files, _commit.SHA); + log.Complete(); + } + + public async Task ResetMultipleToParentRevisionAsync(List changes) + { + var renamed = new List(); + var modified = new List(); + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Renamed) + renamed.Add(c.OriginalPath); + else + modified.Add(c.Path); + } + + var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}~1'"); + + if (modified.Count > 0) + await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(modified, $"{_commit.SHA}~1"); + + if (renamed.Count > 0) + await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(renamed, $"{_commit.SHA}~1"); + + log.Complete(); + } + public async Task> GetRevisionFilesUnderFolderAsync(string parentFolder) { return await new Commands.QueryRevisionObjects(_repo.FullPath, _commit.SHA, parentFolder) @@ -227,7 +329,7 @@ public async Task ViewRevisionFileAsync(Models.Object file) } } - public async Task OpenRevisionFileWithDefaultEditorAsync(string file) + public async Task OpenRevisionFileAsync(string file, Models.ExternalTool tool) { var fullPath = Native.OS.GetAbsPath(_repo.FullPath, file); var fileName = Path.GetFileNameWithoutExtension(fullPath) ?? ""; @@ -238,437 +340,22 @@ await Commands.SaveRevisionFile .RunAsync(_repo.FullPath, _commit.SHA, file, tmpFile) .ConfigureAwait(false); - Native.OS.OpenWithDefaultEditor(tmpFile); - } - - public string GetAbsPath(string path) - { - return Native.OS.GetAbsPath(_repo.FullPath, path); - } - - public void OpenChangeInMergeTool(Models.Change c) - { - var toolType = Preferences.Instance.ExternalMergeToolType; - var toolPath = Preferences.Instance.ExternalMergeToolPath; - var opt = new Models.DiffOption(_commit, c); - new Commands.DiffTool(_repo.FullPath, toolType, toolPath, opt).Open(); - } - - public async Task SaveRevisionFile(Models.Object file) - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == 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(); - var saveTo = Path.Combine(folderPath, Path.GetFileName(file.Path)!); - - await Commands.SaveRevisionFile - .RunAsync(_repo.FullPath, _commit.SHA, file.Path, saveTo) - .ConfigureAwait(false); - } - } - catch (Exception e) - { - App.RaiseException(_repo.FullPath, $"Failed to save file: {e.Message}"); - } - } - - public ContextMenu CreateChangeContextMenuByFolder(ChangeTreeNode node, List changes) - { - var fullPath = Native.OS.GetAbsPath(_repo.FullPath, node.FullPath); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = Directory.Exists(fullPath); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(fullPath, true); - ev.Handled = true; - }; - - var history = new MenuItem(); - history.Header = App.Text("DirHistories"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, ev) => - { - App.ShowWindow(new DirHistories(_repo, node.FullPath, _commit.SHA)); - ev.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = App.Text("FileCM.SaveAsPatch"); - patch.Icon = App.CreateMenuIcon("Icons.Diff"); - patch.Click += async (_, e) => - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == null) - return; - - var options = new FilePickerSaveOptions(); - options.Title = App.Text("FileCM.SaveAsPatch"); - options.DefaultExtension = ".patch"; - options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; - - var baseRevision = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : _commit.Parents[0]; - var storageFile = await storageProvider.SaveFilePickerAsync(options); - if (storageFile != null) - { - var saveTo = storageFile.Path.LocalPath; - var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo.FullPath, changes, baseRevision, _commit.SHA, saveTo); - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } - - e.Handled = true; - }; - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(node.FullPath); - ev.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(fullPath); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(history); - menu.Items.Add(patch); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(copyPath); - menu.Items.Add(copyFullPath); - - return menu; - } - - public ContextMenu CreateChangeContextMenu(Models.Change change) - { - var openWithMerger = new MenuItem(); - openWithMerger.Header = App.Text("OpenInExternalMergeTool"); - openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openWithMerger.Click += (_, ev) => - { - OpenChangeInMergeTool(change); - ev.Handled = true; - }; - - var fullPath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(fullPath); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(fullPath, true); - ev.Handled = true; - }; - - var history = new MenuItem(); - history.Header = App.Text("FileHistory"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, ev) => - { - App.ShowWindow(new FileHistories(_repo, change.Path, _commit.SHA)); - ev.Handled = true; - }; - - var blame = new MenuItem(); - blame.Header = App.Text("Blame"); - blame.Icon = App.CreateMenuIcon("Icons.Blame"); - blame.IsEnabled = change.Index != Models.ChangeState.Deleted; - blame.Click += (_, ev) => - { - App.ShowWindow(new Blame(_repo.FullPath, change.Path, _commit)); - ev.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = App.Text("FileCM.SaveAsPatch"); - patch.Icon = App.CreateMenuIcon("Icons.Diff"); - patch.Click += async (_, e) => - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == null) - return; - - var options = new FilePickerSaveOptions(); - options.Title = App.Text("FileCM.SaveAsPatch"); - options.DefaultExtension = ".patch"; - options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; - - var baseRevision = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : _commit.Parents[0]; - var storageFile = await storageProvider.SaveFilePickerAsync(options); - if (storageFile != null) - { - var saveTo = storageFile.Path.LocalPath; - var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo.FullPath, [change], baseRevision, _commit.SHA, saveTo); - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } - - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(openWithMerger); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(history); - menu.Items.Add(blame); - menu.Items.Add(patch); - menu.Items.Add(new MenuItem { Header = "-" }); - - if (!_repo.IsBare) - { - var resetToThisRevision = new MenuItem(); - resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); - resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - resetToThisRevision.Click += async (_, ev) => - { - await ResetToThisRevisionAsync(change.Path); - ev.Handled = true; - }; - - var resetToFirstParent = new MenuItem(); - resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); - resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - resetToFirstParent.IsEnabled = _commit.Parents.Count > 0; - resetToFirstParent.Click += async (_, ev) => - { - await ResetToParentRevisionAsync(change); - ev.Handled = true; - }; - - menu.Items.Add(resetToThisRevision); - menu.Items.Add(resetToFirstParent); - menu.Items.Add(new MenuItem { Header = "-" }); - - TryToAddContextMenuItemsForGitLFS(menu, fullPath, change.Path); - } - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(change.Path); - ev.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(fullPath); - e.Handled = true; - }; - - menu.Items.Add(copyPath); - menu.Items.Add(copyFullPath); - return menu; - } - - public ContextMenu CreateRevisionFileContextMenuByFolder(string path) - { - var fullPath = Native.OS.GetAbsPath(_repo.FullPath, path); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = Directory.Exists(fullPath); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(fullPath, true); - ev.Handled = true; - }; - - var history = new MenuItem(); - history.Header = App.Text("DirHistories"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, ev) => - { - App.ShowWindow(new DirHistories(_repo, path, _commit.SHA)); - ev.Handled = true; - }; - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(path); - ev.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(fullPath); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(history); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copyPath); - menu.Items.Add(copyFullPath); - return menu; + if (tool == null) + Native.OS.OpenWithDefaultEditor(tmpFile); + else + tool.Open(tmpFile); } - public ContextMenu CreateRevisionFileContextMenu(Models.Object file) + public async Task SaveRevisionFileAsync(Models.Object file, string saveTo) { - if (file.Type == Models.ObjectType.Tree) - return CreateRevisionFileContextMenuByFolder(file.Path); - - var menu = new ContextMenu(); - var fullPath = Native.OS.GetAbsPath(_repo.FullPath, file.Path); - var openWith = new MenuItem(); - openWith.Header = App.Text("OpenWith"); - openWith.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWith.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O"; - openWith.IsEnabled = file.Type == Models.ObjectType.Blob; - openWith.Click += async (_, ev) => - { - await OpenRevisionFileWithDefaultEditorAsync(file.Path); - ev.Handled = true; - }; - - var saveAs = new MenuItem(); - saveAs.Header = App.Text("SaveAs"); - saveAs.Icon = App.CreateMenuIcon("Icons.Save"); - saveAs.IsEnabled = file.Type == Models.ObjectType.Blob; - saveAs.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+S" : "Ctrl+Shift+S"; - saveAs.Click += async (_, ev) => - { - await SaveRevisionFile(file); - ev.Handled = true; - }; - - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(fullPath); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(fullPath, file.Type == Models.ObjectType.Blob); - ev.Handled = true; - }; - - menu.Items.Add(openWith); - menu.Items.Add(saveAs); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem() { Header = "-" }); - - var history = new MenuItem(); - history.Header = App.Text("FileHistory"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, ev) => - { - App.ShowWindow(new FileHistories(_repo, file.Path, _commit.SHA)); - ev.Handled = true; - }; - - var blame = new MenuItem(); - blame.Header = App.Text("Blame"); - blame.Icon = App.CreateMenuIcon("Icons.Blame"); - blame.IsEnabled = file.Type == Models.ObjectType.Blob; - blame.Click += (_, ev) => - { - App.ShowWindow(new Blame(_repo.FullPath, file.Path, _commit)); - ev.Handled = true; - }; - - menu.Items.Add(history); - menu.Items.Add(blame); - menu.Items.Add(new MenuItem() { Header = "-" }); - - if (!_repo.IsBare) - { - var resetToThisRevision = new MenuItem(); - resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); - resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - resetToThisRevision.Click += async (_, ev) => - { - await ResetToThisRevisionAsync(file.Path); - ev.Handled = true; - }; - - var change = _changes.Find(x => x.Path == file.Path) ?? new Models.Change() { Index = Models.ChangeState.None, Path = file.Path }; - var resetToFirstParent = new MenuItem(); - resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); - resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - resetToFirstParent.IsEnabled = _commit.Parents.Count > 0; - resetToFirstParent.Click += async (_, ev) => - { - await ResetToParentRevisionAsync(change); - ev.Handled = true; - }; - - menu.Items.Add(resetToThisRevision); - menu.Items.Add(resetToFirstParent); - menu.Items.Add(new MenuItem() { Header = "-" }); - - TryToAddContextMenuItemsForGitLFS(menu, fullPath, file.Path); - } - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(file.Path); - ev.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(fullPath); - e.Handled = true; - }; - - menu.Items.Add(copyPath); - menu.Items.Add(copyFullPath); - return menu; + await Commands.SaveRevisionFile + .RunAsync(_repo.FullPath, _commit.SHA, file.Path, saveTo) + .ConfigureAwait(false); } private void Refresh() { - _changes = null; + _changes = []; _requestingRevisionFiles = false; _revisionFiles = null; @@ -679,6 +366,7 @@ private void Refresh() Children = null; RevisionFileSearchFilter = string.Empty; RevisionFileSearchSuggestion = null; + ScrollOffset = Vector.Zero; if (_commit == null) return; @@ -694,7 +382,7 @@ private void Refresh() var message = await new Commands.QueryCommitFullMessage(_repo.FullPath, _commit.SHA) .GetResultAsync() .ConfigureAwait(false); - var inlines = await ParseInlinesInMessageAsync(message); + var inlines = await ParseInlinesInMessageAsync(message).ConfigureAwait(false); if (!token.IsCancellationRequested) Dispatcher.UIThread.Post(() => @@ -731,7 +419,7 @@ private void Refresh() Task.Run(async () => { - var parent = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : _commit.Parents[0]; + var parent = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : $"{_commit.SHA}^"; var cmd = new Commands.CompareRevisions(_repo.FullPath, parent, _commit.SHA) { CancellationToken = token }; var changes = await cmd.ReadAsync().ConfigureAwait(false); var visible = changes; @@ -764,7 +452,7 @@ private void Refresh() private async Task ParseInlinesInMessageAsync(string message) { var inlines = new Models.InlineElementCollector(); - if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules) + if (_repo.IssueTrackers is { Count: > 0 } rules) { foreach (var rule in rules) rule.Matches(inlines, message); @@ -803,9 +491,6 @@ private void Refresh() private void RefreshVisibleChanges() { - if (_changes == null) - return; - if (string.IsNullOrEmpty(_searchChangeFilter)) { VisibleChanges = _changes; @@ -823,75 +508,6 @@ private void RefreshVisibleChanges() } } - private void TryToAddContextMenuItemsForGitLFS(ContextMenu menu, string fullPath, string path) - { - if (_repo.Remotes.Count == 0 || !File.Exists(fullPath) || !_repo.IsLFSEnabled()) - return; - - var lfs = new MenuItem(); - lfs.Header = App.Text("GitLFS"); - lfs.Icon = App.CreateMenuIcon("Icons.LFS"); - - var lfsLock = new MenuItem(); - lfsLock.Header = App.Text("GitLFS.Locks.Lock"); - lfsLock.Icon = App.CreateMenuIcon("Icons.Lock"); - if (_repo.Remotes.Count == 1) - { - lfsLock.Click += async (_, e) => - { - await _repo.LockLFSFileAsync(_repo.Remotes[0].Name, path); - e.Handled = true; - }; - } - else - { - foreach (var remote in _repo.Remotes) - { - var remoteName = remote.Name; - var lockRemote = new MenuItem(); - lockRemote.Header = remoteName; - lockRemote.Click += async (_, e) => - { - await _repo.LockLFSFileAsync(remoteName, path); - e.Handled = true; - }; - lfsLock.Items.Add(lockRemote); - } - } - lfs.Items.Add(lfsLock); - - var lfsUnlock = new MenuItem(); - lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock"); - lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock"); - if (_repo.Remotes.Count == 1) - { - lfsUnlock.Click += async (_, e) => - { - await _repo.UnlockLFSFileAsync(_repo.Remotes[0].Name, path, false, true); - e.Handled = true; - }; - } - else - { - foreach (var remote in _repo.Remotes) - { - var remoteName = remote.Name; - var unlockRemote = new MenuItem(); - unlockRemote.Header = remoteName; - unlockRemote.Click += async (_, e) => - { - await _repo.UnlockLFSFileAsync(remoteName, path, false, true); - e.Handled = true; - }; - lfsUnlock.Items.Add(unlockRemote); - } - } - lfs.Items.Add(lfsUnlock); - - menu.Items.Add(lfs); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - private void RefreshRevisionSearchSuggestion() { if (!string.IsNullOrEmpty(_revisionFileSearchFilter)) @@ -1011,25 +627,6 @@ private async Task SetViewingCommitAsync(Models.Object file) } } - private async Task ResetToThisRevisionAsync(string path) - { - var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'"); - - await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, $"{_commit.SHA}"); - log.Complete(); - } - - private async Task ResetToParentRevisionAsync(Models.Change change) - { - var log = _repo.CreateLog($"Reset File to '{_commit.SHA}~1'"); - - if (change.Index == Models.ChangeState.Renamed) - await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.OriginalPath, $"{_commit.SHA}~1"); - - await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.Path, $"{_commit.SHA}~1"); - log.Complete(); - } - [GeneratedRegex(@"\b(https?://|ftp://)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]")] private static partial Regex REG_URL_FORMAT(); @@ -1037,14 +634,13 @@ private async Task ResetToParentRevisionAsync(Models.Change change) private static partial Regex REG_SHA_FORMAT(); private Repository _repo = null; - private bool _rememberActivePageIndex = true; - private int _activePageIndex = 0; + private CommitDetailSharedData _sharedData = null; private Models.Commit _commit = null; private Models.CommitFullMessage _fullMessage = null; private Models.CommitSignInfo _signInfo = null; private List _children = null; - private List _changes = null; - private List _visibleChanges = null; + private List _changes = []; + private List _visibleChanges = []; private List _selectedChanges = null; private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; @@ -1056,5 +652,6 @@ private async Task ResetToParentRevisionAsync(Models.Change change) private string _revisionFileSearchFilter = string.Empty; private List _revisionFileSearchSuggestion = null; private bool _canOpenRevisionFileWithDefaultEditor = false; + private Vector _scrollOffset = Vector.Zero; } } diff --git a/src/ViewModels/ConfirmEmptyCommit.cs b/src/ViewModels/ConfirmEmptyCommit.cs deleted file mode 100644 index 87178b75e..000000000 --- a/src/ViewModels/ConfirmEmptyCommit.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; - -namespace SourceGit.ViewModels -{ - public class ConfirmEmptyCommit - { - public bool HasLocalChanges - { - get; - private set; - } - - public string Message - { - get; - private set; - } - - public ConfirmEmptyCommit(bool hasLocalChanges, Action onSure) - { - HasLocalChanges = hasLocalChanges; - Message = App.Text(hasLocalChanges ? "ConfirmEmptyCommit.WithLocalChanges" : "ConfirmEmptyCommit.NoLocalChanges"); - _onSure = onSure; - } - - public void StageAllThenCommit() - { - _onSure?.Invoke(true); - } - - public void Continue() - { - _onSure?.Invoke(false); - } - - private Action _onSure; - } -} diff --git a/src/ViewModels/Conflict.cs b/src/ViewModels/Conflict.cs index 7a14c7065..ccb8816d5 100644 --- a/src/ViewModels/Conflict.cs +++ b/src/ViewModels/Conflict.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace SourceGit.ViewModels { @@ -19,7 +20,7 @@ public ConflictSourceBranch(Repository repo, Models.Branch branch) { Name = branch.Name; Head = branch.Head; - Revision = new Commands.QuerySingleCommit(repo.FullPath, branch.Head).GetResultAsync().Result ?? new Models.Commit() { SHA = branch.Head }; + Revision = new Commands.QuerySingleCommit(repo.FullPath, branch.Head).GetResult() ?? new Models.Commit() { SHA = branch.Head }; } } @@ -68,7 +69,7 @@ public Conflict(Repository repo, WorkingCopy wc, Models.Change change) if (!isSubmodule && (_change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified)) { CanUseExternalMergeTool = true; - IsResolved = new Commands.IsConflictResolved(repo.FullPath, change).GetResultAsync().Result; + IsResolved = new Commands.IsConflictResolved(repo.FullPath, change).GetResult(); } switch (wc.InProgressContext) @@ -101,19 +102,19 @@ public Conflict(Repository repo, WorkingCopy wc, Models.Change change) } } - public void UseTheirs() + public async Task UseTheirsAsync() { - _wc.UseTheirs([_change]); + await _wc.UseTheirsAsync([_change]); } - public void UseMine() + public async Task UseMineAsync() { - _wc.UseMine([_change]); + await _wc.UseMineAsync([_change]); } - public async void OpenExternalMergeTool() + public async Task OpenExternalMergeToolAsync() { - await _wc.UseExternalMergeTool(_change); + await _wc.UseExternalMergeToolAsync(_change); } private WorkingCopy _wc = null; diff --git a/src/ViewModels/ConventionalCommitMessageBuilder.cs b/src/ViewModels/ConventionalCommitMessageBuilder.cs index d002a77d9..31d7f438e 100644 --- a/src/ViewModels/ConventionalCommitMessageBuilder.cs +++ b/src/ViewModels/ConventionalCommitMessageBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Text; @@ -9,11 +10,17 @@ namespace SourceGit.ViewModels { public class ConventionalCommitMessageBuilder : ObservableValidator { + public List Types + { + get; + private set; + } = []; + [Required(ErrorMessage = "Type of changes can not be null")] - public Models.ConventionalCommitType Type + public Models.ConventionalCommitType SelectedType { - get => _type; - set => SetProperty(ref _type, value, true); + get => _selectedType; + set => SetProperty(ref _selectedType, value, true); } public string Scope @@ -47,8 +54,10 @@ public string ClosedIssue set => SetProperty(ref _closedIssue, value); } - public ConventionalCommitMessageBuilder(Action onApply) + public ConventionalCommitMessageBuilder(string conventionalTypesOverride, Action onApply) { + Types = Models.ConventionalCommitType.Load(conventionalTypesOverride); + SelectedType = Types.Count > 0 ? Types[0] : null; _onApply = onApply; } @@ -63,7 +72,7 @@ public bool Apply() return false; var builder = new StringBuilder(); - builder.Append(_type.Type); + builder.Append(_selectedType.Type); if (!string.IsNullOrEmpty(_scope)) { @@ -103,7 +112,7 @@ public bool Apply() } private Action _onApply = null; - private Models.ConventionalCommitType _type = Models.ConventionalCommitType.Supported[0]; + private Models.ConventionalCommitType _selectedType = null; private string _scope = string.Empty; private string _description = string.Empty; private string _detail = string.Empty; diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index c8d591d5b..593c9fbdf 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -7,7 +7,7 @@ namespace SourceGit.ViewModels public class CreateBranch : Popup { [Required(ErrorMessage = "Branch name is required!")] - [RegularExpression(@"^[\w \-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] + [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(CreateBranch), nameof(ValidateBranchName))] public string Name { @@ -54,17 +54,6 @@ public bool AllowOverwrite } } - public bool IsRecurseSubmoduleVisible - { - get => _repo.Submodules.Count > 0; - } - - public bool RecurseSubmodules - { - get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; - set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; - } - public CreateBranch(Repository repo, Models.Branch branch) { _repo = repo; @@ -101,10 +90,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (!creator._allowOverwrite) { - var fixedName = Models.Branch.FixName(name); foreach (var b in creator._repo.Branches) { - if (b.FriendlyName.Equals(fixedName, StringComparison.Ordinal)) + if (b.FriendlyName.Equals(name, StringComparison.Ordinal)) return new ValidationResult("A branch with same name already exists!"); } } @@ -117,10 +105,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); - var fixedName = Models.Branch.FixName(_name); - var log = _repo.CreateLog($"Create Branch '{fixedName}'"); + var log = _repo.CreateLog($"Create Branch '{_name}'"); Use(log); if (CheckoutAfterCreated) @@ -131,12 +118,9 @@ public override async Task Sure() if (refs.Count == 0) { var msg = App.Text("Checkout.WarnLostCommits"); - var shouldContinue = await App.AskConfirmAsync(msg, null); + var shouldContinue = await App.AskConfirmAsync(msg); if (!shouldContinue) - { - _repo.SetWatcherEnabled(true); return true; - } } } } @@ -147,7 +131,7 @@ public override async Task Sure() var needPopStash = false; if (!DiscardLocalChanges) { - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) @@ -156,7 +140,6 @@ public override async Task Sure() if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } @@ -166,18 +149,15 @@ public override async Task Sure() succ = await new Commands.Checkout(_repo.FullPath) .Use(log) - .BranchAsync(fixedName, _baseOnRevision, DiscardLocalChanges, _allowOverwrite); + .BranchAsync(_name, _baseOnRevision, DiscardLocalChanges, _allowOverwrite); if (succ) { - if (IsRecurseSubmoduleVisible && RecurseSubmodules) - { - var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath).GetResultAsync(); - if (submodules.Count > 0) - await new Commands.Submodule(_repo.FullPath) - .Use(log) - .UpdateAsync(submodules, true, true); - } + var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath, false).GetResultAsync(); + if (submodules.Count > 0) + await new Commands.Submodule(_repo.FullPath) + .Use(log) + .UpdateAsync(submodules, false, true); if (needPopStash) await new Commands.Stash(_repo.FullPath) @@ -187,16 +167,23 @@ public override async Task Sure() } else { - succ = await new Commands.Branch(_repo.FullPath, fixedName) + succ = await new Commands.Branch(_repo.FullPath, _name) .Use(log) .CreateAsync(_baseOnRevision, _allowOverwrite); } + if (succ && BasedOn is Models.Branch { IsLocal: false } basedOn && _name.Equals(basedOn.Name, StringComparison.Ordinal)) + { + await new Commands.Branch(_repo.FullPath, _name) + .Use(log) + .SetUpstreamAsync(basedOn); + } + log.Complete(); if (succ && CheckoutAfterCreated) { - var fake = new Models.Branch() { IsLocal = true, FullName = $"refs/heads/{fixedName}" }; + var fake = new Models.Branch() { IsLocal = true, FullName = $"refs/heads/{_name}" }; if (BasedOn is Models.Branch { IsLocal: false } based) fake.Upstream = based.FullName; @@ -204,13 +191,11 @@ public override async Task Sure() if (folderEndIdx > 10) _repo.Settings.ExpandedBranchNodesInSideBar.Add(fake.FullName.Substring(0, folderEndIdx)); - if (_repo.HistoriesFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(fake, Models.FilterMode.Included, true, false); - + if (_repo.HistoryFilterMode == Models.FilterMode.Included) + _repo.SetBranchFilterMode(fake, Models.FilterMode.Included, false, false); } _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); if (CheckoutAfterCreated) { diff --git a/src/ViewModels/CreateTag.cs b/src/ViewModels/CreateTag.cs index d9113c132..4cf1fca30 100644 --- a/src/ViewModels/CreateTag.cs +++ b/src/ViewModels/CreateTag.cs @@ -13,7 +13,7 @@ public object BasedOn } [Required(ErrorMessage = "Tag name is required!")] - [RegularExpression(@"^(?!\.)(?!/)(?!.*\.$)(?!.*/$)(?!.*\.\.)[\w\-\./]+$", ErrorMessage = "Bad tag name format!")] + [RegularExpression(@"^(?!\.)(?!/)(?!.*\.$)(?!.*/$)(?!.*\.\.)[\w\-\+\./]+$", ErrorMessage = "Bad tag name format!")] [CustomValidation(typeof(CreateTag), nameof(ValidateTagName))] public string TagName { @@ -51,7 +51,7 @@ public CreateTag(Repository repo, Models.Branch branch) _basedOn = branch.Head; BasedOn = branch; - SignTag = new Commands.Config(repo.FullPath).GetAsync("tag.gpgsign").Result.Equals("true", StringComparison.OrdinalIgnoreCase); + SignTag = new Commands.Config(repo.FullPath).Get("tag.gpgsign").Equals("true", StringComparison.OrdinalIgnoreCase); } public CreateTag(Repository repo, Models.Commit commit) @@ -60,7 +60,7 @@ public CreateTag(Repository repo, Models.Commit commit) _basedOn = commit.SHA; BasedOn = commit; - SignTag = new Commands.Config(repo.FullPath).GetAsync("tag.gpgsign").Result.Equals("true", StringComparison.OrdinalIgnoreCase); + SignTag = new Commands.Config(repo.FullPath).Get("tag.gpgsign").Equals("true", StringComparison.OrdinalIgnoreCase); } public static ValidationResult ValidateTagName(string name, ValidationContext ctx) @@ -76,7 +76,7 @@ public static ValidationResult ValidateTagName(string name, ValidationContext ct public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Create tag..."; var remotes = PushToRemotes ? _repo.Remotes : null; @@ -99,7 +99,6 @@ public override async Task Sure() } log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/DeinitSubmodule.cs b/src/ViewModels/DeinitSubmodule.cs index 1769ef80c..f9fff198d 100644 --- a/src/ViewModels/DeinitSubmodule.cs +++ b/src/ViewModels/DeinitSubmodule.cs @@ -25,7 +25,7 @@ public DeinitSubmodule(Repository repo, string submodule) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "De-initialize Submodule"; var log = _repo.CreateLog("De-initialize Submodule"); @@ -36,7 +36,7 @@ public override async Task Sure() .DeinitAsync(Submodule, false); log.Complete(); - _repo.SetWatcherEnabled(true); + _repo.MarkSubmodulesDirtyManually(); return succ; } diff --git a/src/ViewModels/DeleteBranch.cs b/src/ViewModels/DeleteBranch.cs index bcc132c9d..6611194a2 100644 --- a/src/ViewModels/DeleteBranch.cs +++ b/src/ViewModels/DeleteBranch.cs @@ -41,7 +41,7 @@ public DeleteBranch(Repository repo, Models.Branch branch) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Deleting branch..."; var log = _repo.CreateLog("Delete Branch"); @@ -52,18 +52,22 @@ public override async Task Sure() await new Commands.Branch(_repo.FullPath, Target.Name) .Use(log) .DeleteLocalAsync(); + _repo.HistoryFilterCollection.RemoveFilter(Target.FullName, Models.FilterType.LocalBranch); if (_alsoDeleteTrackingRemote && TrackingRemoteBranch != null) + { await DeleteRemoteBranchAsync(TrackingRemoteBranch, log); + _repo.HistoryFilterCollection.RemoveFilter(TrackingRemoteBranch.FullName, Models.FilterType.RemoteBranch); + } } else { await DeleteRemoteBranchAsync(Target, log); + _repo.HistoryFilterCollection.RemoveFilter(Target.FullName, Models.FilterType.RemoteBranch); } log.Complete(); _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/DeleteMultipleBranches.cs b/src/ViewModels/DeleteMultipleBranches.cs index 086656e0a..b5d762a64 100644 --- a/src/ViewModels/DeleteMultipleBranches.cs +++ b/src/ViewModels/DeleteMultipleBranches.cs @@ -19,7 +19,7 @@ public DeleteMultipleBranches(Repository repo, List branches, boo public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Deleting multiple branches..."; var log = _repo.CreateLog("Delete Multiple Branches"); @@ -50,7 +50,6 @@ public override async Task Sure() log.Complete(); _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/DeleteMultipleTags.cs b/src/ViewModels/DeleteMultipleTags.cs new file mode 100644 index 000000000..a11901c34 --- /dev/null +++ b/src/ViewModels/DeleteMultipleTags.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class DeleteMultipleTags : Popup + { + public List Tags + { + get; + } + + public bool DeleteFromRemote + { + get; + set; + } = false; + + public DeleteMultipleTags(Repository repo, List tags) + { + _repo = repo; + Tags = tags; + } + + public override async Task Sure() + { + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = "Deleting multiple tags..."; + + var log = _repo.CreateLog("Delete Multiple Tags"); + Use(log); + + foreach (var tag in Tags) + { + var succ = await new Commands.Tag(_repo.FullPath, tag.Name) + .Use(log) + .DeleteAsync(); + + if (succ && DeleteFromRemote) + { + foreach (var r in _repo.Remotes) + await new Commands.Push(_repo.FullPath, r.Name, $"refs/tags/{tag.Name}", true) + .Use(log) + .RunAsync(); + } + } + + log.Complete(); + _repo.MarkTagsDirtyManually(); + return true; + } + + private readonly Repository _repo; + } +} diff --git a/src/ViewModels/DeleteRemote.cs b/src/ViewModels/DeleteRemote.cs index 595a7d52b..ceef24588 100644 --- a/src/ViewModels/DeleteRemote.cs +++ b/src/ViewModels/DeleteRemote.cs @@ -18,7 +18,7 @@ public DeleteRemote(Repository repo, Models.Remote remote) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Deleting remote ..."; var log = _repo.CreateLog("Delete Remote"); @@ -30,7 +30,6 @@ public override async Task Sure() log.Complete(); _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/DeleteSubmodule.cs b/src/ViewModels/DeleteSubmodule.cs index 34a77c6e1..998777421 100644 --- a/src/ViewModels/DeleteSubmodule.cs +++ b/src/ViewModels/DeleteSubmodule.cs @@ -18,7 +18,7 @@ public DeleteSubmodule(Repository repo, string submodule) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Deleting submodule ..."; var log = _repo.CreateLog("Delete Submodule"); @@ -29,7 +29,6 @@ public override async Task Sure() .DeleteAsync(Submodule); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/DeleteTag.cs b/src/ViewModels/DeleteTag.cs index 04844f75d..95790940c 100644 --- a/src/ViewModels/DeleteTag.cs +++ b/src/ViewModels/DeleteTag.cs @@ -24,7 +24,7 @@ public DeleteTag(Repository repo, Models.Tag tag) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Deleting tag '{Target.Name}' ..."; var remotes = PushToRemotes ? _repo.Remotes : []; @@ -44,8 +44,8 @@ public override async Task Sure() } log.Complete(); + _repo.HistoryFilterCollection.RemoveFilter(Target.Name, Models.FilterType.Tag); _repo.MarkTagsDirtyManually(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index 025ceddd4..c24ee6cec 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -1,9 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; - using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -24,7 +22,39 @@ public bool IgnoreWhitespace { Preferences.Instance.IgnoreWhitespaceChangesInDiff = value; OnPropertyChanged(); - LoadDiffContent(); + LoadContent(); + } + } + } + + public bool ShowEntireFile + { + get => Preferences.Instance.UseFullTextDiff; + set + { + if (value != Preferences.Instance.UseFullTextDiff) + { + Preferences.Instance.UseFullTextDiff = value; + OnPropertyChanged(); + + if (Content is TextDiffContext ctx) + LoadContent(); + } + } + } + + public bool UseSideBySide + { + get => Preferences.Instance.UseSideBySideDiff; + set + { + if (value != Preferences.Instance.UseSideBySideDiff) + { + Preferences.Instance.UseSideBySideDiff = value; + OnPropertyChanged(); + + if (Content is TextDiffContext ctx && ctx.IsSideBySide() != value) + Content = ctx.SwitchMode(); } } } @@ -72,35 +102,44 @@ public DiffContext(string repo, Models.DiffOption option, DiffContext previous = else Title = $"{_option.OrgPath} → {_option.Path}"; - LoadDiffContent(); - } - - public void ToggleFullTextDiff() - { - Preferences.Instance.UseFullTextDiff = !Preferences.Instance.UseFullTextDiff; - LoadDiffContent(); + LoadContent(); } public void IncrUnified() { UnifiedLines = _unifiedLines + 1; - LoadDiffContent(); + LoadContent(); } public void DecrUnified() { UnifiedLines = Math.Max(4, _unifiedLines - 1); - LoadDiffContent(); + LoadContent(); } public void OpenExternalMergeTool() { - var toolType = Preferences.Instance.ExternalMergeToolType; - var toolPath = Preferences.Instance.ExternalMergeToolPath; - new Commands.DiffTool(_repo, toolType, toolPath, _option).Open(); + new Commands.DiffTool(_repo, _option).Open(); + } + + public void CheckSettings() + { + if (Content is TextDiffContext ctx) + { + if ((ShowEntireFile && _info.UnifiedLines != _entireFileLine) || + (!ShowEntireFile && _info.UnifiedLines == _entireFileLine) || + (IgnoreWhitespace != _info.IgnoreWhitespace)) + { + LoadContent(); + return; + } + + if (ctx.IsSideBySide() != UseSideBySide) + Content = ctx.SwitchMode(); + } } - private void LoadDiffContent() + private void LoadContent() { if (_option.Path.EndsWith('/')) { @@ -111,7 +150,7 @@ private void LoadDiffContent() Task.Run(async () => { - var numLines = Preferences.Instance.UseFullTextDiff ? 999999999 : _unifiedLines; + var numLines = Preferences.Instance.UseFullTextDiff ? _entireFileLine : _unifiedLines; var ignoreWhitespace = Preferences.Instance.IgnoreWhitespaceChangesInDiff; var latest = await new Commands.Diff(_repo, _option, numLines, ignoreWhitespace) @@ -155,10 +194,7 @@ private void LoadDiffContent() } if (!isSubmodule) - { - latest.TextDiff.File = _option.Path; rs = latest.TextDiff; - } } else if (latest.IsBinary) { @@ -230,12 +266,22 @@ private void LoadDiffContent() Dispatcher.UIThread.Post(() => { - if (_content is Models.TextDiff old && rs is Models.TextDiff cur && old.File == cur.File) - cur.ScrollOffset = old.ScrollOffset; - FileModeChange = latest.FileModeChange; - Content = rs; - IsTextDiff = rs is Models.TextDiff; + + if (rs is Models.TextDiff cur) + { + IsTextDiff = true; + + if (Preferences.Instance.UseSideBySideDiff) + Content = new TwoSideTextDiff(_option, cur, _content as TextDiffContext); + else + Content = new CombinedTextDiff(_option, cur, _content as TextDiffContext); + } + else + { + IsTextDiff = false; + Content = rs; + } }); }); } @@ -281,6 +327,7 @@ public bool IsSame(Info other) } } + private readonly int _entireFileLine = 999999999; private readonly string _repo; private readonly Models.DiffOption _option = null; private string _fileModeChange = string.Empty; diff --git a/src/ViewModels/DirHistories.cs b/src/ViewModels/DirHistories.cs index 04fee2b27..d4d37f25f 100644 --- a/src/ViewModels/DirHistories.cs +++ b/src/ViewModels/DirHistories.cs @@ -38,7 +38,6 @@ public Models.Commit SelectedCommit public CommitDetail Detail { get => _detail; - private set => SetProperty(ref _detail, value); } public DirHistories(Repository repo, string dir, string revision = null) @@ -49,7 +48,7 @@ public DirHistories(Repository repo, string dir, string revision = null) Title = dir; _repo = repo; - _detail = new CommitDetail(repo, false); + _detail = new CommitDetail(repo, null); _detail.SearchChangeFilter = dir; Task.Run(async () => @@ -87,7 +86,7 @@ public string GetCommitFullMessage(Models.Commit commit) if (_cachedCommitFullMessage.TryGetValue(sha, out var msg)) return msg; - msg = new Commands.QueryCommitFullMessage(_repo.FullPath, sha).GetResultAsync().Result; + msg = new Commands.QueryCommitFullMessage(_repo.FullPath, sha).GetResult(); _cachedCommitFullMessage[sha] = msg; return msg; } diff --git a/src/ViewModels/Discard.cs b/src/ViewModels/Discard.cs index 4e286d148..2be1752de 100644 --- a/src/ViewModels/Discard.cs +++ b/src/ViewModels/Discard.cs @@ -64,20 +64,24 @@ public Discard(Repository repo, List changes) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = _changes == null ? "Discard all local changes ..." : $"Discard total {_changes.Count} changes ..."; - var log = _repo.CreateLog("Discard all"); + var log = _repo.CreateLog("Discard Changes"); Use(log); if (Mode is DiscardAllMode all) + { await Commands.Discard.AllAsync(_repo.FullPath, all.IncludeUntracked, all.IncludeIgnored, log); + _repo.ClearCommitMessage(); + } else + { await Commands.Discard.ChangesAsync(_repo.FullPath, _changes, log); + } log.Complete(); _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/DropHead.cs b/src/ViewModels/DropHead.cs new file mode 100644 index 000000000..b5baed06f --- /dev/null +++ b/src/ViewModels/DropHead.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class DropHead : Popup + { + public Models.Commit Target + { + get; + } + + public Models.Commit NewHead + { + get; + } + + public DropHead(Repository repo, Models.Commit target, Models.Commit parent) + { + _repo = repo; + Target = target; + NewHead = parent; + } + + public override async Task Sure() + { + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = $"Drop HEAD '{Target.SHA}' ..."; + + var log = _repo.CreateLog($"Drop '{Target.SHA}'"); + Use(log); + + var changes = await new Commands.QueryLocalChanges(_repo.FullPath, true).GetResultAsync(); + var needAutoStash = changes.Count > 0; + var succ = false; + + if (needAutoStash) + { + succ = await new Commands.Stash(_repo.FullPath) + .Use(log) + .PushAsync("DROP_HEAD_AUTO_STASH", true); + if (!succ) + { + log.Complete(); + return false; + } + } + + succ = await new Commands.Reset(_repo.FullPath, NewHead.SHA, "--hard") + .Use(log) + .ExecAsync(); + + if (succ && needAutoStash) + await new Commands.Stash(_repo.FullPath) + .Use(log) + .PopAsync("stash@{0}"); + + log.Complete(); + return succ; + } + + private readonly Repository _repo; + } +} diff --git a/src/ViewModels/DropStash.cs b/src/ViewModels/DropStash.cs index 8c826c2cd..7d4136ab9 100644 --- a/src/ViewModels/DropStash.cs +++ b/src/ViewModels/DropStash.cs @@ -14,6 +14,7 @@ public DropStash(Repository repo, Models.Stash stash) public override async Task Sure() { + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Dropping stash: {Stash.Name}"; var log = _repo.CreateLog("Drop Stash"); @@ -24,6 +25,7 @@ public override async Task Sure() .DropAsync(Stash.Name); log.Complete(); + _repo.MarkStashesDirtyManually(); return true; } diff --git a/src/ViewModels/EditBranchDescription.cs b/src/ViewModels/EditBranchDescription.cs new file mode 100644 index 000000000..d4b6634f9 --- /dev/null +++ b/src/ViewModels/EditBranchDescription.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class EditBranchDescription : Popup + { + public Models.Branch Target + { + get; + } + + public string Description + { + get => _description; + set => SetProperty(ref _description, value); + } + + public EditBranchDescription(Repository repo, Models.Branch target, string desc) + { + Target = target; + + _repo = repo; + _originalDescription = desc; + _description = desc; + } + + public override async Task Sure() + { + var trimmed = _description.Trim(); + if (string.IsNullOrEmpty(trimmed)) + { + if (string.IsNullOrEmpty(_originalDescription)) + return true; + } + else if (trimmed.Equals(_originalDescription, StringComparison.Ordinal)) + { + return true; + } + + var log = _repo.CreateLog("Edit Branch Description"); + Use(log); + + await new Commands.Config(_repo.FullPath) + .Use(log) + .SetAsync($"branch.{Target.Name}.description", trimmed); + + log.Complete(); + return true; + } + + private readonly Repository _repo; + private string _originalDescription = string.Empty; + private string _description = string.Empty; + } +} diff --git a/src/ViewModels/EditRemote.cs b/src/ViewModels/EditRemote.cs index 4c7e3237a..84b950e2a 100644 --- a/src/ViewModels/EditRemote.cs +++ b/src/ViewModels/EditRemote.cs @@ -53,7 +53,7 @@ public EditRemote(Repository repo, Models.Remote remote) _useSSH = Models.Remote.IsSSH(remote.URL); if (_useSSH) - SSHKey = new Commands.Config(repo.FullPath).GetAsync($"remote.{remote.Name}.sshkey").Result; + _sshkey = new Commands.Config(repo.FullPath).Get($"remote.{remote.Name}.sshkey"); } public static ValidationResult ValidateRemoteName(string name, ValidationContext ctx) @@ -100,7 +100,7 @@ public static ValidationResult ValidateSSHKey(string sshkey, ValidationContext c public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Editing remote '{_remote.Name}' ..."; if (_remote.Name != _name) @@ -122,8 +122,6 @@ public override async Task Sure() await new Commands.Remote(_repo.FullPath).SetURLAsync(_name, _url, true); await new Commands.Config(_repo.FullPath).SetAsync($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); - - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/EditRepositoryNode.cs b/src/ViewModels/EditRepositoryNode.cs index 38d072a14..d7176c786 100644 --- a/src/ViewModels/EditRepositoryNode.cs +++ b/src/ViewModels/EditRepositoryNode.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -18,6 +19,11 @@ public string Name set => SetProperty(ref _name, value, true); } + public List Bookmarks + { + get; + } + public int Bookmark { get => _bookmark; @@ -37,6 +43,10 @@ public EditRepositoryNode(RepositoryNode node) _name = node.Name; _isRepository = node.IsRepository; _bookmark = node.Bookmark; + + Bookmarks = new List(); + for (var i = 0; i < Models.Bookmarks.Brushes.Length; i++) + Bookmarks.Add(i); } public override Task Sure() diff --git a/src/ViewModels/ExecuteCustomAction.cs b/src/ViewModels/ExecuteCustomAction.cs index fb0f2f819..09026ee21 100644 --- a/src/ViewModels/ExecuteCustomAction.cs +++ b/src/ViewModels/ExecuteCustomAction.cs @@ -15,9 +15,9 @@ public interface ICustomActionControlParameter public class CustomActionControlTextBox : ICustomActionControlParameter { - public string Label { get; set; } = string.Empty; - public string Placeholder { get; set; } = string.Empty; - public string Text { get; set; } = string.Empty; + public string Label { get; set; } + public string Placeholder { get; set; } + public string Text { get; set; } public CustomActionControlTextBox(string label, string placeholder, string defaultValue) { @@ -31,9 +31,9 @@ public CustomActionControlTextBox(string label, string placeholder, string defau public class CustomActionControlPathSelector : ObservableObject, ICustomActionControlParameter { - public string Label { get; set; } = string.Empty; - public string Placeholder { get; set; } = string.Empty; - public bool IsFolder { get; set; } = false; + public string Label { get; set; } + public string Placeholder { get; set; } + public bool IsFolder { get; set; } public string Path { @@ -51,14 +51,14 @@ public CustomActionControlPathSelector(string label, string placeholder, bool is public string GetValue() => _path; - private string _path = string.Empty; + private string _path; } public class CustomActionControlCheckBox : ICustomActionControlParameter { - public string Label { get; set; } = string.Empty; - public string ToolTip { get; set; } = string.Empty; - public string CheckedValue { get; set; } = string.Empty; + public string Label { get; set; } + public string ToolTip { get; set; } + public string CheckedValue { get; set; } public bool IsChecked { get; set; } public CustomActionControlCheckBox(string label, string tooltip, string checkedValue, bool isChecked) @@ -74,8 +74,8 @@ public CustomActionControlCheckBox(string label, string tooltip, string checkedV public class CustomActionControlComboBox : ObservableObject, ICustomActionControlParameter { - public string Label { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; + public string Label { get; set; } + public string Description { get; set; } public List Options { get; set; } = []; public string Value @@ -119,41 +119,17 @@ public List ControlParameters get; } = []; - public ExecuteCustomAction(Repository repo, Models.CustomAction action) + public ExecuteCustomAction(Repository repo, Models.CustomAction action, object scopeTarget) { _repo = repo; CustomAction = action; - Target = new Models.Null(); - PrepareControlParameters(); - } - - public ExecuteCustomAction(Repository repo, Models.CustomAction action, Models.Branch branch) - { - _repo = repo; - CustomAction = action; - Target = branch; - PrepareControlParameters(); - } - - public ExecuteCustomAction(Repository repo, Models.CustomAction action, Models.Commit commit) - { - _repo = repo; - CustomAction = action; - Target = commit; - PrepareControlParameters(); - } - - public ExecuteCustomAction(Repository repo, Models.CustomAction action, Models.Tag tag) - { - _repo = repo; - CustomAction = action; - Target = tag; + Target = scopeTarget ?? new Models.Null(); PrepareControlParameters(); } public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Run custom action ..."; var cmdline = PrepareStringByTarget(CustomAction.Arguments); @@ -174,7 +150,6 @@ public override async Task Sure() _ = Task.Run(() => Run(cmdline)); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } @@ -206,9 +181,11 @@ private string PrepareStringByTarget(string org) return Target switch { - Models.Branch b => org.Replace("${BRANCH}", b.FriendlyName), + Models.Branch b => org.Replace("${BRANCH_FRIENDLY_NAME}", b.FriendlyName).Replace("${BRANCH}", b.Name).Replace("${REMOTE}", b.Remote), Models.Commit c => org.Replace("${SHA}", c.SHA), Models.Tag t => org.Replace("${TAG}", t.Name), + Models.Remote r => org.Replace("${REMOTE}", r.Name), + Models.CustomActionTargetFile f => org.Replace("${FILE}", f.File).Replace("${SHA}", f.Revision?.SHA ?? string.Empty), _ => org }; } @@ -250,8 +227,8 @@ private async Task RunAsync(string args, Models.ICommandLog log) start.StandardErrorEncoding = Encoding.UTF8; start.WorkingDirectory = _repo.FullPath; - using var proc = new Process() { StartInfo = start }; - var builder = new StringBuilder(); + using var proc = new Process(); + proc.StartInfo = start; proc.OutputDataReceived += (_, e) => { @@ -259,6 +236,7 @@ private async Task RunAsync(string args, Models.ICommandLog log) log?.AppendLine(e.Data); }; + var builder = new StringBuilder(); proc.ErrorDataReceived += (_, e) => { if (e.Data != null) diff --git a/src/ViewModels/Fetch.cs b/src/ViewModels/Fetch.cs index 0930e7773..43bc5a18a 100644 --- a/src/ViewModels/Fetch.cs +++ b/src/ViewModels/Fetch.cs @@ -10,10 +10,19 @@ public List Remotes get => _repo.Remotes; } + public bool IsFetchAllRemoteVisible + { + get; + } + public bool FetchAllRemotes { get => _fetchAllRemotes; - set => SetProperty(ref _fetchAllRemotes, value); + set + { + if (SetProperty(ref _fetchAllRemotes, value) && IsFetchAllRemoteVisible) + _repo.Settings.FetchAllRemotes = value; + } } public Models.Remote SelectedRemote @@ -37,7 +46,8 @@ public bool Force public Fetch(Repository repo, Models.Remote preferredRemote = null) { _repo = repo; - _fetchAllRemotes = preferredRemote == null; + IsFetchAllRemoteVisible = repo.Remotes.Count > 1 && preferredRemote == null; + _fetchAllRemotes = IsFetchAllRemoteVisible && _repo.Settings.FetchAllRemotes; if (preferredRemote != null) { @@ -56,8 +66,9 @@ public Fetch(Repository repo, Models.Remote preferredRemote = null) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); + var navigateToUpstreamHEAD = _repo.SelectedView is Histories { AutoSelectedCommit: { IsCurrentHead: true } }; var notags = _repo.Settings.FetchWithoutTags; var force = _repo.Settings.EnableForceOnFetch; var log = _repo.CreateLog("Fetch"); @@ -79,19 +90,21 @@ public override async Task Sure() log.Complete(); - var upstream = _repo.CurrentBranch?.Upstream; - if (!string.IsNullOrEmpty(upstream)) + if (navigateToUpstreamHEAD) { - var upstreamHead = await new Commands.QueryRevisionByRefName(_repo.FullPath, upstream.Substring(13)).GetResultAsync(); - _repo.NavigateToCommit(upstreamHead, true); + var upstream = _repo.CurrentBranch?.Upstream; + if (!string.IsNullOrEmpty(upstream)) + { + var upstreamHead = await new Commands.QueryRevisionByRefName(_repo.FullPath, upstream.Substring(13)).GetResultAsync(); + _repo.NavigateToCommit(upstreamHead, true); + } } _repo.MarkFetched(); - _repo.SetWatcherEnabled(true); return true; } private readonly Repository _repo = null; - private bool _fetchAllRemotes; + private bool _fetchAllRemotes = false; } } diff --git a/src/ViewModels/FetchInto.cs b/src/ViewModels/FetchInto.cs index 3a0879b9e..bcf461203 100644 --- a/src/ViewModels/FetchInto.cs +++ b/src/ViewModels/FetchInto.cs @@ -23,7 +23,7 @@ public FetchInto(Repository repo, Models.Branch local, Models.Branch upstream) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Fast-Forward ..."; var log = _repo.CreateLog($"Fetch Into '{Local.FriendlyName}'"); @@ -35,9 +35,12 @@ public override async Task Sure() log.Complete(); - var newHead = await new Commands.QueryRevisionByRefName(_repo.FullPath, Local.Name).GetResultAsync(); - _repo.NavigateToCommit(newHead, true); - _repo.SetWatcherEnabled(true); + if (_repo.SelectedViewIndex == 0) + { + var newHead = await new Commands.QueryRevisionByRefName(_repo.FullPath, Local.Name).GetResultAsync(); + _repo.NavigateToCommit(newHead, true); + } + return true; } diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 19066ca21..91cd56236 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -36,7 +36,7 @@ public object ViewContent set => SetProperty(ref _viewContent, value); } - public FileHistoriesSingleRevision(Repository repo, string file, Models.Commit revision, bool prevIsDiffMode) + public FileHistoriesSingleRevision(string repo, string file, Models.Commit revision, bool prevIsDiffMode) { _repo = repo; _file = file; @@ -49,7 +49,7 @@ public FileHistoriesSingleRevision(Repository repo, string file, Models.Commit r public async Task ResetToSelectedRevisionAsync() { - return await new Commands.Checkout(_repo.FullPath) + return await new Commands.Checkout(_repo) .FileWithRevisionAsync(_file, $"{_revision.SHA}") .ConfigureAwait(false); } @@ -59,13 +59,13 @@ public async Task OpenWithDefaultEditorAsync() if (_viewContent is not FileHistoriesRevisionFile { CanOpenWithDefaultEditor: true }) return; - var fullPath = Native.OS.GetAbsPath(_repo.FullPath, _file); + var fullPath = Native.OS.GetAbsPath(_repo, _file); var fileName = Path.GetFileNameWithoutExtension(fullPath) ?? ""; var fileExt = Path.GetExtension(fullPath) ?? ""; var tmpFile = Path.Combine(Path.GetTempPath(), $"{fileName}~{_revision.SHA.AsSpan(0, 10)}{fileExt}"); await Commands.SaveRevisionFile - .RunAsync(_repo.FullPath, _revision.SHA, _file, tmpFile) + .RunAsync(_repo, _revision.SHA, _file, tmpFile) .ConfigureAwait(false); Native.OS.OpenWithDefaultEditor(tmpFile); @@ -74,98 +74,92 @@ await Commands.SaveRevisionFile private void RefreshViewContent() { if (_isDiffMode) + { SetViewContentAsDiff(); - else - SetViewContentAsRevisionFile(); + return; + } + + Task.Run(async () => + { + var objs = await new Commands.QueryRevisionObjects(_repo, _revision.SHA, _file) + .GetResultAsync() + .ConfigureAwait(false); + + if (objs.Count == 0) + { + Dispatcher.UIThread.Post(() => ViewContent = new FileHistoriesRevisionFile(_file)); + return; + } + + var revisionContent = await GetRevisionFileContentAsync(objs[0]).ConfigureAwait(false); + Dispatcher.UIThread.Post(() => ViewContent = revisionContent); + }); } - private void SetViewContentAsRevisionFile() + private async Task GetRevisionFileContentAsync(Models.Object obj) { - var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _revision.SHA, _file).GetResultAsync().Result; - if (objs.Count == 0) + if (obj.Type == Models.ObjectType.Blob) { - ViewContent = new FileHistoriesRevisionFile(_file); - return; + var isBinary = await new Commands.IsBinary(_repo, _revision.SHA, _file).GetResultAsync().ConfigureAwait(false); + if (isBinary) + { + var imgDecoder = ImageSource.GetDecoder(_file); + if (imgDecoder != Models.ImageDecoder.None) + { + var source = await ImageSource.FromRevisionAsync(_repo, _revision.SHA, _file, imgDecoder).ConfigureAwait(false); + var image = new Models.RevisionImageFile(_file, source.Bitmap, source.Size); + return new FileHistoriesRevisionFile(_file, image, true); + } + + var size = await new Commands.QueryFileSize(_repo, _file, _revision.SHA).GetResultAsync().ConfigureAwait(false); + var binaryFile = new Models.RevisionBinaryFile() { Size = size }; + return new FileHistoriesRevisionFile(_file, binaryFile, true); + } + + var contentStream = await Commands.QueryFileContent.RunAsync(_repo, _revision.SHA, _file).ConfigureAwait(false); + var content = await new StreamReader(contentStream).ReadToEndAsync(); + var lfs = Models.LFSObject.Parse(content); + if (lfs != null) + { + var imgDecoder = ImageSource.GetDecoder(_file); + if (imgDecoder != Models.ImageDecoder.None) + { + var combined = new RevisionLFSImage(_repo, _file, lfs, imgDecoder); + return new FileHistoriesRevisionFile(_file, combined, true); + } + + var rlfs = new Models.RevisionLFSObject() { Object = lfs }; + return new FileHistoriesRevisionFile(_file, rlfs, true); + } + + var txt = new Models.RevisionTextFile() { FileName = obj.Path, Content = content }; + return new FileHistoriesRevisionFile(_file, txt, true); } - var obj = objs[0]; - switch (obj.Type) + if (obj.Type == Models.ObjectType.Commit) { - case Models.ObjectType.Blob: - Task.Run(async () => - { - var isBinary = await new Commands.IsBinary(_repo.FullPath, _revision.SHA, _file).GetResultAsync().ConfigureAwait(false); - if (isBinary) - { - var imgDecoder = ImageSource.GetDecoder(_file); - if (imgDecoder != Models.ImageDecoder.None) - { - var source = await ImageSource.FromRevisionAsync(_repo.FullPath, _revision.SHA, _file, imgDecoder).ConfigureAwait(false); - var image = new Models.RevisionImageFile(_file, source.Bitmap, source.Size); - Dispatcher.UIThread.Post(() => ViewContent = new FileHistoriesRevisionFile(_file, image, true)); - } - else - { - var size = await new Commands.QueryFileSize(_repo.FullPath, _file, _revision.SHA).GetResultAsync().ConfigureAwait(false); - var binaryFile = new Models.RevisionBinaryFile() { Size = size }; - Dispatcher.UIThread.Post(() => ViewContent = new FileHistoriesRevisionFile(_file, binaryFile, true)); - } - - return; - } - - var contentStream = await Commands.QueryFileContent.RunAsync(_repo.FullPath, _revision.SHA, _file).ConfigureAwait(false); - var content = await new StreamReader(contentStream).ReadToEndAsync(); - var lfs = Models.LFSObject.Parse(content); - if (lfs != null) - { - var imgDecoder = ImageSource.GetDecoder(_file); - if (imgDecoder != Models.ImageDecoder.None) - { - var combined = new RevisionLFSImage(_repo.FullPath, _file, lfs, imgDecoder); - Dispatcher.UIThread.Post(() => ViewContent = new FileHistoriesRevisionFile(_file, combined, true)); - } - else - { - var rlfs = new Models.RevisionLFSObject() { Object = lfs }; - Dispatcher.UIThread.Post(() => ViewContent = new FileHistoriesRevisionFile(_file, rlfs, true)); - } - } - else - { - var txt = new Models.RevisionTextFile() { FileName = obj.Path, Content = content }; - Dispatcher.UIThread.Post(() => ViewContent = new FileHistoriesRevisionFile(_file, txt, true)); - } - }); - break; - case Models.ObjectType.Commit: - Task.Run(async () => - { - var submoduleRoot = Path.Combine(_repo.FullPath, _file); - var commit = await new Commands.QuerySingleCommit(submoduleRoot, obj.SHA).GetResultAsync().ConfigureAwait(false); - var message = commit != null ? await new Commands.QueryCommitFullMessage(submoduleRoot, obj.SHA).GetResultAsync().ConfigureAwait(false) : null; - var module = new Models.RevisionSubmodule() - { - Commit = commit ?? new Models.Commit() { SHA = obj.SHA }, - FullMessage = new Models.CommitFullMessage { Message = message } - }; - - Dispatcher.UIThread.Post(() => ViewContent = new FileHistoriesRevisionFile(_file, module)); - }); - break; - default: - ViewContent = new FileHistoriesRevisionFile(_file); - break; + var submoduleRoot = Path.Combine(_repo, _file); + var commit = await new Commands.QuerySingleCommit(submoduleRoot, obj.SHA).GetResultAsync().ConfigureAwait(false); + var message = commit != null ? await new Commands.QueryCommitFullMessage(submoduleRoot, obj.SHA).GetResultAsync().ConfigureAwait(false) : null; + var module = new Models.RevisionSubmodule() + { + Commit = commit ?? new Models.Commit() { SHA = obj.SHA }, + FullMessage = new Models.CommitFullMessage { Message = message } + }; + + return new FileHistoriesRevisionFile(_file, module); } + + return new FileHistoriesRevisionFile(_file); } private void SetViewContentAsDiff() { var option = new Models.DiffOption(_revision, _file); - ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext); + ViewContent = new DiffContext(_repo, option, _viewContent as DiffContext); } - private Repository _repo = null; + private string _repo = null; private string _file = null; private Models.Commit _revision = null; private bool _isDiffMode = false; @@ -192,7 +186,7 @@ public DiffContext ViewContent set => SetProperty(ref _viewContent, value); } - public FileHistoriesCompareRevisions(Repository repo, string file, Models.Commit start, Models.Commit end) + public FileHistoriesCompareRevisions(string repo, string file, Models.Commit start, Models.Commit end) { _repo = repo; _file = file; @@ -210,7 +204,7 @@ public void Swap() public async Task SaveAsPatch(string saveTo) { return await Commands.SaveChangesAsPatch - .ProcessRevisionCompareChangesAsync(_repo.FullPath, _changes, _startPoint.SHA, _endPoint.SHA, saveTo) + .ProcessRevisionCompareChangesAsync(_repo, _changes, _startPoint.SHA, _endPoint.SHA, saveTo) .ConfigureAwait(false); } @@ -218,7 +212,7 @@ private void RefreshViewContent() { Task.Run(async () => { - _changes = await new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).ReadAsync().ConfigureAwait(false); + _changes = await new Commands.CompareRevisions(_repo, _startPoint.SHA, _endPoint.SHA, _file).ReadAsync().ConfigureAwait(false); if (_changes.Count == 0) { Dispatcher.UIThread.Post(() => ViewContent = null); @@ -226,12 +220,12 @@ private void RefreshViewContent() else { var option = new Models.DiffOption(_startPoint.SHA, _endPoint.SHA, _changes[0]); - Dispatcher.UIThread.Post(() => ViewContent = new DiffContext(_repo.FullPath, option, _viewContent)); + Dispatcher.UIThread.Post(() => ViewContent = new DiffContext(_repo, option, _viewContent)); } }); } - private Repository _repo = null; + private string _repo = null; private string _file = null; private Models.Commit _startPoint = null; private Models.Commit _endPoint = null; @@ -270,7 +264,7 @@ public object ViewContent private set => SetProperty(ref _viewContent, value); } - public FileHistories(Repository repo, string file, string commit = null) + public FileHistories(string repo, string file, string commit = null) { if (!string.IsNullOrEmpty(commit)) Title = $"{file} @ {commit}"; @@ -288,7 +282,7 @@ public FileHistories(Repository repo, string file, string commit = null) .Append(" -- ") .Append(file.Quoted()); - var commits = await new Commands.QueryCommits(_repo.FullPath, argsBuilder.ToString(), false) + var commits = await new Commands.QueryCommits(_repo, argsBuilder.ToString(), false) .GetResultAsync() .ConfigureAwait(false); @@ -317,7 +311,18 @@ public FileHistories(Repository repo, string file, string commit = null) public void NavigateToCommit(Models.Commit commit) { - _repo.NavigateToCommit(commit.SHA); + var launcher = App.GetLauncher(); + if (launcher != null) + { + foreach (var page in launcher.Pages) + { + if (page.Data is Repository repo && repo.FullPath.Equals(_repo, StringComparison.Ordinal)) + { + repo.NavigateToCommit(commit.SHA); + break; + } + } + } } public string GetCommitFullMessage(Models.Commit commit) @@ -326,12 +331,12 @@ public string GetCommitFullMessage(Models.Commit commit) if (_fullCommitMessages.TryGetValue(sha, out var msg)) return msg; - msg = new Commands.QueryCommitFullMessage(_repo.FullPath, sha).GetResultAsync().Result; + msg = new Commands.QueryCommitFullMessage(_repo, sha).GetResult(); _fullCommitMessages[sha] = msg; return msg; } - private readonly Repository _repo = null; + private readonly string _repo = null; private bool _isLoading = true; private bool _prevIsDiffMode = true; private List _commits = null; diff --git a/src/ViewModels/FileHistoryCommandPalette.cs b/src/ViewModels/FileHistoryCommandPalette.cs new file mode 100644 index 000000000..fa2438163 --- /dev/null +++ b/src/ViewModels/FileHistoryCommandPalette.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Threading; + +namespace SourceGit.ViewModels +{ + public class FileHistoryCommandPalette : ICommandPalette + { + public bool IsLoading + { + get => _isLoading; + private set => SetProperty(ref _isLoading, value); + } + + public List VisibleFiles + { + get => _visibleFiles; + private set => SetProperty(ref _visibleFiles, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateVisible(); + } + } + + public string SelectedFile + { + get => _selectedFile; + set => SetProperty(ref _selectedFile, value); + } + + public FileHistoryCommandPalette(Launcher launcher, string repo) + { + _launcher = launcher; + _repo = repo; + _isLoading = true; + + Task.Run(async () => + { + var files = await new Commands.QueryRevisionFileNames(_repo, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + IsLoading = false; + _repoFiles = files; + UpdateVisible(); + }); + }); + } + + public override void Cleanup() + { + _launcher = null; + _repo = null; + _repoFiles.Clear(); + _filter = null; + _visibleFiles.Clear(); + _selectedFile = null; + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public void Launch() + { + if (!string.IsNullOrEmpty(_selectedFile)) + App.ShowWindow(new FileHistories(_repo, _selectedFile)); + _launcher.CancelCommandPalette(); + } + + private void UpdateVisible() + { + if (_repoFiles is { Count: > 0 }) + { + if (string.IsNullOrEmpty(_filter)) + { + VisibleFiles = _repoFiles; + + if (string.IsNullOrEmpty(_selectedFile)) + SelectedFile = _repoFiles[0]; + } + else + { + var visible = new List(); + + foreach (var f in _repoFiles) + { + if (f.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + visible.Add(f); + } + + var autoSelected = _selectedFile; + if (visible.Count == 0) + autoSelected = null; + else if (string.IsNullOrEmpty(_selectedFile) || !visible.Contains(_selectedFile)) + autoSelected = visible[0]; + + VisibleFiles = visible; + SelectedFile = autoSelected; + } + } + } + + private Launcher _launcher = null; + private string _repo = null; + private bool _isLoading = false; + private List _repoFiles = null; + private string _filter = string.Empty; + private List _visibleFiles = []; + private string _selectedFile = null; + } +} diff --git a/src/ViewModels/FilterModeInGraph.cs b/src/ViewModels/FilterModeInGraph.cs index 9930b8164..26d6edd13 100644 --- a/src/ViewModels/FilterModeInGraph.cs +++ b/src/ViewModels/FilterModeInGraph.cs @@ -1,5 +1,4 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { @@ -23,20 +22,9 @@ public FilterModeInGraph(Repository repo, object target) _target = target; if (_target is Models.Branch b) - _mode = GetFilterMode(b.FullName); + _mode = _repo.HistoryFilterCollection.GetFilterMode(b.FullName); else if (_target is Models.Tag t) - _mode = GetFilterMode(t.Name); - } - - private Models.FilterMode GetFilterMode(string pattern) - { - foreach (var filter in _repo.Settings.HistoriesFilters) - { - if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) - return filter.Mode; - } - - return Models.FilterMode.None; + _mode = _repo.HistoryFilterCollection.GetFilterMode(t.Name); } private void SetFilterMode(Models.FilterMode mode) diff --git a/src/ViewModels/GitFlowFinish.cs b/src/ViewModels/GitFlowFinish.cs index 959fa10e0..7ed48cbf7 100644 --- a/src/ViewModels/GitFlowFinish.cs +++ b/src/ViewModels/GitFlowFinish.cs @@ -42,7 +42,7 @@ public GitFlowFinish(Repository repo, Models.Branch branch, Models.GitFlowBranch public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Git Flow - Finish {Branch.Name} ..."; var log = _repo.CreateLog("GitFlow - Finish"); @@ -53,7 +53,6 @@ public override async Task Sure() var succ = await Commands.GitFlow.FinishAsync(_repo.FullPath, Type, name, Squash, AutoPush, KeepBranch, log); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/GitFlowStart.cs b/src/ViewModels/GitFlowStart.cs index e6055f8e7..ee8673aa3 100644 --- a/src/ViewModels/GitFlowStart.cs +++ b/src/ViewModels/GitFlowStart.cs @@ -51,7 +51,7 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Git Flow - Start {Prefix}{_name} ..."; var log = _repo.CreateLog("GitFlow - Start"); @@ -59,7 +59,6 @@ public override async Task Sure() var succ = await Commands.GitFlow.StartAsync(_repo.FullPath, Type, _name, log); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index d16f8680c..455ce43ba 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -2,11 +2,10 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using System.Text; +using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Platform.Storage; - +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -90,6 +89,7 @@ public GridLength BottomArea public Histories(Repository repo) { _repo = repo; + _commitDetailSharedData = new CommitDetailSharedData(); } public void Dispose() @@ -136,48 +136,34 @@ public Models.BisectState UpdateBisectInfo() public void NavigateTo(string commitSHA) { var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal)); - if (commit == null) - { - AutoSelectedCommit = null; - commit = new Commands.QuerySingleCommit(_repo.FullPath, commitSHA).GetResultAsync().Result; - } - else - { - AutoSelectedCommit = commit; - NavigationId = _navigationId + 1; - } - if (commit != null) { - if (_detailContext is CommitDetail detail) - { - detail.Commit = commit; - } - else - { - var commitDetail = new CommitDetail(_repo, true); - commitDetail.Commit = commit; - DetailContext = commitDetail; - } + NavigateTo(commit); + return; } - else + + Task.Run(async () => { - DetailContext = null; - } + var c = await new Commands.QuerySingleCommit(_repo.FullPath, commitSHA) + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => NavigateTo(c)); + }); } public void Select(IList commits) { if (commits.Count == 0) { - _repo.SelectedSearchedCommit = null; + _repo.SearchCommitContext.Selected = null; DetailContext = null; } else if (commits.Count == 1) { var commit = (commits[0] as Models.Commit)!; - if (_repo.SelectedSearchedCommit == null || _repo.SelectedSearchedCommit.SHA != commit.SHA) - _repo.SelectedSearchedCommit = _repo.SearchedCommits.Find(x => x.SHA == commit.SHA); + if (_repo.SearchCommitContext.Selected == null || _repo.SearchCommitContext.Selected.SHA != commit.SHA) + _repo.SearchCommitContext.Selected = _repo.SearchCommitContext.Results?.Find(x => x.SHA == commit.SHA); AutoSelectedCommit = commit; NavigationId = _navigationId + 1; @@ -188,14 +174,14 @@ public void Select(IList commits) } else { - var commitDetail = new CommitDetail(_repo, true); + var commitDetail = new CommitDetail(_repo, _commitDetailSharedData); commitDetail.Commit = commit; DetailContext = commitDetail; } } else if (commits.Count == 2) { - _repo.SelectedSearchedCommit = null; + _repo.SearchCommitContext.Selected = null; var end = commits[0] as Models.Commit; var start = commits[1] as Models.Commit; @@ -203,12 +189,12 @@ public void Select(IList commits) } else { - _repo.SelectedSearchedCommit = null; + _repo.SearchCommitContext.Selected = null; DetailContext = new Models.Count(commits.Count); } } - public bool CheckoutBranchByDecorator(Models.Decorator decorator) + public async Task CheckoutBranchByDecoratorAsync(Models.Decorator decorator) { if (decorator == null) return false; @@ -223,7 +209,7 @@ public bool CheckoutBranchByDecorator(Models.Decorator decorator) if (b == null) return false; - _repo.CheckoutBranch(b); + await _repo.CheckoutBranchAsync(b); return true; } @@ -234,19 +220,19 @@ public bool CheckoutBranchByDecorator(Models.Decorator decorator) return false; var lb = _repo.Branches.Find(x => x.IsLocal && x.Upstream == rb.FullName); - if (lb == null || lb.TrackStatus.Ahead.Count > 0) + if (lb == null || lb.Ahead.Count > 0) { if (_repo.CanCreatePopup()) _repo.ShowPopup(new CreateBranch(_repo, rb)); } - else if (lb.TrackStatus.Behind.Count > 0) + else if (lb.Behind.Count > 0) { if (_repo.CanCreatePopup()) _repo.ShowPopup(new CheckoutAndFastForward(_repo, lb, rb)); } else if (!lb.IsCurrent) { - _repo.CheckoutBranch(lb); + await _repo.CheckoutBranchAsync(lb); } return true; @@ -255,7 +241,7 @@ public bool CheckoutBranchByDecorator(Models.Decorator decorator) return false; } - public void CheckoutBranchByCommit(Models.Commit commit) + public async Task CheckoutBranchByCommitAsync(Models.Commit commit) { if (commit.IsCurrentHead) return; @@ -269,7 +255,7 @@ public void CheckoutBranchByCommit(Models.Commit commit) if (b == null) continue; - _repo.CheckoutBranch(b); + await _repo.CheckoutBranchAsync(b); return; } @@ -280,7 +266,7 @@ public void CheckoutBranchByCommit(Models.Commit commit) continue; var lb = _repo.Branches.Find(x => x.IsLocal && x.Upstream == rb.FullName); - if (lb is { TrackStatus.Ahead.Count: 0 }) + if (lb != null && lb.Behind.Count > 0 && lb.Ahead.Count == 0) { if (_repo.CanCreatePopup()) _repo.ShowPopup(new CheckoutAndFastForward(_repo, lb, rb)); @@ -300,1008 +286,150 @@ public void CheckoutBranchByCommit(Models.Commit commit) } } - public ContextMenu CreateContextMenuForSelectedCommits(List selected, Action onAddSelected) + public async Task CherryPickAsync(Models.Commit commit) { - var current = _repo.CurrentBranch; - if (current == null) - return null; - - if (selected.Count > 1) - { - var canCherryPick = true; - var canMerge = true; - - foreach (var c in selected) - { - if (c.IsMerged) - { - canMerge = false; - canCherryPick = false; - } - else if (c.Parents.Count > 1) - { - canCherryPick = false; - } - } - - var multipleMenu = new ContextMenu(); - - if (!_repo.IsBare) - { - if (canCherryPick) - { - var cherryPickMultiple = new MenuItem(); - cherryPickMultiple.Header = App.Text("CommitCM.CherryPickMultiple"); - cherryPickMultiple.Icon = App.CreateMenuIcon("Icons.CherryPick"); - cherryPickMultiple.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new CherryPick(_repo, selected)); - e.Handled = true; - }; - multipleMenu.Items.Add(cherryPickMultiple); - } - - if (canMerge) - { - var mergeMultiple = new MenuItem(); - mergeMultiple.Header = App.Text("CommitCM.MergeMultiple"); - mergeMultiple.Icon = App.CreateMenuIcon("Icons.Merge"); - mergeMultiple.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new MergeMultiple(_repo, selected)); - e.Handled = true; - }; - multipleMenu.Items.Add(mergeMultiple); - } - - if (canCherryPick || canMerge) - multipleMenu.Items.Add(new MenuItem() { Header = "-" }); - } - - var saveToPatchMultiple = new MenuItem(); - saveToPatchMultiple.Icon = App.CreateMenuIcon("Icons.Diff"); - saveToPatchMultiple.Header = App.Text("CommitCM.SaveAsPatch"); - saveToPatchMultiple.Click += async (_, e) => - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == null) - return; - - var options = new FolderPickerOpenOptions() { AllowMultiple = false }; - CommandLog log = null; - try - { - var picker = await storageProvider.OpenFolderPickerAsync(options); - if (picker.Count == 1) - { - log = _repo.CreateLog("Save as Patch"); - - var folder = picker[0]; - var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder.Path.ToString(); - var succ = false; - for (var i = 0; i < selected.Count; i++) - { - var saveTo = GetPatchFileName(folderPath, selected[i], i); - succ = await new Commands.FormatPatch(_repo.FullPath, selected[i].SHA, saveTo).Use(log).ExecAsync(); - if (!succ) - break; - } - - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } - } - catch (Exception exception) - { - App.RaiseException(_repo.FullPath, $"Failed to save as patch: {exception.Message}"); - } - - log?.Complete(); - e.Handled = true; - }; - multipleMenu.Items.Add(saveToPatchMultiple); - multipleMenu.Items.Add(new MenuItem() { Header = "-" }); - - var copyMultipleSHAs = new MenuItem(); - copyMultipleSHAs.Header = App.Text("CommitCM.CopySHA"); - copyMultipleSHAs.Icon = App.CreateMenuIcon("Icons.Fingerprint"); - copyMultipleSHAs.Click += async (_, e) => - { - var builder = new StringBuilder(); - foreach (var c in selected) - builder.AppendLine(c.SHA); - - await App.CopyTextAsync(builder.ToString()); - e.Handled = true; - }; - - var copyMultipleInfo = new MenuItem(); - copyMultipleInfo.Header = App.Text("CommitCM.CopySHA") + " - " + App.Text("CommitCM.CopySubject"); - copyMultipleInfo.Icon = App.CreateMenuIcon("Icons.Info"); - copyMultipleInfo.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyMultipleInfo.Click += async (_, e) => - { - var builder = new StringBuilder(); - foreach (var c in selected) - builder.Append(c.SHA.AsSpan(0, 10)).Append(" - ").AppendLine(c.Subject); - - await App.CopyTextAsync(builder.ToString()); - e.Handled = true; - }; - - var copyMultiple = new MenuItem(); - copyMultiple.Header = App.Text("Copy"); - copyMultiple.Icon = App.CreateMenuIcon("Icons.Copy"); - copyMultiple.Items.Add(copyMultipleSHAs); - copyMultiple.Items.Add(copyMultipleInfo); - multipleMenu.Items.Add(copyMultiple); - - return multipleMenu; - } - - var commit = selected[0]; - var menu = new ContextMenu(); - var tags = new List(); - - if (commit.HasDecorators) - { - foreach (var d in commit.Decorators) - { - switch (d.Type) - { - case Models.DecoratorType.CurrentBranchHead: - FillCurrentBranchMenu(menu, current); - break; - case Models.DecoratorType.LocalBranchHead: - var lb = _repo.Branches.Find(x => x.IsLocal && d.Name == x.Name); - FillOtherLocalBranchMenu(menu, lb, current, commit.IsMerged); - break; - case Models.DecoratorType.RemoteBranchHead: - var rb = _repo.Branches.Find(x => !x.IsLocal && d.Name == x.FriendlyName); - FillRemoteBranchMenu(menu, rb, current, commit.IsMerged); - break; - case Models.DecoratorType.Tag: - var t = _repo.Tags.Find(x => x.Name == d.Name); - if (t != null) - tags.Add(t); - break; - } - } - - if (menu.Items.Count > 0) - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - if (tags.Count > 0) - { - foreach (var tag in tags) - FillTagMenu(menu, tag, current, commit.IsMerged); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var createBranch = new MenuItem(); - createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); - createBranch.Header = App.Text("CreateBranch"); - createBranch.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+B" : "Ctrl+Shift+B"; - createBranch.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new CreateBranch(_repo, commit)); - e.Handled = true; - }; - menu.Items.Add(createBranch); - - var createTag = new MenuItem(); - createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); - createTag.Header = App.Text("CreateTag"); - createTag.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+T" : "Ctrl+Shift+T"; - createTag.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new CreateTag(_repo, commit)); - e.Handled = true; - }; - menu.Items.Add(createTag); - menu.Items.Add(new MenuItem() { Header = "-" }); - - if (!_repo.IsBare) + if (_repo.CanCreatePopup()) { - var target = commit.GetFriendlyName(); - - if (current.Head != commit.SHA) + if (commit.Parents.Count <= 1) { - var reset = new MenuItem(); - reset.Header = App.Text("CommitCM.Reset", current.Name, target); - reset.Icon = App.CreateMenuIcon("Icons.Reset"); - reset.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Reset(_repo, current, commit)); - e.Handled = true; - }; - menu.Items.Add(reset); + _repo.ShowPopup(new CherryPick(_repo, [commit])); } else { - var reword = new MenuItem(); - reword.Header = App.Text("CommitCM.Reword"); - reword.Icon = App.CreateMenuIcon("Icons.Edit"); - reword.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Reword(_repo, commit)); - e.Handled = true; - }; - menu.Items.Add(reword); - - var squash = new MenuItem(); - squash.Header = App.Text("CommitCM.Squash"); - squash.Icon = App.CreateMenuIcon("Icons.SquashIntoParent"); - squash.IsEnabled = commit.Parents.Count == 1; - squash.Click += (_, e) => - { - if (commit.Parents.Count == 1) - { - var parent = _commits.Find(x => x.SHA == commit.Parents[0]); - if (parent != null && _repo.CanCreatePopup()) - _repo.ShowPopup(new Squash(_repo, parent, commit.SHA)); - } - - e.Handled = true; - }; - menu.Items.Add(squash); - } - - if (!commit.IsMerged) - { - var rebase = new MenuItem(); - rebase.Header = App.Text("CommitCM.Rebase", current.Name, target); - rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); - rebase.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Rebase(_repo, current, commit)); - e.Handled = true; - }; - menu.Items.Add(rebase); - - if (!commit.HasDecorators) + var parents = new List(); + foreach (var sha in commit.Parents) { - var merge = new MenuItem(); - merge.Header = App.Text("CommitCM.Merge", current.Name); - merge.Icon = App.CreateMenuIcon("Icons.Merge"); - merge.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Merge(_repo, commit, current.Name)); + var parent = _commits.Find(x => x.SHA == sha); + if (parent == null) + parent = await new Commands.QuerySingleCommit(_repo.FullPath, sha).GetResultAsync(); - e.Handled = true; - }; - menu.Items.Add(merge); + if (parent != null) + parents.Add(parent); } - var cherryPick = new MenuItem(); - cherryPick.Header = App.Text("CommitCM.CherryPick"); - cherryPick.Icon = App.CreateMenuIcon("Icons.CherryPick"); - cherryPick.Click += async (_, e) => - { - if (_repo.CanCreatePopup()) - { - if (commit.Parents.Count <= 1) - { - _repo.ShowPopup(new CherryPick(_repo, [commit])); - } - else - { - var parents = new List(); - foreach (var sha in commit.Parents) - { - var parent = _commits.Find(x => x.SHA == sha); - if (parent == null) - parent = await new Commands.QuerySingleCommit(_repo.FullPath, sha) - .GetResultAsync(); - - if (parent != null) - parents.Add(parent); - } - - _repo.ShowPopup(new CherryPick(_repo, commit, parents)); - } - } - - e.Handled = true; - }; - menu.Items.Add(cherryPick); + _repo.ShowPopup(new CherryPick(_repo, commit, parents)); } - else - { - var revert = new MenuItem(); - revert.Header = App.Text("CommitCM.Revert"); - revert.Icon = App.CreateMenuIcon("Icons.Undo"); - revert.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Revert(_repo, commit)); - e.Handled = true; - }; - menu.Items.Add(revert); - } - - if (current.Head != commit.SHA) - { - var checkoutCommit = new MenuItem(); - checkoutCommit.Header = App.Text("CommitCM.Checkout"); - checkoutCommit.Icon = App.CreateMenuIcon("Icons.Detached"); - checkoutCommit.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new CheckoutCommit(_repo, commit)); - e.Handled = true; - }; - menu.Items.Add(checkoutCommit); - - if (commit.IsMerged && commit.Parents.Count > 0) - { - var interactiveRebase = new MenuItem(); - interactiveRebase.Header = App.Text("CommitCM.InteractiveRebase"); - interactiveRebase.Icon = App.CreateMenuIcon("Icons.InteractiveRebase"); - - var manually = new MenuItem(); - manually.Header = App.Text("CommitCM.InteractiveRebase.Manually", current.Name, target); - manually.Click += async (_, e) => - { - await App.ShowDialog(new InteractiveRebase(_repo, commit)); - e.Handled = true; - }; - - var reword = new MenuItem(); - reword.Header = App.Text("CommitCM.InteractiveRebase.Reword"); - reword.Click += async (_, e) => - { - var prefill = new InteractiveRebasePrefill(commit.SHA, Models.InteractiveRebaseAction.Reword); - var on = await new Commands.QuerySingleCommit(_repo.FullPath, $"{commit.SHA}~").GetResultAsync(); - await App.ShowDialog(new InteractiveRebase(_repo, on, prefill)); - e.Handled = true; - }; - - var edit = new MenuItem(); - edit.Header = App.Text("CommitCM.InteractiveRebase.Edit"); - edit.Click += async (_, e) => - { - var prefill = new InteractiveRebasePrefill(commit.SHA, Models.InteractiveRebaseAction.Edit); - var on = await new Commands.QuerySingleCommit(_repo.FullPath, $"{commit.SHA}~").GetResultAsync(); - await App.ShowDialog(new InteractiveRebase(_repo, on, prefill)); - e.Handled = true; - }; - - var squash = new MenuItem(); - squash.Header = App.Text("CommitCM.InteractiveRebase.Squash"); - squash.Click += async (_, e) => - { - var prefill = new InteractiveRebasePrefill(commit.SHA, Models.InteractiveRebaseAction.Squash); - var on = await new Commands.QuerySingleCommit(_repo.FullPath, $"{commit.SHA}~~").GetResultAsync(); - if (on != null) - await App.ShowDialog(new InteractiveRebase(_repo, on, prefill)); - else - App.RaiseException(_repo.FullPath, $"Can not squash current commit into parent!"); - - e.Handled = true; - }; - - var fixup = new MenuItem(); - fixup.Header = App.Text("CommitCM.InteractiveRebase.Fixup"); - fixup.Click += async (_, e) => - { - var prefill = new InteractiveRebasePrefill(commit.SHA, Models.InteractiveRebaseAction.Fixup); - var on = await new Commands.QuerySingleCommit(_repo.FullPath, $"{commit.SHA}~~").GetResultAsync(); - if (on != null) - await App.ShowDialog(new InteractiveRebase(_repo, on, prefill)); - else - App.RaiseException(_repo.FullPath, $"Can not fixup current commit into parent!"); - - e.Handled = true; - }; - - var drop = new MenuItem(); - drop.Header = App.Text("CommitCM.InteractiveRebase.Drop"); - drop.Click += async (_, e) => - { - var prefill = new InteractiveRebasePrefill(commit.SHA, Models.InteractiveRebaseAction.Drop); - var on = await new Commands.QuerySingleCommit(_repo.FullPath, $"{commit.SHA}~").GetResultAsync(); - await App.ShowDialog(new InteractiveRebase(_repo, on, prefill)); - e.Handled = true; - }; - - interactiveRebase.Items.Add(manually); - interactiveRebase.Items.Add(new MenuItem() { Header = "-" }); - interactiveRebase.Items.Add(reword); - interactiveRebase.Items.Add(edit); - interactiveRebase.Items.Add(squash); - interactiveRebase.Items.Add(fixup); - interactiveRebase.Items.Add(drop); - - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(interactiveRebase); - } - else - { - var interactiveRebase = new MenuItem(); - interactiveRebase.Header = App.Text("CommitCM.InteractiveRebase.Manually", current.Name, target); - interactiveRebase.Icon = App.CreateMenuIcon("Icons.InteractiveRebase"); - interactiveRebase.Click += async (_, e) => - { - await App.ShowDialog(new InteractiveRebase(_repo, commit)); - e.Handled = true; - }; - - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(interactiveRebase); - } - } - - menu.Items.Add(new MenuItem() { Header = "-" }); } + } - if (current.Head != commit.SHA) + public async Task RewordHeadAsync(Models.Commit head) + { + if (_repo.CanCreatePopup()) { - if (current.TrackStatus.Ahead.Contains(commit.SHA)) - { - var upstream = _repo.Branches.Find(x => x.FullName.Equals(current.Upstream, StringComparison.Ordinal)); - var pushRevision = new MenuItem(); - pushRevision.Header = App.Text("CommitCM.PushRevision", commit.SHA.Substring(0, 10), upstream.FriendlyName); - pushRevision.Icon = App.CreateMenuIcon("Icons.Push"); - pushRevision.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new PushRevision(_repo, commit, upstream)); - e.Handled = true; - }; - menu.Items.Add(pushRevision); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var compareWithHead = new MenuItem(); - compareWithHead.Header = App.Text("CommitCM.CompareWithHead"); - compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); - compareWithHead.Click += async (_, e) => - { - var head = _commits.Find(x => x.SHA == current.Head); - if (head == null) - { - _repo.SelectedSearchedCommit = null; - head = await new Commands.QuerySingleCommit(_repo.FullPath, current.Head).GetResultAsync(); - if (head != null) - DetailContext = new RevisionCompare(_repo.FullPath, commit, head); - } - else - { - onAddSelected?.Invoke(head); - } - - e.Handled = true; - }; - menu.Items.Add(compareWithHead); - - if (_repo.LocalChangesCount > 0) - { - var compareWithWorktree = new MenuItem(); - compareWithWorktree.Header = App.Text("CommitCM.CompareWithWorktree"); - compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); - compareWithWorktree.Click += (_, e) => - { - DetailContext = new RevisionCompare(_repo.FullPath, commit, null); - e.Handled = true; - }; - menu.Items.Add(compareWithWorktree); - } - - menu.Items.Add(new MenuItem() { Header = "-" }); + var message = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync(); + _repo.ShowPopup(new Reword(_repo, head, message)); } + } - var saveToPatch = new MenuItem(); - saveToPatch.Icon = App.CreateMenuIcon("Icons.Diff"); - saveToPatch.Header = App.Text("CommitCM.SaveAsPatch"); - saveToPatch.Click += async (_, e) => + public async Task SquashOrFixupHeadAsync(Models.Commit head, bool fixup) + { + if (head.Parents.Count == 1) { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == null) + var parent = await new Commands.QuerySingleCommit(_repo.FullPath, head.Parents[0]).GetResultAsync(); + if (parent == null) return; - var options = new FolderPickerOpenOptions() { AllowMultiple = false }; - CommandLog log = null; - try - { - var selected = await storageProvider.OpenFolderPickerAsync(options); - if (selected.Count == 1) - { - log = _repo.CreateLog("Save as Patch"); - - var folder = selected[0]; - var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder.Path.ToString(); - var saveTo = GetPatchFileName(folderPath, commit); - var succ = await new Commands.FormatPatch(_repo.FullPath, commit.SHA, saveTo).Use(log).ExecAsync(); - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } - } - catch (Exception exception) + string message = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.Parents[0]).GetResultAsync(); + if (!fixup) { - App.RaiseException(_repo.FullPath, $"Failed to save as patch: {exception.Message}"); + var headMessage = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync(); + message = $"{message}\n\n{headMessage}"; } - log?.Complete(); - e.Handled = true; - }; - menu.Items.Add(saveToPatch); - - var archive = new MenuItem(); - archive.Icon = App.CreateMenuIcon("Icons.Archive"); - archive.Header = App.Text("Archive"); - archive.Click += (_, e) => - { if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Archive(_repo, commit)); - e.Handled = true; - }; - menu.Items.Add(archive); - menu.Items.Add(new MenuItem() { Header = "-" }); - - var actions = _repo.GetCustomActions(Models.CustomActionScope.Commit); - if (actions.Count > 0) - { - var custom = new MenuItem(); - custom.Header = App.Text("CommitCM.CustomAction"); - custom.Icon = App.CreateMenuIcon("Icons.Action"); - - foreach (var action in actions) - { - var (dup, label) = action; - var item = new MenuItem(); - item.Icon = App.CreateMenuIcon("Icons.Action"); - item.Header = label; - item.Click += (_, e) => - { - _repo.ExecCustomAction(dup, commit); - e.Handled = true; - }; - - custom.Items.Add(item); - } - - menu.Items.Add(custom); - menu.Items.Add(new MenuItem() { Header = "-" }); + _repo.ShowPopup(new SquashOrFixupHead(_repo, parent, message, fixup)); } - - var copySHA = new MenuItem(); - copySHA.Header = App.Text("CommitCM.CopySHA"); - copySHA.Icon = App.CreateMenuIcon("Icons.Fingerprint"); - copySHA.Click += async (_, e) => - { - await App.CopyTextAsync(commit.SHA); - e.Handled = true; - }; - - var copySubject = new MenuItem(); - copySubject.Header = App.Text("CommitCM.CopySubject"); - copySubject.Icon = App.CreateMenuIcon("Icons.Subject"); - copySubject.Click += async (_, e) => - { - await App.CopyTextAsync(commit.Subject); - e.Handled = true; - }; - - var copyInfo = new MenuItem(); - copyInfo.Header = App.Text("CommitCM.CopySHA") + " - " + App.Text("CommitCM.CopySubject"); - copyInfo.Icon = App.CreateMenuIcon("Icons.Info"); - copyInfo.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyInfo.Click += async (_, e) => - { - await App.CopyTextAsync($"{commit.SHA.AsSpan(0, 10)} - {commit.Subject}"); - e.Handled = true; - }; - - var copyMessage = new MenuItem(); - copyMessage.Header = App.Text("CommitCM.CopyCommitMessage"); - copyMessage.Icon = App.CreateMenuIcon("Icons.Info"); - copyMessage.Click += async (_, e) => - { - var message = await new Commands.QueryCommitFullMessage(_repo.FullPath, commit.SHA).GetResultAsync(); - await App.CopyTextAsync(message); - e.Handled = true; - }; - - var copyAuthor = new MenuItem(); - copyAuthor.Header = App.Text("CommitCM.CopyAuthor"); - copyAuthor.Icon = App.CreateMenuIcon("Icons.User"); - copyAuthor.Click += async (_, e) => - { - await App.CopyTextAsync(commit.Author.ToString()); - e.Handled = true; - }; - - var copyCommitter = new MenuItem(); - copyCommitter.Header = App.Text("CommitCM.CopyCommitter"); - copyCommitter.Icon = App.CreateMenuIcon("Icons.User"); - copyCommitter.Click += async (_, e) => - { - await App.CopyTextAsync(commit.Committer.ToString()); - e.Handled = true; - }; - - var copy = new MenuItem(); - copy.Header = App.Text("Copy"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Items.Add(copySHA); - copy.Items.Add(copySubject); - copy.Items.Add(copyInfo); - copy.Items.Add(copyMessage); - copy.Items.Add(copyAuthor); - copy.Items.Add(copyCommitter); - menu.Items.Add(copy); - - return menu; } - private void FillCurrentBranchMenu(ContextMenu menu, Models.Branch current) + public async Task DropHeadAsync(Models.Commit head) { - var submenu = new MenuItem(); - submenu.Icon = App.CreateMenuIcon("Icons.Branch"); - submenu.Header = current.Name; - - var visibility = new MenuItem(); - visibility.Classes.Add("filter_mode_switcher"); - visibility.Header = new FilterModeInGraph(_repo, current); - submenu.Items.Add(visibility); - submenu.Items.Add(new MenuItem() { Header = "-" }); - - if (!string.IsNullOrEmpty(current.Upstream)) - { - var upstream = current.Upstream.Substring(13); - - var fastForward = new MenuItem(); - fastForward.Header = App.Text("BranchCM.FastForward", upstream); - fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); - fastForward.IsEnabled = current.TrackStatus.Ahead.Count == 0; - fastForward.Click += (_, e) => - { - var b = _repo.Branches.Find(x => x.FriendlyName == upstream); - if (b == null) - return; + var parent = _commits.Find(x => x.SHA.Equals(head.Parents[0])); + if (parent == null) + parent = await new Commands.QuerySingleCommit(_repo.FullPath, head.Parents[0]).GetResultAsync(); - if (_repo.CanCreatePopup()) - _repo.ShowAndStartPopup(new Merge(_repo, b, current.Name, true)); - - e.Handled = true; - }; - submenu.Items.Add(fastForward); - - var pull = new MenuItem(); - pull.Header = App.Text("BranchCM.Pull", upstream); - pull.Icon = App.CreateMenuIcon("Icons.Pull"); - pull.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Pull(_repo, null)); - e.Handled = true; - }; - submenu.Items.Add(pull); - } - - var push = new MenuItem(); - push.Header = App.Text("BranchCM.Push", current.Name); - push.Icon = App.CreateMenuIcon("Icons.Push"); - push.IsEnabled = _repo.Remotes.Count > 0; - push.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Push(_repo, current)); - e.Handled = true; - }; - submenu.Items.Add(push); - - var rename = new MenuItem(); - rename.Header = App.Text("BranchCM.Rename", current.Name); - rename.Icon = App.CreateMenuIcon("Icons.Rename"); - rename.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new RenameBranch(_repo, current)); - e.Handled = true; - }; - submenu.Items.Add(rename); - submenu.Items.Add(new MenuItem() { Header = "-" }); - - if (!_repo.IsBare) - { - var type = _repo.GetGitFlowType(current); - if (type != Models.GitFlowBranchType.None) - { - var finish = new MenuItem(); - finish.Header = App.Text("BranchCM.Finish", current.Name); - finish.Icon = App.CreateMenuIcon("Icons.GitFlow"); - finish.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new GitFlowFinish(_repo, current, type)); - e.Handled = true; - }; - submenu.Items.Add(finish); - submenu.Items.Add(new MenuItem() { Header = "-" }); - } - } + if (parent != null && _repo.CanCreatePopup()) + _repo.ShowPopup(new DropHead(_repo, head, parent)); + } - var copy = new MenuItem(); - copy.Header = App.Text("BranchCM.CopyName"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, e) => + public async Task InteractiveRebaseAsync(Models.Commit commit, Models.InteractiveRebaseAction act) + { + var prefill = new InteractiveRebasePrefill(commit.SHA, act); + var start = act switch { - await App.CopyTextAsync(current.Name); - e.Handled = true; + Models.InteractiveRebaseAction.Squash or Models.InteractiveRebaseAction.Fixup => $"{commit.SHA}~~", + _ => $"{commit.SHA}~", }; - submenu.Items.Add(copy); - menu.Items.Add(submenu); + var on = await new Commands.QuerySingleCommit(_repo.FullPath, start).GetResultAsync(); + if (on == null) + App.RaiseException(_repo.FullPath, $"Can not squash current commit into parent!"); + else + await App.ShowDialog(new InteractiveRebase(_repo, on, prefill)); } - private void FillOtherLocalBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) + public async Task GetCommitFullMessageAsync(Models.Commit commit) { - var submenu = new MenuItem(); - submenu.Icon = App.CreateMenuIcon("Icons.Branch"); - submenu.Header = branch.Name; - - var visibility = new MenuItem(); - visibility.Classes.Add("filter_mode_switcher"); - visibility.Header = new FilterModeInGraph(_repo, branch); - submenu.Items.Add(visibility); - submenu.Items.Add(new MenuItem() { Header = "-" }); - - if (!_repo.IsBare) - { - var checkout = new MenuItem(); - checkout.Header = App.Text("BranchCM.Checkout", branch.Name); - checkout.Icon = App.CreateMenuIcon("Icons.Check"); - checkout.Click += (_, e) => - { - _repo.CheckoutBranch(branch); - e.Handled = true; - }; - submenu.Items.Add(checkout); - - var merge = new MenuItem(); - merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name); - merge.Icon = App.CreateMenuIcon("Icons.Merge"); - merge.IsEnabled = !merged; - merge.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Merge(_repo, branch, current.Name, false)); - e.Handled = true; - }; - submenu.Items.Add(merge); - } - - var rename = new MenuItem(); - rename.Header = App.Text("BranchCM.Rename", branch.Name); - rename.Icon = App.CreateMenuIcon("Icons.Rename"); - rename.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new RenameBranch(_repo, branch)); - e.Handled = true; - }; - submenu.Items.Add(rename); + return await new Commands.QueryCommitFullMessage(_repo.FullPath, commit.SHA) + .GetResultAsync() + .ConfigureAwait(false); + } - var delete = new MenuItem(); - delete.Header = App.Text("BranchCM.Delete", branch.Name); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); - delete.Click += (_, e) => + public async Task CompareWithHeadAsync(Models.Commit commit) + { + var head = _commits.Find(x => x.IsCurrentHead); + if (head == null) { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new DeleteBranch(_repo, branch)); - e.Handled = true; - }; - submenu.Items.Add(delete); - submenu.Items.Add(new MenuItem() { Header = "-" }); + _repo.SearchCommitContext.Selected = null; + head = await new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResultAsync(); + if (head != null) + DetailContext = new RevisionCompare(_repo.FullPath, commit, head); - if (!_repo.IsBare) - { - var type = _repo.GetGitFlowType(branch); - if (type != Models.GitFlowBranchType.None) - { - var finish = new MenuItem(); - finish.Header = App.Text("BranchCM.Finish", branch.Name); - finish.Icon = App.CreateMenuIcon("Icons.GitFlow"); - finish.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new GitFlowFinish(_repo, branch, type)); - e.Handled = true; - }; - submenu.Items.Add(finish); - submenu.Items.Add(new MenuItem() { Header = "-" }); - } + return null; } - var copy = new MenuItem(); - copy.Header = App.Text("BranchCM.CopyName"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, e) => - { - await App.CopyTextAsync(branch.Name); - e.Handled = true; - }; - submenu.Items.Add(copy); - - menu.Items.Add(submenu); + return head; } - private void FillRemoteBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) + public void CompareWithWorktree(Models.Commit commit) { - var name = branch.FriendlyName; - - var submenu = new MenuItem(); - submenu.Icon = App.CreateMenuIcon("Icons.Branch"); - submenu.Header = name; - - var visibility = new MenuItem(); - visibility.Classes.Add("filter_mode_switcher"); - visibility.Header = new FilterModeInGraph(_repo, branch); - submenu.Items.Add(visibility); - submenu.Items.Add(new MenuItem() { Header = "-" }); - - var checkout = new MenuItem(); - checkout.Header = App.Text("BranchCM.Checkout", name); - checkout.Icon = App.CreateMenuIcon("Icons.Check"); - checkout.Click += (_, e) => - { - _repo.CheckoutBranch(branch); - e.Handled = true; - }; - submenu.Items.Add(checkout); - - var merge = new MenuItem(); - merge.Header = App.Text("BranchCM.Merge", name, current.Name); - merge.Icon = App.CreateMenuIcon("Icons.Merge"); - merge.IsEnabled = !merged; - merge.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Merge(_repo, branch, current.Name, false)); - e.Handled = true; - }; - - submenu.Items.Add(merge); - - var delete = new MenuItem(); - delete.Header = App.Text("BranchCM.Delete", name); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); - delete.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new DeleteBranch(_repo, branch)); - e.Handled = true; - }; - submenu.Items.Add(delete); - submenu.Items.Add(new MenuItem() { Header = "-" }); - - var copy = new MenuItem(); - copy.Header = App.Text("BranchCM.CopyName"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, e) => - { - await App.CopyTextAsync(name); - e.Handled = true; - }; - submenu.Items.Add(copy); - - menu.Items.Add(submenu); + DetailContext = new RevisionCompare(_repo.FullPath, commit, null); } - private void FillTagMenu(ContextMenu menu, Models.Tag tag, Models.Branch current, bool merged) + private void NavigateTo(Models.Commit commit) { - var submenu = new MenuItem(); - submenu.Header = tag.Name; - submenu.Icon = App.CreateMenuIcon("Icons.Tag"); - submenu.MinWidth = 200; - - var visibility = new MenuItem(); - visibility.Classes.Add("filter_mode_switcher"); - visibility.Header = new FilterModeInGraph(_repo, tag); - submenu.Items.Add(visibility); - submenu.Items.Add(new MenuItem() { Header = "-" }); - - var push = new MenuItem(); - push.Header = App.Text("TagCM.Push", tag.Name); - push.Icon = App.CreateMenuIcon("Icons.Push"); - push.IsEnabled = _repo.Remotes.Count > 0; - push.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new PushTag(_repo, tag)); - e.Handled = true; - }; - submenu.Items.Add(push); + AutoSelectedCommit = commit; - if (!_repo.IsBare && !merged) + if (commit == null) { - var merge = new MenuItem(); - merge.Header = App.Text("TagCM.Merge", tag.Name, current.Name); - merge.Icon = App.CreateMenuIcon("Icons.Merge"); - merge.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new Merge(_repo, tag, current.Name)); - e.Handled = true; - }; - submenu.Items.Add(merge); + DetailContext = null; } - - var delete = new MenuItem(); - delete.Header = App.Text("TagCM.Delete", tag.Name); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); - delete.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new DeleteTag(_repo, tag)); - e.Handled = true; - }; - submenu.Items.Add(delete); - submenu.Items.Add(new MenuItem() { Header = "-" }); - - var copy = new MenuItem(); - copy.Header = App.Text("TagCM.Copy"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, e) => + else { - await App.CopyTextAsync(tag.Name); - e.Handled = true; - }; - submenu.Items.Add(copy); - - menu.Items.Add(submenu); - } - - private string GetPatchFileName(string dir, Models.Commit commit, int index = 0) - { - var ignore_chars = new HashSet { '/', '\\', ':', ',', '*', '?', '\"', '<', '>', '|', '`', '$', '^', '%', '[', ']', '+', '-' }; - var builder = new StringBuilder(); - builder.Append(index.ToString("D4")); - builder.Append('-'); + NavigationId = _navigationId + 1; - var chars = commit.Subject.ToCharArray(); - var len = 0; - foreach (var c in chars) - { - if (!ignore_chars.Contains(c)) + if (_detailContext is CommitDetail detail) { - if (c == ' ' || c == '\t') - builder.Append('-'); - else - builder.Append(c); - - len++; - - if (len >= 48) - break; + detail.Commit = commit; + } + else + { + var commitDetail = new CommitDetail(_repo, _commitDetailSharedData); + commitDetail.Commit = commit; + DetailContext = commitDetail; } } - builder.Append(".patch"); - - return Path.Combine(dir, builder.ToString()); } private Repository _repo = null; + private CommitDetailSharedData _commitDetailSharedData = null; private bool _isLoading = true; private List _commits = new List(); private Models.CommitGraph _graph = null; private Models.Commit _autoSelectedCommit = null; + private Models.Bisect _bisect = null; private long _navigationId = 0; private IDisposable _detailContext = null; - private Models.Bisect _bisect = null; - private GridLength _leftArea = new GridLength(1, GridUnitType.Star); private GridLength _rightArea = new GridLength(1, GridUnitType.Star); private GridLength _topArea = new GridLength(1, GridUnitType.Star); diff --git a/src/ViewModels/ICommandPalette.cs b/src/ViewModels/ICommandPalette.cs new file mode 100644 index 000000000..232a8558d --- /dev/null +++ b/src/ViewModels/ICommandPalette.cs @@ -0,0 +1,17 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class ICommandPalette : ObservableObject, IDisposable + { + public void Dispose() + { + Cleanup(); + } + + public virtual void Cleanup() + { + } + } +} diff --git a/src/ViewModels/ImageSource.cs b/src/ViewModels/ImageSource.cs index 3095e4c6f..2f9bdf420 100644 --- a/src/ViewModels/ImageSource.cs +++ b/src/ViewModels/ImageSource.cs @@ -3,6 +3,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; + using Avalonia; using Avalonia.Media.Imaging; using Avalonia.Platform; @@ -52,7 +53,12 @@ public static async Task FromLFSObjectAsync(string repo, Models.LFS if (string.IsNullOrEmpty(lfs.Oid) || lfs.Size == 0) return new ImageSource(null, 0); - var stream = await Commands.QueryFileContent.FromLFSAsync(repo, lfs.Oid, lfs.Size).ConfigureAwait(false); + var commonDir = await new Commands.QueryGitCommonDir(repo).GetResultAsync().ConfigureAwait(false); + var localFile = Path.Combine(commonDir, "lfs", "objects", lfs.Oid.Substring(0, 2), lfs.Oid.Substring(2, 2), lfs.Oid); + if (File.Exists(localFile)) + return await FromFileAsync(localFile, decoder).ConfigureAwait(false); + + await using var stream = await Commands.QueryFileContent.FromLFSAsync(repo, lfs.Oid, lfs.Size).ConfigureAwait(false); return await Task.Run(() => LoadFromStream(stream, decoder)).ConfigureAwait(false); } @@ -102,10 +108,30 @@ private static ImageSource DecodeWithPfim(Stream stream, long size) case ImageFormat.Rgb8: pixelFormat = PixelFormats.Gray8; break; + case ImageFormat.R16f: + pixelFormat = PixelFormats.Gray16; + break; + case ImageFormat.R32f: + pixelFormat = PixelFormats.Gray32Float; + break; case ImageFormat.R5g5b5: - case ImageFormat.R5g5b5a1: pixelFormat = PixelFormats.Bgr555; break; + case ImageFormat.R5g5b5a1: + var pixels1 = pfiImage.DataLen / 2; + data = new byte[pixels1 * 4]; + stride = pfiImage.Width * 4; + for (var i = 0; i < pixels1; i++) + { + var src = BitConverter.ToUInt16(pfiImage.Data, i * 2); + data[i * 4 + 0] = (byte)Math.Round((src & 0x1F) / 31F * 255); // B + data[i * 4 + 1] = (byte)Math.Round(((src >> 5) & 0x1F) / 31F * 255); // G + data[i * 4 + 2] = (byte)Math.Round(((src >> 10) & 0x1F) / 31F * 255); // R + data[i * 4 + 3] = (byte)((src >> 15) * 255F); // A + } + + alphaFormat = AlphaFormat.Unpremul; + break; case ImageFormat.R5g6b5: pixelFormat = PixelFormats.Bgr565; break; @@ -125,10 +151,10 @@ private static ImageSource DecodeWithPfim(Stream stream, long size) data[i * 4 + 3] = (byte)Math.Round(((src >> 12) & 0x0F) / 15F * 255); // A } - alphaFormat = AlphaFormat.Premul; + alphaFormat = AlphaFormat.Unpremul; break; case ImageFormat.Rgba32: - alphaFormat = AlphaFormat.Premul; + alphaFormat = AlphaFormat.Unpremul; break; default: return new ImageSource(null, 0); @@ -149,17 +175,18 @@ private static ImageSource DecodeWithTiff(Stream stream, long size) if (tiff == null) return new ImageSource(null, 0); + // Currently only supports image when its `BITSPERSAMPLE` is one in [1,2,4,8,16] var width = tiff.GetField(TiffTag.IMAGEWIDTH)[0].ToInt(); var height = tiff.GetField(TiffTag.IMAGELENGTH)[0].ToInt(); var pixels = new int[width * height]; - - // Currently only supports image when its `BITSPERSAMPLE` is one in [1,2,4,8,16] tiff.ReadRGBAImageOriented(width, height, pixels, Orientation.TOPLEFT); - var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(pixels, 0); var pixelSize = new PixelSize(width, height); var dpi = new Vector(96, 96); - var bitmap = new Bitmap(PixelFormats.Rgba8888, AlphaFormat.Premul, ptr, pixelSize, dpi, width * 4); + var bitmap = new WriteableBitmap(pixelSize, dpi, PixelFormats.Rgba8888, AlphaFormat.Unpremul); + + using var frameBuffer = bitmap.Lock(); + Marshal.Copy(pixels, 0, frameBuffer.Address, pixels.Length); return new ImageSource(bitmap, size); } } diff --git a/src/ViewModels/InProgressContexts.cs b/src/ViewModels/InProgressContexts.cs index 5104130ec..446cbe989 100644 --- a/src/ViewModels/InProgressContexts.cs +++ b/src/ViewModels/InProgressContexts.cs @@ -5,45 +5,27 @@ namespace SourceGit.ViewModels { public abstract class InProgressContext { - protected InProgressContext(string repo, string cmd) + public async Task ContinueAsync() { - _repo = repo; - _cmd = cmd; + if (_continueCmd != null) + await _continueCmd.ExecAsync(); } - public Task AbortAsync() + public async Task SkipAsync() { - return new Commands.Command() - { - WorkingDirectory = _repo, - Context = _repo, - Args = $"{_cmd} --abort", - }.ExecAsync(); - } - - public virtual Task SkipAsync() - { - return new Commands.Command() - { - WorkingDirectory = _repo, - Context = _repo, - Args = $"{_cmd} --skip", - }.ExecAsync(); + if (_skipCmd != null) + await _skipCmd.ExecAsync(); } - public virtual Task ContinueAsync() + public async Task AbortAsync() { - return new Commands.Command() - { - WorkingDirectory = _repo, - Context = _repo, - Editor = Commands.Command.EditorType.None, - Args = $"{_cmd} --continue", - }.ExecAsync(); + if (_abortCmd != null) + await _abortCmd.ExecAsync(); } - protected string _repo = string.Empty; - protected string _cmd = string.Empty; + protected Commands.Command _continueCmd = null; + protected Commands.Command _skipCmd = null; + protected Commands.Command _abortCmd = null; } public class CherryPickInProgress : InProgressContext @@ -58,10 +40,31 @@ public string HeadName get; } - public CherryPickInProgress(Repository repo) : base(repo.FullPath, "cherry-pick") + public CherryPickInProgress(Repository repo) { + _continueCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "cherry-pick --continue", + }; + + _skipCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "cherry-pick --skip", + }; + + _abortCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "cherry-pick --abort", + }; + var headSHA = File.ReadAllText(Path.Combine(repo.GitDir, "CHERRY_PICK_HEAD")).Trim(); - Head = new Commands.QuerySingleCommit(repo.FullPath, headSHA).GetResultAsync().Result ?? new Models.Commit() { SHA = headSHA }; + Head = new Commands.QuerySingleCommit(repo.FullPath, headSHA).GetResult() ?? new Models.Commit() { SHA = headSHA }; HeadName = Head.GetFriendlyName(); } } @@ -88,8 +91,30 @@ public Models.Commit Onto get; } - public RebaseInProgress(Repository repo) : base(repo.FullPath, "rebase") + public RebaseInProgress(Repository repo) { + _continueCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Editor = Commands.Command.EditorType.RebaseEditor, + Args = "rebase --continue", + }; + + _skipCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "rebase --skip", + }; + + _abortCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "rebase --abort", + }; + HeadName = File.ReadAllText(Path.Combine(repo.GitDir, "rebase-merge", "head-name")).Trim(); if (HeadName.StartsWith("refs/heads/")) HeadName = HeadName.Substring(11); @@ -99,26 +124,15 @@ public RebaseInProgress(Repository repo) : base(repo.FullPath, "rebase") var stoppedSHAPath = Path.Combine(repo.GitDir, "rebase-merge", "stopped-sha"); var stoppedSHA = File.Exists(stoppedSHAPath) ? File.ReadAllText(stoppedSHAPath).Trim() - : new Commands.QueryRevisionByRefName(repo.FullPath, HeadName).GetResultAsync().Result; + : new Commands.QueryRevisionByRefName(repo.FullPath, HeadName).GetResult(); if (!string.IsNullOrEmpty(stoppedSHA)) - StoppedAt = new Commands.QuerySingleCommit(repo.FullPath, stoppedSHA).GetResultAsync().Result ?? new Models.Commit() { SHA = stoppedSHA }; + StoppedAt = new Commands.QuerySingleCommit(repo.FullPath, stoppedSHA).GetResult() ?? new Models.Commit() { SHA = stoppedSHA }; var ontoSHA = File.ReadAllText(Path.Combine(repo.GitDir, "rebase-merge", "onto")).Trim(); - Onto = new Commands.QuerySingleCommit(repo.FullPath, ontoSHA).GetResultAsync().Result ?? new Models.Commit() { SHA = ontoSHA }; + Onto = new Commands.QuerySingleCommit(repo.FullPath, ontoSHA).GetResult() ?? new Models.Commit() { SHA = ontoSHA }; BaseName = Onto.GetFriendlyName(); } - - public override Task ContinueAsync() - { - return new Commands.Command() - { - WorkingDirectory = _repo, - Context = _repo, - Editor = Commands.Command.EditorType.RebaseEditor, - Args = "rebase --continue", - }.ExecAsync(); - } } public class RevertInProgress : InProgressContext @@ -128,10 +142,31 @@ public Models.Commit Head get; } - public RevertInProgress(Repository repo) : base(repo.FullPath, "revert") + public RevertInProgress(Repository repo) { + _continueCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "revert --continue", + }; + + _skipCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "revert --skip", + }; + + _abortCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "revert --abort", + }; + var headSHA = File.ReadAllText(Path.Combine(repo.GitDir, "REVERT_HEAD")).Trim(); - Head = new Commands.QuerySingleCommit(repo.FullPath, headSHA).GetResultAsync().Result ?? new Models.Commit() { SHA = headSHA }; + Head = new Commands.QuerySingleCommit(repo.FullPath, headSHA).GetResult() ?? new Models.Commit() { SHA = headSHA }; } } @@ -152,18 +187,27 @@ public string SourceName get; } - public MergeInProgress(Repository repo) : base(repo.FullPath, "merge") + public MergeInProgress(Repository repo) { - Current = new Commands.QueryCurrentBranch(repo.FullPath).GetResultAsync().Result; + _continueCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "merge --continue", + }; + + _abortCmd = new Commands.Command + { + WorkingDirectory = repo.FullPath, + Context = repo.FullPath, + Args = "merge --abort", + }; + + Current = new Commands.QueryCurrentBranch(repo.FullPath).GetResult(); var sourceSHA = File.ReadAllText(Path.Combine(repo.GitDir, "MERGE_HEAD")).Trim(); - Source = new Commands.QuerySingleCommit(repo.FullPath, sourceSHA).GetResultAsync().Result ?? new Models.Commit() { SHA = sourceSHA }; + Source = new Commands.QuerySingleCommit(repo.FullPath, sourceSHA).GetResult() ?? new Models.Commit() { SHA = sourceSHA }; SourceName = Source.GetFriendlyName(); } - - public override Task SkipAsync() - { - return Task.FromResult(true); - } } } diff --git a/src/ViewModels/Init.cs b/src/ViewModels/Init.cs index 7f349917f..f6740d0f6 100644 --- a/src/ViewModels/Init.cs +++ b/src/ViewModels/Init.cs @@ -39,7 +39,9 @@ public override async Task Sure() if (succ) { - Preferences.Instance.FindOrAddNodeByRepositoryPath(_targetPath, _parentNode, true); + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(_targetPath, _parentNode, true); + await node.UpdateStatusAsync(false, null); + Welcome.Instance.Refresh(); } return succ; diff --git a/src/ViewModels/InitGitFlow.cs b/src/ViewModels/InitGitFlow.cs index d433c8f81..6e425879a 100644 --- a/src/ViewModels/InitGitFlow.cs +++ b/src/ViewModels/InitGitFlow.cs @@ -102,7 +102,7 @@ public static ValidationResult ValidateTagPrefix(string tagPrefix, ValidationCon public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Init git-flow ..."; var log = _repo.CreateLog("Gitflow - Init"); @@ -120,7 +120,6 @@ public override async Task Sure() if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } } @@ -134,7 +133,6 @@ public override async Task Sure() if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } } @@ -162,7 +160,6 @@ public override async Task Sure() _repo.GitFlow = gitflow; } - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/InteractiveRebase.cs b/src/ViewModels/InteractiveRebase.cs index c726605c5..1c85e006b 100644 --- a/src/ViewModels/InteractiveRebase.cs +++ b/src/ViewModels/InteractiveRebase.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Text.Json; using System.Threading.Tasks; - +using Avalonia; using Avalonia.Collections; using Avalonia.Threading; @@ -11,31 +12,18 @@ namespace SourceGit.ViewModels { - public record InteractiveRebasePrefill(string sha, Models.InteractiveRebaseAction action) - { - public string SHA { get; } = sha; - public Models.InteractiveRebaseAction Action { get; } = action; - } + public record InteractiveRebasePrefill(string SHA, Models.InteractiveRebaseAction Action); public class InteractiveRebaseItem : ObservableObject { - public Models.Commit Commit + public int OriginalOrder { get; - private set; } - public bool CanSquashOrFixup + public Models.Commit Commit { - get => _canSquashOrFixup; - set - { - if (SetProperty(ref _canSquashOrFixup, value)) - { - if (_action == Models.InteractiveRebaseAction.Squash || _action == Models.InteractiveRebaseAction.Fixup) - Action = Models.InteractiveRebaseAction.Pick; - } - } + get; } public Models.InteractiveRebaseAction Action @@ -44,6 +32,12 @@ public Models.InteractiveRebaseAction Action set => SetProperty(ref _action, value); } + public Models.InteractiveRebasePendingType PendingType + { + get => _pendingType; + set => SetProperty(ref _pendingType, value); + } + public string Subject { get => _subject; @@ -64,17 +58,58 @@ public string FullMessage } } - public InteractiveRebaseItem(Models.Commit c, string message, bool canSquashOrFixup) + public string OriginalFullMessage + { + get; + set; + } + + public bool CanSquashOrFixup + { + get => _canSquashOrFixup; + set => SetProperty(ref _canSquashOrFixup, value); + } + + public bool ShowEditMessageButton { + get => _showEditMessageButton; + set => SetProperty(ref _showEditMessageButton, value); + } + + public bool IsFullMessageUsed + { + get => _isFullMessageUsed; + set => SetProperty(ref _isFullMessageUsed, value); + } + + public Thickness DropDirectionIndicator + { + get => _dropDirectionIndicator; + set => SetProperty(ref _dropDirectionIndicator, value); + } + + public bool IsMessageUserEdited + { + get; + set; + } = false; + + public InteractiveRebaseItem(int order, Models.Commit c, string message) + { + OriginalOrder = order; Commit = c; FullMessage = message; - CanSquashOrFixup = canSquashOrFixup; + OriginalFullMessage = message; } private Models.InteractiveRebaseAction _action = Models.InteractiveRebaseAction.Pick; + private Models.InteractiveRebasePendingType _pendingType = Models.InteractiveRebasePendingType.None; private string _subject; private string _fullMessage; private bool _canSquashOrFixup = true; + private bool _showEditMessageButton = false; + private bool _isFullMessageUsed = true; + private Thickness _dropDirectionIndicator = new Thickness(0); } public class InteractiveRebase : ObservableObject @@ -88,7 +123,6 @@ public Models.Branch Current public Models.Commit On { get; - private set; } public bool AutoStash @@ -97,9 +131,14 @@ public bool AutoStash set; } = true; - public AvaloniaList IssueTrackerRules + public AvaloniaList IssueTrackers + { + get => _repo.IssueTrackers; + } + + public string ConventionalTypesOverride { - get => _repo.Settings.IssueTrackerRules; + get => _repo.Settings.ConventionalTypesOverride; } public bool IsLoading @@ -111,32 +150,27 @@ public bool IsLoading public AvaloniaList Items { get; - private set; } = []; - public InteractiveRebaseItem SelectedItem + public InteractiveRebaseItem PreSelected { - get => _selectedItem; - set - { - if (SetProperty(ref _selectedItem, value)) - DetailContext.Commit = value?.Commit; - } + get => _preSelected; + private set => SetProperty(ref _preSelected, value); } - public CommitDetail DetailContext + public object Detail { - get; - private set; + get => _detail; + private set => SetProperty(ref _detail, value); } public InteractiveRebase(Repository repo, Models.Commit on, InteractiveRebasePrefill prefill = null) { _repo = repo; + _commitDetail = new CommitDetail(repo, null); Current = repo.CurrentBranch; On = on; IsLoading = true; - DetailContext = new CommitDetail(repo, false); Task.Run(async () => { @@ -148,10 +182,10 @@ public InteractiveRebase(Repository repo, Models.Commit on, InteractiveRebasePre for (var i = 0; i < commits.Count; i++) { var c = commits[i]; - list.Add(new InteractiveRebaseItem(c.Commit, c.Message, i < commits.Count - 1)); + list.Add(new InteractiveRebaseItem(commits.Count - i, c.Commit, c.Message)); } - InteractiveRebaseItem selected = list.Count > 0 ? list[0] : null; + var selected = list.Count > 0 ? list[0] : null; if (prefill != null) { var item = list.Find(x => x.Commit.SHA.Equals(prefill.SHA, StringComparison.Ordinal)); @@ -165,68 +199,116 @@ public InteractiveRebase(Repository repo, Models.Commit on, InteractiveRebasePre Dispatcher.UIThread.Post(() => { Items.AddRange(list); - SelectedItem = selected; + UpdateItems(); + PreSelected = selected; IsLoading = false; }); }); } - public void MoveItemUp(InteractiveRebaseItem item) + public void SelectCommits(List items) { - var idx = Items.IndexOf(item); - if (idx > 0) + if (items.Count == 0) + { + Detail = null; + } + else if (items.Count == 1) + { + _commitDetail.Commit = items[0].Commit; + Detail = _commitDetail; + } + else { - var prev = Items[idx - 1]; - Items.RemoveAt(idx - 1); - Items.Insert(idx, prev); - SelectedItem = item; - UpdateItems(); + Detail = new Models.Count(items.Count); } } - public void MoveItemDown(InteractiveRebaseItem item) + public void ChangeAction(List selected, Models.InteractiveRebaseAction action) { - var idx = Items.IndexOf(item); - if (idx < Items.Count - 1) + if (action == Models.InteractiveRebaseAction.Squash || action == Models.InteractiveRebaseAction.Fixup) { - var next = Items[idx + 1]; - Items.RemoveAt(idx + 1); - Items.Insert(idx, next); - SelectedItem = item; - UpdateItems(); + foreach (var item in selected) + { + if (item.CanSquashOrFixup) + item.Action = action; + } } + else + { + foreach (var item in selected) + item.Action = action; + } + + UpdateItems(); } - public void ChangeAction(InteractiveRebaseItem item, Models.InteractiveRebaseAction action) + public void Move(List commits, int index) { - if (!item.CanSquashOrFixup) + var hashes = new HashSet(); + foreach (var c in commits) + hashes.Add(c.Commit.SHA); + + var before = new List(); + var ordered = new List(); + var after = new List(); + + for (int i = 0; i < index; i++) { - if (action == Models.InteractiveRebaseAction.Squash || action == Models.InteractiveRebaseAction.Fixup) - return; + var item = Items[i]; + if (!hashes.Contains(item.Commit.SHA)) + before.Add(item); + else + ordered.Add(item); } - item.Action = action; + for (int i = index; i < Items.Count; i++) + { + var item = Items[i]; + if (!hashes.Contains(item.Commit.SHA)) + after.Add(item); + else + ordered.Add(item); + } + + Items.Clear(); + Items.AddRange(before); + Items.AddRange(ordered); + Items.AddRange(after); UpdateItems(); } public async Task Start() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); - var saveFile = Path.Combine(_repo.GitDir, "sourcegit_rebase_jobs.json"); + var saveFile = Path.Combine(_repo.GitDir, "sourcegit.interactive_rebase"); var collection = new Models.InteractiveRebaseJobCollection(); collection.OrigHead = _repo.CurrentBranch.Head; collection.Onto = On.SHA; + + InteractiveRebaseItem pending = null; for (int i = Items.Count - 1; i >= 0; i--) { var item = Items[i]; - collection.Jobs.Add(new Models.InteractiveRebaseJob() + var job = new Models.InteractiveRebaseJob() { SHA = item.Commit.SHA, Action = item.Action, - Message = item.FullMessage, - }); + }; + + if (pending != null && item.PendingType != Models.InteractiveRebasePendingType.Ignore) + job.Message = pending.FullMessage; + else + job.Message = item.FullMessage; + + collection.Jobs.Add(job); + + if (item.PendingType == Models.InteractiveRebasePendingType.Last) + pending = null; + else if (item.PendingType == Models.InteractiveRebasePendingType.Target) + pending = item; } + await using (var stream = File.Create(saveFile)) { await JsonSerializer.SerializeAsync(stream, collection, JsonCodeGen.Default.InteractiveRebaseJobCollection); @@ -238,7 +320,6 @@ public async Task Start() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } @@ -258,13 +339,111 @@ private void UpdateItems() else { item.CanSquashOrFixup = false; + if (item.Action == Models.InteractiveRebaseAction.Squash || item.Action == Models.InteractiveRebaseAction.Fixup) + item.Action = Models.InteractiveRebaseAction.Pick; + hasValidParent = item.Action != Models.InteractiveRebaseAction.Drop; } } + + var hasPending = false; + var pendingMessages = new List(); + for (var i = 0; i < Items.Count; i++) + { + var item = Items[i]; + + if (item.Action == Models.InteractiveRebaseAction.Drop) + { + item.IsFullMessageUsed = false; + item.ShowEditMessageButton = false; + item.PendingType = hasPending ? Models.InteractiveRebasePendingType.Ignore : Models.InteractiveRebasePendingType.None; + item.FullMessage = item.OriginalFullMessage; + item.IsMessageUserEdited = false; + continue; + } + + if (item.Action == Models.InteractiveRebaseAction.Fixup || + item.Action == Models.InteractiveRebaseAction.Squash) + { + item.IsFullMessageUsed = false; + item.ShowEditMessageButton = false; + item.PendingType = hasPending ? Models.InteractiveRebasePendingType.Pending : Models.InteractiveRebasePendingType.Last; + item.FullMessage = item.OriginalFullMessage; + item.IsMessageUserEdited = false; + + if (item.Action == Models.InteractiveRebaseAction.Squash) + pendingMessages.Add(item.OriginalFullMessage); + + hasPending = true; + continue; + } + + if (item.Action == Models.InteractiveRebaseAction.Reword || + item.Action == Models.InteractiveRebaseAction.Edit) + { + var oldPendingType = item.PendingType; + item.IsFullMessageUsed = true; + item.ShowEditMessageButton = true; + item.PendingType = hasPending ? Models.InteractiveRebasePendingType.Target : Models.InteractiveRebasePendingType.None; + + if (hasPending) + { + if (!item.IsMessageUserEdited) + { + var builder = new StringBuilder(); + builder.Append(item.OriginalFullMessage); + for (var j = pendingMessages.Count - 1; j >= 0; j--) + builder.Append("\n").Append(pendingMessages[j]); + + item.FullMessage = builder.ToString(); + } + + hasPending = false; + pendingMessages.Clear(); + } + else if (oldPendingType == Models.InteractiveRebasePendingType.Target) + { + if (!item.IsMessageUserEdited) + item.FullMessage = item.OriginalFullMessage; + } + + continue; + } + + if (item.Action == Models.InteractiveRebaseAction.Pick) + { + item.IsFullMessageUsed = true; + item.IsMessageUserEdited = false; + + if (hasPending) + { + var builder = new StringBuilder(); + builder.Append(item.OriginalFullMessage); + for (var j = pendingMessages.Count - 1; j >= 0; j--) + builder.Append("\n").Append(pendingMessages[j]); + + item.Action = Models.InteractiveRebaseAction.Reword; + item.PendingType = Models.InteractiveRebasePendingType.Target; + item.ShowEditMessageButton = true; + item.FullMessage = builder.ToString(); + + hasPending = false; + pendingMessages.Clear(); + } + else + { + item.PendingType = Models.InteractiveRebasePendingType.None; + item.ShowEditMessageButton = false; + item.FullMessage = item.OriginalFullMessage; + } + } + } } private Repository _repo = null; private bool _isLoading = false; - private InteractiveRebaseItem _selectedItem = null; + private InteractiveRebaseItem _preSelected = null; + private object _detail = null; + private CommitDetail _commitDetail = null; } } diff --git a/src/ViewModels/LFSFetch.cs b/src/ViewModels/LFSFetch.cs index 6f8dc7762..4a869abb4 100644 --- a/src/ViewModels/LFSFetch.cs +++ b/src/ViewModels/LFSFetch.cs @@ -21,7 +21,7 @@ public LFSFetch(Repository repo) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Fetching LFS objects from remote ..."; var log = _repo.CreateLog("LFS Fetch"); @@ -32,7 +32,6 @@ public override async Task Sure() .FetchAsync(SelectedRemote.Name); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/LFSLocks.cs b/src/ViewModels/LFSLocks.cs index 0125d7f61..d048398eb 100644 --- a/src/ViewModels/LFSLocks.cs +++ b/src/ViewModels/LFSLocks.cs @@ -10,9 +10,9 @@ public class LFSLocks : ObservableObject { public bool HasValidUserName { - get; - private set; - } = false; + get => _hasValidUsername; + private set => SetProperty(ref _hasValidUsername, value); + } public bool IsLoading { @@ -62,7 +62,7 @@ public async Task UnlockAsync(Models.LFSLock lfsLock, bool force) IsLoading = true; - var succ = await _repo.UnlockLFSFileAsync(_remote, lfsLock.File, force, false); + var succ = await _repo.UnlockLFSFileAsync(_remote, lfsLock.Path, force, false); if (succ) { _cachedLocks.Remove(lfsLock); @@ -72,6 +72,35 @@ public async Task UnlockAsync(Models.LFSLock lfsLock, bool force) IsLoading = false; } + public async Task UnlockAllMyLocksAsync() + { + if (_isLoading || string.IsNullOrEmpty(_userName)) + return; + + var locks = new List(); + foreach (var lfsLock in _cachedLocks) + { + if (lfsLock.Owner.Name.Equals(_userName, StringComparison.Ordinal)) + locks.Add(lfsLock.Path); + } + + if (locks.Count == 0) + return; + + IsLoading = true; + + var log = _repo.CreateLog("Unlock LFS Locks"); + var succ = await new Commands.LFS(_repo.FullPath).Use(log).UnlockMultipleAsync(_remote, locks, true); + if (succ) + { + _cachedLocks.RemoveAll(lfsLock => lfsLock.Owner.Name.Equals(_userName, StringComparison.Ordinal)); + UpdateVisibleLocks(); + } + + log.Complete(); + IsLoading = false; + } + private void UpdateVisibleLocks() { var visible = new List(); @@ -84,7 +113,7 @@ private void UpdateVisibleLocks() { foreach (var lfsLock in _cachedLocks) { - if (lfsLock.User.Equals(_userName, StringComparison.Ordinal)) + if (lfsLock.Owner.Name.Equals(_userName, StringComparison.Ordinal)) visible.Add(lfsLock); } } @@ -99,5 +128,6 @@ private void UpdateVisibleLocks() private List _visibleLocks = []; private bool _showOnlyMyLocks = false; private string _userName; + private bool _hasValidUsername; } } diff --git a/src/ViewModels/LFSPrune.cs b/src/ViewModels/LFSPrune.cs index fb9c7fe28..1353bc0d1 100644 --- a/src/ViewModels/LFSPrune.cs +++ b/src/ViewModels/LFSPrune.cs @@ -11,7 +11,7 @@ public LFSPrune(Repository repo) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "LFS prune ..."; var log = _repo.CreateLog("LFS Prune"); @@ -22,7 +22,6 @@ public override async Task Sure() .PruneAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/LFSPull.cs b/src/ViewModels/LFSPull.cs index f4ae697f0..8b4b1081e 100644 --- a/src/ViewModels/LFSPull.cs +++ b/src/ViewModels/LFSPull.cs @@ -21,7 +21,7 @@ public LFSPull(Repository repo) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Pull LFS objects from remote ..."; var log = _repo.CreateLog("LFS Pull"); @@ -32,7 +32,6 @@ public override async Task Sure() .PullAsync(SelectedRemote.Name); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/LFSPush.cs b/src/ViewModels/LFSPush.cs index 11c17c1ec..e5c28783f 100644 --- a/src/ViewModels/LFSPush.cs +++ b/src/ViewModels/LFSPush.cs @@ -21,7 +21,7 @@ public LFSPush(Repository repo) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Push LFS objects to remote ..."; var log = _repo.CreateLog("LFS Push"); @@ -32,7 +32,6 @@ public override async Task Sure() .PushAsync(SelectedRemote.Name); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/LFSTrackCustomPattern.cs b/src/ViewModels/LFSTrackCustomPattern.cs index 7d66e0f85..d9a98a24d 100644 --- a/src/ViewModels/LFSTrackCustomPattern.cs +++ b/src/ViewModels/LFSTrackCustomPattern.cs @@ -25,7 +25,7 @@ public LFSTrackCustomPattern(Repository repo) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Adding custom LFS tracking pattern ..."; var log = _repo.CreateLog("LFS Add Custom Pattern"); @@ -36,7 +36,6 @@ public override async Task Sure() .TrackAsync(_pattern, IsFilename); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index b079ccf61..fe684c0fa 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -1,8 +1,8 @@ using System; using System.IO; +using System.Text; using Avalonia.Collections; -using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -35,19 +35,14 @@ public LauncherPage ActivePage set { if (SetProperty(ref _activePage, value)) - { - UpdateTitle(); - - if (!_ignoreIndexChange && value is { Data: Repository repo }) - _activeWorkspace.ActiveIdx = _activeWorkspace.Repositories.IndexOf(repo.FullPath); - } + PostActivePageChanged(); } } - public IDisposable Switcher + public ICommandPalette CommandPalette { - get => _switcher; - private set => SetProperty(ref _switcher, value); + get => _commandPalette; + set => SetProperty(ref _commandPalette, value); } public Launcher(string startupRepo) @@ -58,64 +53,47 @@ public Launcher(string startupRepo) AddNewTab(); var pref = Preferences.Instance; - if (string.IsNullOrEmpty(startupRepo)) - { - ActiveWorkspace = pref.GetActiveWorkspace(); + ActiveWorkspace = pref.GetActiveWorkspace(); - var repos = ActiveWorkspace.Repositories.ToArray(); - foreach (var repo in repos) - { - var node = pref.FindNode(repo) ?? - new RepositoryNode - { - Id = repo, - Name = Path.GetFileName(repo), - Bookmark = 0, - IsRepository = true, - }; - - OpenRepositoryInTab(node, null); - } + var repos = ActiveWorkspace.Repositories.ToArray(); + foreach (var repo in repos) + { + var node = pref.FindNode(repo) ?? + new RepositoryNode + { + Id = repo, + Name = Path.GetFileName(repo), + Bookmark = 0, + IsRepository = true, + }; - var activeIdx = ActiveWorkspace.ActiveIdx; - if (activeIdx >= 0 && activeIdx < Pages.Count) - { - ActivePage = Pages[activeIdx]; - } - else - { - ActivePage = Pages[0]; - ActiveWorkspace.ActiveIdx = 0; - } + OpenRepositoryInTab(node, null); } - else - { - ActiveWorkspace = new Workspace() { Name = "Unnamed" }; - foreach (var w in pref.Workspaces) - w.IsActive = false; + _ignoreIndexChange = false; - var test = new Commands.QueryRepositoryRootPath(startupRepo).GetResultAsync().Result; - if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut)) - { - Pages[0].Notifications.Add(new Models.Notification - { - IsError = true, - Message = $"Given path: '{startupRepo}' is NOT a valid repository!" - }); - } - else + if (!string.IsNullOrEmpty(startupRepo)) + { + var test = new Commands.QueryRepositoryRootPath(startupRepo).GetResult(); + if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) { var node = pref.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); Welcome.Instance.Refresh(); + OpenRepositoryInTab(node, null); + return; } } - _ignoreIndexChange = false; + var activeIdx = ActiveWorkspace.ActiveIdx; + if (activeIdx > 0 && activeIdx < Pages.Count) + { + ActivePage = Pages[activeIdx]; + return; + } - if (string.IsNullOrEmpty(_title)) - UpdateTitle(); + ActivePage = Pages[0]; + PostActivePageChanged(); } public void Quit() @@ -128,22 +106,6 @@ public void Quit() _ignoreIndexChange = false; } - public void OpenWorkspaceSwitcher() - { - Switcher = new WorkspaceSwitcher(this); - } - - public void OpenTabSwitcher() - { - Switcher = new LauncherPageSwitcher(this); - } - - public void CancelSwitcher() - { - Switcher?.Dispose(); - Switcher = null; - } - public void SwitchWorkspace(Workspace to) { if (to == null || to.IsActive) @@ -188,18 +150,14 @@ public void SwitchWorkspace(Workspace to) OpenRepositoryInTab(node, null); } + _ignoreIndexChange = false; + var activeIdx = to.ActiveIdx; if (activeIdx >= 0 && activeIdx < Pages.Count) - { ActivePage = Pages[activeIdx]; - } else - { ActivePage = Pages[0]; - to.ActiveIdx = 0; - } - _ignoreIndexChange = false; Preferences.Instance.Save(); GC.Collect(); } @@ -218,17 +176,16 @@ public void MoveTab(LauncherPage from, LauncherPage to) var fromIdx = Pages.IndexOf(from); var toIdx = Pages.IndexOf(to); Pages.Move(fromIdx, toIdx); - ActivePage = from; - ActiveWorkspace.Repositories.Clear(); + _activeWorkspace.Repositories.Clear(); foreach (var p in Pages) { if (p.Data is Repository r) - ActiveWorkspace.Repositories.Add(r.FullPath); + _activeWorkspace.Repositories.Add(r.FullPath); } - ActiveWorkspace.ActiveIdx = ActiveWorkspace.Repositories.IndexOf(from.Node.Id); _ignoreIndexChange = false; + ActivePage = from; } public void GotoNextTab() @@ -258,17 +215,18 @@ public void CloseTab(LauncherPage page) var last = Pages[0]; if (last.Data is Repository repo) { - ActiveWorkspace.Repositories.Clear(); - ActiveWorkspace.ActiveIdx = 0; + _activeWorkspace.Repositories.Clear(); + _activeWorkspace.ActiveIdx = 0; repo.Close(); Welcome.Instance.ClearSearchFilter(); last.Node = new RepositoryNode() { Id = Guid.NewGuid().ToString() }; last.Data = Welcome.Instance; + last.Popup?.Cleanup(); last.Popup = null; - UpdateTitle(); + PostActivePageChanged(); GC.Collect(); } else @@ -306,9 +264,9 @@ public void CloseOtherTabs() } Pages = new AvaloniaList { ActivePage }; - ActiveWorkspace.ActiveIdx = 0; OnPropertyChanged(nameof(Pages)); + _activeWorkspace.ActiveIdx = 0; _ignoreIndexChange = false; GC.Collect(); } @@ -345,7 +303,7 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) return; } - var isBare = new Commands.IsBareRepository(node.Id).GetResultAsync().Result; + var isBare = new Commands.IsBareRepository(node.Id).GetResult(); var gitDir = isBare ? node.Id : GetRepositoryGitDir(node.Id); if (string.IsNullOrEmpty(gitDir)) { @@ -358,14 +316,14 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) if (page == null) { - if (ActivePage == null || ActivePage.Node.IsRepository) + if (_activePage == null || _activePage.Node.IsRepository) { page = new LauncherPage(node, repo); Pages.Add(page); } else { - page = ActivePage; + page = _activePage; page.Node = node; page.Data = repo; } @@ -376,20 +334,34 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) page.Data = repo; } - if (page != _activePage) - ActivePage = page; - else - UpdateTitle(); - - ActiveWorkspace.Repositories.Clear(); + _activeWorkspace.Repositories.Clear(); foreach (var p in Pages) { if (p.Data is Repository r) - ActiveWorkspace.Repositories.Add(r.FullPath); + _activeWorkspace.Repositories.Add(r.FullPath); } - if (!_ignoreIndexChange) - ActiveWorkspace.ActiveIdx = ActiveWorkspace.Repositories.IndexOf(node.Id); + if (_activePage == page) + PostActivePageChanged(); + else + ActivePage = page; + } + + public void OpenCommandPalette(ICommandPalette commandPalette) + { + var old = _commandPalette; + CommandPalette = commandPalette; + old?.Dispose(); + } + + public void CancelCommandPalette() + { + if (_commandPalette != null) + { + _commandPalette?.Dispose(); + CommandPalette = null; + GC.Collect(); + } } public void DispatchNotification(string pageId, string message, bool isError) @@ -419,121 +391,6 @@ public void DispatchNotification(string pageId, string message, bool isError) _activePage?.Notifications.Add(notification); } - public ContextMenu CreateContextForWorkspace() - { - var pref = Preferences.Instance; - var menu = new ContextMenu(); - - for (var i = 0; i < pref.Workspaces.Count; i++) - { - var workspace = pref.Workspaces[i]; - - var icon = App.CreateMenuIcon(workspace.IsActive ? "Icons.Check" : "Icons.Workspace"); - icon.Fill = workspace.Brush; - - var item = new MenuItem(); - item.Header = workspace.Name; - item.Icon = icon; - item.Click += (_, e) => - { - if (!workspace.IsActive) - SwitchWorkspace(workspace); - - e.Handled = true; - }; - - menu.Items.Add(item); - } - - menu.Items.Add(new MenuItem() { Header = "-" }); - - var configure = new MenuItem(); - configure.Header = App.Text("Workspace.Configure"); - configure.Click += async (_, e) => - { - await App.ShowDialog(new ConfigureWorkspace()); - e.Handled = true; - }; - menu.Items.Add(configure); - - return menu; - } - - public ContextMenu CreateContextForPageTab(LauncherPage page) - { - if (page == null) - return null; - - var menu = new ContextMenu(); - var close = new MenuItem(); - close.Header = App.Text("PageTabBar.Tab.Close"); - close.Tag = OperatingSystem.IsMacOS() ? "⌘+W" : "Ctrl+W"; - close.Click += (_, e) => - { - CloseTab(page); - e.Handled = true; - }; - menu.Items.Add(close); - - var closeOthers = new MenuItem(); - closeOthers.Header = App.Text("PageTabBar.Tab.CloseOther"); - closeOthers.Click += (_, e) => - { - CloseOtherTabs(); - e.Handled = true; - }; - menu.Items.Add(closeOthers); - - var closeRight = new MenuItem(); - closeRight.Header = App.Text("PageTabBar.Tab.CloseRight"); - closeRight.Click += (_, e) => - { - CloseRightTabs(); - e.Handled = true; - }; - menu.Items.Add(closeRight); - - if (page.Node.IsRepository) - { - var bookmark = new MenuItem(); - bookmark.Header = App.Text("PageTabBar.Tab.Bookmark"); - bookmark.Icon = App.CreateMenuIcon("Icons.Bookmark"); - - for (int i = 0; i < Models.Bookmarks.Supported.Count; i++) - { - var icon = App.CreateMenuIcon("Icons.Bookmark"); - - if (i != 0) - icon.Fill = Models.Bookmarks.Brushes[i]; - - var dupIdx = i; - var setter = new MenuItem(); - setter.Header = icon; - setter.Click += (_, e) => - { - page.Node.Bookmark = dupIdx; - e.Handled = true; - }; - bookmark.Items.Add(setter); - } - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(bookmark); - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("PageTabBar.Tab.CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Click += async (_, e) => - { - await page.CopyPathAsync(); - e.Handled = true; - }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copyPath); - } - - return menu; - } - private string GetRepositoryGitDir(string repo) { var fullpath = Path.Combine(repo, ".git"); @@ -562,7 +419,7 @@ private string GetRepositoryGitDir(string repo) return null; } - return new Commands.QueryGitDir(repo).GetResultAsync().Result; + return new Commands.QueryGitDir(repo).GetResult(); } private void CloseRepositoryInTab(LauncherPage page, bool removeFromWorkspace = true) @@ -570,46 +427,44 @@ private void CloseRepositoryInTab(LauncherPage page, bool removeFromWorkspace = if (page.Data is Repository repo) { if (removeFromWorkspace) - ActiveWorkspace.Repositories.Remove(repo.FullPath); + _activeWorkspace.Repositories.Remove(repo.FullPath); repo.Close(); } + page.Popup?.Cleanup(); + page.Popup = null; page.Data = null; } - private void UpdateTitle() + private void PostActivePageChanged() { - if (_activeWorkspace == null) + if (_ignoreIndexChange) return; - var workspace = _activeWorkspace.Name; - if (_activePage is { Data: Repository }) - { - var node = _activePage.Node; - var name = node.Name; - var path = node.Id; + var builder = new StringBuilder(512); + var workspaces = Preferences.Instance.Workspaces; + if (workspaces.Count == 0 || workspaces.Count > 1 || workspaces[0] != _activeWorkspace) + builder.Append('[').Append(_activeWorkspace.Name).Append("] "); - if (!OperatingSystem.IsWindows()) - { - var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; - if (path.StartsWith(home, StringComparison.Ordinal)) - path = $"~{path.AsSpan(prefixLen)}"; - } - - Title = $"[{workspace}] {name} ({path})"; + if (_activePage is { Data: Repository repo }) + { + _activeWorkspace.ActiveIdx = _activeWorkspace.Repositories.IndexOf(repo.FullPath); + builder.Append(_activePage.Node.Name); } else { - Title = $"[{workspace}] Repositories"; + builder.Append("Repositories"); } + + Title = builder.ToString(); + CancelCommandPalette(); } - private Workspace _activeWorkspace = null; - private LauncherPage _activePage = null; - private bool _ignoreIndexChange = false; + private Workspace _activeWorkspace; + private LauncherPage _activePage; + private bool _ignoreIndexChange; private string _title = string.Empty; - private IDisposable _switcher = null; + private ICommandPalette _commandPalette; } } diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index 0b4b3d8ad..11ba601ce 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -1,7 +1,8 @@ using System; using System.Threading.Tasks; + using Avalonia.Collections; -using Avalonia.Media; + using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -20,10 +21,10 @@ public object Data set => SetProperty(ref _data, value); } - public IBrush DirtyBrush + public Models.DirtyState DirtyState { - get => _dirtyBrush; - private set => SetProperty(ref _dirtyBrush, value); + get => _dirtyState; + private set => SetProperty(ref _dirtyState, value); } public Popup Popup @@ -66,22 +67,18 @@ public async Task CopyPathAsync() public void ChangeDirtyState(Models.DirtyState flag, bool remove) { + var state = _dirtyState; if (remove) { - if (_dirtyState.HasFlag(flag)) - _dirtyState -= flag; + if (state.HasFlag(flag)) + state -= flag; } else { - _dirtyState |= flag; + state |= flag; } - if (_dirtyState.HasFlag(Models.DirtyState.HasLocalChanges)) - DirtyBrush = Brushes.Gray; - else if (_dirtyState.HasFlag(Models.DirtyState.HasPendingPullOrPush)) - DirtyBrush = Brushes.RoyalBlue; - else - DirtyBrush = null; + DirtyState = state; } public bool CanCreatePopup() @@ -89,15 +86,7 @@ public bool CanCreatePopup() return _popup is not { InProgress: true }; } - public void StartPopup(Popup popup) - { - Popup = popup; - - if (popup.CanStartDirectly()) - ProcessPopup(); - } - - public async void ProcessPopup() + public async Task ProcessPopupAsync() { if (_popup is { InProgress: false } dump) { @@ -110,7 +99,10 @@ public async void ProcessPopup() { var finished = await dump.Sure(); if (finished) + { + dump.Cleanup(); Popup = null; + } } catch (Exception e) { @@ -123,16 +115,15 @@ public async void ProcessPopup() public void CancelPopup() { - if (_popup == null) - return; - if (_popup.InProgress) + if (_popup == null || _popup.InProgress) return; + + _popup?.Cleanup(); Popup = null; } private RepositoryNode _node = null; private object _data = null; - private IBrush _dirtyBrush = null; private Models.DirtyState _dirtyState = Models.DirtyState.None; private Popup _popup = null; } diff --git a/src/ViewModels/LauncherPageSwitcher.cs b/src/ViewModels/LauncherPageSwitcher.cs deleted file mode 100644 index 5f53021d6..000000000 --- a/src/ViewModels/LauncherPageSwitcher.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace SourceGit.ViewModels -{ - public class LauncherPageSwitcher : ObservableObject, IDisposable - { - public List VisiblePages - { - get => _visiblePages; - private set => SetProperty(ref _visiblePages, value); - } - - public string SearchFilter - { - get => _searchFilter; - set - { - if (SetProperty(ref _searchFilter, value)) - UpdateVisiblePages(); - } - } - - public LauncherPage SelectedPage - { - get => _selectedPage; - set => SetProperty(ref _selectedPage, value); - } - - public LauncherPageSwitcher(Launcher launcher) - { - _launcher = launcher; - UpdateVisiblePages(); - } - - public void ClearFilter() - { - SearchFilter = string.Empty; - } - - public void Switch() - { - _launcher.ActivePage = _selectedPage ?? _launcher.ActivePage; - _launcher.CancelSwitcher(); - } - - public void Dispose() - { - _visiblePages.Clear(); - _selectedPage = null; - _searchFilter = string.Empty; - } - - private void UpdateVisiblePages() - { - var visible = new List(); - if (string.IsNullOrEmpty(_searchFilter)) - { - visible.AddRange(_launcher.Pages); - } - else - { - foreach (var page in _launcher.Pages) - { - if (page.Node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase) || - (page.Node.IsRepository && page.Node.Id.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))) - { - visible.Add(page); - } - } - } - - VisiblePages = visible; - SelectedPage = visible.Count > 0 ? visible[0] : null; - } - - private Launcher _launcher = null; - private List _visiblePages = []; - private string _searchFilter = string.Empty; - private LauncherPage _selectedPage = null; - } -} diff --git a/src/ViewModels/LauncherPagesCommandPalette.cs b/src/ViewModels/LauncherPagesCommandPalette.cs new file mode 100644 index 000000000..45a065da3 --- /dev/null +++ b/src/ViewModels/LauncherPagesCommandPalette.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.ViewModels +{ + public class LauncherPagesCommandPalette : ICommandPalette + { + public List VisiblePages + { + get => _visiblePages; + private set => SetProperty(ref _visiblePages, value); + } + + public List VisibleRepos + { + get => _visibleRepos; + private set => SetProperty(ref _visibleRepos, value); + } + + public string SearchFilter + { + get => _searchFilter; + set + { + if (SetProperty(ref _searchFilter, value)) + UpdateVisible(); + } + } + + public LauncherPage SelectedPage + { + get => _selectedPage; + set + { + if (SetProperty(ref _selectedPage, value) && value != null) + SelectedRepo = null; + } + } + + public RepositoryNode SelectedRepo + { + get => _selectedRepo; + set + { + if (SetProperty(ref _selectedRepo, value) && value != null) + SelectedPage = null; + } + } + + public LauncherPagesCommandPalette(Launcher launcher) + { + _launcher = launcher; + + foreach (var page in _launcher.Pages) + { + if (page.Node.IsRepository) + _opened.Add(page.Node.Id); + } + + UpdateVisible(); + } + + public override void Cleanup() + { + _launcher = null; + _opened.Clear(); + _visiblePages.Clear(); + _visibleRepos.Clear(); + _searchFilter = null; + _selectedPage = null; + _selectedRepo = null; + } + + public void ClearFilter() + { + SearchFilter = string.Empty; + } + + public void OpenOrSwitchTo() + { + if (_selectedPage != null) + _launcher.ActivePage = _selectedPage; + else if (_selectedRepo != null) + _launcher.OpenRepositoryInTab(_selectedRepo, null); + + _launcher?.CancelCommandPalette(); + } + + private void UpdateVisible() + { + var pages = new List(); + CollectVisiblePages(pages); + + var repos = new List(); + CollectVisibleRepository(repos, Preferences.Instance.RepositoryNodes); + + var autoSelectPage = _selectedPage; + var autoSelectRepo = _selectedRepo; + + if (_selectedPage != null) + { + if (pages.Contains(_selectedPage)) + { + // Keep selection + } + else if (pages.Count > 0) + { + autoSelectPage = pages[0]; + } + else if (repos.Count > 0) + { + autoSelectPage = null; + autoSelectRepo = repos[0]; + } + else + { + autoSelectPage = null; + } + } + else if (_selectedRepo != null) + { + if (repos.Contains(_selectedRepo)) + { + // Keep selection + } + else if (repos.Count > 0) + { + autoSelectRepo = repos[0]; + } + else if (pages.Count > 0) + { + autoSelectPage = pages[0]; + autoSelectRepo = null; + } + else + { + autoSelectRepo = null; + } + } + else if (pages.Count > 0) + { + autoSelectPage = pages[0]; + autoSelectRepo = null; + } + else if (repos.Count > 0) + { + autoSelectPage = null; + autoSelectRepo = repos[0]; + } + else + { + autoSelectPage = null; + autoSelectRepo = null; + } + + VisiblePages = pages; + VisibleRepos = repos; + SelectedPage = autoSelectPage; + SelectedRepo = autoSelectRepo; + } + + private void CollectVisiblePages(List pages) + { + foreach (var page in _launcher.Pages) + { + if (page == _launcher.ActivePage) + continue; + + if (string.IsNullOrEmpty(_searchFilter) || + page.Node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase) || + (page.Node.IsRepository && page.Node.Id.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))) + pages.Add(page); + } + } + + private void CollectVisibleRepository(List outs, List nodes) + { + foreach (var node in nodes) + { + if (!node.IsRepository) + { + CollectVisibleRepository(outs, node.SubNodes); + continue; + } + + if (_opened.Contains(node.Id)) + continue; + + if (string.IsNullOrEmpty(_searchFilter) || + node.Id.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase) || + node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) + outs.Add(node); + } + } + + private Launcher _launcher = null; + private HashSet _opened = new HashSet(); + private List _visiblePages = []; + private List _visibleRepos = []; + private string _searchFilter = string.Empty; + private LauncherPage _selectedPage = null; + private RepositoryNode _selectedRepo = null; + } +} diff --git a/src/ViewModels/Merge.cs b/src/ViewModels/Merge.cs index 3ccf6a6fe..8da86daed 100644 --- a/src/ViewModels/Merge.cs +++ b/src/ViewModels/Merge.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.IO; +using System.Threading.Tasks; namespace SourceGit.ViewModels { @@ -16,8 +17,20 @@ public string Into public Models.MergeMode Mode { - get; - set; + get => _mode; + set + { + if (SetProperty(ref _mode, value)) + CanEditMessage = _mode == Models.MergeMode.Default || + _mode == Models.MergeMode.FastForward || + _mode == Models.MergeMode.NoFastForward; + } + } + + public bool CanEditMessage + { + get => _canEditMessage; + set => SetProperty(ref _canEditMessage, value); } public bool Edit @@ -58,28 +71,47 @@ public Merge(Repository repo, Models.Tag source, string into) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); _repo.ClearCommitMessage(); ProgressDescription = $"Merging '{_sourceName}' into '{Into}' ..."; var log = _repo.CreateLog($"Merging '{_sourceName}' into '{Into}'"); Use(log); - await new Commands.Merge(_repo.FullPath, _sourceName, Mode.Arg, Edit) + var succ = await new Commands.Merge(_repo.FullPath, _sourceName, Mode.Arg, _canEditMessage && Edit) .Use(log) .ExecAsync(); + if (succ) + { + var squashMsgFile = Path.Combine(_repo.GitDir, "SQUASH_MSG"); + if (Mode == Models.MergeMode.Squash && File.Exists(squashMsgFile)) + { + var msg = await File.ReadAllTextAsync(squashMsgFile); + _repo.SetCommitMessage(msg); + } + + var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath, false).GetResultAsync(); + if (submodules.Count > 0) + await new Commands.Submodule(_repo.FullPath) + .Use(log) + .UpdateAsync(submodules, false, true); + } + log.Complete(); - var head = await new Commands.QueryRevisionByRefName(_repo.FullPath, "HEAD").GetResultAsync(); - _repo.NavigateToCommit(head, true); - _repo.SetWatcherEnabled(true); + if (succ && _repo.SelectedViewIndex == 0) + { + var head = await new Commands.QueryRevisionByRefName(_repo.FullPath, "HEAD").GetResultAsync(); + _repo.NavigateToCommit(head, true); + } + return true; } private Models.MergeMode AutoSelectMergeMode() { - var config = new Commands.Config(_repo.FullPath).GetAsync($"branch.{Into}.mergeoptions").Result; + var config = new Commands.Config(_repo.FullPath).Get($"branch.{Into}.mergeoptions"); var mode = config switch { "--ff-only" => Models.MergeMode.FastForward, @@ -101,5 +133,7 @@ private Models.MergeMode AutoSelectMergeMode() private readonly Repository _repo = null; private readonly string _sourceName; + private Models.MergeMode _mode = Models.MergeMode.Default; + private bool _canEditMessage = true; } } diff --git a/src/ViewModels/MergeCommandPalette.cs b/src/ViewModels/MergeCommandPalette.cs new file mode 100644 index 000000000..ecab68cb0 --- /dev/null +++ b/src/ViewModels/MergeCommandPalette.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.ViewModels +{ + public class MergeCommandPalette : ICommandPalette + { + public List Branches + { + get => _branches; + private set => SetProperty(ref _branches, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateBranches(); + } + } + + public Models.Branch SelectedBranch + { + get => _selectedBranch; + set => SetProperty(ref _selectedBranch, value); + } + + public MergeCommandPalette(Launcher launcher, Repository repo) + { + _launcher = launcher; + _repo = repo; + UpdateBranches(); + } + + public override void Cleanup() + { + _launcher = null; + _repo = null; + _branches.Clear(); + _filter = null; + _selectedBranch = null; + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public void Launch() + { + if (_repo.CanCreatePopup() && _selectedBranch != null) + _repo.ShowPopup(new Merge(_repo, _selectedBranch, _repo.CurrentBranch.Name, false)); + + _launcher?.CancelCommandPalette(); + } + + private void UpdateBranches() + { + var current = _repo.CurrentBranch; + if (current == null) + return; + + var branches = new List(); + foreach (var b in _repo.Branches) + { + if (b == current) + continue; + + if (string.IsNullOrEmpty(_filter) || b.FriendlyName.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + branches.Add(b); + } + + branches.Sort((l, r) => + { + if (l.IsLocal == r.IsLocal) + return l.Name.CompareTo(r.Name); + + return l.IsLocal ? -1 : 1; + }); + + var autoSelected = _selectedBranch; + if (branches.Count == 0) + autoSelected = null; + else if (_selectedBranch == null || !branches.Contains(_selectedBranch)) + autoSelected = branches[0]; + + Branches = branches; + SelectedBranch = autoSelected; + } + + private Launcher _launcher = null; + private Repository _repo = null; + private List _branches = new List(); + private string _filter = string.Empty; + private Models.Branch _selectedBranch = null; + } +} diff --git a/src/ViewModels/MergeMultiple.cs b/src/ViewModels/MergeMultiple.cs index 03f199a87..781cadce1 100644 --- a/src/ViewModels/MergeMultiple.cs +++ b/src/ViewModels/MergeMultiple.cs @@ -40,7 +40,7 @@ public MergeMultiple(Repository repo, List branches) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); _repo.ClearCommitMessage(); ProgressDescription = "Merge head(s) ..."; @@ -56,7 +56,6 @@ public override async Task Sure() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/MoveSubmodule.cs b/src/ViewModels/MoveSubmodule.cs index 52210f41f..b17f21128 100644 --- a/src/ViewModels/MoveSubmodule.cs +++ b/src/ViewModels/MoveSubmodule.cs @@ -34,8 +34,8 @@ public override async Task Sure() if (oldPath.Equals(newPath, StringComparison.Ordinal)) return true; + using var lockWatcher = _repo.LockWatcher(); var log = _repo.CreateLog("Move Submodule"); - _repo.SetWatcherEnabled(false); Use(log); var succ = await new Commands.Move(_repo.FullPath, oldPath, newPath, false) @@ -43,7 +43,6 @@ public override async Task Sure() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/OpenFileCommandPalette.cs b/src/ViewModels/OpenFileCommandPalette.cs new file mode 100644 index 000000000..2ca5b706b --- /dev/null +++ b/src/ViewModels/OpenFileCommandPalette.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Threading; + +namespace SourceGit.ViewModels +{ + public class OpenFileCommandPalette : ICommandPalette + { + public bool IsLoading + { + get => _isLoading; + private set => SetProperty(ref _isLoading, value); + } + + public List VisibleFiles + { + get => _visibleFiles; + private set => SetProperty(ref _visibleFiles, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateVisible(); + } + } + + public string SelectedFile + { + get => _selectedFile; + set => SetProperty(ref _selectedFile, value); + } + + public OpenFileCommandPalette(Launcher launcher, string repo) + { + _launcher = launcher; + _repo = repo; + _isLoading = true; + + Task.Run(async () => + { + var files = await new Commands.QueryRevisionFileNames(_repo, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + IsLoading = false; + _repoFiles = files; + UpdateVisible(); + }); + }); + } + + public override void Cleanup() + { + _launcher = null; + _repo = null; + _repoFiles.Clear(); + _filter = null; + _visibleFiles.Clear(); + _selectedFile = null; + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public void Launch() + { + if (!string.IsNullOrEmpty(_selectedFile)) + Native.OS.OpenWithDefaultEditor(Native.OS.GetAbsPath(_repo, _selectedFile)); + + _launcher.CancelCommandPalette(); + } + + private void UpdateVisible() + { + if (_repoFiles is { Count: > 0 }) + { + if (string.IsNullOrEmpty(_filter)) + { + VisibleFiles = _repoFiles; + + if (string.IsNullOrEmpty(_selectedFile)) + SelectedFile = _repoFiles[0]; + } + else + { + var visible = new List(); + + foreach (var f in _repoFiles) + { + if (f.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + visible.Add(f); + } + + var autoSelected = _selectedFile; + if (visible.Count == 0) + autoSelected = null; + else if (string.IsNullOrEmpty(_selectedFile) || !visible.Contains(_selectedFile)) + autoSelected = visible[0]; + + VisibleFiles = visible; + SelectedFile = autoSelected; + } + } + } + + private Launcher _launcher = null; + private string _repo = null; + private bool _isLoading = false; + private List _repoFiles = null; + private string _filter = string.Empty; + private List _visibleFiles = []; + private string _selectedFile = null; + } +} diff --git a/src/ViewModels/Popup.cs b/src/ViewModels/Popup.cs index 942d33f9f..9d800c50c 100644 --- a/src/ViewModels/Popup.cs +++ b/src/ViewModels/Popup.cs @@ -4,7 +4,7 @@ namespace SourceGit.ViewModels { - public class Popup : ObservableValidator + public class Popup : ObservableValidator, Models.ICommandLogReceiver { public bool InProgress { @@ -27,6 +27,18 @@ public bool Check() return !HasErrors; } + public void OnReceiveCommandLog(string data) + { + var desc = data.Trim(); + if (!string.IsNullOrEmpty(desc)) + ProgressDescription = desc; + } + + public void Cleanup() + { + _log?.Unsubscribe(this); + } + public virtual bool CanStartDirectly() { return true; @@ -39,17 +51,12 @@ public virtual Task Sure() protected void Use(CommandLog log) { - log.Register(SetDescription); - } - - private void SetDescription(string data) - { - var desc = data.Trim(); - if (!string.IsNullOrEmpty(desc)) - ProgressDescription = desc; + _log = log; + _log.Subscribe(this); } private bool _inProgress = false; private string _progressDescription = string.Empty; + private CommandLog _log = null; } } diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 05be06010..564d415b6 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -23,6 +23,7 @@ public static Preferences Instance _instance.PrepareGit(); _instance.PrepareShellOrTerminal(); + _instance.PrepareExternalDiffMergeTool(); _instance.PrepareWorkspaces(); return _instance; @@ -65,7 +66,7 @@ public string DefaultFontFamily set { if (SetProperty(ref _defaultFontFamily, value) && !_isLoading) - App.SetFonts(value, _monospaceFontFamily, _onlyUseMonoFontInEditor); + App.SetFonts(value, _monospaceFontFamily); } } @@ -75,17 +76,7 @@ public string MonospaceFontFamily set { if (SetProperty(ref _monospaceFontFamily, value) && !_isLoading) - App.SetFonts(_defaultFontFamily, value, _onlyUseMonoFontInEditor); - } - } - - public bool OnlyUseMonoFontInEditor - { - get => _onlyUseMonoFontInEditor; - set - { - if (SetProperty(ref _onlyUseMonoFontInEditor, value) && !_isLoading) - App.SetFonts(_defaultFontFamily, _monospaceFontFamily, _onlyUseMonoFontInEditor); + App.SetFonts(_defaultFontFamily, value); } } @@ -119,6 +110,18 @@ public LayoutInfo Layout set => SetProperty(ref _layout, value); } + public bool ShowLocalChangesByDefault + { + get; + set; + } = false; + + public bool ShowChangesInCommitDetailByDefault + { + get; + set; + } = false; + public int MaxHistoryCommits { get => _maxHistoryCommits; @@ -152,6 +155,18 @@ public bool UseFixedTabWidth set => SetProperty(ref _useFixedTabWidth, value); } + public bool UseAutoHideScrollBars + { + get => _useAutoHideScrollBars; + set => SetProperty(ref _useAutoHideScrollBars, value); + } + + public bool UseGitHubStyleAvatar + { + get => _useGitHubStyleAvatar; + set => SetProperty(ref _useGitHubStyleAvatar, value); + } + public bool Check4UpdatesOnStartup { get => _check4UpdatesOnStartup; @@ -255,18 +270,24 @@ public bool UseFullTextDiff set => SetProperty(ref _useFullTextDiff, value); } - public bool UseBlockNavigationInDiffView - { - get => _useBlockNavigationInDiffView; - set => SetProperty(ref _useBlockNavigationInDiffView, value); - } - public int LFSImageActiveIdx { get => _lfsImageActiveIdx; set => SetProperty(ref _lfsImageActiveIdx, value); } + public int ImageDiffActiveIdx + { + get => _imageDiffActiveIdx; + set => SetProperty(ref _imageDiffActiveIdx, value); + } + + public bool EnableCompactFoldersInChangesTree + { + get => _enableCompactFoldersInChangesTree; + set => SetProperty(ref _enableCompactFoldersInChangesTree, value); + } + public Models.ChangeViewMode UnstagedChangeViewMode { get => _unstagedChangeViewMode; @@ -324,12 +345,12 @@ public bool UseLibsecretInsteadOfGCM } } - public int ShellOrTerminal + public int ShellOrTerminalType { - get => _shellOrTerminal; + get => _shellOrTerminalType; set { - if (SetProperty(ref _shellOrTerminal, value)) + if (SetProperty(ref _shellOrTerminalType, value) && !_isLoading) { if (value >= 0 && value < Models.ShellOrTerminal.Supported.Count) Native.OS.SetShellOrTerminal(Models.ShellOrTerminal.Supported[value]); @@ -337,6 +358,7 @@ public int ShellOrTerminal Native.OS.SetShellOrTerminal(null); OnPropertyChanged(nameof(ShellOrTerminalPath)); + OnPropertyChanged(nameof(ShellOrTerminalArgs)); } } } @@ -354,27 +376,77 @@ public string ShellOrTerminalPath } } + public string ShellOrTerminalArgs + { + get => Native.OS.ShellOrTerminalArgs; + set + { + if (value != Native.OS.ShellOrTerminalArgs) + { + Native.OS.ShellOrTerminalArgs = value; + OnPropertyChanged(); + } + } + } + public int ExternalMergeToolType { - get => _externalMergeToolType; + get => Native.OS.ExternalMergerType; set { - var changed = SetProperty(ref _externalMergeToolType, value); - if (changed && !OperatingSystem.IsWindows() && value > 0 && value < Models.ExternalMerger.Supported.Count) + if (Native.OS.ExternalMergerType != value) { - var tool = Models.ExternalMerger.Supported[value]; - if (File.Exists(tool.Exec)) - ExternalMergeToolPath = tool.Exec; - else - ExternalMergeToolPath = string.Empty; + Native.OS.ExternalMergerType = value; + OnPropertyChanged(); + + if (!_isLoading) + { + Native.OS.AutoSelectExternalMergeToolExecFile(); + OnPropertyChanged(nameof(ExternalMergeToolPath)); + OnPropertyChanged(nameof(ExternalMergeToolDiffArgs)); + OnPropertyChanged(nameof(ExternalMergeToolMergeArgs)); + } } } } public string ExternalMergeToolPath { - get => _externalMergeToolPath; - set => SetProperty(ref _externalMergeToolPath, value); + get => Native.OS.ExternalMergerExecFile; + set + { + if (!Native.OS.ExternalMergerExecFile.Equals(value, StringComparison.Ordinal)) + { + Native.OS.ExternalMergerExecFile = value; + OnPropertyChanged(); + } + } + } + + public string ExternalMergeToolDiffArgs + { + get => Native.OS.ExternalDiffArgs; + set + { + if (!Native.OS.ExternalDiffArgs.Equals(value, StringComparison.Ordinal)) + { + Native.OS.ExternalDiffArgs = value; + OnPropertyChanged(); + } + } + } + + public string ExternalMergeToolMergeArgs + { + get => Native.OS.ExternalMergeArgs; + set + { + if (!Native.OS.ExternalMergeArgs.Equals(value, StringComparison.Ordinal)) + { + Native.OS.ExternalMergeArgs = value; + OnPropertyChanged(); + } + } } public uint StatisticsSampleColor @@ -573,7 +645,7 @@ private void PrepareGit() private void PrepareShellOrTerminal() { - if (_shellOrTerminal >= 0) + if (_shellOrTerminalType >= 0) return; for (int i = 0; i < Models.ShellOrTerminal.Supported.Count; i++) @@ -581,12 +653,25 @@ private void PrepareShellOrTerminal() var shell = Models.ShellOrTerminal.Supported[i]; if (Native.OS.TestShellOrTerminal(shell)) { - ShellOrTerminal = i; + ShellOrTerminalType = i; break; } } } + private void PrepareExternalDiffMergeTool() + { + var mergerType = Native.OS.ExternalMergerType; + if (mergerType > 0 && mergerType < Models.ExternalMerger.Supported.Count) + { + var merger = Models.ExternalMerger.Supported[mergerType]; + if (string.IsNullOrEmpty(Native.OS.ExternalDiffArgs)) + Native.OS.ExternalDiffArgs = merger.DiffCmd; + if (string.IsNullOrEmpty(Native.OS.ExternalMergeArgs)) + Native.OS.ExternalMergeArgs = merger.MergeCmd; + } + } + private void PrepareWorkspaces() { if (Workspaces.Count == 0) @@ -682,15 +767,16 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private string _themeOverrides = string.Empty; private string _defaultFontFamily = string.Empty; private string _monospaceFontFamily = string.Empty; - private bool _onlyUseMonoFontInEditor = true; private double _defaultFontSize = 13; private double _editorFontSize = 13; private int _editorTabWidth = 4; - private LayoutInfo _layout = new LayoutInfo(); + private LayoutInfo _layout = new(); private int _maxHistoryCommits = 20000; private int _subjectGuideLength = 50; private bool _useFixedTabWidth = true; + private bool _useAutoHideScrollBars = true; + private bool _useGitHubStyleAvatar = true; private bool _showAuthorTimeInGraph = false; private bool _showChildren = false; @@ -707,8 +793,9 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private bool _enableDiffViewWordWrap = false; private bool _showHiddenSymbolsInDiffView = false; private bool _useFullTextDiff = false; - private bool _useBlockNavigationInDiffView = false; private int _lfsImageActiveIdx = 0; + private int _imageDiffActiveIdx = 0; + private bool _enableCompactFoldersInChangesTree = false; private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List; private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List; @@ -716,11 +803,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private Models.ChangeViewMode _stashChangeViewMode = Models.ChangeViewMode.List; private string _gitDefaultCloneDir = string.Empty; - - private int _shellOrTerminal = -1; - private int _externalMergeToolType = 0; - private string _externalMergeToolPath = string.Empty; - + private int _shellOrTerminalType = -1; private uint _statisticsSampleColor = 0xFF00FF00; } } diff --git a/src/ViewModels/PruneRemote.cs b/src/ViewModels/PruneRemote.cs index 56ba598de..cba2213cc 100644 --- a/src/ViewModels/PruneRemote.cs +++ b/src/ViewModels/PruneRemote.cs @@ -17,7 +17,7 @@ public PruneRemote(Repository repo, Models.Remote remote) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Run `prune` on remote ..."; var log = _repo.CreateLog($"Prune Remote '{Remote.Name}'"); @@ -28,7 +28,6 @@ public override async Task Sure() .PruneAsync(Remote.Name); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/PruneWorktrees.cs b/src/ViewModels/PruneWorktrees.cs index cce335276..561168362 100644 --- a/src/ViewModels/PruneWorktrees.cs +++ b/src/ViewModels/PruneWorktrees.cs @@ -11,7 +11,7 @@ public PruneWorktrees(Repository repo) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Prune worktrees ..."; var log = _repo.CreateLog("Prune Worktrees"); @@ -22,7 +22,6 @@ public override async Task Sure() .PruneAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index c3156536a..81a8ba096 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -50,17 +50,6 @@ public bool UseRebase set => _repo.Settings.PreferRebaseInsteadOfMerge = value; } - public bool IsRecurseSubmoduleVisible - { - get => _repo.Submodules.Count > 0; - } - - public bool RecurseSubmodules - { - get => _repo.Settings.UpdateSubmodulesOnCheckoutBranch; - set => _repo.Settings.UpdateSubmodulesOnCheckoutBranch = value; - } - public Pull(Repository repo, Models.Branch specifiedRemoteBranch) { _repo = repo; @@ -113,13 +102,12 @@ public Pull(Repository repo, Models.Branch specifiedRemoteBranch) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); var log = _repo.CreateLog("Pull"); Use(log); - var updateSubmodules = IsRecurseSubmoduleVisible && RecurseSubmodules; - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); var needPopStash = false; if (changes > 0) { @@ -133,7 +121,6 @@ public override async Task Sure() if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } @@ -148,12 +135,9 @@ public override async Task Sure() UseRebase).Use(log).RunAsync(); if (rs) { - if (updateSubmodules) - { - var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath).GetResultAsync(); - if (submodules.Count > 0) - await new Commands.Submodule(_repo.FullPath).Use(log).UpdateAsync(submodules, true, true); - } + var submodules = await new Commands.QueryUpdatableSubmodules(_repo.FullPath, false).GetResultAsync(); + if (submodules.Count > 0) + await new Commands.Submodule(_repo.FullPath).Use(log).UpdateAsync(submodules, false, true); if (needPopStash) await new Commands.Stash(_repo.FullPath).Use(log).PopAsync("stash@{0}"); @@ -161,9 +145,12 @@ public override async Task Sure() log.Complete(); - var head = await new Commands.QueryRevisionByRefName(_repo.FullPath, "HEAD").GetResultAsync(); - _repo.NavigateToCommit(head, true); - _repo.SetWatcherEnabled(true); + if (_repo.SelectedViewIndex == 0) + { + var head = await new Commands.QueryRevisionByRefName(_repo.FullPath, "HEAD").GetResultAsync(); + _repo.NavigateToCommit(head, true); + } + return rs; } diff --git a/src/ViewModels/Push.cs b/src/ViewModels/Push.cs index 85969ee5a..10999f259 100644 --- a/src/ViewModels/Push.cs +++ b/src/ViewModels/Push.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; @@ -69,9 +70,9 @@ public bool IsSetTrackOptionVisible public bool Tracking { - get; - set; - } = true; + get => _tracking; + set => SetProperty(ref _tracking, value); + } public bool IsCheckSubmodulesVisible { @@ -127,8 +128,10 @@ public Push(Repository repo, Models.Branch localBranch) } // Find preferred remote if selected local branch has upstream. - if (!string.IsNullOrEmpty(_selectedLocalBranch?.Upstream)) + if (!string.IsNullOrEmpty(_selectedLocalBranch?.Upstream) && !_selectedLocalBranch.IsUpstreamGone) { + _tracking = false; + foreach (var branch in repo.Branches) { if (!branch.IsLocal && _selectedLocalBranch.Upstream == branch.FullName) @@ -138,6 +141,10 @@ public Push(Repository repo, Models.Branch localBranch) } } } + else + { + _tracking = true; + } // Set default remote to the first if it has not been set. if (_selectedRemote == null) @@ -153,6 +160,27 @@ public Push(Repository repo, Models.Branch localBranch) AutoSelectBranchByRemote(); } + public void PushToNewBranch(string name) + { + var exist = _remoteBranches.Find(x => x.Name.Equals(name, StringComparison.Ordinal)); + if (exist != null) + { + SelectedRemoteBranch = exist; + return; + } + + var fake = new Models.Branch() + { + Name = name, + Remote = _selectedRemote.Name, + }; + var collection = new List(); + collection.AddRange(_remoteBranches); + collection.Add(fake); + RemoteBranches = collection; + SelectedRemoteBranch = fake; + } + public override bool CanStartDirectly() { return !string.IsNullOrEmpty(_selectedRemoteBranch?.Head); @@ -160,7 +188,7 @@ public override bool CanStartDirectly() public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); var remoteBranchName = _selectedRemoteBranch.Name; ProgressDescription = $"Push {_selectedLocalBranch.Name} -> {_selectedRemote.Name}/{remoteBranchName} ..."; @@ -175,11 +203,10 @@ public override async Task Sure() remoteBranchName, PushAllTags, _repo.Submodules.Count > 0 && CheckSubmodules, - _isSetTrackOptionVisible && Tracking, + _isSetTrackOptionVisible && _tracking, ForcePush).Use(log).RunAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } @@ -235,5 +262,6 @@ private void AutoSelectBranchByRemote() private List _remoteBranches = []; private Models.Branch _selectedRemoteBranch = null; private bool _isSetTrackOptionVisible = false; + private bool _tracking = true; } } diff --git a/src/ViewModels/PushRevision.cs b/src/ViewModels/PushRevision.cs index 491488c4b..4d49b5272 100644 --- a/src/ViewModels/PushRevision.cs +++ b/src/ViewModels/PushRevision.cs @@ -31,7 +31,7 @@ public PushRevision(Repository repo, Models.Commit revision, Models.Branch remot public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Push {Revision.SHA.AsSpan(0, 10)} -> {RemoteBranch.FriendlyName} ..."; var log = _repo.CreateLog("Push Revision"); @@ -48,7 +48,6 @@ public override async Task Sure() Force).Use(log).RunAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/PushTag.cs b/src/ViewModels/PushTag.cs index f2bb4500b..d59548254 100644 --- a/src/ViewModels/PushTag.cs +++ b/src/ViewModels/PushTag.cs @@ -36,7 +36,7 @@ public PushTag(Repository repo, Models.Tag target) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Pushing tag ..."; var log = _repo.CreateLog("Push Tag"); @@ -63,7 +63,6 @@ public override async Task Sure() } log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/Rebase.cs b/src/ViewModels/Rebase.cs index 1727bc868..315e3c634 100644 --- a/src/ViewModels/Rebase.cs +++ b/src/ViewModels/Rebase.cs @@ -42,7 +42,7 @@ public Rebase(Repository repo, Models.Branch current, Models.Commit on) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); _repo.ClearCommitMessage(); ProgressDescription = "Rebasing ..."; @@ -54,7 +54,6 @@ public override async Task Sure() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/RemoveWorktree.cs b/src/ViewModels/RemoveWorktree.cs index a78cedc2f..249c7683b 100644 --- a/src/ViewModels/RemoveWorktree.cs +++ b/src/ViewModels/RemoveWorktree.cs @@ -23,7 +23,7 @@ public RemoveWorktree(Repository repo, Models.Worktree target) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Remove worktree ..."; var log = _repo.CreateLog("Remove worktree"); @@ -34,7 +34,6 @@ public override async Task Sure() .RemoveAsync(Target.FullPath, Force); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/RenameBranch.cs b/src/ViewModels/RenameBranch.cs index 807e3406a..ececb16a5 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -12,7 +12,7 @@ public Models.Branch Target } [Required(ErrorMessage = "Branch name is required!!!")] - [RegularExpression(@"^[\w \-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] + [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(RenameBranch), nameof(ValidateBranchName))] public string Name { @@ -31,10 +31,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (ctx.ObjectInstance is RenameBranch rename) { - var fixedName = Models.Branch.FixName(name); foreach (var b in rename._repo.Branches) { - if (b.IsLocal && b != rename.Target && b.Name.Equals(fixedName, StringComparison.Ordinal)) + if (b.IsLocal && b != rename.Target && b.Name.Equals(name, StringComparison.Ordinal)) return new ValidationResult("A branch with same name already exists!!!"); } } @@ -44,11 +43,10 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext public override async Task Sure() { - var fixedName = Models.Branch.FixName(_name); - if (fixedName.Equals(Target.Name, StringComparison.Ordinal)) + if (Target.Name.Equals(_name, StringComparison.Ordinal)) return true; - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Rename '{Target.Name}'"; var log = _repo.CreateLog($"Rename Branch '{Target.Name}'"); @@ -59,16 +57,16 @@ public override async Task Sure() var succ = await new Commands.Branch(_repo.FullPath, Target.Name) .Use(log) - .RenameAsync(fixedName); + .RenameAsync(_name); if (succ) { - foreach (var filter in _repo.Settings.HistoriesFilters) + foreach (var filter in _repo.HistoryFilterCollection.Filters) { if (filter.Type == Models.FilterType.LocalBranch && filter.Pattern.Equals(oldName, StringComparison.Ordinal)) { - filter.Pattern = $"refs/heads/{fixedName}"; + filter.Pattern = $"refs/heads/{_name}"; break; } } @@ -76,7 +74,6 @@ public override async Task Sure() log.Complete(); _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); if (isCurrent) { diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 450c9e889..5ab2d1c27 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -7,9 +7,6 @@ using System.Threading.Tasks; using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Media; -using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -25,25 +22,12 @@ public bool IsBare public string FullPath { - get => _fullpath; - set - { - if (value != null) - { - var normalized = value.Replace('\\', '/').TrimEnd('/'); - SetProperty(ref _fullpath, normalized); - } - else - { - SetProperty(ref _fullpath, null); - } - } + get; } public string GitDir { - get => _gitDir; - set => SetProperty(ref _gitDir, value); + get; } public Models.RepositorySettings Settings @@ -55,12 +39,17 @@ public Models.GitFlow GitFlow { get; set; - } = new Models.GitFlow(); + } = new(); + + public Models.HistoryFilterCollection HistoryFilterCollection + { + get => _historyFilterCollection; + } - public Models.FilterMode HistoriesFilterMode + public Models.FilterMode HistoryFilterMode { - get => _historiesFilterMode; - private set => SetProperty(ref _historiesFilterMode, value); + get => _historyFilterMode; + private set => SetProperty(ref _historyFilterMode, value); } public bool HasAllowedSignersFile @@ -91,15 +80,28 @@ public object SelectedView set => SetProperty(ref _selectedView, value); } + public bool EnableTopoOrderInHistories + { + get => _settings.EnableTopoOrderInHistories; + set + { + if (value != _settings.EnableTopoOrderInHistories) + { + _settings.EnableTopoOrderInHistories = value; + RefreshCommits(); + } + } + } + public Models.HistoryShowFlags HistoryShowFlags { get => _settings.HistoryShowFlags; - set + private set { if (value != _settings.HistoryShowFlags) { _settings.HistoryShowFlags = value; - Task.Run(RefreshCommits); + RefreshCommits(); } } } @@ -256,96 +258,29 @@ public bool IncludeUntracked { _settings.IncludeUntrackedInLocalChanges = value; OnPropertyChanged(); - Task.Run(RefreshWorkingCopyChanges); + RefreshWorkingCopyChanges(); } } } - public bool IsSearching + public bool IsSearchingCommits { - get => _isSearching; + get => _isSearchingCommits; set { - if (SetProperty(ref _isSearching, value)) + if (SetProperty(ref _isSearchingCommits, value)) { if (value) - { SelectedViewIndex = 0; - CalcWorktreeFilesForSearching(); - } else - { - SearchedCommits = new List(); - SelectedSearchedCommit = null; - SearchCommitFilter = string.Empty; - MatchedFilesForSearching = null; - _requestingWorktreeFiles = false; - _worktreeFiles = null; - } - } - } - } - - public bool IsSearchLoadingVisible - { - get => _isSearchLoadingVisible; - private set => SetProperty(ref _isSearchLoadingVisible, value); - } - - public bool OnlySearchCommitsInCurrentBranch - { - get => _onlySearchCommitsInCurrentBranch; - set - { - if (SetProperty(ref _onlySearchCommitsInCurrentBranch, value) && !string.IsNullOrEmpty(_searchCommitFilter)) - StartSearchCommits(); - } - } - - public int SearchCommitFilterType - { - get => _searchCommitFilterType; - set - { - if (SetProperty(ref _searchCommitFilterType, value)) - { - CalcWorktreeFilesForSearching(); - if (!string.IsNullOrEmpty(_searchCommitFilter)) - StartSearchCommits(); + _searchCommitContext.EndSearch(); } } } - public string SearchCommitFilter - { - get => _searchCommitFilter; - set - { - if (SetProperty(ref _searchCommitFilter, value) && IsSearchingCommitsByFilePath()) - CalcMatchedFilesForSearching(); - } - } - - public List MatchedFilesForSearching - { - get => _matchedFilesForSearching; - private set => SetProperty(ref _matchedFilesForSearching, value); - } - - public List SearchedCommits - { - get => _searchedCommits; - set => SetProperty(ref _searchedCommits, value); - } - - public Models.Commit SelectedSearchedCommit + public SearchCommitContext SearchCommitContext { - get => _selectedSearchedCommit; - set - { - if (SetProperty(ref _selectedSearchedCommit, value) && value != null) - NavigateToCommit(value.SHA); - } + get => _searchCommitContext; } public bool IsLocalBranchGroupExpanded @@ -416,16 +351,40 @@ public bool IsWorktreeGroupExpanded public bool IsSortingLocalBranchByName { get => _settings.LocalBranchSortMode == Models.BranchSortMode.Name; + set + { + _settings.LocalBranchSortMode = value ? Models.BranchSortMode.Name : Models.BranchSortMode.CommitterDate; + OnPropertyChanged(); + + var builder = BuildBranchTree(_branches, _remotes); + LocalBranchTrees = builder.Locals; + RemoteBranchTrees = builder.Remotes; + } } public bool IsSortingRemoteBranchByName { get => _settings.RemoteBranchSortMode == Models.BranchSortMode.Name; + set + { + _settings.RemoteBranchSortMode = value ? Models.BranchSortMode.Name : Models.BranchSortMode.CommitterDate; + OnPropertyChanged(); + + var builder = BuildBranchTree(_branches, _remotes); + LocalBranchTrees = builder.Locals; + RemoteBranchTrees = builder.Remotes; + } } public bool IsSortingTagsByName { get => _settings.TagSortMode == Models.TagSortMode.Name; + set + { + _settings.TagSortMode = value ? Models.TagSortMode.Name : Models.TagSortMode.CreatorDate; + OnPropertyChanged(); + VisibleTags = BuildVisibleTags(); + } } public InProgressContext InProgressContext @@ -451,28 +410,43 @@ public bool IsAutoFetching private set => SetProperty(ref _isAutoFetching, value); } - public int CommitDetailActivePageIndex + public AvaloniaList IssueTrackers { get; - set; - } = 0; + } = []; public AvaloniaList Logs { get; - private set; - } = new AvaloniaList(); + } = []; public Repository(bool isBare, string path, string gitDir) { IsBare = isBare; - FullPath = path; - GitDir = gitDir; + FullPath = path.Replace('\\', '/').TrimEnd('/'); + GitDir = gitDir.Replace('\\', '/').TrimEnd('/'); + + var commonDirFile = Path.Combine(GitDir, "commondir"); + _isWorktree = GitDir.IndexOf("/worktrees/", StringComparison.Ordinal) > 0 && + File.Exists(commonDirFile); + + if (_isWorktree) + { + var commonDir = File.ReadAllText(commonDirFile).Trim(); + if (!Path.IsPathRooted(commonDir)) + commonDir = new DirectoryInfo(Path.Combine(GitDir, commonDir)).FullName; + + _gitCommonDir = commonDir; + } + else + { + _gitCommonDir = GitDir; + } } public void Open() { - var settingsFile = Path.Combine(_gitDir, "sourcegit.settings"); + var settingsFile = Path.Combine(_gitCommonDir, "sourcegit.settings"); if (File.Exists(settingsFile)) { try @@ -490,37 +464,52 @@ public void Open() _settings = new Models.RepositorySettings(); } - try + var historyFilterFile = Path.Combine(GitDir, "sourcegit.filters"); + if (File.Exists(historyFilterFile)) { - // For worktrees, we need to watch the $GIT_COMMON_DIR instead of the $GIT_DIR. - var gitDirForWatcher = _gitDir; - if (_gitDir.Replace('\\', '/').IndexOf("/worktrees/", StringComparison.Ordinal) > 0) + try { - var commonDir = new Commands.QueryGitCommonDir(_fullpath).GetResultAsync().Result; - if (!string.IsNullOrEmpty(commonDir)) - gitDirForWatcher = commonDir; + using var stream = File.OpenRead(historyFilterFile); + _historyFilterCollection = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.HistoryFilterCollection); } + catch + { + _historyFilterCollection = new Models.HistoryFilterCollection(); + } + } + else + { + _historyFilterCollection = new Models.HistoryFilterCollection(); + } - _watcher = new Models.Watcher(this, _fullpath, gitDirForWatcher); + try + { + _watcher = new Models.Watcher(this, FullPath, _gitCommonDir); } catch (Exception ex) { - App.RaiseException(string.Empty, $"Failed to start watcher for repository: '{_fullpath}'. You may need to press 'F5' to refresh repository manually!\n\nReason: {ex.Message}"); + App.RaiseException(string.Empty, $"Failed to start watcher for repository: '{FullPath}'. You may need to press 'F5' to refresh repository manually!\n\nReason: {ex.Message}"); } - if (_settings.HistoriesFilters.Count > 0) - _historiesFilterMode = _settings.HistoriesFilters[0].Mode; - else - _historiesFilterMode = Models.FilterMode.None; - + _historyFilterMode = _historyFilterCollection.Mode; _histories = new Histories(this); - _workingCopy = new WorkingCopy(this); + _workingCopy = new WorkingCopy(this) { CommitMessage = _settings.LastCommitMessage }; _stashesPage = new StashesPage(this); - _selectedView = _histories; - _selectedViewIndex = 0; + _searchCommitContext = new SearchCommitContext(this); + + if (Preferences.Instance.ShowLocalChangesByDefault) + { + _selectedView = _workingCopy; + _selectedViewIndex = 1; + } + else + { + _selectedView = _histories; + _selectedViewIndex = 0; + } - _workingCopy.CommitMessage = _settings.LastCommitMessage; - _autoFetchTimer = new Timer(AutoFetchImpl, null, 5000, 5000); + _lastFetchTime = DateTime.Now; + _autoFetchTimer = new Timer(AutoFetchByTimer, null, 5000, 5000); RefreshAll(); } @@ -529,34 +518,60 @@ public void Close() SelectedView = null; // Do NOT modify. Used to remove exists widgets for GC.Collect Logs.Clear(); - _settings.LastCommitMessage = _workingCopy.CommitMessage; - - var sharedIssueTrackers = new List(); - foreach (var rule in _settings.IssueTrackerRules) - if (rule.IsShared) - sharedIssueTrackers.Add(rule); - - _settings.IssueTrackerRules.RemoveAll(sharedIssueTrackers); - - try + if (!_isWorktree && Directory.Exists(_gitCommonDir)) { - using var stream = File.Create(Path.Combine(_gitDir, "sourcegit.settings")); - JsonSerializer.Serialize(stream, _settings, JsonCodeGen.Default.RepositorySettings); + try + { + if (_workingCopy.InProgressContext != null && !string.IsNullOrEmpty(_workingCopy.CommitMessage)) + File.WriteAllText(Path.Combine(GitDir, "MERGE_MSG"), _workingCopy.CommitMessage); + else + _settings.LastCommitMessage = _workingCopy.CommitMessage; + + using var stream = File.Create(Path.Combine(_gitCommonDir, "sourcegit.settings")); + JsonSerializer.Serialize(stream, _settings, JsonCodeGen.Default.RepositorySettings); + } + catch (Exception) + { + // Ignore + } } - catch + + if (Directory.Exists(GitDir)) { - // Ignore + try + { + using var stream = File.Create(Path.Combine(GitDir, "sourcegit.filters")); + JsonSerializer.Serialize(stream, _historyFilterCollection, JsonCodeGen.Default.HistoryFilterCollection); + } + catch + { + // Ignore + } } + + if (_cancellationRefreshBranches is { IsCancellationRequested: false }) + _cancellationRefreshBranches.Cancel(); + if (_cancellationRefreshTags is { IsCancellationRequested: false }) + _cancellationRefreshTags.Cancel(); + if (_cancellationRefreshWorkingCopyChanges is { IsCancellationRequested: false }) + _cancellationRefreshWorkingCopyChanges.Cancel(); + if (_cancellationRefreshCommits is { IsCancellationRequested: false }) + _cancellationRefreshCommits.Cancel(); + if (_cancellationRefreshStashes is { IsCancellationRequested: false }) + _cancellationRefreshStashes.Cancel(); + _autoFetchTimer.Dispose(); _autoFetchTimer = null; _settings = null; - _historiesFilterMode = Models.FilterMode.None; + _historyFilterCollection = null; + _historyFilterMode = Models.FilterMode.None; _watcher?.Dispose(); _histories.Dispose(); _workingCopy.Dispose(); _stashesPage.Dispose(); + _searchCommitContext.Dispose(); _watcher = null; _histories = null; @@ -574,12 +589,6 @@ public void Close() _visibleTags = null; _submodules.Clear(); _visibleSubmodules = null; - _searchedCommits.Clear(); - _selectedSearchedCommit = null; - - _requestingWorktreeFiles = false; - _worktreeFiles = null; - _matchedFilesForSearching = null; } public bool CanCreatePopup() @@ -598,9 +607,13 @@ public void ShowPopup(Popup popup) page.Popup = popup; } - public void ShowAndStartPopup(Popup popup) + public async Task ShowAndStartPopupAsync(Popup popup) { - GetOwnerPage()?.StartPopup(popup); + var page = GetOwnerPage(); + page.Popup = popup; + + if (popup.CanStartDirectly()) + await page.ProcessPopupAsync(); } public bool IsGitFlowEnabled() @@ -627,23 +640,40 @@ public Models.GitFlowBranchType GetGitFlowType(Models.Branch b) public bool IsLFSEnabled() { - var path = Path.Combine(_fullpath, ".git", "hooks", "pre-push"); + var path = Path.Combine(FullPath, ".git", "hooks", "pre-push"); if (!File.Exists(path)) return false; - var content = File.ReadAllText(path); - return content.Contains("git lfs pre-push"); + try + { + var content = File.ReadAllText(path); + return content.Contains("git lfs pre-push"); + } + catch + { + return false; + } + } + + public async Task InstallLFSAsync() + { + var log = CreateLog("Install LFS"); + var succ = await new Commands.LFS(FullPath).Use(log).InstallAsync(); + if (succ) + App.SendNotification(FullPath, "LFS enabled successfully!"); + + log.Complete(); } public async Task TrackLFSFileAsync(string pattern, bool isFilenameMode) { var log = CreateLog("Track LFS"); - var succ = await new Commands.LFS(_fullpath) + var succ = await new Commands.LFS(FullPath) .Use(log) .TrackAsync(pattern, isFilenameMode); if (succ) - App.SendNotification(_fullpath, $"Tracking successfully! Pattern: {pattern}"); + App.SendNotification(FullPath, $"Tracking successfully! Pattern: {pattern}"); log.Complete(); return succ; @@ -652,12 +682,12 @@ public async Task TrackLFSFileAsync(string pattern, bool isFilenameMode) public async Task LockLFSFileAsync(string remote, string path) { var log = CreateLog("Lock LFS File"); - var succ = await new Commands.LFS(_fullpath) + var succ = await new Commands.LFS(FullPath) .Use(log) .LockAsync(remote, path); if (succ) - App.SendNotification(_fullpath, $"Lock file successfully! File: {path}"); + App.SendNotification(FullPath, $"Lock file successfully! File: {path}"); log.Complete(); return succ; @@ -666,12 +696,12 @@ public async Task LockLFSFileAsync(string remote, string path) public async Task UnlockLFSFileAsync(string remote, string path, bool force, bool notify) { var log = CreateLog("Unlock LFS File"); - var succ = await new Commands.LFS(_fullpath) + var succ = await new Commands.LFS(FullPath) .Use(log) .UnlockAsync(remote, path, force); if (succ && notify) - App.SendNotification(_fullpath, $"Unlock file successfully! File: {path}"); + App.SendNotification(FullPath, $"Unlock file successfully! File: {path}"); log.Complete(); return succ; @@ -686,21 +716,26 @@ public CommandLog CreateLog(string name) public void RefreshAll() { - Task.Run(RefreshCommits); - Task.Run(RefreshBranches); - Task.Run(RefreshTags); - Task.Run(RefreshSubmodules); - Task.Run(RefreshWorktrees); - Task.Run(RefreshWorkingCopyChanges); - Task.Run(RefreshStashes); + RefreshCommits(); + RefreshBranches(); + RefreshTags(); + RefreshSubmodules(); + RefreshWorktrees(); + RefreshWorkingCopyChanges(); + RefreshStashes(); Task.Run(async () => { - var sharedIssueTrackers = await new Commands.SharedIssueTracker(_fullpath).ReadAllAsync().ConfigureAwait(false); - if (sharedIssueTrackers.Count > 0) - Dispatcher.UIThread.Post(() => _settings.IssueTrackerRules.InsertRange(0, sharedIssueTrackers)); + var issuetrackers = new List(); + await new Commands.IssueTracker(FullPath, true).ReadAllAsync(issuetrackers, true).ConfigureAwait(false); + await new Commands.IssueTracker(FullPath, false).ReadAllAsync(issuetrackers, false).ConfigureAwait(false); + Dispatcher.UIThread.Post(() => + { + IssueTrackers.Clear(); + IssueTrackers.AddRange(issuetrackers); + }); - var config = await new Commands.Config(_fullpath).ReadAllAsync().ConfigureAwait(false); + var config = await new Commands.Config(FullPath).ReadAllAsync().ConfigureAwait(false); _hasAllowedSignersFile = config.TryGetValue("gpg.ssh.allowedSignersFile", out var allowedSignersFile) && !string.IsNullOrEmpty(allowedSignersFile); if (config.TryGetValue("gitflow.branch.master", out var masterName)) @@ -716,147 +751,66 @@ public void RefreshAll() }); } - public ContextMenu CreateContextMenuForExternalTools() - { - var menu = new ContextMenu(); - menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - - RenderOptions.SetBitmapInterpolationMode(menu, BitmapInterpolationMode.HighQuality); - RenderOptions.SetEdgeMode(menu, EdgeMode.Antialias); - RenderOptions.SetTextRenderingMode(menu, TextRenderingMode.Antialias); - - var explore = new MenuItem(); - explore.Header = App.Text("Repository.Explore"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.Click += (_, e) => - { - Native.OS.OpenInFileManager(_fullpath); - e.Handled = true; - }; - - var terminal = new MenuItem(); - terminal.Header = App.Text("Repository.Terminal"); - terminal.Icon = App.CreateMenuIcon("Icons.Terminal"); - terminal.Click += (_, e) => - { - Native.OS.OpenTerminal(_fullpath); - e.Handled = true; - }; - - menu.Items.Add(explore); - menu.Items.Add(terminal); - - var tools = Native.OS.ExternalTools; - if (tools.Count > 0) - { - menu.Items.Add(new MenuItem() { Header = "-" }); - - foreach (var tool in tools) - { - var dupTool = tool; - - var item = new MenuItem(); - item.Header = App.Text("Repository.OpenIn", dupTool.Name); - item.Icon = new Image { Width = 16, Height = 16, Source = dupTool.IconImage }; - item.Click += (_, e) => - { - dupTool.Open(_fullpath); - e.Handled = true; - }; - - menu.Items.Add(item); - } - } - - var urls = new Dictionary(); - foreach (var r in _remotes) - { - if (r.TryGetVisitURL(out var visit)) - urls.Add(r.Name, visit); - } - - if (urls.Count > 0) - { - menu.Items.Add(new MenuItem() { Header = "-" }); - - foreach (var (name, addr) in urls) - { - var item = new MenuItem(); - item.Header = App.Text("Repository.Visit", name); - item.Icon = App.CreateMenuIcon("Icons.Remotes"); - item.Click += (_, e) => - { - Native.OS.OpenBrowser(addr); - e.Handled = true; - }; - - menu.Items.Add(item); - } - } - - return menu; - } - - public void Fetch(bool autoStart) + public async Task FetchAsync(bool autoStart) { if (!CanCreatePopup()) return; if (_remotes.Count == 0) { - App.RaiseException(_fullpath, "No remotes added to this repository!!!"); + App.RaiseException(FullPath, "No remotes added to this repository!!!"); return; } if (autoStart) - ShowAndStartPopup(new Fetch(this)); + await ShowAndStartPopupAsync(new Fetch(this)); else ShowPopup(new Fetch(this)); } - public void Pull(bool autoStart) + public async Task PullAsync(bool autoStart) { if (IsBare || !CanCreatePopup()) return; if (_remotes.Count == 0) { - App.RaiseException(_fullpath, "No remotes added to this repository!!!"); + App.RaiseException(FullPath, "No remotes added to this repository!!!"); return; } if (_currentBranch == null) { - App.RaiseException(_fullpath, "Can NOT find current branch!!!"); + App.RaiseException(FullPath, "Can NOT find current branch!!!"); return; } var pull = new Pull(this, null); if (autoStart && pull.SelectedBranch != null) - ShowAndStartPopup(pull); + await ShowAndStartPopupAsync(pull); else ShowPopup(pull); } - public void Push(bool autoStart) + public async Task PushAsync(bool autoStart) { if (!CanCreatePopup()) return; if (_remotes.Count == 0) { - App.RaiseException(_fullpath, "No remotes added to this repository!!!"); + App.RaiseException(FullPath, "No remotes added to this repository!!!"); return; } if (_currentBranch == null) { - App.RaiseException(_fullpath, "Can NOT find current branch!!!"); + App.RaiseException(FullPath, "Can NOT find current branch!!!"); return; } if (autoStart) - ShowAndStartPopup(new Push(this, null)); + await ShowAndStartPopupAsync(new Push(this, null)); else ShowPopup(new Push(this, null)); } @@ -867,29 +821,22 @@ public void ApplyPatch() ShowPopup(new Apply(this)); } - public void ExecCustomAction(Models.CustomAction action, object scope) + public async Task ExecCustomActionAsync(Models.CustomAction action, object scopeTarget) { if (!CanCreatePopup()) return; - var popup = scope switch - { - Models.Branch b => new ExecuteCustomAction(this, action, b), - Models.Commit c => new ExecuteCustomAction(this, action, c), - Models.Tag t => new ExecuteCustomAction(this, action, t), - _ => new ExecuteCustomAction(this, action) - }; - + var popup = new ExecuteCustomAction(this, action, scopeTarget); if (action.Controls.Count == 0) - ShowAndStartPopup(popup); + await ShowAndStartPopupAsync(popup); else ShowPopup(popup); } - public void Cleanup() + public async Task CleanupAsync() { if (CanCreatePopup()) - ShowAndStartPopup(new Cleanup(this)); + await ShowAndStartPopupAsync(new Cleanup(this)); } public void ClearFilter() @@ -897,98 +844,43 @@ public void ClearFilter() Filter = string.Empty; } - public void ClearSearchCommitFilter() + public IDisposable LockWatcher() { - SearchCommitFilter = string.Empty; + return _watcher?.Lock(); } - public void ClearMatchedFilesForSearching() - { - MatchedFilesForSearching = null; - } - - public void StartSearchCommits() + public void MarkBranchesDirtyManually() { - if (_histories == null) - return; - - IsSearchLoadingVisible = true; - SelectedSearchedCommit = null; - MatchedFilesForSearching = null; - - Task.Run(async () => - { - var visible = new List(); - var method = (Models.CommitSearchMethod)_searchCommitFilterType; - - if (method == Models.CommitSearchMethod.BySHA) - { - var isCommitSHA = await new Commands.IsCommitSHA(_fullpath, _searchCommitFilter) - .GetResultAsync() - .ConfigureAwait(false); - - if (isCommitSHA) - { - var commit = await new Commands.QuerySingleCommit(_fullpath, _searchCommitFilter) - .GetResultAsync() - .ConfigureAwait(false); - visible.Add(commit); - } - } - else - { - visible = await new Commands.QueryCommits(_fullpath, _searchCommitFilter, method, _onlySearchCommitsInCurrentBranch) - .GetResultAsync() - .ConfigureAwait(false); - } - - Dispatcher.UIThread.Post(() => - { - SearchedCommits = visible; - IsSearchLoadingVisible = false; - }); - }); + _watcher?.MarkBranchUpdated(); + RefreshBranches(); + RefreshCommits(); + RefreshWorkingCopyChanges(); + RefreshWorktrees(); } - public void SetWatcherEnabled(bool enabled) + public void MarkTagsDirtyManually() { - _watcher?.SetEnabled(enabled); + _watcher?.MarkTagUpdated(); + RefreshTags(); + RefreshCommits(); } - public void MarkBranchesDirtyManually() + public void MarkWorkingCopyDirtyManually() { - if (_watcher == null) - { - Task.Run(RefreshBranches); - Task.Run(RefreshCommits); - Task.Run(RefreshWorkingCopyChanges); - Task.Run(RefreshWorktrees); - } - else - { - _watcher.MarkBranchDirtyManually(); - } + _watcher?.MarkWorkingCopyUpdated(); + RefreshWorkingCopyChanges(); } - public void MarkTagsDirtyManually() + public void MarkStashesDirtyManually() { - if (_watcher == null) - { - Task.Run(RefreshTags); - Task.Run(RefreshCommits); - } - else - { - _watcher.MarkTagDirtyManually(); - } + _watcher?.MarkStashUpdated(); + RefreshStashes(); } - public void MarkWorkingCopyDirtyManually() + public void MarkSubmodulesDirtyManually() { - if (_watcher == null) - Task.Run(RefreshWorkingCopyChanges); - else - _watcher.MarkWorkingCopyDirtyManually(); + _watcher?.MarkSubmodulesUpdated(); + RefreshSubmodules(); } public void MarkFetched() @@ -1002,36 +894,47 @@ public void NavigateToCommit(string sha, bool isDelayMode = false) { _navigateToCommitDelayed = sha; } - else if (_histories != null) + else { SelectedViewIndex = 0; - _histories.NavigateTo(sha); + _histories?.NavigateTo(sha); } } + public void SetCommitMessage(string message) + { + if (_workingCopy is not null) + _workingCopy.CommitMessage = message; + } + public void ClearCommitMessage() { if (_workingCopy is not null) _workingCopy.CommitMessage = string.Empty; } - public void ClearHistoriesFilter() + public Models.Commit GetSelectedCommitInHistory() { - _settings.HistoriesFilters.Clear(); - HistoriesFilterMode = Models.FilterMode.None; + return (_histories?.DetailContext as CommitDetail)?.Commit; + } + + public void ClearHistoryFilters() + { + _historyFilterCollection.Filters.Clear(); + HistoryFilterMode = Models.FilterMode.None; ResetBranchTreeFilterMode(LocalBranchTrees); ResetBranchTreeFilterMode(RemoteBranchTrees); ResetTagFilterMode(); - Task.Run(RefreshCommits); + RefreshCommits(); } - public void RemoveHistoriesFilter(Models.Filter filter) + public void RemoveHistoryFilter(Models.HistoryFilter filter) { - if (_settings.HistoriesFilters.Remove(filter)) + if (_historyFilterCollection.Filters.Remove(filter)) { - HistoriesFilterMode = _settings.HistoriesFilters.Count > 0 ? _settings.HistoriesFilters[0].Mode : Models.FilterMode.None; - RefreshHistoriesFilters(true); + HistoryFilterMode = _historyFilterCollection.Mode; + RefreshHistoryFilters(true); } } @@ -1053,9 +956,9 @@ public void UpdateBranchNodeIsExpanded(BranchTreeNode node) public void SetTagFilterMode(Models.Tag tag, Models.FilterMode mode) { - var changed = _settings.UpdateHistoriesFilter(tag.Name, Models.FilterType.Tag, mode); + var changed = _historyFilterCollection.Update(tag.Name, Models.FilterType.Tag, mode); if (changed) - RefreshHistoriesFilters(true); + RefreshHistoryFilters(true); } public void SetBranchFilterMode(Models.Branch branch, Models.FilterMode mode, bool clearExists, bool refresh) @@ -1072,28 +975,28 @@ public void SetBranchFilterMode(BranchTreeNode node, Models.FilterMode mode, boo if (clearExists) { - _settings.HistoriesFilters.Clear(); - HistoriesFilterMode = Models.FilterMode.None; + _historyFilterCollection.Filters.Clear(); + HistoryFilterMode = Models.FilterMode.None; } if (node.Backend is Models.Branch branch) { var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch; - var changed = _settings.UpdateHistoriesFilter(node.Path, type, mode); + var changed = _historyFilterCollection.Update(node.Path, type, mode); if (!changed) return; if (isLocal && !string.IsNullOrEmpty(branch.Upstream) && !branch.IsUpstreamGone) - _settings.UpdateHistoriesFilter(branch.Upstream, Models.FilterType.RemoteBranch, mode); + _historyFilterCollection.Update(branch.Upstream, Models.FilterType.RemoteBranch, mode); } else { var type = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; - var changed = _settings.UpdateHistoriesFilter(node.Path, type, mode); + var changed = _historyFilterCollection.Update(node.Path, type, mode); if (!changed) return; - _settings.RemoveChildrenBranchFilters(node.Path); + _historyFilterCollection.RemoveBranchFiltersByPrefix(node.Path); } var parentType = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; @@ -1109,26 +1012,35 @@ public void SetBranchFilterMode(BranchTreeNode node, Models.FilterMode mode, boo if (parent == null) break; - _settings.UpdateHistoriesFilter(parent.Path, parentType, Models.FilterMode.None); + _historyFilterCollection.Update(parent.Path, parentType, Models.FilterMode.None); cur = parent; } while (true); - RefreshHistoriesFilters(refresh); + RefreshHistoryFilters(refresh); } - public void StashAll(bool autoStart) + public async Task StashAllAsync(bool autoStart) { - _workingCopy?.StashAll(autoStart); + if (!CanCreatePopup()) + return; + + var popup = new StashChanges(this, null); + if (autoStart) + await ShowAndStartPopupAsync(popup); + else + ShowPopup(popup); } - public void SkipMerge() + public async Task SkipMergeAsync() { - _workingCopy?.SkipMerge(); + if (_workingCopy != null) + await _workingCopy.SkipMergeAsync(); } - public void AbortMerge() + public async Task AbortMergeAsync() { - _workingCopy?.AbortMerge(); + if (_workingCopy != null) + await _workingCopy.AbortMergeAsync(); } public List<(Models.CustomAction, CustomActionContextMenuLabel)> GetCustomActions(Models.CustomActionScope scope) @@ -1152,140 +1064,179 @@ public void AbortMerge() public async Task ExecBisectCommandAsync(string subcmd) { + using var lockWatcher = _watcher?.Lock(); IsBisectCommandRunning = true; - SetWatcherEnabled(false); var log = CreateLog($"Bisect({subcmd})"); - var succ = await new Commands.Bisect(_fullpath, subcmd).Use(log).ExecAsync(); + var succ = await new Commands.Bisect(FullPath, subcmd).Use(log).ExecAsync(); log.Complete(); - var head = await new Commands.QueryRevisionByRefName(_fullpath, "HEAD").GetResultAsync(); + var head = await new Commands.QueryRevisionByRefName(FullPath, "HEAD").GetResultAsync(); if (!succ) - App.RaiseException(_fullpath, log.Content.Substring(log.Content.IndexOf('\n')).Trim()); + App.RaiseException(FullPath, log.Content.Substring(log.Content.IndexOf('\n')).Trim()); else if (log.Content.Contains("is the first bad commit")) - App.SendNotification(_fullpath, log.Content.Substring(log.Content.IndexOf('\n')).Trim()); + App.SendNotification(FullPath, log.Content.Substring(log.Content.IndexOf('\n')).Trim()); MarkBranchesDirtyManually(); NavigateToCommit(head, true); - SetWatcherEnabled(true); IsBisectCommandRunning = false; } public bool MayHaveSubmodules() { - var modulesFile = Path.Combine(_fullpath, ".gitmodules"); + var modulesFile = Path.Combine(FullPath, ".gitmodules"); var info = new FileInfo(modulesFile); return info.Exists && info.Length > 20; } public void RefreshBranches() { - var branches = new Commands.QueryBranches(_fullpath).GetResultAsync().Result; - var remotes = new Commands.QueryRemotes(_fullpath).GetResultAsync().Result; - var builder = BuildBranchTree(branches, remotes); + if (_cancellationRefreshBranches is { IsCancellationRequested: false }) + _cancellationRefreshBranches.Cancel(); - Dispatcher.UIThread.Invoke(() => - { - lock (_lockRemotes) - Remotes = remotes; + _cancellationRefreshBranches = new CancellationTokenSource(); + var token = _cancellationRefreshBranches.Token; - Branches = branches; - CurrentBranch = branches.Find(x => x.IsCurrent); - LocalBranchTrees = builder.Locals; - RemoteBranchTrees = builder.Remotes; + Task.Run(async () => + { + var branches = await new Commands.QueryBranches(FullPath).GetResultAsync().ConfigureAwait(false); + var remotes = await new Commands.QueryRemotes(FullPath).GetResultAsync().ConfigureAwait(false); + var builder = BuildBranchTree(branches, remotes); - var localBranchesCount = 0; - foreach (var b in branches) + Dispatcher.UIThread.Invoke(() => { - if (b.IsLocal && !b.IsDetachedHead) - localBranchesCount++; - } - LocalBranchesCount = localBranchesCount; + if (token.IsCancellationRequested) + return; - if (_workingCopy != null) - _workingCopy.HasRemotes = remotes.Count > 0; + Remotes = remotes; + Branches = branches; + CurrentBranch = branches.Find(x => x.IsCurrent); + LocalBranchTrees = builder.Locals; + RemoteBranchTrees = builder.Remotes; - var hasPendingPullOrPush = CurrentBranch?.TrackStatus.IsVisible ?? false; - GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasPendingPullOrPush, !hasPendingPullOrPush); - }); + var localBranchesCount = 0; + foreach (var b in branches) + { + if (b.IsLocal && !b.IsDetachedHead) + localBranchesCount++; + } + LocalBranchesCount = localBranchesCount; + + if (_workingCopy != null) + _workingCopy.HasRemotes = remotes.Count > 0; + + var hasPendingPullOrPush = CurrentBranch?.IsTrackStatusVisible ?? false; + GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasPendingPullOrPush, !hasPendingPullOrPush); + }); + }, token); } public void RefreshWorktrees() { - var worktrees = new Commands.Worktree(_fullpath).ReadAllAsync().Result; - var cleaned = new List(); - - foreach (var worktree in worktrees) + Task.Run(async () => { - if (worktree.IsBare || worktree.FullPath.Equals(_fullpath)) - continue; + var worktrees = await new Commands.Worktree(FullPath).ReadAllAsync().ConfigureAwait(false); + if (worktrees.Count == 0) + { + Dispatcher.UIThread.Invoke(() => Worktrees = worktrees); + return; + } - cleaned.Add(worktree); - } + var cleaned = new List(); + foreach (var worktree in worktrees) + { + if (worktree.FullPath.Equals(FullPath, StringComparison.Ordinal) || + worktree.FullPath.Equals(GitDir, StringComparison.Ordinal)) + continue; - Dispatcher.UIThread.Invoke(() => - { - Worktrees = cleaned; + cleaned.Add(worktree); + } + + Dispatcher.UIThread.Invoke(() => Worktrees = cleaned); }); } public void RefreshTags() { - var tags = new Commands.QueryTags(_fullpath).GetResultAsync().Result; - Dispatcher.UIThread.Invoke(() => + if (_cancellationRefreshTags is { IsCancellationRequested: false }) + _cancellationRefreshTags.Cancel(); + + _cancellationRefreshTags = new CancellationTokenSource(); + var token = _cancellationRefreshTags.Token; + + Task.Run(async () => { - Tags = tags; - VisibleTags = BuildVisibleTags(); - }); + var tags = await new Commands.QueryTags(FullPath).GetResultAsync().ConfigureAwait(false); + Dispatcher.UIThread.Invoke(() => + { + if (token.IsCancellationRequested) + return; + + Tags = tags; + VisibleTags = BuildVisibleTags(); + }); + }, token); } public void RefreshCommits() { - Dispatcher.UIThread.Invoke(() => _histories.IsLoading = true); + if (_cancellationRefreshCommits is { IsCancellationRequested: false }) + _cancellationRefreshCommits.Cancel(); - var builder = new StringBuilder(); - builder.Append($"-{Preferences.Instance.MaxHistoryCommits} "); + _cancellationRefreshCommits = new CancellationTokenSource(); + var token = _cancellationRefreshCommits.Token; - if (_settings.EnableTopoOrderInHistories) - builder.Append("--topo-order "); - else - builder.Append("--date-order "); + Task.Run(async () => + { + await Dispatcher.UIThread.InvokeAsync(() => _histories.IsLoading = true); + + var builder = new StringBuilder(); + builder.Append($"-{Preferences.Instance.MaxHistoryCommits} "); + + if (_settings.EnableTopoOrderInHistories) + builder.Append("--topo-order "); + else + builder.Append("--date-order "); - if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.Reflog)) - builder.Append("--reflog "); + if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.Reflog)) + builder.Append("--reflog "); - if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)) - builder.Append("--first-parent "); + if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)) + builder.Append("--first-parent "); - if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.SimplifyByDecoration)) - builder.Append("--simplify-by-decoration "); + if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.SimplifyByDecoration)) + builder.Append("--simplify-by-decoration "); - var filters = _settings.BuildHistoriesFilter(); - if (string.IsNullOrEmpty(filters)) - builder.Append("--branches --remotes --tags HEAD"); - else - builder.Append(filters); + var filters = _historyFilterCollection.Build(); + if (string.IsNullOrEmpty(filters)) + builder.Append("--branches --remotes --tags HEAD"); + else + builder.Append(filters); - var commits = new Commands.QueryCommits(_fullpath, builder.ToString()).GetResultAsync().Result; - var graph = Models.CommitGraph.Parse(commits, _settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)); + var commits = await new Commands.QueryCommits(FullPath, builder.ToString()).GetResultAsync().ConfigureAwait(false); + var graph = Models.CommitGraph.Parse(commits, _settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)); - Dispatcher.UIThread.Invoke(() => - { - if (_histories != null) + Dispatcher.UIThread.Invoke(() => { - _histories.IsLoading = false; - _histories.Commits = commits; - _histories.Graph = graph; + if (token.IsCancellationRequested) + return; + + if (_histories != null) + { + _histories.IsLoading = false; + _histories.Commits = commits; + _histories.Graph = graph; - BisectState = _histories.UpdateBisectInfo(); + BisectState = _histories.UpdateBisectInfo(); - if (!string.IsNullOrEmpty(_navigateToCommitDelayed)) - NavigateToCommit(_navigateToCommitDelayed); - } + if (!string.IsNullOrEmpty(_navigateToCommitDelayed)) + NavigateToCommit(_navigateToCommitDelayed); + } - _navigateToCommitDelayed = string.Empty; - }); + _navigateToCommitDelayed = string.Empty; + }); + }, token); } public void RefreshSubmodules() @@ -1304,41 +1255,43 @@ public void RefreshSubmodules() return; } - var submodules = new Commands.QuerySubmodules(_fullpath).GetResultAsync().Result; - _watcher?.SetSubmodules(submodules); - - Dispatcher.UIThread.Invoke(() => + Task.Run(async () => { - bool hasChanged = _submodules.Count != submodules.Count; - if (!hasChanged) - { - var old = new Dictionary(); - foreach (var module in _submodules) - old.Add(module.Path, module); + var submodules = await new Commands.QuerySubmodules(FullPath).GetResultAsync().ConfigureAwait(false); - foreach (var module in submodules) + Dispatcher.UIThread.Invoke(() => + { + bool hasChanged = _submodules.Count != submodules.Count; + if (!hasChanged) { - if (!old.TryGetValue(module.Path, out var exist)) + var old = new Dictionary(); + foreach (var module in _submodules) + old.Add(module.Path, module); + + foreach (var module in submodules) { - hasChanged = true; - break; + if (!old.TryGetValue(module.Path, out var exist)) + { + hasChanged = true; + break; + } + + hasChanged = !exist.SHA.Equals(module.SHA, StringComparison.Ordinal) || + !exist.Branch.Equals(module.Branch, StringComparison.Ordinal) || + !exist.URL.Equals(module.URL, StringComparison.Ordinal) || + exist.Status != module.Status; + + if (hasChanged) + break; } - - hasChanged = !exist.SHA.Equals(module.SHA, StringComparison.Ordinal) || - !exist.Branch.Equals(module.Branch, StringComparison.Ordinal) || - !exist.URL.Equals(module.URL, StringComparison.Ordinal) || - exist.Status != module.Status; - - if (hasChanged) - break; } - } - if (hasChanged) - { - Submodules = submodules; - VisibleSubmodules = BuildVisibleSubmodules(); - } + if (hasChanged) + { + Submodules = submodules; + VisibleSubmodules = BuildVisibleSubmodules(); + } + }); }); } @@ -1347,19 +1300,34 @@ public void RefreshWorkingCopyChanges() if (IsBare) return; - var changes = new Commands.QueryLocalChanges(_fullpath, _settings.IncludeUntrackedInLocalChanges).GetResultAsync().Result; - if (_workingCopy == null) - return; + if (_cancellationRefreshWorkingCopyChanges is { IsCancellationRequested: false }) + _cancellationRefreshWorkingCopyChanges.Cancel(); - changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); - _workingCopy.SetData(changes); + _cancellationRefreshWorkingCopyChanges = new CancellationTokenSource(); + var token = _cancellationRefreshWorkingCopyChanges.Token; - Dispatcher.UIThread.Invoke(() => + Task.Run(async () => { - LocalChangesCount = changes.Count; - OnPropertyChanged(nameof(InProgressContext)); - GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, changes.Count == 0); - }); + var changes = await new Commands.QueryLocalChanges(FullPath, _settings.IncludeUntrackedInLocalChanges) + .GetResultAsync() + .ConfigureAwait(false); + + if (_workingCopy == null || token.IsCancellationRequested) + return; + + changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); + _workingCopy.SetData(changes, token); + + Dispatcher.UIThread.Invoke(() => + { + if (token.IsCancellationRequested) + return; + + LocalChangesCount = changes.Count; + OnPropertyChanged(nameof(InProgressContext)); + GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, changes.Count == 0); + }); + }, token); } public void RefreshStashes() @@ -1367,21 +1335,41 @@ public void RefreshStashes() if (IsBare) return; - var stashes = new Commands.QueryStashes(_fullpath).GetResultAsync().Result; - Dispatcher.UIThread.Invoke(() => + if (_cancellationRefreshStashes is { IsCancellationRequested: false }) + _cancellationRefreshStashes.Cancel(); + + _cancellationRefreshStashes = new CancellationTokenSource(); + var token = _cancellationRefreshStashes.Token; + + Task.Run(async () => { - if (_stashesPage != null) - _stashesPage.Stashes = stashes; + var stashes = await new Commands.QueryStashes(FullPath).GetResultAsync().ConfigureAwait(false); + Dispatcher.UIThread.Invoke(() => + { + if (token.IsCancellationRequested) + return; - StashesCount = stashes.Count; - }); + if (_stashesPage != null) + _stashesPage.Stashes = stashes; + + StashesCount = stashes.Count; + }); + }, token); + } + + public void ToggleHistoryShowFlag(Models.HistoryShowFlags flag) + { + if (_settings.HistoryShowFlags.HasFlag(flag)) + HistoryShowFlags -= flag; + else + HistoryShowFlags |= flag; } public void CreateNewBranch() { if (_currentBranch == null) { - App.RaiseException(_fullpath, "Git cannot create a branch before your first commit."); + App.RaiseException(FullPath, "Git cannot create a branch before your first commit."); return; } @@ -1389,7 +1377,7 @@ public void CreateNewBranch() ShowPopup(new CreateBranch(this, _currentBranch)); } - public void CheckoutBranch(Models.Branch branch) + public async Task CheckoutBranchAsync(Models.Branch branch) { if (branch.IsLocal) { @@ -1412,7 +1400,7 @@ public void CheckoutBranch(Models.Branch branch) if (_localChangesCount > 0 || _submodules.Count > 0) ShowPopup(new Checkout(this, branch.Name)); else - ShowAndStartPopup(new Checkout(this, branch.Name)); + await ShowAndStartPopupAsync(new Checkout(this, branch.Name)); } else { @@ -1420,12 +1408,12 @@ public void CheckoutBranch(Models.Branch branch) { if (b.IsLocal && b.Upstream.Equals(branch.FullName, StringComparison.Ordinal) && - b.TrackStatus.Ahead.Count == 0) + b.Ahead.Count == 0) { - if (b.TrackStatus.Behind.Count > 0) + if (b.Behind.Count > 0) ShowPopup(new CheckoutAndFastForward(this, b, branch)); else if (!b.IsCurrent) - CheckoutBranch(b); + await CheckoutBranchAsync(b); return; } @@ -1435,6 +1423,25 @@ public void CheckoutBranch(Models.Branch branch) } } + public async Task CheckoutTagAsync(Models.Tag tag) + { + var c = await new Commands.QuerySingleCommit(FullPath, tag.SHA).GetResultAsync(); + if (c != null && _histories != null) + await _histories.CheckoutBranchByCommitAsync(c); + } + + public async Task CompareBranchWithWorktreeAsync(Models.Branch branch) + { + if (_histories != null) + { + _searchCommitContext.Selected = null; + + var target = await new Commands.QuerySingleCommit(FullPath, branch.Head).GetResultAsync(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, null); + } + } + public void DeleteBranch(Models.Branch branch) { if (CanCreatePopup()) @@ -1457,7 +1464,7 @@ public void CreateNewTag() { if (_currentBranch == null) { - App.RaiseException(_fullpath, "Git cannot create a branch before your first commit."); + App.RaiseException(FullPath, "Git cannot create a branch before your first commit."); return; } @@ -1501,7 +1508,7 @@ public void OpenSubmodule(string submodule) if (selfPage == null) return; - var root = Path.GetFullPath(Path.Combine(_fullpath, submodule)); + var root = Path.GetFullPath(Path.Combine(FullPath, submodule)); var normalizedPath = root.Replace('\\', '/').TrimEnd('/'); var node = Preferences.Instance.FindNode(normalizedPath) ?? @@ -1522,10 +1529,10 @@ public void AddWorktree() ShowPopup(new AddWorktree(this)); } - public void PruneWorktrees() + public async Task PruneWorktreesAsync() { if (CanCreatePopup()) - ShowAndStartPopup(new PruneWorktrees(this)); + await ShowAndStartPopupAsync(new PruneWorktrees(this)); } public void OpenWorktree(Models.Worktree worktree) @@ -1539,7 +1546,27 @@ public void OpenWorktree(Models.Worktree worktree) IsRepository = true, }; - App.GetLauncher()?.OpenRepositoryInTab(node, null); + App.GetLauncher().OpenRepositoryInTab(node, null); + } + + public async Task LockWorktreeAsync(Models.Worktree worktree) + { + using var lockWatcher = _watcher?.Lock(); + var log = CreateLog("Lock Worktree"); + var succ = await new Commands.Worktree(FullPath).Use(log).LockAsync(worktree.FullPath); + if (succ) + worktree.IsLocked = true; + log.Complete(); + } + + public async Task UnlockWorktreeAsync(Models.Worktree worktree) + { + using var lockWatcher = _watcher?.Lock(); + var log = CreateLog("Unlock Worktree"); + var succ = await new Commands.Worktree(FullPath).Use(log).UnlockAsync(worktree.FullPath); + if (succ) + worktree.IsLocked = false; + log.Complete(); } public List GetPreferredOpenAIServices() @@ -1564,352 +1591,6 @@ public void OpenWorktree(Models.Worktree worktree) return all; } - public ContextMenu CreateContextMenuForGitFlow() - { - var menu = new ContextMenu(); - menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - - if (IsGitFlowEnabled()) - { - var startFeature = new MenuItem(); - startFeature.Header = App.Text("GitFlow.StartFeature"); - startFeature.Icon = App.CreateMenuIcon("Icons.GitFlow.Feature"); - startFeature.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new GitFlowStart(this, Models.GitFlowBranchType.Feature)); - e.Handled = true; - }; - - var startRelease = new MenuItem(); - startRelease.Header = App.Text("GitFlow.StartRelease"); - startRelease.Icon = App.CreateMenuIcon("Icons.GitFlow.Release"); - startRelease.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new GitFlowStart(this, Models.GitFlowBranchType.Release)); - e.Handled = true; - }; - - var startHotfix = new MenuItem(); - startHotfix.Header = App.Text("GitFlow.StartHotfix"); - startHotfix.Icon = App.CreateMenuIcon("Icons.GitFlow.Hotfix"); - startHotfix.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new GitFlowStart(this, Models.GitFlowBranchType.Hotfix)); - e.Handled = true; - }; - - menu.Items.Add(startFeature); - menu.Items.Add(startRelease); - menu.Items.Add(startHotfix); - } - else - { - var init = new MenuItem(); - init.Header = App.Text("GitFlow.Init"); - init.Icon = App.CreateMenuIcon("Icons.Init"); - init.Click += (_, e) => - { - if (_currentBranch == null) - App.RaiseException(_fullpath, "Git flow init failed: No branch found!!!"); - else if (CanCreatePopup()) - ShowPopup(new InitGitFlow(this)); - - e.Handled = true; - }; - menu.Items.Add(init); - } - return menu; - } - - public ContextMenu CreateContextMenuForGitLFS() - { - var menu = new ContextMenu(); - menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - - if (IsLFSEnabled()) - { - var addPattern = new MenuItem(); - addPattern.Header = App.Text("GitLFS.AddTrackPattern"); - addPattern.Icon = App.CreateMenuIcon("Icons.File.Add"); - addPattern.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new LFSTrackCustomPattern(this)); - - e.Handled = true; - }; - menu.Items.Add(addPattern); - menu.Items.Add(new MenuItem() { Header = "-" }); - - var fetch = new MenuItem(); - fetch.Header = App.Text("GitLFS.Fetch"); - fetch.Icon = App.CreateMenuIcon("Icons.Fetch"); - fetch.IsEnabled = _remotes.Count > 0; - fetch.Click += (_, e) => - { - if (CanCreatePopup()) - { - if (_remotes.Count == 1) - ShowAndStartPopup(new LFSFetch(this)); - else - ShowPopup(new LFSFetch(this)); - } - - e.Handled = true; - }; - menu.Items.Add(fetch); - - var pull = new MenuItem(); - pull.Header = App.Text("GitLFS.Pull"); - pull.Icon = App.CreateMenuIcon("Icons.Pull"); - pull.IsEnabled = _remotes.Count > 0; - pull.Click += (_, e) => - { - if (CanCreatePopup()) - { - if (_remotes.Count == 1) - ShowAndStartPopup(new LFSPull(this)); - else - ShowPopup(new LFSPull(this)); - } - - e.Handled = true; - }; - menu.Items.Add(pull); - - var push = new MenuItem(); - push.Header = App.Text("GitLFS.Push"); - push.Icon = App.CreateMenuIcon("Icons.Push"); - push.IsEnabled = _remotes.Count > 0; - push.Click += (_, e) => - { - if (CanCreatePopup()) - { - if (_remotes.Count == 1) - ShowAndStartPopup(new LFSPush(this)); - else - ShowPopup(new LFSPush(this)); - } - - e.Handled = true; - }; - menu.Items.Add(push); - - var prune = new MenuItem(); - prune.Header = App.Text("GitLFS.Prune"); - prune.Icon = App.CreateMenuIcon("Icons.Clean"); - prune.Click += (_, e) => - { - if (CanCreatePopup()) - ShowAndStartPopup(new LFSPrune(this)); - - e.Handled = true; - }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(prune); - - var locks = new MenuItem(); - locks.Header = App.Text("GitLFS.Locks"); - locks.Icon = App.CreateMenuIcon("Icons.Lock"); - locks.IsEnabled = _remotes.Count > 0; - if (_remotes.Count == 1) - { - locks.Click += async (_, e) => - { - await App.ShowDialog(new LFSLocks(this, _remotes[0].Name)); - e.Handled = true; - }; - } - else - { - foreach (var remote in _remotes) - { - var remoteName = remote.Name; - var lockRemote = new MenuItem(); - lockRemote.Header = remoteName; - lockRemote.Click += async (_, e) => - { - await App.ShowDialog(new LFSLocks(this, remoteName)); - e.Handled = true; - }; - locks.Items.Add(lockRemote); - } - } - - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(locks); - } - else - { - var install = new MenuItem(); - install.Header = App.Text("GitLFS.Install"); - install.Icon = App.CreateMenuIcon("Icons.Init"); - install.Click += async (_, e) => - { - var log = CreateLog("Install LFS"); - var succ = await new Commands.LFS(_fullpath).Use(log).InstallAsync(); - if (succ) - App.SendNotification(_fullpath, "LFS enabled successfully!"); - - log.Complete(); - e.Handled = true; - }; - menu.Items.Add(install); - } - - return menu; - } - - public ContextMenu CreateContextMenuForCustomAction() - { - var menu = new ContextMenu(); - menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - - var actions = GetCustomActions(Models.CustomActionScope.Repository); - if (actions.Count > 0) - { - foreach (var action in actions) - { - var (dup, label) = action; - var item = new MenuItem(); - item.Icon = App.CreateMenuIcon("Icons.Action"); - item.Header = label; - item.Click += (_, e) => - { - ExecCustomAction(dup, null); - e.Handled = true; - }; - - menu.Items.Add(item); - } - } - else - { - menu.Items.Add(new MenuItem() { Header = App.Text("Repository.CustomActions.Empty") }); - } - - return menu; - } - - public ContextMenu CreateContextMenuForHistoryAdvancedOption() - { - var layout = new MenuItem(); - layout.Header = App.Text("Repository.HistoriesLayout"); - layout.IsEnabled = false; - - var isHorizontal = Preferences.Instance.UseTwoColumnsLayoutInHistories; - var horizontal = new MenuItem(); - horizontal.Header = App.Text("Repository.HistoriesLayout.Horizontal"); - if (isHorizontal) - horizontal.Icon = App.CreateMenuIcon("Icons.Check"); - horizontal.Click += (_, ev) => - { - Preferences.Instance.UseTwoColumnsLayoutInHistories = true; - ev.Handled = true; - }; - - var vertical = new MenuItem(); - vertical.Header = App.Text("Repository.HistoriesLayout.Vertical"); - if (!isHorizontal) - vertical.Icon = App.CreateMenuIcon("Icons.Check"); - vertical.Click += (_, ev) => - { - Preferences.Instance.UseTwoColumnsLayoutInHistories = false; - ev.Handled = true; - }; - - var showFlags = new MenuItem(); - showFlags.Header = App.Text("Repository.ShowFlags"); - showFlags.IsEnabled = false; - - var reflog = new MenuItem(); - reflog.Header = App.Text("Repository.ShowLostCommits"); - reflog.Tag = "--reflog"; - if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.Reflog)) - reflog.Icon = App.CreateMenuIcon("Icons.Check"); - reflog.Click += (_, e) => - { - ToggleHistoryShowFlag(Models.HistoryShowFlags.Reflog); - e.Handled = true; - }; - - var firstParentOnly = new MenuItem(); - firstParentOnly.Header = App.Text("Repository.ShowFirstParentOnly"); - firstParentOnly.Tag = "--first-parent"; - if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)) - firstParentOnly.Icon = App.CreateMenuIcon("Icons.Check"); - firstParentOnly.Click += (_, e) => - { - ToggleHistoryShowFlag(Models.HistoryShowFlags.FirstParentOnly); - e.Handled = true; - }; - - var simplifyByDecoration = new MenuItem(); - simplifyByDecoration.Header = App.Text("Repository.ShowDecoratedCommitsOnly"); - simplifyByDecoration.Tag = "--simplify-by-decoration"; - if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.SimplifyByDecoration)) - simplifyByDecoration.Icon = App.CreateMenuIcon("Icons.Check"); - simplifyByDecoration.Click += (_, e) => - { - ToggleHistoryShowFlag(Models.HistoryShowFlags.SimplifyByDecoration); - e.Handled = true; - }; - - var order = new MenuItem(); - order.Header = App.Text("Repository.HistoriesOrder"); - order.IsEnabled = false; - - var dateOrder = new MenuItem(); - dateOrder.Header = App.Text("Repository.HistoriesOrder.ByDate"); - dateOrder.Tag = "--date-order"; - if (!_settings.EnableTopoOrderInHistories) - dateOrder.Icon = App.CreateMenuIcon("Icons.Check"); - dateOrder.Click += (_, ev) => - { - if (_settings.EnableTopoOrderInHistories) - { - _settings.EnableTopoOrderInHistories = false; - Task.Run(RefreshCommits); - } - - ev.Handled = true; - }; - - var topoOrder = new MenuItem(); - topoOrder.Header = App.Text("Repository.HistoriesOrder.Topo"); - topoOrder.Tag = "--topo-order"; - if (_settings.EnableTopoOrderInHistories) - topoOrder.Icon = App.CreateMenuIcon("Icons.Check"); - topoOrder.Click += (_, ev) => - { - if (!_settings.EnableTopoOrderInHistories) - { - _settings.EnableTopoOrderInHistories = true; - Task.Run(RefreshCommits); - } - - ev.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(layout); - menu.Items.Add(horizontal); - menu.Items.Add(vertical); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(showFlags); - menu.Items.Add(reflog); - menu.Items.Add(firstParentOnly); - menu.Items.Add(simplifyByDecoration); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(order); - menu.Items.Add(dateOrder); - menu.Items.Add(topoOrder); - return menu; - } - public void DiscardAllChanges() { if (CanCreatePopup()) @@ -1922,927 +1603,37 @@ public void ClearStashes() ShowPopup(new ClearStashes(this)); } - public ContextMenu CreateContextMenuForLocalBranch(Models.Branch branch) - { - var menu = new ContextMenu(); - - var push = new MenuItem(); - push.Header = App.Text("BranchCM.Push", branch.Name); - push.Icon = App.CreateMenuIcon("Icons.Push"); - push.IsEnabled = _remotes.Count > 0; - push.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Push(this, branch)); - e.Handled = true; - }; - - if (branch.IsCurrent) - { - if (!IsBare) - { - if (!string.IsNullOrEmpty(branch.Upstream)) - { - var upstream = branch.Upstream.Substring(13); - var fastForward = new MenuItem(); - fastForward.Header = App.Text("BranchCM.FastForward", upstream); - fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); - fastForward.IsEnabled = branch.TrackStatus.Ahead.Count == 0; - fastForward.Click += (_, e) => - { - var b = _branches.Find(x => x.FriendlyName == upstream); - if (b == null) - return; - - if (CanCreatePopup()) - ShowAndStartPopup(new Merge(this, b, branch.Name, true)); - - e.Handled = true; - }; - - var pull = new MenuItem(); - pull.Header = App.Text("BranchCM.Pull", upstream); - pull.Icon = App.CreateMenuIcon("Icons.Pull"); - pull.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Pull(this, null)); - e.Handled = true; - }; - - menu.Items.Add(fastForward); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(pull); - } - } - - menu.Items.Add(push); - } - else - { - if (!IsBare) - { - var checkout = new MenuItem(); - checkout.Header = App.Text("BranchCM.Checkout", branch.Name); - checkout.Icon = App.CreateMenuIcon("Icons.Check"); - checkout.Click += (_, e) => - { - CheckoutBranch(branch); - e.Handled = true; - }; - menu.Items.Add(checkout); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var worktree = _worktrees.Find(x => x.Branch == branch.FullName); - var upstream = _branches.Find(x => x.FullName == branch.Upstream); - if (upstream != null && worktree == null) - { - var fastForward = new MenuItem(); - fastForward.Header = App.Text("BranchCM.FastForward", upstream.FriendlyName); - fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); - fastForward.IsEnabled = branch.TrackStatus.Ahead.Count == 0; - fastForward.Click += (_, e) => - { - if (CanCreatePopup()) - ShowAndStartPopup(new ResetWithoutCheckout(this, branch, upstream)); - e.Handled = true; - }; - menu.Items.Add(fastForward); - - var fetchInto = new MenuItem(); - fetchInto.Header = App.Text("BranchCM.FetchInto", upstream.FriendlyName, branch.Name); - fetchInto.Icon = App.CreateMenuIcon("Icons.Fetch"); - fetchInto.IsEnabled = branch.TrackStatus.Ahead.Count == 0; - fetchInto.Click += (_, e) => - { - if (CanCreatePopup()) - ShowAndStartPopup(new FetchInto(this, branch, upstream)); - e.Handled = true; - }; - - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(fetchInto); - } - - menu.Items.Add(push); - - if (!IsBare) - { - var merge = new MenuItem(); - merge.Header = App.Text("BranchCM.Merge", branch.Name, _currentBranch.Name); - merge.Icon = App.CreateMenuIcon("Icons.Merge"); - merge.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Merge(this, branch, _currentBranch.Name, false)); - e.Handled = true; - }; - - var rebase = new MenuItem(); - rebase.Header = App.Text("BranchCM.Rebase", _currentBranch.Name, branch.Name); - rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); - rebase.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Rebase(this, _currentBranch, branch)); - e.Handled = true; - }; - - menu.Items.Add(merge); - menu.Items.Add(rebase); - } - - if (worktree == null) - { - var selectedCommit = (_histories?.DetailContext as CommitDetail)?.Commit; - if (selectedCommit != null && !selectedCommit.SHA.Equals(branch.Head, StringComparison.Ordinal)) - { - var move = new MenuItem(); - move.Header = App.Text("BranchCM.ResetToSelectedCommit", branch.Name, selectedCommit.SHA.Substring(0, 10)); - move.Icon = App.CreateMenuIcon("Icons.Reset"); - move.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new ResetWithoutCheckout(this, branch, selectedCommit)); - e.Handled = true; - }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(move); - } - } - - var compareWithCurrent = new MenuItem(); - compareWithCurrent.Header = App.Text("BranchCM.CompareWithCurrent", _currentBranch.Name); - compareWithCurrent.Icon = App.CreateMenuIcon("Icons.Compare"); - compareWithCurrent.Click += (_, _) => - { - App.ShowWindow(new BranchCompare(_fullpath, branch, _currentBranch)); - }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(compareWithCurrent); - - if (_localChangesCount > 0) - { - var compareWithWorktree = new MenuItem(); - compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); - compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); - compareWithWorktree.Click += async (_, _) => - { - SelectedSearchedCommit = null; - - if (_histories != null) - { - var target = await new Commands.QuerySingleCommit(_fullpath, branch.Head).GetResultAsync(); - _histories.AutoSelectedCommit = null; - _histories.DetailContext = new RevisionCompare(_fullpath, target, null); - } - }; - menu.Items.Add(compareWithWorktree); - } - } - - if (!IsBare) - { - var type = GetGitFlowType(branch); - if (type != Models.GitFlowBranchType.None) - { - var finish = new MenuItem(); - finish.Header = App.Text("BranchCM.Finish", branch.Name); - finish.Icon = App.CreateMenuIcon("Icons.GitFlow"); - finish.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new GitFlowFinish(this, branch, type)); - e.Handled = true; - }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(finish); - } - } - - var rename = new MenuItem(); - rename.Header = App.Text("BranchCM.Rename", branch.Name); - rename.Icon = App.CreateMenuIcon("Icons.Rename"); - rename.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new RenameBranch(this, branch)); - e.Handled = true; - }; - - var delete = new MenuItem(); - delete.Header = App.Text("BranchCM.Delete", branch.Name); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); - delete.IsEnabled = !branch.IsCurrent; - delete.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new DeleteBranch(this, branch)); - e.Handled = true; - }; - - var createBranch = new MenuItem(); - createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); - createBranch.Header = App.Text("CreateBranch"); - createBranch.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new CreateBranch(this, branch)); - e.Handled = true; - }; - - var createTag = new MenuItem(); - createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); - createTag.Header = App.Text("CreateTag"); - createTag.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new CreateTag(this, branch)); - e.Handled = true; - }; - - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(rename); - menu.Items.Add(delete); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(createBranch); - menu.Items.Add(createTag); - menu.Items.Add(new MenuItem() { Header = "-" }); - TryToAddCustomActionsToBranchContextMenu(menu, branch); - - if (!IsBare) - { - var remoteBranches = new List(); - foreach (var b in _branches) - { - if (!b.IsLocal) - remoteBranches.Add(b); - } - - if (remoteBranches.Count > 0) - { - var tracking = new MenuItem(); - tracking.Header = App.Text("BranchCM.Tracking"); - tracking.Icon = App.CreateMenuIcon("Icons.Track"); - tracking.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new SetUpstream(this, branch, remoteBranches)); - e.Handled = true; - }; - menu.Items.Add(tracking); - } - } - - var archive = new MenuItem(); - archive.Icon = App.CreateMenuIcon("Icons.Archive"); - archive.Header = App.Text("Archive"); - archive.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Archive(this, branch)); - e.Handled = true; - }; - menu.Items.Add(archive); - menu.Items.Add(new MenuItem() { Header = "-" }); - - var copy = new MenuItem(); - copy.Header = App.Text("BranchCM.CopyName"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, e) => - { - await App.CopyTextAsync(branch.Name); - e.Handled = true; - }; - menu.Items.Add(copy); - - return menu; - } - - public ContextMenu CreateContextMenuForRemote(Models.Remote remote) + public async Task SaveCommitAsPatchAsync(Models.Commit commit, string folder, int index = 0) { - var menu = new ContextMenu(); - - if (remote.TryGetVisitURL(out string visitURL)) - { - var visit = new MenuItem(); - visit.Header = App.Text("RemoteCM.OpenInBrowser"); - visit.Icon = App.CreateMenuIcon("Icons.OpenWith"); - visit.Click += (_, e) => - { - Native.OS.OpenBrowser(visitURL); - e.Handled = true; - }; - - menu.Items.Add(visit); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var fetch = new MenuItem(); - fetch.Header = App.Text("RemoteCM.Fetch"); - fetch.Icon = App.CreateMenuIcon("Icons.Fetch"); - fetch.Click += (_, e) => - { - if (CanCreatePopup()) - ShowAndStartPopup(new Fetch(this, remote)); - e.Handled = true; - }; - - var prune = new MenuItem(); - prune.Header = App.Text("RemoteCM.Prune"); - prune.Icon = App.CreateMenuIcon("Icons.Clean"); - prune.Click += (_, e) => - { - if (CanCreatePopup()) - ShowAndStartPopup(new PruneRemote(this, remote)); - e.Handled = true; - }; - - var edit = new MenuItem(); - edit.Header = App.Text("RemoteCM.Edit"); - edit.Icon = App.CreateMenuIcon("Icons.Edit"); - edit.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new EditRemote(this, remote)); - e.Handled = true; - }; - - var delete = new MenuItem(); - delete.Header = App.Text("RemoteCM.Delete"); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); - delete.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new DeleteRemote(this, remote)); - e.Handled = true; - }; - - var copy = new MenuItem(); - copy.Header = App.Text("RemoteCM.CopyURL"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, e) => - { - await App.CopyTextAsync(remote.URL); - e.Handled = true; - }; - - menu.Items.Add(fetch); - menu.Items.Add(prune); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(edit); - menu.Items.Add(delete); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copy); - return menu; - } - - public ContextMenu CreateContextMenuForRemoteBranch(Models.Branch branch) - { - var menu = new ContextMenu(); - var name = branch.FriendlyName; - - var checkout = new MenuItem(); - checkout.Header = App.Text("BranchCM.Checkout", name); - checkout.Icon = App.CreateMenuIcon("Icons.Check"); - checkout.Click += (_, e) => - { - CheckoutBranch(branch); - e.Handled = true; - }; - menu.Items.Add(checkout); - menu.Items.Add(new MenuItem() { Header = "-" }); - - if (_currentBranch != null) - { - var pull = new MenuItem(); - pull.Header = App.Text("BranchCM.PullInto", name, _currentBranch.Name); - pull.Icon = App.CreateMenuIcon("Icons.Pull"); - pull.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Pull(this, branch)); - e.Handled = true; - }; - - var merge = new MenuItem(); - merge.Header = App.Text("BranchCM.Merge", name, _currentBranch.Name); - merge.Icon = App.CreateMenuIcon("Icons.Merge"); - merge.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Merge(this, branch, _currentBranch.Name, false)); - e.Handled = true; - }; - - var rebase = new MenuItem(); - rebase.Header = App.Text("BranchCM.Rebase", _currentBranch.Name, name); - rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); - rebase.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Rebase(this, _currentBranch, branch)); - e.Handled = true; - }; - - menu.Items.Add(pull); - menu.Items.Add(merge); - menu.Items.Add(rebase); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var compareWithHead = new MenuItem(); - compareWithHead.Header = App.Text("BranchCM.CompareWithCurrent", _currentBranch.Name); - compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); - compareWithHead.Click += (_, _) => - { - App.ShowWindow(new BranchCompare(_fullpath, branch, _currentBranch)); - }; - menu.Items.Add(compareWithHead); - - if (_localChangesCount > 0) - { - var compareWithWorktree = new MenuItem(); - compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); - compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); - compareWithWorktree.Click += async (_, _) => - { - SelectedSearchedCommit = null; - - if (_histories != null) - { - var target = await new Commands.QuerySingleCommit(_fullpath, branch.Head).GetResultAsync(); - _histories.AutoSelectedCommit = null; - _histories.DetailContext = new RevisionCompare(_fullpath, target, null); - } - }; - menu.Items.Add(compareWithWorktree); - } - menu.Items.Add(new MenuItem() { Header = "-" }); - - var delete = new MenuItem(); - delete.Header = App.Text("BranchCM.Delete", name); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); - delete.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new DeleteBranch(this, branch)); - e.Handled = true; - }; - - var createBranch = new MenuItem(); - createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); - createBranch.Header = App.Text("CreateBranch"); - createBranch.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new CreateBranch(this, branch)); - e.Handled = true; - }; - - var createTag = new MenuItem(); - createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); - createTag.Header = App.Text("CreateTag"); - createTag.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new CreateTag(this, branch)); - e.Handled = true; - }; - - var archive = new MenuItem(); - archive.Icon = App.CreateMenuIcon("Icons.Archive"); - archive.Header = App.Text("Archive"); - archive.Click += (_, e) => - { - if (CanCreatePopup()) - ShowPopup(new Archive(this, branch)); - e.Handled = true; - }; - - var copy = new MenuItem(); - copy.Header = App.Text("BranchCM.CopyName"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, e) => - { - await App.CopyTextAsync(name); - e.Handled = true; - }; - - menu.Items.Add(delete); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(createBranch); - menu.Items.Add(createTag); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(archive); - menu.Items.Add(new MenuItem() { Header = "-" }); - TryToAddCustomActionsToBranchContextMenu(menu, branch); - menu.Items.Add(copy); - - return menu; - } - - public ContextMenu CreateContextMenuForTag(Models.Tag tag) - { - var createBranch = new MenuItem(); - createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); - createBranch.Header = App.Text("CreateBranch"); - createBranch.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new CreateBranch(this, tag)); - ev.Handled = true; - }; - - var pushTag = new MenuItem(); - pushTag.Header = App.Text("TagCM.Push", tag.Name); - pushTag.Icon = App.CreateMenuIcon("Icons.Push"); - pushTag.IsEnabled = _remotes.Count > 0; - pushTag.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new PushTag(this, tag)); - ev.Handled = true; - }; - - var deleteTag = new MenuItem(); - deleteTag.Header = App.Text("TagCM.Delete", tag.Name); - deleteTag.Icon = App.CreateMenuIcon("Icons.Clear"); - deleteTag.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new DeleteTag(this, tag)); - ev.Handled = true; - }; - - var archive = new MenuItem(); - archive.Icon = App.CreateMenuIcon("Icons.Archive"); - archive.Header = App.Text("Archive"); - archive.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new Archive(this, tag)); - ev.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(createBranch); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(pushTag); - menu.Items.Add(deleteTag); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(archive); - menu.Items.Add(new MenuItem() { Header = "-" }); - - var actions = GetCustomActions(Models.CustomActionScope.Tag); - if (actions.Count > 0) - { - var custom = new MenuItem(); - custom.Header = App.Text("TagCM.CustomAction"); - custom.Icon = App.CreateMenuIcon("Icons.Action"); - - foreach (var action in actions) - { - var (dup, label) = action; - var item = new MenuItem(); - item.Icon = App.CreateMenuIcon("Icons.Action"); - item.Header = label; - item.Click += (_, e) => - { - ExecCustomAction(dup, tag); - e.Handled = true; - }; - - custom.Items.Add(item); - } - - menu.Items.Add(custom); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var copy = new MenuItem(); - copy.Header = App.Text("TagCM.Copy"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, ev) => - { - await App.CopyTextAsync(tag.Name); - ev.Handled = true; - }; - - var copyMessage = new MenuItem(); - copyMessage.Header = App.Text("TagCM.CopyMessage"); - copyMessage.Icon = App.CreateMenuIcon("Icons.Copy"); - copyMessage.IsEnabled = !string.IsNullOrEmpty(tag.Message); - copyMessage.Click += async (_, ev) => - { - await App.CopyTextAsync(tag.Message); - ev.Handled = true; - }; - - menu.Items.Add(copy); - menu.Items.Add(copyMessage); - return menu; - } + var ignoredChars = new HashSet { '/', '\\', ':', ',', '*', '?', '\"', '<', '>', '|', '`', '$', '^', '%', '[', ']', '+', '-' }; + var builder = new StringBuilder(); + builder.Append(index.ToString("D4")); + builder.Append('-'); - public ContextMenu CreateContextMenuForBranchSortMode(bool local) - { - var mode = local ? _settings.LocalBranchSortMode : _settings.RemoteBranchSortMode; - var changeMode = new Action(m => + var chars = commit.Subject.ToCharArray(); + var len = 0; + foreach (var c in chars) { - if (local) + if (!ignoredChars.Contains(c)) { - _settings.LocalBranchSortMode = m; - OnPropertyChanged(nameof(IsSortingLocalBranchByName)); - } - else - { - _settings.RemoteBranchSortMode = m; - OnPropertyChanged(nameof(IsSortingRemoteBranchByName)); - } - - var builder = BuildBranchTree(_branches, _remotes); - LocalBranchTrees = builder.Locals; - RemoteBranchTrees = builder.Remotes; - }); - - var byNameAsc = new MenuItem(); - byNameAsc.Header = App.Text("Repository.BranchSort.ByName"); - if (mode == Models.BranchSortMode.Name) - byNameAsc.Icon = App.CreateMenuIcon("Icons.Check"); - byNameAsc.Click += (_, ev) => - { - if (mode != Models.BranchSortMode.Name) - changeMode(Models.BranchSortMode.Name); - - ev.Handled = true; - }; - - var byCommitterDate = new MenuItem(); - byCommitterDate.Header = App.Text("Repository.BranchSort.ByCommitterDate"); - if (mode == Models.BranchSortMode.CommitterDate) - byCommitterDate.Icon = App.CreateMenuIcon("Icons.Check"); - byCommitterDate.Click += (_, ev) => - { - if (mode != Models.BranchSortMode.CommitterDate) - changeMode(Models.BranchSortMode.CommitterDate); + if (c == ' ' || c == '\t') + builder.Append('-'); + else + builder.Append(c); - ev.Handled = true; - }; + len++; - var menu = new ContextMenu(); - menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - menu.Items.Add(byNameAsc); - menu.Items.Add(byCommitterDate); - return menu; - } - - public ContextMenu CreateContextMenuForTagSortMode() - { - var mode = _settings.TagSortMode; - var changeMode = new Action(m => - { - if (_settings.TagSortMode != m) - { - _settings.TagSortMode = m; - OnPropertyChanged(nameof(IsSortingTagsByName)); - VisibleTags = BuildVisibleTags(); + if (len >= 48) + break; } - }); - - var byCreatorDate = new MenuItem(); - byCreatorDate.Header = App.Text("Repository.Tags.OrderByCreatorDate"); - if (mode == Models.TagSortMode.CreatorDate) - byCreatorDate.Icon = App.CreateMenuIcon("Icons.Check"); - byCreatorDate.Click += (_, ev) => - { - changeMode(Models.TagSortMode.CreatorDate); - ev.Handled = true; - }; - - var byName = new MenuItem(); - byName.Header = App.Text("Repository.Tags.OrderByName"); - if (mode == Models.TagSortMode.Name) - byName.Icon = App.CreateMenuIcon("Icons.Check"); - byName.Click += (_, ev) => - { - changeMode(Models.TagSortMode.Name); - ev.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Placement = PlacementMode.BottomEdgeAlignedLeft; - menu.Items.Add(byCreatorDate); - menu.Items.Add(byName); - return menu; - } - - public ContextMenu CreateContextMenuForSubmodule(Models.Submodule submodule) - { - var open = new MenuItem(); - open.Header = App.Text("Submodule.Open"); - open.Icon = App.CreateMenuIcon("Icons.Folder.Open"); - open.IsEnabled = submodule.Status != Models.SubmoduleStatus.NotInited; - open.Click += (_, ev) => - { - OpenSubmodule(submodule.Path); - ev.Handled = true; - }; - - var update = new MenuItem(); - update.Header = App.Text("Submodule.Update"); - update.Icon = App.CreateMenuIcon("Icons.Loading"); - update.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new UpdateSubmodules(this, submodule)); - ev.Handled = true; - }; - - var move = new MenuItem(); - move.Header = App.Text("Submodule.Move"); - move.Icon = App.CreateMenuIcon("Icons.MoveTo"); - move.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new MoveSubmodule(this, submodule)); - ev.Handled = true; - }; - - var setURL = new MenuItem(); - setURL.Header = App.Text("Submodule.SetURL"); - setURL.Icon = App.CreateMenuIcon("Icons.Edit"); - setURL.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new ChangeSubmoduleUrl(this, submodule)); - ev.Handled = true; - }; - - var setBranch = new MenuItem(); - setBranch.Header = App.Text("Submodule.SetBranch"); - setBranch.Icon = App.CreateMenuIcon("Icons.Track"); - setBranch.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new SetSubmoduleBranch(this, submodule)); - ev.Handled = true; - }; - - var deinit = new MenuItem(); - deinit.Header = App.Text("Submodule.Deinit"); - deinit.Icon = App.CreateMenuIcon("Icons.Undo"); - deinit.IsEnabled = submodule.Status != Models.SubmoduleStatus.NotInited; - deinit.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new DeinitSubmodule(this, submodule.Path)); - ev.Handled = true; - }; - - var rm = new MenuItem(); - rm.Header = App.Text("Submodule.Remove"); - rm.Icon = App.CreateMenuIcon("Icons.Clear"); - rm.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new DeleteSubmodule(this, submodule.Path)); - ev.Handled = true; - }; - - var histories = new MenuItem(); - histories.Header = App.Text("Submodule.Histories"); - histories.Icon = App.CreateMenuIcon("Icons.Histories"); - histories.Click += (_, ev) => - { - App.ShowWindow(new FileHistories(this, submodule.Path)); - ev.Handled = true; - }; - - var copySHA = new MenuItem(); - copySHA.Header = App.Text("CommitDetail.Info.SHA"); - copySHA.Icon = App.CreateMenuIcon("Icons.Fingerprint"); - copySHA.Click += async (_, ev) => - { - await App.CopyTextAsync(submodule.SHA); - ev.Handled = true; - }; - - var copyRelativePath = new MenuItem(); - copyRelativePath.Header = App.Text("Submodule.CopyPath"); - copyRelativePath.Icon = App.CreateMenuIcon("Icons.Folder"); - copyRelativePath.Click += async (_, ev) => - { - await App.CopyTextAsync(submodule.Path); - ev.Handled = true; - }; - - var copyURL = new MenuItem(); - copyURL.Header = App.Text("Submodule.URL"); - copyURL.Icon = App.CreateMenuIcon("Icons.Link"); - copyURL.Click += async (_, ev) => - { - await App.CopyTextAsync(submodule.URL); - ev.Handled = true; - }; - - var copyBranch = new MenuItem(); - copyBranch.Header = App.Text("Submodule.Branch"); - copyBranch.Icon = App.CreateMenuIcon("Icons.Branch"); - copyBranch.Click += async (_, ev) => - { - await App.CopyTextAsync(submodule.Branch); - ev.Handled = true; - }; - - var copy = new MenuItem(); - copy.Header = App.Text("Copy"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Items.Add(copySHA); - copy.Items.Add(copyBranch); - copy.Items.Add(copyRelativePath); - copy.Items.Add(copyURL); - - var menu = new ContextMenu(); - menu.Items.Add(open); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(update); - menu.Items.Add(setURL); - menu.Items.Add(setBranch); - menu.Items.Add(move); - menu.Items.Add(deinit); - menu.Items.Add(rm); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(histories); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copy); - return menu; - } - - public ContextMenu CreateContextMenuForWorktree(Models.Worktree worktree) - { - var menu = new ContextMenu(); - - if (worktree.IsLocked) - { - var unlock = new MenuItem(); - unlock.Header = App.Text("Worktree.Unlock"); - unlock.Icon = App.CreateMenuIcon("Icons.Unlock"); - unlock.Click += async (_, ev) => - { - SetWatcherEnabled(false); - var log = CreateLog("Unlock Worktree"); - var succ = await new Commands.Worktree(_fullpath).Use(log).UnlockAsync(worktree.FullPath); - if (succ) - worktree.IsLocked = false; - log.Complete(); - SetWatcherEnabled(true); - ev.Handled = true; - }; - menu.Items.Add(unlock); } - else - { - var loc = new MenuItem(); - loc.Header = App.Text("Worktree.Lock"); - loc.Icon = App.CreateMenuIcon("Icons.Lock"); - loc.Click += async (_, ev) => - { - SetWatcherEnabled(false); - var log = CreateLog("Lock Worktree"); - var succ = await new Commands.Worktree(_fullpath).Use(log).LockAsync(worktree.FullPath); - if (succ) - worktree.IsLocked = true; - log.Complete(); - SetWatcherEnabled(true); - ev.Handled = true; - }; - menu.Items.Add(loc); - } - - var remove = new MenuItem(); - remove.Header = App.Text("Worktree.Remove"); - remove.Icon = App.CreateMenuIcon("Icons.Clear"); - remove.Click += (_, ev) => - { - if (CanCreatePopup()) - ShowPopup(new RemoveWorktree(this, worktree)); - ev.Handled = true; - }; - menu.Items.Add(remove); + builder.Append(".patch"); - var copy = new MenuItem(); - copy.Header = App.Text("Worktree.CopyPath"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += async (_, e) => - { - await App.CopyTextAsync(worktree.FullPath); - e.Handled = true; - }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copy); - - return menu; + var saveTo = Path.Combine(folder, builder.ToString()); + var log = CreateLog("Save Commit as Patch"); + var succ = await new Commands.FormatPatch(FullPath, commit.SHA, saveTo).Use(log).ExecAsync(); + log.Complete(); + return succ; } private LauncherPage GetOwnerPage() @@ -2853,7 +1644,7 @@ private LauncherPage GetOwnerPage() foreach (var page in launcher.Pages) { - if (page.Node.Id.Equals(_fullpath)) + if (page.Node.Id.Equals(FullPath)) return page; } @@ -2883,9 +1674,9 @@ private BranchTreeNode.Builder BuildBranchTree(List branches, Lis builder.Run(visibles, remotes, true); } - var historiesFilters = _settings.CollectHistoriesFilters(); - UpdateBranchTreeFilterMode(builder.Locals, historiesFilters); - UpdateBranchTreeFilterMode(builder.Remotes, historiesFilters); + var filterMap = _historyFilterCollection.ToMap(); + UpdateBranchTreeFilterMode(builder.Locals, filterMap); + UpdateBranchTreeFilterMode(builder.Remotes, filterMap); return builder; } @@ -2915,13 +1706,23 @@ private object BuildVisibleTags() } } - var historiesFilters = _settings.CollectHistoriesFilters(); - UpdateTagFilterMode(historiesFilters); + var filterMap = _historyFilterCollection.ToMap(); + UpdateTagFilterMode(filterMap); if (Preferences.Instance.ShowTagsAsTree) - return TagCollectionAsTree.Build(visible, _visibleTags as TagCollectionAsTree); + { + var tree = TagCollectionAsTree.Build(visible, _visibleTags as TagCollectionAsTree); + foreach (var node in tree.Tree) + node.UpdateFilterMode(filterMap); + return tree; + } else - return new TagCollectionAsList() { Tags = visible }; + { + var list = new TagCollectionAsList(visible); + foreach (var item in list.TagItems) + item.FilterMode = filterMap.GetValueOrDefault(item.Tag.Name, Models.FilterMode.None); + return list; + } } private object BuildVisibleSubmodules() @@ -2946,40 +1747,41 @@ private object BuildVisibleSubmodules() return new SubmoduleCollectionAsList() { Submodules = visible }; } - private void RefreshHistoriesFilters(bool refresh) + private void RefreshHistoryFilters(bool refresh) { - if (_settings.HistoriesFilters.Count > 0) - HistoriesFilterMode = _settings.HistoriesFilters[0].Mode; - else - HistoriesFilterMode = Models.FilterMode.None; - + HistoryFilterMode = _historyFilterCollection.Mode; if (!refresh) return; - var filters = _settings.CollectHistoriesFilters(); - UpdateBranchTreeFilterMode(LocalBranchTrees, filters); - UpdateBranchTreeFilterMode(RemoteBranchTrees, filters); - UpdateTagFilterMode(filters); - - Task.Run(RefreshCommits); + var map = _historyFilterCollection.ToMap(); + UpdateBranchTreeFilterMode(LocalBranchTrees, map); + UpdateBranchTreeFilterMode(RemoteBranchTrees, map); + UpdateTagFilterMode(map); + RefreshCommits(); } - private void UpdateBranchTreeFilterMode(List nodes, Dictionary filters) + private void UpdateBranchTreeFilterMode(List nodes, Dictionary map) { foreach (var node in nodes) { - node.FilterMode = filters.GetValueOrDefault(node.Path, Models.FilterMode.None); + node.FilterMode = map.GetValueOrDefault(node.Path, Models.FilterMode.None); if (!node.IsBranch) - UpdateBranchTreeFilterMode(node.Children, filters); + UpdateBranchTreeFilterMode(node.Children, map); } } - private void UpdateTagFilterMode(Dictionary filters) + private void UpdateTagFilterMode(Dictionary map) { - foreach (var tag in _tags) + if (VisibleTags is TagCollectionAsTree tree) { - tag.FilterMode = filters.GetValueOrDefault(tag.Name, Models.FilterMode.None); + foreach (var node in tree.Tree) + node.UpdateFilterMode(map); + } + else if (VisibleTags is TagCollectionAsList list) + { + foreach (var item in list.TagItems) + item.FilterMode = map.GetValueOrDefault(item.Tag.Name, Models.FilterMode.None); } } @@ -2995,12 +1797,24 @@ private void ResetBranchTreeFilterMode(List nodes) private void ResetTagFilterMode() { - foreach (var tag in _tags) - tag.FilterMode = Models.FilterMode.None; + if (VisibleTags is TagCollectionAsTree tree) + { + var filters = new Dictionary(); + foreach (var node in tree.Tree) + node.UpdateFilterMode(filters); + } + else if (VisibleTags is TagCollectionAsList list) + { + foreach (var item in list.TagItems) + item.FilterMode = Models.FilterMode.None; + } } private BranchTreeNode FindBranchNode(List nodes, string path) { + if (string.IsNullOrEmpty(path)) + return null; + foreach (var node in nodes) { if (node.Path.Equals(path, StringComparison.Ordinal)) @@ -3017,110 +1831,27 @@ private BranchTreeNode FindBranchNode(List nodes, string path) return null; } - private void TryToAddCustomActionsToBranchContextMenu(ContextMenu menu, Models.Branch branch) + private void AutoFetchByTimer(object sender) { - var actions = GetCustomActions(Models.CustomActionScope.Branch); - if (actions.Count == 0) - return; - - var custom = new MenuItem(); - custom.Header = App.Text("BranchCM.CustomAction"); - custom.Icon = App.CreateMenuIcon("Icons.Action"); - - foreach (var action in actions) - { - var (dup, label) = action; - var item = new MenuItem(); - item.Icon = App.CreateMenuIcon("Icons.Action"); - item.Header = label; - item.Click += (_, e) => - { - ExecCustomAction(dup, branch); - e.Handled = true; - }; - - custom.Items.Add(item); - } - - menu.Items.Add(custom); - menu.Items.Add(new MenuItem() { Header = "-" }); + Dispatcher.UIThread.Invoke(AutoFetchOnUIThread); } - private bool IsSearchingCommitsByFilePath() + private async Task AutoFetchOnUIThread() { - return _isSearching && _searchCommitFilterType == (int)Models.CommitSearchMethod.ByPath; - } - - private void CalcWorktreeFilesForSearching() - { - if (!IsSearchingCommitsByFilePath()) - { - _requestingWorktreeFiles = false; - _worktreeFiles = null; - MatchedFilesForSearching = null; - GC.Collect(); - return; - } - - if (_requestingWorktreeFiles) - return; - - _requestingWorktreeFiles = true; - - Task.Run(async () => - { - _worktreeFiles = await new Commands.QueryRevisionFileNames(_fullpath, "HEAD") - .GetResultAsync() - .ConfigureAwait(false); + CommandLog log = null; - Dispatcher.UIThread.Post(() => - { - if (IsSearchingCommitsByFilePath() && _requestingWorktreeFiles) - CalcMatchedFilesForSearching(); - - _requestingWorktreeFiles = false; - }); - }); - } - - private void CalcMatchedFilesForSearching() - { - if (_worktreeFiles == null || _worktreeFiles.Count == 0 || _searchCommitFilter.Length < 3) + try { - MatchedFilesForSearching = null; - return; - } + if (_settings is not { EnableAutoFetch: true }) + return; - var matched = new List(); - foreach (var file in _worktreeFiles) - { - if (file.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase) && file.Length != _searchCommitFilter.Length) + if (!CanCreatePopup()) { - matched.Add(file); - if (matched.Count > 100) - break; - } - } - - MatchedFilesForSearching = matched; - } - - private void ToggleHistoryShowFlag(Models.HistoryShowFlags flag) - { - if (_settings.HistoryShowFlags.HasFlag(flag)) - HistoryShowFlags -= flag; - else - HistoryShowFlags |= flag; - } - - private async void AutoFetchImpl(object sender) - { - try - { - if (!_settings.EnableAutoFetch || _isAutoFetching) + _lastFetchTime = DateTime.Now; return; + } - var lockFile = Path.Combine(_gitDir, "index.lock"); + var lockFile = Path.Combine(GitDir, "index.lock"); if (File.Exists(lockFile)) return; @@ -3130,28 +1861,45 @@ private async void AutoFetchImpl(object sender) return; var remotes = new List(); - lock (_lockRemotes) + foreach (var r in _remotes) + remotes.Add(r.Name); + + if (remotes.Count == 0) + return; + + IsAutoFetching = true; + log = CreateLog("Auto-Fetch"); + + if (_settings.FetchAllRemotes) { - foreach (var remote in _remotes) - remotes.Add(remote.Name); + foreach (var remote in remotes) + await new Commands.Fetch(FullPath, remote).Use(log).RunAsync(); + } + else + { + var remote = string.IsNullOrEmpty(_settings.DefaultRemote) ? + remotes.Find(x => x.Equals(_settings.DefaultRemote, StringComparison.Ordinal)) : + remotes[0]; + + await new Commands.Fetch(FullPath, remote).Use(log).RunAsync(); } - Dispatcher.UIThread.Invoke(() => IsAutoFetching = true); - foreach (var remote in remotes) - await new Commands.Fetch(_fullpath, remote, false, false) { RaiseError = false }.RunAsync(); _lastFetchTime = DateTime.Now; - Dispatcher.UIThread.Invoke(() => IsAutoFetching = false); + IsAutoFetching = false; } catch { - // DO nothing, but prevent `System.AggregateException` + // Ignore all exceptions. } + + log?.Complete(); } - private string _fullpath = string.Empty; - private string _gitDir = string.Empty; + private readonly bool _isWorktree = false; + private readonly string _gitCommonDir = null; private Models.RepositorySettings _settings = null; - private Models.FilterMode _historiesFilterMode = Models.FilterMode.None; + private Models.HistoryFilterCollection _historyFilterCollection = null; + private Models.FilterMode _historyFilterMode = Models.FilterMode.None; private bool _hasAllowedSignersFile = false; private Models.Watcher _watcher = null; @@ -3165,29 +1913,21 @@ private async void AutoFetchImpl(object sender) private int _localChangesCount = 0; private int _stashesCount = 0; - private bool _isSearching = false; - private bool _isSearchLoadingVisible = false; - private int _searchCommitFilterType = (int)Models.CommitSearchMethod.ByMessage; - private bool _onlySearchCommitsInCurrentBranch = false; - private string _searchCommitFilter = string.Empty; - private List _searchedCommits = new List(); - private Models.Commit _selectedSearchedCommit = null; - private bool _requestingWorktreeFiles = false; - private List _worktreeFiles = null; - private List _matchedFilesForSearching = null; + private bool _isSearchingCommits = false; + private SearchCommitContext _searchCommitContext = null; private string _filter = string.Empty; - private readonly Lock _lockRemotes = new(); - private List _remotes = new List(); - private List _branches = new List(); + private List _remotes = []; + private List _branches = []; private Models.Branch _currentBranch = null; - private List _localBranchTrees = new List(); - private List _remoteBranchTrees = new List(); - private List _worktrees = new List(); - private List _tags = new List(); + private List _localBranchTrees = []; + private List _remoteBranchTrees = []; + private List _worktrees = []; + private List _tags = []; private object _visibleTags = null; - private List _submodules = new List(); + private List _submodules = []; private object _visibleSubmodules = null; + private string _navigateToCommitDelayed = string.Empty; private bool _isAutoFetching = false; private Timer _autoFetchTimer = null; @@ -3196,6 +1936,10 @@ private async void AutoFetchImpl(object sender) private Models.BisectState _bisectState = Models.BisectState.None; private bool _isBisectCommandRunning = false; - private string _navigateToCommitDelayed = string.Empty; + private CancellationTokenSource _cancellationRefreshBranches = null; + private CancellationTokenSource _cancellationRefreshTags = null; + private CancellationTokenSource _cancellationRefreshWorkingCopyChanges = null; + private CancellationTokenSource _cancellationRefreshCommits = null; + private CancellationTokenSource _cancellationRefreshStashes = null; } } diff --git a/src/ViewModels/RepositoryCommandPalette.cs b/src/ViewModels/RepositoryCommandPalette.cs new file mode 100644 index 000000000..576bc072b --- /dev/null +++ b/src/ViewModels/RepositoryCommandPalette.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.ViewModels +{ + public class RepositoryCommandPaletteCmd + { + public string Key { get; set; } + public Action Action { get; set; } + public string Label => $"{App.Text(Key)}..."; + + public RepositoryCommandPaletteCmd(string key, Action action) + { + Key = key; + Action = action; + } + } + + public class RepositoryCommandPalette : ICommandPalette + { + public List VisibleCmds + { + get => _visibleCmds; + private set => SetProperty(ref _visibleCmds, value); + } + + public RepositoryCommandPaletteCmd SelectedCmd + { + get => _selectedCmd; + set => SetProperty(ref _selectedCmd, value); + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateVisible(); + } + } + + public RepositoryCommandPalette(Launcher launcher, Repository repo) + { + _launcher = launcher; + _repo = repo; + + _cmds.Add(new("Blame", () => + { + var sub = new BlameCommandPalette(_launcher, _repo.FullPath); + _launcher.OpenCommandPalette(sub); + })); + + _cmds.Add(new("BranchCompare", () => + { + var sub = new BranchCompareCommandPalette(_launcher, _repo); + _launcher.OpenCommandPalette(sub); + })); + + _cmds.Add(new("Checkout", () => + { + var sub = new CheckoutCommandPalette(_launcher, _repo); + _launcher.OpenCommandPalette(sub); + })); + + _cmds.Add(new("FileHistory", () => + { + var sub = new FileHistoryCommandPalette(_launcher, _repo.FullPath); + _launcher.OpenCommandPalette(sub); + })); + + _cmds.Add(new("Merge", () => + { + var sub = new MergeCommandPalette(_launcher, _repo); + _launcher.OpenCommandPalette(sub); + })); + + _cmds.Add(new("OpenFile", () => + { + var sub = new OpenFileCommandPalette(_launcher, _repo.FullPath); + _launcher.OpenCommandPalette(sub); + })); + + _visibleCmds = _cmds; + _selectedCmd = _cmds[0]; + } + + public override void Cleanup() + { + _launcher = null; + _repo = null; + _cmds.Clear(); + _visibleCmds.Clear(); + _selectedCmd = null; + _filter = null; + } + + public void ClearFilter() + { + Filter = string.Empty; + } + + public void Exec() + { + if (_selectedCmd != null) + _selectedCmd.Action?.Invoke(); + else + _launcher?.CancelCommandPalette(); + } + + private void UpdateVisible() + { + if (string.IsNullOrEmpty(_filter)) + { + VisibleCmds = _cmds; + } + else + { + var visible = new List(); + + foreach (var cmd in _cmds) + { + if (cmd.Key.Contains(_filter, StringComparison.OrdinalIgnoreCase) || + cmd.Label.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + visible.Add(cmd); + } + + var autoSelected = _selectedCmd; + if (!visible.Contains(_selectedCmd)) + autoSelected = visible.Count > 0 ? visible[0] : null; + + VisibleCmds = visible; + SelectedCmd = autoSelected; + } + } + + private Launcher _launcher = null; + private Repository _repo = null; + private List _cmds = []; + private List _visibleCmds = []; + private RepositoryCommandPaletteCmd _selectedCmd = null; + private string _filter; + } +} diff --git a/src/ViewModels/RepositoryConfigure.cs b/src/ViewModels/RepositoryConfigure.cs index f118a66d0..bc1e59b36 100644 --- a/src/ViewModels/RepositoryConfigure.cs +++ b/src/ViewModels/RepositoryConfigure.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Collections; @@ -75,6 +76,19 @@ public string HttpProxy set => SetProperty(ref _httpProxy, value); } + public string ConventionalTypesOverride + { + get => _repo.Settings.ConventionalTypesOverride; + set + { + if (_repo.Settings.ConventionalTypesOverride != value) + { + _repo.Settings.ConventionalTypesOverride = value; + OnPropertyChanged(); + } + } + } + public bool EnablePruneOnFetch { get; @@ -112,15 +126,15 @@ public Models.CommitTemplate SelectedCommitTemplate set => SetProperty(ref _selectedCommitTemplate, value); } - public AvaloniaList IssueTrackerRules + public AvaloniaList IssueTrackers { - get => _repo.Settings.IssueTrackerRules; - } + get; + } = []; - public Models.IssueTrackerRule SelectedIssueTrackerRule + public Models.IssueTracker SelectedIssueTracker { - get => _selectedIssueTrackerRule; - set => SetProperty(ref _selectedIssueTrackerRule, value); + get => _selectedIssueTracker; + set => SetProperty(ref _selectedIssueTracker, value); } public List AvailableOpenAIServices @@ -161,7 +175,7 @@ public RepositoryConfigure(Repository repo) if (!AvailableOpenAIServices.Contains(PreferredOpenAIService)) PreferredOpenAIService = "---"; - _cached = new Commands.Config(repo.FullPath).ReadAllAsync().Result; + _cached = new Commands.Config(repo.FullPath).ReadAll(); if (_cached.TryGetValue("user.name", out var name)) UserName = name; if (_cached.TryGetValue("user.email", out var email)) @@ -176,6 +190,17 @@ public RepositoryConfigure(Repository repo) HttpProxy = proxy; if (_cached.TryGetValue("fetch.prune", out var prune)) EnablePruneOnFetch = (prune == "true"); + + foreach (var rule in _repo.IssueTrackers) + { + IssueTrackers.Add(new() + { + IsShared = rule.IsShared, + Name = rule.Name, + RegexString = rule.RegexString, + URLTemplate = rule.URLTemplate, + }); + } } public void ClearHttpProxy() @@ -197,113 +222,37 @@ public void RemoveSelectedCommitTemplate() SelectedCommitTemplate = null; } - public void AddSampleGitHubIssueTracker() + public List GetRemoteVisitUrls() { - var link = "https://github.com/username/repository/issues/$1"; + var outs = new List(); foreach (var remote in _repo.Remotes) { - if (remote.URL.Contains("github.com", System.StringComparison.Ordinal) && - remote.TryGetVisitURL(out string url)) - { - link = $"{url}/issues/$1"; - break; - } + if (remote.TryGetVisitURL(out var url)) + outs.Add(url); } - - SelectedIssueTrackerRule = _repo.Settings.AddIssueTracker("GitHub ISSUE", @"#(\d+)", link); - } - - public void AddSampleJiraIssueTracker() - { - SelectedIssueTrackerRule = _repo.Settings.AddIssueTracker("Jira Tracker", @"PROJ-(\d+)", "https://jira.yourcompany.com/browse/PROJ-$1"); + return outs; } - public void AddSampleAzureWorkItemTracker() + public void AddIssueTracker(string name, string regex, string url) { - SelectedIssueTrackerRule = _repo.Settings.AddIssueTracker("Azure DevOps Tracker", @"#(\d+)", "https://dev.azure.com/yourcompany/workspace/_workitems/edit/$1"); - } - - public void AddSampleGitLabIssueTracker() - { - var link = "https://gitlab.com/username/repository/-/issues/$1"; - foreach (var remote in _repo.Remotes) + var rule = new Models.IssueTracker() { - if (remote.TryGetVisitURL(out string url)) - { - link = $"{url}/-/issues/$1"; - break; - } - } + IsShared = false, + Name = name, + RegexString = regex, + URLTemplate = url, + }; - SelectedIssueTrackerRule = _repo.Settings.AddIssueTracker("GitLab ISSUE", @"#(\d+)", link); + IssueTrackers.Add(rule); + SelectedIssueTracker = rule; } - public void AddSampleGitLabMergeRequestTracker() + public void RemoveIssueTracker() { - var link = "https://gitlab.com/username/repository/-/merge_requests/$1"; - foreach (var remote in _repo.Remotes) - { - if (remote.TryGetVisitURL(out string url)) - { - link = $"{url}/-/merge_requests/$1"; - break; - } - } + if (_selectedIssueTracker is { } rule) + IssueTrackers.Remove(rule); - SelectedIssueTrackerRule = _repo.Settings.AddIssueTracker("GitLab MR", @"!(\d+)", link); - } - - public void AddSampleGiteeIssueTracker() - { - var link = "https://gitee.com/username/repository/issues/$1"; - foreach (var remote in _repo.Remotes) - { - if (remote.URL.Contains("gitee.com", System.StringComparison.Ordinal) && - remote.TryGetVisitURL(out string url)) - { - link = $"{url}/issues/$1"; - break; - } - } - - SelectedIssueTrackerRule = _repo.Settings.AddIssueTracker("Gitee ISSUE", @"#([0-9A-Z]{6,10})", link); - } - - public void AddSampleGiteePullRequestTracker() - { - var link = "https://gitee.com/username/repository/pulls/$1"; - foreach (var remote in _repo.Remotes) - { - if (remote.URL.Contains("gitee.com", System.StringComparison.Ordinal) && - remote.TryGetVisitURL(out string url)) - { - link = $"{url}/pulls/$1"; - } - } - - SelectedIssueTrackerRule = _repo.Settings.AddIssueTracker("Gitee Pull Request", @"!(\d+)", link); - } - - public void NewIssueTracker() - { - SelectedIssueTrackerRule = _repo.Settings.AddIssueTracker("New Issue Tracker", @"#(\d+)", "https://xxx/$1"); - } - - public void RemoveSelectedIssueTracker() - { - _repo.Settings.RemoveIssueTracker(_selectedIssueTrackerRule); - SelectedIssueTrackerRule = null; - } - - public async Task ChangeIssueTrackerShareModeAsync() - { - if (_selectedIssueTrackerRule is not { } rule) - return; - - if (rule.IsShared) - await new Commands.SharedIssueTracker(_repo.FullPath).AddAsync(rule); - else - await new Commands.SharedIssueTracker(_repo.FullPath).RemoveAsync(rule); + SelectedIssueTracker = null; } public void AddNewCustomAction() @@ -338,6 +287,8 @@ public async Task SaveAsync() await SetIfChangedAsync("user.signingkey", GPGUserSigningKey, ""); await SetIfChangedAsync("http.proxy", HttpProxy, ""); await SetIfChangedAsync("fetch.prune", EnablePruneOnFetch ? "true" : "false", "false"); + + await ApplyIssueTrackerChangesAsync(); } private async Task SetIfChangedAsync(string key, string value, string defValue) @@ -346,11 +297,67 @@ private async Task SetIfChangedAsync(string key, string value, string defValue) await new Commands.Config(_repo.FullPath).SetAsync(key, value); } + private async Task ApplyIssueTrackerChangesAsync() + { + var changed = false; + var oldRules = new Dictionary(); + foreach (var rule in _repo.IssueTrackers) + oldRules.Add(rule.Name, rule); + + foreach (var rule in IssueTrackers) + { + if (oldRules.TryGetValue(rule.Name, out var old)) + { + if (old.IsShared != rule.IsShared) + { + changed = true; + await new Commands.IssueTracker(_repo.FullPath, old.IsShared).RemoveAsync(old.Name); + await new Commands.IssueTracker(_repo.FullPath, rule.IsShared).AddAsync(rule); + } + else + { + if (!old.RegexString.Equals(rule.RegexString, StringComparison.Ordinal)) + { + changed = true; + await new Commands.IssueTracker(_repo.FullPath, old.IsShared).UpdateRegexAsync(rule); + } + + if (!old.URLTemplate.Equals(rule.URLTemplate, StringComparison.Ordinal)) + { + changed = true; + await new Commands.IssueTracker(_repo.FullPath, old.IsShared).UpdateURLTemplateAsync(rule); + } + } + + oldRules.Remove(rule.Name); + } + else + { + changed = true; + await new Commands.IssueTracker(_repo.FullPath, rule.IsShared).AddAsync(rule); + } + } + + if (oldRules.Count > 0) + { + changed = true; + + foreach (var kv in oldRules) + await new Commands.IssueTracker(_repo.FullPath, kv.Value.IsShared).RemoveAsync(kv.Key); + } + + if (changed) + { + _repo.IssueTrackers.Clear(); + _repo.IssueTrackers.AddRange(IssueTrackers); + } + } + private readonly Repository _repo = null; private readonly Dictionary _cached = null; private string _httpProxy; private Models.CommitTemplate _selectedCommitTemplate = null; - private Models.IssueTrackerRule _selectedIssueTrackerRule = null; + private Models.IssueTracker _selectedIssueTracker = null; private Models.CustomAction _selectedCustomAction = null; } } diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index c65d1dbd0..1f5515b9d 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Text.Json.Serialization; - +using System.Threading; +using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -62,12 +64,30 @@ public int Depth set; } = 0; + public Models.RepositoryStatus Status + { + get => _status; + set => SetProperty(ref _status, value); + } + public List SubNodes { get; set; } = []; + public void Open() + { + if (IsRepository) + { + App.GetLauncher().OpenRepositoryInTab(this, null); + return; + } + + foreach (var subNode in SubNodes) + subNode.Open(); + } + public void Edit() { var activePage = App.GetLauncher().ActivePage; @@ -82,6 +102,13 @@ public void AddSubFolder() activePage.Popup = new CreateGroup(this); } + public void Move() + { + var activePage = App.GetLauncher().ActivePage; + if (activePage != null && activePage.CanCreatePopup()) + activePage.Popup = new MoveRepositoryNode(this); + } + public void OpenInFileManager() { if (!IsRepository) @@ -103,11 +130,53 @@ public void Delete() activePage.Popup = new DeleteRepositoryNode(this); } + public async Task UpdateStatusAsync(bool force, CancellationToken? token) + { + if (token is { IsCancellationRequested: true }) + return; + + if (!_isRepository) + { + Status = null; + + if (SubNodes.Count > 0) + { + // avoid collection was modified while enumerating. + var nodes = new List(); + nodes.AddRange(SubNodes); + + foreach (var node in nodes) + await node.UpdateStatusAsync(force, token); + } + + return; + } + + if (!Directory.Exists(_id)) + { + _lastUpdateStatus = DateTime.Now; + Status = null; + return; + } + + if (!force) + { + var passed = DateTime.Now - _lastUpdateStatus; + if (passed.TotalSeconds < 10.0) + return; + } + + _lastUpdateStatus = DateTime.Now; + Status = await new Commands.QueryRepositoryStatus(_id).GetResultAsync(); + } + private string _id = string.Empty; private string _name = string.Empty; private bool _isRepository = false; private int _bookmark = 0; private bool _isExpanded = false; private bool _isVisible = true; + private Models.RepositoryStatus _status = null; + private DateTime _lastUpdateStatus = DateTime.UnixEpoch.ToLocalTime(); } } diff --git a/src/ViewModels/Reset.cs b/src/ViewModels/Reset.cs index 5a628b1ea..0e70754b1 100644 --- a/src/ViewModels/Reset.cs +++ b/src/ViewModels/Reset.cs @@ -30,7 +30,7 @@ public Reset(Repository repo, Models.Branch current, Models.Commit to) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Reset current branch to {To.SHA} ..."; var log = _repo.CreateLog($"Reset HEAD to '{To.SHA}'"); @@ -41,7 +41,6 @@ public override async Task Sure() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/ResetWithoutCheckout.cs b/src/ViewModels/ResetWithoutCheckout.cs index f9b119f91..fb32265b3 100644 --- a/src/ViewModels/ResetWithoutCheckout.cs +++ b/src/ViewModels/ResetWithoutCheckout.cs @@ -32,7 +32,7 @@ public ResetWithoutCheckout(Repository repo, Models.Branch target, Models.Commit public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = $"Reset {Target.Name} to {_revision} ..."; var log = _repo.CreateLog($"Reset '{Target.Name}' to '{_revision}'"); @@ -44,7 +44,6 @@ public override async Task Sure() log.Complete(); _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/Revert.cs b/src/ViewModels/Revert.cs index b39076a09..ea6f86d09 100644 --- a/src/ViewModels/Revert.cs +++ b/src/ViewModels/Revert.cs @@ -24,7 +24,7 @@ public Revert(Repository repo, Models.Commit target) public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); _repo.ClearCommitMessage(); ProgressDescription = $"Revert commit '{Target.SHA}' ..."; @@ -36,7 +36,6 @@ public override async Task Sure() .ExecAsync(); log.Complete(); - _repo.SetWatcherEnabled(true); return true; } diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 18e8044ba..540338cc4 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -1,11 +1,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; - -using Avalonia.Controls; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -32,6 +28,12 @@ public object EndPoint public bool CanSaveAsPatch { get; } + public int TotalChanges + { + get => _totalChanges; + private set => SetProperty(ref _totalChanges, value); + } + public List VisibleChanges { get => _visibleChanges; @@ -80,8 +82,7 @@ public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endP _startPoint = (object)startPoint ?? new Models.Null(); _endPoint = (object)endPoint ?? new Models.Null(); CanSaveAsPatch = startPoint != null && endPoint != null; - - Task.Run(Refresh); + Refresh(); } public void Dispose() @@ -96,6 +97,12 @@ public void Dispose() _diffContext = null; } + public void OpenChangeWithExternalDiffTool(Models.Change change) + { + var opt = new Models.DiffOption(GetSHA(_startPoint), GetSHA(_endPoint), change); + new Commands.DiffTool(_repo, opt).Open(); + } + public void NavigateTo(string commitSHA) { var launcher = App.GetLauncher(); @@ -118,84 +125,24 @@ public void Swap() VisibleChanges = []; SelectedChanges = []; IsLoading = true; - Task.Run(Refresh); + Refresh(); } - public void SaveAsPatch(string saveTo) + public string GetAbsPath(string path) { - Task.Run(async () => - { - var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); - if (succ) - App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); - }); + return Native.OS.GetAbsPath(_repo, path); } - public void ClearSearchFilter() + public async Task SaveChangesAsPatchAsync(List changes, string saveTo) { - SearchFilter = string.Empty; + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes ?? _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); + if (succ) + App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); } - public ContextMenu CreateChangeContextMenu() + public void ClearSearchFilter() { - if (_selectedChanges is not { Count: 1 }) - return null; - - var change = _selectedChanges[0]; - var menu = new ContextMenu(); - - var openWithMerger = new MenuItem(); - openWithMerger.Header = App.Text("OpenInExternalMergeTool"); - openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openWithMerger.Click += (_, ev) => - { - var opt = new Models.DiffOption(GetSHA(_startPoint), GetSHA(_endPoint), change); - var toolType = Preferences.Instance.ExternalMergeToolType; - var toolPath = Preferences.Instance.ExternalMergeToolPath; - new Commands.DiffTool(_repo, toolType, toolPath, opt).Open(); - ev.Handled = true; - }; - menu.Items.Add(openWithMerger); - - if (change.Index != Models.ChangeState.Deleted) - { - var full = Path.GetFullPath(Path.Combine(_repo, change.Path)); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(full); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(full, true); - ev.Handled = true; - }; - menu.Items.Add(explore); - } - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(change.Path); - ev.Handled = true; - }; - menu.Items.Add(copyPath); - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(Native.OS.GetAbsPath(_repo, change.Path)); - e.Handled = true; - }; - menu.Items.Add(copyFullPath); - - return menu; + SearchFilter = string.Empty; } private void RefreshVisible() @@ -222,28 +169,34 @@ private void RefreshVisible() private void Refresh() { - _changes = new Commands.CompareRevisions(_repo, GetSHA(_startPoint), GetSHA(_endPoint)).ReadAsync().Result; - - var visible = _changes; - if (!string.IsNullOrWhiteSpace(_searchFilter)) + Task.Run(async () => { - visible = []; - foreach (var c in _changes) + _changes = await new Commands.CompareRevisions(_repo, GetSHA(_startPoint), GetSHA(_endPoint)) + .ReadAsync() + .ConfigureAwait(false); + + var visible = _changes; + if (!string.IsNullOrWhiteSpace(_searchFilter)) { - if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) - visible.Add(c); + visible = []; + foreach (var c in _changes) + { + if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) + visible.Add(c); + } } - } - Dispatcher.UIThread.Post(() => - { - VisibleChanges = visible; - IsLoading = false; + Dispatcher.UIThread.Post(() => + { + TotalChanges = _changes.Count; + VisibleChanges = visible; + IsLoading = false; - if (VisibleChanges.Count > 0) - SelectedChanges = [VisibleChanges[0]]; - else - SelectedChanges = []; + if (VisibleChanges.Count > 0) + SelectedChanges = [VisibleChanges[0]]; + else + SelectedChanges = []; + }); }); } @@ -256,6 +209,7 @@ private string GetSHA(object obj) private bool _isLoading = true; private object _startPoint = null; private object _endPoint = null; + private int _totalChanges = 0; private List _changes = null; private List _visibleChanges = null; private List _selectedChanges = null; diff --git a/src/ViewModels/RevisionLFSImage.cs b/src/ViewModels/RevisionLFSImage.cs index 2cdd8e665..2e51c69ce 100644 --- a/src/ViewModels/RevisionLFSImage.cs +++ b/src/ViewModels/RevisionLFSImage.cs @@ -25,7 +25,7 @@ public RevisionLFSImage(string repo, string file, Models.LFSObject lfs, Models.I { var source = await ImageSource.FromLFSObjectAsync(repo, lfs, decoder).ConfigureAwait(false); var img = new Models.RevisionImageFile(file, source.Bitmap, source.Size); - Dispatcher.UIThread.Invoke(() => Image = img); + Dispatcher.UIThread.Post(() => Image = img); }); } diff --git a/src/ViewModels/Reword.cs b/src/ViewModels/Reword.cs index 2ce278531..efea03e8d 100644 --- a/src/ViewModels/Reword.cs +++ b/src/ViewModels/Reword.cs @@ -18,10 +18,10 @@ public string Message set => SetProperty(ref _message, value, true); } - public Reword(Repository repo, Models.Commit head) + public Reword(Repository repo, Models.Commit head, string oldMessage) { _repo = repo; - _oldMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync().Result; + _oldMessage = oldMessage; _message = _oldMessage; Head = head; } @@ -31,7 +31,7 @@ public override async Task Sure() if (string.Compare(_message, _oldMessage, StringComparison.Ordinal) == 0) return true; - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Editing HEAD message ..."; var log = _repo.CreateLog("Reword HEAD"); @@ -39,6 +39,7 @@ public override async Task Sure() var changes = await new Commands.QueryLocalChanges(_repo.FullPath, false).GetResultAsync(); var signOff = _repo.Settings.EnableSignOffForCommit; + var noVerify = _repo.Settings.NoVerifyOnCommit; var needAutoStash = false; var succ = false; @@ -59,12 +60,11 @@ public override async Task Sure() if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } } - succ = await new Commands.Commit(_repo.FullPath, _message, signOff, true, false) + succ = await new Commands.Commit(_repo.FullPath, _message, signOff, noVerify, true, false) .Use(log) .RunAsync(); @@ -74,7 +74,6 @@ public override async Task Sure() .PopAsync("stash@{0}"); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index b01547707..e7316dba5 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; @@ -8,12 +7,23 @@ namespace SourceGit.ViewModels { public class ScanRepositories : Popup { + public bool UseCustomDir + { + get => _useCustomDir; + set => SetProperty(ref _useCustomDir, value); + } + + public string CustomDir + { + get => _customDir; + set => SetProperty(ref _customDir, value); + } + public List ScanDirs { get; } - [Required(ErrorMessage = "Scan directory is required!!!")] public Models.ScanDir Selected { get => _selected; @@ -33,16 +43,43 @@ public ScanRepositories() if (ScanDirs.Count > 0) _selected = ScanDirs[0]; + else + _useCustomDir = true; GetManagedRepositories(Preferences.Instance.RepositoryNodes, _managed); } public override async Task Sure() { - ProgressDescription = $"Scan repositories under '{_selected.Path}' ..."; + string selectedDir; + if (_useCustomDir) + { + if (string.IsNullOrEmpty(_customDir)) + { + App.RaiseException(null, "Missing root directory to scan!"); + return false; + } + + selectedDir = _customDir; + } + else + { + if (_selected == null || string.IsNullOrEmpty(_selected.Path)) + { + App.RaiseException(null, "Missing root directory to scan!"); + return false; + } + + selectedDir = _selected.Path; + } + + if (!Directory.Exists(selectedDir)) + return true; + + ProgressDescription = $"Scan repositories under '{selectedDir}' ..."; var minDelay = Task.Delay(500); - var rootDir = new DirectoryInfo(_selected.Path); + var rootDir = new DirectoryInfo(selectedDir); var found = new List(); await GetUnmanagedRepositoriesAsync(rootDir, found, new EnumerationOptions() @@ -60,13 +97,15 @@ public override async Task Sure() var parent = new DirectoryInfo(f).Parent!.FullName.Replace('\\', '/').TrimEnd('/'); if (parent.Equals(normalizedRoot, StringComparison.Ordinal)) { - Preferences.Instance.FindOrAddNodeByRepositoryPath(f, null, false, false); + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(f, null, false, false); + await node.UpdateStatusAsync(false, null); } else if (parent.StartsWith(normalizedRoot, StringComparison.Ordinal)) { var relative = parent.Substring(normalizedRoot.Length).TrimStart('/'); var group = FindOrCreateGroupRecursive(Preferences.Instance.RepositoryNodes, relative); - Preferences.Instance.FindOrAddNodeByRepositoryPath(f, group, false, false); + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(f, group, false, false); + await node.UpdateStatusAsync(false, null); } } @@ -105,7 +144,7 @@ private async Task GetUnmanagedRepositoriesAsync(DirectoryInfo dir, List var gitDir = Path.Combine(subdir.FullName, ".git"); if (Directory.Exists(gitDir) || File.Exists(gitDir)) { - var test = await new Commands.QueryRepositoryRootPath(subdir.FullName).GetResultAsync().ConfigureAwait(false); + var test = await new Commands.QueryRepositoryRootPath(subdir.FullName).GetResultAsync(); if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) { var normalized = test.StdOut.Trim().Replace('\\', '/').TrimEnd('/'); @@ -116,7 +155,7 @@ private async Task GetUnmanagedRepositoriesAsync(DirectoryInfo dir, List continue; } - var isBare = await new Commands.IsBareRepository(subdir.FullName).GetResultAsync().ConfigureAwait(false); + var isBare = await new Commands.IsBareRepository(subdir.FullName).GetResultAsync(); if (isBare) { outs.Add(normalizedSelf); @@ -162,6 +201,8 @@ private RepositoryNode FindOrCreateGroup(List collection, string } private HashSet _managed = new(); + private bool _useCustomDir = false; + private string _customDir = string.Empty; private Models.ScanDir _selected = null; } } diff --git a/src/ViewModels/SearchCommitContext.cs b/src/ViewModels/SearchCommitContext.cs new file mode 100644 index 000000000..374336e5e --- /dev/null +++ b/src/ViewModels/SearchCommitContext.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class SearchCommitContext : ObservableObject, IDisposable + { + public int Method + { + get => _method; + set + { + if (SetProperty(ref _method, value)) + { + UpdateSuggestions(); + StartSearch(); + } + } + } + + public string Filter + { + get => _filter; + set + { + if (SetProperty(ref _filter, value)) + UpdateSuggestions(); + } + } + + public bool OnlySearchCurrentBranch + { + get => _onlySearchCurrentBranch; + set + { + if (SetProperty(ref _onlySearchCurrentBranch, value)) + StartSearch(); + } + } + + public List Suggestions + { + get => _suggestions; + private set => SetProperty(ref _suggestions, value); + } + + public bool IsQuerying + { + get => _isQuerying; + private set => SetProperty(ref _isQuerying, value); + } + + public List Results + { + get => _results; + private set => SetProperty(ref _results, value); + } + + public Models.Commit Selected + { + get => _selected; + set + { + if (SetProperty(ref _selected, value) && value != null) + _repo.NavigateToCommit(value.SHA); + } + } + + public SearchCommitContext(Repository repo) + { + _repo = repo; + } + + public void Dispose() + { + _repo = null; + _suggestions?.Clear(); + _results?.Clear(); + _worktreeFiles?.Clear(); + } + + public void ClearFilter() + { + Filter = string.Empty; + Selected = null; + Results = null; + } + + public void ClearSuggestions() + { + Suggestions = null; + } + + public void StartSearch() + { + Results = null; + Selected = null; + Suggestions = null; + + if (string.IsNullOrEmpty(_filter)) + return; + + IsQuerying = true; + + Task.Run(async () => + { + var result = new List(); + var method = (Models.CommitSearchMethod)_method; + var repoPath = _repo.FullPath; + + if (method == Models.CommitSearchMethod.BySHA) + { + var isCommitSHA = await new Commands.IsCommitSHA(repoPath, _filter) + .GetResultAsync() + .ConfigureAwait(false); + + if (isCommitSHA) + { + var commit = await new Commands.QuerySingleCommit(repoPath, _filter) + .GetResultAsync() + .ConfigureAwait(false); + + commit.IsMerged = await new Commands.IsAncestor(repoPath, commit.SHA, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); + + result.Add(commit); + } + } + else if (_onlySearchCurrentBranch) + { + result = await new Commands.QueryCommits(repoPath, _filter, method, true) + .GetResultAsync() + .ConfigureAwait(false); + + foreach (var c in result) + c.IsMerged = true; + } + else + { + result = await new Commands.QueryCommits(repoPath, _filter, method, false) + .GetResultAsync() + .ConfigureAwait(false); + + if (result.Count > 0) + { + var set = await new Commands.QueryCurrentBranchCommitHashes(repoPath, result[^1].CommitterTime) + .GetResultAsync() + .ConfigureAwait(false); + + foreach (var c in result) + c.IsMerged = set.Contains(c.SHA); + } + } + + Dispatcher.UIThread.Post(() => + { + IsQuerying = false; + + if (_repo.IsSearchingCommits) + Results = result; + }); + }); + } + + public void EndSearch() + { + _worktreeFiles = null; + Suggestions = null; + Results = null; + GC.Collect(); + } + + private void UpdateSuggestions() + { + if (_method != (int)Models.CommitSearchMethod.ByPath || _requestingWorktreeFiles) + { + Suggestions = null; + return; + } + + if (_worktreeFiles == null) + { + _requestingWorktreeFiles = true; + + Task.Run(async () => + { + var files = await new Commands.QueryRevisionFileNames(_repo.FullPath, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + _requestingWorktreeFiles = false; + + if (_repo.IsSearchingCommits) + { + _worktreeFiles = files; + UpdateSuggestions(); + } + }); + }); + + return; + } + + if (_worktreeFiles.Count == 0 || _filter.Length < 3) + { + Suggestions = null; + return; + } + + var matched = new List(); + foreach (var file in _worktreeFiles) + { + if (file.Contains(_filter, StringComparison.OrdinalIgnoreCase) && file.Length != _filter.Length) + { + matched.Add(file); + if (matched.Count > 100) + break; + } + } + + Suggestions = matched; + } + + private Repository _repo = null; + private int _method = (int)Models.CommitSearchMethod.ByMessage; + private string _filter = string.Empty; + private bool _onlySearchCurrentBranch = false; + private List _suggestions = null; + private bool _isQuerying = false; + private List _results = null; + private Models.Commit _selected = null; + private bool _requestingWorktreeFiles = false; + private List _worktreeFiles = null; + } +} diff --git a/src/ViewModels/SetSubmoduleBranch.cs b/src/ViewModels/SetSubmoduleBranch.cs index 5abeec87d..e6edd674d 100644 --- a/src/ViewModels/SetSubmoduleBranch.cs +++ b/src/ViewModels/SetSubmoduleBranch.cs @@ -30,8 +30,8 @@ public override async Task Sure() if (_changeTo.Equals(Submodule.Branch, StringComparison.Ordinal)) return true; + using var lockWatcher = _repo.LockWatcher(); var log = _repo.CreateLog("Set Submodule's Branch"); - _repo.SetWatcherEnabled(false); Use(log); var succ = await new Commands.Submodule(_repo.FullPath) @@ -39,7 +39,6 @@ public override async Task Sure() .SetBranchAsync(Submodule.Path, _changeTo); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/SetUpstream.cs b/src/ViewModels/SetUpstream.cs index 60ceb5137..e3ad871f2 100644 --- a/src/ViewModels/SetUpstream.cs +++ b/src/ViewModels/SetUpstream.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -53,17 +54,24 @@ public SetUpstream(Repository repo, Models.Branch local, List rem public override async Task Sure() { ProgressDescription = "Setting upstream..."; + Models.Branch upstream = _unset ? null : SelectedRemoteBranch; - var upstream = (_unset || SelectedRemoteBranch == null) ? string.Empty : SelectedRemoteBranch.FullName; - if (upstream == Local.Upstream) + if (upstream == null) + { + if (string.IsNullOrEmpty(Local.Upstream)) + return true; + } + else if (upstream.FullName.Equals(Local.Upstream, StringComparison.Ordinal)) + { return true; + } var log = _repo.CreateLog("Set Upstream"); Use(log); var succ = await new Commands.Branch(_repo.FullPath, Local.Name) .Use(log) - .SetUpstreamAsync(upstream.Replace("refs/remotes/", "")); + .SetUpstreamAsync(upstream); log.Complete(); if (succ) diff --git a/src/ViewModels/Squash.cs b/src/ViewModels/SquashOrFixupHead.cs similarity index 73% rename from src/ViewModels/Squash.cs rename to src/ViewModels/SquashOrFixupHead.cs index a575a64c8..18b5b6f91 100644 --- a/src/ViewModels/Squash.cs +++ b/src/ViewModels/SquashOrFixupHead.cs @@ -3,8 +3,13 @@ namespace SourceGit.ViewModels { - public class Squash : Popup + public class SquashOrFixupHead : Popup { + public bool IsFixupMode + { + get; + } + public Models.Commit Target { get; @@ -17,23 +22,26 @@ public string Message set => SetProperty(ref _message, value, true); } - public Squash(Repository repo, Models.Commit target, string shaToGetPreferMessage) + public SquashOrFixupHead(Repository repo, Models.Commit target, string message, bool fixup) { - _repo = repo; - _message = new Commands.QueryCommitFullMessage(_repo.FullPath, shaToGetPreferMessage).GetResultAsync().Result; + IsFixupMode = fixup; Target = target; + + _repo = repo; + _message = message; } public override async Task Sure() { - _repo.SetWatcherEnabled(false); - ProgressDescription = "Squashing ..."; + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = IsFixupMode ? "Fixup ..." : "Squashing ..."; - var log = _repo.CreateLog("Squash"); + var log = _repo.CreateLog(IsFixupMode ? "Fixup" : "Squash"); Use(log); var changes = await new Commands.QueryLocalChanges(_repo.FullPath, false).GetResultAsync(); var signOff = _repo.Settings.EnableSignOffForCommit; + var noVerify = _repo.Settings.NoVerifyOnCommit; var needAutoStash = false; var succ = false; @@ -50,11 +58,10 @@ public override async Task Sure() { succ = await new Commands.Stash(_repo.FullPath) .Use(log) - .PushAsync("SQUASH_AUTO_STASH"); + .PushAsync(IsFixupMode ? "FIXUP_AUTO_STASH" : "SQUASH_AUTO_STASH"); if (!succ) { log.Complete(); - _repo.SetWatcherEnabled(true); return false; } } @@ -64,7 +71,7 @@ public override async Task Sure() .ExecAsync(); if (succ) - succ = await new Commands.Commit(_repo.FullPath, _message, signOff, true, false) + succ = await new Commands.Commit(_repo.FullPath, _message, signOff, noVerify, true, false) .Use(log) .RunAsync(); @@ -74,7 +81,6 @@ public override async Task Sure() .PopAsync("stash@{0}"); log.Complete(); - _repo.SetWatcherEnabled(true); return succ; } diff --git a/src/ViewModels/StashChanges.cs b/src/ViewModels/StashChanges.cs index 62e942f75..154c89b2c 100644 --- a/src/ViewModels/StashChanges.cs +++ b/src/ViewModels/StashChanges.cs @@ -15,7 +15,7 @@ public string Message public bool HasSelectedFiles { - get; + get => _changes != null; } public bool IncludeUntracked @@ -43,16 +43,15 @@ public int ChangesAfterStashing set => _repo.Settings.ChangesAfterStashing = value; } - public StashChanges(Repository repo, List changes, bool hasSelectedFiles) + public StashChanges(Repository repo, List selectedChanges) { _repo = repo; - _changes = changes; - HasSelectedFiles = hasSelectedFiles; + _changes = selectedChanges; } public override async Task Sure() { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Stash changes ..."; var log = _repo.CreateLog("Stash Local Changes"); @@ -62,7 +61,7 @@ public override async Task Sure() var keepIndex = mode == DealWithChangesAfterStashing.KeepIndex; bool succ; - if (!HasSelectedFiles) + if (_changes == null) { if (OnlyStaged) { @@ -74,8 +73,12 @@ public override async Task Sure() } else { + var all = await new Commands.QueryLocalChanges(_repo.FullPath, false) + .Use(log) + .GetResultAsync(); + var staged = new List(); - foreach (var c in _changes) + foreach (var c in all) { if (c.Index != Models.ChangeState.None && c.Index != Models.ChangeState.Untracked) staged.Add(c); @@ -103,7 +106,7 @@ public override async Task Sure() log.Complete(); _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); + _repo.MarkStashesDirtyManually(); return succ; } diff --git a/src/ViewModels/StashesPage.cs b/src/ViewModels/StashesPage.cs index 414fb055d..96f01c7a0 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -1,12 +1,8 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Platform.Storage; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -140,184 +136,115 @@ public void Dispose() _diffContext = null; } - public ContextMenu MakeContextMenu(Models.Stash stash) + public void ClearSearchFilter() { - var apply = new MenuItem(); - apply.Header = App.Text("StashCM.Apply"); - apply.Icon = App.CreateMenuIcon("Icons.CheckCircled"); - apply.Click += (_, ev) => - { - Apply(stash); - ev.Handled = true; - }; - - var drop = new MenuItem(); - drop.Header = App.Text("StashCM.Drop"); - drop.Icon = App.CreateMenuIcon("Icons.Clear"); - drop.Tag = "Back/Delete"; - drop.Click += (_, ev) => - { - Drop(stash); - ev.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = App.Text("StashCM.SaveAsPatch"); - patch.Icon = App.CreateMenuIcon("Icons.Diff"); - patch.Click += async (_, e) => - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == null) - return; - - var options = new FilePickerSaveOptions(); - options.Title = App.Text("StashCM.SaveAsPatch"); - options.DefaultExtension = ".patch"; - options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; - - var storageFile = await storageProvider.SaveFilePickerAsync(options); - if (storageFile != null) - { - var opts = new List(); - foreach (var c in _changes) - { - if (_untracked.Contains(c)) - opts.Add(new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], c)); - else - opts.Add(new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, c)); - } + SearchFilter = string.Empty; + } - var succ = await Commands.SaveChangesAsPatch.ProcessStashChangesAsync(_repo.FullPath, opts, storageFile.Path.LocalPath); - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } + public string GetAbsPath(string path) + { + return Native.OS.GetAbsPath(_repo.FullPath, path); + } - e.Handled = true; - }; + public void Apply(Models.Stash stash) + { + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new ApplyStash(_repo, stash)); + } - var copy = new MenuItem(); - copy.Header = App.Text("StashCM.CopyMessage"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copy.Click += async (_, ev) => - { - await App.CopyTextAsync(stash.Message); - ev.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(apply); - menu.Items.Add(drop); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(patch); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(copy); - return menu; + public void Drop(Models.Stash stash) + { + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new DropStash(_repo, stash)); } - public ContextMenu MakeContextMenuForChange() + public async Task SaveStashAsPatchAsync(Models.Stash stash, string saveTo) { - if (_selectedChanges is not { Count: 1 }) - return null; - - var change = _selectedChanges[0]; - var openWithMerger = new MenuItem(); - openWithMerger.Header = App.Text("OpenInExternalMergeTool"); - openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openWithMerger.Click += (_, ev) => - { - var toolType = Preferences.Instance.ExternalMergeToolType; - var toolPath = Preferences.Instance.ExternalMergeToolPath; - var opt = new Models.DiffOption($"{_selectedStash.SHA}^", _selectedStash.SHA, change); - new Commands.DiffTool(_repo.FullPath, toolType, toolPath, opt).Open(); - ev.Handled = true; - }; - - var fullPath = Path.Combine(_repo.FullPath, change.Path); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(fullPath); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(fullPath, true); - ev.Handled = true; - }; - - var resetToThisRevision = new MenuItem(); - resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); - resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - resetToThisRevision.Click += async (_, ev) => + var opts = new List(); + var changes = await new Commands.CompareRevisions(_repo.FullPath, $"{stash.SHA}^", stash.SHA) + .ReadAsync() + .ConfigureAwait(false); + + foreach (var c in changes) + opts.Add(new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, c)); + + if (stash.Parents.Count == 3) { - var log = _repo.CreateLog($"Reset File to '{_selectedStash.SHA}'"); + var untracked = await new Commands.CompareRevisions(_repo.FullPath, Models.Commit.EmptyTreeSHA1, stash.Parents[2]) + .ReadAsync() + .ConfigureAwait(false); - if (_untracked.Contains(change)) - { - await Commands.SaveRevisionFile.RunAsync(_repo.FullPath, _selectedStash.Parents[2], change.Path, fullPath); - } - else if (change.Index == Models.ChangeState.Added) - { - await Commands.SaveRevisionFile.RunAsync(_repo.FullPath, _selectedStash.SHA, change.Path, fullPath); - } - else - { - await new Commands.Checkout(_repo.FullPath) - .Use(log) - .FileWithRevisionAsync(change.Path, $"{_selectedStash.SHA}"); - } + foreach (var c in untracked) + opts.Add(new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], c)); - log.Complete(); - ev.Handled = true; - }; + changes.AddRange(untracked); + } - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(change.Path); - ev.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(Native.OS.GetAbsPath(_repo.FullPath, change.Path)); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(openWithMerger); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(resetToThisRevision); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(copyPath); - menu.Items.Add(copyFullPath); - - return menu; + var succ = await Commands.SaveChangesAsPatch.ProcessStashChangesAsync(_repo.FullPath, opts, saveTo); + if (succ) + App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } - public void ClearSearchFilter() + public void OpenChangeWithExternalDiffTool(Models.Change change) { - SearchFilter = string.Empty; + Models.DiffOption opt; + if (_untracked.Contains(change)) + opt = new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], change); + else + opt = new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, change); + + new Commands.DiffTool(_repo.FullPath, opt).Open(); } - public void Apply(Models.Stash stash) + public async Task CheckoutSingleFileAsync(Models.Change change) { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new ApplyStash(_repo, stash)); + var revision = _selectedStash.SHA; + if (_untracked.Contains(change) && _selectedStash.Parents.Count == 3) + revision = _selectedStash.Parents[2]; + else if (change.Index == Models.ChangeState.Added && _selectedStash.Parents.Count > 1) + revision = _selectedStash.Parents[1]; + + var log = _repo.CreateLog($"Reset File to '{_selectedStash.Name}'"); + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, revision); + log.Complete(); } - public void Drop(Models.Stash stash) + public async Task CheckoutMultipleFileAsync(List changes) { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new DropStash(_repo, stash)); + var untracked = new List(); + var added = new List(); + var modified = new List(); + + foreach (var c in changes) + { + if (_untracked.Contains(c) && _selectedStash.Parents.Count == 3) + untracked.Add(c.Path); + else if (c.Index == Models.ChangeState.Added && _selectedStash.Parents.Count > 1) + added.Add(c.Path); + else + modified.Add(c.Path); + } + + var log = _repo.CreateLog($"Reset File to '{_selectedStash.Name}'"); + + if (untracked.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(untracked, _selectedStash.Parents[2]); + + if (added.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(added, _selectedStash.Parents[1]); + + if (modified.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(modified, _selectedStash.SHA); + + log.Complete(); } private void RefreshVisible() diff --git a/src/ViewModels/TagCollection.cs b/src/ViewModels/TagCollection.cs index 0848be1d8..50503dc22 100644 --- a/src/ViewModels/TagCollection.cs +++ b/src/ViewModels/TagCollection.cs @@ -1,21 +1,27 @@ +using System.Collections; using System.Collections.Generic; +using Avalonia; using Avalonia.Collections; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class TagTreeNodeToolTip + public class TagToolTip { public string Name { get; private set; } public bool IsAnnotated { get; private set; } + public Models.User Creator { get; private set; } + public string CreatorDateStr { get; private set; } public string Message { get; private set; } - public TagTreeNodeToolTip(Models.Tag t) + public TagToolTip(Models.Tag t) { Name = t.Name; IsAnnotated = t.IsAnnotated; + Creator = t.Creator; + CreatorDateStr = t.CreatorDateStr; Message = t.Message; } } @@ -25,7 +31,7 @@ public class TagTreeNode : ObservableObject public string FullPath { get; private set; } public int Depth { get; private set; } = 0; public Models.Tag Tag { get; private set; } = null; - public TagTreeNodeToolTip ToolTip { get; private set; } = null; + public TagToolTip ToolTip { get; private set; } = null; public List Children { get; private set; } = []; public int Counter { get; set; } = 0; @@ -34,6 +40,24 @@ public bool IsFolder get => Tag == null; } + public bool IsSelected + { + get; + set; + } + + public Models.FilterMode FilterMode + { + get => _filterMode; + set => SetProperty(ref _filterMode, value); + } + + public CornerRadius CornerRadius + { + get => _cornerRadius; + set => SetProperty(ref _cornerRadius, value); + } + public bool IsExpanded { get => _isExpanded; @@ -50,7 +74,7 @@ public TagTreeNode(Models.Tag t, int depth) FullPath = t.Name; Depth = depth; Tag = t; - ToolTip = new TagTreeNodeToolTip(t); + ToolTip = new TagToolTip(t); IsExpanded = false; } @@ -62,6 +86,19 @@ public TagTreeNode(string path, bool isExpanded, int depth) Counter = 1; } + public void UpdateFilterMode(Dictionary filters) + { + if (Tag == null) + { + foreach (var child in Children) + child.UpdateFilterMode(filters); + } + else + { + FilterMode = filters.GetValueOrDefault(FullPath, Models.FilterMode.None); + } + } + public static List Build(List tags, HashSet expanded) { var nodes = new List(); @@ -127,16 +164,103 @@ private static void InsertFolder(List collection, TagTreeNode subFo collection.Add(subFolder); } + private Models.FilterMode _filterMode = Models.FilterMode.None; + private CornerRadius _cornerRadius = new CornerRadius(4); private bool _isExpanded = true; } + public class TagListItem : ObservableObject + { + public Models.Tag Tag + { + get; + set; + } + + public bool IsSelected + { + get; + set; + } + + public Models.FilterMode FilterMode + { + get => _filterMode; + set => SetProperty(ref _filterMode, value); + } + + public TagToolTip ToolTip + { + get; + set; + } + + public CornerRadius CornerRadius + { + get => _cornerRadius; + set => SetProperty(ref _cornerRadius, value); + } + + private Models.FilterMode _filterMode = Models.FilterMode.None; + private CornerRadius _cornerRadius = new CornerRadius(4); + } + public class TagCollectionAsList { - public List Tags + public List TagItems { get; set; } = []; + + public TagCollectionAsList(List tags) + { + foreach (var tag in tags) + TagItems.Add(new TagListItem() { Tag = tag, ToolTip = new TagToolTip(tag) }); + } + + public void ClearSelection() + { + foreach (var item in TagItems) + { + item.IsSelected = false; + item.CornerRadius = new CornerRadius(4); + } + } + + public void UpdateSelection(IList selectedItems) + { + var set = new HashSet(); + foreach (var item in selectedItems) + { + if (item is TagListItem tagItem) + set.Add(tagItem.Tag.Name); + } + + TagListItem last = null; + foreach (var item in TagItems) + { + item.IsSelected = set.Contains(item.Tag.Name); + if (item.IsSelected) + { + if (last is { IsSelected: true }) + { + last.CornerRadius = new CornerRadius(last.CornerRadius.TopLeft, 0); + item.CornerRadius = new CornerRadius(0, 4); + } + else + { + item.CornerRadius = new CornerRadius(4); + } + } + else + { + item.CornerRadius = new CornerRadius(4); + } + + last = item; + } + } } public class TagCollectionAsTree @@ -206,6 +330,46 @@ public void ToggleExpand(TagTreeNode node) } } + public void ClearSelection() + { + foreach (var node in Tree) + ClearSelectionRecursively(node); + } + + public void UpdateSelection(IList selectedItems) + { + var set = new HashSet(); + foreach (var item in selectedItems) + { + if (item is TagTreeNode node) + set.Add(node.FullPath); + } + + TagTreeNode last = null; + foreach (var row in Rows) + { + row.IsSelected = set.Contains(row.FullPath); + if (row.IsSelected) + { + if (last is { IsSelected: true }) + { + last.CornerRadius = new CornerRadius(last.CornerRadius.TopLeft, 0); + row.CornerRadius = new CornerRadius(0, 4); + } + else + { + row.CornerRadius = new CornerRadius(4); + } + } + else + { + row.CornerRadius = new CornerRadius(4); + } + + last = row; + } + } + private static void MakeTreeRows(List rows, List nodes) { foreach (var node in nodes) @@ -218,5 +382,17 @@ private static void MakeTreeRows(List rows, List nodes MakeTreeRows(rows, node.Children); } } + + private static void ClearSelectionRecursively(TagTreeNode node) + { + if (node.IsSelected) + { + node.IsSelected = false; + node.CornerRadius = new CornerRadius(4); + } + + foreach (var child in node.Children) + ClearSelectionRecursively(child); + } } } diff --git a/src/ViewModels/TextDiffContext.cs b/src/ViewModels/TextDiffContext.cs new file mode 100644 index 000000000..90c7df6e2 --- /dev/null +++ b/src/ViewModels/TextDiffContext.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public record TextDiffDisplayRange(int Start, int End); + + public record TextDiffSelectedChunk(double Y, double Height, int StartIdx, int EndIdx, bool Combined, bool IsOldSide) + { + public static bool IsChanged(TextDiffSelectedChunk oldValue, TextDiffSelectedChunk newValue) + { + if (newValue == null) + return oldValue != null; + + if (oldValue == null) + return true; + + return Math.Abs(newValue.Y - oldValue.Y) > 0.001 || + Math.Abs(newValue.Height - oldValue.Height) > 0.001 || + newValue.StartIdx != oldValue.StartIdx || + newValue.EndIdx != oldValue.EndIdx || + newValue.Combined != oldValue.Combined || + newValue.IsOldSide != oldValue.IsOldSide; + } + } + + public class TextDiffContext : ObservableObject + { + public Models.DiffOption Option => _option; + public Models.TextDiff Data => _data; + + public Vector ScrollOffset + { + get => _scrollOffset; + set => SetProperty(ref _scrollOffset, value); + } + + public BlockNavigation BlockNavigation + { + get => _blockNavigation; + set => SetProperty(ref _blockNavigation, value); + } + + public TextDiffDisplayRange DisplayRange + { + get => _displayRange; + set => SetProperty(ref _displayRange, value); + } + + public TextDiffSelectedChunk SelectedChunk + { + get => _selectedChunk; + set => SetProperty(ref _selectedChunk, value); + } + + public (int, int) FindRangeByIndex(List lines, int lineIdx) + { + var startIdx = -1; + var endIdx = -1; + + var normalLineCount = 0; + var modifiedLineCount = 0; + + for (int i = lineIdx; i >= 0; i--) + { + var line = lines[i]; + if (line.Type == Models.TextDiffLineType.Indicator) + { + startIdx = i; + break; + } + + if (line.Type == Models.TextDiffLineType.Normal) + { + normalLineCount++; + if (normalLineCount >= 2) + { + startIdx = i; + break; + } + } + else + { + normalLineCount = 0; + modifiedLineCount++; + } + } + + normalLineCount = lines[lineIdx].Type == Models.TextDiffLineType.Normal ? 1 : 0; + for (int i = lineIdx + 1; i < lines.Count; i++) + { + var line = lines[i]; + if (line.Type == Models.TextDiffLineType.Indicator) + { + endIdx = i; + break; + } + + if (line.Type == Models.TextDiffLineType.Normal) + { + normalLineCount++; + if (normalLineCount >= 2) + { + endIdx = i; + break; + } + } + else + { + normalLineCount = 0; + modifiedLineCount++; + } + } + + if (endIdx == -1) + endIdx = lines.Count - 1; + + return modifiedLineCount > 0 ? (startIdx, endIdx) : (-1, -1); + } + + public virtual bool IsSideBySide() + { + return false; + } + + public virtual TextDiffContext SwitchMode() + { + return null; + } + + protected void TryKeepPrevState(TextDiffContext prev, List lines) + { + var fastTest = prev != null && + prev._option.IsUnstaged == _option.IsUnstaged && + prev._option.Path.Equals(_option.Path, StringComparison.Ordinal) && + prev._option.OrgPath.Equals(_option.OrgPath, StringComparison.Ordinal) && + prev._option.Revisions.Count == _option.Revisions.Count; + + if (!fastTest) + { + _blockNavigation = new BlockNavigation(lines, 0); + return; + } + + for (int i = 0; i < _option.Revisions.Count; i++) + { + if (!prev._option.Revisions[i].Equals(_option.Revisions[i], StringComparison.Ordinal)) + { + _blockNavigation = new BlockNavigation(lines, 0); + return; + } + } + + _blockNavigation = new BlockNavigation(lines, prev._blockNavigation.GetCurrentBlockIndex()); + } + + protected Models.DiffOption _option = null; + protected Models.TextDiff _data = null; + protected Vector _scrollOffset = Vector.Zero; + protected BlockNavigation _blockNavigation = null; + + private TextDiffDisplayRange _displayRange = null; + private TextDiffSelectedChunk _selectedChunk = null; + } + + public class CombinedTextDiff : TextDiffContext + { + public CombinedTextDiff(Models.DiffOption option, Models.TextDiff diff, TextDiffContext previous = null) + { + _option = option; + _data = diff; + + TryKeepPrevState(previous, _data.Lines); + } + + public override TextDiffContext SwitchMode() + { + return new TwoSideTextDiff(_option, _data, this); + } + } + + public class TwoSideTextDiff : TextDiffContext + { + public List Old { get; } = []; + public List New { get; } = []; + + public TwoSideTextDiff(Models.DiffOption option, Models.TextDiff diff, TextDiffContext previous = null) + { + _option = option; + _data = diff; + + foreach (var line in diff.Lines) + { + switch (line.Type) + { + case Models.TextDiffLineType.Added: + New.Add(line); + break; + case Models.TextDiffLineType.Deleted: + Old.Add(line); + break; + default: + FillEmptyLines(); + Old.Add(line); + New.Add(line); + break; + } + } + + FillEmptyLines(); + TryKeepPrevState(previous, Old); + } + + public override bool IsSideBySide() + { + return true; + } + + public override TextDiffContext SwitchMode() + { + return new CombinedTextDiff(_option, _data, this); + } + + public void ConvertsToCombinedRange(ref int startLine, ref int endLine, bool isOldSide) + { + endLine = Math.Min(endLine, _data.Lines.Count - 1); + + var oneSide = isOldSide ? Old : New; + var firstContentLine = -1; + for (int i = startLine; i <= endLine; i++) + { + var line = oneSide[i]; + if (line.Type != Models.TextDiffLineType.None) + { + firstContentLine = i; + break; + } + } + + if (firstContentLine < 0) + return; + + var endContentLine = -1; + for (int i = Math.Min(endLine, oneSide.Count - 1); i >= startLine; i--) + { + var line = oneSide[i]; + if (line.Type != Models.TextDiffLineType.None) + { + endContentLine = i; + break; + } + } + + if (endContentLine < 0) + return; + + var firstContent = oneSide[firstContentLine]; + var endContent = oneSide[endContentLine]; + startLine = _data.Lines.IndexOf(firstContent); + endLine = _data.Lines.IndexOf(endContent); + } + + private void FillEmptyLines() + { + if (Old.Count < New.Count) + { + int diff = New.Count - Old.Count; + for (int i = 0; i < diff; i++) + Old.Add(new Models.TextDiffLine()); + } + else if (Old.Count > New.Count) + { + int diff = Old.Count - New.Count; + for (int i = 0; i < diff; i++) + New.Add(new Models.TextDiffLine()); + } + } + } +} diff --git a/src/ViewModels/TwoSideTextDiff.cs b/src/ViewModels/TwoSideTextDiff.cs deleted file mode 100644 index 3fb1e63b3..000000000 --- a/src/ViewModels/TwoSideTextDiff.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; - -using Avalonia; - -using CommunityToolkit.Mvvm.ComponentModel; - -namespace SourceGit.ViewModels -{ - public class TwoSideTextDiff : ObservableObject - { - public string File { get; set; } - public List Old { get; set; } = new List(); - public List New { get; set; } = new List(); - public int MaxLineNumber = 0; - - public Vector SyncScrollOffset - { - get => _syncScrollOffset; - set => SetProperty(ref _syncScrollOffset, value); - } - - public TwoSideTextDiff(Models.TextDiff diff, TwoSideTextDiff previous = null) - { - File = diff.File; - MaxLineNumber = diff.MaxLineNumber; - - foreach (var line in diff.Lines) - { - switch (line.Type) - { - case Models.TextDiffLineType.Added: - New.Add(line); - break; - case Models.TextDiffLineType.Deleted: - Old.Add(line); - break; - default: - FillEmptyLines(); - Old.Add(line); - New.Add(line); - break; - } - } - - FillEmptyLines(); - - if (previous != null && previous.File == File) - _syncScrollOffset = previous._syncScrollOffset; - } - - public void ConvertsToCombinedRange(Models.TextDiff combined, ref int startLine, ref int endLine, bool isOldSide) - { - endLine = Math.Min(endLine, combined.Lines.Count - 1); - - var oneSide = isOldSide ? Old : New; - var firstContentLine = -1; - for (int i = startLine; i <= endLine; i++) - { - var line = oneSide[i]; - if (line.Type != Models.TextDiffLineType.None) - { - firstContentLine = i; - break; - } - } - - if (firstContentLine < 0) - return; - - var endContentLine = -1; - for (int i = Math.Min(endLine, oneSide.Count - 1); i >= startLine; i--) - { - var line = oneSide[i]; - if (line.Type != Models.TextDiffLineType.None) - { - endContentLine = i; - break; - } - } - - if (endContentLine < 0) - return; - - var firstContent = oneSide[firstContentLine]; - var endContent = oneSide[endContentLine]; - startLine = combined.Lines.IndexOf(firstContent); - endLine = combined.Lines.IndexOf(endContent); - } - - private void FillEmptyLines() - { - if (Old.Count < New.Count) - { - int diff = New.Count - Old.Count; - for (int i = 0; i < diff; i++) - Old.Add(new Models.TextDiffLine()); - } - else if (Old.Count > New.Count) - { - int diff = Old.Count - New.Count; - for (int i = 0; i < diff; i++) - New.Add(new Models.TextDiffLine()); - } - } - - private Vector _syncScrollOffset = Vector.Zero; - } -} diff --git a/src/ViewModels/UpdateSubmodules.cs b/src/ViewModels/UpdateSubmodules.cs index 3d4d6d0fd..53aa8c17f 100644 --- a/src/ViewModels/UpdateSubmodules.cs +++ b/src/ViewModels/UpdateSubmodules.cs @@ -88,7 +88,7 @@ public override async Task Sure() return true; var log = _repo.CreateLog("Update Submodule"); - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); Use(log); await new Commands.Submodule(_repo.FullPath) @@ -96,7 +96,7 @@ public override async Task Sure() .UpdateAsync(targets, EnableInit, EnableRecursive, EnableRemote); log.Complete(); - _repo.SetWatcherEnabled(true); + _repo.MarkSubmodulesDirtyManually(); return true; } diff --git a/src/ViewModels/ViewLogs.cs b/src/ViewModels/ViewLogs.cs index 21ab81ab7..8dc3e9605 100644 --- a/src/ViewModels/ViewLogs.cs +++ b/src/ViewModels/ViewLogs.cs @@ -19,6 +19,7 @@ public CommandLog SelectedLog public ViewLogs(Repository repo) { _repo = repo; + _selectedLog = repo.Logs?.Count > 0 ? repo.Logs[0] : null; } public void ClearAll() diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 7ee2c80cd..1fa1797a8 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Avalonia.Collections; -using Avalonia.Controls; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -53,6 +53,23 @@ public void Refresh() Rows.AddRange(rows); } + public async Task UpdateStatusAsync(bool force, CancellationToken? token) + { + if (_isUpdatingStatus) + return; + + _isUpdatingStatus = true; + + // avoid collection was modified while enumerating. + var nodes = new List(); + nodes.AddRange(Preferences.Instance.RepositoryNodes); + + foreach (var node in nodes) + await node.UpdateStatusAsync(force, token); + + _isUpdatingStatus = false; + } + public void ToggleNodeIsExpanded(RepositoryNode node) { node.IsExpanded = !node.IsExpanded; @@ -83,35 +100,32 @@ public void ToggleNodeIsExpanded(RepositoryNode node) } } - public void OpenOrInitRepository(string path, RepositoryNode parent, bool bMoveExistedNode) + public async Task GetRepositoryRootAsync(string path) { - if (!Directory.Exists(path)) + if (!Preferences.Instance.IsGitConfigured()) { - if (File.Exists(path)) - path = Path.GetDirectoryName(path); - else - return; + App.RaiseException(string.Empty, App.Text("NotConfigured")); + return null; } - var isBare = new Commands.IsBareRepository(path).GetResultAsync().Result; - var repoRoot = path; - if (!isBare) + var root = path; + if (!Directory.Exists(root)) { - var test = new Commands.QueryRepositoryRootPath(path).GetResultAsync().Result; - if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut)) - { - InitRepository(path, parent, test.StdErr); - return; - } - - repoRoot = test.StdOut.Trim(); + if (File.Exists(root)) + root = Path.GetDirectoryName(root); + else + return null; } - var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(repoRoot, parent, bMoveExistedNode); - Refresh(); + var isBare = await new Commands.IsBareRepository(root).GetResultAsync(); + if (isBare) + return root; - var launcher = App.GetLauncher(); - launcher?.OpenRepositoryInTab(node, launcher.ActivePage); + var rs = await new Commands.QueryRepositoryRootPath(root).GetResultAsync(); + if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) + return null; + + return rs.StdOut.Trim(); } public void InitRepository(string path, RepositoryNode parent, string reason) @@ -127,6 +141,15 @@ public void InitRepository(string path, RepositoryNode parent, string reason) activePage.Popup = new Init(activePage.Node.Id, path, parent, reason); } + public async Task AddRepositoryAsync(string path, RepositoryNode parent, bool moveNode, bool open) + { + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, parent, moveNode); + await node.UpdateStatusAsync(false, null); + + if (open) + node.Open(); + } + public void Clone() { if (!Preferences.Instance.IsGitConfigured()) @@ -173,6 +196,22 @@ public void AddRootNode() activePage.Popup = new CreateGroup(null); } + public RepositoryNode FindNodeById(string id, RepositoryNode root = null) + { + var collection = (root == null) ? Preferences.Instance.RepositoryNodes : root.SubNodes; + foreach (var node in collection) + { + if (node.Id.Equals(id, StringComparison.Ordinal)) + return node; + + var sub = FindNodeById(id, node); + if (sub != null) + return sub; + } + + return null; + } + public RepositoryNode FindParentGroup(RepositoryNode node, RepositoryNode group = null) { var collection = (group == null) ? Preferences.Instance.RepositoryNodes : group.SubNodes; @@ -198,111 +237,6 @@ public void MoveNode(RepositoryNode from, RepositoryNode to) Refresh(); } - public ContextMenu CreateContextMenu(RepositoryNode node) - { - var menu = new ContextMenu(); - - if (!node.IsRepository && node.SubNodes.Count > 0) - { - var openAll = new MenuItem(); - openAll.Header = App.Text("Welcome.OpenAllInNode"); - openAll.Icon = App.CreateMenuIcon("Icons.Folder.Open"); - openAll.Click += (_, e) => - { - OpenAllInNode(App.GetLauncher(), node); - e.Handled = true; - }; - - menu.Items.Add(openAll); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - if (node.IsRepository) - { - var open = new MenuItem(); - open.Header = App.Text("Welcome.OpenOrInit"); - open.Icon = App.CreateMenuIcon("Icons.Folder.Open"); - open.Click += (_, e) => - { - App.GetLauncher()?.OpenRepositoryInTab(node, null); - e.Handled = true; - }; - - var explore = new MenuItem(); - explore.Header = App.Text("Repository.Explore"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.Click += (_, e) => - { - node.OpenInFileManager(); - e.Handled = true; - }; - - var terminal = new MenuItem(); - terminal.Header = App.Text("Repository.Terminal"); - terminal.Icon = App.CreateMenuIcon("Icons.Terminal"); - terminal.Click += (_, e) => - { - node.OpenTerminal(); - e.Handled = true; - }; - - menu.Items.Add(open); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(explore); - menu.Items.Add(terminal); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - else - { - var addSubFolder = new MenuItem(); - addSubFolder.Header = App.Text("Welcome.AddSubFolder"); - addSubFolder.Icon = App.CreateMenuIcon("Icons.Folder.Add"); - addSubFolder.Click += (_, e) => - { - node.AddSubFolder(); - e.Handled = true; - }; - menu.Items.Add(addSubFolder); - } - - var edit = new MenuItem(); - edit.Header = App.Text("Welcome.Edit"); - edit.Icon = App.CreateMenuIcon("Icons.Edit"); - edit.Click += (_, e) => - { - node.Edit(); - e.Handled = true; - }; - - var move = new MenuItem(); - move.Header = App.Text("Welcome.Move"); - move.Icon = App.CreateMenuIcon("Icons.MoveTo"); - move.Click += (_, e) => - { - var activePage = App.GetLauncher().ActivePage; - if (activePage != null && activePage.CanCreatePopup()) - activePage.Popup = new MoveRepositoryNode(node); - - e.Handled = true; - }; - - var delete = new MenuItem(); - delete.Header = App.Text("Welcome.Delete"); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); - delete.Click += (_, e) => - { - node.Delete(); - e.Handled = true; - }; - - menu.Items.Add(edit); - menu.Items.Add(move); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(delete); - - return menu; - } - private void ResetVisibility(RepositoryNode node) { node.IsVisible = true; @@ -355,17 +289,7 @@ private void MakeTreeRows(List rows, List nodes, } } - private void OpenAllInNode(Launcher launcher, RepositoryNode node) - { - foreach (var subNode in node.SubNodes) - { - if (subNode.IsRepository) - launcher.OpenRepositoryInTab(subNode, null); - else if (subNode.SubNodes.Count > 0) - OpenAllInNode(launcher, subNode); - } - } - private string _searchFilter = string.Empty; + private bool _isUpdatingStatus = false; } } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 154ab97cc..f7c131c65 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -1,23 +1,21 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Platform.Storage; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public record CommitMessageRecord(string subject) - { - public string Subject { get; set; } = subject; - } - public class WorkingCopy : ObservableObject, IDisposable { + public Repository Repository + { + get => _repo; + } + public bool IncludeUntracked { get => _repo.IncludeUntracked; @@ -73,6 +71,12 @@ public bool EnableSignOff set => _repo.Settings.EnableSignOffForCommit = value; } + public bool NoVerifyOnCommit + { + get => _repo.Settings.NoVerifyOnCommit; + set => _repo.Settings.NoVerifyOnCommit = value; + } + public bool UseAmend { get => _useAmend; @@ -91,9 +95,7 @@ public bool UseAmend return; } - CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, currentBranch.Head) - .GetResultAsync() - .Result; + CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, currentBranch.Head).GetResult(); } else { @@ -251,15 +253,17 @@ public void Dispose() _commitMessage = string.Empty; } - public void SetData(List changes) + public void SetData(List changes, CancellationToken cancellationToken) { if (!IsChanged(_cached, changes)) { // Just force refresh selected changes. Dispatcher.UIThread.Invoke(() => { - HasUnsolvedConflicts = _cached.Find(x => x.IsConflicted) != null; + if (cancellationToken.IsCancellationRequested) + return; + HasUnsolvedConflicts = _cached.Find(x => x.IsConflicted) != null; UpdateDetail(); UpdateInProgressState(); }); @@ -313,6 +317,9 @@ public void SetData(List changes) Dispatcher.UIThread.Invoke(() => { + if (cancellationToken.IsCancellationRequested) + return; + _isLoadingData = true; HasUnsolvedConflicts = hasConflict; VisibleUnstaged = visibleUnstaged; @@ -328,42 +335,78 @@ public void SetData(List changes) }); } - public void OpenWithDefaultEditor(Models.Change c) + public async Task StageChangesAsync(List changes, Models.Change next) { - var absPath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); - if (File.Exists(absPath)) - Native.OS.OpenWithDefaultEditor(absPath); + var canStaged = await GetCanStageChangesAsync(changes); + var count = canStaged.Count; + if (count == 0) + return; + + IsStaging = true; + _selectedUnstaged = next != null ? [next] : []; + + using var lockWatcher = _repo.LockWatcher(); + + var log = _repo.CreateLog("Stage"); + var pathSpecFile = Path.GetTempFileName(); + await using (var writer = new StreamWriter(pathSpecFile)) + { + foreach (var c in canStaged) + await writer.WriteLineAsync(c.Path); + } + + await new Commands.Add(_repo.FullPath, pathSpecFile).Use(log).ExecAsync(); + File.Delete(pathSpecFile); + log.Complete(); + + _repo.MarkWorkingCopyDirtyManually(); + IsStaging = false; } - public void StashAll(bool autoStart) + public async Task UnstageChangesAsync(List changes, Models.Change next) { - if (!_repo.CanCreatePopup()) + var count = changes.Count; + if (count == 0) return; - if (autoStart) - _repo.ShowAndStartPopup(new StashChanges(_repo, _cached, false)); - else - _repo.ShowPopup(new StashChanges(_repo, _cached, false)); - } + IsUnstaging = true; + _selectedStaged = next != null ? [next] : []; - public void StageSelected(Models.Change next) - { - StageChanges(_selectedUnstaged, next); - } + using var lockWatcher = _repo.LockWatcher(); - public void StageAll() - { - StageChanges(_visibleUnstaged, null); - } + var log = _repo.CreateLog("Unstage"); + if (_useAmend) + { + log.AppendLine("$ git update-index --index-info "); + await new Commands.UpdateIndexInfo(_repo.FullPath, changes).ExecAsync(); + } + else + { + var pathSpecFile = Path.GetTempFileName(); + await using (var writer = new StreamWriter(pathSpecFile)) + { + foreach (var c in changes) + { + await writer.WriteLineAsync(c.Path); + if (c.Index == Models.ChangeState.Renamed) + await writer.WriteLineAsync(c.OriginalPath); + } + } - public void UnstageSelected(Models.Change next) - { - UnstageChanges(_selectedStaged, next); + await new Commands.Reset(_repo.FullPath, pathSpecFile).Use(log).ExecAsync(); + File.Delete(pathSpecFile); + } + log.Complete(); + + _repo.MarkWorkingCopyDirtyManually(); + IsUnstaging = false; } - public void UnstageAll() + public async Task SaveChangesToPatchAsync(List changes, bool isUnstaged, string saveTo) { - UnstageChanges(_visibleStaged, null); + var succ = await Commands.SaveChangesAsPatch.ProcessLocalChangesAsync(_repo.FullPath, changes, isUnstaged, saveTo); + if (succ) + App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } public void Discard(List changes) @@ -377,9 +420,9 @@ public void ClearFilter() Filter = string.Empty; } - public async void UseTheirs(List changes) + public async Task UseTheirsAsync(List changes) { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); var files = new List(); var needStage = new List(); @@ -421,12 +464,11 @@ public async void UseTheirs(List changes) log.Complete(); _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); } - public async void UseMine(List changes) + public async Task UseMineAsync(List changes) { - _repo.SetWatcherEnabled(false); + using var lockWatcher = _repo.LockWatcher(); var files = new List(); var needStage = new List(); @@ -468,1448 +510,280 @@ public async void UseMine(List changes) log.Complete(); _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); } - public async Task UseExternalMergeTool(Models.Change change) + public async Task UseExternalMergeToolAsync(Models.Change change) { - var toolType = Preferences.Instance.ExternalMergeToolType; - var toolPath = Preferences.Instance.ExternalMergeToolPath; - var file = change?.Path; - return await new Commands.MergeTool(_repo.FullPath, toolType, toolPath, file).OpenAsync(); + return await new Commands.MergeTool(_repo.FullPath, change?.Path).OpenAsync(); } - public void ContinueMerge() + public void UseExternalDiffTool(Models.Change change, bool isUnstaged) { - IsCommitting = true; + new Commands.DiffTool(_repo.FullPath, new Models.DiffOption(change, isUnstaged)).Open(); + } + public async Task ContinueMergeAsync() + { if (_inProgressContext != null) { - _repo.SetWatcherEnabled(false); - Task.Run(async () => - { - var mergeMsgFile = Path.Combine(_repo.GitDir, "MERGE_MSG"); - if (File.Exists(mergeMsgFile) && !string.IsNullOrWhiteSpace(_commitMessage)) - await File.WriteAllTextAsync(mergeMsgFile, _commitMessage); + using var lockWatcher = _repo.LockWatcher(); + IsCommitting = true; - var succ = await _inProgressContext.ContinueAsync(); - await Dispatcher.UIThread.InvokeAsync(() => - { - if (succ) - CommitMessage = string.Empty; + var mergeMsgFile = Path.Combine(_repo.GitDir, "MERGE_MSG"); + if (File.Exists(mergeMsgFile) && !string.IsNullOrWhiteSpace(_commitMessage)) + await File.WriteAllTextAsync(mergeMsgFile, _commitMessage); - _repo.SetWatcherEnabled(true); - IsCommitting = false; - }); - }); + await _inProgressContext.ContinueAsync(); + CommitMessage = string.Empty; + IsCommitting = false; } else { _repo.MarkWorkingCopyDirtyManually(); - IsCommitting = false; } } - public void SkipMerge() + public async Task SkipMergeAsync() { - IsCommitting = true; - if (_inProgressContext != null) { - _repo.SetWatcherEnabled(false); - Task.Run(async () => - { - var succ = await _inProgressContext.SkipAsync(); - await Dispatcher.UIThread.InvokeAsync(() => - { - if (succ) - CommitMessage = string.Empty; + using var lockWatcher = _repo.LockWatcher(); + IsCommitting = true; - _repo.SetWatcherEnabled(true); - IsCommitting = false; - }); - }); + await _inProgressContext.SkipAsync(); + CommitMessage = string.Empty; + IsCommitting = false; } else { _repo.MarkWorkingCopyDirtyManually(); - IsCommitting = false; } } - public void AbortMerge() + public async Task AbortMergeAsync() { - IsCommitting = true; - if (_inProgressContext != null) { - _repo.SetWatcherEnabled(false); - Task.Run(async () => - { - var succ = await _inProgressContext.AbortAsync(); - await Dispatcher.UIThread.InvokeAsync(() => - { - if (succ) - CommitMessage = string.Empty; + using var lockWatcher = _repo.LockWatcher(); + IsCommitting = true; - _repo.SetWatcherEnabled(true); - IsCommitting = false; - }); - }); + await _inProgressContext.AbortAsync(); + CommitMessage = string.Empty; + IsCommitting = false; } else { _repo.MarkWorkingCopyDirtyManually(); - IsCommitting = false; } } - public void Commit() + public void ApplyCommitMessageTemplate(Models.CommitTemplate tmpl) { - DoCommit(false, false); + CommitMessage = tmpl.Apply(_repo.CurrentBranch, _staged); } - public void CommitWithAutoStage() + public async Task ClearCommitMessageHistoryAsync() { - DoCommit(true, false); + var sure = await App.AskConfirmAsync(App.Text("WorkingCopy.ClearCommitHistories.Confirm")); + if (sure) + _repo.Settings.CommitMessages.Clear(); } - public void CommitWithPush() + public async Task CommitAsync(bool autoStage, bool autoPush) { - DoCommit(false, true); - } + if (string.IsNullOrWhiteSpace(_commitMessage)) + return; - public ContextMenu CreateContextMenuForUnstagedChanges(string selectedSingleFolder) - { - if (_selectedUnstaged == null || _selectedUnstaged.Count == 0) - return null; + if (!_repo.CanCreatePopup()) + { + App.RaiseException(_repo.FullPath, "Repository has an unfinished job! Please wait!"); + return; + } - var hasSelectedFolder = !string.IsNullOrEmpty(selectedSingleFolder); - var menu = new ContextMenu(); - if (_selectedUnstaged.Count == 1) + if (autoStage && HasUnsolvedConflicts) + { + App.RaiseException(_repo.FullPath, "Repository has unsolved conflict(s). Auto-stage and commit is disabled!"); + return; + } + + if (_repo.CurrentBranch is { IsDetachedHead: true }) + { + var msg = App.Text("WorkingCopy.ConfirmCommitWithDetachedHead"); + var sure = await App.AskConfirmAsync(msg); + if (!sure) + return; + } + + if (!string.IsNullOrEmpty(_filter) && _staged.Count > _visibleStaged.Count) { - var change = _selectedUnstaged[0]; - var path = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + var msg = App.Text("WorkingCopy.ConfirmCommitWithFilter", _staged.Count, _visibleStaged.Count, _staged.Count - _visibleStaged.Count); + var sure = await App.AskConfirmAsync(msg); + if (!sure) + return; + } - if (!change.IsConflicted || change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified) + if (!_useAmend) + { + if ((!autoStage && _staged.Count == 0) || (autoStage && _cached.Count == 0)) { - var openMerger = new MenuItem(); - openMerger.Header = App.Text("OpenInExternalMergeTool"); - openMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openMerger.Click += async (_, e) => - { - if (change.IsConflicted) - { - await UseExternalMergeTool(change); - } - else - { - var toolType = Preferences.Instance.ExternalMergeToolType; - var toolPath = Preferences.Instance.ExternalMergeToolPath; - var opt = new Models.DiffOption(change, true); - new Commands.DiffTool(_repo.FullPath, toolType, toolPath, opt).Open(); - } + var rs = await App.AskConfirmEmptyCommitAsync(_cached.Count > 0); + if (rs == Models.ConfirmEmptyCommitResult.Cancel) + return; - e.Handled = true; - }; - menu.Items.Add(openMerger); + if (rs == Models.ConfirmEmptyCommitResult.StageAllAndCommit) + autoStage = true; } + } - var openWith = new MenuItem(); - openWith.Header = App.Text("OpenWith"); - openWith.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWith.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O"; - openWith.IsEnabled = File.Exists(path); - openWith.Click += (_, e) => - { - OpenWithDefaultEditor(change); - e.Handled = true; - }; - menu.Items.Add(openWith); - - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(path) || Directory.Exists(path); - explore.Click += (_, e) => - { - var target = hasSelectedFolder ? Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder) : path; - Native.OS.OpenInFileManager(target, true); - e.Handled = true; - }; - menu.Items.Add(explore); - menu.Items.Add(new MenuItem() { Header = "-" }); - - if (change.IsConflicted) - { - var useTheirs = new MenuItem(); - useTheirs.Icon = App.CreateMenuIcon("Icons.Incoming"); - useTheirs.Click += (_, e) => - { - UseTheirs(_selectedUnstaged); - e.Handled = true; - }; + using var lockWatcher = _repo.LockWatcher(); + IsCommitting = true; + _repo.Settings.PushCommitMessage(_commitMessage); - var useMine = new MenuItem(); - useMine.Icon = App.CreateMenuIcon("Icons.Local"); - useMine.Click += (_, e) => - { - UseMine(_selectedUnstaged); - e.Handled = true; - }; + if (autoStage && _unstaged.Count > 0) + await StageChangesAsync(_unstaged, null); - switch (_inProgressContext) - { - case CherryPickInProgress cherryPick: - useTheirs.Header = App.Text("FileCM.ResolveUsing", cherryPick.HeadName); - useMine.Header = App.Text("FileCM.ResolveUsing", _repo.CurrentBranch.Name); - break; - case RebaseInProgress rebase: - useTheirs.Header = App.Text("FileCM.ResolveUsing", rebase.HeadName); - useMine.Header = App.Text("FileCM.ResolveUsing", rebase.BaseName); - break; - case RevertInProgress revert: - useTheirs.Header = App.Text("FileCM.ResolveUsing", $"{revert.Head.SHA.AsSpan(0, 10)} (revert)"); - useMine.Header = App.Text("FileCM.ResolveUsing", _repo.CurrentBranch.Name); - break; - case MergeInProgress merge: - useTheirs.Header = App.Text("FileCM.ResolveUsing", merge.SourceName); - useMine.Header = App.Text("FileCM.ResolveUsing", _repo.CurrentBranch.Name); - break; - default: - useTheirs.Header = App.Text("FileCM.UseTheirs"); - useMine.Header = App.Text("FileCM.UseMine"); - break; - } + var log = _repo.CreateLog("Commit"); + var succ = await new Commands.Commit(_repo.FullPath, _commitMessage, EnableSignOff, NoVerifyOnCommit, _useAmend, _resetAuthor) + .Use(log) + .RunAsync() + .ConfigureAwait(false); - menu.Items.Add(useTheirs); - menu.Items.Add(useMine); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - else + log.Complete(); + + if (succ) + { + CommitMessage = string.Empty; + UseAmend = false; + if (autoPush && _repo.Remotes.Count > 0) { - var stage = new MenuItem(); - stage.Header = App.Text("FileCM.Stage"); - stage.Icon = App.CreateMenuIcon("Icons.File.Add"); - stage.Tag = "Enter/Space"; - stage.Click += (_, e) => + Models.Branch pushBranch = null; + if (_repo.CurrentBranch == null) { - StageChanges(_selectedUnstaged, null); - e.Handled = true; - }; + var currentBranchName = await new Commands.QueryCurrentBranch(_repo.FullPath).GetResultAsync(); + pushBranch = new Models.Branch() { Name = currentBranchName }; + } - var discard = new MenuItem(); - discard.Header = App.Text("FileCM.Discard"); - discard.Icon = App.CreateMenuIcon("Icons.Undo"); - discard.Tag = "Back/Delete"; - discard.Click += (_, e) => - { - Discard(_selectedUnstaged); - e.Handled = true; - }; + if (_repo.CanCreatePopup()) + await _repo.ShowAndStartPopupAsync(new Push(_repo, pushBranch)); + } + } - var stash = new MenuItem(); - stash.Header = App.Text("FileCM.Stash"); - stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add"); - stash.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new StashChanges(_repo, _selectedUnstaged, true)); + _repo.MarkBranchesDirtyManually(); + IsCommitting = false; + } - e.Handled = true; - }; + private List GetVisibleChanges(List changes) + { + if (string.IsNullOrEmpty(_filter)) + return changes; - var patch = new MenuItem(); - patch.Header = App.Text("FileCM.SaveAsPatch"); - patch.Icon = App.CreateMenuIcon("Icons.Diff"); - patch.Click += async (_, e) => - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == null) - return; + var visible = new List(); - var options = new FilePickerSaveOptions(); - options.Title = App.Text("FileCM.SaveAsPatch"); - options.DefaultExtension = ".patch"; - options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; + foreach (var c in changes) + { + if (c.Path.Contains(_filter, StringComparison.OrdinalIgnoreCase)) + visible.Add(c); + } - var storageFile = await storageProvider.SaveFilePickerAsync(options); - if (storageFile != null) - { - var succ = await Commands.SaveChangesAsPatch.ProcessLocalChangesAsync(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath); - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } + return visible; + } - e.Handled = true; - }; + private async Task> GetCanStageChangesAsync(List changes) + { + if (!HasUnsolvedConflicts) + return changes; - var assumeUnchanged = new MenuItem(); - assumeUnchanged.Header = App.Text("FileCM.AssumeUnchanged"); - assumeUnchanged.Icon = App.CreateMenuIcon("Icons.File.Ignore"); - assumeUnchanged.IsVisible = change.WorkTree != Models.ChangeState.Untracked; - assumeUnchanged.Click += async (_, e) => + var outs = new List(); + foreach (var c in changes) + { + if (c.IsConflicted) + { + var isResolved = c.ConflictReason switch { - var log = _repo.CreateLog("Assume File Unchanged"); - await new Commands.AssumeUnchanged(_repo.FullPath, change.Path, true).Use(log).ExecAsync(); - log.Complete(); - e.Handled = true; + Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified => + await new Commands.IsConflictResolved(_repo.FullPath, c).GetResultAsync(), + _ => false, }; - menu.Items.Add(stage); - menu.Items.Add(discard); - menu.Items.Add(stash); - menu.Items.Add(patch); - menu.Items.Add(assumeUnchanged); - menu.Items.Add(new MenuItem() { Header = "-" }); + if (!isResolved) + continue; + } - var extension = Path.GetExtension(change.Path); - var hasExtra = false; - if (change.WorkTree == Models.ChangeState.Untracked) - { - var addToIgnore = new MenuItem(); - addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore"); - addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore"); + outs.Add(c); + } - if (hasSelectedFolder) - { - var ignoreFolder = new MenuItem(); - ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder"); - ignoreFolder.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new AddToIgnore(_repo, $"{selectedSingleFolder}/")); - e.Handled = true; - }; - addToIgnore.Items.Add(ignoreFolder); - } - else - { - var isRooted = change.Path.IndexOf('/') <= 0; - var singleFile = new MenuItem(); - singleFile.Header = App.Text("WorkingCopy.AddToGitIgnore.SingleFile"); - singleFile.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new AddToIgnore(_repo, change.Path)); - e.Handled = true; - }; - addToIgnore.Items.Add(singleFile); - - if (!string.IsNullOrEmpty(extension)) - { - var byExtension = new MenuItem(); - byExtension.Header = App.Text("WorkingCopy.AddToGitIgnore.Extension", extension); - byExtension.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new AddToIgnore(_repo, $"*{extension}")); - e.Handled = true; - }; - addToIgnore.Items.Add(byExtension); - - var byExtensionInSameFolder = new MenuItem(); - byExtensionInSameFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.ExtensionInSameFolder", extension); - byExtensionInSameFolder.IsVisible = !isRooted; - byExtensionInSameFolder.Click += (_, e) => - { - var dir = Path.GetDirectoryName(change.Path)!.Replace('\\', '/').TrimEnd('/'); - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new AddToIgnore(_repo, $"{dir}/*{extension}")); - e.Handled = true; - }; - addToIgnore.Items.Add(byExtensionInSameFolder); - } - } + return outs; + } - menu.Items.Add(addToIgnore); - hasExtra = true; - } - else if (hasSelectedFolder) - { - var addToIgnore = new MenuItem(); - addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore"); - addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore"); + private List GetStagedChanges() + { + if (_useAmend) + { + var head = new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResult(); + return new Commands.QueryStagedChangesWithAmend(_repo.FullPath, head.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : $"{head.SHA}^").GetResult(); + } - var ignoreFolder = new MenuItem(); - ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder"); - ignoreFolder.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new AddToIgnore(_repo, $"{selectedSingleFolder}/")); - e.Handled = true; - }; - addToIgnore.Items.Add(ignoreFolder); - - menu.Items.Add(addToIgnore); - hasExtra = true; - } + var rs = new List(); + foreach (var c in _cached) + { + if (c.Index != Models.ChangeState.None) + rs.Add(c); + } + return rs; + } - if (_repo.IsLFSEnabled()) - { - var lfs = new MenuItem(); - lfs.Header = App.Text("GitLFS"); - lfs.Icon = App.CreateMenuIcon("Icons.LFS"); + private void UpdateDetail() + { + if (_selectedUnstaged.Count == 1) + SetDetail(_selectedUnstaged[0], true); + else if (_selectedStaged.Count == 1) + SetDetail(_selectedStaged[0], false); + else + SetDetail(null, false); + } - var isLFSFiltered = new Commands.IsLFSFiltered(_repo.FullPath, change.Path).GetResultAsync().Result; - if (!isLFSFiltered) - { - var filename = Path.GetFileName(change.Path); - var lfsTrackThisFile = new MenuItem(); - lfsTrackThisFile.Header = App.Text("GitLFS.Track", filename); - lfsTrackThisFile.Click += async (_, e) => - { - await _repo.TrackLFSFileAsync(filename, true); - e.Handled = true; - }; - lfs.Items.Add(lfsTrackThisFile); - - if (!string.IsNullOrEmpty(extension)) - { - var lfsTrackByExtension = new MenuItem(); - lfsTrackByExtension.Header = App.Text("GitLFS.TrackByExtension", extension); - lfsTrackByExtension.Click += async (_, e) => - { - await _repo.TrackLFSFileAsync($"*{extension}", false); - e.Handled = true; - }; - lfs.Items.Add(lfsTrackByExtension); - } - - lfs.Items.Add(new MenuItem() { Header = "-" }); - } + private void UpdateInProgressState() + { + var oldType = _inProgressContext != null ? _inProgressContext.GetType() : null; - var lfsLock = new MenuItem(); - lfsLock.Header = App.Text("GitLFS.Locks.Lock"); - lfsLock.Icon = App.CreateMenuIcon("Icons.Lock"); - lfsLock.IsEnabled = _repo.Remotes.Count > 0; - if (_repo.Remotes.Count == 1) - { - lfsLock.Click += async (_, e) => - { - await _repo.LockLFSFileAsync(_repo.Remotes[0].Name, change.Path); - e.Handled = true; - }; - } - else - { - foreach (var remote in _repo.Remotes) - { - var remoteName = remote.Name; - var lockRemote = new MenuItem(); - lockRemote.Header = remoteName; - lockRemote.Click += async (_, e) => - { - await _repo.LockLFSFileAsync(remoteName, change.Path); - e.Handled = true; - }; - lfsLock.Items.Add(lockRemote); - } - } - lfs.Items.Add(lfsLock); + if (File.Exists(Path.Combine(_repo.GitDir, "CHERRY_PICK_HEAD"))) + InProgressContext = new CherryPickInProgress(_repo); + else if (Directory.Exists(Path.Combine(_repo.GitDir, "rebase-merge")) || Directory.Exists(Path.Combine(_repo.GitDir, "rebase-apply"))) + InProgressContext = new RebaseInProgress(_repo); + else if (File.Exists(Path.Combine(_repo.GitDir, "REVERT_HEAD"))) + InProgressContext = new RevertInProgress(_repo); + else if (File.Exists(Path.Combine(_repo.GitDir, "MERGE_HEAD"))) + InProgressContext = new MergeInProgress(_repo); + else + InProgressContext = null; - var lfsUnlock = new MenuItem(); - lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock"); - lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock"); - lfsUnlock.IsEnabled = _repo.Remotes.Count > 0; - if (_repo.Remotes.Count == 1) - { - lfsUnlock.Click += async (_, e) => - { - await _repo.UnlockLFSFileAsync(_repo.Remotes[0].Name, change.Path, false, true); - e.Handled = true; - }; - } - else - { - foreach (var remote in _repo.Remotes) - { - var remoteName = remote.Name; - var unlockRemote = new MenuItem(); - unlockRemote.Header = remoteName; - unlockRemote.Click += async (_, e) => - { - await _repo.UnlockLFSFileAsync(remoteName, change.Path, false, true); - e.Handled = true; - }; - lfsUnlock.Items.Add(unlockRemote); - } - } - lfs.Items.Add(lfsUnlock); + if (_inProgressContext != null && _inProgressContext.GetType() == oldType && !string.IsNullOrEmpty(_commitMessage)) + return; - menu.Items.Add(lfs); - hasExtra = true; - } + if (LoadCommitMessageFromFile(Path.Combine(_repo.GitDir, "MERGE_MSG"))) + return; - if (hasExtra) - menu.Items.Add(new MenuItem() { Header = "-" }); - } + if (_inProgressContext is not RebaseInProgress { } rebasing) + return; - if (hasSelectedFolder) - { - var history = new MenuItem(); - history.Header = App.Text("DirHistories"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, e) => - { - App.ShowWindow(new DirHistories(_repo, selectedSingleFolder)); - e.Handled = true; - }; + if (LoadCommitMessageFromFile(Path.Combine(_repo.GitDir, "rebase-merge", "message"))) + return; - menu.Items.Add(history); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - else if (change.WorkTree is not (Models.ChangeState.Untracked or Models.ChangeState.Added)) - { - var history = new MenuItem(); - history.Header = App.Text("FileHistory"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, e) => - { - App.ShowWindow(new FileHistories(_repo, change.Path)); - e.Handled = true; - }; + CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, rebasing.StoppedAt.SHA).GetResult(); + } - var blame = new MenuItem(); - blame.Header = App.Text("Blame") + " (HEAD-only)"; - blame.Icon = App.CreateMenuIcon("Icons.Blame"); - blame.Click += async (_, ev) => - { - var commit = await new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResultAsync(); - App.ShowWindow(new Blame(_repo.FullPath, change.Path, commit)); - ev.Handled = true; - }; + private bool LoadCommitMessageFromFile(string file) + { + if (!File.Exists(file)) + return false; - menu.Items.Add(history); - menu.Items.Add(blame); - menu.Items.Add(new MenuItem() { Header = "-" }); - } + var msg = File.ReadAllText(file).Trim(); + if (string.IsNullOrEmpty(msg)) + return false; - var copy = new MenuItem(); - copy.Header = App.Text("CopyPath"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copy.Click += async (_, e) => - { - await App.CopyTextAsync(hasSelectedFolder ? selectedSingleFolder : change.Path); - e.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(hasSelectedFolder ? Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder) : path); - e.Handled = true; - }; - - menu.Items.Add(copy); - menu.Items.Add(copyFullPath); - } - else - { - var hasConflicts = false; - var hasNonConflicts = false; - foreach (var change in _selectedUnstaged) - { - if (change.IsConflicted) - hasConflicts = true; - else - hasNonConflicts = true; - } - - if (hasConflicts) - { - if (hasNonConflicts) - { - App.RaiseException(_repo.FullPath, "Selection contains both conflict and non-conflict changes!"); - return null; - } - - var useTheirs = new MenuItem(); - useTheirs.Icon = App.CreateMenuIcon("Icons.Incoming"); - useTheirs.Click += (_, e) => - { - UseTheirs(_selectedUnstaged); - e.Handled = true; - }; - - var useMine = new MenuItem(); - useMine.Icon = App.CreateMenuIcon("Icons.Local"); - useMine.Click += (_, e) => - { - UseMine(_selectedUnstaged); - e.Handled = true; - }; - - switch (_inProgressContext) - { - case CherryPickInProgress cherryPick: - useTheirs.Header = App.Text("FileCM.ResolveUsing", cherryPick.HeadName); - useMine.Header = App.Text("FileCM.ResolveUsing", _repo.CurrentBranch.Name); - break; - case RebaseInProgress rebase: - useTheirs.Header = App.Text("FileCM.ResolveUsing", rebase.HeadName); - useMine.Header = App.Text("FileCM.ResolveUsing", rebase.BaseName); - break; - case RevertInProgress revert: - useTheirs.Header = App.Text("FileCM.ResolveUsing", $"{revert.Head.SHA.AsSpan(0, 10)} (revert)"); - useMine.Header = App.Text("FileCM.ResolveUsing", _repo.CurrentBranch.Name); - break; - case MergeInProgress merge: - useTheirs.Header = App.Text("FileCM.ResolveUsing", merge.SourceName); - useMine.Header = App.Text("FileCM.ResolveUsing", _repo.CurrentBranch.Name); - break; - default: - useTheirs.Header = App.Text("FileCM.UseTheirs"); - useMine.Header = App.Text("FileCM.UseMine"); - break; - } - - menu.Items.Add(useTheirs); - menu.Items.Add(useMine); - return menu; - } - - if (hasSelectedFolder) - { - var dir = Path.Combine(_repo.FullPath, selectedSingleFolder); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = Directory.Exists(dir); - explore.Click += (_, e) => - { - Native.OS.OpenInFileManager(dir, true); - e.Handled = true; - }; - menu.Items.Add(explore); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var stage = new MenuItem(); - stage.Header = App.Text("FileCM.StageMulti", _selectedUnstaged.Count); - stage.Icon = App.CreateMenuIcon("Icons.File.Add"); - stage.Tag = "Enter/Space"; - stage.Click += (_, e) => - { - StageChanges(_selectedUnstaged, null); - e.Handled = true; - }; - - var discard = new MenuItem(); - discard.Header = App.Text("FileCM.DiscardMulti", _selectedUnstaged.Count); - discard.Icon = App.CreateMenuIcon("Icons.Undo"); - discard.Tag = "Back/Delete"; - discard.Click += (_, e) => - { - Discard(_selectedUnstaged); - e.Handled = true; - }; - - var stash = new MenuItem(); - stash.Header = App.Text("FileCM.StashMulti", _selectedUnstaged.Count); - stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add"); - stash.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new StashChanges(_repo, _selectedUnstaged, true)); - - e.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = App.Text("FileCM.SaveAsPatch"); - patch.Icon = App.CreateMenuIcon("Icons.Diff"); - patch.Click += async (_, e) => - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == 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 storageProvider.SaveFilePickerAsync(options); - if (storageFile != null) - { - var succ = await Commands.SaveChangesAsPatch.ProcessLocalChangesAsync(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath); - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } - - e.Handled = true; - }; - - menu.Items.Add(stage); - menu.Items.Add(discard); - menu.Items.Add(stash); - menu.Items.Add(patch); - - if (hasSelectedFolder) - { - var ignoreFolder = new MenuItem(); - ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder"); - ignoreFolder.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new AddToIgnore(_repo, $"{selectedSingleFolder}/")); - e.Handled = true; - }; - - var addToIgnore = new MenuItem(); - addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore"); - addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore"); - addToIgnore.Items.Add(ignoreFolder); - - var history = new MenuItem(); - history.Header = App.Text("DirHistories"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, e) => - { - App.ShowWindow(new DirHistories(_repo, selectedSingleFolder)); - e.Handled = true; - }; - - var copy = new MenuItem(); - copy.Header = App.Text("CopyPath"); - copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copy.Click += async (_, e) => - { - await App.CopyTextAsync(selectedSingleFolder); - e.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder)); - e.Handled = true; - }; - - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(addToIgnore); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(history); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copy); - menu.Items.Add(copyFullPath); - } - } - - return menu; - } - - public ContextMenu CreateContextMenuForStagedChanges(string selectedSingleFolder) - { - if (_selectedStaged == null || _selectedStaged.Count == 0) - return null; - - var menu = new ContextMenu(); - - MenuItem ai = null; - var services = _repo.GetPreferredOpenAIServices(); - if (services.Count > 0) - { - ai = new MenuItem(); - ai.Icon = App.CreateMenuIcon("Icons.AIAssist"); - ai.Header = App.Text("ChangeCM.GenerateCommitMessage"); - - if (services.Count == 1) - { - ai.Click += async (_, e) => - { - await App.ShowDialog(new AIAssistant(_repo, services[0], _selectedStaged, t => CommitMessage = t)); - e.Handled = true; - }; - } - else - { - foreach (var service in services) - { - var dup = service; - - var item = new MenuItem(); - item.Header = service.Name; - item.Click += async (_, e) => - { - await App.ShowDialog(new AIAssistant(_repo, dup, _selectedStaged, t => CommitMessage = t)); - e.Handled = true; - }; - - ai.Items.Add(item); - } - } - } - - var hasSelectedFolder = !string.IsNullOrEmpty(selectedSingleFolder); - if (_selectedStaged.Count == 1) - { - var change = _selectedStaged[0]; - var path = Native.OS.GetAbsPath(_repo.FullPath, change.Path); - - var openWithMerger = new MenuItem(); - openWithMerger.Header = App.Text("OpenInExternalMergeTool"); - openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openWithMerger.Click += (_, ev) => - { - var toolType = Preferences.Instance.ExternalMergeToolType; - var toolPath = Preferences.Instance.ExternalMergeToolPath; - var opt = new Models.DiffOption(change, false); - new Commands.DiffTool(_repo.FullPath, toolType, toolPath, opt).Open(); - ev.Handled = true; - }; - - var openWith = new MenuItem(); - openWith.Header = App.Text("OpenWith"); - openWith.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWith.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O"; - openWith.IsEnabled = File.Exists(path); - openWith.Click += (_, e) => - { - OpenWithDefaultEditor(change); - e.Handled = true; - }; - - var explore = new MenuItem(); - explore.IsEnabled = File.Exists(path) || Directory.Exists(path); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.Click += (_, e) => - { - var target = hasSelectedFolder ? Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder) : path; - Native.OS.OpenInFileManager(target, true); - e.Handled = true; - }; - - var unstage = new MenuItem(); - unstage.Header = App.Text("FileCM.Unstage"); - unstage.Icon = App.CreateMenuIcon("Icons.File.Remove"); - unstage.Tag = "Enter/Space"; - unstage.Click += (_, e) => - { - UnstageChanges(_selectedStaged, null); - e.Handled = true; - }; - - var stash = new MenuItem(); - stash.Header = App.Text("FileCM.Stash"); - stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add"); - stash.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new StashChanges(_repo, _selectedStaged, true)); - - e.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = App.Text("FileCM.SaveAsPatch"); - patch.Icon = App.CreateMenuIcon("Icons.Diff"); - patch.Click += async (_, e) => - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == 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 storageProvider.SaveFilePickerAsync(options); - if (storageFile != null) - { - var succ = await Commands.SaveChangesAsPatch.ProcessLocalChangesAsync(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath); - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } - - e.Handled = true; - }; - - menu.Items.Add(openWithMerger); - menu.Items.Add(openWith); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(unstage); - menu.Items.Add(stash); - menu.Items.Add(patch); - menu.Items.Add(new MenuItem() { Header = "-" }); - - if (_repo.IsLFSEnabled()) - { - var lfs = new MenuItem(); - lfs.Header = App.Text("GitLFS"); - lfs.Icon = App.CreateMenuIcon("Icons.LFS"); - - var lfsLock = new MenuItem(); - lfsLock.Header = App.Text("GitLFS.Locks.Lock"); - lfsLock.Icon = App.CreateMenuIcon("Icons.Lock"); - lfsLock.IsEnabled = _repo.Remotes.Count > 0; - if (_repo.Remotes.Count == 1) - { - lfsLock.Click += async (_, e) => - { - await _repo.LockLFSFileAsync(_repo.Remotes[0].Name, change.Path); - e.Handled = true; - }; - } - else - { - foreach (var remote in _repo.Remotes) - { - var remoteName = remote.Name; - var lockRemote = new MenuItem(); - lockRemote.Header = remoteName; - lockRemote.Click += async (_, e) => - { - await _repo.LockLFSFileAsync(remoteName, change.Path); - e.Handled = true; - }; - lfsLock.Items.Add(lockRemote); - } - } - lfs.Items.Add(lfsLock); - - var lfsUnlock = new MenuItem(); - lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock"); - lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock"); - lfsUnlock.IsEnabled = _repo.Remotes.Count > 0; - if (_repo.Remotes.Count == 1) - { - lfsUnlock.Click += async (_, e) => - { - await _repo.UnlockLFSFileAsync(_repo.Remotes[0].Name, change.Path, false, true); - e.Handled = true; - }; - } - else - { - foreach (var remote in _repo.Remotes) - { - var remoteName = remote.Name; - var unlockRemote = new MenuItem(); - unlockRemote.Header = remoteName; - unlockRemote.Click += async (_, e) => - { - await _repo.UnlockLFSFileAsync(remoteName, change.Path, false, true); - e.Handled = true; - }; - lfsUnlock.Items.Add(unlockRemote); - } - } - lfs.Items.Add(lfsUnlock); - - menu.Items.Add(lfs); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - if (ai != null) - { - menu.Items.Add(ai); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - if (hasSelectedFolder) - { - var history = new MenuItem(); - history.Header = App.Text("DirHistories"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, e) => - { - App.ShowWindow(new DirHistories(_repo, selectedSingleFolder)); - e.Handled = true; - }; - - menu.Items.Add(history); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - else if (change.Index is not (Models.ChangeState.Added or Models.ChangeState.Renamed)) - { - var history = new MenuItem(); - history.Header = App.Text("FileHistory"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, e) => - { - App.ShowWindow(new FileHistories(_repo, change.Path)); - e.Handled = true; - }; - - var blame = new MenuItem(); - blame.Header = App.Text("Blame") + " (HEAD-only)"; - blame.Icon = App.CreateMenuIcon("Icons.Blame"); - blame.Click += async (_, e) => - { - var commit = await new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResultAsync(); - App.ShowWindow(new Blame(_repo.FullPath, change.Path, commit)); - e.Handled = true; - }; - - menu.Items.Add(history); - menu.Items.Add(blame); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, e) => - { - await App.CopyTextAsync(hasSelectedFolder ? selectedSingleFolder : change.Path); - e.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - var target = hasSelectedFolder ? Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder) : path; - await App.CopyTextAsync(target); - e.Handled = true; - }; - - menu.Items.Add(copyPath); - menu.Items.Add(copyFullPath); - } - else - { - if (hasSelectedFolder) - { - var dir = Path.Combine(_repo.FullPath, selectedSingleFolder); - var explore = new MenuItem(); - explore.IsEnabled = Directory.Exists(dir); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.Click += (_, e) => - { - Native.OS.OpenInFileManager(dir, true); - e.Handled = true; - }; - - menu.Items.Add(explore); - menu.Items.Add(new MenuItem() { Header = "-" }); - } - - var unstage = new MenuItem(); - unstage.Header = App.Text("FileCM.UnstageMulti", _selectedStaged.Count); - unstage.Icon = App.CreateMenuIcon("Icons.File.Remove"); - unstage.Tag = "Enter/Space"; - unstage.Click += (_, e) => - { - UnstageChanges(_selectedStaged, null); - e.Handled = true; - }; - - var stash = new MenuItem(); - stash.Header = App.Text("FileCM.StashMulti", _selectedStaged.Count); - stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add"); - stash.Click += (_, e) => - { - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new StashChanges(_repo, _selectedStaged, true)); - - e.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = App.Text("FileCM.SaveAsPatch"); - patch.Icon = App.CreateMenuIcon("Icons.Diff"); - patch.Click += async (_, e) => - { - var storageProvider = App.GetStorageProvider(); - if (storageProvider == 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 storageProvider.SaveFilePickerAsync(options); - if (storageFile != null) - { - var succ = await Commands.SaveChangesAsPatch.ProcessLocalChangesAsync(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath); - if (succ) - App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); - } - - e.Handled = true; - }; - - menu.Items.Add(unstage); - menu.Items.Add(stash); - menu.Items.Add(patch); - - if (ai != null) - { - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(ai); - } - - if (hasSelectedFolder) - { - var history = new MenuItem(); - history.Header = App.Text("DirHistories"); - history.Icon = App.CreateMenuIcon("Icons.Histories"); - history.Click += (_, e) => - { - App.ShowWindow(new DirHistories(_repo, selectedSingleFolder)); - e.Handled = true; - }; - - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, e) => - { - await App.CopyTextAsync(selectedSingleFolder); - e.Handled = true; - }; - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, e) => - { - await App.CopyTextAsync(Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder)); - e.Handled = true; - }; - - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(history); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copyPath); - menu.Items.Add(copyFullPath); - } - } - - return menu; - } - - public ContextMenu CreateContextMenuForCommitMessages() - { - var menu = new ContextMenu(); - - var gitTemplate = new Commands.Config(_repo.FullPath).GetAsync("commit.template").Result; - var templateCount = _repo.Settings.CommitTemplates.Count; - if (templateCount == 0 && string.IsNullOrEmpty(gitTemplate)) - { - menu.Items.Add(new MenuItem() - { - Header = App.Text("WorkingCopy.NoCommitTemplates"), - Icon = App.CreateMenuIcon("Icons.Code"), - IsEnabled = false - }); - } - else - { - for (int i = 0; i < templateCount; i++) - { - var template = _repo.Settings.CommitTemplates[i]; - var item = new MenuItem(); - item.Header = App.Text("WorkingCopy.UseCommitTemplate", template.Name); - item.Icon = App.CreateMenuIcon("Icons.Code"); - item.Click += (_, e) => - { - CommitMessage = template.Apply(_repo.CurrentBranch, _staged); - e.Handled = true; - }; - menu.Items.Add(item); - } - - if (!string.IsNullOrEmpty(gitTemplate)) - { - if (!Path.IsPathRooted(gitTemplate)) - gitTemplate = Native.OS.GetAbsPath(_repo.FullPath, gitTemplate); - - var friendlyName = gitTemplate; - if (!OperatingSystem.IsWindows()) - { - var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; - if (gitTemplate.StartsWith(home, StringComparison.Ordinal)) - friendlyName = $"~{gitTemplate.AsSpan(prefixLen)}"; - } - - var gitTemplateItem = new MenuItem(); - gitTemplateItem.Header = App.Text("WorkingCopy.UseCommitTemplate", friendlyName); - gitTemplateItem.Icon = App.CreateMenuIcon("Icons.Code"); - gitTemplateItem.Click += (_, e) => - { - if (File.Exists(gitTemplate)) - CommitMessage = File.ReadAllText(gitTemplate); - e.Handled = true; - }; - menu.Items.Add(gitTemplateItem); - } - } - - menu.Items.Add(new MenuItem() { Header = "-" }); - - var historiesCount = _repo.Settings.CommitMessages.Count; - if (historiesCount == 0) - { - menu.Items.Add(new MenuItem() - { - Header = App.Text("WorkingCopy.NoCommitHistories"), - Icon = App.CreateMenuIcon("Icons.Histories"), - IsEnabled = false - }); - } - else - { - for (int i = 0; i < historiesCount; i++) - { - var dup = _repo.Settings.CommitMessages[i].Trim(); - var message = dup.ReplaceLineEndings(" "); - var item = new MenuItem(); - item.Header = new CommitMessageRecord(message); - item.Icon = App.CreateMenuIcon("Icons.Histories"); - item.Click += (_, e) => - { - CommitMessage = dup; - e.Handled = true; - }; - - menu.Items.Add(item); - } - } - - return menu; - } - - public ContextMenu CreateContextForOpenAI() - { - if (_staged == null || _staged.Count == 0) - { - App.RaiseException(_repo.FullPath, "No files added to commit!"); - return null; - } - - var services = _repo.GetPreferredOpenAIServices(); - if (services.Count == 0) - { - App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI"); - return null; - } - - if (services.Count == 1) - { - _ = App.ShowDialog(new AIAssistant(_repo, services[0], _staged, t => CommitMessage = t)); - return null; - } - - var menu = new ContextMenu() { Placement = PlacementMode.TopEdgeAlignedLeft }; - foreach (var service in services) - { - var dup = service; - var item = new MenuItem(); - item.Header = service.Name; - item.Click += async (_, e) => - { - await App.ShowDialog(new AIAssistant(_repo, dup, _staged, t => CommitMessage = t)); - e.Handled = true; - }; - - menu.Items.Add(item); - } - - return menu; - } - - private List GetVisibleChanges(List changes) - { - if (string.IsNullOrEmpty(_filter)) - return changes; - - var visible = new List(); - - foreach (var c in changes) - { - if (c.Path.Contains(_filter, StringComparison.OrdinalIgnoreCase)) - visible.Add(c); - } - - return visible; - } - - private List GetCanStagedChanges(List changes) - { - if (!HasUnsolvedConflicts) - return changes; - - var outs = new List(); - foreach (var c in changes) - { - if (!c.IsConflicted) - outs.Add(c); - } - - return outs; - } - - private List GetStagedChanges() - { - if (_useAmend) - { - var head = new Commands.QuerySingleCommit(_repo.FullPath, "HEAD") - .GetResultAsync() - .Result; - - return new Commands.QueryStagedChangesWithAmend(_repo.FullPath, head.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : $"{head.SHA}^") - .GetResultAsync() - .Result; - } - - var rs = new List(); - foreach (var c in _cached) - { - if (c.Index != Models.ChangeState.None) - rs.Add(c); - } - return rs; - } - - private void UpdateDetail() - { - if (_selectedUnstaged.Count == 1) - SetDetail(_selectedUnstaged[0], true); - else if (_selectedStaged.Count == 1) - SetDetail(_selectedStaged[0], false); - else - SetDetail(null, false); - } - - private void UpdateInProgressState() - { - if (string.IsNullOrEmpty(_commitMessage)) - { - var mergeMsgFile = Path.Combine(_repo.GitDir, "MERGE_MSG"); - if (File.Exists(mergeMsgFile)) - CommitMessage = File.ReadAllText(mergeMsgFile); - } - - if (File.Exists(Path.Combine(_repo.GitDir, "CHERRY_PICK_HEAD"))) - { - InProgressContext = new CherryPickInProgress(_repo); - } - else if (Directory.Exists(Path.Combine(_repo.GitDir, "rebase-merge")) || Directory.Exists(Path.Combine(_repo.GitDir, "rebase-apply"))) - { - var rebasing = new RebaseInProgress(_repo); - InProgressContext = rebasing; - - if (string.IsNullOrEmpty(_commitMessage)) - { - var rebaseMsgFile = Path.Combine(_repo.GitDir, "rebase-merge", "message"); - if (File.Exists(rebaseMsgFile)) - CommitMessage = File.ReadAllText(rebaseMsgFile); - else if (rebasing.StoppedAt != null) - CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, rebasing.StoppedAt.SHA).GetResultAsync().Result; - } - } - else if (File.Exists(Path.Combine(_repo.GitDir, "REVERT_HEAD"))) - { - InProgressContext = new RevertInProgress(_repo); - } - else if (File.Exists(Path.Combine(_repo.GitDir, "MERGE_HEAD"))) - { - InProgressContext = new MergeInProgress(_repo); - } - else - { - InProgressContext = null; - } - } - - private async void StageChanges(List changes, Models.Change next) - { - var canStaged = GetCanStagedChanges(changes); - var count = canStaged.Count; - if (count == 0) - return; - - IsStaging = true; - _selectedUnstaged = next != null ? [next] : []; - _repo.SetWatcherEnabled(false); - - var log = _repo.CreateLog("Stage"); - if (count == _unstaged.Count) - { - await new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Use(log).ExecAsync(); - } - else - { - var pathSpecFile = Path.GetTempFileName(); - await using (var writer = new StreamWriter(pathSpecFile)) - { - foreach (var c in canStaged) - await writer.WriteLineAsync(c.Path); - } - - await new Commands.Add(_repo.FullPath, pathSpecFile).Use(log).ExecAsync(); - File.Delete(pathSpecFile); - } - log.Complete(); - - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); - IsStaging = false; - } - - private async void UnstageChanges(List changes, Models.Change next) - { - var count = changes.Count; - if (count == 0) - return; - - IsUnstaging = true; - _selectedStaged = next != null ? [next] : []; - _repo.SetWatcherEnabled(false); - - var log = _repo.CreateLog("Unstage"); - if (_useAmend) - { - log.AppendLine("$ git update-index --index-info "); - await new Commands.UnstageChangesForAmend(_repo.FullPath, changes).ExecAsync(); - } - else - { - var pathSpecFile = Path.GetTempFileName(); - await using (var writer = new StreamWriter(pathSpecFile)) - { - foreach (var c in changes) - { - await writer.WriteLineAsync(c.Path); - if (c.Index == Models.ChangeState.Renamed) - await writer.WriteLineAsync(c.OriginalPath); - } - } - - await new Commands.Restore(_repo.FullPath, pathSpecFile, true).Use(log).ExecAsync(); - File.Delete(pathSpecFile); - } - log.Complete(); - - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); - IsUnstaging = false; - } + CommitMessage = msg; + return true; + } private void SetDetail(Models.Change change, bool isUnstaged) { @@ -1924,92 +798,6 @@ private void SetDetail(Models.Change change, bool isUnstaged) DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), _detailContext as DiffContext); } - private void DoCommit(bool autoStage, bool autoPush, CommitCheckPassed checkPassed = CommitCheckPassed.None) - { - if (string.IsNullOrWhiteSpace(_commitMessage)) - return; - - if (!_repo.CanCreatePopup()) - { - App.RaiseException(_repo.FullPath, "Repository has an unfinished job! Please wait!"); - return; - } - - if (autoStage && HasUnsolvedConflicts) - { - App.RaiseException(_repo.FullPath, "Repository has unsolved conflict(s). Auto-stage and commit is disabled!"); - return; - } - - if (_repo.CurrentBranch is { IsDetachedHead: true } && checkPassed < CommitCheckPassed.DetachedHead) - { - var msg = App.Text("WorkingCopy.ConfirmCommitWithDetachedHead"); - _ = App.AskConfirmAsync(msg, () => DoCommit(autoStage, autoPush, CommitCheckPassed.DetachedHead)); - return; - } - - if (!string.IsNullOrEmpty(_filter) && _staged.Count > _visibleStaged.Count && checkPassed < CommitCheckPassed.Filter) - { - var msg = App.Text("WorkingCopy.ConfirmCommitWithFilter", _staged.Count, _visibleStaged.Count, _staged.Count - _visibleStaged.Count); - _ = App.AskConfirmAsync(msg, () => DoCommit(autoStage, autoPush, CommitCheckPassed.Filter)); - return; - } - - if (checkPassed < CommitCheckPassed.FileCount && !_useAmend) - { - if ((!autoStage && _staged.Count == 0) || (autoStage && _cached.Count == 0)) - { - _ = App.ShowDialog(new ConfirmEmptyCommit(_cached.Count > 0, stageAll => DoCommit(stageAll, autoPush, CommitCheckPassed.FileCount))); - return; - } - } - - IsCommitting = true; - _repo.Settings.PushCommitMessage(_commitMessage); - _repo.SetWatcherEnabled(false); - - var signOff = _repo.Settings.EnableSignOffForCommit; - var log = _repo.CreateLog("Commit"); - Task.Run(async () => - { - var succ = true; - if (autoStage && _unstaged.Count > 0) - succ = await new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Use(log).ExecAsync().ConfigureAwait(false); - - if (succ) - succ = await new Commands.Commit(_repo.FullPath, _commitMessage, signOff, _useAmend, _resetAuthor).Use(log).RunAsync().ConfigureAwait(false); - - log.Complete(); - - Dispatcher.UIThread.Post(async () => - { - if (succ) - { - CommitMessage = string.Empty; - UseAmend = false; - - if (autoPush && _repo.Remotes.Count > 0) - { - if (_repo.CurrentBranch == null) - { - var currentBranchName = await new Commands.QueryCurrentBranch(_repo.FullPath).GetResultAsync(); - var tmp = new Models.Branch() { Name = currentBranchName }; - _repo.ShowAndStartPopup(new Push(_repo, tmp)); - } - else - { - _repo.ShowAndStartPopup(new Push(_repo, null)); - } - } - } - - _repo.MarkBranchesDirtyManually(); - _repo.SetWatcherEnabled(true); - IsCommitting = false; - }); - }); - } - private bool IsChanged(List old, List cur) { if (old.Count != cur.Count) @@ -2019,21 +807,13 @@ private bool IsChanged(List old, List cur) { var o = old[idx]; var c = cur[idx]; - if (o.Path != c.Path || o.Index != c.Index || o.WorkTree != c.WorkTree) + if (!o.Path.Equals(c.Path, StringComparison.Ordinal) || o.Index != c.Index || o.WorkTree != c.WorkTree) return true; } return false; } - private enum CommitCheckPassed - { - None = 0, - DetachedHead, - Filter, - FileCount, - } - private Repository _repo = null; private bool _isLoadingData = false; private bool _isStaging = false; diff --git a/src/ViewModels/WorkspaceSwitcher.cs b/src/ViewModels/WorkspaceSwitcher.cs deleted file mode 100644 index 7a2da9bee..000000000 --- a/src/ViewModels/WorkspaceSwitcher.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace SourceGit.ViewModels -{ - public class WorkspaceSwitcher : ObservableObject, IDisposable - { - public List VisibleWorkspaces - { - get => _visibleWorkspaces; - private set => SetProperty(ref _visibleWorkspaces, value); - } - - public string SearchFilter - { - get => _searchFilter; - set - { - if (SetProperty(ref _searchFilter, value)) - UpdateVisibleWorkspaces(); - } - } - - public Workspace SelectedWorkspace - { - get => _selectedWorkspace; - set => SetProperty(ref _selectedWorkspace, value); - } - - public WorkspaceSwitcher(Launcher launcher) - { - _launcher = launcher; - UpdateVisibleWorkspaces(); - } - - public void ClearFilter() - { - SearchFilter = string.Empty; - } - - public void Switch() - { - _launcher.SwitchWorkspace(_selectedWorkspace); - _launcher.CancelSwitcher(); - } - - public void Dispose() - { - _visibleWorkspaces.Clear(); - _selectedWorkspace = null; - _searchFilter = string.Empty; - } - - private void UpdateVisibleWorkspaces() - { - var visible = new List(); - if (string.IsNullOrEmpty(_searchFilter)) - { - visible.AddRange(Preferences.Instance.Workspaces); - } - else - { - foreach (var workspace in Preferences.Instance.Workspaces) - { - if (workspace.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) - visible.Add(workspace); - } - } - - VisibleWorkspaces = visible; - SelectedWorkspace = visible.Count == 0 ? null : visible[0]; - } - - private Launcher _launcher = null; - private List _visibleWorkspaces = null; - private string _searchFilter = string.Empty; - private Workspace _selectedWorkspace = null; - } -} diff --git a/src/Views/About.axaml b/src/Views/About.axaml index d0d4d2be5..8de188c94 100644 --- a/src/Views/About.axaml +++ b/src/Views/About.axaml @@ -34,7 +34,7 @@ IsVisible="{OnPlatform True, macOS=False}"/> - + - - + + - + PointerPressed="OnVisitReleaseNotes" + ToolTip.Tip="{DynamicResource Text.About.ReleaseNotes}"> + - + + + - + @@ -67,13 +70,8 @@ - - + + - - - - - diff --git a/src/Views/AddRemote.axaml b/src/Views/AddRemote.axaml index f42fbf1ff..b9c66f48b 100644 --- a/src/Views/AddRemote.axaml +++ b/src/Views/AddRemote.axaml @@ -8,10 +8,17 @@ x:Class="SourceGit.Views.AddRemote" x:DataType="vm:AddRemote"> - - + + + + + + - - - - - + + + + + + + diff --git a/src/Views/AddSubmodule.axaml b/src/Views/AddSubmodule.axaml index 2b5061ff3..716c9f303 100644 --- a/src/Views/AddSubmodule.axaml +++ b/src/Views/AddSubmodule.axaml @@ -8,9 +8,16 @@ x:Class="SourceGit.Views.AddSubmodule" x:DataType="vm:AddSubmodule"> - + + + + + + - + + + + + + - + diff --git a/src/Views/AddWorktree.axaml b/src/Views/AddWorktree.axaml index 1811008c6..6fcc546ec 100644 --- a/src/Views/AddWorktree.axaml +++ b/src/Views/AddWorktree.axaml @@ -3,14 +3,22 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.AddWorktree" x:DataType="vm:AddWorktree"> - - + + + + + + + @@ -93,8 +102,10 @@ + IsChecked="{Binding SetTrackingBranch, Mode=TwoWay}" + IsVisible="{Binding RemoteBranches, Converter={x:Static c:ListConverters.IsNotNullOrEmpty}}"/> diff --git a/src/Views/AddWorktree.axaml.cs b/src/Views/AddWorktree.axaml.cs index 4ac2d85f1..f5d90d550 100644 --- a/src/Views/AddWorktree.axaml.cs +++ b/src/Views/AddWorktree.axaml.cs @@ -1,4 +1,5 @@ using System; + using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Platform.Storage; diff --git a/src/Views/Apply.axaml b/src/Views/Apply.axaml index 6c2478bb5..1b683e9b1 100644 --- a/src/Views/Apply.axaml +++ b/src/Views/Apply.axaml @@ -9,9 +9,15 @@ x:Class="SourceGit.Views.Apply" x:DataType="vm:Apply"> - + + + + + - + + + + + + - + diff --git a/src/Views/Archive.axaml b/src/Views/Archive.axaml index e9d0ad02f..2cbdfba51 100644 --- a/src/Views/Archive.axaml +++ b/src/Views/Archive.axaml @@ -10,9 +10,16 @@ x:Class="SourceGit.Views.Archive" x:DataType="vm:Archive"> - + + + + + + - + diff --git a/src/Views/Avatar.cs b/src/Views/Avatar.cs index dc3971967..692dc6d62 100644 --- a/src/Views/Avatar.cs +++ b/src/Views/Avatar.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Security.Cryptography; @@ -6,6 +7,7 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -24,9 +26,25 @@ public Models.User User set => SetValue(UserProperty, value); } + public static readonly StyledProperty UseGitHubStyleAvatarProperty = + AvaloniaProperty.Register(nameof(UseGitHubStyleAvatar)); + + public bool UseGitHubStyleAvatar + { + get => GetValue(UseGitHubStyleAvatarProperty); + set => SetValue(UseGitHubStyleAvatarProperty, value); + } + public Avatar() { RenderOptions.SetBitmapInterpolationMode(this, BitmapInterpolationMode.HighQuality); + + this.Bind(UseGitHubStyleAvatarProperty, new Binding() + { + Mode = BindingMode.OneWay, + Source = ViewModels.Preferences.Instance, + Path = "UseGitHubStyleAvatar" + }); } public override void Render(DrawingContext context) @@ -42,6 +60,34 @@ public override void Render(DrawingContext context) { context.DrawImage(_img, rect); } + else if (!UseGitHubStyleAvatar) + { + var fallback = GetFallbackString(User.Name); + var typeface = new Typeface("fonts:SourceGit#JetBrains Mono"); + var label = new FormattedText( + fallback, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + Math.Max(Bounds.Width * 0.65, 10), + Brushes.White); + + var chars = fallback.ToCharArray(); + var sum = 0; + foreach (var c in chars) + sum += Math.Abs(c); + + var bg = new LinearGradientBrush() + { + GradientStops = FALLBACK_GRADIENTS[sum % FALLBACK_GRADIENTS.Length], + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), + }; + + Point textOrigin = new Point((Bounds.Width - label.Width) * 0.5, (Bounds.Height - label.Height) * 0.5); + context.DrawRectangle(bg, null, new Rect(0, 0, Bounds.Width, Bounds.Height), corner, corner); + context.DrawText(label, textOrigin); + } else { context.DrawRectangle(Brushes.White, new Pen(new SolidColorBrush(Colors.Black, 0.3f), 0.65f), rect, corner, corner); @@ -130,6 +176,11 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang _img = Models.AvatarManager.Instance.Request(User.Email, false); InvalidateVisual(); } + else if (change.Property == UseGitHubStyleAvatarProperty) + { + if (_img == null) + InvalidateVisual(); + } } private void OnContextRequested(object sender, ContextRequestedEventArgs e) @@ -220,6 +271,30 @@ private void OnContextRequested(object sender, ContextRequestedEventArgs e) menu.Open(this); } + private string GetFallbackString(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return "?"; + + var parts = name.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var chars = new List(); + foreach (var part in parts) + chars.Add(part[0]); + + if (chars.Count >= 2 && char.IsAsciiLetterOrDigit(chars[0]) && char.IsAsciiLetterOrDigit(chars[^1])) + return string.Format("{0}{1}", chars[0], chars[^1]); + + return name.Substring(0, 1); + } + + private static readonly GradientStops[] FALLBACK_GRADIENTS = [ + new GradientStops() { new GradientStop(Colors.Orange, 0), new GradientStop(Color.FromRgb(255, 213, 134), 1) }, + new GradientStops() { new GradientStop(Colors.DodgerBlue, 0), new GradientStop(Colors.LightSkyBlue, 1) }, + new GradientStops() { new GradientStop(Colors.LimeGreen, 0), new GradientStop(Color.FromRgb(124, 241, 124), 1) }, + new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) }, + new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) }, + ]; + private Bitmap _img = null; } } diff --git a/src/Views/Blame.axaml b/src/Views/Blame.axaml index bd8a2f811..af27d3563 100644 --- a/src/Views/Blame.axaml +++ b/src/Views/Blame.axaml @@ -38,7 +38,7 @@ - + @@ -76,6 +76,15 @@ TextTrimming="CharacterEllipsis"/> + + @@ -107,7 +116,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding IsBinary}"> - + diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs index 734160e53..0533beef8 100644 --- a/src/Views/Blame.axaml.cs +++ b/src/Views/Blame.axaml.cs @@ -223,7 +223,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) if (rect.Contains(pos)) { if (DataContext is ViewModels.Blame blame) - blame.NavigateToCommit(info.CommitSHA); + blame.NavigateToCommit(info.CommitSHA, false); e.Handled = true; break; @@ -288,10 +288,10 @@ public int TabWidth _textMate = Models.TextMateHelper.CreateForEditor(this); - TextArea.LeftMargins.Add(new LineNumberMargin() { Margin = new Thickness(8, 0) }); - TextArea.LeftMargins.Add(new VerticalSeparatorMargin(this)); TextArea.LeftMargins.Add(new CommitInfoMargin(this) { Margin = new Thickness(8, 0) }); TextArea.LeftMargins.Add(new VerticalSeparatorMargin(this)); + TextArea.LeftMargins.Add(new LineNumberMargin() { Margin = new Thickness(8, 0) }); + TextArea.LeftMargins.Add(new VerticalSeparatorMargin(this)); TextArea.Caret.PositionChanged += OnTextAreaCaretPositionChanged; TextArea.TextView.ContextRequested += OnTextViewContextRequested; TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged; diff --git a/src/Views/BlameCommandPalette.axaml b/src/Views/BlameCommandPalette.axaml new file mode 100644 index 000000000..b20ffa519 --- /dev/null +++ b/src/Views/BlameCommandPalette.axaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BlameCommandPalette.axaml.cs b/src/Views/BlameCommandPalette.axaml.cs new file mode 100644 index 000000000..32c5381fa --- /dev/null +++ b/src/Views/BlameCommandPalette.axaml.cs @@ -0,0 +1,63 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class BlameCommandPalette : UserControl + { + public BlameCommandPalette() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (DataContext is not ViewModels.BlameCommandPalette vm) + return; + + if (e.Key == Key.Enter) + { + vm.Launch(); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + if (FileListBox.IsKeyboardFocusWithin) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + else if (e.Key == Key.Down || e.Key == Key.Tab) + { + if (FilterTextBox.IsKeyboardFocusWithin) + { + if (vm.VisibleFiles.Count > 0) + FileListBox.Focus(NavigationMethod.Directional); + + e.Handled = true; + return; + } + + if (FileListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + } + + private void OnItemTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.BlameCommandPalette vm) + { + vm.Launch(); + e.Handled = true; + } + } + } +} diff --git a/src/Views/BranchCompare.axaml b/src/Views/BranchCompare.axaml index b80fefb5d..f222d2546 100644 --- a/src/Views/BranchCompare.axaml +++ b/src/Views/BranchCompare.axaml @@ -46,15 +46,15 @@ VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding BaseHead.Author}"/> - + - + - - + + - + @@ -70,15 +70,15 @@ VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding ToHead.Author}"/> - + - + - - + + - + @@ -93,7 +93,7 @@ - + + ContextRequested="OnChangeContextRequested" + KeyDown="OnChangeCollectionViewKeyDown"/> @@ -135,6 +137,16 @@ Width="48" Height="48" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding IsLoading}"/> + + + + + + + + 0 } selected } vm && + sender is ChangeCollectionView view) { - var menu = vm.CreateChangeContextMenu(); - menu?.Open(view); + var menu = new ContextMenu(); + var repo = vm.RepositoryPath; + + var patch = new MenuItem(); + patch.Header = App.Text("FileCM.SaveAsPatch"); + patch.Icon = App.CreateMenuIcon("Icons.Diff"); + patch.Click += async (_, e) => + { + var storageProvider = this.StorageProvider; + if (storageProvider == null) + return; + + var options = new FilePickerSaveOptions(); + options.Title = App.Text("FileCM.SaveAsPatch"); + options.DefaultExtension = ".patch"; + options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; + + try + { + var storageFile = await storageProvider.SaveFilePickerAsync(options); + if (storageFile != null) + { + var saveTo = storageFile.Path.LocalPath; + await vm.SaveChangesAsPatchAsync(selected, saveTo); + } + } + catch (Exception exception) + { + App.RaiseException(repo, $"Failed to save as patch: {exception.Message}"); + } + + e.Handled = true; + }; + + if (selected.Count == 1) + { + var change = selected[0]; + var openWithMerger = new MenuItem(); + openWithMerger.Header = App.Text("OpenInExternalMergeTool"); + openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); + openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; + openWithMerger.Click += (_, ev) => + { + new Commands.DiffTool(repo, new Models.DiffOption(vm.Base.Head, vm.To.Head, change)).Open(); + ev.Handled = true; + }; + menu.Items.Add(openWithMerger); + + if (change.Index != Models.ChangeState.Deleted) + { + var full = Path.GetFullPath(Path.Combine(repo, change.Path)); + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = App.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = File.Exists(full); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(full, true); + ev.Handled = true; + }; + menu.Items.Add(explore); + } + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + await App.CopyTextAsync(change.Path); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + await App.CopyTextAsync(Native.OS.GetAbsPath(repo, change.Path)); + ev.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(patch); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + } + else + { + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(c.Path); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(Native.OS.GetAbsPath(repo, c.Path)); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + menu.Items.Add(patch); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + } + + menu.Open(view); } e.Handled = true; @@ -28,5 +158,32 @@ private void OnPressedSHA(object sender, PointerPressedEventArgs e) e.Handled = true; } + + private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) + { + if (DataContext is not ViewModels.BranchCompare vm) + return; + + if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges }) + return; + + if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C) + { + var builder = new StringBuilder(); + var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + if (selectedChanges.Count == 1) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + } + else + { + foreach (var c in selectedChanges) + builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); + } + + await App.CopyTextAsync(builder.ToString()); + e.Handled = true; + } + } } } diff --git a/src/Views/BranchCompareCommandPalette.axaml b/src/Views/BranchCompareCommandPalette.axaml new file mode 100644 index 000000000..edb37b5d7 --- /dev/null +++ b/src/Views/BranchCompareCommandPalette.axaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BranchCompareCommandPalette.axaml.cs b/src/Views/BranchCompareCommandPalette.axaml.cs new file mode 100644 index 000000000..936ae94c8 --- /dev/null +++ b/src/Views/BranchCompareCommandPalette.axaml.cs @@ -0,0 +1,63 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class BranchCompareCommandPalette : UserControl + { + public BranchCompareCommandPalette() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (DataContext is not ViewModels.BranchCompareCommandPalette vm) + return; + + if (e.Key == Key.Enter) + { + vm.Launch(); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + if (BranchListBox.IsKeyboardFocusWithin) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + else if (e.Key == Key.Down || e.Key == Key.Tab) + { + if (FilterTextBox.IsKeyboardFocusWithin) + { + if (vm.Branches.Count > 0) + BranchListBox.Focus(NavigationMethod.Directional); + + e.Handled = true; + return; + } + + if (BranchListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + } + + private void OnItemTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.BranchCompareCommandPalette vm) + { + vm.Launch(); + e.Handled = true; + } + } + } +} diff --git a/src/Views/BranchOrTagNameTextBox.cs b/src/Views/BranchOrTagNameTextBox.cs new file mode 100644 index 000000000..e7078b17c --- /dev/null +++ b/src/Views/BranchOrTagNameTextBox.cs @@ -0,0 +1,66 @@ +using System; +using System.Text; + +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; + +namespace SourceGit.Views +{ + public class BranchOrTagNameTextBox : TextBox + { + protected override Type StyleKeyOverride => typeof(TextBox); + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + PastingFromClipboard += OnPastingFromClipboard; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + PastingFromClipboard -= OnPastingFromClipboard; + base.OnUnloaded(e); + } + + protected override void OnTextInput(TextInputEventArgs e) + { + if (string.IsNullOrEmpty(e.Text)) + return; + + var builder = new StringBuilder(e.Text.Length); + var chars = e.Text.ToCharArray(); + foreach (var ch in chars) + { + if (char.IsWhiteSpace(ch)) + builder.Append('-'); + else + builder.Append(ch); + } + + e.Text = builder.ToString(); + base.OnTextInput(e); + } + + private async void OnPastingFromClipboard(object sender, RoutedEventArgs e) + { + e.Handled = true; + + try + { + var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; + if (clipboard != null) + { + var text = await clipboard.TryGetTextAsync(); + if (!string.IsNullOrEmpty(text)) + OnTextInput(new TextInputEventArgs() { Text = text }); + } + } + catch + { + // Ignore exceptions + } + } + } +} diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml index 7361e6ba2..a627a0d90 100644 --- a/src/Views/BranchTree.axaml +++ b/src/Views/BranchTree.axaml @@ -15,6 +15,7 @@ SelectionMode="Multiple" SelectionChanged="OnNodesSelectionChanged" KeyDown="OnTreeKeyDown" + Padding="0,0,2,0" ContextRequested="OnTreeContextRequested"> @@ -43,25 +44,54 @@ - - - - - - + + + - - + + + + + + + - - + + + + + + + + + + + + + @@ -95,7 +125,7 @@ IsChecked="{Binding IsExpanded, Mode=OneWay}" IsVisible="{Binding !IsBranch}"/> - + @@ -118,7 +147,6 @@ Width="12" Height="12" Margin="8,0" Background="Transparent" - ToolTip.Tip="{DynamicResource Text.BranchUpstreamInvalid}" IsVisible="{Binding ShowUpstreamGoneTip}"> @@ -133,7 +161,7 @@ diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index 891efa4fc..fd14fc3a2 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -50,25 +50,27 @@ private void UpdateContent() if (node.Backend is Models.Remote) { - CreateContent(new Thickness(0, 0, 0, 0), "Icons.Remote", false); + CreateContent(new Thickness(0, 0, 0, 0), "Icons.Remote"); } else if (node.Backend is Models.Branch branch) { if (branch.IsCurrent) - CreateContent(new Thickness(0, 0, 0, 0), "Icons.CheckCircled", true); + CreateContent(new Thickness(0, 0, 0, 0), "Icons.CheckCircled", Brushes.Green); + else if (branch.IsLocal && !string.IsNullOrEmpty(branch.WorktreePath)) + CreateContent(new Thickness(2, 0, 0, 0), "Icons.Branch", Brushes.DarkCyan); else - CreateContent(new Thickness(2, 0, 0, 0), "Icons.Branch", false); + CreateContent(new Thickness(2, 0, 0, 0), "Icons.Branch"); } else { if (node.IsExpanded) - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open", false); + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open"); else - CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder", false); + CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder"); } } - private void CreateContent(Thickness margin, string iconKey, bool highlight) + private void CreateContent(Thickness margin, string iconKey, IBrush fill = null) { if (this.FindResource(iconKey) is not StreamGeometry geo) return; @@ -83,8 +85,8 @@ private void CreateContent(Thickness margin, string iconKey, bool highlight) Data = geo, }; - if (highlight) - path.Fill = Brushes.Green; + if (fill != null) + path.Fill = fill; Content = path; } @@ -181,11 +183,11 @@ protected override Size MeasureOverride(Size availableSize) if (DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) { - var status = branch.TrackStatus.ToString(); - if (!string.IsNullOrEmpty(status)) + var desc = branch.TrackStatusDescription; + if (!string.IsNullOrEmpty(desc)) { _label = new FormattedText( - status, + desc, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(FontFamily), @@ -200,6 +202,72 @@ protected override Size MeasureOverride(Size availableSize) private FormattedText _label = null; } + public class BranchTreeNodeTrackStatusTooltip : TextBlock + { + protected override Type StyleKeyOverride => typeof(TextBlock); + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + Text = string.Empty; + + if (DataContext is not Models.Branch { IsTrackStatusVisible: true } branch) + { + SetCurrentValue(IsVisibleProperty, false); + return; + } + + var ahead = branch.Ahead.Count; + var behind = branch.Behind.Count; + if (ahead > 0) + Text = behind > 0 ? App.Text("BranchTree.AheadBehind", ahead, behind) : App.Text("BranchTree.Ahead", ahead); + else + Text = App.Text("BranchTree.Behind", behind); + + SetCurrentValue(IsVisibleProperty, true); + } + } + + public class BranchTreeNodeDescription : TextBlock + { + protected override Type StyleKeyOverride => typeof(TextBlock); + + public BranchTreeNodeDescription() + { + IsVisible = false; + } + + protected override async void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + var visible = false; + + do + { + if (DataContext is not Models.Branch branch) + break; + + if (e.Root is not PopupRoot { Parent: Popup { Parent: Border owner } }) + break; + + var tree = owner.FindAncestorOfType(); + if (tree is not { DataContext: ViewModels.Repository repo }) + break; + + var description = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description"); + if (string.IsNullOrEmpty(description)) + break; + + Text = description; + visible = true; + } while (false); + + SetCurrentValue(IsVisibleProperty, visible); + } + } + public partial class BranchTree : UserControl { public static readonly StyledProperty> NodesProperty = @@ -352,6 +420,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang private void OnNodePointerPressed(object sender, PointerPressedEventArgs e) { + var ctrl = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + if (e.KeyModifiers.HasFlag(ctrl) || e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + return; + var p = e.GetCurrentPoint(this); if (!p.Properties.IsLeftButtonPressed) return; @@ -365,13 +437,6 @@ private void OnNodePointerPressed(object sender, PointerPressedEventArgs e) 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); } @@ -435,8 +500,7 @@ private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2) if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) { - var menu = repo.CreateContextMenuForRemote(remote); - menu?.Open(this); + CreateContextMenuForRemote(repo, remote).Open(this); return; } @@ -450,10 +514,8 @@ private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2) if (branches.Count == 1) { var branch = branches[0]; - var menu = branch.IsLocal ? - repo.CreateContextMenuForLocalBranch(branch) : - repo.CreateContextMenuForRemoteBranch(branch); - menu?.Open(this); + var menu = branch.IsLocal ? CreateContextMenuForLocalBranch(repo, branch) : CreateContextMenuForRemoteBranch(repo, branch); + menu.Open(this); } else if (branches.Find(x => x.IsCurrent) == null) { @@ -522,7 +584,7 @@ private void OnTreeKeyDown(object _, KeyEventArgs e) e.Handled = true; } - private void OnDoubleTappedBranchNode(object sender, TappedEventArgs _) + private async void OnDoubleTappedBranchNode(object sender, TappedEventArgs _) { if (sender is Grid { DataContext: ViewModels.BranchTreeNode node }) { @@ -532,7 +594,7 @@ private void OnDoubleTappedBranchNode(object sender, TappedEventArgs _) return; if (DataContext is ViewModels.Repository { Settings: not null } repo) - repo.CheckoutBranch(branch); + await repo.CheckoutBranchAsync(branch); } else { @@ -585,6 +647,627 @@ private void FindTreePath(List outPath, List x.FullName.Equals(branch.Upstream, StringComparison.Ordinal)); + + var push = new MenuItem(); + push.Header = App.Text("BranchCM.Push", branch.Name); + push.Icon = App.CreateMenuIcon("Icons.Push"); + push.IsEnabled = repo.Remotes.Count > 0; + push.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Push(repo, branch)); + e.Handled = true; + }; + + if (branch.IsCurrent) + { + if (!repo.IsBare) + { + if (upstream != null) + { + var fastForward = new MenuItem(); + fastForward.Header = App.Text("BranchCM.FastForward", upstream.FriendlyName); + fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); + fastForward.IsEnabled = branch.Ahead.Count == 0 && branch.Behind.Count > 0; + fastForward.Click += async (_, e) => + { + if (repo.CanCreatePopup()) + await repo.ShowAndStartPopupAsync(new ViewModels.Merge(repo, upstream, branch.Name, true)); + e.Handled = true; + }; + + var pull = new MenuItem(); + pull.Header = App.Text("BranchCM.Pull", upstream.FriendlyName); + pull.Icon = App.CreateMenuIcon("Icons.Pull"); + pull.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Pull(repo, null)); + e.Handled = true; + }; + + menu.Items.Add(fastForward); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(pull); + } + } + + menu.Items.Add(push); + } + else + { + var hasNoWorktree = string.IsNullOrEmpty(branch.WorktreePath); + + var checkout = new MenuItem(); + checkout.Header = App.Text(hasNoWorktree ? "BranchCM.Checkout" : "BranchCM.SwitchToWorktree", branch.Name); + checkout.Icon = App.CreateMenuIcon("Icons.Check"); + checkout.IsEnabled = !repo.IsBare || !hasNoWorktree; + checkout.Click += async (_, e) => + { + await repo.CheckoutBranchAsync(branch); + e.Handled = true; + }; + menu.Items.Add(checkout); + menu.Items.Add(new MenuItem() { Header = "-" }); + + if (upstream != null && hasNoWorktree) + { + var fastForward = new MenuItem(); + fastForward.Header = App.Text("BranchCM.FastForward", upstream.FriendlyName); + fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); + fastForward.IsEnabled = branch.Ahead.Count == 0 && branch.Behind.Count > 0; + fastForward.Click += async (_, e) => + { + if (repo.CanCreatePopup()) + await repo.ShowAndStartPopupAsync(new ViewModels.ResetWithoutCheckout(repo, branch, upstream)); + e.Handled = true; + }; + menu.Items.Add(fastForward); + + var fetchInto = new MenuItem(); + fetchInto.Header = App.Text("BranchCM.FetchInto", upstream.FriendlyName, branch.Name); + fetchInto.Icon = App.CreateMenuIcon("Icons.Fetch"); + fetchInto.IsEnabled = branch.Ahead.Count == 0; + fetchInto.Click += async (_, e) => + { + if (repo.CanCreatePopup()) + await repo.ShowAndStartPopupAsync(new ViewModels.FetchInto(repo, branch, upstream)); + e.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(fetchInto); + } + + menu.Items.Add(push); + + if (!repo.IsBare) + { + var merge = new MenuItem(); + merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name); + merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Merge(repo, branch, current.Name, false)); + e.Handled = true; + }; + + var rebase = new MenuItem(); + rebase.Header = App.Text("BranchCM.Rebase", current.Name, branch.Name); + rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); + rebase.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Rebase(repo, current, branch)); + e.Handled = true; + }; + + menu.Items.Add(merge); + menu.Items.Add(rebase); + } + + if (hasNoWorktree) + { + var selectedCommit = repo.GetSelectedCommitInHistory(); + if (selectedCommit != null && !selectedCommit.SHA.Equals(branch.Head, StringComparison.Ordinal)) + { + var move = new MenuItem(); + move.Header = App.Text("BranchCM.ResetToSelectedCommit", branch.Name, selectedCommit.SHA.Substring(0, 10)); + move.Icon = App.CreateMenuIcon("Icons.Reset"); + move.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.ResetWithoutCheckout(repo, branch, selectedCommit)); + e.Handled = true; + }; + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(move); + } + } + + var compareWithCurrent = new MenuItem(); + compareWithCurrent.Header = App.Text("BranchCM.CompareWithCurrent", current.Name); + compareWithCurrent.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithCurrent.Click += (_, _) => + { + App.ShowWindow(new ViewModels.BranchCompare(repo.FullPath, branch, current)); + }; + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(compareWithCurrent); + + if (repo.LocalChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += async (_, e) => + { + await repo.CompareBranchWithWorktreeAsync(branch); + e.Handled = true; + }; + menu.Items.Add(compareWithWorktree); + } + } + + if (!repo.IsBare) + { + var type = repo.GetGitFlowType(branch); + if (type != Models.GitFlowBranchType.None) + { + var finish = new MenuItem(); + finish.Header = App.Text("BranchCM.Finish", branch.Name); + finish.Icon = App.CreateMenuIcon("Icons.GitFlow"); + finish.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.GitFlowFinish(repo, branch, type)); + e.Handled = true; + }; + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(finish); + } + } + + var rename = new MenuItem(); + rename.Header = App.Text("BranchCM.Rename", branch.Name); + rename.Icon = App.CreateMenuIcon("Icons.Rename"); + rename.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.RenameBranch(repo, branch)); + e.Handled = true; + }; + + var editDescription = new MenuItem(); + editDescription.Header = App.Text("BranchCM.EditDescription", branch.Name); + editDescription.Icon = App.CreateMenuIcon("Icons.Edit"); + editDescription.Click += async (_, e) => + { + var desc = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description"); + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.EditBranchDescription(repo, branch, desc)); + e.Handled = true; + }; + + var delete = new MenuItem(); + delete.Header = App.Text("BranchCM.Delete", branch.Name); + delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.IsEnabled = !branch.IsCurrent; + delete.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch)); + e.Handled = true; + }; + + var createBranch = new MenuItem(); + createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); + createBranch.Header = App.Text("CreateBranch"); + createBranch.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CreateBranch(repo, branch)); + e.Handled = true; + }; + + var createTag = new MenuItem(); + createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); + createTag.Header = App.Text("CreateTag"); + createTag.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CreateTag(repo, branch)); + e.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(editDescription); + menu.Items.Add(rename); + menu.Items.Add(delete); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(createBranch); + menu.Items.Add(createTag); + + if (upstream != null) + { + var remote = repo.Remotes.Find(x => x.Name.Equals(upstream.Remote, StringComparison.Ordinal)); + if (remote != null && remote.TryGetCreatePullRequestURL(out var prURL, upstream.Name)) + { + var createPR = new MenuItem(); + createPR.Header = App.Text("BranchCM.CreatePRForUpstream", upstream.FriendlyName); + createPR.Icon = App.CreateMenuIcon("Icons.CreatePR"); + createPR.Click += (_, e) => + { + Native.OS.OpenBrowser(prURL); + e.Handled = true; + }; + + menu.Items.Add(createPR); + } + } + menu.Items.Add(new MenuItem() { Header = "-" }); + TryToAddCustomActionsToBranchContextMenu(repo, menu, branch); + + if (!repo.IsBare) + { + var remoteBranches = new List(); + foreach (var b in repo.Branches) + { + if (!b.IsLocal) + remoteBranches.Add(b); + } + + if (remoteBranches.Count > 0) + { + var tracking = new MenuItem(); + tracking.Header = App.Text("BranchCM.Tracking"); + tracking.Icon = App.CreateMenuIcon("Icons.Track"); + tracking.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.SetUpstream(repo, branch, remoteBranches)); + e.Handled = true; + }; + menu.Items.Add(tracking); + } + } + + var archive = new MenuItem(); + archive.Icon = App.CreateMenuIcon("Icons.Archive"); + archive.Header = App.Text("Archive"); + archive.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Archive(repo, branch)); + e.Handled = true; + }; + menu.Items.Add(archive); + menu.Items.Add(new MenuItem() { Header = "-" }); + + var copy = new MenuItem(); + copy.Header = App.Text("BranchCM.CopyName"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, e) => + { + await App.CopyTextAsync(branch.Name); + e.Handled = true; + }; + menu.Items.Add(copy); + + return menu; + } + + private ContextMenu CreateContextMenuForRemote(ViewModels.Repository repo, Models.Remote remote) + { + var menu = new ContextMenu(); + + if (remote.TryGetVisitURL(out string visitURL)) + { + var visit = new MenuItem(); + visit.Header = App.Text("RemoteCM.OpenInBrowser"); + visit.Icon = App.CreateMenuIcon("Icons.OpenWith"); + visit.Click += (_, e) => + { + Native.OS.OpenBrowser(visitURL); + e.Handled = true; + }; + + menu.Items.Add(visit); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var fetch = new MenuItem(); + fetch.Header = App.Text("RemoteCM.Fetch"); + fetch.Icon = App.CreateMenuIcon("Icons.Fetch"); + fetch.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Fetch(repo, remote)); + e.Handled = true; + }; + + var prune = new MenuItem(); + prune.Header = App.Text("RemoteCM.Prune"); + prune.Icon = App.CreateMenuIcon("Icons.Clean"); + prune.Click += async (_, e) => + { + if (repo.CanCreatePopup()) + await repo.ShowAndStartPopupAsync(new ViewModels.PruneRemote(repo, remote)); + e.Handled = true; + }; + + var edit = new MenuItem(); + edit.Header = App.Text("RemoteCM.Edit"); + edit.Icon = App.CreateMenuIcon("Icons.Edit"); + edit.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.EditRemote(repo, remote)); + e.Handled = true; + }; + + var delete = new MenuItem(); + delete.Header = App.Text("RemoteCM.Delete"); + delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteRemote(repo, remote)); + e.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = App.Text("RemoteCM.CopyURL"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, e) => + { + await App.CopyTextAsync(remote.URL); + e.Handled = true; + }; + + menu.Items.Add(fetch); + menu.Items.Add(prune); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(edit); + menu.Items.Add(delete); + menu.Items.Add(new MenuItem() { Header = "-" }); + TryToAddCustomActionsToRemoteContextMenu(repo, menu, remote); + menu.Items.Add(copy); + return menu; + } + + public ContextMenu CreateContextMenuForRemoteBranch(ViewModels.Repository repo, Models.Branch branch) + { + var menu = new ContextMenu(); + var name = branch.FriendlyName; + + var checkout = new MenuItem(); + checkout.Header = App.Text("BranchCM.Checkout", name); + checkout.Icon = App.CreateMenuIcon("Icons.Check"); + checkout.Click += async (_, e) => + { + await repo.CheckoutBranchAsync(branch); + e.Handled = true; + }; + menu.Items.Add(checkout); + menu.Items.Add(new MenuItem() { Header = "-" }); + + if (repo.CurrentBranch is { } current) + { + var pull = new MenuItem(); + pull.Header = App.Text("BranchCM.PullInto", name, current.Name); + pull.Icon = App.CreateMenuIcon("Icons.Pull"); + pull.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Pull(repo, branch)); + e.Handled = true; + }; + + var merge = new MenuItem(); + merge.Header = App.Text("BranchCM.Merge", name, current.Name); + merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Merge(repo, branch, current.Name, false)); + e.Handled = true; + }; + + var rebase = new MenuItem(); + rebase.Header = App.Text("BranchCM.Rebase", current.Name, name); + rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); + rebase.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Rebase(repo, current, branch)); + e.Handled = true; + }; + + menu.Items.Add(pull); + menu.Items.Add(merge); + menu.Items.Add(rebase); + menu.Items.Add(new MenuItem() { Header = "-" }); + + var compareWithHead = new MenuItem(); + compareWithHead.Header = App.Text("BranchCM.CompareWithCurrent", current.Name); + compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithHead.Click += (_, _) => + { + App.ShowWindow(new ViewModels.BranchCompare(repo.FullPath, branch, current)); + }; + menu.Items.Add(compareWithHead); + } + + if (repo.LocalChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += async (_, e) => + { + await repo.CompareBranchWithWorktreeAsync(branch); + e.Handled = true; + }; + menu.Items.Add(compareWithWorktree); + } + menu.Items.Add(new MenuItem() { Header = "-" }); + + var editDescription = new MenuItem(); + editDescription.Header = App.Text("BranchCM.EditDescription", branch.Name); + editDescription.Icon = App.CreateMenuIcon("Icons.Edit"); + editDescription.Click += async (_, e) => + { + var desc = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description"); + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.EditBranchDescription(repo, branch, desc)); + e.Handled = true; + }; + + var delete = new MenuItem(); + delete.Header = App.Text("BranchCM.Delete", name); + delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch)); + e.Handled = true; + }; + + menu.Items.Add(editDescription); + menu.Items.Add(delete); + menu.Items.Add(new MenuItem() { Header = "-" }); + + var createBranch = new MenuItem(); + createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); + createBranch.Header = App.Text("CreateBranch"); + createBranch.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CreateBranch(repo, branch)); + e.Handled = true; + }; + + var createTag = new MenuItem(); + createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); + createTag.Header = App.Text("CreateTag"); + createTag.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CreateTag(repo, branch)); + e.Handled = true; + }; + + menu.Items.Add(createBranch); + menu.Items.Add(createTag); + + var remote = repo.Remotes.Find(x => x.Name.Equals(branch.Remote, StringComparison.Ordinal)); + if (remote != null && remote.TryGetCreatePullRequestURL(out var prURL, branch.Name)) + { + var createPR = new MenuItem(); + createPR.Header = App.Text("BranchCM.CreatePR"); + createPR.Icon = App.CreateMenuIcon("Icons.CreatePR"); + createPR.Click += (_, e) => + { + Native.OS.OpenBrowser(prURL); + e.Handled = true; + }; + + menu.Items.Add(createPR); + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + + var archive = new MenuItem(); + archive.Icon = App.CreateMenuIcon("Icons.Archive"); + archive.Header = App.Text("Archive"); + archive.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Archive(repo, branch)); + e.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = App.Text("BranchCM.CopyName"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, e) => + { + await App.CopyTextAsync(name); + e.Handled = true; + }; + + menu.Items.Add(archive); + menu.Items.Add(new MenuItem() { Header = "-" }); + TryToAddCustomActionsToBranchContextMenu(repo, menu, branch); + menu.Items.Add(copy); + return menu; + } + + private void TryToAddCustomActionsToBranchContextMenu(ViewModels.Repository repo, ContextMenu menu, Models.Branch branch) + { + var actions = repo.GetCustomActions(Models.CustomActionScope.Branch); + if (actions.Count == 0) + return; + + var custom = new MenuItem(); + custom.Header = App.Text("BranchCM.CustomAction"); + custom.Icon = App.CreateMenuIcon("Icons.Action"); + + foreach (var action in actions) + { + var (dup, label) = action; + var item = new MenuItem(); + item.Icon = App.CreateMenuIcon("Icons.Action"); + item.Header = label; + item.Click += async (_, e) => + { + await repo.ExecCustomActionAsync(dup, branch); + e.Handled = true; + }; + + custom.Items.Add(item); + } + + menu.Items.Add(custom); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + private void TryToAddCustomActionsToRemoteContextMenu(ViewModels.Repository repo, ContextMenu menu, Models.Remote remote) + { + var actions = repo.GetCustomActions(Models.CustomActionScope.Remote); + if (actions.Count == 0) + return; + + var custom = new MenuItem(); + custom.Header = App.Text("RemoteCM.CustomAction"); + custom.Icon = App.CreateMenuIcon("Icons.Action"); + + foreach (var action in actions) + { + var (dup, label) = action; + var item = new MenuItem(); + item.Icon = App.CreateMenuIcon("Icons.Action"); + item.Header = label; + item.Click += async (_, e) => + { + await repo.ExecCustomActionAsync(dup, remote); + e.Handled = true; + }; + + custom.Items.Add(item); + } + + menu.Items.Add(custom); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + private bool _disableSelectionChangingEvent = false; } } diff --git a/src/Views/CaptionButtons.axaml b/src/Views/CaptionButtons.axaml index f43230e46..b1ee6a7b2 100644 --- a/src/Views/CaptionButtons.axaml +++ b/src/Views/CaptionButtons.axaml @@ -6,13 +6,13 @@ x:Class="SourceGit.Views.CaptionButtons" x:Name="ThisControl"> - - - diff --git a/src/Views/ChangeCollectionView.axaml b/src/Views/ChangeCollectionView.axaml index 9ff1486ff..a00570f50 100644 --- a/src/Views/ChangeCollectionView.axaml +++ b/src/Views/ChangeCollectionView.axaml @@ -33,7 +33,7 @@ @@ -65,8 +65,8 @@ IsVisible="{Binding !IsFolder}"/> - - + + @@ -78,7 +78,7 @@ @@ -93,12 +93,11 @@ Change="{Binding}" /> - - + + @@ -111,7 +110,7 @@ @@ -126,8 +125,8 @@ Change="{Binding}" /> - - + + diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index 8b9f90586..237a69da4 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -33,7 +33,7 @@ public class ChangeCollectionContainer : ListBox { protected override Type StyleKeyOverride => typeof(ListBox); - protected override async void OnKeyDown(KeyEventArgs e) + protected override void OnKeyDown(KeyEventArgs e) { if (SelectedItems is [ViewModels.ChangeTreeNode node]) { @@ -43,36 +43,6 @@ protected override async void OnKeyDown(KeyEventArgs e) this.FindAncestorOfType()?.ToggleNodeIsExpanded(node); e.Handled = true; } - else if (e.Key == Key.C && - e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) - { - var path = node.FullPath; - - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - { - do - { - var repoView = this.FindAncestorOfType(); - if (repoView is { DataContext: ViewModels.Repository repo }) - { - path = Native.OS.GetAbsPath(repo.FullPath, path); - break; - } - - var branchCompareView = this.FindAncestorOfType(); - if (branchCompareView is { DataContext: ViewModels.BranchCompare branchCompare }) - { - path = branchCompare.GetAbsPath(path); - break; - } - - // NOTE: if there is another window uses ChangeCollectionView, add it here! - } while (false); - } - - await App.CopyTextAsync(path); - e.Handled = true; - } } if (!e.Handled && e.Key != Key.Space && e.Key != Key.Enter) @@ -91,15 +61,6 @@ public bool IsUnstagedChange set => SetValue(IsUnstagedChangeProperty, value); } - public static readonly StyledProperty SelectionModeProperty = - AvaloniaProperty.Register(nameof(SelectionMode)); - - public SelectionMode SelectionMode - { - get => GetValue(SelectionModeProperty); - set => SetValue(SelectionModeProperty, value); - } - public static readonly StyledProperty ViewModeProperty = AvaloniaProperty.Register(nameof(ViewMode), Models.ChangeViewMode.Tree); @@ -109,6 +70,15 @@ public Models.ChangeViewMode ViewMode set => SetValue(ViewModeProperty, value); } + public static readonly StyledProperty EnableCompactFoldersProperty = + AvaloniaProperty.Register(nameof(EnableCompactFolders)); + + public bool EnableCompactFolders + { + get => GetValue(EnableCompactFoldersProperty); + set => SetValue(EnableCompactFoldersProperty, value); + } + public static readonly StyledProperty> ChangesProperty = AvaloniaProperty.Register>(nameof(Changes)); @@ -254,6 +224,9 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang UpdateDataSource(false); else if (change.Property == SelectedChangesProperty) UpdateSelection(); + + if (change.Property == EnableCompactFoldersProperty && ViewMode == Models.ChangeViewMode.Tree) + UpdateDataSource(true); } private void OnRowDataContextChanged(object sender, EventArgs e) @@ -384,7 +357,7 @@ private void UpdateDataSource(bool onlyViewModeChange) } var tree = new ViewModels.ChangeCollectionAsTree(); - tree.Tree = ViewModels.ChangeTreeNode.Build(changes, oldFolded); + tree.Tree = ViewModels.ChangeTreeNode.Build(changes, oldFolded, EnableCompactFolders); var rows = new List(); MakeTreeRows(rows, tree.Tree); diff --git a/src/Views/ChangeSubmoduleUrl.axaml b/src/Views/ChangeSubmoduleUrl.axaml index ad458133e..a12a04890 100644 --- a/src/Views/ChangeSubmoduleUrl.axaml +++ b/src/Views/ChangeSubmoduleUrl.axaml @@ -7,9 +7,16 @@ x:Class="SourceGit.Views.ChangeSubmoduleUrl" x:DataType="vm:ChangeSubmoduleUrl"> - + + + + + + - + + + + + - - - diff --git a/src/Views/CheckoutAndFastForward.axaml b/src/Views/CheckoutAndFastForward.axaml index ad0422b3f..13c12d337 100644 --- a/src/Views/CheckoutAndFastForward.axaml +++ b/src/Views/CheckoutAndFastForward.axaml @@ -7,16 +7,22 @@ x:Class="SourceGit.Views.CheckoutAndFastForward" x:DataType="vm:CheckoutAndFastForward"> - + + + + + - + IsVisible="{Binding LocalBranch.IsTrackStatusVisible, Mode=OneWay}"> + Text="{Binding LocalBranch.TrackStatusDescription, Mode=OneWay}"/> @@ -56,18 +62,11 @@ - - diff --git a/src/Views/CheckoutCommandPalette.axaml b/src/Views/CheckoutCommandPalette.axaml new file mode 100644 index 000000000..17897a26b --- /dev/null +++ b/src/Views/CheckoutCommandPalette.axaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CheckoutCommandPalette.axaml.cs b/src/Views/CheckoutCommandPalette.axaml.cs new file mode 100644 index 000000000..3601c5422 --- /dev/null +++ b/src/Views/CheckoutCommandPalette.axaml.cs @@ -0,0 +1,63 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class CheckoutCommandPalette : UserControl + { + public CheckoutCommandPalette() + { + InitializeComponent(); + } + + protected override async void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (DataContext is not ViewModels.CheckoutCommandPalette vm) + return; + + if (e.Key == Key.Enter) + { + await vm.ExecAsync(); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + if (BranchListBox.IsKeyboardFocusWithin) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + else if (e.Key == Key.Down || e.Key == Key.Tab) + { + if (FilterTextBox.IsKeyboardFocusWithin) + { + if (vm.Branches.Count > 0) + BranchListBox.Focus(NavigationMethod.Directional); + + e.Handled = true; + return; + } + + if (BranchListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + } + + private async void OnItemTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.CheckoutCommandPalette vm) + { + await vm.ExecAsync(); + e.Handled = true; + } + } + } +} diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml index 11b4b5d04..3315a8610 100644 --- a/src/Views/CheckoutCommit.axaml +++ b/src/Views/CheckoutCommit.axaml @@ -8,18 +8,30 @@ x:Class="SourceGit.Views.CheckoutCommit" x:DataType="vm:CheckoutCommit"> - + + - + + + + + + + + + + - + @@ -27,23 +39,16 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,8,0" Text="{DynamicResource Text.Checkout.LocalChanges}"/> - - + - - - + - + - + + + + + + - + + + + + + diff --git a/src/Views/ClearStashes.axaml b/src/Views/ClearStashes.axaml index b986211b5..05ae7a362 100644 --- a/src/Views/ClearStashes.axaml +++ b/src/Views/ClearStashes.axaml @@ -7,9 +7,15 @@ x:Class="SourceGit.Views.ClearStashes" x:DataType="vm:ClearStashes"> - + + + + + - + + + + + { + var old = v.TextRunProperties.Typeface; v.TextRunProperties.SetForegroundBrush(Brushes.Red); + v.TextRunProperties.SetTypeface(new Typeface(old.FontFamily, old.Style, FontWeight.Bold)); }); } } @@ -91,6 +93,14 @@ public string PureText TextArea.TextView.Margin = new Thickness(4, 0); TextArea.TextView.Options.EnableHyperlinks = false; TextArea.TextView.Options.EnableEmailHyperlinks = false; + TextArea.TextView.Options.AllowScrollBelowDocument = false; + } + + public void OnReceiveCommandLog(string line) + { + AppendText("\n"); + AppendText(line); + ScrollToEnd(); } protected override void OnLoaded(RoutedEventArgs e) @@ -124,10 +134,13 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == LogProperty) { - if (change.NewValue is ViewModels.CommandLog log) + if (change.OldValue is ViewModels.CommandLog oldLog) + oldLog.Unsubscribe(this); + + if (change.NewValue is ViewModels.CommandLog newLog) { - Text = log.Content; - log.Register(OnLogLineReceived); + Text = newLog.Content; + newLog.Subscribe(this); } else { @@ -141,12 +154,6 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } - 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 index 6dec67b63..0810ba415 100644 --- a/src/Views/CommandLogTime.cs +++ b/src/Views/CommandLogTime.cs @@ -1,7 +1,6 @@ using System; using System.Threading; -using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Threading; @@ -10,15 +9,6 @@ namespace SourceGit.Views { public class CommandLogTime : TextBlock { - public static readonly StyledProperty LogProperty = - AvaloniaProperty.Register(nameof(Log)); - - public ViewModels.CommandLog Log - { - get => GetValue(LogProperty); - set => SetValue(LogProperty, value); - } - protected override Type StyleKeyOverride => typeof(TextBlock); protected override void OnUnloaded(RoutedEventArgs e) @@ -27,19 +17,16 @@ protected override void OnUnloaded(RoutedEventArgs e) StopTimer(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnDataContextChanged(EventArgs e) { - base.OnPropertyChanged(change); + base.OnDataContextChanged(e); - if (change.Property == LogProperty) - { - StopTimer(); + StopTimer(); - if (change.NewValue is ViewModels.CommandLog log) - SetupCommandLog(log); - else - Text = string.Empty; - } + if (DataContext is ViewModels.CommandLog log) + SetupCommandLog(log); + else + Text = string.Empty; } private void SetupCommandLog(ViewModels.CommandLog log) @@ -74,12 +61,12 @@ private static string GetDisplayText(ViewModels.CommandLog log) var duration = endTime - log.StartTime; if (duration.TotalMinutes >= 1) - return $"{log.StartTime:T} ({duration.TotalMinutes:G3} minutes)"; + return $"{duration.TotalMinutes:G3} min"; if (duration.TotalSeconds >= 1) - return $"{log.StartTime:T} ({duration.TotalSeconds:G3} s)"; + return $"{duration.TotalSeconds:G3} s"; - return $"{log.StartTime:T} ({duration.TotalMilliseconds:G3} ms)"; + return $"{duration.TotalMilliseconds:G3} ms"; } private Timer _refreshTimer = null; diff --git a/src/Views/CommandPaletteDataTemplates.cs b/src/Views/CommandPaletteDataTemplates.cs new file mode 100644 index 000000000..eec8dd717 --- /dev/null +++ b/src/Views/CommandPaletteDataTemplates.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; + +namespace SourceGit.Views +{ + public class CommandPaletteDataTemplates : IDataTemplate + { + public Control Build(object param) => App.CreateViewForViewModel(param); + public bool Match(object data) => data is ViewModels.ICommandPalette; + } +} diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index 4d0c7b0ba..ab1520ed7 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -18,15 +18,17 @@ - - - - - - + + + + + + + + @@ -34,15 +36,17 @@ - - - - - - + + + + + + + + @@ -55,10 +59,7 @@ - + - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/CommitMessageTextBox.axaml.cs b/src/Views/CommitMessageTextBox.axaml.cs deleted file mode 100644 index cf08eb6b0..000000000 --- a/src/Views/CommitMessageTextBox.axaml.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; - -namespace SourceGit.Views -{ - public class EnhancedTextBox : TextBox - { - public static readonly RoutedEvent PreviewKeyDownEvent = - RoutedEvent.Register(nameof(KeyEventArgs), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - public event EventHandler PreviewKeyDown - { - add { AddHandler(PreviewKeyDownEvent, value); } - remove { RemoveHandler(PreviewKeyDownEvent, value); } - } - - protected override Type StyleKeyOverride => typeof(TextBox); - - public void Paste(string text) - { - OnTextInput(new TextInputEventArgs() { Text = text }); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - var dump = new KeyEventArgs() - { - RoutedEvent = PreviewKeyDownEvent, - Route = RoutingStrategies.Direct, - Source = e.Source, - Key = e.Key, - KeyModifiers = e.KeyModifiers, - PhysicalKey = e.PhysicalKey, - KeySymbol = e.KeySymbol, - }; - - RaiseEvent(dump); - - if (dump.Handled) - e.Handled = true; - else - base.OnKeyDown(e); - } - } - - public partial class CommitMessageTextBox : UserControl - { - public enum TextChangeWay - { - None, - FromSource, - FromEditor, - } - - public static readonly StyledProperty ShowAdvancedOptionsProperty = - AvaloniaProperty.Register(nameof(ShowAdvancedOptions)); - - public static readonly StyledProperty TextProperty = - AvaloniaProperty.Register(nameof(Text), string.Empty); - - public static readonly StyledProperty SubjectProperty = - AvaloniaProperty.Register(nameof(Subject), string.Empty); - - 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); - set => SetValue(TextProperty, value); - } - - public string Subject - { - get => GetValue(SubjectProperty); - set => SetValue(SubjectProperty, value); - } - - public string Description - { - get => GetValue(DescriptionProperty); - set => SetValue(DescriptionProperty, value); - } - - public CommitMessageTextBox() - { - InitializeComponent(); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == TextProperty && _changingWay == TextChangeWay.None) - { - _changingWay = TextChangeWay.FromSource; - var normalized = Text.ReplaceLineEndings("\n"); - var parts = normalized.Split("\n\n", 2); - if (parts.Length != 2) - parts = [normalized, string.Empty]; - SetCurrentValue(SubjectProperty, parts[0].ReplaceLineEndings(" ")); - SetCurrentValue(DescriptionProperty, parts[1]); - _changingWay = TextChangeWay.None; - } - else if ((change.Property == SubjectProperty || change.Property == DescriptionProperty) && _changingWay == TextChangeWay.None) - { - _changingWay = TextChangeWay.FromEditor; - SetCurrentValue(TextProperty, $"{Subject}\n\n{Description}"); - _changingWay = TextChangeWay.None; - } - } - - private async void OnSubjectTextBoxPreviewKeyDown(object _, KeyEventArgs e) - { - if (e.Key == Key.Enter || (e.Key == Key.Right && SubjectEditor.CaretIndex == Subject.Length)) - { - DescriptionEditor.Focus(); - DescriptionEditor.CaretIndex = 0; - e.Handled = true; - } - else if (e.Key == Key.V && e.KeyModifiers == (OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) - { - e.Handled = true; - - var text = await App.GetClipboardTextAsync(); - if (!string.IsNullOrWhiteSpace(text)) - { - text = text.Trim(); - - if (SubjectEditor.CaretIndex == Subject.Length) - { - var parts = text.Split('\n', 2); - if (parts.Length != 2) - { - SubjectEditor.Paste(text); - } - else - { - SubjectEditor.Paste(parts[0]); - DescriptionEditor.Focus(); - DescriptionEditor.CaretIndex = 0; - DescriptionEditor.Paste(parts[1].Trim()); - } - } - else - { - SubjectEditor.Paste(text.ReplaceLineEndings(" ")); - } - } - } - } - - private void OnDescriptionTextBoxPreviewKeyDown(object _, KeyEventArgs e) - { - if ((e.Key == Key.Back || e.Key == Key.Left) && DescriptionEditor.CaretIndex == 0) - { - SubjectEditor.Focus(); - SubjectEditor.CaretIndex = Subject.Length; - e.Handled = true; - } - } - - 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) - { - var toplevel = TopLevel.GetTopLevel(this); - if (toplevel is Window owner) - { - var vm = new ViewModels.ConventionalCommitMessageBuilder(text => Text = text); - var builder = new ConventionalCommitMessageBuilder() { DataContext = vm }; - builder.ShowDialog(owner); - } - - e.Handled = true; - } - - private async void CopyAllText(object sender, RoutedEventArgs e) - { - await App.CopyTextAsync(Text); - e.Handled = true; - } - - private TextChangeWay _changingWay = TextChangeWay.None; - } -} diff --git a/src/Views/CommitMessageToolBox.axaml b/src/Views/CommitMessageToolBox.axaml new file mode 100644 index 000000000..1c937ca7b --- /dev/null +++ b/src/Views/CommitMessageToolBox.axaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitMessageToolBox.axaml.cs b/src/Views/CommitMessageToolBox.axaml.cs new file mode 100644 index 000000000..22beffd7b --- /dev/null +++ b/src/Views/CommitMessageToolBox.axaml.cs @@ -0,0 +1,573 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +using AvaloniaEdit; +using AvaloniaEdit.CodeCompletion; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using AvaloniaEdit.Rendering; +using AvaloniaEdit.Utils; + +namespace SourceGit.Views +{ + public class CommitMessageCodeCompletionData : ICompletionData + { + public IImage Image + { + get => null; + } + + public string Text + { + get; + } + + public object Content + { + get => Text; + } + + public object Description + { + get => null; + } + + public double Priority + { + get => 0; + } + + public CommitMessageCodeCompletionData(string text) + { + Text = text; + } + + public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) + { + textArea.Document.Replace(completionSegment, Text); + } + } + + public class CommitMessageTextEditor : TextEditor + { + public static readonly StyledProperty CommitMessageProperty = + AvaloniaProperty.Register(nameof(CommitMessage), string.Empty); + + public string CommitMessage + { + get => GetValue(CommitMessageProperty); + set => SetValue(CommitMessageProperty, value); + } + + public static readonly StyledProperty PlaceholderProperty = + AvaloniaProperty.Register(nameof(Placeholder), string.Empty); + + public string Placeholder + { + get => GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); + } + + public static readonly StyledProperty SubjectLengthProperty = + AvaloniaProperty.Register(nameof(SubjectLength), 0); + + public int SubjectLength + { + get => GetValue(SubjectLengthProperty); + set => SetValue(SubjectLengthProperty, value); + } + + public static readonly StyledProperty SubjectLineBrushProperty = + AvaloniaProperty.Register(nameof(SubjectLineBrush), Brushes.Gray); + + public IBrush SubjectLineBrush + { + get => GetValue(SubjectLineBrushProperty); + set => SetValue(SubjectLineBrushProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextEditor); + + public CommitMessageTextEditor() : base(new TextArea(), new TextDocument()) + { + IsReadOnly = false; + WordWrap = true; + ShowLineNumbers = false; + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + + TextArea.TextView.Margin = new Thickness(4, 2); + TextArea.TextView.Options.EnableHyperlinks = false; + TextArea.TextView.Options.EnableEmailHyperlinks = false; + TextArea.TextView.Options.AllowScrollBelowDocument = false; + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var w = Bounds.Width; + var pen = new Pen(SubjectLineBrush) { DashStyle = DashStyle.Dash }; + + if (SubjectLength == 0) + { + var placeholder = Placeholder; + if (!string.IsNullOrEmpty(placeholder)) + { + var formatted = new FormattedText( + Placeholder, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily), + FontSize, + Brushes.Gray); + + context.DrawText(formatted, new Point(4, 2)); + + var y = 6 + formatted.Height; + context.DrawLine(pen, new Point(0, y), new Point(w, y)); + } + + return; + } + + if (TextArea.TextView is not { VisualLinesValid: true } view) + return; + + var lines = new List(); + foreach (var line in view.VisualLines) + { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + + lines.Add(line); + } + + if (lines.Count == 0) + return; + + lines.Sort((l, r) => l.StartOffset - r.StartOffset); + + for (var i = 0; i < lines.Count; i++) + { + var line = lines[i]; + if (line.FirstDocumentLine.LineNumber == _subjectEndLine) + { + var y = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - view.VerticalOffset + 4; + context.DrawLine(pen, new Point(0, y), new Point(w, y)); + return; + } + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged; + TextArea.TextView.ContextRequested += OnTextViewContextRequested; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + TextArea.TextView.ContextRequested -= OnTextViewContextRequested; + TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged; + + base.OnUnloaded(e); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == CommitMessageProperty) + { + if (!_isEditing) + Text = CommitMessage; + + var lines = CommitMessage.ReplaceLineEndings("\n").Split('\n'); + var subjectLen = 0; + var foundSubjectEnd = false; + + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i]; + if (string.IsNullOrWhiteSpace(line)) + { + if (subjectLen == 0) + continue; + + _subjectEndLine = i; + foundSubjectEnd = true; + break; + } + + var validCharLen = line.TrimEnd().Length; + if (subjectLen > 0) + subjectLen += (validCharLen + 1); + else + subjectLen = validCharLen; + } + + if (!foundSubjectEnd) + _subjectEndLine = lines.Length; + + SetCurrentValue(SubjectLengthProperty, subjectLen); + } + else if (change.Property == PlaceholderProperty && IsLoaded) + { + if (string.IsNullOrWhiteSpace(CommitMessage)) + InvalidateVisual(); + } + } + + protected override void OnTextChanged(EventArgs e) + { + base.OnTextChanged(e); + + if (!IsLoaded) + return; + + _isEditing = true; + SetCurrentValue(CommitMessageProperty, Text); + _isEditing = false; + + var caretOffset = CaretOffset; + var start = caretOffset; + for (; start > 0; start--) + { + var ch = Text[start - 1]; + if (ch == '\n') + break; + + if (!char.IsAscii(ch)) + return; + } + + if (caretOffset < start + 2) + { + _completionWnd?.Close(); + return; + } + + var word = Text.Substring(start, caretOffset - start); + var matches = new List(); + foreach (var keyword in _keywords) + { + if (keyword.StartsWith(word, StringComparison.OrdinalIgnoreCase) && keyword.Length != word.Length) + matches.Add(new(keyword)); + } + + if (matches.Count > 0) + { + if (_completionWnd == null) + { + _completionWnd = new CompletionWindow(TextArea); + _completionWnd.Closed += (_, ev) => _completionWnd = null; + _completionWnd.Show(); + } + + _completionWnd.CompletionList.CompletionData.Clear(); + _completionWnd.CompletionList.CompletionData.AddRange(matches); + _completionWnd.StartOffset = start; + _completionWnd.EndOffset = caretOffset; + } + else + { + _completionWnd?.Close(); + } + } + + private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) + { + var selection = TextArea.Selection; + var hasSelected = selection is { IsEmpty: false }; + + var copy = new MenuItem(); + copy.Header = App.Text("Copy"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.IsEnabled = hasSelected; + copy.Click += (o, ev) => + { + Copy(); + ev.Handled = true; + }; + + var cut = new MenuItem(); + cut.Header = App.Text("Cut"); + cut.Icon = App.CreateMenuIcon("Icons.Cut"); + cut.IsEnabled = hasSelected; + cut.Click += (o, ev) => + { + Cut(); + ev.Handled = true; + }; + + var paste = new MenuItem(); + paste.Header = App.Text("Paste"); + paste.Icon = App.CreateMenuIcon("Icons.Paste"); + paste.Click += (o, ev) => + { + Paste(); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(copy); + menu.Items.Add(cut); + menu.Items.Add(paste); + menu.Open(TextArea.TextView); + e.Handled = true; + } + + private void OnTextViewVisualLinesChanged(object sender, EventArgs e) + { + InvalidateVisual(); + } + + private readonly List _keywords = ["Acked-by: ", "Co-authored-by: ", "Reviewed-by: ", "Signed-off-by: ", "on-behalf-of: @", "BREAKING CHANGE: ", "Refs: "]; + private bool _isEditing = false; + private int _subjectEndLine = 0; + private CompletionWindow _completionWnd = null; + } + + public partial class CommitMessageToolBox : UserControl + { + public static readonly StyledProperty ShowAdvancedOptionsProperty = + AvaloniaProperty.Register(nameof(ShowAdvancedOptions)); + + public bool ShowAdvancedOptions + { + get => GetValue(ShowAdvancedOptionsProperty); + set => SetValue(ShowAdvancedOptionsProperty, value); + } + + public static readonly StyledProperty CommitMessageProperty = + AvaloniaProperty.Register(nameof(CommitMessage), string.Empty); + + public string CommitMessage + { + get => GetValue(CommitMessageProperty); + set => SetValue(CommitMessageProperty, value); + } + + public CommitMessageToolBox() + { + InitializeComponent(); + } + + private async void OnOpenCommitMessagePicker(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.WorkingCopy vm && ShowAdvancedOptions) + { + var repo = vm.Repository; + var foreground = this.FindResource("Brush.FG1") as IBrush; + + var menu = new ContextMenu(); + menu.MaxWidth = 480; + + var gitTemplate = await new Commands.Config(repo.FullPath).GetAsync("commit.template"); + var templateCount = repo.Settings.CommitTemplates.Count; + if (templateCount == 0 && string.IsNullOrEmpty(gitTemplate)) + { + menu.Items.Add(new MenuItem() + { + Header = App.Text("WorkingCopy.NoCommitTemplates"), + Icon = App.CreateMenuIcon("Icons.Code"), + IsEnabled = false + }); + } + else + { + for (int i = 0; i < templateCount; i++) + { + var icon = App.CreateMenuIcon("Icons.Code"); + icon.Fill = foreground; + + var template = repo.Settings.CommitTemplates[i]; + var item = new MenuItem(); + item.Header = App.Text("WorkingCopy.UseCommitTemplate", template.Name); + item.Icon = icon; + item.Click += (_, ev) => + { + vm.ApplyCommitMessageTemplate(template); + ev.Handled = true; + }; + menu.Items.Add(item); + } + + if (!string.IsNullOrEmpty(gitTemplate)) + { + if (!Path.IsPathRooted(gitTemplate)) + gitTemplate = Native.OS.GetAbsPath(repo.FullPath, gitTemplate); + + var friendlyName = gitTemplate; + if (!OperatingSystem.IsWindows()) + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; + if (gitTemplate.StartsWith(home, StringComparison.Ordinal)) + friendlyName = $"~{gitTemplate.AsSpan(prefixLen)}"; + } + + var icon = App.CreateMenuIcon("Icons.Code"); + icon.Fill = foreground; + + var gitTemplateItem = new MenuItem(); + gitTemplateItem.Header = App.Text("WorkingCopy.UseCommitTemplate", friendlyName); + gitTemplateItem.Icon = icon; + gitTemplateItem.Click += (_, ev) => + { + if (File.Exists(gitTemplate)) + vm.CommitMessage = File.ReadAllText(gitTemplate); + ev.Handled = true; + }; + menu.Items.Add(gitTemplateItem); + } + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + + var historiesCount = repo.Settings.CommitMessages.Count; + if (historiesCount == 0) + { + menu.Items.Add(new MenuItem() + { + Header = App.Text("WorkingCopy.NoCommitHistories"), + Icon = App.CreateMenuIcon("Icons.Histories"), + IsEnabled = false + }); + } + else + { + for (int i = 0; i < historiesCount; i++) + { + var dup = repo.Settings.CommitMessages[i].Trim(); + var header = new TextBlock() + { + Text = dup.ReplaceLineEndings(" "), + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis + }; + + var icon = App.CreateMenuIcon("Icons.Histories"); + icon.Fill = foreground; + + var item = new MenuItem(); + item.Header = header; + item.Icon = icon; + item.Click += (_, ev) => + { + vm.CommitMessage = dup; + ev.Handled = true; + }; + + menu.Items.Add(item); + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + + var clearIcon = App.CreateMenuIcon("Icons.Clear"); + clearIcon.Fill = foreground; + + var clearHistoryItem = new MenuItem(); + clearHistoryItem.Header = App.Text("WorkingCopy.ClearCommitHistories"); + clearHistoryItem.Icon = clearIcon; + clearHistoryItem.Click += async (_, ev) => + { + await vm.ClearCommitMessageHistoryAsync(); + ev.Handled = true; + }; + + menu.Items.Add(clearHistoryItem); + } + + button.IsEnabled = false; + menu.Placement = PlacementMode.TopEdgeAlignedLeft; + menu.Closed += (o, ev) => button.IsEnabled = true; + menu.Open(button); + } + + e.Handled = true; + } + + private async void OnOpenOpenAIHelper(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.WorkingCopy vm && sender is Button button && ShowAdvancedOptions) + { + var repo = vm.Repository; + + if (vm.Staged == null || vm.Staged.Count == 0) + { + App.RaiseException(repo.FullPath, "No files added to commit!"); + return; + } + + var services = repo.GetPreferredOpenAIServices(); + if (services.Count == 0) + { + App.RaiseException(repo.FullPath, "Bad configuration for OpenAI"); + return; + } + + if (services.Count == 1) + { + await App.ShowDialog(new ViewModels.AIAssistant(repo, services[0], vm.Staged)); + return; + } + + var menu = new ContextMenu(); + foreach (var service in services) + { + var dup = service; + var item = new MenuItem(); + item.Header = service.Name; + item.Click += async (_, ev) => + { + await App.ShowDialog(new ViewModels.AIAssistant(repo, dup, vm.Staged)); + ev.Handled = true; + }; + + menu.Items.Add(item); + } + + button.IsEnabled = false; + menu.Placement = PlacementMode.TopEdgeAlignedLeft; + menu.Closed += (o, ev) => button.IsEnabled = true; + menu.Open(button); + } + + e.Handled = true; + } + + private void OnOpenConventionalCommitHelper(object _, RoutedEventArgs e) + { + var owner = TopLevel.GetTopLevel(this) as Window; + if (owner == null) + return; + + var conventionalTypesOverride = owner switch + { + Launcher { DataContext: ViewModels.Launcher { ActivePage: { Data: ViewModels.Repository repo } } } => repo.Settings.ConventionalTypesOverride, + RepositoryConfigure { DataContext: ViewModels.RepositoryConfigure config } => config.ConventionalTypesOverride, + CommitMessageEditor editor => editor.ConventionalTypesOverride, + _ => string.Empty + }; + + var vm = new ViewModels.ConventionalCommitMessageBuilder(conventionalTypesOverride, text => CommitMessage = text); + var builder = new ConventionalCommitMessageBuilder() { DataContext = vm }; + builder.Show(owner); + + e.Handled = true; + } + } +} diff --git a/src/Views/CommitRefsPresenter.cs b/src/Views/CommitRefsPresenter.cs index d221c155a..a6ed72646 100644 --- a/src/Views/CommitRefsPresenter.cs +++ b/src/Views/CommitRefsPresenter.cs @@ -116,18 +116,18 @@ public override void Render(DrawingContext context) var fg = Foreground; var bg = Background; var allowWrap = AllowWrap; - var x = 1.0; - var y = 0.0; + var x = 1.5; + var y = 0.5; foreach (var item in _items) { - if (allowWrap && x > 1.0 && x + item.Width > Bounds.Width) + if (allowWrap && x > 1.5 && x + item.Width > Bounds.Width) { - x = 1.0; + x = 1.5; y += 20.0; } - var entireRect = new RoundedRect(new Rect(x, y, item.Width, 16), new CornerRadius(2)); + var entireRect = new RoundedRect(new Rect(x, y, item.Width, 16), new CornerRadius(4)); if (item.IsHead) { @@ -147,7 +147,7 @@ public override void Render(DrawingContext context) if (bg != null) context.DrawRectangle(bg, null, entireRect); - var labelRect = new RoundedRect(new Rect(x + 16, y, 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, 4, 4, 0)); using (context.PushOpacity(.2)) context.DrawRectangle(item.Brush, null, labelRect); @@ -183,7 +183,7 @@ protected override Size MeasureOverride(Size availableSize) var typeface = new Typeface(FontFamily); var typefaceBold = new Typeface(FontFamily, FontStyle.Normal, FontWeight.Bold); var fg = Foreground; - var normalBG = UseGraphColor ? commit.Brush : Brushes.Gray; + var normalBG = UseGraphColor ? Models.CommitGraph.Pens[commit.Color].Brush : Brushes.Gray; var labelSize = FontSize; var requiredHeight = 16.0; var x = 0.0; diff --git a/src/Views/CommitRelationTracking.axaml b/src/Views/CommitRelationTracking.axaml index 9d036e10b..53906bfe3 100644 --- a/src/Views/CommitRelationTracking.axaml +++ b/src/Views/CommitRelationTracking.axaml @@ -23,7 +23,7 @@ - + diff --git a/src/Views/CommitStatusIndicator.cs b/src/Views/CommitStatusIndicator.cs index 7073011a0..49d74c98f 100644 --- a/src/Views/CommitStatusIndicator.cs +++ b/src/Views/CommitStatusIndicator.cs @@ -52,14 +52,13 @@ public override void Render(DrawingContext context) protected override Size MeasureOverride(Size availableSize) { - if (DataContext is Models.Commit commit && CurrentBranch is not null) + if (DataContext is Models.Commit commit && CurrentBranch is { } b) { var sha = commit.SHA; - var track = CurrentBranch.TrackStatus; - if (track.Ahead.Contains(sha)) + if (b.Ahead.Contains(sha)) _status = Status.Ahead; - else if (track.Behind.Contains(sha)) + else if (b.Behind.Contains(sha)) _status = Status.Behind; else _status = Status.Normal; diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs index 5fc4cb5d7..622d5ec0c 100644 --- a/src/Views/CommitSubjectPresenter.cs +++ b/src/Views/CommitSubjectPresenter.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; using System.Globalization; using System.Text.RegularExpressions; @@ -75,6 +77,15 @@ public IBrush LinkForeground set => SetValue(LinkForegroundProperty, value); } + public static readonly StyledProperty ShowStrikethroughProperty = + AvaloniaProperty.Register(nameof(ShowStrikethrough), false); + + public bool ShowStrikethrough + { + get => GetValue(ShowStrikethroughProperty); + set => SetValue(ShowStrikethroughProperty, value); + } + public static readonly StyledProperty SubjectProperty = AvaloniaProperty.Register(nameof(Subject)); @@ -84,13 +95,13 @@ public string Subject set => SetValue(SubjectProperty, value); } - public static readonly StyledProperty> IssueTrackerRulesProperty = - AvaloniaProperty.Register>(nameof(IssueTrackerRules)); + public static readonly StyledProperty> IssueTrackersProperty = + AvaloniaProperty.Register>(nameof(IssueTrackers)); - public AvaloniaList IssueTrackerRules + public AvaloniaList IssueTrackers { - get => GetValue(IssueTrackerRulesProperty); - set => SetValue(IssueTrackerRulesProperty, value); + get => GetValue(IssueTrackersProperty); + set => SetValue(IssueTrackersProperty, value); } public override void Render(DrawingContext context) @@ -114,10 +125,11 @@ public override void Render(DrawingContext context) { var height = Bounds.Height; var width = Bounds.Width; + var maxX = 0.0; foreach (var inline in _inlines) { if (inline.X > width) - return; + break; if (inline.Element is { Type: Models.InlineElementType.Code }) { @@ -125,12 +137,17 @@ public override void Render(DrawingContext context) 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)); + maxX = Math.Min(width, inline.X + inline.Text.WidthIncludingTrailingWhitespace + 8); } else { context.DrawText(inline.Text, new Point(inline.X, (height - inline.Text.Height) * 0.5)); + maxX = Math.Min(width, inline.X + inline.Text.WidthIncludingTrailingWhitespace); } } + + if (ShowStrikethrough) + context.DrawLine(new Pen(Foreground), new Point(0, height * 0.5), new Point(maxX, height * 0.5)); } } @@ -138,45 +155,21 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { base.OnPropertyChanged(change); - if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty) + if (change.Property == SubjectProperty) { - _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); - foreach (Match match in codeMatches) - { - 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; + GenerateInlineElements(); InvalidateVisual(); } + else if (change.Property == IssueTrackersProperty) + { + if (change.OldValue is AvaloniaList oldValue) + oldValue.CollectionChanged -= OnIssueTrackersChanged; + if (change.NewValue is AvaloniaList newValue) + newValue.CollectionChanged += OnIssueTrackersChanged; + + OnIssueTrackersChanged(null, null); + } else if (change.Property == FontFamilyProperty || change.Property == CodeFontFamilyProperty || change.Property == FontSizeProperty || @@ -187,7 +180,8 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang _needRebuildInlines = true; InvalidateVisual(); } - else if (change.Property == InlineCodeBackgroundProperty) + else if (change.Property == InlineCodeBackgroundProperty || + change.Property == ShowStrikethroughProperty) { InvalidateVisual(); } @@ -230,6 +224,51 @@ protected override void OnPointerExited(PointerEventArgs e) ClearHoveredIssueLink(); } + private void OnIssueTrackersChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _needRebuildInlines = true; + GenerateInlineElements(); + InvalidateVisual(); + } + + private void GenerateInlineElements() + { + _elements.Clear(); + ClearHoveredIssueLink(); + + var subject = Subject; + if (string.IsNullOrEmpty(subject)) + { + _needRebuildInlines = true; + InvalidateVisual(); + return; + } + + var rules = IssueTrackers ?? []; + 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); + foreach (Match match in codeMatches) + { + 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(); + } + private void GenerateFormattedTextElements() { _inlines.Clear(); diff --git a/src/Views/ConfigureCustomActionControls.axaml b/src/Views/ConfigureCustomActionControls.axaml index 1e7c9fa83..e24bc0101 100644 --- a/src/Views/ConfigureCustomActionControls.axaml +++ b/src/Views/ConfigureCustomActionControls.axaml @@ -177,7 +177,7 @@ diff --git a/src/Views/Confirm.axaml.cs b/src/Views/Confirm.axaml.cs index f214da23e..3756fa3a6 100644 --- a/src/Views/Confirm.axaml.cs +++ b/src/Views/Confirm.axaml.cs @@ -1,16 +1,9 @@ -using System; using Avalonia.Interactivity; namespace SourceGit.Views { public partial class Confirm : ChromelessWindow { - public Action OnSure - { - get; - set; - } - public Confirm() { InitializeComponent(); @@ -18,7 +11,6 @@ public Confirm() private void Sure(object _1, RoutedEventArgs _2) { - OnSure?.Invoke(); Close(true); } diff --git a/src/Views/ConfirmEmptyCommit.axaml b/src/Views/ConfirmEmptyCommit.axaml index bfa42ef0e..2fee08345 100644 --- a/src/Views/ConfirmEmptyCommit.axaml +++ b/src/Views/ConfirmEmptyCommit.axaml @@ -3,10 +3,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:v="using:SourceGit.Views" - xmlns:vm="using:SourceGit.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.ConfirmEmptyCommit" - x:DataType="vm:ConfirmEmptyCommit" x:Name="ThisControl" Icon="/App.ico" Title="{DynamicResource Text.Warn}" @@ -39,15 +37,15 @@ - + - @@ -69,16 +64,13 @@ - - - - - - - - - - + + + + + + + - - - - @@ -142,7 +119,7 @@ - + @@ -150,8 +127,7 @@ @@ -172,8 +148,8 @@ ToolTip.Tip="{DynamicResource Text.Diff.ToggleWordWrap}"> - - + + @@ -197,7 +173,7 @@ @@ -234,16 +210,16 @@ - + - + - + - + @@ -259,16 +235,16 @@ - + - + - + - + @@ -286,7 +262,7 @@ - + @@ -311,7 +287,7 @@ - + @@ -361,10 +337,8 @@ - - + + diff --git a/src/Views/DiffView.axaml.cs b/src/Views/DiffView.axaml.cs index 53fe08030..ebd1eca41 100644 --- a/src/Views/DiffView.axaml.cs +++ b/src/Views/DiffView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -12,50 +11,35 @@ public DiffView() InitializeComponent(); } + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (DataContext is ViewModels.DiffContext vm) + vm.CheckSettings(); + } + private void OnGotoFirstChange(object _, RoutedEventArgs e) { - this.FindDescendantOfType()?.GotoFirstChange(); + this.FindDescendantOfType()?.GotoChange(ViewModels.BlockNavigationDirection.First); e.Handled = true; } private void OnGotoPrevChange(object _, RoutedEventArgs e) { - this.FindDescendantOfType()?.GotoPrevChange(); + this.FindDescendantOfType()?.GotoChange(ViewModels.BlockNavigationDirection.Prev); e.Handled = true; } private void OnGotoNextChange(object _, RoutedEventArgs e) { - this.FindDescendantOfType()?.GotoNextChange(); + this.FindDescendantOfType()?.GotoChange(ViewModels.BlockNavigationDirection.Next); 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(); - - 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(); + this.FindDescendantOfType()?.GotoChange(ViewModels.BlockNavigationDirection.Last); e.Handled = true; } } diff --git a/src/Views/DirHistories.axaml b/src/Views/DirHistories.axaml index b06fa07d2..c9aef4e60 100644 --- a/src/Views/DirHistories.axaml +++ b/src/Views/DirHistories.axaml @@ -38,7 +38,7 @@ - + @@ -85,9 +85,8 @@ - + - + - + diff --git a/src/Views/Discard.axaml b/src/Views/Discard.axaml index 8aee9a1cc..748e1af66 100644 --- a/src/Views/Discard.axaml +++ b/src/Views/Discard.axaml @@ -8,9 +8,15 @@ x:Class="SourceGit.Views.Discard" x:DataType="vm:Discard"> - + + + + + diff --git a/src/Views/DropHead.axaml b/src/Views/DropHead.axaml new file mode 100644 index 000000000..91b41b852 --- /dev/null +++ b/src/Views/DropHead.axaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Squash.axaml.cs b/src/Views/DropHead.axaml.cs similarity index 62% rename from src/Views/Squash.axaml.cs rename to src/Views/DropHead.axaml.cs index 552faec25..5dc43cf14 100644 --- a/src/Views/Squash.axaml.cs +++ b/src/Views/DropHead.axaml.cs @@ -2,9 +2,9 @@ namespace SourceGit.Views { - public partial class Squash : UserControl + public partial class DropHead : UserControl { - public Squash() + public DropHead() { InitializeComponent(); } diff --git a/src/Views/DropStash.axaml b/src/Views/DropStash.axaml index aa5de85aa..63759d55a 100644 --- a/src/Views/DropStash.axaml +++ b/src/Views/DropStash.axaml @@ -7,9 +7,16 @@ x:Class="SourceGit.Views.DropStash" x:DataType="vm:DropStash"> - + + + + + + - + diff --git a/src/Views/EditBranchDescription.axaml b/src/Views/EditBranchDescription.axaml new file mode 100644 index 000000000..eb3766226 --- /dev/null +++ b/src/Views/EditBranchDescription.axaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Views/EditBranchDescription.axaml.cs b/src/Views/EditBranchDescription.axaml.cs new file mode 100644 index 000000000..559dfb276 --- /dev/null +++ b/src/Views/EditBranchDescription.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class EditBranchDescription : UserControl + { + public EditBranchDescription() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/EditRemote.axaml b/src/Views/EditRemote.axaml index 7d64a53a6..251bdc7ad 100644 --- a/src/Views/EditRemote.axaml +++ b/src/Views/EditRemote.axaml @@ -8,9 +8,15 @@ x:Class="SourceGit.Views.EditRemote" x:DataType="vm:EditRemote"> - + + + + + - - - - - + + + + + + + diff --git a/src/Views/EditRepositoryNode.axaml b/src/Views/EditRepositoryNode.axaml index 615e3f110..4b42434dd 100644 --- a/src/Views/EditRepositoryNode.axaml +++ b/src/Views/EditRepositoryNode.axaml @@ -10,15 +10,21 @@ x:Class="SourceGit.Views.EditRepositoryNode" x:DataType="vm:EditRepositoryNode"> - + + - + + + + @@ -34,16 +40,16 @@ - + - + diff --git a/src/Views/EnhancedSelectableTextBlock.cs b/src/Views/EnhancedSelectableTextBlock.cs deleted file mode 100644 index 183b7021e..000000000 --- a/src/Views/EnhancedSelectableTextBlock.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 index 4f0c6c6d9..b0525c123 100644 --- a/src/Views/ExecuteCustomAction.axaml +++ b/src/Views/ExecuteCustomAction.axaml @@ -10,9 +10,15 @@ x:DataType="vm:ExecuteCustomAction" Loaded="OnLoaded"> - + + + + + - + @@ -50,6 +56,13 @@ + + + + + + + diff --git a/src/Views/Fetch.axaml b/src/Views/Fetch.axaml index 676693808..1a60f3eb7 100644 --- a/src/Views/Fetch.axaml +++ b/src/Views/Fetch.axaml @@ -8,10 +8,17 @@ x:Class="SourceGit.Views.Fetch" x:DataType="vm:Fetch"> - - + + + + + + + + SelectedItem="{Binding SelectedRemote, Mode=TwoWay}"> + + + + + + @@ -38,8 +50,10 @@ ToolTip.Tip="--force"/> - + + + + + + diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index b7914c3d2..af5e950d3 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -38,7 +38,7 @@ - + @@ -84,9 +84,8 @@ - + - + - + @@ -146,7 +145,6 @@ + ToolTip.Tip="{DynamicResource Text.Open}"> @@ -201,15 +199,15 @@ - + - + - - + + - + diff --git a/src/Views/FileHistories.axaml.cs b/src/Views/FileHistories.axaml.cs index 24c450861..793fd4114 100644 --- a/src/Views/FileHistories.axaml.cs +++ b/src/Views/FileHistories.axaml.cs @@ -51,11 +51,19 @@ private async void OnSaveAsPatch(object sender, RoutedEventArgs e) options.DefaultExtension = ".patch"; options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; - var storageFile = await StorageProvider.SaveFilePickerAsync(options); - if (storageFile != null) - await compare.SaveAsPatch(storageFile.Path.LocalPath); + try + { + var storageFile = await StorageProvider.SaveFilePickerAsync(options); + if (storageFile != null) + await compare.SaveAsPatch(storageFile.Path.LocalPath); + + NotifyDonePanel.IsVisible = true; + } + catch (Exception exception) + { + App.RaiseException(string.Empty, $"Failed to save as patch: {exception.Message}"); + } - NotifyDonePanel.IsVisible = true; e.Handled = true; } } diff --git a/src/Views/FileHistoryCommandPalette.axaml b/src/Views/FileHistoryCommandPalette.axaml new file mode 100644 index 000000000..ecc3fed31 --- /dev/null +++ b/src/Views/FileHistoryCommandPalette.axaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/FileHistoryCommandPalette.axaml.cs b/src/Views/FileHistoryCommandPalette.axaml.cs new file mode 100644 index 000000000..2361e9712 --- /dev/null +++ b/src/Views/FileHistoryCommandPalette.axaml.cs @@ -0,0 +1,63 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class FileHistoryCommandPalette : UserControl + { + public FileHistoryCommandPalette() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (DataContext is not ViewModels.FileHistoryCommandPalette vm) + return; + + if (e.Key == Key.Enter) + { + vm.Launch(); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + if (FileListBox.IsKeyboardFocusWithin) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + else if (e.Key == Key.Down || e.Key == Key.Tab) + { + if (FilterTextBox.IsKeyboardFocusWithin) + { + if (vm.VisibleFiles.Count > 0) + FileListBox.Focus(NavigationMethod.Directional); + + e.Handled = true; + return; + } + + if (FileListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + } + + private void OnItemTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.FileHistoryCommandPalette vm) + { + vm.Launch(); + e.Handled = true; + } + } + } +} diff --git a/src/Views/FilterModeInGraph.axaml b/src/Views/FilterModeInGraph.axaml index b9e72b54d..d8d4b165e 100644 --- a/src/Views/FilterModeInGraph.axaml +++ b/src/Views/FilterModeInGraph.axaml @@ -8,7 +8,9 @@ x:DataType="vm:FilterModeInGraph"> + Text="{DynamicResource Text.Repository.FilterCommits}" + FontWeight="Bold" + Foreground="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"/> diff --git a/src/Views/FilterModeSwitchButton.axaml b/src/Views/FilterModeSwitchButton.axaml index b202c4346..7e3b21d73 100644 --- a/src/Views/FilterModeSwitchButton.axaml +++ b/src/Views/FilterModeSwitchButton.axaml @@ -11,11 +11,12 @@ Padding="0" Background="Transparent" VerticalContentAlignment="Center" - Click="OnChangeFilterModeButtonClicked"> + Click="OnChangeFilterModeButtonClicked" + ToolTip.Tip="{DynamicResource Text.Repository.FilterCommits}"> - { - 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; - }; + menu.Open(button); + e.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) => + private void FillContextMenuForTag(ContextMenu menu, ViewModels.Repository repo, Models.Tag tag, Models.FilterMode current) + { + if (current != Models.FilterMode.None) + { + var unset = new MenuItem(); + unset.Header = App.Text("Repository.FilterCommits.Default"); + unset.Click += (_, ev) => { - repo.SetTagFilterMode(tag, Models.FilterMode.Excluded); + repo.SetTagFilterMode(tag, Models.FilterMode.None); ev.Handled = true; }; - menu.Items.Add(include); - menu.Items.Add(exclude); + menu.Items.Add(unset); + menu.Items.Add(new MenuItem() { Header = "-" }); } - else if (DataContext is ViewModels.BranchTreeNode node) + else { - mode = node.FilterMode; + IsContextMenuOpening = true; + menu.Closed += (_, _) => IsContextMenuOpening = false; + } - 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 include = new MenuItem(); + include.Icon = App.CreateMenuIcon("Icons.Filter"); + include.Header = App.Text("Repository.FilterCommits.Include"); + include.IsEnabled = current != 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 = current != Models.FilterMode.Excluded; + exclude.Click += (_, ev) => + { + repo.SetTagFilterMode(tag, Models.FilterMode.Excluded); + 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) => + menu.Items.Add(include); + menu.Items.Add(exclude); + } + + private void FillContextMenuForBranch(ContextMenu menu, ViewModels.Repository repo, ViewModels.BranchTreeNode node, Models.FilterMode current) + { + if (current != Models.FilterMode.None) + { + var unset = new MenuItem(); + unset.Header = App.Text("Repository.FilterCommits.Default"); + unset.Click += (_, ev) => { - repo.SetBranchFilterMode(node, Models.FilterMode.Excluded, false, true); + repo.SetBranchFilterMode(node, Models.FilterMode.None, false, true); ev.Handled = true; }; - menu.Items.Add(include); - menu.Items.Add(exclude); + menu.Items.Add(unset); + menu.Items.Add(new MenuItem() { Header = "-" }); } - - if (mode == Models.FilterMode.None) + else { IsContextMenuOpening = true; menu.Closed += (_, _) => IsContextMenuOpening = false; } - menu.Open(button); - e.Handled = true; + var include = new MenuItem(); + include.Icon = App.CreateMenuIcon("Icons.Filter"); + include.Header = App.Text("Repository.FilterCommits.Include"); + include.IsEnabled = current != 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 = current != 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); } } } diff --git a/src/Views/GitFlowFinish.axaml b/src/Views/GitFlowFinish.axaml index 43fde72d2..66097c0f0 100644 --- a/src/Views/GitFlowFinish.axaml +++ b/src/Views/GitFlowFinish.axaml @@ -8,18 +8,25 @@ x:Class="SourceGit.Views.GitFlowFinish" x:DataType="vm:GitFlowFinish"> - - - + + + + + + + + - - - + + + + + + + + + + + + + + + @@ -105,7 +122,7 @@ - + @@ -135,15 +151,15 @@ + IssueTrackers="{Binding $parent[v:Histories].IssueTrackers}" + FontWeight="{Binding IsCurrentHead, Converter={x:Static c:BoolConverters.IsBoldToFontWeight}}" + Opacity="{Binding IsMerged, Converter={x:Static c:BoolConverters.IsMergedToOpacity}}"/> @@ -169,15 +185,14 @@ HorizontalAlignment="Left" VerticalAlignment="Center" User="{Binding Author}" - Opacity="{Binding Opacity}" + Opacity="{Binding IsMerged, Converter={x:Static c:BoolConverters.IsMergedToOpacity}}" IsHitTestVisible="False"/> - - + + @@ -192,10 +207,9 @@ - + @@ -218,9 +232,8 @@ - @@ -240,6 +253,17 @@ IsHitTestVisible="False" ClipToBounds="True"/> + + + + diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index bb9433562..3a381bae1 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -8,6 +8,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Platform.Storage; using Avalonia.VisualTree; namespace SourceGit.Views @@ -94,13 +95,13 @@ public Models.Bisect Bisect set => SetValue(BisectProperty, value); } - public static readonly StyledProperty> IssueTrackerRulesProperty = - AvaloniaProperty.Register>(nameof(IssueTrackerRules)); + public static readonly StyledProperty> IssueTrackersProperty = + AvaloniaProperty.Register>(nameof(IssueTrackers)); - public AvaloniaList IssueTrackerRules + public AvaloniaList IssueTrackers { - get => GetValue(IssueTrackerRulesProperty); - set => SetValue(IssueTrackerRulesProperty, value); + get => GetValue(IssueTrackersProperty); + set => SetValue(IssueTrackersProperty, value); } public static readonly StyledProperty OnlyHighlightCurrentBranchProperty = @@ -121,6 +122,15 @@ public long NavigationId set => SetValue(NavigationIdProperty, value); } + public static readonly StyledProperty IsScrollToTopVisibleProperty = + AvaloniaProperty.Register(nameof(IsScrollToTopVisible)); + + public bool IsScrollToTopVisible + { + get => GetValue(IsScrollToTopVisibleProperty); + set => SetValue(IsScrollToTopVisibleProperty, value); + } + public Histories() { InitializeComponent(); @@ -175,10 +185,12 @@ private void OnCommitListLayoutUpdated(object _1, EventArgs _2) } } + SetCurrentValue(IsScrollToTopVisibleProperty, startY >= rowHeight); + var clipWidth = dataGrid.Columns[0].ActualWidth - 4; - if (_lastGraphStartY != startY || - _lastGraphClipWidth != clipWidth || - _lastGraphRowHeight != rowHeight) + if (Math.Abs(_lastGraphStartY - startY) > 0.01 || + Math.Abs(_lastGraphClipWidth - clipWidth) > 0.01 || + Math.Abs(_lastGraphRowHeight - rowHeight) > 0.01) { _lastGraphStartY = startY; _lastGraphClipWidth = clipWidth; @@ -188,6 +200,12 @@ private void OnCommitListLayoutUpdated(object _1, EventArgs _2) } } + private void OnScrollToTopPointerPressed(object sender, PointerPressedEventArgs e) + { + if (DataContext is ViewModels.Histories histories) + CommitListContainer.ScrollIntoView(histories.Commits[0], null); + } + private void OnCommitListSelectionChanged(object _, SelectionChangedEventArgs e) { if (DataContext is ViewModels.Histories histories) @@ -198,10 +216,13 @@ private void OnCommitListSelectionChanged(object _, SelectionChangedEventArgs e) private void OnCommitListContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.Histories histories && - sender is DataGrid { SelectedItems: { } selected } dataGrid && + if (sender is DataGrid { SelectedItems: { } selected } dataGrid && e.Source is Control { DataContext: Models.Commit }) { + var repoView = this.FindAncestorOfType(); + if (repoView is not { DataContext: ViewModels.Repository repo }) + return; + var commits = new List(); for (var i = selected.Count - 1; i >= 0; i--) { @@ -209,10 +230,15 @@ private void OnCommitListContextRequested(object sender, ContextRequestedEventAr commits.Add(c); } - if (selected.Count > 0) + if (selected.Count > 1) + { + var menu = CreateContextMenuForMultipleCommits(repo, commits); + menu.Open(dataGrid); + } + else if (selected.Count == 1) { - var menu = histories.CreateContextMenuForSelectedCommits(commits, c => dataGrid.SelectedItems.Add(c)); - menu?.Open(dataGrid); + var menu = CreateContextMenuForSingleCommit(repo, commits[0]); + menu.Open(dataGrid); } } @@ -266,13 +292,11 @@ private async void OnCommitListKeyDown(object sender, KeyEventArgs e) repo.ShowPopup(new ViewModels.CreateTag(repo, commit)); e.Handled = true; } - - return; } } } - private void OnCommitListDoubleTapped(object sender, TappedEventArgs e) + private async void OnCommitListDoubleTapped(object sender, TappedEventArgs e) { e.Handled = true; @@ -284,13 +308,952 @@ sender is DataGrid grid && if (e.Source is CommitRefsPresenter crp) { var decorator = crp.DecoratorAt(e.GetPosition(crp)); - if (histories.CheckoutBranchByDecorator(decorator)) + var succ = await histories.CheckoutBranchByDecoratorAsync(decorator); + if (succ) return; } if (e.Source is Control { DataContext: Models.Commit c }) - histories.CheckoutBranchByCommit(c); + await histories.CheckoutBranchByCommitAsync(c); + } + } + + private ContextMenu CreateContextMenuForMultipleCommits(ViewModels.Repository repo, List selected) + { + var canCherryPick = true; + var canMerge = true; + + foreach (var c in selected) + { + if (c.IsMerged) + { + canMerge = false; + canCherryPick = false; + } + else if (c.Parents.Count > 1) + { + canCherryPick = false; + } + } + + var menu = new ContextMenu(); + + if (!repo.IsBare) + { + if (canCherryPick) + { + var cherryPick = new MenuItem(); + cherryPick.Header = App.Text("CommitCM.CherryPickMultiple"); + cherryPick.Icon = App.CreateMenuIcon("Icons.CherryPick"); + cherryPick.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CherryPick(repo, selected)); + e.Handled = true; + }; + menu.Items.Add(cherryPick); + } + + if (canMerge) + { + var merge = new MenuItem(); + merge.Header = App.Text("CommitCM.MergeMultiple"); + merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.MergeMultiple(repo, selected)); + e.Handled = true; + }; + menu.Items.Add(merge); + } + + if (canCherryPick || canMerge) + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var saveToPatch = new MenuItem(); + saveToPatch.Icon = App.CreateMenuIcon("Icons.Diff"); + saveToPatch.Header = App.Text("CommitCM.SaveAsPatch"); + saveToPatch.Click += async (_, e) => + { + var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider; + if (storageProvider == null) + return; + + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + try + { + var picker = await storageProvider.OpenFolderPickerAsync(options); + if (picker.Count == 1) + { + var folder = picker[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder.Path.ToString(); + var succ = false; + for (var i = 0; i < selected.Count; i++) + { + succ = await repo.SaveCommitAsPatchAsync(selected[i], folderPath, i); + if (!succ) + break; + } + + if (succ) + App.SendNotification(repo.FullPath, App.Text("SaveAsPatchSuccess")); + } + } + catch (Exception exception) + { + App.RaiseException(repo.FullPath, $"Failed to save as patch: {exception.Message}"); + } + + e.Handled = true; + }; + menu.Items.Add(saveToPatch); + menu.Items.Add(new MenuItem() { Header = "-" }); + + var copyShas = new MenuItem(); + copyShas.Header = App.Text("CommitCM.CopySHA"); + copyShas.Icon = App.CreateMenuIcon("Icons.Hash"); + copyShas.Click += async (_, e) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(c.SHA); + + await App.CopyTextAsync(builder.ToString()); + e.Handled = true; + }; + + var copyInfos = new MenuItem(); + copyInfos.Header = App.Text("CommitCM.CopySHA") + " - " + App.Text("CommitCM.CopySubject"); + copyInfos.Icon = App.CreateMenuIcon("Icons.ShaAndSubject"); + copyInfos.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyInfos.Click += async (_, e) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.Append(c.SHA.AsSpan(0, 10)).Append(" - ").AppendLine(c.Subject); + + await App.CopyTextAsync(builder.ToString()); + e.Handled = true; + }; + + var copyMessage = new MenuItem(); + copyMessage.Header = App.Text("CommitCM.CopyCommitMessage"); + copyMessage.Icon = App.CreateMenuIcon("Icons.Info"); + copyMessage.Click += async (_, e) => + { + var vm = DataContext as ViewModels.Histories; + var messages = new List(); + foreach (var c in selected) + { + var message = await vm.GetCommitFullMessageAsync(c); + messages.Add(message); + } + + await App.CopyTextAsync(string.Join("\n-----\n", messages)); + e.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = App.Text("Copy"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Items.Add(copyShas); + copy.Items.Add(copyInfos); + copy.Items.Add(copyMessage); + menu.Items.Add(copy); + return menu; + } + + private ContextMenu CreateContextMenuForSingleCommit(ViewModels.Repository repo, Models.Commit commit) + { + var current = repo.CurrentBranch; + var vm = DataContext as ViewModels.Histories; + if (current == null || vm == null) + return null; + + var menu = new ContextMenu(); + var tags = new List(); + var isHead = commit.IsCurrentHead; + + if (commit.HasDecorators) + { + foreach (var d in commit.Decorators) + { + switch (d.Type) + { + case Models.DecoratorType.CurrentBranchHead: + FillCurrentBranchMenu(menu, repo, current); + break; + case Models.DecoratorType.LocalBranchHead: + var lb = repo.Branches.Find(x => x.IsLocal && d.Name == x.Name); + FillOtherLocalBranchMenu(menu, repo, lb, current, commit.IsMerged); + break; + case Models.DecoratorType.RemoteBranchHead: + var rb = repo.Branches.Find(x => !x.IsLocal && d.Name == x.FriendlyName); + FillRemoteBranchMenu(menu, repo, rb, current, commit.IsMerged); + break; + case Models.DecoratorType.Tag: + var t = repo.Tags.Find(x => x.Name == d.Name); + if (t != null) + tags.Add(t); + break; + } + } + + if (menu.Items.Count > 0) + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + if (tags.Count > 0) + { + foreach (var tag in tags) + FillTagMenu(menu, repo, tag, current, commit.IsMerged); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var createBranch = new MenuItem(); + createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); + createBranch.Header = App.Text("CreateBranch"); + createBranch.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+B" : "Ctrl+Shift+B"; + createBranch.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CreateBranch(repo, commit)); + e.Handled = true; + }; + menu.Items.Add(createBranch); + + var createTag = new MenuItem(); + createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); + createTag.Header = App.Text("CreateTag"); + createTag.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+T" : "Ctrl+Shift+T"; + createTag.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CreateTag(repo, commit)); + e.Handled = true; + }; + menu.Items.Add(createTag); + menu.Items.Add(new MenuItem() { Header = "-" }); + + if (!repo.IsBare) + { + var target = commit.GetFriendlyName(); + + if (isHead) + { + var reword = new MenuItem(); + reword.Header = App.Text("CommitCM.Reword"); + reword.Icon = App.CreateMenuIcon("Icons.Edit"); + reword.Click += async (_, e) => + { + await vm.RewordHeadAsync(commit); + e.Handled = true; + }; + menu.Items.Add(reword); + + var squash = new MenuItem(); + squash.Header = App.Text("CommitCM.Squash"); + squash.Icon = App.CreateMenuIcon("Icons.SquashIntoParent"); + squash.IsEnabled = commit.Parents.Count == 1; + squash.Click += async (_, e) => + { + await vm.SquashOrFixupHeadAsync(commit, false); + e.Handled = true; + }; + menu.Items.Add(squash); + + var fixup = new MenuItem(); + fixup.Header = App.Text("CommitCM.Fixup"); + fixup.Icon = App.CreateMenuIcon("Icons.Fix"); + fixup.IsEnabled = commit.Parents.Count == 1; + fixup.Click += async (_, e) => + { + await vm.SquashOrFixupHeadAsync(commit, true); + e.Handled = true; + }; + menu.Items.Add(fixup); + } + else + { + var reset = new MenuItem(); + reset.Header = App.Text("CommitCM.Reset", current.Name, target); + reset.Icon = App.CreateMenuIcon("Icons.Reset"); + reset.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Reset(repo, current, commit)); + e.Handled = true; + }; + menu.Items.Add(reset); + } + + if (!commit.IsMerged) + { + var rebase = new MenuItem(); + rebase.Header = App.Text("CommitCM.Rebase", current.Name, target); + rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); + rebase.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Rebase(repo, current, commit)); + e.Handled = true; + }; + menu.Items.Add(rebase); + + if (!commit.HasDecorators) + { + var merge = new MenuItem(); + merge.Header = App.Text("CommitCM.Merge", current.Name); + merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Merge(repo, commit, current.Name)); + + e.Handled = true; + }; + menu.Items.Add(merge); + } + + var cherryPick = new MenuItem(); + cherryPick.Header = App.Text("CommitCM.CherryPick"); + cherryPick.Icon = App.CreateMenuIcon("Icons.CherryPick"); + cherryPick.Click += async (_, e) => + { + await vm.CherryPickAsync(commit); + e.Handled = true; + }; + menu.Items.Add(cherryPick); + } + + var revert = new MenuItem(); + revert.Header = App.Text("CommitCM.Revert"); + revert.Icon = App.CreateMenuIcon("Icons.Undo"); + revert.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Revert(repo, commit)); + e.Handled = true; + }; + menu.Items.Add(revert); + + if (isHead) + { + var dropHead = new MenuItem(); + dropHead.Header = App.Text("CommitCM.Drop"); + dropHead.Icon = App.CreateMenuIcon("Icons.Clear"); + dropHead.Click += async (_, e) => + { + await vm.DropHeadAsync(commit); + e.Handled = true; + }; + menu.Items.Add(dropHead); + } + else + { + var checkoutCommit = new MenuItem(); + checkoutCommit.Header = App.Text("CommitCM.Checkout"); + checkoutCommit.Icon = App.CreateMenuIcon("Icons.Detached"); + checkoutCommit.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CheckoutCommit(repo, commit)); + e.Handled = true; + }; + menu.Items.Add(checkoutCommit); + + if (commit.IsMerged && commit.Parents.Count > 0) + { + var manually = new MenuItem(); + manually.Header = App.Text("CommitCM.InteractiveRebase.Manually", current.Name, target); + manually.Icon = App.CreateMenuIcon("Icons.InteractiveRebase"); + manually.Click += async (_, e) => + { + await App.ShowDialog(new ViewModels.InteractiveRebase(repo, commit)); + e.Handled = true; + }; + + var reword = new MenuItem(); + reword.Header = App.Text("CommitCM.InteractiveRebase.Reword"); + reword.Icon = App.CreateMenuIcon("Icons.Rename"); + reword.Click += async (_, e) => + { + await vm.InteractiveRebaseAsync(commit, Models.InteractiveRebaseAction.Reword); + e.Handled = true; + }; + + var edit = new MenuItem(); + edit.Header = App.Text("CommitCM.InteractiveRebase.Edit"); + edit.Icon = App.CreateMenuIcon("Icons.Edit"); + edit.Click += async (_, e) => + { + await vm.InteractiveRebaseAsync(commit, Models.InteractiveRebaseAction.Edit); + e.Handled = true; + }; + + var squash = new MenuItem(); + squash.Header = App.Text("CommitCM.InteractiveRebase.Squash"); + squash.Icon = App.CreateMenuIcon("Icons.SquashIntoParent"); + squash.Click += async (_, e) => + { + await vm.InteractiveRebaseAsync(commit, Models.InteractiveRebaseAction.Squash); + e.Handled = true; + }; + + var fixup = new MenuItem(); + fixup.Header = App.Text("CommitCM.InteractiveRebase.Fixup"); + fixup.Icon = App.CreateMenuIcon("Icons.Fix"); + fixup.Click += async (_, e) => + { + await vm.InteractiveRebaseAsync(commit, Models.InteractiveRebaseAction.Fixup); + e.Handled = true; + }; + + var drop = new MenuItem(); + drop.Header = App.Text("CommitCM.InteractiveRebase.Drop"); + drop.Icon = App.CreateMenuIcon("Icons.Clear"); + drop.Click += async (_, e) => + { + await vm.InteractiveRebaseAsync(commit, Models.InteractiveRebaseAction.Drop); + e.Handled = true; + }; + + var interactiveRebase = new MenuItem(); + interactiveRebase.Header = App.Text("CommitCM.InteractiveRebase"); + interactiveRebase.Icon = App.CreateMenuIcon("Icons.InteractiveRebase"); + interactiveRebase.Items.Add(manually); + interactiveRebase.Items.Add(new MenuItem() { Header = "-" }); + interactiveRebase.Items.Add(reword); + interactiveRebase.Items.Add(edit); + interactiveRebase.Items.Add(squash); + interactiveRebase.Items.Add(fixup); + interactiveRebase.Items.Add(drop); + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(interactiveRebase); + } + else + { + var interactiveRebase = new MenuItem(); + interactiveRebase.Header = App.Text("CommitCM.InteractiveRebase.Manually", current.Name, target); + interactiveRebase.Icon = App.CreateMenuIcon("Icons.InteractiveRebase"); + interactiveRebase.Click += async (_, e) => + { + await App.ShowDialog(new ViewModels.InteractiveRebase(repo, commit)); + e.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(interactiveRebase); + } + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + if (!isHead) + { + if (current.Ahead.Contains(commit.SHA)) + { + var upstream = repo.Branches.Find(x => x.FullName.Equals(current.Upstream, StringComparison.Ordinal)); + var pushRevision = new MenuItem(); + pushRevision.Header = App.Text("CommitCM.PushRevision", commit.SHA.Substring(0, 10), upstream.FriendlyName); + pushRevision.Icon = App.CreateMenuIcon("Icons.Push"); + pushRevision.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.PushRevision(repo, commit, upstream)); + e.Handled = true; + }; + menu.Items.Add(pushRevision); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var compareWithHead = new MenuItem(); + compareWithHead.Header = App.Text("CommitCM.CompareWithHead"); + compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithHead.Click += async (_, e) => + { + var head = await vm.CompareWithHeadAsync(commit); + if (head != null) + CommitListContainer.SelectedItems.Add(head); + + e.Handled = true; + }; + menu.Items.Add(compareWithHead); + + if (repo.LocalChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("CommitCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += (_, e) => + { + vm.CompareWithWorktree(commit); + e.Handled = true; + }; + menu.Items.Add(compareWithWorktree); + } + + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var saveToPatch = new MenuItem(); + saveToPatch.Icon = App.CreateMenuIcon("Icons.Diff"); + saveToPatch.Header = App.Text("CommitCM.SaveAsPatch"); + saveToPatch.Click += async (_, e) => + { + var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider; + if (storageProvider == 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(); + await repo.SaveCommitAsPatchAsync(commit, folderPath); + } + } + catch (Exception exception) + { + App.RaiseException(repo.FullPath, $"Failed to save as patch: {exception.Message}"); + } + + e.Handled = true; + }; + menu.Items.Add(saveToPatch); + + var archive = new MenuItem(); + archive.Icon = App.CreateMenuIcon("Icons.Archive"); + archive.Header = App.Text("Archive"); + archive.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Archive(repo, commit)); + e.Handled = true; + }; + menu.Items.Add(archive); + menu.Items.Add(new MenuItem() { Header = "-" }); + + var actions = repo.GetCustomActions(Models.CustomActionScope.Commit); + if (actions.Count > 0) + { + var custom = new MenuItem(); + custom.Header = App.Text("CommitCM.CustomAction"); + custom.Icon = App.CreateMenuIcon("Icons.Action"); + + foreach (var action in actions) + { + var (dup, label) = action; + var item = new MenuItem(); + item.Icon = App.CreateMenuIcon("Icons.Action"); + item.Header = label; + item.Click += async (_, e) => + { + await repo.ExecCustomActionAsync(dup, commit); + e.Handled = true; + }; + + custom.Items.Add(item); + } + + menu.Items.Add(custom); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var copySHA = new MenuItem(); + copySHA.Header = App.Text("CommitCM.CopySHA"); + copySHA.Icon = App.CreateMenuIcon("Icons.Hash"); + copySHA.Click += async (_, e) => + { + await App.CopyTextAsync(commit.SHA); + e.Handled = true; + }; + + var copySubject = new MenuItem(); + copySubject.Header = App.Text("CommitCM.CopySubject"); + copySubject.Icon = App.CreateMenuIcon("Icons.Subject"); + copySubject.Click += async (_, e) => + { + await App.CopyTextAsync(commit.Subject); + e.Handled = true; + }; + + var copyInfo = new MenuItem(); + copyInfo.Header = App.Text("CommitCM.CopySHA") + " - " + App.Text("CommitCM.CopySubject"); + copyInfo.Icon = App.CreateMenuIcon("Icons.ShaAndSubject"); + copyInfo.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyInfo.Click += async (_, e) => + { + await App.CopyTextAsync($"{commit.SHA.AsSpan(0, 10)} - {commit.Subject}"); + e.Handled = true; + }; + + var copyMessage = new MenuItem(); + copyMessage.Header = App.Text("CommitCM.CopyCommitMessage"); + copyMessage.Icon = App.CreateMenuIcon("Icons.Info"); + copyMessage.Click += async (_, e) => + { + var message = await vm.GetCommitFullMessageAsync(commit); + await App.CopyTextAsync(message); + e.Handled = true; + }; + + var copyAuthor = new MenuItem(); + copyAuthor.Header = App.Text("CommitCM.CopyAuthor"); + copyAuthor.Icon = App.CreateMenuIcon("Icons.User"); + copyAuthor.Click += async (_, e) => + { + await App.CopyTextAsync(commit.Author.ToString()); + e.Handled = true; + }; + + var copyCommitter = new MenuItem(); + copyCommitter.Header = App.Text("CommitCM.CopyCommitter"); + copyCommitter.Icon = App.CreateMenuIcon("Icons.User"); + copyCommitter.Click += async (_, e) => + { + await App.CopyTextAsync(commit.Committer.ToString()); + e.Handled = true; + }; + + var copy = new MenuItem(); + copy.Header = App.Text("Copy"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Items.Add(copySHA); + copy.Items.Add(copySubject); + copy.Items.Add(copyInfo); + copy.Items.Add(copyMessage); + copy.Items.Add(copyAuthor); + copy.Items.Add(copyCommitter); + menu.Items.Add(copy); + + return menu; + } + + private void FillCurrentBranchMenu(ContextMenu menu, ViewModels.Repository repo, Models.Branch current) + { + var submenu = new MenuItem(); + submenu.Icon = App.CreateMenuIcon("Icons.Branch"); + submenu.Header = current.Name; + + var visibility = new MenuItem(); + visibility.Classes.Add("filter_mode_switcher"); + visibility.Header = new ViewModels.FilterModeInGraph(repo, current); + submenu.Items.Add(visibility); + submenu.Items.Add(new MenuItem() { Header = "-" }); + + if (!string.IsNullOrEmpty(current.Upstream)) + { + var upstream = current.Upstream.Substring(13); + + var fastForward = new MenuItem(); + fastForward.Header = App.Text("BranchCM.FastForward", upstream); + fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); + fastForward.IsEnabled = current.Ahead.Count == 0 && current.Behind.Count > 0; + fastForward.Click += async (_, e) => + { + var b = repo.Branches.Find(x => x.FriendlyName == upstream); + if (b == null) + return; + + if (repo.CanCreatePopup()) + await repo.ShowAndStartPopupAsync(new ViewModels.Merge(repo, b, current.Name, true)); + + e.Handled = true; + }; + submenu.Items.Add(fastForward); + + var pull = new MenuItem(); + pull.Header = App.Text("BranchCM.Pull", upstream); + pull.Icon = App.CreateMenuIcon("Icons.Pull"); + pull.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Pull(repo, null)); + e.Handled = true; + }; + submenu.Items.Add(pull); + } + + var push = new MenuItem(); + push.Header = App.Text("BranchCM.Push", current.Name); + push.Icon = App.CreateMenuIcon("Icons.Push"); + push.IsEnabled = repo.Remotes.Count > 0; + push.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Push(repo, current)); + e.Handled = true; + }; + submenu.Items.Add(push); + + var rename = new MenuItem(); + rename.Header = App.Text("BranchCM.Rename", current.Name); + rename.Icon = App.CreateMenuIcon("Icons.Rename"); + rename.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.RenameBranch(repo, current)); + e.Handled = true; + }; + submenu.Items.Add(rename); + submenu.Items.Add(new MenuItem() { Header = "-" }); + + if (!repo.IsBare) + { + var type = repo.GetGitFlowType(current); + if (type != Models.GitFlowBranchType.None) + { + var finish = new MenuItem(); + finish.Header = App.Text("BranchCM.Finish", current.Name); + finish.Icon = App.CreateMenuIcon("Icons.GitFlow"); + finish.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.GitFlowFinish(repo, current, type)); + e.Handled = true; + }; + submenu.Items.Add(finish); + submenu.Items.Add(new MenuItem() { Header = "-" }); + } + } + + var copy = new MenuItem(); + copy.Header = App.Text("BranchCM.CopyName"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, e) => + { + await App.CopyTextAsync(current.Name); + e.Handled = true; + }; + submenu.Items.Add(copy); + + menu.Items.Add(submenu); + } + + private void FillOtherLocalBranchMenu(ContextMenu menu, ViewModels.Repository repo, Models.Branch branch, Models.Branch current, bool merged) + { + var submenu = new MenuItem(); + submenu.Icon = App.CreateMenuIcon("Icons.Branch"); + submenu.Header = branch.Name; + + var visibility = new MenuItem(); + visibility.Classes.Add("filter_mode_switcher"); + visibility.Header = new ViewModels.FilterModeInGraph(repo, branch); + submenu.Items.Add(visibility); + submenu.Items.Add(new MenuItem() { Header = "-" }); + + if (!repo.IsBare) + { + var checkout = new MenuItem(); + checkout.Header = App.Text("BranchCM.Checkout", branch.Name); + checkout.Icon = App.CreateMenuIcon("Icons.Check"); + checkout.Click += async (_, e) => + { + await repo.CheckoutBranchAsync(branch); + e.Handled = true; + }; + submenu.Items.Add(checkout); + + var merge = new MenuItem(); + merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name); + merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.IsEnabled = !merged; + merge.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Merge(repo, branch, current.Name, false)); + e.Handled = true; + }; + submenu.Items.Add(merge); + } + + var rename = new MenuItem(); + rename.Header = App.Text("BranchCM.Rename", branch.Name); + rename.Icon = App.CreateMenuIcon("Icons.Rename"); + rename.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.RenameBranch(repo, branch)); + e.Handled = true; + }; + submenu.Items.Add(rename); + + var delete = new MenuItem(); + delete.Header = App.Text("BranchCM.Delete", branch.Name); + delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch)); + e.Handled = true; + }; + submenu.Items.Add(delete); + submenu.Items.Add(new MenuItem() { Header = "-" }); + + if (!repo.IsBare) + { + var type = repo.GetGitFlowType(branch); + if (type != Models.GitFlowBranchType.None) + { + var finish = new MenuItem(); + finish.Header = App.Text("BranchCM.Finish", branch.Name); + finish.Icon = App.CreateMenuIcon("Icons.GitFlow"); + finish.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.GitFlowFinish(repo, branch, type)); + e.Handled = true; + }; + submenu.Items.Add(finish); + submenu.Items.Add(new MenuItem() { Header = "-" }); + } } + + var copy = new MenuItem(); + copy.Header = App.Text("BranchCM.CopyName"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, e) => + { + await App.CopyTextAsync(branch.Name); + e.Handled = true; + }; + submenu.Items.Add(copy); + + menu.Items.Add(submenu); + } + + private void FillRemoteBranchMenu(ContextMenu menu, ViewModels.Repository repo, Models.Branch branch, Models.Branch current, bool merged) + { + var name = branch.FriendlyName; + + var submenu = new MenuItem(); + submenu.Icon = App.CreateMenuIcon("Icons.Branch"); + submenu.Header = name; + + var visibility = new MenuItem(); + visibility.Classes.Add("filter_mode_switcher"); + visibility.Header = new ViewModels.FilterModeInGraph(repo, branch); + submenu.Items.Add(visibility); + submenu.Items.Add(new MenuItem() { Header = "-" }); + + var checkout = new MenuItem(); + checkout.Header = App.Text("BranchCM.Checkout", name); + checkout.Icon = App.CreateMenuIcon("Icons.Check"); + checkout.Click += async (_, e) => + { + await repo.CheckoutBranchAsync(branch); + e.Handled = true; + }; + submenu.Items.Add(checkout); + + var merge = new MenuItem(); + merge.Header = App.Text("BranchCM.Merge", name, current.Name); + merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.IsEnabled = !merged; + merge.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Merge(repo, branch, current.Name, false)); + e.Handled = true; + }; + + submenu.Items.Add(merge); + + var delete = new MenuItem(); + delete.Header = App.Text("BranchCM.Delete", name); + delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch)); + e.Handled = true; + }; + submenu.Items.Add(delete); + submenu.Items.Add(new MenuItem() { Header = "-" }); + + var copy = new MenuItem(); + copy.Header = App.Text("BranchCM.CopyName"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, e) => + { + await App.CopyTextAsync(name); + e.Handled = true; + }; + submenu.Items.Add(copy); + + menu.Items.Add(submenu); + } + + private void FillTagMenu(ContextMenu menu, ViewModels.Repository repo, Models.Tag tag, Models.Branch current, bool merged) + { + var submenu = new MenuItem(); + submenu.Header = tag.Name; + submenu.Icon = App.CreateMenuIcon("Icons.Tag"); + submenu.MinWidth = 200; + + var visibility = new MenuItem(); + visibility.Classes.Add("filter_mode_switcher"); + visibility.Header = new ViewModels.FilterModeInGraph(repo, tag); + submenu.Items.Add(visibility); + submenu.Items.Add(new MenuItem() { Header = "-" }); + + var push = new MenuItem(); + push.Header = App.Text("TagCM.Push", tag.Name); + push.Icon = App.CreateMenuIcon("Icons.Push"); + push.IsEnabled = repo.Remotes.Count > 0; + push.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.PushTag(repo, tag)); + e.Handled = true; + }; + submenu.Items.Add(push); + + if (!repo.IsBare && !merged) + { + var merge = new MenuItem(); + merge.Header = App.Text("TagCM.Merge", tag.Name, current.Name); + merge.Icon = App.CreateMenuIcon("Icons.Merge"); + merge.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Merge(repo, tag, current.Name)); + e.Handled = true; + }; + submenu.Items.Add(merge); + } + + var delete = new MenuItem(); + delete.Header = App.Text("TagCM.Delete", tag.Name); + delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteTag(repo, tag)); + e.Handled = true; + }; + submenu.Items.Add(delete); + submenu.Items.Add(new MenuItem() { Header = "-" }); + + var copy = new MenuItem(); + copy.Header = App.Text("TagCM.CopyName"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, e) => + { + await App.CopyTextAsync(tag.Name); + e.Handled = true; + }; + submenu.Items.Add(copy); + + menu.Items.Add(submenu); } private double _lastGraphStartY = 0; diff --git a/src/Views/Hotkeys.axaml b/src/Views/Hotkeys.axaml index d020ff8b5..5a8bf5e95 100644 --- a/src/Views/Hotkeys.axaml +++ b/src/Views/Hotkeys.axaml @@ -46,32 +46,32 @@ Margin="0,0,0,8"/> - + - + - + - + - + - + - - + + - - + + - - + + - - + + - + - + - + - + - + - + - + - + - + - + + + + - - + + - + - + - + - + - + diff --git a/src/Views/ImageContainer.cs b/src/Views/ImageContainer.cs index 995f269bf..da0dafb8b 100644 --- a/src/Views/ImageContainer.cs +++ b/src/Views/ImageContainer.cs @@ -361,4 +361,119 @@ private void RenderSingleSide(DrawingContext context, Bitmap img, double w, doub private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Plus, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; } + + public class ImageDifferenceControl : ImageContainer + { + public static readonly StyledProperty AlphaProperty = + AvaloniaProperty.Register(nameof(Alpha), 1.0); + + public double Alpha + { + get => GetValue(AlphaProperty); + set => SetValue(AlphaProperty, value); + } + + public static readonly StyledProperty OldImageProperty = + AvaloniaProperty.Register(nameof(OldImage)); + + public Bitmap OldImage + { + get => GetValue(OldImageProperty); + set => SetValue(OldImageProperty, value); + } + + public static readonly StyledProperty NewImageProperty = + AvaloniaProperty.Register(nameof(NewImage)); + + public Bitmap NewImage + { + get => GetValue(NewImageProperty); + set => SetValue(NewImageProperty, value); + } + + static ImageDifferenceControl() + { + AffectsMeasure(OldImageProperty, NewImageProperty); + AffectsRender(AlphaProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var alpha = Alpha; + var left = OldImage; + var right = NewImage; + var drawLeft = left != null && alpha < 1.0; + var drawRight = right != null && alpha > 0.0; + + if (drawLeft && drawRight) + { + using (var rt = new RenderTargetBitmap(new PixelSize((int)Bounds.Width, (int)Bounds.Height), right.Dpi)) + { + using (var dc = rt.CreateDrawingContext()) + { + using (dc.PushRenderOptions(RO_SRC)) + RenderSingleSide(dc, left, rt.Size.Width, rt.Size.Height, Math.Min(1.0, 2.0 - 2.0 * alpha)); + + using (dc.PushRenderOptions(RO_DST)) + RenderSingleSide(dc, right, rt.Size.Width, rt.Size.Height, Math.Min(1.0, 2.0 * alpha)); + } + + context.DrawImage(rt, new Rect(0, 0, Bounds.Width, Bounds.Height)); + } + } + else if (drawLeft) + { + RenderSingleSide(context, left, Bounds.Width, Bounds.Height, 1 - alpha); + } + else if (drawRight) + { + RenderSingleSide(context, right, Bounds.Width, Bounds.Height, alpha); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var left = OldImage; + var right = NewImage; + + if (left == null) + return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); + + if (right == null) + return GetDesiredSize(left.Size, availableSize); + + var ls = GetDesiredSize(left.Size, availableSize); + var rs = GetDesiredSize(right.Size, availableSize); + return ls.Width > rs.Width ? ls : rs; + } + + private Size GetDesiredSize(Size img, Size available) + { + var sw = available.Width / img.Width; + var sh = available.Height / img.Height; + var scale = Math.Min(1, Math.Min(sw, sh)); + return new Size(scale * img.Width, scale * img.Height); + } + + private void RenderSingleSide(DrawingContext context, Bitmap img, double w, double h, double alpha) + { + var imgW = img.Size.Width; + var imgH = img.Size.Height; + var scale = Math.Min(1, Math.Min(w / imgW, h / imgH)); + + var scaledW = img.Size.Width * scale; + var scaledH = img.Size.Height * scale; + + var src = new Rect(0, 0, imgW, imgH); + var dst = new Rect((w - scaledW) * 0.5, (h - scaledH) * 0.5, scaledW, scaledH); + + using (context.PushOpacity(alpha)) + context.DrawImage(img, src, dst); + } + + private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; + private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Difference, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; + } } diff --git a/src/Views/ImageDiffView.axaml b/src/Views/ImageDiffView.axaml index 239991502..ca954a7e1 100644 --- a/src/Views/ImageDiffView.axaml +++ b/src/Views/ImageDiffView.axaml @@ -3,12 +3,13 @@ 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.ImageDiffView" x:DataType="m:ImageDiff"> - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/LauncherPagesCommandPalette.axaml.cs b/src/Views/LauncherPagesCommandPalette.axaml.cs new file mode 100644 index 000000000..805f47466 --- /dev/null +++ b/src/Views/LauncherPagesCommandPalette.axaml.cs @@ -0,0 +1,99 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class LauncherPagesCommandPalette : UserControl + { + public LauncherPagesCommandPalette() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (DataContext is not ViewModels.LauncherPagesCommandPalette vm) + return; + + if (e.Key == Key.Enter) + { + vm.OpenOrSwitchTo(); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + if (RepoListBox.IsKeyboardFocusWithin) + { + if (vm.VisiblePages.Count > 0) + { + PageListBox.Focus(NavigationMethod.Directional); + vm.SelectedPage = vm.VisiblePages[^1]; + } + else + { + FilterTextBox.Focus(NavigationMethod.Directional); + } + + e.Handled = true; + return; + } + + if (PageListBox.IsKeyboardFocusWithin) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + else if (e.Key == Key.Down || e.Key == Key.Tab) + { + if (FilterTextBox.IsKeyboardFocusWithin) + { + if (vm.VisiblePages.Count > 0) + { + PageListBox.Focus(NavigationMethod.Directional); + vm.SelectedPage = vm.VisiblePages[0]; + } + else if (vm.VisibleRepos.Count > 0) + { + RepoListBox.Focus(NavigationMethod.Directional); + vm.SelectedRepo = vm.VisibleRepos[0]; + } + + e.Handled = true; + return; + } + + if (PageListBox.IsKeyboardFocusWithin) + { + if (vm.VisibleRepos.Count > 0) + { + RepoListBox.Focus(NavigationMethod.Directional); + vm.SelectedRepo = vm.VisibleRepos[0]; + } + + e.Handled = true; + return; + } + + if (RepoListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + } + + private void OnItemTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.LauncherPagesCommandPalette vm) + { + vm.OpenOrSwitchTo(); + e.Handled = true; + } + } + } +} diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index ce35697fd..cde6d2268 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:c="using:SourceGit.Converters" xmlns:v="using:SourceGit.Views" @@ -12,7 +13,8 @@ @@ -41,21 +43,27 @@ - + - + + + + - + + IsVisible="{Binding DirtyState, Converter={x:Static ObjectConverters.NotEqual}, ConverterParameter={x:Static m:DirtyState.None}}" + Fill="{Binding DirtyState, Converter={x:Static c:DirtyStateConverters.ToBrush}}"/> @@ -116,14 +122,14 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/MergeCommandPalette.axaml.cs b/src/Views/MergeCommandPalette.axaml.cs new file mode 100644 index 000000000..4d81f48b3 --- /dev/null +++ b/src/Views/MergeCommandPalette.axaml.cs @@ -0,0 +1,63 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class MergeCommandPalette : UserControl + { + public MergeCommandPalette() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (DataContext is not ViewModels.MergeCommandPalette vm) + return; + + if (e.Key == Key.Enter) + { + vm.Launch(); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + if (BranchListBox.IsKeyboardFocusWithin) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + else if (e.Key == Key.Down || e.Key == Key.Tab) + { + if (FilterTextBox.IsKeyboardFocusWithin) + { + if (vm.Branches.Count > 0) + BranchListBox.Focus(NavigationMethod.Directional); + + e.Handled = true; + return; + } + + if (BranchListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + } + + private void OnItemTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.MergeCommandPalette vm) + { + vm.Launch(); + e.Handled = true; + } + } + } +} diff --git a/src/Views/MergeMultiple.axaml b/src/Views/MergeMultiple.axaml index 332d9fef8..0c7465c94 100644 --- a/src/Views/MergeMultiple.axaml +++ b/src/Views/MergeMultiple.axaml @@ -9,9 +9,15 @@ x:Class="SourceGit.Views.MergeMultiple" x:DataType="vm:MergeMultiple"> - + + + + + - + + + + + @@ -77,7 +83,6 @@ IsChecked="True"/> diff --git a/src/Views/MoveSubmodule.axaml b/src/Views/MoveSubmodule.axaml index 7a8d7f7cd..7d3839667 100644 --- a/src/Views/MoveSubmodule.axaml +++ b/src/Views/MoveSubmodule.axaml @@ -7,9 +7,16 @@ x:Class="SourceGit.Views.MoveSubmodule" x:DataType="vm:MoveSubmodule"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/OpenFileCommandPalette.axaml.cs b/src/Views/OpenFileCommandPalette.axaml.cs new file mode 100644 index 000000000..24c6082b3 --- /dev/null +++ b/src/Views/OpenFileCommandPalette.axaml.cs @@ -0,0 +1,63 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class OpenFileCommandPalette : UserControl + { + public OpenFileCommandPalette() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (DataContext is not ViewModels.OpenFileCommandPalette vm) + return; + + if (e.Key == Key.Enter) + { + vm.Launch(); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + if (FileListBox.IsKeyboardFocusWithin) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + else if (e.Key == Key.Down || e.Key == Key.Tab) + { + if (FilterTextBox.IsKeyboardFocusWithin) + { + if (vm.VisibleFiles.Count > 0) + FileListBox.Focus(NavigationMethod.Directional); + + e.Handled = true; + return; + } + + if (FileListBox.IsKeyboardFocusWithin && e.Key == Key.Tab) + { + FilterTextBox.Focus(NavigationMethod.Directional); + e.Handled = true; + return; + } + } + } + + private void OnItemTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.OpenFileCommandPalette vm) + { + vm.Launch(); + e.Handled = true; + } + } + } +} diff --git a/src/Views/PopupDataTemplates.cs b/src/Views/PopupDataTemplates.cs new file mode 100644 index 000000000..e9462bccc --- /dev/null +++ b/src/Views/PopupDataTemplates.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; + +namespace SourceGit.Views +{ + public class PopupDataTemplates : IDataTemplate + { + public Control Build(object param) => App.CreateViewForViewModel(param); + public bool Match(object data) => data is ViewModels.Popup; + } +} diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 39055dfe3..0d5c3f8d3 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -46,7 +46,7 @@ - + + + + + - - - + + + + - - + + - + @@ -458,14 +479,14 @@ + SelectedIndex="{Binding ShellOrTerminalType, Mode=TwoWay}"> @@ -476,11 +497,13 @@ - + + + + + + + + + + + + + + @@ -499,7 +541,7 @@ - + @@ -528,11 +570,13 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -554,10 +636,10 @@ - + - + + + + + + @@ -663,20 +758,13 @@ - - - - - - + + @@ -737,13 +825,9 @@ - - - - @@ -774,7 +858,10 @@ - + + - + + + + + + diff --git a/src/Views/PruneWorktrees.axaml b/src/Views/PruneWorktrees.axaml index a3a0f770e..b6e45d611 100644 --- a/src/Views/PruneWorktrees.axaml +++ b/src/Views/PruneWorktrees.axaml @@ -5,9 +5,16 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.PruneWorktrees"> - + + + + + + diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index 11f883362..b09f280f4 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -8,9 +8,16 @@ x:Class="SourceGit.Views.Pull" x:DataType="vm:Pull"> - + + + + + + @@ -18,7 +25,6 @@ - @@ -60,12 +67,6 @@ - - - - - - @@ -94,13 +95,6 @@ Content="{DynamicResource Text.Pull.UseRebase}" IsChecked="{Binding UseRebase, Mode=TwoWay}" ToolTip.Tip="--rebase"/> - - diff --git a/src/Views/Push.axaml b/src/Views/Push.axaml index a9a408081..d0475845e 100644 --- a/src/Views/Push.axaml +++ b/src/Views/Push.axaml @@ -8,9 +8,15 @@ x:Class="SourceGit.Views.Push" x:DataType="vm:Push"> - + + + + + @@ -32,12 +39,6 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - + + (); + if (launcher == null) + return; + + var dialog = new PushToNewBranch(); + dialog.SetRemote(push.SelectedRemote.Name); + + var name = await dialog.ShowDialog(launcher); + if (!string.IsNullOrEmpty(name)) + push.PushToNewBranch(name); + } } } diff --git a/src/Views/PushRevision.axaml b/src/Views/PushRevision.axaml index 092212c15..13034ebd5 100644 --- a/src/Views/PushRevision.axaml +++ b/src/Views/PushRevision.axaml @@ -8,9 +8,15 @@ x:Class="SourceGit.Views.PushRevision" x:DataType="vm:PushRevision"> - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -116,14 +131,19 @@ - + + + - + + + @@ -804,7 +832,7 @@ - @@ -815,7 +843,7 @@ diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 2d8568dae..8f040310d 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -1,105 +1,12 @@ using System; -using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Media; namespace SourceGit.Views { - public class CounterPresenter : Control - { - public static readonly StyledProperty CountProperty = - AvaloniaProperty.Register(nameof(Count)); - - public int Count - { - get => GetValue(CountProperty); - set => SetValue(CountProperty, value); - } - - public static readonly StyledProperty FontFamilyProperty = - TextBlock.FontFamilyProperty.AddOwner(); - - public FontFamily FontFamily - { - get => GetValue(FontFamilyProperty); - set => SetValue(FontFamilyProperty, value); - } - - public static readonly StyledProperty FontSizeProperty = - TextBlock.FontSizeProperty.AddOwner(); - - public double FontSize - { - get => GetValue(FontSizeProperty); - set => SetValue(FontSizeProperty, value); - } - - public static readonly StyledProperty ForegroundProperty = - AvaloniaProperty.Register(nameof(Foreground), Brushes.White); - - public IBrush Foreground - { - get => GetValue(ForegroundProperty); - set => SetValue(ForegroundProperty, value); - } - - public static readonly StyledProperty BackgroundProperty = - AvaloniaProperty.Register(nameof(Background), Brushes.White); - - public IBrush Background - { - get => GetValue(BackgroundProperty); - set => SetValue(BackgroundProperty, value); - } - - static CounterPresenter() - { - AffectsMeasure( - FontSizeProperty, - FontFamilyProperty, - ForegroundProperty, - CountProperty); - } - - public override void Render(DrawingContext context) - { - base.Render(context); - - if (_label != null) - { - context.DrawRectangle(Background, null, new RoundedRect(new Rect(0, 0, _label.Width + 18, 18), new CornerRadius(9))); - context.DrawText(_label, new Point(9, 9 - _label.Height * 0.5)); - } - } - - protected override Size MeasureOverride(Size availableSize) - { - if (Count > 0) - { - _label = new FormattedText( - Count.ToString(), - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - new Typeface(FontFamily), - FontSize, - Foreground); - } - else - { - _label = null; - } - - InvalidateVisual(); - return _label != null ? new Size(_label.Width + 18, 18) : new Size(0, 0); - } - - private FormattedText _label = null; - } - public partial class Repository : UserControl { public Repository() @@ -126,14 +33,12 @@ private void OnSearchKeyDown(object _, KeyEventArgs e) if (e.Key == Key.Enter) { - if (!string.IsNullOrWhiteSpace(repo.SearchCommitFilter)) - repo.StartSearchCommits(); - + repo.SearchCommitContext.StartSearch(); e.Handled = true; } else if (e.Key == Key.Down) { - if (repo.MatchedFilesForSearching is { Count: > 0 }) + if (repo.SearchCommitContext.Suggestions is { Count: > 0 }) { SearchSuggestionBox.Focus(NavigationMethod.Tab); SearchSuggestionBox.SelectedIndex = 0; @@ -143,11 +48,20 @@ private void OnSearchKeyDown(object _, KeyEventArgs e) } else if (e.Key == Key.Escape) { - repo.ClearMatchedFilesForSearching(); + repo.SearchCommitContext.ClearSuggestions(); e.Handled = true; } } + private void OnClearSearchCommitFilter(object _, RoutedEventArgs e) + { + if (DataContext is not ViewModels.Repository repo) + return; + + repo.SearchCommitContext.ClearFilter(); + e.Handled = true; + } + private void OnBranchTreeRowsChanged(object _, RoutedEventArgs e) { UpdateLeftSidebarLayout(); @@ -188,8 +102,66 @@ private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs { if (sender is ListBox { SelectedItem: Models.Worktree worktree } grid && DataContext is ViewModels.Repository repo) { - var menu = repo.CreateContextMenuForWorktree(worktree); - menu?.Open(grid); + var menu = new ContextMenu(); + + var switchTo = new MenuItem(); + switchTo.Header = App.Text("Worktree.Open"); + switchTo.Icon = App.CreateMenuIcon("Icons.Folder.Open"); + switchTo.Click += (_, ev) => + { + repo.OpenWorktree(worktree); + ev.Handled = true; + }; + menu.Items.Add(switchTo); + menu.Items.Add(new MenuItem() { Header = "-" }); + + if (worktree.IsLocked) + { + var unlock = new MenuItem(); + unlock.Header = App.Text("Worktree.Unlock"); + unlock.Icon = App.CreateMenuIcon("Icons.Unlock"); + unlock.Click += async (_, ev) => + { + await repo.UnlockWorktreeAsync(worktree); + ev.Handled = true; + }; + menu.Items.Add(unlock); + } + else + { + var loc = new MenuItem(); + loc.Header = App.Text("Worktree.Lock"); + loc.Icon = App.CreateMenuIcon("Icons.Lock"); + loc.Click += async (_, ev) => + { + await repo.LockWorktreeAsync(worktree); + ev.Handled = true; + }; + menu.Items.Add(loc); + } + + var remove = new MenuItem(); + remove.Header = App.Text("Worktree.Remove"); + remove.Icon = App.CreateMenuIcon("Icons.Clear"); + remove.Click += (_, ev) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.RemoveWorktree(repo, worktree)); + ev.Handled = true; + }; + menu.Items.Add(remove); + + var copy = new MenuItem(); + copy.Header = App.Text("Worktree.CopyPath"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.Click += async (_, ev) => + { + await App.CopyTextAsync(worktree.FullPath); + ev.Handled = true; + }; + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copy); + menu.Open(grid); } e.Handled = true; @@ -345,14 +317,14 @@ private void OnSearchSuggestionBoxKeyDown(object _, KeyEventArgs e) if (e.Key == Key.Escape) { - repo.ClearMatchedFilesForSearching(); + repo.SearchCommitContext.ClearSuggestions(); e.Handled = true; } else if (e.Key == Key.Enter && SearchSuggestionBox.SelectedItem is string content) { - repo.SearchCommitFilter = content; + repo.SearchCommitContext.Filter = content; TxtSearchCommitsBox.CaretIndex = content.Length; - repo.StartSearchCommits(); + repo.SearchCommitContext.StartSearch(); e.Handled = true; } } @@ -365,9 +337,9 @@ private void OnSearchSuggestionDoubleTapped(object sender, TappedEventArgs e) var content = (sender as StackPanel)?.DataContext as string; if (!string.IsNullOrEmpty(content)) { - repo.SearchCommitFilter = content; + repo.SearchCommitContext.Filter = content; TxtSearchCommitsBox.CaretIndex = content.Length; - repo.StartSearchCommits(); + repo.SearchCommitContext.StartSearch(); } e.Handled = true; } @@ -376,8 +348,111 @@ private void OnOpenAdvancedHistoriesOption(object sender, RoutedEventArgs e) { if (sender is Button button && DataContext is ViewModels.Repository repo) { - var menu = repo.CreateContextMenuForHistoryAdvancedOption(); - menu?.Open(button); + var pref = ViewModels.Preferences.Instance; + + var layout = new MenuItem(); + layout.Header = App.Text("Repository.HistoriesLayout"); + layout.IsEnabled = false; + + var isHorizontal = pref.UseTwoColumnsLayoutInHistories; + var horizontal = new MenuItem(); + horizontal.Header = App.Text("Repository.HistoriesLayout.Horizontal"); + if (isHorizontal) + horizontal.Icon = App.CreateMenuIcon("Icons.Check"); + horizontal.Click += (_, ev) => + { + pref.UseTwoColumnsLayoutInHistories = true; + ev.Handled = true; + }; + + var vertical = new MenuItem(); + vertical.Header = App.Text("Repository.HistoriesLayout.Vertical"); + if (!isHorizontal) + vertical.Icon = App.CreateMenuIcon("Icons.Check"); + vertical.Click += (_, ev) => + { + pref.UseTwoColumnsLayoutInHistories = false; + ev.Handled = true; + }; + + var showFlags = new MenuItem(); + showFlags.Header = App.Text("Repository.ShowFlags"); + showFlags.IsEnabled = false; + + var reflog = new MenuItem(); + reflog.Header = App.Text("Repository.ShowLostCommits"); + reflog.Tag = "--reflog"; + if (repo.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.Reflog)) + reflog.Icon = App.CreateMenuIcon("Icons.Check"); + reflog.Click += (_, ev) => + { + repo.ToggleHistoryShowFlag(Models.HistoryShowFlags.Reflog); + ev.Handled = true; + }; + + var firstParentOnly = new MenuItem(); + firstParentOnly.Header = App.Text("Repository.ShowFirstParentOnly"); + firstParentOnly.Tag = "--first-parent"; + if (repo.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)) + firstParentOnly.Icon = App.CreateMenuIcon("Icons.Check"); + firstParentOnly.Click += (_, ev) => + { + repo.ToggleHistoryShowFlag(Models.HistoryShowFlags.FirstParentOnly); + ev.Handled = true; + }; + + var simplifyByDecoration = new MenuItem(); + simplifyByDecoration.Header = App.Text("Repository.ShowDecoratedCommitsOnly"); + simplifyByDecoration.Tag = "--simplify-by-decoration"; + if (repo.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.SimplifyByDecoration)) + simplifyByDecoration.Icon = App.CreateMenuIcon("Icons.Check"); + simplifyByDecoration.Click += (_, ev) => + { + repo.ToggleHistoryShowFlag(Models.HistoryShowFlags.SimplifyByDecoration); + ev.Handled = true; + }; + + var order = new MenuItem(); + order.Header = App.Text("Repository.HistoriesOrder"); + order.IsEnabled = false; + + var dateOrder = new MenuItem(); + dateOrder.Header = App.Text("Repository.HistoriesOrder.ByDate"); + dateOrder.Tag = "--date-order"; + if (!repo.EnableTopoOrderInHistories) + dateOrder.Icon = App.CreateMenuIcon("Icons.Check"); + dateOrder.Click += (_, ev) => + { + repo.EnableTopoOrderInHistories = false; + ev.Handled = true; + }; + + var topoOrder = new MenuItem(); + topoOrder.Header = App.Text("Repository.HistoriesOrder.Topo"); + topoOrder.Tag = "--topo-order"; + if (repo.EnableTopoOrderInHistories) + topoOrder.Icon = App.CreateMenuIcon("Icons.Check"); + topoOrder.Click += (_, ev) => + { + repo.EnableTopoOrderInHistories = true; + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; + menu.Items.Add(layout); + menu.Items.Add(horizontal); + menu.Items.Add(vertical); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(showFlags); + menu.Items.Add(reflog); + menu.Items.Add(firstParentOnly); + menu.Items.Add(simplifyByDecoration); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(order); + menu.Items.Add(dateOrder); + menu.Items.Add(topoOrder); + menu.Open(button); } e.Handled = true; @@ -387,8 +462,34 @@ 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); + var isSortByName = repo.IsSortingLocalBranchByName; + var byNameAsc = new MenuItem(); + byNameAsc.Header = App.Text("Repository.BranchSort.ByName"); + if (isSortByName) + byNameAsc.Icon = App.CreateMenuIcon("Icons.Check"); + byNameAsc.Click += (_, ev) => + { + if (!isSortByName) + repo.IsSortingLocalBranchByName = true; + ev.Handled = true; + }; + + var byCommitterDate = new MenuItem(); + byCommitterDate.Header = App.Text("Repository.BranchSort.ByCommitterDate"); + if (!isSortByName) + byCommitterDate.Icon = App.CreateMenuIcon("Icons.Check"); + byCommitterDate.Click += (_, ev) => + { + if (isSortByName) + repo.IsSortingLocalBranchByName = false; + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; + menu.Items.Add(byNameAsc); + menu.Items.Add(byCommitterDate); + menu.Open(button); } e.Handled = true; @@ -398,8 +499,34 @@ 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); + var isSortByName = repo.IsSortingRemoteBranchByName; + var byNameAsc = new MenuItem(); + byNameAsc.Header = App.Text("Repository.BranchSort.ByName"); + if (isSortByName) + byNameAsc.Icon = App.CreateMenuIcon("Icons.Check"); + byNameAsc.Click += (_, ev) => + { + if (!isSortByName) + repo.IsSortingRemoteBranchByName = true; + ev.Handled = true; + }; + + var byCommitterDate = new MenuItem(); + byCommitterDate.Header = App.Text("Repository.BranchSort.ByCommitterDate"); + if (!isSortByName) + byCommitterDate.Icon = App.CreateMenuIcon("Icons.Check"); + byCommitterDate.Click += (_, ev) => + { + if (isSortByName) + repo.IsSortingRemoteBranchByName = false; + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; + menu.Items.Add(byNameAsc); + menu.Items.Add(byCommitterDate); + menu.Open(button); } e.Handled = true; @@ -409,25 +536,67 @@ private void OnOpenSortTagMenu(object sender, RoutedEventArgs e) { if (sender is Button button && DataContext is ViewModels.Repository repo) { - var menu = repo.CreateContextMenuForTagSortMode(); - menu?.Open(button); + var isSortByName = repo.IsSortingTagsByName; + var byCreatorDate = new MenuItem(); + byCreatorDate.Header = App.Text("Repository.Tags.OrderByCreatorDate"); + if (!isSortByName) + byCreatorDate.Icon = App.CreateMenuIcon("Icons.Check"); + byCreatorDate.Click += (_, ev) => + { + if (isSortByName) + repo.IsSortingTagsByName = false; + ev.Handled = true; + }; + + var byName = new MenuItem(); + byName.Header = App.Text("Repository.Tags.OrderByName"); + if (isSortByName) + byName.Icon = App.CreateMenuIcon("Icons.Check"); + byName.Click += (_, ev) => + { + if (!isSortByName) + repo.IsSortingTagsByName = true; + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; + menu.Items.Add(byName); + menu.Items.Add(byCreatorDate); + menu.Open(button); } e.Handled = true; } - private void OnSkipInProgress(object sender, RoutedEventArgs e) + private async void OnPruneWorktrees(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + await repo.PruneWorktreesAsync(); + + e.Handled = true; + } + + private async void OnSkipInProgress(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + await repo.SkipMergeAsync(); + + e.Handled = true; + } + + private async void OnAbortInProgress(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.Repository repo) - repo.SkipMerge(); + await repo.AbortMergeAsync(); e.Handled = true; } - private void OnRemoveSelectedHistoriesFilter(object sender, RoutedEventArgs e) + private void OnRemoveSelectedHistoryFilter(object sender, RoutedEventArgs e) { - if (DataContext is ViewModels.Repository repo && sender is Button { DataContext: Models.Filter filter }) - repo.RemoveHistoriesFilter(filter); + if (DataContext is ViewModels.Repository repo && sender is Button { DataContext: Models.HistoryFilter filter }) + repo.RemoveHistoryFilter(filter); e.Handled = true; } diff --git a/src/Views/WorkspaceSwitcher.axaml b/src/Views/RepositoryCommandPalette.axaml similarity index 66% rename from src/Views/WorkspaceSwitcher.axaml rename to src/Views/RepositoryCommandPalette.axaml index 4b2691dee..f1ea843d5 100644 --- a/src/Views/WorkspaceSwitcher.axaml +++ b/src/Views/RepositoryCommandPalette.axaml @@ -4,22 +4,18 @@ 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.WorkspaceSwitcher" - x:DataType="vm:WorkspaceSwitcher"> + x:Class="SourceGit.Views.RepositoryCommandPalette" + x:DataType="vm:RepositoryCommandPalette"> - - - @@ -35,7 +31,7 @@ Width="16" Margin="0,0,6,0" Command="{Binding ClearFilter}" - IsVisible="{Binding SearchFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" + IsVisible="{Binding Filter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" HorizontalAlignment="Right"> + + + ItemsSource="{Binding VisibleCmds, Mode=OneWay}" + SelectedItem="{Binding SelectedCmd, Mode=TwoWay}" + IsVisible="{Binding VisibleCmds, Converter={x:Static c:ListConverters.IsNotNullOrEmpty}}"> - @@ -22,33 +18,22 @@ - - - - - - - - - - - - - - - - - + SelectionChanged="OnSelectionChanged" + ContextRequested="OnTagsContextMenuRequested"> + + + - + - - - - - - - + + + @@ -87,49 +66,102 @@ + SelectionChanged="OnSelectionChanged" + ContextRequested="OnTagsContextMenuRequested"> + + + - + - - - - - - - - - - - - - - + Data="{StaticResource Icons.Tag}" + IsVisible="{Binding Tag.IsAnnotated}"/> + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs index b73a5b5e8..d2275d047 100644 --- a/src/Views/TagsView.axaml.cs +++ b/src/Views/TagsView.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia; using Avalonia.Controls; @@ -62,19 +63,19 @@ private void UpdateContent() } if (node.Tag != null) - CreateContent(new Thickness(0, 0, 0, 0), "Icons.Tag"); + CreateContent(new Thickness(0, 0, 0, 0), "Icons.Tag", node.ToolTip is { IsAnnotated: 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 stroke) { if (this.FindResource(iconKey) is not StreamGeometry geo) return; - Content = new Avalonia.Controls.Shapes.Path() + var path = new Avalonia.Controls.Shapes.Path() { Width = 12, Height = 12, @@ -83,6 +84,15 @@ private void CreateContent(Thickness margin, string iconKey) Margin = margin, Data = geo, }; + + if (stroke) + { + path.Fill = Brushes.Transparent; + path.Stroke = this.FindResource("Brush.FG1") as IBrush; + path.StrokeThickness = 1; + } + + Content = path; } } @@ -143,7 +153,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (Content is ViewModels.TagCollectionAsTree tree) Rows = tree.Rows.Count; else if (Content is ViewModels.TagCollectionAsList list) - Rows = list.Tags.Count; + Rows = list.TagItems.Count; else Rows = 0; @@ -155,16 +165,30 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } - private void OnItemDoubleTapped(object sender, TappedEventArgs e) + private async void OnItemDoubleTapped(object sender, TappedEventArgs e) { - if (sender is Control { DataContext: ViewModels.TagTreeNode { IsFolder: true } node }) - ToggleNodeIsExpanded(node); + if (sender is Control { DataContext: ViewModels.TagTreeNode node }) + { + if (node.IsFolder) + ToggleNodeIsExpanded(node); + else if (DataContext is ViewModels.Repository repo) + await repo.CheckoutTagAsync(node.Tag); + } + else if (sender is Control { DataContext: ViewModels.TagListItem item }) + { + if (DataContext is ViewModels.Repository repo) + await repo.CheckoutTagAsync(item.Tag); + } e.Handled = true; } private void OnItemPointerPressed(object sender, PointerPressedEventArgs e) { + var ctrl = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + if (e.KeyModifiers.HasFlag(ctrl) || e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + return; + var p = e.GetCurrentPoint(this); if (!p.Properties.IsLeftButtonPressed) return; @@ -172,29 +196,170 @@ private void OnItemPointerPressed(object sender, PointerPressedEventArgs e) 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 } }) + if (sender is not Control control) + return; + + if (control.DataContext is ViewModels.TagListItem { Tag: { } itemTag }) + repo.NavigateToCommit(itemTag.SHA); + else if (control.DataContext is ViewModels.TagTreeNode { Tag: { } nodeTag }) repo.NavigateToCommit(nodeTag.SHA); } - private void OnItemContextRequested(object sender, ContextRequestedEventArgs e) + private void OnTagsContextMenuRequested(object sender, ContextRequestedEventArgs e) { - if (sender is not Control control) + if (sender is not ListBox { SelectedItems: { Count: > 0 } selectedItems } listBox) return; - Models.Tag selected; - if (control.DataContext is ViewModels.TagTreeNode node) - selected = node.Tag; - else if (control.DataContext is Models.Tag tag) - selected = tag; - else - selected = null; + if (DataContext is not ViewModels.Repository repo) + return; - if (selected != null && DataContext is ViewModels.Repository repo) + var selected = new List(); + foreach (var item in selectedItems) { - var menu = repo.CreateContextMenuForTag(selected); - menu?.Open(control); + if (item is ViewModels.TagListItem i) + selected.Add(i.Tag); + else if (item is ViewModels.TagTreeNode n) + CollectTagsInNode(n, selected); + } + + if (selected.Count == 1) + { + var tag = selected[0]; + + var createBranch = new MenuItem(); + createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); + createBranch.Header = App.Text("CreateBranch"); + createBranch.Click += (_, ev) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CreateBranch(repo, tag)); + ev.Handled = true; + }; + + var pushTag = new MenuItem(); + pushTag.Header = App.Text("TagCM.Push", tag.Name); + pushTag.Icon = App.CreateMenuIcon("Icons.Push"); + pushTag.IsEnabled = repo.Remotes.Count > 0; + pushTag.Click += (_, ev) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.PushTag(repo, tag)); + ev.Handled = true; + }; + + var deleteTag = new MenuItem(); + deleteTag.Header = App.Text("TagCM.Delete", tag.Name); + deleteTag.Icon = App.CreateMenuIcon("Icons.Clear"); + deleteTag.Click += (_, ev) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteTag(repo, tag)); + ev.Handled = true; + }; + + var archive = new MenuItem(); + archive.Icon = App.CreateMenuIcon("Icons.Archive"); + archive.Header = App.Text("Archive"); + archive.Click += (_, ev) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.Archive(repo, tag)); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(createBranch); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(pushTag); + menu.Items.Add(deleteTag); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(archive); + menu.Items.Add(new MenuItem() { Header = "-" }); + + var actions = repo.GetCustomActions(Models.CustomActionScope.Tag); + if (actions.Count > 0) + { + var custom = new MenuItem(); + custom.Header = App.Text("TagCM.CustomAction"); + custom.Icon = App.CreateMenuIcon("Icons.Action"); + + foreach (var action in actions) + { + var (dup, label) = action; + var item = new MenuItem(); + item.Icon = App.CreateMenuIcon("Icons.Action"); + item.Header = label; + item.Click += async (_, ev) => + { + await repo.ExecCustomActionAsync(dup, tag); + ev.Handled = true; + }; + + custom.Items.Add(item); + } + + menu.Items.Add(custom); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var copy = new MenuItem(); + copy.Header = App.Text("Copy"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + + var copyName = new MenuItem(); + copyName.Header = App.Text("TagCM.Copy.Name"); + copyName.Icon = App.CreateMenuIcon("Icons.Tag"); + copyName.Click += async (_, ev) => + { + await App.CopyTextAsync(tag.Name); + ev.Handled = true; + }; + + var copyMessage = new MenuItem(); + copyMessage.Header = App.Text("TagCM.Copy.Message"); + copyMessage.Icon = App.CreateMenuIcon("Icons.Info"); + copyMessage.IsEnabled = !string.IsNullOrEmpty(tag.Message); + copyMessage.Click += async (_, ev) => + { + await App.CopyTextAsync(tag.Message); + ev.Handled = true; + }; + + copy.Items.Add(copyName); + copy.Items.Add(copyMessage); + + if (tag.Creator is { Email: { Length: > 0 } }) + { + var copyCreator = new MenuItem(); + copyCreator.Header = App.Text("TagCM.Copy.Tagger"); + copyCreator.Icon = App.CreateMenuIcon("Icons.User"); + copyCreator.Click += async (_, ev) => + { + await App.CopyTextAsync(tag.Creator.ToString()); + ev.Handled = true; + }; + copy.Items.Add(copyCreator); + } + + menu.Items.Add(copy); + menu.Open(listBox); + } + else if (selected.Count > 0) + { + var deleteMultiple = new MenuItem(); + deleteMultiple.Header = App.Text("TagCM.DeleteMultiple", selected.Count); + deleteMultiple.Icon = App.CreateMenuIcon("Icons.Clear"); + deleteMultiple.Click += (_, ev) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteMultipleTags(repo, selected)); + + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(deleteMultiple); + menu.Open(listBox); } e.Handled = true; @@ -202,15 +367,25 @@ private void OnItemContextRequested(object sender, ContextRequestedEventArgs e) private void OnSelectionChanged(object sender, SelectionChangedEventArgs _) { - var selected = (sender as ListBox)?.SelectedItem; - Models.Tag selectedTag = null; - if (selected is ViewModels.TagTreeNode node) - selectedTag = node.Tag; - else if (selected is Models.Tag tag) - selectedTag = tag; + if (sender is not ListBox listBox) + return; + + if (listBox.SelectedItems is { Count: 0 }) + { + if (Content is ViewModels.TagCollectionAsList list) + list.ClearSelection(); + else if (Content is ViewModels.TagCollectionAsTree tree) + tree.ClearSelection(); + } + else if (listBox.SelectedItems is { Count: > 0 }) + { + if (Content is ViewModels.TagCollectionAsList list) + list.UpdateSelection(listBox.SelectedItems); + else if (Content is ViewModels.TagCollectionAsTree tree) + tree.UpdateSelection(listBox.SelectedItems); - if (selectedTag != null) RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); + } } private void OnKeyDown(object sender, KeyEventArgs e) @@ -224,10 +399,24 @@ private void OnKeyDown(object sender, KeyEventArgs e) 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); + else if (selected is ViewModels.TagListItem { Tag: { } tagInItem }) + repo.DeleteTag(tagInItem); e.Handled = true; } + + private void CollectTagsInNode(ViewModels.TagTreeNode node, List outs) + { + if (node.Tag is { } tag) + { + if (!outs.Contains(tag)) + outs.Add(tag); + + return; + } + + foreach (var child in node.Children) + CollectTagsInNode(child, outs); + } } } diff --git a/src/Views/TextDiffView.axaml b/src/Views/TextDiffView.axaml index 45745d384..f301817eb 100644 --- a/src/Views/TextDiffView.axaml +++ b/src/Views/TextDiffView.axaml @@ -2,21 +2,19 @@ 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.TextDiffView" - x:Name="ThisControl" + x:DataType="vm:TextDiffContext" Background="{DynamicResource Brush.Contents}"> - + - - + + + SelectedChunk="{Binding SelectedChunk, Mode=TwoWay}" + BlockNavigation="{Binding BlockNavigation, Mode=OneWay}"/> @@ -47,9 +44,8 @@ + SelectedChunk="{Binding SelectedChunk, Mode=TwoWay}" + BlockNavigation="{Binding BlockNavigation, Mode=OneWay}"/> + SelectedChunk="{Binding SelectedChunk, Mode=TwoWay}" + BlockNavigation="{Binding BlockNavigation, Mode=OneWay}"/> @@ -103,7 +97,7 @@ - - - -